Skip to content

yizhepku/Raspberry-Chess

Folders and files

NameName
Last commit message
Last commit date

Latest commit

yizheyizhe
yizhe
and
yizhe
Dec 28, 2019
89b6ad5 · Dec 28, 2019

History

13 Commits
Dec 28, 2019
Dec 28, 2019
Dec 22, 2019
Dec 28, 2019
Dec 23, 2019
Dec 22, 2019
Dec 23, 2019
Dec 28, 2019
Dec 23, 2019

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