KOI AI Air Hockey

AI Air Hockey robot can follow the hockey puck and defend against human players, it is an education set combining AI Technology with Amusement Factor.

Students can understand how Computer Vision works by playing against the KOI AI algorithm, inspiring their interests in Artificial Intelligence.

More advanced students can also program their own KOI AI algorithm in Python.

Building Instructions

18MB
hockey.pdf
pdf

Wiring

Kit Contents

  1. Robotbit EDU with protective base shield *1

  2. KOI AI Camera *1

  3. GeekServo 2KG Servo *2

  4. Building Bricks Set *1

  5. Desktop Air Hockey *1

  6. KOI Connecting Cable *1

  7. 1m USB Cable *1

  8. 18650 Li-ion Battery *1

Extra Items Required

  • USB Portable Charger OR

  • USB Wall Plug 5V Chargers

Operation Instructions

After turning on the power for Robotbit, KOI will track and try to block the red hockey puck.

Demonstration Video

Sample Program

The sample program has already been loaded onto KOI, if needed to modify or reupload the program, load this python file on KittenCode and upload to KOI.

8KB
main.py
import sensor, image, time, lcd, utime
import math, ustruct
from maix import KPU, GPIO, I2S
from machine import UART, I2C

from fpioa_manager import fm

import gc

grid = [
    (56,74),(96,74),(137,72),(179,70),(220,68),(262,67),
    (52,113),(94,116),(136,112),(179,109),(222,107),(264,104),
    (51,157),(93,156),(139,154),(181,151),(227,150),(269,147),
]

PCA9685_ADDRESS    = 0x40
MODE1              = 0x00
MODE2              = 0x01
SUBADR1            = 0x02
SUBADR2            = 0x03
SUBADR3            = 0x04
PRESCALE           = 0xFE
LED0_ON_L          = 0x06
LED0_ON_H          = 0x07
LED0_OFF_L         = 0x08
LED0_OFF_H         = 0x09
ALL_LED_ON_L       = 0xFA
ALL_LED_ON_H       = 0xFB
ALL_LED_OFF_L      = 0xFC
ALL_LED_OFF_H      = 0xFD

S1 = 0x1
S2 = 0x2
S3 = 0x3
S4 = 0x4
S5 = 0x5
S6 = 0x6
S7 = 0x7
S8 = 0x8

RESTART            = 0x80
SLEEP              = 0x10
ALLCALL            = 0x01
INVRT              = 0x10
OUTDRV             = 0x04
RESET              = 0x00

positions = [
    [240, 195], [235, 175], [220, 155], [210, 135], [190, 125], [160, 115],
    [260, 190], [250, 165], [235, 145], [220, 125], [195, 105], [170, 95],
    [285, 190], [275, 170], [250, 140], [220, 105], [195, 85], [170, 70]
]


class RobotBit:

    def __init__(self):
        self.address = PCA9685_ADDRESS
        self.i2c = I2C(I2C.I2C0, freq=400000, scl=17, sda=14, addr_size=7)
        self.i2c.writeto(self.address, bytearray([MODE1, RESET])) # reset not sure if needed but other libraries do it
        self.i2c.writeto(self.address, bytearray([MODE1, RESET]))
        self.i2c.writeto(self.address, bytearray([MODE2, OUTDRV]))
        self.i2c.writeto(self.address, bytearray([MODE1, ALLCALL]))
        time.sleep_ms(5)
        mode1 = self.i2c.readfrom_mem(self.address, MODE1, 1)[0]
        mode1 = mode1 & ~SLEEP  # wake up (reset sleep)
        self.i2c.writeto(self.address, bytearray([MODE1, mode1]))
        time.sleep_ms(5)
        self.set_pwm_freq(50)
        self.inited = True
    
    def set_pwm_freq(self, freq_hz):
        """Set the PWM frequency to the provided value in hertz."""
        prescaleval = 25000000.0    # 25MHz
        prescaleval /= 4096.0       # 12-bit
        prescaleval /= float(freq_hz)
        prescaleval -= 1.0
        prescale = int(math.floor(prescaleval + 0.5))
        oldmode = self.i2c.readfrom_mem(self.address, MODE1, 1)[0]
        newmode = (oldmode & 0x7F) | 0x10    # sleep
        self.i2c.writeto(self.address, bytearray([MODE1, newmode]))
        self.i2c.writeto(self.address, bytearray([PRESCALE, prescale]))
        self.i2c.writeto(self.address, bytearray([MODE1, oldmode]))
        time.sleep_ms(5)
        self.i2c.writeto(self.address, bytearray([MODE1, oldmode | 0x80]))


    def set_pwm(self, channel, on, off):
        """Sets a single PWM channel."""
        if not self.inited:
            self.initRobotBit()
        if on is None or off is None:
            data = self.i2c.mem_read(4, self.address, LED0_ON_L+4*channel)
            return ustruct.unpack('<HH', data)
        self.i2c.writeto(self.address, bytearray([LED0_ON_L+4*channel, on & 0xFF]))
        self.i2c.writeto(self.address, bytearray([LED0_ON_H+4*channel, on >> 8]))
        self.i2c.writeto(self.address, bytearray([LED0_OFF_L+4*channel, off & 0xFF]))
        self.i2c.writeto(self.address, bytearray([LED0_OFF_H+4*channel, off >> 8]))

    def geekServo(self, index, degree):
        # 50hz: 25,000 us
        # 500~2650us->0~360
        # v_us = degree * 50 / 9 +500
        v_us = 200/36*degree + 500  # calibrated
        value = int(v_us*4096/20000)
        self.set_pwm(index+7, 0, value)


class AirHockey:

    def __init__(self):
        self.thresholdMap = {
            'red': [30,100,15,127,15,127],
            'blue': [0,50,-64,64,-127,-20],
            'red2': [29,98,-9,57,0,47]
        }
        sensor.reset()
        sensor.set_pixformat(sensor.RGB565)
        sensor.set_framesize(sensor.QVGA)
        lcd.init()
        lcd.rotation(0)
        lcd.clear()
        sensor.set_vflip(True)
        sensor.run(1)
        sensor.skip_frames(time = 2000)
        self.displayGrid()
        self.cachedPosition = []
        self.t0 = time.ticks_ms()
        self.lastIndex = -1
        self.robo = RobotBit()

    def displayGrid(self):
        self.img = sensor.snapshot()
        for i in range(len(grid)):
            self.img.draw_cross(grid[i][0],grid[i][1],5,color=(0,255,0))
        lcd.display(self.img)

    def colorCalibrate(self, key, r=[(320//2)-(50//2), (240//2)-(50//2), 50, 50]):
        for i in range(60):
            self.img = sensor.snapshot()
            self.img.draw_string(40, 0, "put the color\nin the rect", scale=2,color=(0,255,0))
            self.img.draw_rectangle(r)
            lcd.display(self.img)

        threshold = [50, 50, 0, 0, 0, 0] # Middle L, A, B values.
        for i in range(60):
            self.img = sensor.snapshot()
            self.img.draw_string(40, 0, "be learning...", scale=2,color=(0,255,0))
            hist = self.img.get_histogram(roi=r)
            lo = hist.get_percentile(0.01) # Get the CDF of the histogram at the 1% range (ADJUST AS NECESSARY)!
            hi = hist.get_percentile(0.99) # Get the CDF of the histogram at the 99% range (ADJUST AS NECESSARY)!
            # Average in percentile values.
            threshold[0] = (threshold[0] + lo.l_value()) // 2
            threshold[1] = (threshold[1] + hi.l_value()) // 2
            threshold[2] = (threshold[2] + lo.a_value()) // 2
            threshold[3] = (threshold[3] + hi.a_value()) // 2
            threshold[4] = (threshold[4] + lo.b_value()) // 2
            threshold[5] = (threshold[5] + hi.b_value()) // 2
            for blob in self.img.find_blobs([threshold], pixels_threshold=100, area_threshold=100, merge=True):
                self.img.draw_rectangle(blob.rect())
                self.img.draw_cross(blob.cx(), blob.cy())
                self.img.draw_rectangle(r)
            lcd.display(self.img)
        self.thresholdMap[key] = threshold
        print(threshold)
    

    def getPosition(self, x, y):
        # calculate the closest grid index
        minDist = 100000
        minIndex = -1
        for i in range(len(grid)):
            dist = (grid[i][0]-x)**2 + (grid[i][1]-y)**2
            if dist < minDist:
                minDist = dist
                minIndex = i
        return minIndex
    

    def setPositon(self, index):
        pos = positions[index]
        self.robo.geekServo(S8, pos[1])
        self.robo.geekServo(S7, pos[0])
        
    def colorTrack(self,key='red'):
        cx=-1
        cy=-1
        maxArea = 0
        #([self.thresholdMap[key]], pixels_threshold=100, area_threshold=100, merge=True, margin=10)
        for blob in self.img.find_blobs([self.thresholdMap[key]], pixels_threshold=200, area_threshold=200, merge=True, margin=10):
            self.img.draw_rectangle(blob.rect())
            self.img.draw_cross(blob.cx(), blob.cy())
            cx = blob.cx()
            cy = blob.cy()
            if blob.rect()[2] * blob.rect()[3] > maxArea:
                maxArea = blob.rect()[2] * blob.rect()[3]
        return (cx, cy)

    def tick(self):
        self.img = sensor.snapshot()
        (rx, ry) = self.colorTrack('red')
        if rx == -1:
            self.cachedPosition = []
            lcd.display(self.img)
            self.setPositon(8)
            return
        index = self.getPosition(rx, ry)
        self.img.draw_cross(grid[index][0],grid[index][1],5,color=(0,255,0))
        print(index)
        # print history lines
        for i in range(len(self.cachedPosition)):
            self.img.draw_cross(self.cachedPosition[i][0], self.cachedPosition[i][1],color=(0,100,200))
        lcd.display(self.img)
        self.setPositon(index)
        self.cachedPosition.append((rx,ry))
        if len(self.cachedPosition) > 10:
            self.cachedPosition.pop(0)

    def run(self):
        while True:
            self.tick()
            time.sleep_ms(50)
            gc.collect()


ak = AirHockey()
#ak.colorCalibrate(key='red2')
ak.run()

Servo Calibration

Last updated