OpenCV围棋棋盘棋子识别:从照片到SGF棋谱的完整实现
发布时间:2026/9/28 2:30:43来源:尧图网络
简介这是一套基于 Python 与 OpenCV 实现的围棋棋盘棋子识别项目定位服务于毕业设计、课程设计以及个人项目开发。代码经过严格测试可直接运行也便于进一步定制配套文档梳理了图像预处理、棋盘网格定位、棋子检测与状态判断等关键环节能帮助读者理解原理并按需修改。资源压缩包共 40 个文件总体积约 4.83MB主要包含 33 张 JPG 测试图片、4 张 PNG 示例图、2 个 Python 主程序和 1 份 Markdown 说明文件图片素材覆盖不同棋局场景脚本与文档协同呈现完整实现逻辑。当前已有 280 人浏览学习适合需要快速搭建视觉识别原型或完成课程设计、毕业设计的计算机视觉方向读者。通过研读项目源码可以掌握 OpenCV 在棋盘分割、轮廓提取和棋子识别中的典型应用并可迁移到其他棋类或目标检测项目中。1. 围棋棋盘棋子识别从一张照片到结构化棋谱坐标如果现在给你一张手机拍的围棋棋盘照片你能在几分钟内把一个一个棋子的黑白和位置精确读出来吗基于Python及OpenCV的围棋棋盘棋子识别做的就是这件事把19路棋盘上的交叉点、棋子颜色和空点从图像转成结构化的坐标数据直接对接棋谱自动记录、AI复盘和围棋教学辅助。这套做法的价值在于链路清晰、效果可直观展示既适合作为毕业设计和课程设计的主攻方向也能当成项目开发里的算法骨架。不需要专属硬件一台手机加一台电脑就能跑通接下来就沿着环境搭建、棋盘检测、棋子识别、源码组织、避坑和验证这条线讲透。2. 环境与图像预处理把棋盘照片变成干净的检测输入2.1 环境搭建Python与OpenCV的最小可用配置OpenCV这个库装起来很容易踩坑最常见的是装完import直接报错因为opencv-python和opencv-contrib-python两个包混着装导致版本冲突。对棋盘识别这个任务基础版完全够用不需要contrib模块。用pip从官方源装就最省事pip install opencv-python numpy装完之后用一段最小脚本验证能打印版本号说明环境正常import cv2 import numpy as np print(OpenCV, cv2.__version__) print(NumPy, np.__version__) img np.zeros((200, 200, 3), dtypenp.uint8) cv2.imwrite(test.jpg, img)这段验证脚本里import cv2是OpenCV的Python绑定几乎所有的图像读写、滤波和变换都走这个接口NumPy提供底层数组对象图像在OpenCV里本质就是NumPy数组两者缺一不可。最后两行生成一张纯黑图片并写入磁盘相当于做一次完整的读写链路测试cv2.imwrite抛出异常时说明安装有问题。版本参数上Python建议3.8及以上OpenCV用4.x系列。尤其注意不要手动指定NumPy版本让pip自己做依赖解析就行了。如果你用的是Anacondaconda install opencv其实也可以但最忌讳的是pip和conda混着装装出两个互相覆盖的OpenCV副本到时候怎么import都不对。2.2 棋盘线检测用OpenCV检测直线与交叉点坐标实际拍照时棋盘不会正对镜头第一步要把19条横线和19条竖线找出来。这里有一个理论前提棋盘线的特征是长、直、间距近似均匀这正是OpenCV检测直线的经典场景。先做灰度化和边缘提取import cv2 import numpy as np 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)灰度化把三通道图像压成单通道计算量一下子降到三分之一高斯模糊的卷积核是(5, 5)用来滤掉传感器噪声和木纹带来的细碎边缘。Canny的两个阈值分别控制边缘的弱阈值和强阈值50/150是通用起步值照片里阴影重就把低阈值压到30木纹噪声多就把高阈值提到200。调试时千万盯住edges这张图棋盘线应该连贯清晰背景应该尽量干净这一张图的质量直接决定后续直线检测的成败。紧接着用HoughLinesP做直线检测lines cv2.HoughLinesP(edges, 1, np.pi / 180, threshold150, minLineLength200, maxLineGap30)HoughLinesP是概率霍夫变换返回每条线段的两个端点坐标比传统霍夫变换更实用。参数里最敏感的是threshold和minLineLengththreshold太小会把边缘噪声当成直线minLineLength如果超过棋盘边长的三分之一棋子挡住横线时就会断线。下面这张参数表是调试起点每个数据集都得重新标定一遍。参数起步值调小方向调大方向作用Canny threshold150检出更多弱边缘过滤低对比度噪声边缘敏感度Canny threshold2150保留渐变边缘只留强边缘边缘连续度HoughLinesP threshold150直线更易检出只保留长直线直线数量控制minLineLength200接受短线段丢弃断裂碎线棋盘线长度下限maxLineGap30线段更严格允许跨棋子接续断线合并容差获得线段集合后把它们按斜率分成横线和竖线两组再做截距聚类同一根线上的多个线段会被合并成一条完整直线for x1, y1, x2, y2 in lines[:, 0]: k (y2 - y1) / (x2 - x1 1e-6) if abs(k) 0.1: horizontal.append(((x1, y1), (x2, y2))) elif abs(k) 5: vertical.append(((x1, y1), (x2, y2)))斜率接近0的归为横线绝对值很大的归为竖线加1e-6是为了防止除零。注意如果照片旋转角度大于15度横线斜率也会明显偏离0这种情况下要先根据主方向做一次旋转校正再做分类。聚类完成后理论上横竖各19条线实际会多出几条噪声线按相邻线间距的均值把偏离过大的线剔除剩下来的就是棋盘网格。2.3 透视校正把斜拍的棋盘拉正再切网格横竖线交点算出来之后棋盘就变成一个不规则排列的19x19点阵。直接在这个点阵上切棋子区域会产生系统性偏差尤其是棋盘四角的误差会放大。所以要先把棋盘变换成正方形。src np.array([[x0, y0], [x1, y1], [x2, y2], [x3, y3]], dtypenp.float32) dst np.array([[0, 0], [size, 0], [size, size], [0, size]], dtypenp.float32) M cv2.getPerspectiveTransform(src, dst) warped cv2.warpPerspective(img, M, (size, size))getPerspectiveTransform接收四个对应点src是原始图上棋盘四个角的像素坐标dst是校正后正方形的四个角。常见做法是取棋盘最长边映射到600x600既保留棋子细节又不会让后续计算太慢。warpPerspective输出的图像横竖线接近水平和垂直后面按网格均分就不会有坐标偏差了。这里有三个容易忽略的细节第一四个角必须按左上、右上、右下、左下的顺序传入顺序错乱会导致棋盘镜像或者翻转棋子黑白判断全部颠倒第二src和dst的数据类型必须是float32用int会直接报类型错误第三如果整张图里棋盘只占中间一小块可以先放大ROI再校正避免棋盘边缘像素被重采样污染。3. 棋子检测与分类轮廓加颜色判定的实战组合3.1 棋盘点位切分按网格坐标提取圆形区域透视校正之后棋盘从第0行到第18行、第0列到第18列均匀分布每个交叉点的像素坐标可以直接用线性插值算出来size 600 # 校正后棋盘边长 cell size / 18 # 19条线之间是18个间隔 points [] for row in range(19): for col in range(19): x int(round(col * cell)) y int(round(row * cell)) points.append((x, y, row, col))这段代码生成181个交叉点坐标。cell是相邻两条线的像素间隔size600时cell大约是33像素。注意round和int的类型转换要一起用Python的round返回float直接传给OpenCV的坐标接口会报类型错误。对每个交叉点取一个小邻域作为后续判断的区域def judge_point(warped, x, y, r12): roi warped[max(0, y-r):yr1, max(0, x-r):xr1] return analyze_roi(roi)r取值在cell的0.35到0.4倍之间600边长下用12像素比较合适。取得太大相邻棋子的边缘会卷进来判断区域互相污染取得太小棋子边缘的亮色部分被裁掉黑棋很容易误判成空点。这里有个很实用的检查办法把每个点位截出来的小图拼成一张大图用眼睛过一遍立刻能确定r合不合适比盲目跑准确率快得多。3.2 黑白棋判定HSV空间的阈值设计棋子颜色判定建议用HSV空间而不是RGB因为光照变化主要集中在亮度通道而色调和饱和度相对稳定。黑棋和白棋在V通道上差异明显但直接看灰度值容易受影子干扰所以要把S和V联合起来判断def analyze_roi(roi): hsv cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) # 黑色棋子整体亮度低 black cv2.inRange(hsv, (0, 0, 0), (180, 255, 80)) # 白色棋子亮度高且饱和度低 white cv2.inRange(hsv, (0, 0, 170), (180, 60, 255)) black_ratio cv2.countNonZero(black) / (roi.shape[0] * roi.shape[1]) white_ratio cv2.countNonZero(white) / (roi.shape[0] * roi.shape[1]) if black_ratio 0.3: return B elif white_ratio 0.3: return W else: return E # empty空点两次inRange分别筛出黑色像素和白色像素再用各自占比决定当前交叉点状态。黑色阈值是V小于80白色是V大于170且S小于60这两个上下限必须根据实际照片微调。阈值0.3是参考值棋盘上本身有木纹或污渍时提到0.5环境光太强导致黑棋反光时黑色上限从80提到100但要小心别把深色棋盘木色也框进来。用区域占比判断比单点颜色可靠得多。千万不要只看交叉点那个像素的颜色——棋盘线、木纹、灰尘会频繁干扰单像素判断基本就是赌运气。占比策略天然对噪声不敏感这也是这套方案能稳定跑通的原因。3.3 空点识别黑白都不匹配就是空点有人会在空点识别上单独做一套模板匹配或者训练一个分类器实际上对19路标准棋盘来说空点的定义就是这里既没有黑子也没有白子。黑白两个比例都低于阈值自然就是空点不需要第三个分类器。但浅色棋盘配白色棋子时有一个特殊问题棋盘格子本身颜色很浅空点区域的white_ratio也可能超过阈值。这种场景下加一道圆形轮廓检查更稳gray_roi cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) contours, _ cv2.findContours(gray_roi, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(contours) 0: return E # 圆形度 4 * pi * 面积 / 周长^2接近1表示接近圆 area cv2.contourArea(contours[0]) perimeter cv2.arcLength(contours[0], True) if perimeter 0 and 4 * np.pi * area / (perimeter * perimeter) 0.5: return E先看ROI里有没有轮廓没有轮廓直接判定空点。有轮廓再算圆形度黑棋白棋投影都接近正圆圆形度大于0.5而碎木纹、污渍的不规则形状圆形度远低于这个值。把圆形度检查加在颜色判定之后相当于给浅色棋盘加了一道保险。这个技巧在实拍场景中能明显降低白棋误判率代价是每个点多了几十毫秒的计算量完全可以接受。4. 源码结构与文档配置课设和毕设怎么组织才加分4.1 代码模块划分main、detect、utils三层拿到项目源码后第一步不是读算法而是把目录结构理清楚。一个能交差的课程设计项目判断标准不是算法多深而是别人能不能快速看懂、能不能直接运行。常见的目录结构是这样board_reader/ main.py # 入口读图、串流程、输出结果 detect_board.py # 棋盘线检测与透视校正 detect_stone.py # 棋子识别与黑白分类 utils.py # 可视化、文件读写、结果保存 requirements.txt每个文件对应一条明确的职责线。main.py只做流程调度不写具体算法detect_board.py只管棋盘检测detect_stone.py只管棋子识别utils.py放跨文件使用的辅助函数。答辩时老师问一句“我想换一套棋盘图片怎么办”你能直接回答改哪个函数这是很大的加分项。main.py把整个流程串起来import cv2 from detect_board import find_board, warp_board from detect_stone import read_stones def main(image_path): img cv2.imread(image_path) warped warp_board(img) stones read_stones(warped) print(stones) return stones if __name__ __main__: main(board.jpg)函数之间用接口传递数据而不是共享全局变量意味着每个环节可以单独调试。调试这类视觉项目最忌讳只在控制台打印坐标把中间结果画到图上才直观def visualize(points, colors, warped): out warped.copy() for (x, y), c in zip(points, colors): color (0, 255, 0) if c B else (0, 0, 255) if c W else (200, 200, 0) cv2.circle(out, (x, y), 8, color, 2) cv2.imwrite(result.jpg, out)visualize函数把识别结果画成带彩色圆圈的棋盘图黑棋画绿圈、白棋画红圈、空点画黄圈。这张结果图不仅是答辩的核心素材也是定位算法问题的第一现场哪个点位的颜色画错了一眼就能看到。4.2 文档与答辩材料从需求到演示的四段式结构毕业设计和课程设计的文档要求大同小异常见结构是需求分析、设计方案、关键代码讲解、实验结果。文档里至少放三张图原始照片、棋盘线检测结果图、最终圈出所有棋子的效果图。这三张图对应算法最直观的三个中间产物评审老师看这三张图就能理解整个项目的价值。性能指标这一块要提前准备一个小测试集大概10张不同角度和光照的棋盘照片人工标注每张照片的真实局面再和代码输出对比算准确率。公式建议这样写准确率 正确识别的棋子数 / 总棋子数如果测试集里包含对局中途的盘面把阴影造成的误判照片单独抽出来说明算法边界比藏着掖着更有说服力。你在文档里主动暴露已知问题并提出改进方向评委会觉得你对项目理解是完整的。写实验记录时还有个节省时间的技巧每跑一张图把原图、边缘图、校正图、结果图都保存成文件按序号命名。答辩做PPT时直接按流程顺序贴图整个处理链路一目了然。很多同学程序写完了但讲不清楚问题就出在中间过程没有留档。4.3 用自己的照片跑通流程数据与参数的协同调整把示例代码跑通之后换上自己手机拍的照片大概率会失败。因为手机拍摄角度、光线方向和棋盘底色和示例完全不同。这时候要建立一个小数据集拿十张以上的照片反复试而不是盯着一张图死磕调参。对每张测试图记录三组信息拍摄角度是俯拍还是斜拍、光线是顺光还是逆光侧光、棋盘底色是深色还是浅色。然后按顺序调三类参数Canny的threshold1和threshold2控制边缘敏感度和连续性HoughLinesP的threshold和minLineLength控制直线检出的完整度黑白判定的HSV上下限控制棋子颜色分类调参有个固定的顺序先调棋盘线参数让网格完整再调棋子颜色参数让分类准确。顺序反了会陷入一个死循环棋盘线都是歪的棋子颜色怎么调都不对。用一个简单的脚本把参数逐组打印出来手动对比比拍脑袋乱改快得多。5. 避坑指南棋盘识别最容易翻车的六个细节5.1 光照不均导致棋子颜色误判现象棋盘一半亮一半暗暗处的黑棋被识别成空点亮处的白棋被误判成黑棋。原因固定阈值对全局亮度太敏感同一个V等于100的像素在亮区看起来是暗的在暗区看起来又是亮的阈值一刀切必然出错。解决先用CLAHE做自适应直方图均衡化再算HSV阈值。注意必须对整张校正图做不能只对单个ROI做否则相邻点之间的亮度依然不一致。clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) gray_e clahe.apply(gray)这一行预处理能解决大部分由光照产生的分类错误。如果画面里还有从窗户射进来的强光带更彻底的做法是分区域统计亮度并做局部归一化把每个点的亮度统一到同一水平再做阈值判断。5.2 HoughLinesP参数玄学threshold调低后直线满天飞现象threshold降到50之后棋盘边缘的木纹全被检测成直线横线竖线数量远超19条聚类结果乱成一团。原因概率霍夫变换对短线段特别敏感木纹在边缘图上形成大量短线段threshold一低就全被当成候选。解决不要盲目降低threshold优先提高minLineLength把短线段直接过滤掉再按斜率聚类合并共线线段。最后加一道合理性检查横线或竖线如果超过22条自动把threshold加50重新跑一遍。注意直线检测的参数不要照抄先看一眼edges边缘图再决定往哪个方向调。5.3 棋盘边缘被棋子挡住导致横线断裂现象盘面上棋子很多时横线在棋子位置断开minLineLength设得较大就检测不到完整的跨棋线。原因棋子本身有边缘把棋盘线分割成多段短线线段长度小于minLineLength就被丢弃。解决minLineLength设为棋盘边长的三分之一maxLineGap设为边长的十分之一允许线段之间存在缺口。聚类时再把共线的片段合并成一条线。如果断口太多用19路等距的先验做拟合补全把断裂的位置按相邻间距推算出来。5.4 白棋和浅色棋盘颜色相近现象浅色榧木棋盘上白棋整体丢失大量位置被识别成空点黑棋偶尔也受影响。原因白棋和浅色背景在RGB和HSV下的灰度分布高度重叠颜色阈值无法区分它们。解决不依赖颜色阈值改用圆形轮廓检查。白棋是凸起的圆球体边缘会形成一圈深色阴影轮廓用边缘检测找圆形比找颜色可靠得多。这就是第3章末尾加圆形度检查的动机。实拍时如果棋盘纹理特别重还可以先把灰度图做形态学闭运算把木纹填充掉再做轮廓提取。5.5 透视变换角点顺序导致棋盘镜像现象透视校正后的棋盘左右颠倒或者上下颠倒坐标全乱黑白棋位置对不上。原因getPerspectiveTransform要求四个角点按左上、右上、右下、左下的顺序传入而轮廓检测返回的角点顺序是任意的直接拿去做变换必然出错。解决增加一次排序先按y坐标把四个点分成上下两组每组内部按x坐标排序得到左上、右上、左下、右下再调用变换函数。这段排序逻辑要抽成独立函数def order_points(pts): pts sorted(pts, keylambda p: p[1]) top, bottom pts[:2], pts[2:] top sorted(top, keylambda p: p[0]) bottom sorted(bottom, keylambda p: p[0]) return np.array([top[0], top[1], bottom[1], bottom[0]], dtypenp.float32)这个函数是透视校正环节的后悔药写一次就能避免反复踩同一个坑。用自动角点检测时尤其依赖它因为检测结果很少恰好符合OpenCV的输入顺序要求。5.6 相机畸变导致棋盘四周的棋子偏移现象棋盘中心的棋子识别全部正确四角和边缘的棋子总是往某个方向偏一个像素识别坐标渐渐对不上真实位置。原因手机镜头在小广角下存在桶形畸变棋盘拍得越大边缘畸变越明显。解决如果棋盘在画面中占比不超过一半畸变可以忽略如果棋盘几乎满屏就要用cv2.undistort校正镜头畸变这需要先用标定板拿到相机内参。低成本替代方案是只识别棋盘中心区域边缘留出安全距离丢弃最后再做坐标补偿。6. 进阶把识别结果导出SGF棋谱并用对局验证坐标识别只是中间产物真正让项目完整的是把结果转成标准SGF棋谱这样可以直接导入围棋软件复盘。SGF格式的核心是棋盘尺寸、每一步棋的坐标和颜色。19路棋盘坐标对应字母a到s左上角是aa右下角是ss转换逻辑非常简单def export_sgf(detected, path): # detected是[(row, col, color)]row和col范围0到18 header (;GM[1]FF[4]SZ[19] moves for row, col, color in detected: x chr(ord(a) col) y chr(ord(a) row) moves f;{color}[{x}{y}] with open(path, w) as f: f.write(header moves ))这段代码里GM[1]表示围棋项目FF[4]表示SGF版本4SZ[19]声明19路棋盘。黑棋写B白棋写W坐标顺序是先列后行与OpenCV的(col, row)顺序保持一致。一个高频错误是把行列写反导致棋局在软件里发生转置。拍摄时棋盘如果放反了要先统一方向再导出。验证环节推荐一个铁律识别完一张复杂盘面的照片后导出SGF再导入任意围棋软件在软件里和原图逐块对照确认黑白双方和位置一致。这个动作能快速暴露镜像翻转和坐标偏移类错误这些错误只盯着控制台输出根本看不出来。准确率统计我一般这样做取一百手左右的棋局照片人工数出每行每列的黑白数量再让程序跑一遍计算黑白空三类的一致率。低于95%就先回头检查光照预处理和圆形度阈值稳定在98%以上项目就具备了作为课程设计或者毕业设计核心功能的交付能力。参数调稳后误判往往集中在棋盘边缘两行两列。实用中可以先排除边界区域或者用第二轮精修单独处理边缘点位。最后说一个习惯把每个参数放到配置文件里用命令行参数覆盖不要写死在函数内部。不然明天换一张照片又得翻代码改数字改完还忘了原来是什么。希望这套从环境搭建到避坑再到SGF导出的完整链路能帮到你照着主线跑通一次之后后面就是愉快的调参时间了。本文还有配套的精品资源点击获取
网站建设高端定制企业官网