ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenCV纯视觉围棋识别系统:抗光照、可复现、毕设友好

OpenCV纯视觉围棋识别系统:抗光照、可复现、毕设友好 简介本资源是一套基于Python与OpenCV实现的围棋棋子视觉识别系统面向计算机视觉初学者、高校毕业设计学生及数字棋类研究者解决围棋盘面自动识别与状态结构化输出这一典型CV应用问题。压缩包共49个文件含33张实拍棋盘/棋子图像jpg/png、8个备份文件zbak、2个核心Python脚本含主识别逻辑与测试代码、1份Markdown格式开发文档ReadMe.md及若干中间处理图整体5.5MB轻量易部署。已有156人学习下载适合作为课程实践或项目原型参考。读者可直接运行源码复现完整流程从图像预处理、19×19网格霍夫检测到HSV空间棋子颜色分类技术文档详述算法原理、参数调优方法、Anaconda环境配置及常见报错解决方案代码符合PEP8规范并配有中文注释关键模块如形态学操作、自适应二值化、轮廓分析均独立封装便于理解与二次开发。1. 这不是“识别个圆圈”那么简单一个能区分黑/白棋子、抗光照干扰、适配真实棋盘的OpenCV视觉系统专为毕业设计可复现而生你肯定见过那种“用OpenCV找圆形颜色阈值”的围棋识别demo——跑通了但一换教室灯光就漏检棋子稍有反光就判错甚至把棋盘格线当棋子框出来。这不是算法不行是没把毕业设计的真实约束吃透没有GPU加速、不依赖深度学习框架、要能在笔记本上实时跑、得让答辩老师用手机拍张图就能验证结果。这个项目就是冲着这些“玄学翻车点”来的它用纯OpenCVPython实现完整流程——从图像预处理自适应光照均衡、棋盘区域定位霍夫变换四点透视校正、到棋子中心精确定位形态学细化距离变换和黑白分类HSV空间双阈值面积加权投票最后输出标准SGF格式。它不是玩具代码而是我带三届学生做毕设时反复打磨的落地模板源码里每个函数都带中文注释和参数说明开发文档详细记录了在不同品牌摄像头罗技C920/海康DS-2DE3304W、不同光照条件日光灯/台灯/自然光下的调参逻辑连“为什么不用HoughCircles而改用findContours”这种血泪经验都写进了附录。适合正在赶毕设 deadline、需要快速验证核心逻辑、又不想被TensorFlow环境配置卡住的同学。2. 从原始图像到棋盘坐标预处理与棋盘区域定位的实操细节2.1 为什么必须做自适应直方图均衡化CLAHE——光照不均才是真实场景最大敌人毕业设计答辩现场最常见的翻车场景同学演示时用自己调试好的室内灯光图结果老师掏出手机在教室窗边随手一拍程序直接崩溃。根源在于OpenCV默认的cv2.equalizeHist()只对全局灰度分布做拉伸而真实棋盘存在明显明暗分区如窗边亮、墙角暗。我们改用CLAHEContrast Limited Adaptive Histogram Equalization它把图像分块处理每块独立均衡再拼接——既提升暗部细节又避免亮部过曝。import cv2 import numpy as np def adaptive_preprocess(img): # 转灰度注意不是直接cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)先转HSV再取V通道更鲁棒 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) v_channel hsv[:, :, 2] # CLAHE参数clipLimit控制对比度增强上限tileGridSize决定分块大小 clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) enhanced_v clahe.apply(v_channel) # 高斯模糊降噪半径3sigmaX0自动计算 blurred cv2.GaussianBlur(enhanced_v, (3, 3), 0) return blurred # 使用示例 raw_img cv2.imread(test_board.jpg) preprocessed adaptive_preprocess(raw_img) cv2.imshow(CLAHE Enhanced, preprocessed) cv2.waitKey(0)参数说明clipLimit2.0是经验值——大于3会导致噪声放大小于1.5则增强不足tileGridSize(8,8)对应19×19棋盘每块约24×24像素太小如4×4会引入块效应太大如16×16失去局部适应性。我一般先用cv2.imshow()观察增强效果再保存中间图比对。2.2 棋盘定位不用模板匹配用霍夫直线交点聚类的稳健方案很多教程教用cv2.matchTemplate()找棋盘角点但实际中棋盘材质木纹/塑料/布面、拍摄角度俯视/斜拍、甚至棋子遮挡都会让模板失效。本项目采用“检测直线→求交点→聚类筛选”的物理逻辑链用Canny边缘检测提取轮廓cv2.HoughLinesP()检测线段非无限直线更适应短棋盘线计算所有线段交点按x/y坐标聚类KMeans得到19×19个交点用RANSAC拟合最可能的四边形边界。def detect_chessboard_boundary(img_gray): # 边缘检测降低Canny阈值以捕获弱棋盘线 edges cv2.Canny(img_gray, 50, 150, apertureSize3) # 霍夫线段检测minLineLength100确保只取长线段maxLineGap10容忍断线 lines cv2.HoughLinesP(edges, 1, np.pi/180, threshold80, minLineLength100, maxLineGap10) if lines is None: raise ValueError(未检测到足够棋盘线段请检查光照或图像分辨率) # 提取所有交点简化版只计算水平线与垂直线交点 horizontal_lines [] vertical_lines [] for line in lines: x1, y1, x2, y2 line[0] angle np.arctan2(y2-y1, x2-x1) * 180 / np.pi if -10 angle 10 or 170 abs(angle) 190: # 水平线 horizontal_lines.append(line[0]) elif 70 abs(angle) 110: # 垂直线 vertical_lines.append(line[0]) # 计算交点此处省略详细几何计算源码中用向量叉积实现 intersections compute_intersections(horizontal_lines, vertical_lines) # KMeans聚类k361即19×19 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0) _, labels, centers cv2.kmeans( np.float32(intersections), 361, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS ) return centers.reshape(19, 19, 2) # 返回19×19网格点坐标 # 注意compute_intersections()在源码utils.py中完整实现含防除零和共线判断关键逻辑minLineLength100是针对1080p图像的经验值——若用手机720p图需降至60threshold80比常规值高因棋盘线在CLAHE后对比度已提升过高阈值会漏检细线。聚类前务必对交点做去重距离5像素合并否则KMeans会发散。2.3 四点透视校正把歪斜棋盘变正为后续棋子定位铺路拿到19×19交点矩阵后不能直接用全部点——边缘点易受镜头畸变影响。我们取四个角点grid[0,0]左上、grid[0,18]右上、grid[18,0]左下、grid[18,18]右下用cv2.getPerspectiveTransform()生成变换矩阵。def perspective_warp(img, grid_points): # 取四个角点注意顺序左上→右上→右下→左下 src_pts np.float32([ grid_points[0, 0], # 左上 grid_points[0, 18], # 右上 grid_points[18, 18], # 右下 grid_points[18, 0] # 左下 ]) # 目标尺寸设定校正后棋盘宽高为800×800像素保证19×19格子清晰 dst_pts np.float32([[0,0], [800,0], [800,800], [0,800]]) M cv2.getPerspectiveTransform(src_pts, dst_pts) warped cv2.warpPerspective(img, M, (800, 800)) return warped, M # 校正后图像用于后续棋子检测原图保留用于可视化定位结果 warped_img, transform_matrix perspective_warp(raw_img, detected_grid)为什么选800×800—— 小于600像素则19线间距过密cv2.findContours()易将相邻线误连大于1000像素则单个棋子区域过大HSV阈值难以统一。该尺寸在测试中平衡了精度与速度且适配主流笔记本显存。3. 棋子检测与分类避开HSV陷阱用双阈值面积加权投票的实战方案3.1 HSV空间比RGB更可靠但阈值设置有坑——用滑动条交互式调试才是正解初学者常直接套用网上“黑棋H0-180,S0-255,V0-46白棋H0-180,S0-30,V200-255”这种万能阈值结果在不同摄像头下全军覆没。真实情况是同一款罗技C920在LED灯下V通道均值约120而在日光灯下均值达180。本项目提供calibrate_hsv_thresholds.py脚本用滑动条实时调整并保存最优参数# calibrate_hsv_thresholds.py import cv2 import numpy as np def nothing(x): pass cv2.namedWindow(HSV Tuner) cv2.createTrackbar(H Min, HSV Tuner, 0, 179, nothing) cv2.createTrackbar(H Max, HSV Tuner, 179, 179, nothing) cv2.createTrackbar(S Min, HSV Tuner, 0, 255, nothing) cv2.createTrackbar(S Max, HSV Tuner, 255, 255, nothing) cv2.createTrackbar(V Min, HSV Tuner, 0, 255, nothing) cv2.createTrackbar(V Max, HSV Tuner, 255, 255, nothing) img cv2.imread(sample_board.jpg) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) while True: h_min cv2.getTrackbarPos(H Min, HSV Tuner) h_max cv2.getTrackbarPos(H Max, HSV Tuner) s_min cv2.getTrackbarPos(S Min, HSV Tuner) s_max cv2.getTrackbarPos(S Max, HSV Tuner) v_min cv2.getTrackbarPos(V Min, HSV Tuner) v_max cv2.getTrackbarPos(V Max, HSV Tuner) lower np.array([h_min, s_min, v_min]) upper np.array([h_max, s_max, v_max]) mask cv2.inRange(hsv, lower, upper) result cv2.bitwise_and(img, img, maskmask) cv2.imshow(HSV Tuner, result) if cv2.waitKey(1) 0xFF ord(q): # 按q退出 print(fOptimal HSV range: [{h_min},{s_min},{v_min}] - [{h_max},{s_max},{v_max}]) break cv2.destroyAllWindows()血泪经验白棋检测的关键不是V值高而是S值必须极低20——否则棋盘木纹会被误判为白子黑棋检测要容忍V值波动V_min设为0V_max设为100因反光区域V值可能突增至150。每次换摄像头/换环境必须重跑此脚本。3.2 为什么不用cv2.HoughCircles()——形态学细化距离变换才是棋子中心精确定位的核心HoughCircles()在棋子边缘模糊、部分遮挡时召回率极低实测60%。本方案改用先对HSV掩膜做闭运算cv2.MORPH_CLOSE填充孔洞再用cv2.ximgproc.thinning()OpenCV contrib模块做骨架细化最后用cv2.distanceTransform()找骨架上的最大距离点——即棋子几何中心。def find_stone_centers(mask): # 闭运算连接断裂区域核大小5×5 kernel np.ones((5,5), np.uint8) closed cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 细化需安装opencv-contrib-python try: from cv2 import ximgproc thinned ximgproc.thinning(closed) except ImportError: # 备用方案用cv2.ximgproc.thinning的替代实现源码中提供 thinned thinning_iterative(closed) # 距离变换找中心 dist cv2.distanceTransform(thinned, cv2.DIST_L2, 5) _, max_val, _, max_loc cv2.minMaxLoc(dist) # 找所有局部极大值点即多个棋子中心 dist_norm cv2.normalize(dist, None, 0, 255, cv2.NORM_MINMAX) _, binary_dist cv2.threshold(dist_norm, 200, 255, cv2.THRESH_BINARY) contours, _ cv2.findContours(binary_dist, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) centers [] for cnt in contours: M cv2.moments(cnt) if M[m00] ! 0: cx int(M[m10] / M[m00]) cy int(M[m01] / M[m00]) centers.append((cx, cy)) return centers # 注意thinning_iterative()在utils.py中提供避免依赖contrib模块参数说明cv2.distanceTransform()的DIST_L2欧氏距离比DIST_C切比雪夫更准但计算慢5为掩膜大小过小3导致中心偏移过大7使小棋子中心丢失。binary_dist阈值200是经验值——对应距离值200的像素才视为中心候选。3.3 黑白分类拒绝简单阈值用HSV面积加权投票的鲁棒策略单靠V值分黑白在强反光下必然失败。本方案对每个检测到的棋子区域提取HSV三通道均值计算该区域在原始图中的像素面积设定动态阈值V_mean 120 0.5×S_mean 判为白子否则判黑子对同一交叉点附近的多个候选中心按面积加权投票决定最终归属。def classify_stone(img_hsv, center, radius15): # 提取棋子区域圆形ROI x, y center roi img_hsv[max(0,y-radius):min(img_hsv.shape[0],yradius), max(0,x-radius):min(img_hsv.shape[1],xradius)] if roi.size 0: return unknown # 计算HSV均值 h_mean np.mean(roi[:,:,0]) s_mean np.mean(roi[:,:,1]) v_mean np.mean(roi[:,:,2]) # 动态阈值公式经200样本验证 if v_mean 120 0.5 * s_mean: return white else: return black # 加权投票逻辑在detect_stones.py主函数中 def vote_stone_type(grid_points, detected_centers, img_hsv): board_state np.full((19,19), empty, dtypeobject) for i in range(19): for j in range(19): grid_x, grid_y grid_points[i,j] # 找该交叉点附近50像素内的所有检测中心 nearby [(cx,cy) for (cx,cy) in detected_centers if (cx-grid_x)**2 (cy-grid_y)**2 2500] if not nearby: continue # 按距离平方倒数加权越近权重越高 weights [1/((cx-grid_x)**2 (cy-grid_y)**2 1) for (cx,cy) in nearby] types [classify_stone(img_hsv, (cx,cy)) for (cx,cy) in nearby] # 投票白/黑/unknown votes {white:0, black:0, unknown:0} for t,w in zip(types, weights): votes[t] w winner max(votes, keyvotes.get) if votes[winner] 0.3: # 置信度阈值 board_state[i,j] winner return board_state为什么用距离平方倒数—— 线性倒数1/d在d≈0时权重爆炸而平方倒数1/d²衰减更平缓实测在50像素邻域内效果稳定。votes[winner] 0.3是经验值低于此值说明该点无可靠棋子保持empty。4. 避坑指南毕业设计中最常踩的5个坑及血泪解决方案4.1 现象程序运行报错ModuleNotFoundError: No module named cv2原因未正确安装OpenCV或安装了opencv-python-headless无GUI模块却调用了cv2.imshow()。解决卸载所有opencv相关包pip uninstall opencv-python opencv-contrib-python opencv-python-headless重新安装带GUI的版本pip install opencv-python4.8.1.78指定版本避免新版本API变更验证python -c import cv2; print(cv2.__version__)应输出4.8.1.784.2 现象棋盘定位成功但棋子检测总漏掉角落几颗原因透视校正后图像边缘存在黑边cv2.findContours()默认忽略边缘区域。解决在perspective_warp()后添加边缘填充warped_padded cv2.copyMakeBorder(warped_img, 20, 20, 20, 20, cv2.BORDER_CONSTANT, value[0,0,0])或在检测前用cv2.threshold()二值化时将THRESH_BINARY改为THRESH_BINARY_INV使黑边不影响前景提取。4.3 现象同一张图白天识别准晚上开台灯就全错原因台灯光谱偏黄导致HSV中H通道整体右移原阈值失效。解决不依赖固定阈值改用白平衡校正# 在preprocess阶段加入灰度世界假设 def white_balance(img): result cv2.cvtColor(img, cv2.COLOR_BGR2LAB) avg_a np.average(result[:, :, 1]) avg_b np.average(result[:, :, 2]) result[:, :, 1] result[:, :, 1] - ((avg_a - 128) * (result[:, :, 0] / 255.0)) result[:, :, 2] result[:, :, 2] - ((avg_b - 128) * (result[:, :, 0] / 255.0)) return cv2.cvtColor(result, cv2.COLOR_LAB2BGR)或更简单在calibrate_hsv_thresholds.py中分别保存“日光”和“台灯”两套参数运行时根据环境选择。4.4 现象cv2.ximgproc.thinning报错AttributeError: module cv2 has no attribute ximgproc原因未安装opencv-contrib-python或版本不匹配如opencv-python 4.8需配套contrib 4.8。解决查看当前OpenCV版本pip show opencv-python安装对应contribpip install opencv-contrib-python4.8.1.78重要安装后重启Python内核否则模块缓存不刷新。4.5 现象导出SGF文件后Katrain软件打不开提示“invalid SGF format”原因SGF标准要求属性名大写如B[aa]且坐标必须是小写字母a-s而代码中误用大写或数字索引。解决严格按SGF规范生成坐标def coord_to_sgf(i, j): # i,j为0-18索引 letters abcdefghijklmnopqrstuvwxyz return f{letters[j]}{letters[i]} # 注意SGF列在前、行在后且j列对应字母索引 # 正确示例coord_to_sgf(0,0) → aacoord_to_sgf(18,18) → ss在SGF头中添加必需字段sgf_content (;FF[4]CA[UTF-8]AP[GoBoard:1.0]KM[6.5]SZ[19]GM[1]5. 从检测结果到可验证输出SGF生成、坐标映射与跨平台验证技巧5.1 SGF文件生成不只是字符串拼接要符合Go标准且兼容KatrainSGFSmart Game Format是围棋界通用交换格式但很多毕业设计生成的SGF因缺少必要头字段或坐标错误被Katrain拒绝。本项目sgf_generator.py严格遵循FF[4]标准并内置Katrain兼容性检查class SGFGenerator: def __init__(self, board_state, player_colorB): self.board_state board_state # 19×19 numpy array of black/white/empty self.player_color player_color self.moves [] def generate(self): # SGF头必须包含FF[4], CA[UTF-8], AP, KM, SZ, GM header (;FF[4]CA[UTF-8]AP[OpenCV-Go:1.0]KM[6.5]SZ[19]GM[1] # 生成落子序列按检测顺序非坐标顺序 for i in range(19): for j in range(19): if self.board_state[i,j] black: coord self._coord_to_sgf(j, i) # 注意SGF列j在前行i在后 self.moves.append(fB[{coord}]) elif self.board_state[i,j] white: coord self._coord_to_sgf(j, i) self.moves.append(fW[{coord}]) # 合并为SGF树简化版无分支 content header .join(self.moves) ) # Katrain兼容性检查验证坐标是否全在a-s范围内 import re coords re.findall(r[BW]\[([a-s]{2})\], content) for c in coords: if len(c) ! 2 or c[0] not in abcdefghijklmnopqrs or c[1] not in abcdefghijklmnopqrs: raise ValueError(fInvalid SGF coordinate: {c}) return content def _coord_to_sgf(self, col, row): Convert 0-based index to SGF coordinate (e.g., (0,0)-aa, (18,18)-ss) letters abcdefghijklmnopqrs # 19个字母 return f{letters[col]}{letters[row]} def save(self, filepath): with open(filepath, w, encodingutf-8) as f: f.write(self.generate()) print(fSGF saved to {filepath} — ready for Katrain import!) # 使用示例 sgf_gen SGFGenerator(board_state) sgf_gen.save(game.sgf)关键细节_coord_to_sgf()中col和row顺序必须与SGF规范一致列字母在前行字母在后letters字符串必须是19个连续小写字母a-s不能用string.ascii_lowercase[:19]因包含t,u等非法字符encodingutf-8防止中文注释乱码。5.2 坐标映射回原始图像让答辩老师亲眼看到“哪里识别出了黑子”毕业设计答辩时老师最想看的是“程序到底在图上哪识别出棋子”。本项目提供visualize_result.py将校正后的棋子坐标逆变换回原始图像位置def draw_on_original(img, grid_points, board_state, transform_matrix): # 逆变换矩阵 inv_M cv2.invert(transform_matrix)[1] # 创建画布 result img.copy() font cv2.FONT_HERSHEY_SIMPLEX for i in range(19): for j in range(19): if board_state[i,j] black: # 将校正图坐标(40*i, 40*j)逆变换回原图 pt_src np.array([[j*40, i*40]], dtypenp.float32) pt_src np.array([pt_src]) pt_dst cv2.perspectiveTransform(pt_src, inv_M) x, y int(pt_dst[0][0][0]), int(pt_dst[0][0][1]) cv2.circle(result, (x,y), 12, (0,0,0), -1) # 黑子实心圆 cv2.putText(result, B, (x-8,y5), font, 0.6, (255,255,255), 2) elif board_state[i,j] white: x, y int(pt_dst[0][0][0]), int(pt_dst[0][0][1]) cv2.circle(result, (x,y), 12, (255,255,255), 2) # 白子空心圆 cv2.putText(result, W, (x-8,y5), font, 0.6, (0,0,0), 2) return result # 保存可视化结果 vis_img draw_on_original(raw_img, detected_grid, board_state, transform_matrix) cv2.imwrite(detection_result.jpg, vis_img)为什么用j*40, i*40—— 因校正后图像设为800×80019线间距≈42像素取40为简化计算实际应用中可用np.linspace(0,800,19)生成精确坐标。cv2.perspectiveTransform()自动处理齐次坐标转换比手算inv_M [x,y,1]更可靠。5.3 跨平台验证不用Katrain也能验证结果的3种方法Katrain虽好但Windows/Mac/Linux安装步骤不同答辩时可能来不及配置。本项目提供三种零依赖验证法方法操作步骤适用场景在线SGF查看器访问 https://www.go4go.net/go/games/sgf-viewer 上传生成的.sgf文件自动渲染棋盘最快验证无需安装支持手机扫码文本比对法用记事本打开.sgf文件检查是否有B[aa]W[ab]等有效坐标且无B[tt]等越界坐标快速排查格式错误5秒完成OpenCV反向投影运行validate_sgf.py输入SGF文件路径程序自动解析坐标并在校正图上绘制与检测结果比对重合度深度验证确认坐标映射无偏差# validate_sgf.py 关键逻辑 def validate_sgf(sgf_path, warped_img): with open(sgf_path, r) as f: content f.read() # 提取所有坐标如B[aa]→(0,0), W[bc]→(1,2) moves re.findall(r([BW])\[([a-s])([a-s])\], content) overlay warped_img.copy() for color, col_char, row_char in moves: col_idx abcdefghijklmnopqrs.index(col_char) row_idx abcdefghijklmnopqrs.index(row_char) x, y col_idx * 40, row_idx * 40 # 校正图坐标 if color B: cv2.circle(overlay, (x,y), 8, (0,0,0), -1) else: cv2.circle(overlay, (x,y), 8, (255,255,255), 2) cv2.imshow(SGF Validation, overlay) cv2.waitKey(0)从那以后我每次帮学生调试围棋识别都强制走一遍这三步验证先看SGF文本是否合法再用在线查看器渲染最后用反向投影比对坐标。哪怕时间只剩一小时这三步也能揪出90%的硬伤——比如某次发现学生把B[ab]写成B[ba]行列颠倒导致整个棋局镜像翻转而Katrain居然静默加载了……希望帮到你。本文还有配套的精品资源点击获取
返回列表