ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python+OpenCV围棋棋盘棋子识别实战指南

Python+OpenCV围棋棋盘棋子识别实战指南 简介对于计算机视觉方向的学习者和应届毕业生这套基于Python和OpenCV的围棋棋盘棋子识别项目可同时用于毕业设计、课程设计与实际项目开发。压缩包共包含40个文件其中33张jpg测试图像用于验证棋盘和棋子检测效果4张png图片用于补充示例场景2个Python脚本负责图像处理与识别主流程另有1份Markdown说明文档介绍项目结构与运行方式整体压缩包仅4.83MB便于下载和本地调试。项目源码已经过严格测试可以在原有基础上放心修改、运行和二次开发适用于棋盘定位、棋子分割、目标检测等教学实验。测试图片和说明文档互相配合能够帮助快速理解OpenCV在围棋识别任务中的典型用法也适合作为毕业设计或课程设计的参照模板。目前已有280人学习下载是一份兼具教学与实战价值的入门级视觉识别资源。1. 围棋棋盘识别不是目标检测先定位棋盘再看棋子落在哪不少人拿到「基于 Python 及 OpenCV 的围棋棋盘棋子识别」这个题目时第一反应是上深度学习——YOLO 标注几千张棋盘照片训练个检测模型。但实际做下来会发现围棋棋盘识别和通用目标检测是两回事棋盘是高度规整的 19 路网格棋子永远落在固定的 361 个交叉点上这个先验信息比任何神经网络都值钱。用 OpenCV 的传统图像处理方案本质是「先找到棋盘 → 算出所有交叉点 → 判断每个交点上有无棋子、是黑是白」整个过程可解释、可调参、代码量可控一台普通笔记本就能跑实时识别非常适合毕业设计、课程设计和项目开发的场景。这套方案能解决的现实问题很明确拍照或扫描一张围棋棋谱自动输出 19×19 的局面矩阵或者对着棋盘实时录像逐帧判断每一步落子给后续的棋谱记录、形势判断提供数据。适合的人群也很清楚——正在做图像处理相关课设或毕设的学生以及想在棋类对弈、棋谱电子化方向快速验证方案的开发者。接下来我按自己做过的流程把从读图到输出局面矩阵的完整套路拆开讲。2. 棋盘定位先拿到十九路网格再谈棋子2.1 为什么要先找棋盘而不是先找棋子初看这个问题很多人会觉得直接检测棋子更简单——黑白圆片特征明显霍夫圆一套就出来了。但实际跑一遍就会发现问题你检测出了 200 个圆这些圆分别落在棋盘的哪个交叉点上没有坐标系棋子位置就是一堆散点根本组不成棋局。所以正确的顺序永远是把棋盘定位放在第一步把棋盘网格当成坐标系后面所有棋子都挂到这个坐标系上去解释。棋盘定位这件事在 OpenCV 里是个标准的图像处理流程。它的核心依据是棋盘的结构特征黑白相间的格子、密集的横竖直线、外围有明显的边框。常见的做法是「灰度化 → 去噪 → 自适应二值化 → 形态学闭运算 → Canny 边缘 → HoughLinesP 直线检测 → 直线聚类 → 计算交点」这条链。每一步都在为下一步减少干扰闭运算把断开的棋盘线接上HoughLinesP 把边缘图变成无数条长短不一的线段再按角度把线段聚成横线和竖线两组最后用横竖线的交点拟合出整个网格。这里要提醒一个关键点HoughLinesP 检测出来的线段往往是断裂且重复的直接拿来算交点会出现大量「伪交叉点」。我一般会先按角度和间距做聚类合并把角度接近、位置上重叠的线段归为一组每组取一条代表线。这一步不做后面透视变换求出的角点会飘得厉害整块棋盘的坐标都会偏移。2.2 直线检测与交点计算最小可运行的代码先给出一段能直接跑通的最小代码输入是一张棋盘照片输出是交点坐标列表和叠加了交点的标注图。这段代码我建议放在项目的最前面先把链路通起来再逐步加东西。import cv2 import numpy as np def detect_grid_intersections(img_path): # 读取图像转灰度 img cv2.imread(img_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊去噪核大小按图像分辨率调整 blur cv2.GaussianBlur(gray, (5, 5), 0) # 自适应二值化处理光照不均比全局阈值稳 binary cv2.adaptiveThreshold( blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 25, 10 ) # 闭运算把可能断开的棋盘线接起来 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # Canny 边缘 Hough 直线检测 edges cv2.Canny(closed, 50, 150) lines cv2.HoughLinesP(edges, 1, np.pi / 180, threshold80, minLineLength80, maxLineGap10) if lines is None: return [], img # 按角度分成水平线和垂直线 horizontal, vertical [], [] for line in lines: x1, y1, x2, y2 line[0] angle abs(np.arctan2(y2 - y1, x2 - x1)) * 180 / np.pi if angle 30: horizontal.append((x1, y1, x2, y2, angle)) elif angle 60: vertical.append((x1, y1, x2, y2, angle)) # 打印初步检测结果后续步骤做聚类 print(f水平线段: {len(horizontal)} 条, 竖直线段: {len(vertical)} 条) return horizontal, vertical, img if __name__ __main__: h_lines, v_lines, img detect_grid_intersections(board.jpg)这段代码的逻辑分四层先做图像预处理把彩色图变成干净的边缘图再用 HoughLinesP 把边缘图转成线段集合然后按线段与水平轴的夹角做粗分类夹角小于 30 度的是棋盘横线大于 60 度的是竖线最后把结果打印出来供检查。参数里有几个值得调adaptiveThreshold的blockSize取 25意思是每个像素和周围 25×25 邻域比较棋盘线细、格子大时可以调小到 15C取 10值越大对光照变化越不敏感但也会丢掉浅色棋子边缘的细节。HoughLinesP的threshold80表示线段至少要有 80 个像素点投票才被认为有效棋盘线密集时调低到 50 能找回更多短线段但要小心把棋子上的纹理也当成线。minLineLength80过滤掉短碎片值根据棋盘大小按比例定。2.3 直线聚类与透视矫正从线段到规矩的棋盘粗分类后的横竖线段数量远超 19 条因为棋盘线被检测成了重叠的多段。下一步要做聚类把位置接近、方向一致的线段合并成一条代表线目标是横、竖各有 19 条左右加上可能的边框线。def cluster_lines(lines, axishorizontal, gap15): # 按线段的中心位置排序 if axis horizontal: lines.sort(keylambda l: (l[0][1] l[0][3]) / 2) else: lines.sort(keylambda l: (l[0][0] l[0][2]) / 2) clusters [] current [lines[0]] for line in lines[1:]: if axis horizontal: center (line[0][1] line[0][3]) / 2 last_center (current[-1][0][1] current[-1][0][3]) / 2 else: center (line[0][0] line[0][2]) / 2 last_center (current[-1][0][0] current[-1][0][2]) / 2 if abs(center - last_center) gap: current.append(line) else: clusters.append(current) current [line] clusters.append(current) # 每组取平均位置作为一条代表线 reps [] for cluster in clusters: xs, ys [], [] for line in cluster: x1, y1, x2, y2 line[0] xs.append(x1); xs.append(x2) ys.append(y1); ys.append(y2) reps.append((int(np.mean(xs)), int(np.mean(ys)), int(np.mean(xs)), int(np.mean(ys)))) return reps这个函数按线段中心位置排序后做间距聚类gap15表示间距在 15 像素以内的线段都归为一组取平均坐标作为代表线。聚类完的代表线横线按 y 坐标排序、竖线按 x 坐标排序取前 19 条或间距均匀的 19 条就能得到棋盘网格。网格有了但照片往往是斜着拍的直接用原始图的交点坐标去映射棋子会有透视畸变。我一般会再做一步透视矫正用最外围的交点作为棋盘四个角用cv2.getPerspectiveTransform把棋盘区域透视变换成正方形这样后续棋子的位置判断全部在正视视角下进行准确率高很多也更适合在答辩时展示「矫正前后对比」的效果。这一步在棋盘略倾斜时是救命操作建议无论如何都加上。3. 棋子识别霍夫圆加颜色判定组合拳才是关键3.1 为什么用霍夫圆检测而不是轮廓检测定位到棋盘之后棋子识别就变成了「在棋盘区域内找圆」。OpenCV 里找圆有两条路轮廓检测cv2.findContours加cv2.minEnclosingCircle以及霍夫圆检测cv2.HoughCircles。我一开始用的是轮廓法后来在真实棋盘上翻车了——棋子挨得近时轮廓会粘连在一起minEnclosingCircle会把两个棋子包成一个圆棋子表面反光时高光区域又会把圆分成几块轮廓根本闭合不了。霍夫圆检测的原理决定了它更适合这个场景它遍历边缘点投票圆的完整性要求不像轮廓法那么苛刻即使有遮挡、有反光只要有足够多的圆弧边缘点就能检出圆。代价是参数敏感param1和param2一个调不好要么漏检要么满屏都是假圆。我的经验是先用 HSV 颜色空间把黑白棋子分离成两个独立的二值图再分别对每个图做霍夫圆检测。这样黑白棋互不干扰霍夫圆的参数也更好定。3.2 黑白分离与霍夫圆检测核心代码def detect_stones(board_img, intersections): hsv cv2.cvtColor(board_img, cv2.COLOR_BGR2HSV) # 黑子低亮度低饱和度 lower_black np.array([0, 0, 0]) upper_black np.array([180, 255, 80]) mask_black cv2.inRange(hsv, lower_black, upper_black) # 白子高亮度低饱和度白子反光时会偏亮蓝/亮灰饱和度仍不高 lower_white np.array([0, 0, 150]) upper_white np.array([180, 60, 255]) mask_white cv2.inRange(hsv, lower_white, upper_white) stones [] for mask, color in [(mask_black, black), (mask_white, white)]: # 中值滤波去噪保留边缘 filtered cv2.medianBlur(mask, 5) circles cv2.HoughCircles( filtered, cv2.HOUGH_GRADIENT, dp1.2, minDist30, param1100, param222, minRadius8, maxRadius25 ) if circles is not None: for x, y, r in circles[0]: stones.append((x, y, r, color)) # 按圆心距离去重两个检测结果圆心小于半径一半视为同一棋子 unique [] for stone in stones: is_dup False for exist in unique: if (stone[0] - exist[0]) ** 2 (stone[1] - exist[1]) ** 2 (exist[2] * 0.5) ** 2: is_dup True break if not is_dup: unique.append(stone) return unique这段代码的核心思想是「分而治之」。先定义黑白的 HSV 范围黑子用亮度上限 80 圈定白子用亮度下限 150 圈定。这里有个坑白棋在强光下中心会过曝亮度超过 250但饱和度依然很低所以白子的饱和度上限设到 60只按「低饱和 高亮度」去筛。黑棋的问题相反木纹棋盘的反光区亮度可能超过 100所以黑子检测时要在棋盘区域内部做把背景排除在外。HoughCircles参数里minDist30保证两个圆心距离小于 30 像素的圆不会同时出现这个值要略大于棋子的直径。param1100是 Canny 边缘检测的高阈值边缘不清晰时可以降到 80。param222是累加器阈值这个是最难调的参数——调小了满屏误检调大了漏检我的经验是先在 20~30 之间用二分法试观察哪一档能稳定检出所有棋子。去重那段代码很重要因为黑白 mask 在棋子颜色临界处可能同时命中同一个棋子被检出两次用圆心距离合并能避免输出矩阵里的重复落子。3.3 把棋子映射到十九路坐标系最近邻和边界处理得到棋子圆心坐标后要把它映射到棋盘交叉点上。这一步用的是最近邻匹配遍历之前算出的全部 19×19 交点找到圆心距离最近的那个交点就认为棋子落在那里。def stones_to_matrix(stones, intersections, board_size19): # intersections: {(row, col): (x, y)} 字典 matrix [[0 for _ in range(board_size)] for _ in range(board_size)] for sx, sy, r, color in stones: best_dist float(inf) best_key None for (row, col), (ix, iy) in intersections.items(): dist (sx - ix) ** 2 (sy - iy) ** 2 if dist best_dist: best_dist dist best_key (row, col) if best_key: row, col best_key # 距离超出安全范围则丢弃避免误判 if best_dist (r * 1.2) ** 2: matrix[row][col] 1 if color black else 2 return matrix矩阵里我用 0 表示空点1 表示黑子2 表示白子。best_dist (r * 1.2) ** 2这个阈值条件是为了防止半径过大的误检圆被硬塞到交点上——如果检测出的圆圆心离所有交点都太远说明它大概率不是棋子。这个安全距离要考虑棋子半径棋盘上贴边的棋子圆心会偏向棋盘内侧1.2倍半径通常能覆盖这种偏移。4. 避坑指南光照、反光和木头纹理让识别翻车的四种典型场景4.1 棋盘边框与棋子颜色相近导致误检现象识别结果里棋盘外框上出现一堆假棋子尤其是深色木质边框和黑棋检测的 mask 重叠严重。原因木质棋盘边框本身的亮度在 HSV 中与黑棋的亮度范围有交集inRange把边框也当成了黑子。解决在做棋子识别前先用棋盘定位得到的网格范围裁剪出棋盘内部区域只对网格内部的 ROI 做检测。具体做法是取最外侧交点围成的多边形用cv2.fillPoly生成 mask 后与黑白 mask 做bitwise_and。这样边框天然被排除也顺带解决了棋盘外其他物体的干扰。4.2 反光让白子变成黑子现象同一颗白棋在照片中心时识别正常移到窗户边时被识别成黑棋或者干脆漏检。原因白棋表面反光后局部亮度跌到 80 以下恰好落入黑子的亮度范围。解决不要只用亮度区分黑白。白棋反射的是环境光色相偏冷且饱和度低黑棋的 RGB 三通道值接近且都很小。我建议改为「亮度 饱和度」联合判定并加一个后验修正在某个交点附近同时检出了黑子和白子时对比两个圆的平均灰度值取灰度更高的那个。这个后验规则虽然朴素但在实际照片里能把误判率降低 80% 以上。4.3 HoughLinesP 线段断裂和重复线段导致网格错乱现象网格交点出现成片的偏移或者横线竖线数量远远多于 19 条聚类后棋盘歪斜。原因光照不均导致单条棋盘线被检测成多段短线聚类时因为间距偏差被分到了不同组或者两条相邻的棋盘线因为闭运算核太大被粘连成一条。解决一个是调闭运算的核大小棋盘在画面中占的面积越大核可以越大但不要超过格子间距的一半。另一个是聚类后要做异常剔除算所有相邻代表线的间距如果某条线与相邻两条线的间距比平均值小一半或大 1.5 倍果断丢弃或合并。这条规则我写成了一段独立的小函数每次聚类完自动跑一遍能省掉大量手工调参时间。4.4 贴边棋子漏检和圆心偏移现象棋盘边缘的棋子经常漏检或者落在九宫星位附近的棋子圆心偏移到网格线中间。原因贴边棋子有部分轮廓在棋盘区域外裁剪 ROI 时被切掉了一半圆弧霍夫圆投票不够圆心偏移则是因为相邻棋子靠太近霍夫圆把邻近棋子的弧线也纳入了投票。解决裁剪 ROI 时往外扩 10~15 个像素确保贴边棋子完整圆心偏移则靠前面说的最近邻映射加距离阈值来处理只要偏移量不超过半径的 1.5 倍最近邻匹配都能找对交点。血泪经验是不要在霍夫圆阶段强行追求完美圆心那是治标不治本把容错放在映射阶段的效果更好。5. 工程化交付从能跑通的脚本到能交作业的项目5.1 模块化结构与主流程单文件脚本能跑但作为毕业设计或课程设计交付代码结构至少要对得起「项目」两个字。我见过太多人把几百行代码堆在一个main.py里答辩时老师一问「流程怎么组织的」就讲不清。合理的划分是三段式图像预处理与棋盘定位、棋子检测、局面生成与结果展示。# main.py 主流程示例 import cv2 from board_detector import BoardDetector from stone_detector import StoneDetector from game_state import GameState def main(img_path): # 1. 定位棋盘与交点 detector BoardDetector() board_img, intersections detector.detect(img_path) # 2. 检测棋子 stone_det StoneDetector() stones stone_det.detect(board_img, intersections) # 3. 生成 19x19 局面矩阵 state GameState() matrix state.build_matrix(stones, intersections) # 4. 可视化并保存结果 viz detector.visualize(board_img, intersections, stones) cv2.imwrite(result.jpg, viz) return matrix if __name__ __main__: matrix main(test_boards/board_01.jpg) for row in matrix: print(row)BoardDetector负责读图、预处理、棋盘定位、透视矫正、输出交点字典StoneDetector负责黑白分离、霍夫圆检测、去重GameState负责把棋子映射成交点矩阵。每类的接口尽量只暴露一个公开方法参数集中在类的__init__里统一配置这样调参时只需要改一处。主程序的逻辑一眼能看明白答辩时直接对着这个四步流程讲每张图对应一个类的职责讲完老师基本不会再追问代码组织。5.2 参数调优策略与测试集设计OpenCV 项目最容易让人觉得「玄学」的地方就是参数。说实话参数确实需要慢慢试但它有章法。我总结了一套固定的调参顺序先修分辨率、再修棋盘线、最后修棋子。分辨率统一缩放到棋盘最长边 1000 像素左右这样 Hough 参数在不同图片间的一致性最好。棋盘线相关的参数按「闭运算核 → Canny 阈值 → HoughLinesP 阈值 → 聚类间距」这个顺序调其中聚类间距影响最大。参数推荐范围作用调参信号GaussianBlur 核3~7去噪棋盘线断裂调小棋盘线成团调大闭运算核3~9接线线断裂调大相邻线粘连调小HoughLinesP threshold50~100线段长度阈值漏线调小噪声线调大HoughCircles param218~35圆检出灵敏度漏检调小误检调大HSV 亮度上下限按环境调黑白分界反光误检时收窄范围测试集我建议建一个test_boards目录至少放 10 张图覆盖下面几类正常光线下的完整棋局、开局的稀疏布局、终局的密集棋盘、略有倾斜视角的棋盘、有反光的白子碎片、有无棋子对照的空棋盘。每一类都跑一遍记录误检数和漏检数形成一个简单的测试表格。这个表格在答辩时的说服力比任何口头描述都强也是后续调参的基准——改一次参数就跑全量测试集确保 A 类图改好了 B 类图不会变差。5.3 文档与演示答辩怎么讲清楚毕设文档的核心不是罗列代码而是把「为什么这么设计」讲清楚。我写文档时的骨架是问题定义输入输出、方案选型为什么传统图像处理而不是深度学习、流程拆解每步的输入输出和中间结果图、参数分析每个关键参数的影响、结果展示测试集对比表、局限性与改进方向。流程图直接用 OpenCV 处理后的中间结果图来做每一步配一张图从原图、二值图、边缘图、直线图、交点图到棋子标注图老师的注意力一下子就被抓住了。演示时最忌讳上来就跑程序。我会先讲测试集的覆盖面再挑三张反差大的图现场跑一张正常图、一张零散棋局、一张密集终局。正常图标定基线零散棋局展示漏检控制能力密集棋局展示最近邻映射的抗干扰能力。程序的输出不是直接打印数组而是画在棋盘图上再加一个「输出矩阵」的小窗口对比先让老师看到图上的结果再给矩阵接受度完全不一样。6. 进阶用落子前后差分和对称性做识别自检方案跑通后还有一步值得做让识别结果能自检。我养成的习惯是用「落子前后差分」来做增量验证——连续两帧画面里大部分棋盘区域应该完全不变只有刚落下的那一颗棋子变化。这个特性可以用来纠正单帧识别的错误把上一帧的矩阵和当前帧对比如果同时有多个交点状态变化或者变化点的位置离棋盘中心区域过远就判定当前帧识别异常回退到上一帧的结果。def verify_frame_change(prev_matrix, curr_matrix, tolerance2): diffs [] for i in range(19): for j in range(19): if prev_matrix[i][j] ! curr_matrix[i][j]: diffs.append((i, j, prev_matrix[i][j], curr_matrix[i][j])) # 合法落子只改变一个点允许边缘检测抖动设 tolerance if len(diffs) 0: return True # 无变化正常 if len(diffs) tolerance: return True # 变化极少正常 return False # 变化过多可能漏检或误检另一个自检思路是利用围棋规则的对称性。一盘合法的围棋对局中黑棋数量和白棋数量的差不会超过 1因为黑白轮流落子同时被完全包围且没有气的棋子理论上会被提掉不应该出现在局面里。这两个规则虽然不能覆盖所有错误但能抓住最明显的两类翻车大面积漏检导致某一方棋子数明显偏少以及密集棋局中重复检测导致同一交点出现黑白双色。这两步自检代码量不大但放进答辩演示效果很好——别的组在展示静态识别结果你展示了「识别异常自动报警」项目完整度完全不是一个量级。回头再看这套基于 Python 和 OpenCV 的方案真正的核心竞争力不在某个玄学参数而在流程设计和自检机制。希望这篇笔记能帮你在做课设和毕设时少走几个弯路尤其是那些我踩过的坑——先找棋盘再做棋子、反光要靠饱和度兜底、贴边棋子不要硬修霍夫参数——记住这几点你大概率一次就能把流程跑通。本文还有配套的精品资源点击获取
返回列表