简介:智能人机对弈五子棋机器人设计相关学术论文PDF,内容基于国家自然科学基金项目,面向机器人、嵌入式及AI方向的学习者,提供一套低成本、软硬件一体化的五子棋人机对战实现方案。资源仅含1个PDF文件,压缩包大小2.55MB,包含论文全文、图表与核心公式。已有193人学习,适合作为毕业设计、机器人竞赛或嵌入式课程的参考资料。论文详述了采用普通摄像头采集棋盘图像,通过灰度变换、Canny边缘检测、霍夫直线检测和模板匹配实现棋子识别;利用决策树算法预测最优落子位置;并借助STM32主控系统控制数字舵机完成落子动作。硬件上选用廉价数字舵机替代工业机械臂,有效降低成本,同时保证了落子准确度。文中还对比了山东海大机器人公司的同类产品,突出了方案的经济性和普适性,对智能游戏机器人开发具有直接借鉴意义。
1. 智能人机对弈五子棋机器人:先把坐标对齐,再谈AI
展厅里放一台五子棋机器人,观众下白子,机械臂从棋盒吸起黑子放到棋盘。多数人以为核心是五子棋AI,实际上AI用一套评估函数就能打赢普通路人,真正耗时间的是两件事:摄像头像素坐标怎么换算成机械臂基座坐标,以及吸盘怎么把棋子放进格子不碰倒旁边的子。整台设备涉及视觉识别、博弈搜索、机器人运动学三块硬骨头,对做嵌入式、算法、控制方向的人来说都是很好的练手项目。本方案按常规桌面级实现来讲:OpenCV感知,alpha-beta决策,四轴或六轴机械臂加吸盘执行。适合课程设计、毕业设计或想给自己配一台实机对弈设备的工程师。这条路线没有黑科技,但调通每一层接口需要很细的耐心。
2. 从相机像素到机器人基座:五子棋机器人的坐标链路标定
机器人要落子,最少要过三套坐标系:相机的像素坐标系、棋盘的格子坐标系、机械臂的基座坐标系。多数人一开始只盯着AI算法,结果视觉识别做完了,机械臂却把子放到格子外面,问题全出在这条坐标链路上。
2.1 感知、决策、执行三层的分工
感知层由固定相机完成,拍棋盘后检测棋盘角点和棋子位置;决策层维护一个15×15的局面数组,用搜索算法算出下一步应该落在哪个交叉点;执行层负责把格子坐标换算成机械臂末端的目标位姿,再驱动吸盘取子、落子。桌面级五子棋机器人一般把相机装在棋盘正上方拍全局图,这样棋盘平面近似水平,像素坐标到格子坐标就是一个平面透视变换,标定最简单。如果非要装在机械臂末端,就得做完整的手眼标定,难度上了一个台阶,对这种固定场景没必要。
2.2 用四个点建立像素到格子坐标的单应矩阵
有同学直接按像素比例换算坐标,棋盘摆正时勉强能用,但只要相机有一点点安装倾角,棋盘边缘就会偏好几个像素。五子棋每格常见尺寸约15毫米,机械臂末端偏3毫米,棋子放下去肉眼看就是压线。常见的稳定做法是用单应矩阵做平面射影变换,用棋盘四个角点的像素坐标和对应的格子坐标,求一个3×3矩阵。OpenCV里一行就能算出来:
import cv2 import numpy as np # 四个角点在画面中的像素位置 pts_src = np.array([ [112, 84], [608, 96], [596, 572], [104, 560] ], dtype=np.float32) # 对应的格子坐标,按左上、右上、右下、左下排列 pts_dst = np.array([ [0, 0], [14, 0], [14, 14], [0, 14] ], dtype=np.float32) H = cv2.getPerspectiveTransform(pts_src, pts_dst) def pixel_to_grid(u, v): # 齐次坐标变换后除以 w 分量 out = H @ np.array([u, v, 1.0]) return out[0] / out[2], out[1] / out[2]单应矩阵有8个自由度,4对点就能解,但pts_dst的顺序必须和pts_src一一对应,顺序错一个角点结果就完全不对。标定完要立刻验证,把已知格子的像素坐标代入,看换算出来的格子坐标是否和真实值一致。如果镜头畸变比较大,先用相机内参做一次undistort再算单应矩阵,边缘位置的误差会明显降下来。
2.3 机器人与棋盘的坐标对齐:三点示教算仿射变换
格子坐标到机器人基座坐标,常见做法是假设机械臂X、Y轴与棋盘边缘平行,只加一个平移量。这个假设在真机上容易翻车,安装时偏一两度,棋盘远端就会偏出好几毫米。更可靠的做法是在棋盘上示教三个点,求一个6参数的平面仿射变换,旋转、缩放、平移一次算齐:
import numpy as np def build_grid_to_robot(grid_pts, robot_pts): # grid_pts: [[0,0], [14,0], [0,14]] # robot_pts: 三个点对应的机械臂基座坐标 A = np.zeros((6, 6)) b = np.zeros(6) for i, (gx, gy) in enumerate(grid_pts): rx, ry = robot_pts[i] A[2*i] = [gx, gy, 1, 0, 0, 0] A[2*i+1] = [0, 0, 0, gx, gy, 1] b[2*i] = rx b[2*i+1] = ry m = np.linalg.solve(A, b) return m def grid_to_robot(gx, gy, m): # 仿射变换应用 return m[0]*gx + m[1]*gy + m[2], m[3]*gx + m[4]*gy + m[5]示教点选择格子(0,0)、(14,0)、(0,14),三点不能共线。操作方法是装好吸盘后,用手动模式把吸嘴分别移到这三个交叉点正上方,记录控制器的X、Y读数。最后再用角点(14,14)做验证,对比计算坐标和实际示教坐标,差值在0.5毫米内才算通过。这个套路和工业机器人做视觉引导抓取是同一套逻辑,ABB、FANUC的控制器里帮你封装好了工具坐标和TCP校准,桌面级舵机机械臂就自己写这几行矩阵运算。
2.4 坐标链路自检流程
| 坐标系 | 代表变量 | 标定手段 | 常见误差 |
|---|---|---|---|
| 像素坐标系 | u, v | 相机内参加角点检测 | ±0.5像素 |
| 格子坐标系 | gx, gy | 单应矩阵 | ±0.05格 |
| 机器人坐标系 | Rx, Ry | 三点示教仿射 | ±0.3毫米 |
链路全通的验证方法:把一颗棋子放到任意交叉点,视觉算出格子坐标,机器人按算出的坐标去吸。如果发现吸嘴中心偏离棋子中心,方向固定、距离固定,先不要调视觉,去检查仿射矩阵有没有算错。如果四个角都正确、中间区域偏,优先怀疑角点检测本身歪了,而不是机器人误差。
3. 用OpenCV做棋盘、棋子和落点的实时识别
视觉部分常见的坑是一上来就用霍夫圆检测整帧找棋子,结果反光、手指入画、木头纹理全变成误检。稳定的做法是分两层走:先粗定位棋盘,再在棋盘区域内检测落子和棋色。
3.1 为什么先做棋盘粗定位
棋盘是场景里最稳定的结构,横竖线条固定,纹理几乎不变。如果相机安装位置固定,可以在安装完成后手动标定一次棋盘四个角点的像素坐标,之后每帧只裁剪棋盘区域做处理,既减少计算量,也排除大量背景干扰。用findChessboardCorners检测标准棋盘格标定板当然可以,但五子棋棋盘不是标定板,15路交叉点对应14×14格子,内角点数量对不上,反而容易出错。我更倾向于开机后手动点四个角点完成射影矫正,一次标定永久使用,稳且快。
3.2 用帧间差分定位新落子
玩家落子的一瞬间,棋盘画面在局部区域发生明显变化。用上一帧减当前帧,变化区域就是刚落下的子。这个方案对单步轮流落子非常合适,比每次都全盘识别所有棋子快一个数量级:
import cv2 gray_prev = None def detect_new_stone(gray_cur): global gray_prev if gray_prev is None: gray_prev = gray_cur.copy() return None diff = cv2.absdiff(gray_prev, gray_cur) diff = cv2.GaussianBlur(diff, (5, 5), 0) _, thresh = cv2.threshold(diff, 45, 255, cv2.THRESH_BINARY) contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: gray_prev = gray_cur.copy() return None c = max(contours, key=cv2.contourArea) if cv2.contourArea(c) < 300: # 小面积噪声直接忽略 gray_prev = gray_cur.copy() return None x, y, w, h = cv2.boundingRect(c) u, v = x + w // 2, y + h // 2 gray_prev = gray_cur.copy() return u, vthreshold取45是经验值,环境光稳定时可以拍三张空棋盘算像素级标准差,再取三倍标准差作为阈值。GaussianBlur的核大小影响相邻落子的区分度,核太大会把两个离得近的新落子黏成一个区域,太小则噪声点多。area阈值300像素,在1080p画面里大约对应6×6像素的噪点,过滤手指投影和灰尘足够。检测到落子后调用2.2节的pixel_to_grid换算格子坐标,换算结果可能是(3.2, 7.8)这种小数,不要直接四舍五入,应该比较这个点到四个邻近交叉点的距离,取最近的一个,否则落在棋盘线中间时就容易选错格。
3.3 用HSV区分黑白棋,处理反光
棋盘画面上最烦人的是反光,白棋有高光点,黑棋也会泛灰。RGB空间里黑色和白色在亮度通道上分布重叠,不如直接用HSV分。取落子区域中心5×5像素的HSV均值,按S和V阈值分类:
| 棋子类型 | H | S | V |
|---|---|---|---|
| 白棋 | 不限 | < 70 | > 140 |
| 黑棋 | 不限 | < 120 | < 70 |
黑白都是无彩色,H通道本身没有区分意义,所以H列写不限。强反光时白棋V值会过曝,黑棋泛灰V值可能到90以上,兜底逻辑是:若S很低且V大于120,优先判白棋。棋色分类错误会导致局面数组和真实棋盘不一致,开局阶段不容易暴露,中盘就会出现双方争同一个位置的诡异局面,且这种问题很难从日志里一眼看出,所以这一层宁可判错重拍一次,也不要急于落子。
4. 五子棋AI:评估函数与alpha-beta剪枝为主,深度学习不是必须
机器人对弈体验的核心指标是响应速度。真人每步思考十几秒很正常,机器如果也要算5秒就失去意义。15×15棋盘有225个可落点,搜索树很深,但五子棋本身规则简单,用评估函数加alpha-beta剪枝,在单核上跑4层深度轻松做到秒级响应。
4.1 为什么评估函数加博弈搜索仍然是主流选择
深度学习开局需要训练数据和推理硬件,还要处理延迟和不确定性,而五子棋在固定棋盘上的规则完全可枚举。评估函数加搜索是稳定、可解释的方案,出问题时能定位到“是对手子的棋型统计错了”还是“搜索深度不够”。实际对局中,评估函数对棋型的理解程度比搜索深度更影响棋力,所以这一节把重点放在棋型评估上。
4.2 棋型表驱动的评估函数
评估函数最先要分清活三和冲三。活三两端都没被堵,不应的话下一步就是活四;冲三有一端被堵,威胁小一个量级。实现时从每个已落棋子位置往四个方向延伸,每个方向取前后各4格组成长度9的序列,再用模式串匹配统计棋型。长度取9是因为一个棋型能影响的完整范围最多覆盖9格:
PATTERNS = { "AAAAA": 1000000, # 五连 ".AAAA.": 100000, # 活四 "AAAA.": 10000, # 冲四,堵一侧 ".AAAA": 10000, ".AAA..": 6000, # 活三,两空位 "..AAA.": 6000, ".AAA.": 3000, # 活三,空间窄 ".AA..": 300, # 活二 "..AA.": 300, ".A.A.": 300, # 隔空活二 } def evaluate_single(board, x, y, player): score = 0 for dx, dy in ((1, 0), (0, 1), (1, 1), (1, -1)): line = [] for step in range(-4, 5): nx, ny = x + dx * step, y + dy * step if 0 <= nx < 15 and 0 <= ny < 15: line.append(board[nx][ny]) else: line.append(-1) # 越界视作墙 s = "".join( "A" if c == player else ("." if c == 0 else "W") for c in line ) for pattern, base in PATTERNS.items(): score += s.count(pattern) * base return score def evaluate_player(board, player): total = 0 for x in range(15): for y in range(15): if board[x][y] == player: total += evaluate_single(board, x, y, player) return total def evaluate(board, player): # 返回 player 视角的评分 return evaluate_player(board, player) - evaluate_player(board, 1 - player)这里把对手棋子和棋盘边界统一视为“W”,因为它们在阻挡效果上语义一致。用s.count统计模式会有重叠计分,但双方都重叠,相对差值仍然可用。每个棋子会在多个滑动窗口里被重复计入分数,绝对数值没有太多含义,主要用于候选点排序和搜索树的局面比较,够用就行。
4.3 alpha-beta剪枝核心实现
import math EMPTY = 0 AI = 1 HUMAN = 2 def alphabeta(board, depth, alpha, beta, maximizing): score = evaluate(board, AI) if depth == 0 or score >= PATTERNS["AAAAA"] or score <= -PATTERNS["AAAAA"]: return score player = AI if maximizing else HUMAN moves = candidates(board, player) if maximizing: value = -math.inf for x, y in moves: board[x][y] = AI value = max(value, alphabeta(board, depth - 1, alpha, beta, False)) board[x][y] = EMPTY # 悔子,必须和落子成对出现 alpha = max(alpha, value) if alpha >= beta: break return value else: value = math.inf for x, y in moves: board[x][y] = HUMAN value = min(value, alphabeta(board, depth - 1, alpha, beta, True)) board[x][y] = EMPTY beta = min(beta, value) if beta <= alpha: break return value递归里落子和悔子必须成对,漏掉悔子,棋盘上会积累大量幻影棋子,之后所有评估全部失真。score直接使用AI视角的差值,所以max节点和min节点的终止条件可以共用。maximizing节点更新alpha,minimizing节点更新beta,两者不能混写。单看这段代码只能跑4层,想加深就得靠下一节的候选点压缩和排序。
4.4 候选点范围与走子顺序:剪枝效率的真正来源
alpha-beta剪枝的效率高度依赖走子顺序。如果候选点从全盘225个位置里选,搜索树还是太大。常见做法是只搜已有棋子周围两格以内的空位,一手棋如果离所有棋子超过两格,在五子棋战术中几乎不可能产生有效威胁。这样候选点从200多个压到20个上下。生成后还要对候选点做静态估值排序,优先搜索看起来最强的着法,剪枝率会明显提升:
def candidates(board, for_player): around = set() for x in range(15): for y in range(15): if board[x][y] == EMPTY: continue for dx in (-2, -1, 0, 1, 2): for dy in (-2, -1, 0, 1, 2): nx, ny = x + dx, y + dy if 0 <= nx < 15 and 0 <= ny < 15 and board[nx][ny] == EMPTY: around.add((nx, ny)) def quick_score(p): # 假设该点落子,评估其棋型价值 x, y = p board[x][y] = for_player s = evaluate_single(board, x, y, for_player) board[x][y] = EMPTY return s return sorted(around, key=quick_score, reverse=True)quick_score临时落子再悔子,只统计以该点为中心的四个方向的棋型价值,作为排序依据足够。深度6配合排序后,单步响应可以控制在2秒内;如果还超时,就用第6章的迭代加深做时间预算。这个策略本质上就是启发式搜索,五子棋工程里它比换一颗更快的CPU作用大得多。
5. 机械臂运动控制:吸盘末端与落子精度补偿
执行层是机器人和纯软件方案的最大区别。桌面级五子棋机器人最常见的末端执行器是真空吸盘,不是夹爪。吸盘从正上方接触棋子,姿态容差大,占用空间小,也不会像夹爪那样两侧空间不够碰到邻子。
5.1 末端执行器选型:吸盘而不是夹爪
五子棋棋子直径一般两厘米左右,重约3克,用直径8毫米的硅胶吸嘴、微型真空泵加电磁阀就能稳定吸取。选吸盘的理由有三个:夹爪需要两侧避让相邻棋子,末端空间经常不够;磁吸方案只适用于铁棋子,还会干扰视觉对棋色的判断;吸盘对姿态偏差容忍度高,歪一两度也能吸起来。真机上最容易出问题的是吸嘴内壁沾灰,吸力会明显下降,视觉看起来吸住了,一移动就掉,所以定期清洁吸嘴和棋子表面是日常维护项。
5.2 取子到落子的动作序列
一整轮动作拆成五段:从棋盒上方移动到目标棋子上方,垂直下降接触棋子,等待真空建立,抬升到安全高度,再水平移动到目标格子上方,垂直下降放子,破真空,抬升。安全高度至少要比棋子表面高2厘米,保证水平移动时不刮到棋面。真空建立延时我一般给100毫秒,太短电磁阀还没吸稳就抬升,棋子会留在原处,这类故障在实机上最常出现:
def pick_and_place(target_grid, source_grid): arm = get_robot_arm() # 机械臂控制句柄 src = grid_to_robot(*source_grid) arm.move_lin(src) # 直线移动到棋子正上方 arm.move_lin(src_z_pick) # 垂直下降接触棋子 vacuum_on() sleep(0.12) # 等真空建立 arm.move_lin(safe_height) # 抬升 dst = grid_to_robot(*target_grid) arm.move_lin(dst) arm.move_lin(dst_z_drop) # 垂直下降 vacuum_off() sleep(0.05) # 放气后稍等再抬升 arm.move_lin(safe_height)move_lin是直线插补,move_j是关节插补。落子阶段要用直线插补,关节插补会让吸嘴划弧线,经过相邻格子上方时如果高度不够就会碰倒已经落好的棋子。工业机械臂控制器里自带直线插补指令,桌面级机械臂的SDK一般也提供相同接口。ROSL2里用MoveIt规划时,默认是在关节空间搜索路径,需要额外设置笛卡尔路径约束,或者干脆手写分层直线插补。
5.3 落子位置偏差补偿与Z轴下降细节
即使坐标标定全部正确,机械臂的机械间隙也会导致实际落点和目标点之间有固定偏移。如果发现偏差方向固定、大小固定,可以做一个偏差表:在棋盘四角和中心分别示教一次,记录实际落点和目标点的偏差(dx, dy),再用双线性插值估算任意格点的补偿量:
def bias_compensate(gx, gy, bias_grid): # bias_grid: {(0,0): (dx,dy), (14,0): ..., (14,14): ..., (0,14): ..., (7,7): ...} x0, y0 = 0, 0 x1, y1 = 14, 14 dx00, dy00 = bias_grid[(0, 0)] dx01, dy01 = bias_grid[(0, 14)] dx10, dy10 = bias_grid[(14, 0)] dx11, dy11 = bias_grid[(14, 14)] tx = (gx - x0) / (x1 - x0) ty = (gy - y0) / (y1 - y0) dx = (1 - tx) * (1 - ty) * dx00 + tx * (1 - ty) * dx10 + (1 - tx) * ty * dx01 + tx * ty * dx11 dy = (1 - tx) * (1 - ty) * dy00 + tx * (1 - ty) * dy10 + (1 - tx) * ty * dy01 + tx * ty * dy11 return dx, dy参数上,(7,7)是可选的第5个标定点,加上它之后棋盘中心的偏差不会完全依赖四角线性外推。如果偏差超过1厘米,不要无脑补偿,优先检查机械臂基座螺丝和连杆是否有松动,很多“标定不准”实际上是机械结构松动。最后是落子深度的细节:吸嘴下降终点要穿过棋子上表面约0.5毫米,靠吸盘硅胶的弹性把棋子压稳,否则放气瞬间残余吸力会把棋子带起来。这个量根据硅胶硬度微调,一般在0.3到1毫米之间。
6. 从仿真到实机的验证方法与落子超时排错
整台设备联调时最怕三个模块的问题混在一起。我的习惯是分阶段验证,每一层独立跑通后再对接下一层。
6.1 先验证AI:命令行自对弈
把视觉和机械臂全部断开,用Python实现一个命令行五子棋程序,让AI自己执黑执白对弈。这种基于Python的五子棋游戏实现最少只需要一个局面数组和一个终端渲染函数,却能快速暴露评估函数和搜索逻辑的问题。
python gomoku.py --self-play 200 --max-time 2.0观察两个指标:全程无非法落子,平均单步耗时稳定。再用几个残局测试用例验证棋型判断,比开局摆一个活三,看AI应手是否落在堵活三的位置上。
6.2 离线录像验证视觉坐标
视觉调试不要对着实时画面猜。录一段包含空棋盘到逐步落子的视频,离线跑检测脚本,把每步识别出的格子坐标输出成CSV,再人工对照实际落子位置。改参数后重跑同一段视频,才能对比出参数变化对识别结果的影响。如果视觉坐标稳定但偏差固定,优先检查单应矩阵的角点是否标对;如果连续几帧坐标抖动,是差分阈值临界导致的,把threshold调高一点再看。
6.3 搜索超时的迭代加深保底
alpha-beta搜索到第6层时,单步耗时可能因为局面复杂度波动。给搜索加时间预算的常见做法是迭代加深:从浅层开始逐层加深,每层完成后检查剩余时间,超时就沿用上一层的落子结果:
def alphabeta_root(board, depth): best_move = (7, 7) best_val = -math.inf for x, y in candidates(board, AI): board[x][y] = AI val = alphabeta(board, depth - 1, -math.inf, math.inf, False) board[x][y] = EMPTY if val > best_val: best_val, best_move = val, (x, y) return best_move def iterative_deepening(board, max_seconds=2.0): best = (7, 7) start = time.perf_counter() for depth in range(2, 10): if time.perf_counter() - start > max_seconds * 0.7: break best = alphabeta_root(board, depth) return best0.7是经验值,保留三成时间给机械臂动作和通信开销。迭代加深不会明显削弱棋力,因为浅层搜索给出的着法在大多数局面下已经够好,深层搜索的价值主要在中盘攻防中体现。实机联调时把视觉、AI、机械臂三个模块的日志时间戳对齐,出问题先确认是哪一层超时,不要一上来就调机械臂速度。
本文还有配套的精品资源,点击获取