Skip to content

YizhePKU/Raspberry-Chess

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Raspberry Chess 项目报告

项目介绍

Raspberry Chess是基于树莓派平台的自动下棋机器人。 整个项目有大约600行Python代码,利用机器视觉和开源象棋引擎,控制机械臂,从而完成与用户对弈的功能。

项目主要分为三个模块:

  • 视觉:识别棋盘和棋子的位置和类别
  • 行棋逻辑:分析用户走法,计算最佳走法
  • 机械臂控制:操作机械臂移动棋子

代码开源在Github

报告的第二部分是方案设计,例如在几个可能的方案中,我是如何做出选择的。 报告的第三部分是代码的详细文档,以及如何搭建这个项目的经验。

方案设计

器材的选用

我第一个考虑的问题是,如何让机械臂稳定地抓取和移动棋子?

与中国象棋扁平的棋子不同,国际象棋的棋子大都是立体的,并且顶面并不平整。 我使用的机械臂(越疆魔术师)有两种抓取模式,钩爪和吸盘,但对于形状不规整的棋子来说,这两种模式都不能很好地抓取棋子。 此外,机械臂的另一个问题是臂展相对较短,伸缩幅度只有25cm左右。这就意味着棋盘不能太大。 这两个问题意味着,市面上常见的国际象棋棋盘不能满足项目的需要。

几个可能的解决方案:

  1. 对常见的棋盘加以改造,例如在棋子顶端黏贴硬纸板,以方便吸盘吸取。但这并不能解决机械臂臂展不足的问题。
  2. 定制棋盘,例如自己设计棋子和棋盘,然后3d打印。缺点是比较麻烦。

最终的解决方法比较幸运:我恰好在淘宝上发现了合适的棋盘(商品链接)。 这种棋盘的棋子是平面的,而且棋盘较小,很适合机械臂的操作。

棋盘位置的识别

对棋盘进行图像处理的第一步,就是找出棋盘四个角在图片中的位置,用透视变换把这四个角放到图片的四个角上去。

OpenCV提供一个函数:findChessboardCorners()。 这个函数会识别一个黑白棋盘的内部格点。例如,一个8x8的棋盘会识别出7x7个内部格点。 对这些内部格点做一个线性外插,应该就能得到棋盘四个角对应的位置。

我在这个方案上花了两节课时间,试图让findChessboardCorners()能够工作。 但是这个函数也有不少问题。

  1. findChessboardCorners()的设计初衷是用于摄像头的矫正,而不是识别一个实际的象棋棋盘。因此在棋盘上有棋子时,识别正确率会大大降低。
  2. 运算量很大,在树莓派上运行速度很慢(调用一次需要1~2秒)。
  3. 无法分辨哪个是棋盘的“左上角”,哪个是“右下角”等等。

最终我放弃了这个方案。 新方案是,在棋盘的四个角贴上四种颜色的贴纸,利用颜色识别四个角。 这个方案的优点是简单方便,运行速度快,准确率高。 但缺点是,四块贴纸的中心与棋盘的四个角并不是完全重合的,需要在代码里手动矫正(比较hacky,第三部分会提到)。

棋子类型的识别

有了棋盘的图片,下面只需要把图片切割成8x8的小方格,然后分别识别棋子的类型即可。

这里有个小技巧:我们其实只需要识别棋子的颜色(白子,黑子,空格),而不需要识别棋子的类型(车,象,王,后)。 这是因为,国际象棋的每一步走法都可以用(棋子的出发地,棋子的目的地)这个二元组合唯一地表示。 例如,如果我们知道一步走法的出发点是e2,目的地是e4,那么我们就能判断出,白方的兵从e2走到了e4。 我们将这个状态保存起来,下次再遇见e4的时候,我们就知道这里肯定是一个白兵,不需要通过图像处理的手段识别棋子类型了。

有一个例外,就是兵的升变。国际象棋的规则是,兵在走到底线后,可以变成(后,车,象,马)中的任何一个棋子。 实际的对弈中大部分棋手都会选择变后(因为后最强大),但也有极少数情况,变后不是最佳选择。 不过对我们来说,在出现兵升变的情况时,默认变后已经足够了。

识别棋子类型的算法在第三部分会详细解释。

用户着法的分析,最佳着法的获取

在发现棋盘有变动之后,我们就可以通过对比棋盘前后哪些方格有变化,分析出用户的着法。 之后,将着法历史转换为代数记谱法(例如 ["e2e4", "d7d5", "e4d5"]),传给开源象棋引擎Stockfish。Stockfish会计算出机械臂的下一步最佳着法。 我利用了python-chess的一些封装,简化调用Stockfish的过程。

此外,从python-chess和Stockfish获取的最佳着法只包含了简要的走子信息(例如“f3上的马吃e5的兵”), 我们还需要将其转化为机械臂的实际操作流程(例如“先将e5上的棋子移出棋盘,再将f3上的棋子移到e5”)

机械臂的定位和校准

至此,最后一步就是让机械臂执行具体的操作指令(例如“将f3上的棋子移到e5”)。

如何定位呢? 最理想的方法是反馈控制,即用摄像头拍摄的实时画面,对机械臂的位置进行微调。但这个方式实现起来太过麻烦。 因此我采用了开环控制,在棋局开始时,让用户将机械臂放置在棋盘中央,之后的定位都按照相对这个中央的位置进行移动。 显然,这个做法会导致误差累积,偶尔会有定位偏差导致吸盘没能抓起棋子。此时我会稍微移动一下棋盘,手动纠正误差。 如果要实现真正的反馈控制,那么图像处理那方面就会更加复杂了。

代码文档与搭建经验

搭建准备

需要的硬件:

  • 树莓派开发板
  • 越疆魔术师机械臂(配吸盘)
  • 摄像头
  • 用于固定摄像头的支架
  • 国际象棋棋盘

在树莓派上安装依赖:

  • OpenCV-python,版本3(树莓派不支持版本4)
  • python3 -m pip install pydobot python-chess
  • apt-get install stockfish

然后如图搭建。下面是几个注意事项:

  1. 棋盘的左上角的贴纸应该是红色。“外黑内白”的棋子是白子,应该放在用户一侧。“外白内黑”的棋子是黑子,应该放在机械臂一侧。
  2. 棋盘稍微垫高一点,因为机械臂的臂展与操作的高度有关。
  3. 棋盘如果不是正常的开局(例如少了几个棋子)需要在代码中设定(main.py)。目前版本的代码对应的就是如图的棋盘布局。
  4. 摄像头固定得越高越好,可以减少因为画面扭曲造成的失真。
  5. 机械臂开机后,长按Key键五秒后松开,可以重置机械臂,有利于提高定位精度。

搭建范例

视觉模块:arm.py

SquareType表示一个方格的类型。 SquareType.empty表示这个方格上没有棋子。 SquareType.white表示这个方格上有一个白子。 SquareType.black表示这个方格上有一个黑子。

视觉模块提供了下面几个函数:

  • find_corners(image) 从摄像头拍摄的图片中提取棋盘四个角的坐标。
  • transform(image, corners) 对图片进行透视变换。
  • detect_square_type(image) 识别(已经切割好的)一块图片是黑子还是白子。
  • get_position_from_image(image) 上面三个函数的合并,从摄像头拍摄的图片中获取棋盘所有方格是黑子还是白子。

find_corners(image) 利用cv2.inRange()cv2.find_contour()识别棋盘上贴的四张彩色贴纸。 识别颜色时用到的masks对于光线和摄像头很敏感。如果不能正常识别色块,可能需要调整masks中对应的hsv色彩范围。

由于贴纸的中心与棋盘四个角并不完全重合,adjusted_points对点的位置做了一些相对调整。搭建时需要观察程序识别的结果,适当调整参数,保证透视变换完成的图片刚好只包括棋盘本身,不要包括棋盘外面的画面。

detect_square_type(image)利用FloodFill算法判断一块图片是黑子还是白子。 算法如下:

  1. 先裁剪图片,只保留中间部分,去除那些由切割图片不准确导致的其他方格的色素。
  2. 选择合适的阈值,将图片转换为黑白。阈值的选取与场地光照有关。
  3. 此时进入正题。图片的背景可能是黑方块,也可能是白方块。图片里可能有一个白子,一个黑子,或者什么都没有。一共六种情况。按照下面的算法做判断。
    1. 检查图片是否是单色的(纯黑或纯白),如果是,那么返回结果“这里没有棋子”。
    2. 对图片边缘FloodFill黑色。再次检查图片是否是单色,如果是,那么返回结果“这里有一个黑子”(对应的是黑子在白色背景上的情况)。
    3. 对图片边缘FloodFill白色。再次检查图片是否是单色,如果是,那么返回结果“这里有一个白子”。否则返回结果“这里有一个黑子”(对应的是黑子在黑色背景上的情况)

行棋逻辑模块:positions.py

position指的是一个8x8的SquareType数组,用来记录棋盘的情况。

行棋逻辑模块提供了下面几个函数:

  • print_position(position) 打印一个position,用于调试。
  • get_position(board) 从一个chess.Board对象中提取出position
  • get_move_from_diff(board, old_position, new_position) 从前后两个position中分析出用户的走法。这个函数本来还应该处理兵升变的特殊情况(记谱法会有一些变化),但是这部分最终没时间实现了。

这些代码大部分涉及到国际象棋的具体规则,所以如果出问题的话...最好找个会下棋的程序员来看看。

机械臂控制模块:arm.py

机械臂控制模块提供了一个类,class Arm。 这个类提供了以下方法:

  • __init__(self) 初始化机械臂,打开串口通信。
  • calibrate(self) 将机械臂当前的位置设为参考位置。
  • act(self, board, move) 分析走法(是否是吃子,易位等特殊规则),将走法转化为指令序列并执行。

按照越疆魔术师的文档,机械臂面朝的方向为x轴正半轴,x轴逆时针旋转90度为y轴正半轴。 所有距离均以cm为单位。

棋盘每个正方形方格的边长为20cm。

如果机械臂的x-y轴与棋盘恰好一致,那么只需要一个参考位置就可以准确定位。实际操作的时候,机械臂的x轴偶尔会有一点点歪(不完全是机械臂面朝的方向)。这应该是机械臂的问题,重置一下应该会好。

运行项目:main.py

python3 main.py运行项目。项目会做下面的事:

  1. 初始化摄像头,机械臂,象棋引擎
  2. 设定棋盘的初始局面。如果棋盘少一些棋子,则需要在这里设定。
  3. 显示摄像头的拍摄画面,提示用户设定机械臂的参考位置。用户需要将机械臂的吸盘移动到棋盘最中间那个格点(交叉点),吸盘紧贴棋盘,然后按空格键完成设定。机械臂会自动归位。
  4. 游戏开始。程序额外打开两个窗口,显示经过矫正的棋盘。代码会不断检查局面,如果发现局面变化,则尝试分析用户的着法。如果用户着法合理,那么调用象棋引擎计算最佳着法,并让机械臂执行着法。如果用户着法不合理,或者视觉模块分析出的结果不稳定(连续5帧画面分析出相同的结果认为是稳定)会在命令行打印相关日记。游戏结束时(将死/和棋)程序自动退出。

参考文献

Raspberry Turk是国外一个自动下棋的机械臂的项目,给了我很多思路和启发。

About

基于树莓派的自动下棋机器人

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages