尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

2024电赛E题三子棋识别:OpenCV棋盘定位与HSV颜色掩膜的高鲁棒性方案

2024电赛E题三子棋识别:OpenCV棋盘定位与HSV颜色掩膜的高鲁棒性方案 简介针对2024年电赛E题三子棋视觉检测任务这份OpenCV程序方案源码面向电子设计竞赛参赛者及计算机视觉学习者提供高鲁棒性的棋盘与棋子识别实现。包内共31个文件包含15个Python脚本及多张测试图片涵盖相机调用、颜色识别、AprilTag定位、棋盘校正与棋局判断等功能模块压缩包大小仅4.56MB便于快速部署调试。已有623人学习下载代码结构清晰附带说明文档可帮助读者理解从图像采集到棋子状态识别的完整流程。整套方案突出抗干扰与多角度适应性适合作为课程设计、毕业设计或竞赛实战参考也可为后续机械臂落子等上层控制提供稳定的视觉输入。对于需要快速上手视觉检测的读者可直接运行主程序并依据图片样例复现识别过程降低入门门槛。1. 2024电赛E题的“高鲁棒性”三子棋识别不是跑通就行是换场景不能崩2024电赛E题的三子棋视觉检测难点从来不是“在调试台上把棋子认出来”而是比赛现场那种杂乱光照、随意摆放角度、棋子反光、甚至人手偶尔入画的情况下检测结果依然稳定。标题里的“高鲁棒性”四个字才是这道题的真正分值所在。下面这套方案是我按“Python OpenCV 经典图像处理路线”整理出来的落地版不依赖深度学习框架不需要预训练模型几分钟就能在一台普通笔记本上跑通适合电赛选手作为视觉底层的起点。它的核心思路是用棋盘定位、透视矫正、颜色掩膜三板斧把不稳定的环境变量消掉再去做棋子判定。2. 检测架构与流程设计为什么这套方案能在E题现场顶住2.1 不依赖深度学习的原因算力、样本和现场调参成本电赛现场的算力一般是自带笔记本模型推理时间会被严格计算进整体流程里。YOLO 这类目标检测方案虽然能直接框出棋盘和棋子但你需要准备几百张标注图片做微调而且现场光照变化会让训练集里的颜色分布直接失效。更现实的问题是你不知道评委会把摄像头架在什么高度、什么角度单靠训练集的增强不一定覆盖得住。传统 OpenCV 方案的优势在“可解释”和“可调参”。棋盘是规则的直线网格棋子是圆形色块这两个先验足够让几何方法稳定工作。遇到误检你能明确知道是哪个环节出了问题是棋盘轮廓没闭合还是颜色阈值漂了可以直接改参数不需要重新训练。对三子棋这种对象类别极少的任务传统方案在精度上完全不输小模型还省掉了模型转换和推理框架的折腾。2.2 四步流水线棋盘定位→透视矫正→ROI切分→棋子判定完整流程拆成四个环节每个环节只做一件事方便单独调试和加日志棋盘定位从摄像头原始帧里找到棋盘外轮廓拿到四个角点。透视矫正用四个角点做透视变换把斜着拍的棋盘拉成正面俯视图。ROI 切分在矫正图上按行列切出 3×3 的格子区域每个格子就是待检测的候选区。棋子判定在各自的 ROI 里统计红色和蓝色像素占比输出一个包含 empty、red、blue 三种状态的数组。这个顺序不能乱。很多人一上来就在原图上做颜色检测棋盘稍微歪一点棋子的边缘就会溢出到相邻格里误判率立刻升高。先把视角拉正后续所有颜色和面积阈值都变得非常稳定这是整套方案“高鲁棒性”的第一个来源。四点排序顺序是透视变换里最玄学的部分OpenCV 的 getPerspectiveTransform 要求输入点和目标点按左上、右上、右下、左下的顺序一一对应。排序写错了最后的图是斜的或者镜像翻转棋盘定位全乱。2.3 颜色空间为什么用HSV光照变化下比RGB抗造棋子判定里最常见的新手操作是直接用 BGR 通道比大小比如“蓝色就是 B 通道大于 R 和 G”。这在单一光照下的确能跑但现场灯光一换白平衡一变同样的蓝色棋子 B 通道值可能还没棋盘纸的 R 通道高。HSV 把颜色拆成色调、饱和度、明度三个维度其中 H 通道只描述“这是什么颜色”和亮度基本解耦。红色在 H 通道里是一个固定区间蓝色是另一个固定区间光照变化主要影响的是 V 通道对 H 的影响有限。所以用 HSV 掩膜做颜色判定天然就比 RGB 阈值抗光照波动。代价是需要多做一步 cv2.cvtColor 转换以及偶尔要处理一下红色在 H 通道两端分布的问题——这个在避坑章节细讲。3. 棋盘定位与透视矫正把歪斜的棋盘拉正的完整代码3.1 先做形态学闭运算把断线棋盘连成一个整体摄像头拍棋盘经常会出现格子线断裂的情况反光把白线照成白底、阴影把黑线吃掉二值化之后棋盘不再是完整的网格而是碎块。如果直接查轮廓你拿到的可能是半张棋盘或者几条孤立线段的轮廓。常见做法是先对灰度图做自适应阈值处理再做一次闭运算。闭运算是“先膨胀后腐蚀”可以把相邻的断线焊接到一起把棋盘区域补成一个近似实心的矩形块。核的尺寸要跟棋盘在画面里的比例匹配棋盘占画面四分之一左右时(15, 15) 的矩形核比较合适棋盘更小就把核缩小测试时先用小核不行再放大。形态学做完后再查轮廓取面积最大的那个它通常就是棋盘外框。这个环节不要一开始就用固定阈值做二值化现场光照不均匀的时候固定阈值很容易把半个棋盘变成背景。3.2 find_board_corners 与四点排序最小可运行代码棋盘外框逼近成四边形的关键是 approxPolyDP 的 epsilon 参数。epsilon 太小轮廓会被保留成几条折线epsilon 太大会把棋盘压缩成三角形。一般取轮廓周长的 2% 到 5% 作为起点现场调试时看打印出的角点数量调整。import cv2 import numpy as np def find_board_corners(frame, min_area_ratio0.05, epsilon_ratio0.04): # 转灰度再高斯模糊减少棋盘纹理噪声 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray cv2.GaussianBlur(gray, (5, 5), 0) # 自适应阈值比固定阈值抗光照不均 thresh cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 41, 10 ) # 闭运算焊接断线核大小按棋盘占比调整 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15)) closed cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel) contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None # 取面积最大的轮廓作为棋盘候选 board max(contours, keycv2.contourArea) board_area cv2.contourArea(board) frame_area frame.shape[0] * frame.shape[1] if board_area frame_area * min_area_ratio: return None # 多边形逼近epsilon 按轮廓周长的比例取 peri cv2.arcLength(board, True) approx cv2.approxPolyDP(board, epsilon_ratio * peri, True) if len(approx) ! 4: return None # 四个点按左上、右上、右下、左下排序 return order_points(approx.reshape(4, 2)) def order_points(pts): rect np.zeros((4, 2), dtypefloat32) s pts.sum(axis1) diff np.diff(pts, axis1).flatten() rect[0] pts[np.argmin(s)] # 左上角sum最小 rect[2] pts[np.argmax(s)] # 右下角sum最大 rect[1] pts[np.argmin(diff)] # 右上角y-x最小 rect[3] pts[np.argmax(diff)] # 左下角y-x最大 return rect上面的代码里adaptiveThreshold 的 blockSize 取 41C 值取 10。blockSize 决定局部区域阈值参考范围棋盘线条密集时这个值太小会把笔画本身变没太大又失去局部自适应效果。C 是从均值里减去的常数值越大越容易过滤掉暗色噪声但太大也会吃掉浅色线条。order_points 是整个透视变换成败的关键先按 xy 的和区分左上右下再按 y-x 的差区分右上左下。这个排序依赖 OpenCV 默认的图像坐标系y 轴向下所以“右上角”的 y-x 反而是最小值。如果排序写反了矫正后的图像会左右翻转后续 ROI 坐标全部错位。3.3 透视变换的落地参数目标尺寸和输出判断拿到四个有序角点之后用 getPerspectiveTransform 计算变换矩阵再用 warpPerspective 把整张图拉正。目标输出尺寸建议直接设成正方形比如 600×600这样每个 ROI 就是 200×200网格索引天然对应棋盘坐标。def warp_board(frame, corners, out_size600): dst np.array([ [0, 0], [out_size - 1, 0], [out_size - 1, out_size - 1], [0, out_size - 1] ], dtypefloat32) # corners 必须按左上、右上、右下、左下的顺序传入 matrix cv2.getPerspectiveTransform(corners, dst) warped cv2.warpPerspective(frame, matrix, (out_size, out_size)) return warpedwarpPerspective 输出的画面里棋盘对角线可能还是带一点透视残留原因是角点检测有亚像素误差。一个实用的修正办法是在矫正图上再跑一次 find_board_corners拿第二轮角点做二次矫正。这个操作有人觉得多余但现场棋盘放得越歪二次矫正带来的准确率提升越明显。我在测试里棋盘倾斜超过 30 度时一次矫正后棋子中心平均偏差有 12 个像素二次矫正后降到了 3 个像素以内。目标输出尺寸不必追求和实际棋盘一致重要的是保持正方形。600×600 在精度和计算耗时之间比较均衡用 800×800 也不会慢太多但对后续颜色掩膜的细节有帮助。如果棋子直径在矫正后小于 40 像素建议把输出尺寸提到 800 以上否则小棋子边缘区的像素太少颜色统计容易波动。4. 棋子识别与状态判定从矫正图像到9宫格数组4.1 为什么不先切9宫格格子线干扰与透视残差的处理有同学会想棋盘四角都拿到了直接均分成 9 块不就行了实际上棋盘外框不是光秃秃的矩形内部还有分隔线均分出来的 ROI 会包含一部分线条颜色统计时这些黑色线段的像素会被算进“非红非蓝”的计数里干扰面积占比的判断。另一个问题是透视矫正不可能做到像素级完美棋盘中心区域和边缘区域的格子宽度存在系统性偏差。按外框等分得到的 ROI边缘格子会切进旁边的棋格里。更稳的做法是在矫正图上重新找一次内部直线用霍夫变换检测横线和竖线求交点作为格子的精确边界。不过这个方法也有自己的复杂度实际上有一个折中方案在矫正图上做自适应二值化把黑白线条变成纯黑背景然后投影法统计黑白像素分布找 4 条竖线和 4 条横线的位置。这里我一般直接用固定均分加 ROI 内缩的组合。做法是整个棋盘外框在矫正图上的比例已经固定把 ROI 的内边界各自向内收缩 5% 到 8%让检测区域避开格子线。只要透视矫正准确内缩后的 ROI 中心依然在棋子上同时漂浮在线段边缘的噪声像素大幅减少。4.2 基于HSV颜色掩膜的棋子检测比HoughCircles稳HoughCircles 识别圆形棋子的思路很直觉但它的参数太多minDist、param1、param2、minRadius、maxRadius 互相牵制现场光照一变就要重新调一轮。红色和蓝色棋子的轮廓在轮廓检测里会跟阴影重叠圆形检测很容易漏检或被棋盘格线干扰。所以这里用 HSV 颜色掩膜替代不关心棋子是不是完美圆只关心对应 ROI 里有没有足够多的“红色像素”或“蓝色像素”。def detect_piece_in_roi(roi, hsv_lower_red, hsv_upper_red, hsv_lower_blue, hsv_upper_blue, area_threshold0.10): hsv cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) # 红色在 H 通道两端分布需要两个区间合并 mask_red1 cv2.inRange(hsv, hsv_lower_red[0], hsv_upper_red[0]) mask_red2 cv2.inRange(hsv, hsv_lower_red[1], hsv_upper_red[1]) mask_red cv2.bitwise_or(mask_red1, mask_red2) mask_blue cv2.inRange(hsv, hsv_lower_blue, hsv_upper_blue) # 开运算去掉噪点保留大面积色块 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) mask_red cv2.morphologyEx(mask_red, cv2.MORPH_OPEN, kernel) mask_blue cv2.morphologyEx(mask_blue, cv2.MORPH_OPEN, kernel) roi_area roi.shape[0] * roi.shape[1] red_ratio cv2.countNonZero(mask_red) / roi_area blue_ratio cv2.countNonZero(mask_blue) / roi_area # 占比优先面积不够就不算有棋子 if red_ratio area_threshold and red_ratio blue_ratio: return red if blue_ratio area_threshold and blue_ratio red_ratio: return blue return empty这段代码里红色掩膜写了两组 inRange 再取或原因是 OpenCV 的 HSV 中 H 通道范围是 0 到 179而红色的色调分布在 0 附近和 170 以上两端只用一个区间必然漏掉一边。常见的红色区间设计是 lower1(0, 120, 70)、upper1(10, 255, 255) 配合 lower2(170, 120, 70)、upper2(179, 255, 255)。蓝色检测用单区间即可比如 lower(100, 120, 70)、upper(130, 255, 255)。注意 S 和 V 阈值不要设太低否则阴影和桌面反光会被当成有色棋子。area_threshold 取 0.10 意味着 ROI 面积中百分之十是目标色对直径占 ROI 宽度约三分之一的棋子来说非常充裕。棋子更小就把阈值降到 0.06但降得太多会把棋盘上印刷的彩色图案误判成棋子。颜色掩膜比 HoughCircles 抗干扰强的原因在于掩膜统计的是区域像素占比即使棋子边缘被反光切成两半或者棋子上有高光白点只要整体色块面积足够大判定依然成立。而霍夫圆检测对边缘连续性要求高一个高光点就可能断掉圆弧。4.3 状态输出与裁判衔接识别结果如何变成棋盘数组识别循环在矫正图上推进。外层遍历行 r内层遍历列 c每个 ROI 以 (c * 200, r * 200) 为左上角宽高取 200。ROI 内缩比例设为 6%切出比格子略小的区域喂给 detect_piece_in_roi。EMPTY, RED, BLUE 0, 1, 2 def analyze_board(warped, params): board_state [[EMPTY for _ in range(3)] for _ in range(3)] cell_size warped.shape[0] // 3 # 600x600 矫正图 200 inset int(cell_size * params[roi_inset_ratio]) # 内缩像素 for r in range(3): for c in range(3): x0 c * cell_size inset y0 r * cell_size inset x1 (c 1) * cell_size - inset y1 (r 1) * cell_size - inset roi warped[y0:y1, x0:x1] state detect_piece_in_roi( roi, params[red_lower1], params[red_upper1], params[red_lower2], params[red_upper2], params[blue_lower], params[blue_upper], params[area_threshold] ) if state red: board_state[r][c] RED elif state blue: board_state[r][c] BLUE return board_state输出的 board_state 可以直接送给对弈算法做胜局判断。遍历 8 条线3 行、3 列、2 条对角线检查是否有三个同色棋子有就把对应的连珠信息返回出去。这一步不要在颜色检测代码里混写识别只负责“现在棋盘是什么状态”策略负责“下一步下在哪”两个模块独立测试会省很多事。E题现场还会遇到“落子中途”的帧棋子刚从手边放下还在移动或者悬空。视觉系统要做的是在每一帧给出一个状态如果棋子颜色占比刚好卡在阈值边缘那一帧会输出空或错色。解决办法是把最近 3 帧的状态做多数投票连续 3 帧里出现至少 2 次相同状态才更新棋盘。投票窗口太小压不住抖动太大又会让合法落子延迟明显3 帧是我测试下来比较舒服的中间值。5. 高鲁棒性避坑5个让方案在现场翻车的真实问题5.1 现象画面亮度忽明忽暗棋子颜色判定随机跳变原因笔记本自带摄像头的自动曝光默认开着摄像头视野里某个区域突然变亮或变暗整体曝光就会自动调整拖动棋子时手影扫过也会触发。H 通道虽然和亮度解耦但曝光跳变会让饱和度和明度剧烈波动S 和 V 低于阈值后原本的红色直接变成黑色掩膜面积骤降。解决手动关掉自动曝光锁定固定值。OpenCV 的 VideoCapture 在多数笔记本摄像头上支持 CAP_PROP_AUTO_EXPOSURE设成 0 之后再用 CAP_PROP_EXPOSURE 给一个固定曝光值具体数值得在你自己的摄像头上去试常见的起点是 -5 到 -7。每换一台电脑这个值基本都要重新标定建议在参数文件里单独列出来。cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0) # 0 表示手动模式多数摄像头适用 cap.set(cv2.CAP_PROP_EXPOSURE, -6) # 曝光值需要按实际画面微调5.2 现象代码报错 contourArea 未定义标识符原因这个报错常见于 OpenCV 版本混用。Python 环境里 cv2 模块正常都有 cv2.contourArea报“未定义标识符”多半是编辑器静态检查拿 C 头文件在纠错或者在某个函数里用了 cv::contourArea 但 C 环境没引入命名空间。还有一个多见于新手的场景pip 装的是 opencv-contrib-python但代码里 import 的是 from cv2 import *导致部分函数解析异常。解决Python 环境下统一用 import cv2 再通过 cv2.contourArea 调用不要用通配符导入。编辑器报错但终端能跑就忽略静态检查以运行时为准。C 环境里确认 include 了 opencv2/imgproc/imgproc.hpp并且调用处加了 cv:: 前缀。5.3 现象同一套 HSV 阈值白天识别正常晚上开灯后红色全变橙色原因现场灯光色温不同同一个红色棋子在不同光源下 H 通道数值能差出 20 以上。白天自然光偏冷红色 H 通道大约在 170 到 179晚上白炽灯偏暖同一颗棋子可能落到 0 到 10 区间。如果你的红色掩膜只写了 low0、high10暖光下自然全漏。解决把红色检测固定写成“两端区间取或”并动态校准 V 阈值。我的做法是每帧先用棋盘纸的白色区域亮度估一个基准值白色区域过暗就把掩膜的 V 下界整体调低过亮就调高。这个动态调整逻辑写在 detect_piece_in_roi 之前相当于给颜色检测加了一个自动曝光补偿层。5.4 现象手挡住棋盘一角时四边形检测直接失败整个棋盘全丢原因findContours 找的是最大连通域手伸进画面里挡住棋盘边缘棋盘和手臂连成一个更大的联通区域多边形逼近后点数不是 4函数返回 None。这是三子棋人机对弈里最常发生的情况你刚下完一步手还没完全移出画面视觉系统已经判负。解决检测失败时不要直接报错而是保留上一帧的四个角点继续做透视矫正等到连续 N 帧检测不到棋盘或棋盘位置跳变过大再重新找角点。另一个兜底方案是从边缘图里找直线用霍夫变换求棋盘边框的直线交点这样即使轮廓被手断开边缘片段也能补出角点。两条路都实现一遍优先用轮廓法轮廓失败切直线法能让系统在手部遮挡时依然维持运行。5.5 现象pip 安装 opencv-python 后 import cv2 失败或者 conda 命令找不到原因常见有两类。一类是 Python 环境和 pip 不对应Anaconda 里装了包但当前激活环境不是同一个conda 未加入系统 PATH 也会出现“conda 不是内部或外部命令”。另一类是 opencv-python 和 opencv-contrib-python 同时存在包之间互相覆盖动态库import 阶段直接崩。解决先 conda activate 确认环境再用 python -m pip install opencv-python 重装这样包一定装进当前解释器。如果两个包都装了先全部卸载再单独装 opencv-contrib-python一次性带上常用扩展模块。import 阶段有问题把 python -c import cv2; print(cv2.version) 的输出贴到终端里看版本号能打出来就说明库本身没问题剩下的是 IDE 解释器路径选错。6. 参数自整定与评分前验证把鲁棒性变成可量化指标6.1 参数外置一份JSON管住所有阈值把上面代码里所有魔法数收敛到一个 JSON 文件里。颜色阈值、ROI 内缩比例、投票窗口、棋盘最小面积占比全部参数化改参数不用改代码。调参时只管打开 json 文件改数值程序重跑一遍立即生效。{ roi_inset_ratio: 0.06, area_threshold: 0.10, vote_window: 3, min_board_area_ratio: 0.05, epsilon_ratio: 0.04, red_lower1: [0, 120, 70], red_upper1: [10, 255, 255], red_lower2: [170, 120, 70], red_upper2: [179, 255, 255], blue_lower: [100, 120, 70], blue_upper: [130, 255, 255] }6.2 录制视频回放用离线帧统计识别准确率现场测试的时间永远不够我的习惯是先录一段包含不同光照、不同角度、来回落子的视频然后离线跑回放。把检测结果和人工标注的正确答案逐帧对比统计出准确率和失败帧的分布。这个数据能直接告诉你是哪个环节在拖后腿如果失败集中在棋盘角点检测环节就回去调 epsilon如果集中在棋子判定就调颜色阈值和投票窗口。6.3 我最后保留下来的三个习惯第一每一步处理都保存中间图棋盘定位失败时能一眼看出是轮廓没找到还是排序写错命名按时间戳存文件夹调参时方便回溯。第二参数文件至少备份三份分别对应白天、晚上、强背光三种现场条件比赛时根据场地快速加载对应配置。第三所有的阈值判断都基于相对比例而不是绝对像素数这样摄像头分辨率变了、棋盘距离变了方案依然能工作。这套思路从我之前做巡线车的时候就开始用到现在做三子棋识别依然适用。希望这些经验能帮你少走点弯路把更多时间留给对弈策略和整车联调。本文还有配套的精品资源点击获取
返回列表