三目並べロボット
動画チュートリアル:星瞳科技(SingTown)OpenMV動画チュートリアル - 三目並べロボットアーム 2024電子設計コンテスト
三目並べロボットは、大きく3つのステップに分かれています。1、画像認識でコマの配列を取得する。2、対局戦略アルゴリズム。3、ロボットアームによるコマの取得と着手の制御。
ハードウェアには星瞳科技(SingTown)のOpenMV4 H7を使用しており、すべてのコードはOpenMV上で実行されます!ロボットアームの制御、リレー制御、対局戦略アルゴリズム、画像認識アルゴリズムをすべて含みます。
ロボットアームは3Dプリントで直接製作したもので、3個のサーボモーターを使用しており、OpenMVのサーボ拡張ボードで直接制御できます。
コマの吸着には電磁石を利用しています。手元にあった10個のボタン電池をそのままコマとして流用したためです。OpenMVはリレーを介して電磁石を制御できます。
1、画像認識は非常にシンプルです。まずグレースケール画像を取得し、9つの盤面領域それぞれで色の統計情報を取得します。OpenMVではget_statisticsを使って実現しています。グレースケール情報から、黒番か、白番か、あるいは空きマスかを判定できます。 2、次の一手をどこに打つかの計算については、良き師であるChatGPT先生にminimaxアルゴリズムを教えてもらい、このアルゴリズム部分のコードも書いてもらいました。 3、ロボットアームの制御も比較的シンプルです。コマの取得待機エリアと、3×3の盤面の各着手エリアそれぞれについて、あらかじめロボットアームの位置を取得しておきます。通常はティーチング機能で行いますが、私のロボットアームは1日で急いで製作したため、コードで直接座標を確認しながら位置を決めています。
# robot.py
import time
from servo import Servos
from machine import SoftI2C, Pin
import math
# リレーを制御する。リレーが電磁石を制御する
pin1 = Pin('P1', Pin.OUT_PP, Pin.PULL_NONE)
pin1.value(0)
# PCA9685 サーボ拡張ボード
i2c = SoftI2C(sda=Pin('P5'), scl=Pin('P4'))
servo = Servos(i2c, address=0x40, freq=50, min_us=650, max_us=2800, degrees=180)
# 3つのサーボモーターの初期位置
servo.position(0, 0)
servo.position(1, 90)
servo.position(2, 90)
# ゆっくり移動させるためのグローバル変数
servo_positions = [0,90,90]
# 1つのサーボモーターの移動を制御する
def move(index, angle):
servo.position(index, angle)
servo_positions[index] = angle
# 3つのサーボモーターの移動を制御する
def move_list(angle_list):
print(angle_list)
move(0, int(angle_list[0]))
move(1, int(angle_list[1]))
move(2, int(angle_list[2]))
# 3つのサーボモーターをゆっくり移動させる
def slow_move_to(angle_list):
init_positions = servo_positions.copy()
d0 = angle_list[0] - init_positions[0]
d1 = angle_list[1] - init_positions[1]
d2 = angle_list[2] - init_positions[2]
dm = int(max(abs(d0), abs(d1), abs(d2)))
if dm == 0:
return
for i in range(dm+1):
move_list([init_positions[0]+i*d0/dm,
init_positions[1]+i*d1/dm,
init_positions[2]+i*d2/dm])
time.sleep_ms(40)
# コマの取得エリアの位置を設定する
PICK = [[74,128,19], [82,128,17], [90,125,14], [98,128,17], [106,128,19]]
# 取得エリアでロボットアームを持ち上げた位置
HIGH_PICK = [90,95,55]
# 盤面の設置位置
BOARD = [
[[82,150,55], [82,140,40], [81,133,30]],
[[90,150,55], [90,140,40], [90,133,30]],
[[98,150,55], [98,140,40], [99,133,30]]
]
# 盤面の着手位置の上方の位置
HIGH_BOARD = [90,120,70]
# コマを取得し、x, y の位置に置く
def pick_and_place(x,y):
slow_move_to(HIGH_PICK)
time.sleep_ms(500)
slow_move_to(PICK[2])
time.sleep_ms(500)
slow_move_to(HIGH_PICK)
time.sleep_ms(500)
slow_move_to(HIGH_BOARD)
time.sleep_ms(500)
slow_move_to(BOARD[y][x])
time.sleep_ms(500)
pin1.value(1) # リレーをオンにする
time.sleep_ms(500)
slow_move_to(HIGH_PICK)
pin1.value(0) # リレーをオフにする
time.sleep_ms(500)
slow_move_to([0,90,90])
if __name__ == "__main__":
# キャリブレーションテスト用
time.sleep_ms(1)
for order in [
BOARD[1][1], BOARD[0][0], BOARD[1][0],
BOARD[2][0], BOARD[2][1], BOARD[2][2],
BOARD[1][2], BOARD[0][2], BOARD[0][1],
]:
slow_move_to(HIGH_BOARD)
time.sleep_ms(500)
slow_move_to(order)
time.sleep_ms(500)
#slow_move_to(BOARD[0][2])
#for x in PICK:
#slow_move_to(HIGH_PICK)
#time.sleep_ms(500)
#slow_move_to(x)
#time.sleep_ms(500)
slow_move_to([0,90,90])
# chess.py
SIZE = 3
# 勝利したか確認する
def check_win(board, player):
# 行と列を確認する
for i in range(SIZE):
if all(board[i][j] == player for j in range(SIZE)) or \
all(board[j][i] == player for j in range(SIZE)):
return True
# 対角線を確認する
if all(board[i][i] == player for i in range(SIZE)) or \
all(board[i][SIZE - 1 - i] == player for i in range(SIZE)):
return True
return False
# 引き分けかどうか確認する
def check_draw(board):
return all(board[i][j] != ' ' for i in range(SIZE) for j in range(SIZE))
# 戦略スコアを計算する
def minimax(board, depth, is_maximizing):
computer = 'X'
player = 'O'
if check_win(board, computer):
return 10 - depth
if check_win(board, player):
return depth - 10
if check_draw(board):
return 0
if is_maximizing:
best_score = float('-inf')
for i in range(SIZE):
for j in range(SIZE):
if board[i][j] == ' ':
board[i][j] = computer
score = minimax(board, depth + 1, False)
board[i][j] = ' '
best_score = max(score, best_score)
return best_score
else:
best_score = float('inf')
for i in range(SIZE):
for j in range(SIZE):
if board[i][j] == ' ':
board[i][j] = player
score = minimax(board, depth + 1, True)
board[i][j] = ' '
best_score = min(score, best_score)
return best_score
# 次の一手の位置を計算する
def computer_move(board):
if board == [
[" "," "," "],
[" "," "," "],
[" "," "," "]
]:
return 1,1
best_score = float('-inf')
move = (-1, -1)
for i in range(SIZE):
for j in range(SIZE):
if board[i][j] == ' ':
board[i][j] = 'X'
score = minimax(board, 0, False)
board[i][j] = ' '
if score > best_score:
best_score = score
move = (i, j)
if move != (-1, -1):
# board[move[0]][move[1]] = 'X'
print(f"Computer places X at ({move[0]}, {move[1]})")
return move[0], move[1]
# どちらの手番か確認する
def check_turn(board):
x_count = sum(row.count("X") for row in board)
o_count = sum(row.count("O") for row in board)
return "X" if x_count == o_count else "O"
# main.py
import csi, image, time
csi0 = csi.CSI()
from pyb import Pin
import robot
import chess
csi0.reset()
csi0.pixformat(csi.GRAYSCALE)
csi0.framesize(csi.QVGA)
csi0.snapshot(time = 2000)
clock = time.clock()
# タクトスイッチ
pin0 = Pin('P0', Pin.IN, Pin.PULL_UP)
distance = 43
block = 10
# 3×3盤面の各エリアの位置を生成する
def generate_centered_rois(width, height, b, k):
rois = []
# 各ROIの中心位置のオフセットを計算する
offset = (b - k) // 2
# 3×3全体の幅と高さを計算する
total_width = 3 * b
total_height = 3 * b
# マトリックスを中央に配置するための左上の開始点を計算する
start_x = (width - total_width) // 2
start_y = (height - total_height) // 2
for i in range(3):
row = []
for j in range(3):
x_center = start_x + j * b + b // 2
y_center = start_y + i * b + b // 2
x = x_center - k // 2
y = y_center - k // 2
row.append((x, y, k, k))
rois.append(row)
return rois
# 3×3盤面の各エリアの位置
rois = generate_centered_rois(csi0.width(), csi0.height(), distance, block)
# 盤面の配列
# 黒番:X
# 白番:O
# コマなし:空文字
board = [
[" "," "," "],
[" "," "," "],
[" "," "," "],
]
# スイッチが押されて離されるのを待つ
def wait_key():
while pin0.value():
img = csi0.snapshot().lens_corr(1.8)
for y in range(len(rois)):
for x in range(len(rois[y])):
img.draw_rectangle(rois[y][x])
while not pin0.value():
time.sleep_ms(1)
while(True):
clock.tick()
wait_key()
img = csi0.snapshot().lens_corr(1.8)
# 画像認識で盤面配列を取得する
for y in range(len(rois)):
for x in range(len(rois[y])):
gray = img.get_statistics(roi=rois[y][x]).mean
if gray < 100:
board[y][x] = "X"
elif gray > 200:
board[y][x] = "O"
else:
board[y][x] = " "
# 現在の盤面配列を表示する
for line in board:
print(line)
print()
# 盤面配列を描画する
for y in range(len(rois)):
for x in range(len(rois[y])):
if board[y][x] == "X":
color = 255
elif board[y][x] == "O":
color = 0
elif board[y][x] == " ":
color = 127
img.draw_rectangle(rois[y][x], color=color)
# 対局戦略
if chess.check_win(board, 'O'):
print("你赢啦!")
elif chess.check_win(board, 'X'):
print("我赢啦!")
elif chess.check_draw(board):
print("平局啦!")
elif chess.check_turn(board) == "X":
# 次のコマをどこに置くか計算する
line,row = chess.computer_move(board)
# 対象の盤面に十字を描く
img.draw_cross((int(rois[line][row][0]+block/2), int(rois[line][row][1]+block/2)), size=block, color=0)
csi0.flush()
# ロボットがコマを取得して置く
robot.pick_and_place(row, line)
csi0.flush()
elif chess.check_turn(board) == "O":
print("该你下了!")