KOI AI 氣墊球
Last updated
Last updated
AI氣墊球機械人會自動追蹤圓球並防守龍門,結合了人工智能技術和玩樂元素。
學生可以透過與KOI人工智能競技,從遊戲中了解現今機器視覺的原理,激發對人工智能的興趣。
更高階的學生更可以透過Python編程編寫自己的AI氣墊球機械人。
Robotbit EDU連底座 *1
KOI AI鏡頭 *1
GeekServo 2KG灰色舵機 *2
積木件 *1套
氣墊球機 *1
KOI連接線 *1
1m USB線 *1
18650鋰電池 *1
USB移動式電源 或
USB 5V充電器
打開Robotbit電源後,KOI會追蹤並阻擋紅色球片。
出廠時KOI已經預載參考程式,如有需要請用KittenCode載入此py檔上載至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()