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
Wiring
Kit Contents
Robotbit EDU with protective base shield *1
KOI AI Camera *1
GeekServo 2KG Servo *2
Building Bricks Set *1
Desktop Air Hockey *1
KOI Connecting Cable *1
1m USB Cable *1
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.
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