围棋作为一项古老的棋类游戏,有深厚的文化底蕴和复杂的棋局变化。无论是初学者还是高手,都离不开复盘这一提升棋艺的重要环节。复盘不仅帮助我们反思自己的落子思路,还能通过借鉴名局、模拟对战进一步提升。然而,现实中一个常见的困扰是:当我们在网站上欣赏一局精彩的对弈时,却无法轻松下载棋谱SGF(Smart Game Format)文件。特别是许多在线平台,我们只能截取带手数标注的棋盘截图,却无法导出棋谱。
想象一下,你在观看一场紧张刺激的围棋对局,发现一手妙招想深入研究,但苦于无法保存SGF文件,只能手动输入每步棋,这不仅繁琐,还容易出错。因此,我开始探索如何将围棋对弈截图直接转换为SGF文件的实现路径,并制作了这个工具:

相比于网上的其他类似工具,该工具有以下优势 :
- 能识别围棋的手数(这是绝大多数类似工具无法实现的)
- 自主训练的CNN模型,识别效果良好,不需要联网
- 支持m x n (1 ≤ m, n ≤ 25)路的棋盘
- 兼容无手数标记的截图
- 工具界面支持拖曳图片,增删改手数等操作
本文将围绕几个核心问题展开讨论,不再拘泥于代码细节,而是从实际技术难点和实现策略谈起。
如果你仅需使用该工具,欢迎直接跳转到文末下载。
1. 怎么识别棋盘边界?
围棋棋盘的识别是整个截图转SGF工具中的第一步。准确定位棋盘的边界,对于后续围棋子的定位、以及手数识别至关重要。
1.1 霍夫直线变换 ×
一开始,我考虑采用霍夫线变换检测边缘的策略,即首先转化为灰度图,经高斯模糊去噪及Canny边缘检测等预处理后,使用HoughLinesP方法检测直线。

然后筛选直线中的水平线和垂直线,将纵坐标最小和最大的水平线作为棋盘的上下边界,将横坐标最小和最大的垂直线作为棋盘的左右边界。
但是,这种方法对截图有很高的要求。对于截取到窗口的图片,这种方法可能误将窗口边框当作棋盘边界;而对于未截取到棋盘边界的图片,这种方法可能将1路和19路围成的边框当作棋盘边界。鉴于1路和19路围成的边框与棋盘边界都是近似正方形,且大小相近,仅仅通过形状轮廓难以区别,我开始转向棋盘的其他特征来识别边界。
1.2 借助红色通道 ×
由于棋盘背景以黑灰色(比如野狐棋盘)和蓝绿色(比如Sabaki、101围棋网)居多,而棋盘以黄色居多,二者BGR红色的含量有明显的差距,因此我考虑在红色通道上做做文章。
red = _image[:, :, 2]
# 设置阈值二值化
_, thresh = cv2.threshold(red, 170, 255, cv2.THRESH_BINARY)
# 找到图像的轮廓
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 找到包围棋盘的最小矩形
if contours:
# 获取面积最大的轮廓
max_contour = max(contours, key=cv2.contourArea)
# 获取最小外接矩形
x, y, w, h = cv2.boundingRect(max_contour)
在红色通道上设置阈值二值化进一步区分背景和棋盘,然后通过轮廓检测找到最大轮廓,从而分离出棋盘区域。经过测试,这种方法在几张棋盘截图上获得了较满意的效果。本在得意之时,我又发现这种方法缺乏泛用性。对于白色背景,因为白色和黄色BGR红色的含量都很高,这种方法难以区分出边界。
1.3 借助HSV色彩空间 √

后来我根据ChatGPT的建议将BGR转化为HSV。HSV色彩空间是用来表示颜色的一种方式,与常见的RGB颜色空间不同,它更符合人类对颜色的感知方式。在BGR颜色空间中,颜色由蓝、绿、红三个分量的强度来表示,而HSV颜色空间通过色调(Hue)、饱和度(Saturation)和亮度(Value)三个参数来定义颜色。经过测试,我发现棋盘的色调大致在(16, 50, 128)到(22, 255, 255)之间,因此:
# 裁剪边缘
def crop(_image):
# 转换为HSV颜色空间
hsv = cv2.cvtColor(_image, cv2.COLOR_BGR2HSV)[:, :, 0]
# 创建遮罩
mask = cv2.inRange(hsv, (16, 50, 128), (22, 255, 255))
# 找到图像的轮廓
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 找到包围棋盘的最小矩形
if contours:
# 获取面积最大的轮廓
max_contour = max(contours, key=cv2.contourArea)
# 获取最小外接矩形
x, y, w, h = cv2.boundingRect(max_contour)
# 裁剪后宽高为原图90%以上,避免误裁
if h > _image.shape[0] * 0.9 and w > _image.shape[1] * 0.9:
# 裁剪图像到矩形区域
return _image[y:y + h, x:x + w]
return _image
为了避免意料之外的误裁,限定裁剪后图片宽高为原图90%以上 。即使存在干扰,无法裁剪掉背景,只要棋盘空间占据图片的绝大部分,对后续识别影响也不大。
2. 检测圆还是检测直线?
棋盘边界已经找出来了,那么下一步就是找到棋子了。检测棋子大体有两条路径:其一是使用霍夫直线变换检测出棋盘中的横线和竖线并计算横线与竖线的交叉点,然后依据交叉点的位置独立出周围的图像,最后使用颜色阈值来判断图像中有无棋子;其二是使用霍夫圆变换在截图中直接找到棋子的圆心位置。
2.1 检测直线发生的问题
虽然检测直线的方法更加繁琐,但这种方法可以直接获得棋子的位置(第几行第几列),因此这也是我最先尝试的方法。这种办法对于棋盘上棋子比较少时检测效果尚可,但是当棋盘上棋子较多时,实际效果差强人意......

棋盘中识别出了大量的伪线,而本应识别出的直线却没有识别出来。反复调整参数,仍然无法收到满意的效果,因此我放弃了霍夫线变换的办法,转向霍夫圆变换。