基于Python与OpenCV的围棋棋盘棋子识别实践

发布时间:2026/9/12 12:35:52
基于Python与OpenCV的围棋棋盘棋子识别实践 简介基于Python与OpenCV的围棋棋盘棋子识别项目完整提供源码与配套文档面向毕业设计、课程设计及实际项目开发场景适合具备基础图像处理知识、需要快速完成识别模块的学生或开发者。源码经过严格测试可直接运行并在此之上继续扩展功能。压缩包共40个文件大小约4.83MB以jpg和png测试图片为主覆盖棋盘定位、棋子检测等不同场景同时包含Python主程序与辅助脚本以及Markdown格式的说明文档便于理解实现思路与二次开发。文档对运行环境和关键函数进行了说明配合测试图片可快速验证不同输入下的识别效果。文件组织清晰能够帮助使用者快速找到代码、图像与文档入口。目前已有280人浏览学习作为轻量级入门参考可有效节省从零搭建识别流程的时间。1. Python OpenCV 做围棋棋盘棋子识别先分清看和想打开一张随手拍的棋盘照片程序能不能说出第 3 行第 5 列是黑子、白子还是空这就是基于 Python 和 OpenCV 的围棋棋盘棋子识别要解决的问题。毕设里常见的形态是接摄像头实时输出 19×19 状态矩阵课程设计里则常简化为单张图片离线识别。它只负责看不负责想核心是几何与颜色信息。为什么绕不开 OpenCV边缘检测、直线检测、透视矫正、圆形检测全部内置Python 接口完善。难度上限取决于拍摄条件俯拍最容易倾斜、遮挡、光线不均才是拉开差距的地方。下文按棋盘定位 → 透视矫正 → 棋子分类展开每步给可复现代码。适合准备毕业设计、课程设计的学生以及想把棋谱照片批量转成数字棋谱的工具开发者。读完你能跑通最小实现也能从现象反推问题出在哪一层。2. 围棋棋盘识别的 OpenCV 理论基础预处理、边缘与直线的逻辑2.1 预处理为什么是灰度加高斯模糊Canny 阈值怎么定围棋棋盘的核心特征是深色木纹背景上的一组等间距横竖直线。先把图像转成灰度把颜色信息压成单通道再高斯模糊削弱木纹和噪点最后用 Canny 找边缘。颜色信息在这个项目里用处不大因为木纹颜色和网格线很接近直接做颜色分割反而引入大量噪声所以通道数越少越干净。环境上只需要装好 Python 和 opencv-python、numpy 两个包。import cv2 img cv2.imread(board.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blur cv2.GaussianBlur(gray, (5, 5), 0) edges cv2.Canny(blur, 50, 150)GaussianBlur 的核大小决定模糊强度(5, 5) 是折中值核太小木纹残留边缘图里会冒出一堆短线段核太大比如 (15, 15)细网格线会被一起抹掉。Canny 的 50 和 150 是滞后双阈值梯度大于 150 必为边缘小于 50 直接丢弃介于两者之间只有在连着强边缘时才算。棋盘这类高对比场景用 50–150 够用如果出现断线把两个值一起下调比如 30–90优先保证外框线完整。Canny 输出只回答哪些像素是边缘不给方向和长度。棋盘线的方向信息要到霍夫变换里才用得上所以这两步之间不要省中间量调试时把 gray、blur、edges 分别用 imshow 弹出来看一眼能快速定位是去噪不够还是阈值太严。2.2 HoughLinesP 检测网格线参数表与调坏的症状霍夫直线变换把每个边缘像素映射到参数空间累加投票票数高的参数组合就是一条直线。OpenCV 里标准 HoughLines 返回极坐标参数概率版 HoughLinesP 只对局部投票高的线段输出速度快且带端点坐标棋盘这种长直线密集场景基本都用后者。h, w img.shape[:2] lines cv2.HoughLinesP(edges, 1, np.pi / 180, threshold120, minLineLengthmin(h, w) // 3, maxLineGap15)参数直接决定输出质量调参顺序建议先固定 threshold再动 minLineLength参数常见取值作用与调坏的症状rho1参数空间距离分辨率一般不动thetanp.pi/180角度分辨率 1 度够用threshold100~150线段的投票门限调大漏检调小碎线满天飞minLineLength短边/3过滤短线段调大丢边框线maxLineGap10~20合并断线的最大间隙调大把不相关的线接上输出 lines 是 N×1×4 数组每个元素是 (x1, y1, x2, y2)。下一步的工作很机械按斜率把线段分成横向组和竖向组再把同一条网格线被拆出的多段合并成一根。合并这一步直接决定后续交点和透视矫正的精度值得多花时间调 gap。2.3 为什么要透视矫正正交网格才是识别棋子的底座棋盘是矩形平面相机不垂直桌面拍摄时成像里的网格不再是等距正交。棋子识别是在交叉点附近取小邻域做判断直接用倾斜图上的像素坐标交叉点间距不均同一个半径的邻域会盖到别的格子上误判率明显上升。解决方式是单应性变换。原理上用四条边界线的交点构成源四边形映射到目标矩形算出 3×3 矩阵 M再对整张图做重采样。cv2.getPerspectiveTransform 要求源四点按左上、右上、右下、左下顺序排列顺序错了输出就是翻转或撕裂的图。矫正之后19 路棋盘相邻交叉点间距变成常量棋子判断半径可以固定这是整套方案里性价比最高的一步。3. 用 HoughLinesP 重建棋盘网格横竖线聚类、等距插值与透视矫正3.1 拆横竖线角度过滤与重复直线合并HoughLinesP 返回的线段里属于棋盘的只是一部分另外还有外框、桌面纹理甚至棋子边缘的短线。第一步先按角度过滤横向线接近 0 度或 180 度竖向线接近 90 度。倾斜拍摄时棋盘线角度不会严格是 0 或 90所以容忍度给到 5 度别用 1 度把真实网格过滤掉。import numpy as np def filter_and_cluster(lines, gap12): h_lines, v_lines [], [] for x1, y1, x2, y2 in lines[:, 0]: angle abs(np.degrees(np.arctan2(y2 - y1, x2 - x1))) if angle 5 or angle 175: h_lines.append((y1 y2) / 2) elif 85 angle 95: v_lines.append((x1 x2) / 2) def cluster(vals): vals sorted(vals) groups [] for v in vals: if groups and v - groups[-1][-1] gap: groups[-1].append(v) else: groups.append([v]) return [sum(g) / len(g) for g in groups] return cluster(h_lines), cluster(v_lines)横向线只记录平均 y 坐标竖向线只记录平均 x 坐标方向信息在这步之后就不需要了。cluster 做一维聚类同一条网格线在 Canny 图里常被高光或棋子截断Hough 输出两三条相邻线段不合并的话交点会算歪。gap12 表示两条线段中点距离在 12 像素内视为同一条A4 打印的棋盘图够用4K 大图调到 25 左右原则是间隙小于一个方格边长的一半。3.2 外边界与等距插值重建 19 路网格交点拿到横纵坐标列表后不能直接取前 19 个Hough 可能把棋盘外的桌子边缘也检出来线数多于 19。这里利用围棋棋盘自带的先验网格等距只取最外侧两条线作为上下左右边界中间用等距插值生成整个网格。def build_grid(h_ys, v_xs, board_size19): top, bottom min(h_ys), max(h_ys) left, right min(v_xs), max(v_xs) ys np.linspace(top, bottom, board_size) xs np.linspace(left, right, board_size) return xs, ysnp.linspace 从边界值生成 19 个等间隔坐标省去逐条匹配。前提是检测结果里确实包含棋盘最外两条线多数拍摄场景满足。这个只信边界、中间插值的做法在极端侧拍角度下会牺牲一点精度但换来的是对多余线段的强鲁棒性实际工程里很划算。board_size 参数换成 9 或 13同一份代码就能适配小棋盘这也是源码里最值得抽成配置项的地方。3.3 透视矫正四条边界到四个角点的最小实现有了边界坐标就能构造四个角点。源点顺序必须是左上、右上、右下、左下目标矩形的点按同样顺序给映射才不会翻转。def warp_perspective(img, xs, ys, size760): left, right xs[0], xs[-1] top, bottom ys[0], ys[-1] src np.float32([[left, top], [right, top], [right, bottom], [left, bottom]]) dst np.float32([[0, 0], [size, 0], [size, size], [0, size]]) M cv2.getPerspectiveTransform(src, dst) warped cv2.warpPerspective(img, M, (size, size)) return warped, M, np.linalg.inv(M)size760 的由来19 路棋盘有 18 个间隔760 除以 18 约 42 像素这个间距足够容纳棋子直径也让后续取邻域的半径比较好设。目标尺寸过大只拖慢速度不增加信息过小则格子之间区域太小黑白子特征会被糊掉。逆矩阵 inv_M 留给最后在原图上画标注用识别结果在矫正图上做展示时再映射回原图坐标。这一步跑完记得把 warped 存下来看一眼如果四条边没闭合或边缘有白色空白区域说明角点取错了问题多半在聚类合并那一步。4. 棋子识别与黑白分类交叉点邻域灰度差与圆形校验4.1 直接在交叉点取像素均值为什么不准网格线经过交叉点直接以交叉点为中心取方块算均值会把线一起算进去。网格线颜色接近黑子空点会被系统性判成黑。正确做法是把交叉点邻域拆成中心圆和外围环形两部分比较两者的灰度差有棋子时中心明显区别于环空点时两者接近。这种相对比较天然抗光照整体漂移因为同一时刻同一局部的光照是一致的。def classify_stone(warped_gray, cx, cy, stone_r10, ring_r18): h, w warped_gray.shape y0, y1 max(0, cy - ring_r), min(h, cy ring_r) x0, x1 max(0, cx - ring_r), min(w, cx ring_r) region warped_gray[y0:y1, x0:x1] cxx, cyy cx - x0, cy - y0 center_mask np.zeros(region.shape, np.uint8) cv2.circle(center_mask, (cxx, cyy), stone_r, 255, -1) ring_mask np.zeros(region.shape, np.uint8) cv2.circle(ring_mask, (cxx, cyy), ring_r, 255, -1) ring_mask cv2.subtract(ring_mask, center_mask) center_mean region[center_mask 0].mean() ring_mean region[ring_mask 0].mean() diff ring_mean - center_mean if diff 18: return 2 elif diff -18: return 1 else: return 0diff 是环平均灰度减中心平均灰度白子让中心变亮diff 为正且大黑子让中心变暗diff 为负。18 是 0~255 灰度域里的经验阈值跟曝光强弱有关调参时打印一批采样点的 diff 分布再定。stone_r 和 ring_r 与矫正图的分辨率挂钩用前面 size760 的图10 和 18 是合理起点stone_r 如果偏大相邻棋子的中心圆会重叠diff 会被稀释。4.2 星位干扰与 HoughCircles 圆形校验灰度差法在两类场景会误判棋子紧邻时两个中心圆连成一片以及棋盘星位。星位是印刷在交叉点上的小黑点空棋盘上它会让中心变暗被判成黑子。常用的补救手段是叠加霍夫圆检测只在与圆心匹配的交叉点位置才启用棋子分类。blurred cv2.GaussianBlur(warped_gray, (9, 9), 2) circles cv2.HoughCircles(blurred, cv2.HOUGH_GRADIENT, dp1.2, minDist15, param1100, param228, minRadius7, maxRadius14)HoughCircles 前先用 9×9 高斯核再模糊一次圆心检测对噪声比边缘检测更敏感。dp1.2 是输入图与累加器分辨率比值越大越快但定位越粗minDist 是相邻圆心最小距离棋盘相邻交叉点约 42 像素设 15 足够param2 是圆心投票阈值调大只保留轮廓更完整的圆。黑子对比度低param2 用 20~30 更稳妥。拿到圆列表后跟 build_grid 生成的交叉点做最近邻匹配def match_circles_to_intersections(circles, xs, ys, max_dist8): stones set() for x, y, r in circles[:, 0]: gi int(np.argmin((ys - y) ** 2)) gj int(np.argmin((xs - x) ** 2)) if np.hypot(xs[gj] - x, ys[gi] - y) max_dist: stones.add((gi, gj)) return stones这里直接在 19 个网格坐标里找最近的索引比遍历全图快得多。最近距离超过 max_dist 的圆视为背景噪声不参与分类。这个双重校验基本能把星位误判压到零代价是检测圆的耗时增加离线批量处理无所谓实时模式建议只在灰度差结果可疑的点上做圆校验。4.3 摄像头实时识别与状态矩阵输出离线图片跑通后最常做的扩展是换成摄像头实时识别。cv2.VideoCapture(0) 打开默认摄像头read() 返回 (ret, frame)ret 为 False 说明摄像头被占用或驱动异常。每一帧执行同一套流程但棋盘定位没必要每帧重做每隔 30 帧或按 A 键手动刷新一次网格能避免画面抖动引起的边界线跳变。def game_state(warped_gray, xs, ys): board np.zeros((19, 19), dtypenp.int8) for i, y in enumerate(ys): for j, x in enumerate(xs): board[i, j] classify_stone(warped_gray, int(x), int(y)) return board输出是 19×19 的 int8 矩阵黑子记 1、白子记 2、空记 0。之后无论是存成 SGF 棋谱、给形势判断喂数据还是按 GTP 协议对接引擎都以这个矩阵为唯一数据源。答辩和评审最常检查的就是这个矩阵跟真实棋局是否一致它的正确性比界面美观重要得多。5. 光照不均、星位误判与识别率验收把识别率从 80% 提到 95%5.1 光照不均先用 CLAHE别急着换算法 实拍照片最常见的失败模式是半边亮半边暗。棋盘木纹反光、窗外强光造成的大面积灰度漂移会让固定灰度阈值全面失效。OpenCV 自带的 CLAHE 自适应直方图均衡比全局均衡好用它把图像分块处理并限制对比度增强幅度避免木纹噪声被一起放大。clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) gray clahe.apply(gray)clipLimit2.0 是经验起点木纹明显的棋盘降到 1.5防止板面纹路增强成假边缘。这个操作放在高斯模糊之前Canny 和后续管线都不用改改动成本极低收益却很大。5.2 三个高频踩坑点的排查顺序第一个坑是重复直线聚类 gap 设太小。4K 大图里同一条网格线的线段中点可能差 20 像素以上gap12 分不出是同一根交点坐标被拉偏。按图片长边比例设 gap长边 3000 像素以上就用 25并同步调大 minLineLength。第二个坑是星位被判成黑子。空棋盘上星位黑点让中心均值低于环均值diff 超过阈值。解决思路是 4.2 节的圆形校验或者对空棋盘预先记录星位坐标识别时跳过这些固定点。排查时先看空棋盘的混淆矩阵如果空点错误地从 0 变 1优先怀疑星位而不是光照。第三个坑是透视矫正前棋盘不完整。四条边界线必须全部在画面内这是硬前提。手机拍摄时给棋盘点留出边距别让网格线贴着图片边缘否则断线后 minLineLength 会直接过滤掉残线边界插值就会整体偏移。5.3 用混淆矩阵验收识别率写进项目文档空口说效果不错在答辩和课程设计评审里没有说服力。准备一个量化验证脚本手工标注 3~5 张测试图的真实状态矩阵跟程序输出比对算出三类各自的准确率。from sklearn.metrics import confusion_matrix gt np.load(gt.npy) # 手工标注的 19x19 矩阵 pred np.load(pred.npy) # 程序输出的 19x19 矩阵 print(confusion_matrix(gt.ravel(), pred.ravel(), labels[0, 1, 2]))混淆矩阵对角线是各类别正确数。空点成片误判成黑子优先查星位和网格线干扰黑白互混回查 diff 的阈值方向是不是反了某一区域成片错误基本是光照或透视矫正的锅。把这个验证过程连同五张测试图、程序输出一起放进源码包的 README正好呼应标题里源码文档的要求也比单纯贴代码更容易拿到高分。本文还有配套的精品资源点击获取