
简介基于Python与OpenCV搭建的围棋棋盘棋子识别项目融合图像处理与计算机视觉技术专为毕业设计、课程设计及项目开发场景准备。项目针对重难点进行拆解包含棋盘定位、棋子检测与黑白分类等关键模块从图像采集到结果输出形成完整流程既有可直接运行的Python源码也有详细说明文档适合计算机视觉方向的学生参考与二次扩展。压缩包共40个文件其中37个图像样本用于测试与验证2个Python脚本分别承担主程序和辅助测试1个Markdown说明文档介绍使用方式整体约4.83MB目录结构清晰便于对照学习。目前已有280人浏览/学习。源码经过严格测试可在此基础上延伸实现棋子计数、落子识别等高级功能文档梳理了识别流程与使用方法配合多样图片样本能帮助快速理解OpenCV在围棋场景中的落地方式也便于直接用于课程汇报或毕设演示。1. 基于 Python 与 OpenCV 的围棋棋盘棋子识别到底在解决什么问题拿到这个标题时我第一反应是这是个典型的“图像处理综合项目”把一张围棋棋盘的俯拍照片变成一行行结构化数据——棋盘上哪里落了一颗黑子哪里落了一颗白子。基于 Python 与 OpenCV 的围棋棋盘棋子识别核心就两件事定位棋盘网格再识别网格交叉点上的棋子颜色。它的适用范围很明确毕业设计、课程设计、入门级图像处理项目开发。相比用深度学习检测棋子这条纯 OpenCV 的路线有天然优势——每步都有中间结果可看、参数可调、答辩时能讲清楚原理而且在一台普通笔记本上跑实时视频流也毫无压力。我自己做过类似的棋类识别项目最深的体会是这题目难度不在“会不会用函数”而在“参数和边界条件”。棋盘反光、棋子阴影、透视变形随便一个都能让识别率掉一半。所以这篇笔记不打算铺开讲 API而是按“棋盘定位 → 棋子检测 → 坐标映射 → 颜色判断 → 踩坑排查”这条完整流程给出能直接跑的代码和参数依据。2. 方案选型与棋盘定位先让程序找到棋盘再谈棋子识别2.1 为什么选传统 CV 方案而不是直接上深度学习做围棋棋子识别常见的技术路线有三条纯传统 CVOpenCV 的直线检测、圆检测、传统 CV 定位棋盘 小分类器判棋子、端到端的目标检测YOLO 等。我一般会建议学生在课程设计和毕业设计阶段优先选第一条理由很实际深度学习方法在“棋子检测”这个任务上精度确实更高但你需要一块能跑训练的显卡、一份围棋棋子的标注数据集还要花时间调训练参数。而传统 CV 方案的数据集就是你拿手机拍几张照片甚至可以用程序合成带棋盘的图片来调通流程。从答辩角度讲霍夫变换检测直线、霍夫梯度法检测圆每一个环节都是图像处理课程里学过的知识点老师问起来你有东西可讲。深度学习的黑匣子反而容易在答辩时露怯。我并不是说深度学习方案不行。如果你后续想往工程化方向走或者要处理视频流中的运动模糊、遮挡那换 YOLO 是合理的。但作为“毕业设计课程设计项目开发”这个定位先用传统 CV 把完整流程跑通再在某个环节替换成深度学习做对比实验是性价比最高的做法。2.2 棋盘定位实现从轮廓检测到透视校正棋子识别的前提是棋盘网格要“正”。现实中拍棋盘很难做到完全俯拍多少带点透视导致棋盘上的圆看起来是椭圆。所以流程第一步是把图片中的棋盘区域抠出来做透视变换矫正成正方形。import cv2 import numpy as np src cv2.imread(go_board.jpg) gray cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) blur cv2.GaussianBlur(gray, (5, 5), 0) edges cv2.Canny(blur, 50, 150) contours, _ cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours sorted(contours, keycv2.contourArea, reverseTrue) board_pts None for cnt in contours[:5]: peri cv2.arcLength(cnt, True) approx cv2.approxPolyDP(cnt, 0.02 * peri, True) if len(approx) 4: board_pts approx.reshape(4, 2).astype(np.float32) break if board_pts is None: raise ValueError(没有找到四边形棋盘区域)这段代码做了四件事转灰度、高斯模糊降噪、Canny 边缘检测、找最大外轮廓。核心参数有两个。Canny 的两个阈值 50 和 150表示梯度强度低于 50 的像素直接丢弃高于 150 的认定为强边缘介于两者之间且与强边缘连接的才保留。光照强、阴影重时把低阈值调到 80 左右能减少棋盘格内木纹产生的碎边缘。approxPolyDP的0.02 * peri是逼近精度数值越小多边形越接近原始轮廓数值越大越容易把弯曲边缘拟合成直线。识别不到的常见原因是棋盘边缘和背景对比度太低这种情况先加大 Canny 低阈值再看轮廓排序逻辑是否把桌面边框当成了棋盘。拿到棋盘四角后下一步是透视校正。这里有个关键坑approxPolyDP返回的四个点顺序不固定可能是左上、右上、右下、左下也可能是右下、左上、左下、右上。直接把四点和目标矩形四点对齐会出现棋盘被旋转或镜像的怪结果。我通常先按“左上 → 右上 → 右下 → 左下”的顺序排序def order_points(pts): s pts.sum(axis1) diff np.diff(pts, axis1).ravel() tl pts[np.argmin(s)] br pts[np.argmax(s)] tr pts[np.argmin(diff)] bl pts[np.argmax(diff)] return np.array([tl, tr, br, bl], dtypenp.float32) board_pts order_points(board_pts) W, H 760, 760 dst_pts np.array([[0, 0], [W - 1, 0], [W - 1, H - 1], [0, H - 1]], dtypenp.float32) M cv2.getPerspectiveTransform(board_pts, dst_pts) warped cv2.warpPerspective(src, M, (W, H))排序原理很简单sum(axis1)最小的是左上角最大的是右下角np.diff是 x 减 y 的差值左上角 x 小于 y差值最小所以取argmin那个点作为右上角。透视变换矩阵M把任意四边形的棋盘区域映射到一个 760×760 的正方形画布输出尺寸建议设成能被 18 整除的数这样后面算网格间距时不会出现小数点错误。这里W-1、H-1是为了避免像素索引越界属于 OpenCV 绘图的常见细节。2.3 棋盘网格提取霍夫直线检测与轮廓法的取舍棋盘定位还有一种常见做法不找四边形轮廓而是用霍夫直线检测找横竖网格线再求直线交点。对于背景干净、棋盘线清晰的图片霍夫直线法很直接但如果桌面上有纹路、棋盘周围有文字检测出的直线会混入大量噪声过滤起来非常麻烦。我的做法是把两种方法结合先用轮廓法拿到棋盘四边并做透视校正在校正后的正方形图上再跑霍夫直线检测来获取精确的网格间距。这样霍夫直线只需要在棋盘内部工作干扰少得多。轮廓法负责“粗定位”直线法负责“细定位”各干各的活调试也方便。3. 棋子识别HoughCircles 的三组核心参数与漏检补检3.1 HoughCircles 参数解析棋盘矫正成正方形后棋子检测就变成在固定区域内找圆。OpenCV 里最常用的是HoughCircles但很多初学者在这里翻车同样的代码换张角度稍微偏一点的照片就一个圆都检测不到。问题基本出在参数上尤其是param2和minDist。roi_gray cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) roi_gray cv2.medianBlur(roi_gray, 5) circles cv2.HoughCircles( roi_gray, cv2.HOUGH_GRADIENT, dp1, minDist25, param1120, param228, minRadius8, maxRadius20 ) if circles is not None: circles np.round(circles[0]).astype(int) for x, y, r in circles: cv2.circle(warped, (x, y), r, (0, 255, 0), 2)这里有一个影响成败的细节HoughCircles之前要先用medianBlur而不是GaussianBlur。中值滤波对椒盐噪声和棋子表面的高光点有奇效能直接干掉一批虚假圆高斯滤波做不到这点。参数方面dp1表示累加器分辨率与原图一致调成 2 会让检测变快但小圆容易丢。minDist25是两个圆心之间的最小距离棋盘上相邻棋子的间距通常在 35 像素左右760 像素画布除 18设 25 足够防止同一个棋子被检测成多个圆。param1120是 Canny 高阈值传给内部边缘检测用。param228是圆心累加器阈值是“一个圆至少需要多少边缘点支持”的判定值调小可以检出更多圆但误检也会变多。minRadius和maxRadius要根据棋盘尺寸和棋子大小比例来定通常把棋子直径设为两相邻网格间距的 0.8 倍左右。这套参数在室内均匀光照下识别率能到 90% 以上。如果你拍的图是斜 30 度角透视校正后棋子会轻微变形可以把param2降到 22同时把minRadius调小一号让检测更激进后面再用轮廓圆度过滤噪声。3.2 漏检补检轮廓圆度过滤与固定半径模板匹配HoughCircles 不是万能的棋盘上棋子排布紧密时minDist设大了漏检设小了把相邻棋子连成一个大圆。另一种更稳的做法是先用findContours找所有封闭轮廓再用圆度指标过滤。contours, _ cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: area cv2.contourArea(cnt) if area 80 or area 600: continue perimeter cv2.arcLength(cnt, True) circularity 4 * np.pi * area / (perimeter * perimeter) if circularity 0.8: (x, y), radius cv2.minEnclosingCircle(cnt) cv2.circle(warped, (int(x), int(y)), int(radius), (0, 255, 0), 2)圆度的计算公式是4π面积/周长平方正圆的圆度等于 1形状越不规则值越小。取 0.8 作为阈值能过滤掉棋盘网格交叉线产生的三角形小块以及棋子阴影形成的不规则轮廓。面积阈值 80 到 600 需要按实际画布尺寸缩放760×760 的画布上棋子半径约 12 像素面积约 450留出上下余量是合理的。实际项目中我会让 HoughCircles 和轮廓圆度检测同时跑两份结果做并集。HoughCircles 在棋子边缘模糊时容易漏检轮廓法在这种情况下反而更稳因为 Canny 边缘对棋子内部的纹理不敏感。并集之后再去重两个圆心的距离小于 8 像素视为同一个漏检率能降到 5% 以内。4. 坐标映射与棋子颜色判断从像素到十九路盘面4.1 像素坐标转棋盘点位检测到棋子圆心后下一个核心问题是如何把像素坐标对应到围棋 19×19 盘面的交叉点上。围棋的走棋记录通常写成“S16”“C7”这种字母加数字的坐标课程设计大多不需要做这么完整但至少要把 (x, y) 像素坐标换算成行列号。由于前面已经做了透视校正棋盘被映射成了一个规则的 760×760 正方形网格间距是(760 / 18)像素。棋盘点位计算的代码很简单def pixel_to_grid(x, y, n19, margin20): # margin 是棋盘最外边线到画布边缘的留白 if x margin or x W - margin or y margin or y H - margin: return -1, -1 step_x (W - 2 * margin) / (n - 1) step_y (H - 2 * margin) / (n - 1) col int((x - margin) / step_x 0.5) row int((y - margin) / step_y 0.5) return row, col这里有个新手很容易搞混的地方n19但这个 19 是交叉点数不是格子数。19 路棋盘有 19 个交叉点间距数量是 18所以分母是n-1而不是n。如果不小心把分母写成 19整盘棋子的坐标都会偏移半格越靠边越明显这是我做项目时真实踩过的坑。margin的取值要跟透视变换时棋盘四角的选取对应。如果在提取棋盘轮廓时四边形刚好卡在整块木板的外边线上那四边最外侧的交叉点距离边缘还有一小段距离margin 取 20 像素是合理的。如果四边形紧贴最外侧交叉点margin 应该设成 0。我建议在预处理阶段就固定“轮廓取外框、margin 取 20”的策略避免每次都重新量。绘制网格点可以直观检查映射是否正确for row in range(19): for col in range(19): x margin col * (W - 2 * margin) / 18 y margin row * (H - 2 * margin) / 18 cv2.circle(warped, (int(x), int(y)), 3, (255, 0, 0), -1)把检测到的棋子圆心和最近网格点的距离打印出来如果大部分距离小于 5 像素说明定位环节是准的。如果系统性偏移半个格距优先怀疑n-1写错其次是margin值不对。4.2 黑白子判定局部掩膜均值比全局阈值更抗光照干扰棋子颜色判断看起来简单——取圆心像素的灰度值深色是黑子浅色是白子。但真实拍摄场景里棋盘木纹颜色有深有浅白子受反光影响可能局部发灰黑子在强光下也可能拍出亮斑。用全图统一阈值切很快会翻车。我一般用局部掩膜方式以圆心为中心、半径略小于棋子半径的圆内取所有像素的加权亮度均值再和一个亮度阈值比较。def stone_color(warped_bgr, center_x, center_y, radius12): mask np.zeros((H, W), dtypenp.uint8) cv2.circle(mask, (int(center_x), int(center_y)), radius, 255, -1) mean_val cv2.mean(warped_bgr, mask)[:3] brightness 0.114 * mean_val[0] 0.587 * mean_val[1] 0.299 * mean_val[2] return black if brightness 100 else white亮度公式用的是 OpenCV 内置的 RGB 转灰度权重B 通道权重 0.114、G 通道 0.587、R 通道 0.299。人为调用这个公式而不是直接cv2.cvtColor是为了只计算掩膜内部区域的均值cvtColor是对整张图操作。阈值 100 是一个经验值它在 0 到 255 的亮度标尺中间偏暗。室内光照均匀时黑子亮度通常在 50 到 80白子在 180 到 220中间有足够余量。但如果你使用的是深色木质棋盘空交叉点的木纹亮度可能在 90 到 110这时黑子和空点容易混淆。我的处理办法是把“棋子是否存在”和“棋子颜色”做成两个判断先用掩膜区域的标准差判断有没有棋子标准差大说明该区域有高对比度的边缘属于棋子边界再判断颜色。空交叉点区域亮度均匀、标准差小有棋子的区域标准差大。还有一个在强反光下更稳的技巧把 BGR 图转成 HSV只看 V明度通道。白子的 V 值即使反光也保持在 200 以上黑子的 V 值即使被环境光照亮也难超过 120两个类别的分布重叠更小。5. 常见问题与避坑排查环境、定位、检测三个层面的真实翻车记录5.1 环境配置装好 OpenCV 却 import 失败现象pip install opencv-python安装成功但在 VSCode 里运行import cv2报ModuleNotFoundError: No module named cv2。原因十个里有七个是 Python 解释器选错。电脑上装了 Anaconda、系统 Python、VSCode 内置虚拟环境pip 装到的是某个特定环境的 site-packages而 VSCode 右下角选中的解释器是另一个。还有两成情况是 pip 与 python 版本不对应用python -m pip install opencv-python才能保证装到当前 python 对应的包目录里。解决在 VSCode 里按CtrlShiftP输入 “Python: Select Interpreter”选择你 pip 对应环境。然后在终端跑python -c import cv2; print(cv2.__version__)能输出版本号就说明环境通了。我自己习惯新建一个 conda 环境专门跑图像处理项目把opencv-python、numpy、matplotlib一次装齐避免多个项目互相污染依赖。5.2 棋盘定位findContours 返回值报错或轮廓取错现象老代码contours, hierarchy cv2.findContours(...)报not enough values to unpack (expected 3, got 2)。原因这是 OpenCV 3.x 到 4.x 的经典破坏性变更。旧版findContours返回三个值新版本只返回两个。网上大量博客的代码是照着旧版写的直接复制就容易撞上。解决统一用新写法contours, _ cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)其中cv2.RETR_EXTERNAL表示只取最外层轮廓这个标志位在棋盘定位场景下是刻意选的——它能把棋盘内部纵横网格线产生的子轮廓全部排除避免轮廓排序时抓到一个小网格而不是整个棋盘。排序按area倒序后取前几个候选因为有时桌面上的纸片轮廓比棋盘大直接取第一个会锁定错误目标。另一个常见问题是桌面上有圆形纹理或者木桌边缘也被检成四边形。我的兜底策略是同时计算候选四边形的长宽比和面积占整图比例棋盘的长宽比一般接近 1面积占比通常在 30% 以上两个条件都满足才接受。5.3 棋子检测HoughCircles 漏检、误检与颜色误判现象棋盘中间和天元位置的棋子都能检测到但棋盘四角和边缘的棋子频繁漏检或者把数据棋子相互粘连成一个椭圆。原因透视校正后的棋盘边角区域通常有明显畸变残留尤其当原始照片是从斜上方 45 度拍的时候。边角棋子成像时已经接近椭圆HoughCircles 的梯度累加器对这种变形的圆敏感度降低。另外拼接在一起的同色棋子之间边缘梯度弱两个相邻圆心的距离小于minDist时累加器会把它们当作同一个圆心。解决两个方向配合。第一拍照时尽量让手机正对棋盘中心减少透视角度这是最省事的办法。第二检测策略上把minDist设为相邻棋子间距的 0.6 到 0.7 倍给轻微变形的圆留余量边角区域单独切 ROI对每个 ROI 单独跑HoughCircles并用更低的param2阈值检测完再合并去重。颜色误判的翻车则大多发生在暖色灯光下白子被照成淡黄色RGB 三个通道的均值整体偏高按单一亮度阈值切还勉强能用但如果是日光灯加窗外自然光的混合光源最好用 HSV 的 V 通道配合色相做二次判断。6. 让结果可验证、可展示评估脚本与SGF导出课程设计和项目开发的区别在于后者需要让用户相信你的识别结果是可信的。我做完识别后不会只看反馈图而是写一个评估脚本对着人工标注的答案逐点比对输出准确率、漏检率、误检率三个数字。这个数字在验收和答辩时特别好用比“看起来识别得不错”有说服力得多。评估脚本的做法是把每张测试图的真实棋谱存成一个文本文件每行一个坐标点加颜色比如3,15,b、7,10,w。程序跑完识别后把所有识别结果也存成同样格式两个文件做集合对比。def evaluate(truth_path, pred_path): def load(path): points set() with open(path, r) as f: for line in f: line line.strip() if not line: continue parts line.split(,) points.add((int(parts[0]), int(parts[1]), parts[2])) return points truth load(truth_path) pred load(pred_path) hit len(truth pred) precision hit / len(pred) if pred else 0 recall hit / len(truth) if truth else 0 return precision, recall这里的truth pred利用的是 Python 集合的交集运算。要是更严格一点棋子检测存在“圆找到了但坐标偏了一格”的情况此时按精确坐标比对会误判为漏检可以在比较前把每个预测坐标映射到最近的网格点上再做匹配。更进一步我会把识别结果导出为 SGF 格式。SGF 是围棋棋谱的标准文件格式几乎所有围棋软件都能直接打开。导出格式很简洁def export_sgf(moves, output_path): # moves 元素格式: (row, col, color)color 为 b 或 w with open(output_path, w) as f: f.write((;GM[1]SZ[19]\n) for i, (row, col, color) in enumerate(moves, start1): # A 对应第一列行列从 0 开始 col_char chr(ord(A) col) f.write(f;{color}[{col_char}{19 - row}]) if i % 5 0: f.write(\n) f.write()\n)SGF 坐标的字母序列里 I 不存在为了不和数字 1 混淆但 0-18 列映射到 A-T 后会跳过 I。如果项目做到了这一步等于把“识别”和“应用”打通了——识别出的棋盘点位可以导入任何围棋软件复盘这已经超出普通课程设计的要求作为项目开发的亮点是足够的。我自己的习惯是每调完一组参数就把测试图和标注结果一起存档命名带上参数摘要比如param2_28_mindist_25.jpg。回看时能直观看到参数对结果的影响也方便向别人说明“我调参是有依据的”而不是拍脑袋。做这类图像项目最怕的就是陷入“调一个参数、全图变个样”的玄学状态把每一次实验都留档等于给自己一颗后悔药。希望这篇笔记能帮你把这条识别流水线跑通少走我走过的弯路。本文还有配套的精品资源点击获取