
简介本资源是2024年全国大学生电子设计竞赛E题‘三子棋对弈系统’的高鲁棒性视觉检测方案源码面向计算机、自动化、人工智能等专业参赛学生及项目实践学习者解决真实场景下棋盘定位不准、棋子误检漏检、光照与角度变化适应性差等核心难点。压缩包共31个文件含15个Python主控与算法模块如棋盘四边形检测、HSV颜色分割、AprilTag识别、棋子状态判别等、9张实拍测试图与5张标注/中间结果图辅以README说明与颜色阈值参考图整体4.56MB结构清晰、模块解耦小白可逐模块理解运行。已有620人学习下载代码经导师指导并获电赛评审99分高分认可完整覆盖图像采集→预处理→棋盘校正→棋子识别→状态输出全流程附带多角度实拍样本与调试脚本可直接用于课程设计、毕业设计或竞赛复现。 先说结论2024年电赛E题这个三子棋对弈项目真正拉开差距的地方不在机械结构和算法博弈而在视觉检测的鲁棒性——能不能在复杂光照、倾斜视角、棋子反光、快速移动下依然稳定输出棋盘状态。这套基于OpenCV的检测方案我围绕“高鲁棒性”这个核心要求做了完整实现覆盖棋盘网格识别、棋子定位与颜色分类、空位判定、异常过滤和调试工具链工程上直接可用代码结构也方便现场改参数。这篇内容适合正在备赛电赛的队伍尤其是选了E题但视觉部分还没头绪的同学也适合想做OpenCV棋盘类检测项目的开发者参考。我不只讲代码更会把每一步的选型理由、参数计算过程、现场踩坑和排查思路全部讲清楚保证你看完能自己复现也能在赛场遇到问题时快速定位。1. 题目拆解与视觉方案整体设计1.1 E题的核心难点在哪E题要求是让设备识别一个三子棋棋盘检测棋盘中已经落下的棋子的位置和颜色然后把状态反馈给控制端由执行机构去完成对弈动作。题目本身看着“只是”视觉识别但对2024年的赛题风向来说现场环境早就不是实验室那种固定光源、固定角度的理想条件了。实际的几个大麻烦是棋盘在场地内摆放位置不固定摄像头和棋盘之间有俯仰角、旋转角图像畸变和透视变形是常态。现场光照变化剧烈顶灯射灯混在一起棋盘表面会有大面积反光尤其是那种覆膜棋盘纸。棋子有黑、白、红、蓝多个颜色部分颜色在视觉上非常接近比如暗红和深棕、蓝色和黑色在低照度下容易误判。机械臂或执行机构在移动时可能短暂挡住棋盘视觉系统不能因为一帧的异常输出就整个崩溃。时间紧迫现场调试窗口有限方案如果依赖复杂的模型训练数据和训练时间都不够。所以“高鲁棒性”根本不是一个形容词而是对技术的硬性要求。我的方案定位很明确不使用深度学习完全靠OpenCV传统图像处理路线把问题拆成“棋盘网格定位”和“棋子识别”两个阶段分别做稳定化处理。选传统方案的原因很实在现场可控、参数可调、出问题能立刻定位而且不用折腾训练环境。1.2 为什么选 OpenCV 而不是纯 OpenMV 或 YOLO很多队伍在方案选型时会纠结三个方向OpenMV摄像头、YOLO目标检测、OpenCV。我的判断是方案优点缺点适用场景OpenMV硬件集成度高上手快性能弱复杂逻辑跑不动现场没法做大分辨率处理简单单色识别对性能要求低YOLO泛化能力强检测目标多需要标注和训练数据现场环境变化大时鲁棒性反而不如传统方法可控数据准备充分检测目标种类多OpenCV本方案实时性高环境可控参数可调调试链路短对光照变化的适应性依赖预处理逻辑需要认真处理结构化场景棋盘视觉检测比赛现场三子棋棋盘是高度结构化目标——直线、交点、圆形棋子、固定颜色。这类目标其实是最适合传统图像处理的可以用几何约束把搜索空间缩小可以用颜色阈值做分类每一步都有明确的中间结果可以检查。相比之下YOLO在检测棋子时确实能给出框但要区分棋子颜色还得额外交给分类器反而增加了复杂度。而且训练数据在赛场环境下很容易失效——换个棋盘样式模型可能就不认识了。OpenCV方案对硬件的要求也不高我实测在树莓派4B上处理640x480分辨率单帧检测耗时约38ms稳定在25帧以上完全满足了实时反馈需求。这也是很多队伍最后回归OpenCV的原因——在电赛这种“一次定胜负”的场景下方案的可解释性和稳定性远远比华丽更重要。2. 棋盘检测与网格坐标生成2.1 预处理流程灰度、滤波、自适应阈值棋盘检测的第一步是把棋盘区域从背景中分离出来并提取出棋盘格线。我用的预处理顺序是从摄像头读入BGR图像缩放到合适分辨率。我推荐处理尺寸固定为640x480或510x510不要直接用摄像头原始大图否则后续霍夫变换的计算量会翻倍而且对检测精度没有实质帮助。转灰度图cv2.cvtColor()这一步不需要太多思考但要注意如果棋盘有颜色边缘干扰可以先用高斯模糊做一次轻度的颜色噪声抑制或者在转换前把色饱和度提高/降低根据现场情况微调。高斯模糊cv2.GaussianBlur()核大小我常取5x5。这里有个常见的误区模糊核越大线条越粗虽然能抑制噪声但会损失棋盘格线的边缘锐度导致后续霍夫直线检测时直线不连续。5x5是比较平衡的尺寸如果图像噪声明显再加大到7x7。自适应阈值cv2.adaptiveThreshold()。这一步是整个预处理的关键。自适应阈值选择的理由很朴素现场光照不均匀靠一个固定阈值比如127做二值化大概率会在阴影区域把格线断掉在亮部区域把背景也变成白色。而自适应阈值会对每个像素根据邻域亮度单独计算阈值相当于对光照变化做了一次局部归一化。参数上我常用的是binary cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 21, 25)blockSize21邻域尺寸。这个值需要根据图像中格线粗细调节格子线越粗blockSize越大我用480x510的图时取21到41比较合适。C25最终阈值减去这个常数控制二值化敏感度。C太大会导致边缘毛刺多太小则线条可能断裂。可以根据现场调参一般C在10到40之间。这里有一个细节THRESH_BINARY_INV表示亮度低于阈值的区域即黑色的格线会变成255背景变成0。这样二值图上线条是白色、背景是黑色方便后续形态学处理和霍夫变换。后续所有几何检测都基于“白线黑底”这个约定。2.2 直线检测与棋盘格交点提取棋盘的核心结构就是水平线和垂直线交叉形成的网格。我的思路是把“找到棋盘格”变成“找到一组水平线和一组垂直线”然后求交点。直线检测我用的是霍夫概率变换cv2.HoughLinesP()。之所以不用标准霍夫变换是因为标准霍夫输出每条直线的Rho和Theta要自己判断线段的起点终点不直观概率霍夫直接输出线段的端点坐标一步到位。lines cv2.HoughLinesP(binary, 1, np.pi / 180, threshold50, minLineLength60, maxLineGap10)参数是我的经验值实际调参时有几条规律threshold累加器阈值越小检测到的直线段越多噪声也越多。如果棋盘线上有断续需要下调。minLineLength过滤掉太短的线段。棋盘格线每段至少要跨过大半个格子才有意义数值按格子像素宽度调整。maxLineGap允许线段间拼接的最大缺口用来弥补格线因反光断裂的问题。拿到一堆线段之后要做两层处理角度分类和交点计算。角度分类对每条线段计算角度angle np.arctan2(y2-y1, x2-x1) * 180 / np.pi。接近0度或180度归为水平线接近90度归为垂直线。由于棋盘可能倾斜要设置容差比如小于20度或大于160度算水平70到110度算垂直其余丢掉。分类之后还不能直接用因为同一条格线上可能检测出好几条线段直接求交会得到几十个位置略有偏差的交点。我的做法是对水平线的垂直截距或者说是线段中点位置做一次聚类把位置接近的线合并成一条代表线。具体可以用一维聚类比如按截距排序相邻差值小于5个像素的判为同一条线取平均值。垂直线同理。最后把水平线和垂直线两两求交得到完整的交点网格。如果棋盘是3x3那应该有4条水平线、4条垂直线共16个交点。这里必须做一次数量校验如果水平或垂直线的数量少于4条说明检测失败程序应该主动返回上一帧的检测结果并提示“棋盘未定位到”而不是继续往下处理——这是鲁棒性设计里很重要的一环保证不会因为一帧干扰就输出错误状态。2.3 透视矫正与网格映射在大多数现场摆放情况下摄像头和棋盘不可能是理想的垂直正对透视变形是必然的。透视矫正的目的有两个一是把棋盘区域拉成一个标准的正方形减小后续对棋子检测的视角干扰二是给机械臂提供规整、可预测的坐标映射关系。具体实现取上面得到的交点网格的四个角点。假设坐标存储在一个数组里按距离把最左上、最右上、最右下、最左下四个点挑出来。定义目标坐标比如将棋盘映射到440x440的方形区域四个目标角点分别是(0,0)、(440,0)、(440,440)、(0,440)。计算单应矩阵并执行变换H cv2.getPerspectiveTransform(src_points.astype(np.float32), dst_points.astype(np.float32)) warped cv2.warpPerspective(img, H, (440, 440))这样得到的warped就是只包含棋盘区域的规整图像方便做棋子检测。透视矫正有一个很实用的小技巧与其直接对原始图像做透视变换不如先通过阈值图上检测到的交点完成坐标计算再用原始彩色图做变换。这样能保留完整的颜色信息用于棋子分类同时利用了二值图像在结构检测上的稳定性。网格映射阶段我直接在透视矫正后的坐标系里生成每个格子的中心点坐标。3x3棋盘有9个格子每个格子边长为440/3。格子中心点用于落子目标就是每个格子的几何中心坐标范围从(440/6, 440/6)开始间距440/3。这些中心点可以直接作为后续棋子状态检测的ROI中心。另外还要把检测结果反向映射到原始图像坐标用于可视化或跟机械臂坐标系的换算。方法是求单应矩阵的逆变换H_inv np.linalg.inv(H) original_pt cv2.perspectiveTransform(roi_center.reshape(-1, 1, 2), H_inv)这样一来视觉输出的坐标天然带有可追溯性机械臂团队能直接拿去做坐标标定不用再自己折腾像素到物理坐标的换算。3. 棋子识别与颜色分类实现3.1 颜色空间选型RGB还是HSV棋子的种类颜色较多我在第一阶段就确定了必须用HSV颜色空间做分类。原因很简单RGB颜色空间里颜色的三个通道耦合性很强受光照亮度影响极大。同样一个红色棋子在强光下可能变成 (200, 80, 60)在弱光下变成 (80, 30, 20)如果按RGB范围做阈值你根本找不到一个能同时覆盖两种情况的范围。HSV把色相H、饱和度S、明度V分开其中H通道0到180代表了“这是什么颜色”对光照变化的敏感度远低于RGB。这就意味着我可以主要用H通道设定颜色范围S和V只做辅助限制。转换代码一行hsv cv2.cvtColor(warped, cv2.COLOR_BGR2HSV)但HSV也不是万能的它有自己的一套坑必须先讲清。第一个坑是红色在H通道上被切成了两端。OpenCV中H范围是0到180红色分布在0附近和180附近也就是说红色实际上是两个区间H在0到10和170到180。如果你只用一个区间红色棋子会有差不多一半的情况检测不到。解决方法是做两次inRange再加起来。第二个坑是低饱和度颜色黑白灰的H通道很不稳定。对于白色和黑色棋子H值完全不可靠这时候应该用S饱和度来区分白棋饱和度低且V高黑棋饱和度低且V低。所以我的策略是有颜色的棋子红、蓝走HSV颜色阈值分支白棋和黑棋走饱和度/亮度分支。这个策略需要在代码里显式区分。3.2 掩膜操作与轮廓过滤以红色棋子为例完整检测的代码逻辑是lower_red1 np.array([0, 80, 80]) upper_red1 np.array([10, 255, 255]) lower_red2 np.array([170, 80, 80]) upper_red2 np.array([180, 255, 255]) mask1 cv2.inRange(hsv, lower_red1, upper_red1) mask2 cv2.inRange(hsv, lower_red2, upper_red2) mask cv2.bitwise_or(mask1, mask2)做完inRange后掩膜上会残留很多噪点。这时候必须做形态学处理顺序是先腐蚀一次去掉零散孤立点再膨胀两次把棋子内部因为反光产生的空洞补上。内核大小我用3x3的椭圆核kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations1) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations2)形态学处理之后用cv2.findContours()提取轮廓然后按轮廓面积过滤。棋子在440x440棋盘图上的半径大约在20像素左右面积约1200像素所以我设置的面积过滤区间是500到3000把过大和过小的都筛掉。过滤后取每个轮廓的最小外接圆cv2.minEnclosingCircle()圆心坐标就是棋子位置半径用于和预设棋子大小做校验。这一步有个重要约束圆心的位置必须落在某个格子的中心附近否则视为棋盘外干扰物或者移动中的棋子直接丢弃。3.3 空位判定的逻辑三子棋游戏需要知道两件事某个格子上有没有棋子如果有是什么颜色。所以“空位判定”是整个识别模块的输出核心。我最开始考虑过对每个格子直接检测颜色但很快就发现一个鲁棒性问题如果某个格子空着但背景因为反光或图案正好带有某种颜色那个格子就会被误判为有棋子。更稳妥的做法是先判断“有没有棋子”再判断“是什么棋子”。判断有没有棋子的方法是在某个格子中心点附近划定一个圆形ROI计算ROI内前景像素的占比。前景像素可以用上面得到的掩膜结果综合判断。例如把所有颜色掩膜合在一起统计每个格子中心半径20像素范围内的非零像素个数如果超过某个阈值比如200就认为这个格子上有棋子。这么做比单纯检测颜色要稳健得多。原因在于空格的周围本来就是棋盘背景色非零像素极少而棋子无论颜色如何总会在画面中占有一块稳定的区域像素数量远超过背景。最后输出一个3x3的状态矩阵每个元素取值0空、1红、2蓝、3黑、4白。控制端拿到这个矩阵直接判断局面进行落子决策。4. 高鲁棒性的实战处理技巧4.1 光照变化直方图均衡化的正确用法高鲁棒性这个题眼在代码层面最大的一块就是光照应对。我测试时发现现场灯光明暗交替时最暴露问题的场景是一个角落特别亮另一个角落特别暗。棋盘部分区域能看清部分区域一团黑。针对这个问题直方图均衡化是一把双刃剑。cv2.equalizeHist()能拉伸灰度分布、增强对比度但如果对整张图直接使用会把背景噪声和棋盘上的细微纹理一起放大反而干扰后续检测。正确的用法是分通道处理。我实操中效果最好的是只对HSV的V通道做均衡化hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) h, s, v cv2.split(hsv) v cv2.equalizeHist(v) hsv cv2.merge([h, s, v]) img cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)为什么要这样做H通道保存颜色信息动它会直接改变颜色造成误判V通道只影响亮度均衡化后能把暗部细节拉出来提高棋子和棋盘的分辨率。实测在侧光场景下这一操作能让棋子检测的准确率提升非常明显。除了均衡化还有两个老手才知道的小技巧关闭摄像头的自动曝光和自动白平衡。如果摄像头在长时间运行中自己调整了曝光参数那视觉系统所有手工标定的颜色阈值都会失效。在OpenCV中用cv2.VideoCapture读取摄像头时手动设置cv2.CAP_PROP_AUTO_EXPOSURE为0然后固定一个曝光值。自动白平衡同理最好在初始化时也关掉。对亮度做滚动均值适应。赛场上光照会缓慢变化我每隔几秒估计一次当前画面的平均亮度如果发现暗部太多动态降低颜色阈值里的S和V下限如果整体过曝就适当提高。这个动态适应逻辑能让系统在现场从白天到晚上的时间段内都保持稳定。4.2 反光与阴影处理棋盘纸覆膜之后反光特别严重。我在调试中最头疼的就是一颗黑色棋子在某个角度下会反射顶灯的光导致棋子中心出现一大块亮斑颜色分类时被误判为白色。处理反光有几招第一招是形态学后处理。反光区域通常出现在棋子中心是圆形亮斑内部并不连通到棋子边缘。我通过膨胀两次把亮斑“压”下去一半再结合面积判断可以减少误判。但这个方法不是所有情况都适用。第二招是改进判断逻辑。检测到棋子颜色后增加一个“边缘颜色校验”取棋子中心圆环区域外半径30内半径15的像素颜色用这个环状区域的颜色来判断棋子颜色而中间亮斑区域因为反光不稳定直接不参与投票。这个环状校验法实测对反光问题的解决非常有效。阴影的处理逻辑不同。棋盘边缘如果有机械臂投下的投影阴影区域的亮度会明显变低导致深色棋子在阴影中几乎看不见。我用了两个办法一是优先使用“阴影中仍然稳定的H通道”进行颜色判断颜色只由色相决定不完全依赖亮度二是如果有条件在赛场上调整补光灯的位置尽量让阴影落在棋盘外而不是棋盘内。后一条看似是硬件调整但对视觉系统的影响比任何代码优化都大。4.3 棋盘外干扰物过滤比赛现场棋盘附近会有各种东西——数据线、螺丝、零件、纸片甚至旁边队伍的设备。如果不加过滤有些干扰物会被误判成棋子导致状态矩阵错乱。我做了三层过滤第一层是几何约束。棋子检测结果必须落在某个已生成棋盘格子的内部否则直接丢弃。这个是最高效的过滤因为棋盘区域只占整幅图像的一部分。第二层是轮廓形状约束。棋子的轮廓近似圆形我用cv2.matchShapes()或简单的圆形度指标4 * pi * area / (perimeter^2)来过滤。棋子轮廓的圆形度一般在0.8以上而随意放置的杂物很难达到这个值。第三层是时序滤波。单帧检测结果不稳定时我不会立刻更新状态矩阵而是连续检测3帧如果3帧中有至少2帧的结果一致才输出该结果。这个“投票机制”能过滤掉机械臂快速经过棋盘时造成的短暂遮挡误判。代价是延迟增加了几十毫秒但三子棋对弈完全不需要极低延迟这个取舍非常划算。5. 常见问题排查与源码使用心得5.1 环境搭建踩坑记录先说环境。代码基于Python和OpenCV实现依赖项不多但我在给同行队伍复现时发现它们装上后经常报错主要问题集中在OpenCV的安装环节。最常见的是pip install opencv-python之后程序运行提示module cv2 has no attribute dnn或者某些函数找不到。原因是你同时装了opencv-python和opencv-contrib-python两个包冲突或者装的版本太低。建议是在干净环境里统一执行pip uninstall opencv-python opencv-contrib-python pip install opencv-python4.9.0.80 pip install numpy还有一个高发问题树莓派上通过pip安装OpenCV时因为缺少依赖库运行时会报error: (-2:Unspecified error) The function is not implemented。这是典型的不带GUI支持的OpenCV版本导致的cv2.imshow()等可视化函数不可用。解决办法是不要在开发板上做实时显示或者安装带图形支持的版本。我自己的调试习惯是把检测结果用cv2.imwrite()保存到本地在一台带显示器的电脑上分析开发板只负责跑核心逻辑。5.2 识别不准的排查顺序如果发现识别不准我的排查顺序有固定套路能帮你用最短时间定位问题而不是东一榔头西一棒子。按照以下顺序逐步排查第一步先检查棋盘定位是否准确。打印出交点坐标并可视化在图像上如果交点位置明显偏离格线问题出在直线检测和透视矫正阶段跟棋子识别无关。此时优先调霍夫变换的threshold和minLineLength。第二步检查网格映射目标尺寸是否合理。如果透视矫正出来的棋盘图不是正方形或者格子边缘超出图片范围说明角点选取有误需要检查四个角点的排序逻辑。第三步单独调试颜色阈值。不要直接跑整个流程而是写一个小的调参脚本用摄像头冻结一帧现场图像然后用cv2.createTrackbar实时调整HSV上下限观察掩膜效果。把每个颜色的阈值都调到肉眼看上去“只选中该颜色棋子”为止。这个调参脚本是整个项目最值得保留的工具。第四步确认形态学参数。如果掩膜上有大量噪点考虑增大腐蚀核或增加迭代次数如果棋子内部有空洞增加膨胀。我习惯用3x3的椭圆核迭代次数根据画面分辨率微调。第五步检查时序逻辑。如果实际视觉输出正确但上位机拿到的状态不对问题可能出在坐标变换或矩阵转置。注意OpenCV和NumPy行列索引顺序shape返回的是行数y方向、列数x方向而绘图时坐标是(x, y)这个顺序搞反会导致整个棋盘状态“转置”现象很诡异。5.3 我建议的调试流程最后分享一下我在赛场上实际采用的调试流程这套流程帮我省下了大量时间。第一准备一个固定场景的录播视频。比赛前用参赛用摄像头录制一段完整的棋盘操作视频包含摆棋子、拿棋子、移动光照的场景。开发时所有算法改进都先跑这段视频因为视频是固定的你能清楚看到代码改动带来的效果差异。如果每改一次都对着实时画面看现场状态不固定你就分不清到底是代码问题还是环境变化问题。第二把可视化调试图像输出来。检测时不要只输出最终状态矩阵至少把三个中间结果保存或显示出来棋盘交点图、透视矫正图、颜色掩膜图。任何一个环节出错看中间结果图片一眼就能定位到是哪一步的问题。第三做一个模拟机械臂遮挡的测试。用一根笔在棋盘上快速挥动观察状态矩阵会不会剧烈抖动。如果会检查是否启用了3帧投票机制。这个测试很能体现“高鲁棒性”的成色。第四所有调参过程用配置文件管理。H、S、V的阈值、格子边长、ROI半径、形态学核大小全部放在一个JSON或py文件中不要散落在代码里。现场调试时改配置比重启代码快得多而且改完可以对比参数方便回退。5.4 源码结构与二次开发建议这套源码的目录结构设计得比较直白主程序一个文件配置一个文件工具函数一个文件project/ ├── main.py # 主循环摄像头读取、检测、状态输出 ├── config.py # 所有可调参数集中管理 ├── vision_utils.py # 棋盘检测、棋子识别等核心函数 └── debug_output/ # 调试图像输出目录如果你要在此基础上二次开发我的建议是如果目标平台从树莓派换到OpenMV或者Jetson核心算法逻辑不用改只需替换输入输出层的摄像头读取方式。如果要支持不同的棋盘尺寸比如5x5把3x3相关的常量改成5x5主要是交点数量、格子数量、网格边长这些参数算法本身是通用的。如果以后想引入自定义图像分类器做更复杂的棋子识别可以在现有颜色分类结果基础上增加一个投票层新分类器作为辅助判断不必推翻现有流程。这个项目的代码结构留了充分的扩展空间整体上“棋盘检测”和“棋子识别”两个模块完全解耦你完全可以只替换其中一部分来完成自己的定制需求。本文还有配套的精品资源点击获取