探索如何使用计算机视觉技术识别棋盘和棋子,并将它们转换为数字格式。
儿子,一个8岁的国际象棋棋手,向提出了一个挑战:开发一个应用,可以将现实国际象棋游戏中的棋盘和棋子转换为数字格式。这样他就可以在自己的设备上继续游戏,与朋友分享,或者从像Stockfish这样的国际象棋引擎获取走棋建议。决定将这个想法变为现实。
在这篇文章中,将详细介绍如何构建一个使用计算机视觉技术识别棋盘上的棋子的应用,并创建一个棋盘的数字表示,以便可以写出Forsyth-Edwards Notation(FEN)数据。这些数据随后可以导出到像Lichess这样的工具中,以数字化地表示照片中的物理棋盘。
整个构建应用的过程和与该项目相关的完整代码已经发布在GitHub账户上。
让从一个清晰的问题陈述开始:想要拍摄一张现实国际象棋棋盘的照片,并输出棋盘和棋子的机器可读格式。
很快发现,棋盘识别是计算机视觉、机器学习和模式识别中的一个热门问题。多年来,这个主题上有很多研究。找到的第一项研究发表于1997年。看到不同的研究人员如何使用他们当时的技术和算法来解决这个问题,这非常有趣。
阅读先前的工作帮助意识到,棋盘视觉问题可以分解为两个主要的子问题:
- 棋盘识别:在图像中识别棋盘并识别其特征:位置、方向和方格的位置。
- 棋子识别:在棋盘上检测棋子,对它们进行分类,并在方格上定位它们。
这是需要弄清楚的工作流程草图:用户拍摄一张国际象棋游戏的照片,运行棋盘识别算法,运行棋子识别算法,然后将输出转换为FEN数据。FEN是描述国际象棋游戏位置的标准表示方法。FEN数据易于计算机解释。
发现有三种主要方法来识别棋盘及其特征:
- 基于角点的方法;
- 基于线条的方法;
- 热图方法。
在尝试了这些方法之后,发现版本的基于角点的方法最适合现实生活的图像。使用YOLOv8来检测棋盘角点。一旦有了角点的坐标,就可以开始计算棋盘其余部分的位置。
为了检测角点,创建了一个大约50张棋盘图像的小数据集,并使用Roboflow平台快速标注了它们的角点。然后使用Roboflow将数据集快速扩充到原来的3倍大小。训练了一个YOLOv8 nano模型来检测棋盘角点:
from ultralytics import YOLO
# 加载模型。
model = YOLO('yolov8n.pt')
# 训练。
results = model.train(
data='data.yaml',
imgsz=640,
epochs=100,
batch=8,
name='yolov8n_corners')
model_trained = YOLO("runs/detect/yolov8n_corners/weights/best.pt")
results = model_trained.predict(source='1.jpgresized.jpg', line_thickness=1, conf=0.25, save_txt=True, save=True)
在测试中,方法不受不同光照条件、拍摄角度或棋盘类型的影响。这部分是由于应用的增强,帮助创建了一个更具代表性的数据集。
看到这种简单的解决方案在案例中胜过了更复杂的方法,这真是令人惊讶。在检测角点方面取得了很好的结果,可以继续进行下一阶段。
检测到角点后,想将它们重新排列成固定顺序。发现了一个小程序,它对这个问题应用了简单的逻辑。左上点将具有x和y之间最小的和,而右下点将具有最大的和。右上点将具有最小的差,而左下点将具有最大的差。这是使用的逻辑:
def order_points(pts):
# 对4个坐标点进行排序:
# 0:左上,
# 1:右上
# 2:右下,
# 3:左下
rect = np.zeros((4, 2), dtype = "float32")
s = pts.sum(axis = 1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
diff = np.diff(pts, axis = 1)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
return rect
现在已经按照固定顺序得到了棋盘四个角点的坐标,可以使用OpenCV2计算透视变换矩阵。这种变换算法以直线方式变换图像。这在计算棋盘上所有方格的位置时非常有用。
在这个阶段,可以通过将图像的上边缘除以8,然后对左边缘做同样的操作,然后将所有点连接成线,轻松计算出棋盘上所有方格的坐标。
然后,可以将每个方格映射到其对应的FEN数据。这幅图示显示了如何用FEN描述棋盘上的每个位置:
棋盘的FEN矩阵。然后每个方格可以使用相同的方法计算:
a8 = np.array([[xA,y9], [xB, y9], [xB, y8], [xA, y8]])
a7 = np.array([[xA,y8], [xB, y8], [xB, y7], [xA, y7]])
…
其中xA、xB、y9、y8等是:
xA = ptsT[0][0]
xB = ptsT[1][0]
y9 = ptsL[0][1]
y8 = ptsL[1][1]
…
现在已经得到了棋盘上所有方格的坐标,可以继续对棋子进行分类。然后,可以将每个检测与其对应的方格连接起来。
再次转向YOLOv8和Roboflow,这次注释了一个更大的棋盘棋子数据集,这些棋子使用上述的透视变换矩阵进行了变换。
Roboflow使这个阶段变得简单。大约一个小时后,就有了一个注释好的数据集,Roboflow也自动对其进行了增强,可以开始训练另一个YOLOv8模型来解决棋子检测问题。
YOLOv8在检测棋子方面做得很好!下面的图表显示了模型在训练期间的性能:
现在可以将所有棋子连接起来。有棋盘上所有方格的坐标,也有棋子检测的坐标。使用"shapely"包,写了一个小程序,它计算预测的边界框和棋盘上每个方格之间的交集比(IoU)。
然后写了一个函数,将每个检测连接到正确的方格——这将是与检测的边界框IoU最大的那个方格。对于像皇后或国王这样的高个子棋子,只考虑边界框的下半部分。
def calculate_iou(box_1, box_2):
poly_1 = Polygon(box_1)
poly_2 = Polygon(box_2)
iou = poly_1.intersection(poly_2).area / poly_1.union(poly_2).area
return iou
在这个阶段,可以用一个简单的脚本来写出棋盘的FEN,将连接的检测和方格按照正确的格式排序,然后输出到Lichess FEN URL。有了这个URL,用户可以享受计算机国际象棋的所有魔力:分享游戏、稍后保存、分析、获取建议等。
board_FEN = []
corrected_FEN = []
complete_board_FEN = []
for line in FEN_annotation:
line_to_FEN = []
for square in line:
piece_on_square = connect_square_to_detection(detections, square)
line_to_FEN.append(piece_on_square)
corrected_FEN = [i.replace('empty', '1') for i in line_to_FEN]
print(corrected_FEN)
board_FEN.append(corrected_FEN)
complete_board_FEN = [''.join(line) for line in board_FEN]
to_FEN = '/'.join(complete_board_FEN)
print("https://lichess.org/analysis/" + to_FEN)
所描述的方法表现非常好,棋盘角点检测的准确率超过99.5%,棋子识别的准确率为99%。这种方法简单、快速、轻量级,同时避免了耗时且资源密集的训练过程。它可以很容易地推广到其他类型的棋盘和棋子。
然而,这种方法实用性的最好证明是,儿子实际上和他的朋友们一起使用它😊
请随时通过LinkedIn与联系,提出这个项目未来迭代的想法。