简介:来自2021中国工程机器人大赛暨国际公开赛(RoboWork)视觉机器狗识别赛的赛用代码包,面向参赛学生与机器人视觉开发者,提供可复用的视觉识别与机器狗动作控制方案。压缩包共267个文件,约21.03MB,以Python源码为主(107个py),同时包含75个d6a动作数据文件、46张jpg图像、caffemodel与prototxt深度学习模型文件,以及csv、npz等配置与训练数据,并附有md和pdf设计文档。其中py脚本覆盖模型加载、数据读取与推理控制,d6a对应多种步态和转向动作,jpg便于可视化调试,整体结构清晰,适合对照理解算法与工程实现。目前已有111人学习此代码包,可用于备赛复盘或二次开发。学习者可获取完整赛题方案、视觉识别模型、机器狗步态与转向动作数据、设计文档及可运行源码,快速掌握从模型推理到动作执行的完整链路。资源包按源码、数据、文档、模型分类存放,方便按需检索和针对性调试。
1. 视觉机器狗识别赛到底在比什么:一个目标、两个系统、三种坑
2021中国工程机器人大赛暨国际公开赛(RoboWork)的视觉机器狗识别赛,任务听起来很简单:场地里有一只机器狗,机器人要通过摄像头找到它、算出它在哪、然后走过去完成指定动作。赛用代码zip里装的,就是这条“识别—定位—引导移动”的完整链路,它不只是一段调摄像头的脚本,而是一套从图像到坐标再到运动的工程方案。
这套代码适合三类人:准备参加同类比赛的学生队伍、做机器人视觉引导入门的工程师、以及想用OpenCV做目标识别定位的从业者。机器狗识别不靠深度学习,靠的是颜色空间转换、轮廓分析和坐标变换。它真正考的是相机标定、颜色阈值和坐标系转换三件事,这三件事做扎实,换目标也一样能识别。先跑通再优化,是这类赛用代码最靠谱的落地路径。
2. 先定识别方案:机器狗识别为什么不用深度学习,而用 HSV 颜色空间
2.1 赛题约束决定了方案选型
机器人视觉识别业界有两条成熟路线:一条是深度学习,用YOLO这类模型做端到端检测;另一条是传统视觉,基于颜色空间、轮廓和几何特征做检测。2021年RoboWork视觉机器狗识别赛的场地是固定的,光照由竞赛方统一布置,机器狗的涂装和外形也基本确定。在这个前提下,深度学习方案的泛化优势根本发挥不出来,反而把问题搞复杂了。
我见过几支用深度学习做识别的队伍,大多栽在同一处:数据标注量和训练时间。临时标注几千张图训出来的模型,在验证集里挺准,比赛现场换个光照角度或者机器狗摆放姿势变一点,漏检误检就来了。这不是模型差,而是数据量撑不起它要学的变化。
反过来看传统视觉方案,识别依据是颜色和几何形状,不是纹理和语义。机器狗识别赛的诉求很明确:在固定场地里快速、稳定地找到目标物体并算出坐标。传统视觉在可控场景下的稳定性、可调试性和帧率都优于当时的深度学习方案。我一般会先把传统视觉跑通,确认满足不了需求再考虑上模型,顺序不能反。
2.2 HSV颜色空间:选它不是玄学,是对光照变化不敏感
OpenCV读进来的图像默认是BGR颜色空间。直接拿BGR范围做颜色阈值分割不是不行,但BGR三个通道对光照强度非常敏感。同一块红色在亮光和暗光下,BGR值能差出一倍。这就导致一种常见的翻车场景:上午调好的阈值,下午光线一变就失灵了。
HSV颜色空间把色相(Hue)、饱和度(Saturation)、明度(Value)拆开了。关键是色相通道,它描述的是“这到底是什么颜色”这个属性,光照变化时相对稳定。红色在HSV里表现为H值在0到10和170到180两个区间,在BGR下它可能从亮红直接变成暗红,稳定性差很多。
这里必须提醒一个细节:OpenCV的HSV范围是H:0-179,S:0-255,V:0-255。网上很多资料写H:0-360,那是图像处理理论教材的写法,直接搬到OpenCV会踩坑。写赛用代码时,我会把HSV上下限放到配置文件里,方便现场调参,后面章节会详细讲。
2.3 赛用代码里的识别主循环:从取流到判定
按赛用代码最常见的组织方式,写一段可运行的识别主循环。它做四件事:读取摄像头帧、转到HSV、颜色阈值分割、提取轮廓判定目标。
import cv2 import numpy as np # 颜色阈值配置,实际使用时应从配置文件读取 hsv_lower = np.array([0, 80, 60]) # H、S、V下限 hsv_upper = np.array([10, 255, 255]) # 上限,这里是红色的第一段 cap = cv2.VideoCapture(0) # USB相机索引通常是0 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) def find_dog(frame): hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv, hsv_lower, hsv_upper) # 开运算去噪点,闭运算填内部空洞 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 只取最外层轮廓 contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None, mask max_contour = max(contours, key=cv2.contourArea) if cv2.contourArea(max_contour) < 800: return None, mask x, y, w, h = cv2.boundingRect(max_contour) return (x, y, w, h), mask while True: ret, frame = cap.read() if not ret: print("取流失败,检查摄像头连接") break box, mask = find_dog(frame) if box is not None: x, y, w, h = box cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cx, cy = x + w // 2, y + h // 2 cv2.putText(frame, "dog", (x, y-8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) print("目标中心像素坐标: ({}, {})".format(cx, cy)) cv2.imshow("frame", frame) cv2.imshow("mask", mask) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()代码逻辑按三步拆。inRange输出一张二值掩膜:颜色落在区间内的像素变白,其余变黑。形态学开闭运算处理的是掩膜上的噪点和空洞,开运算先腐蚀后膨胀,去掉零星噪点;闭运算先膨胀后腐蚀,填上目标内部的高光空洞,机器狗身上的反光点会让mask内部出现大量黑孔,这一步不能省。
轮廓提取用RETR_EXTERNAL只取最外层轮廓。如果换RETR_LIST或RETR_TREE,机器狗身上的贴纸、文字、白色特征点会被提取成独立轮廓,干扰最大轮廓的判定。最后用contourArea过滤面积小于800的小块,这个阈值取决于镜头到目标的距离,目标离得远,像素面积就小,现场要按实际画面调整,不是固定值。
循环里打印的是轮廓外接矩形的中心像素坐标,这个值就是下一章要转换的原始量。注意waitKey里按q退出,比赛时如果发现画面卡住不动,先检查是不是之前跑的程序没有release摄像头,USB摄像头被占用时VideoCapture会静默失败,ret一直是False。
2.4 阈值参数怎么调才不来回翻车
不少队伍现场翻车,不是代码逻辑不行,是HSV阈值凭感觉填。最不靠谱的调法是边跑边改,改一个数看不出效果,改多了不知道是哪一步把识别改坏的。可靠的做法是打印鼠标所在位置的HSV值,鼠标移到机器狗身上看实际数值,再填进阈值。
如果机器狗涂装是红色,典型启动范围为H:0-10和H:170-180两段合并处理。S下限从80起步,太高会丢掉暗红色区域,太低会把灰白色背景误判成红色。V下限从60起步,光线强就调高一点,光线弱就调低,这个值必须在实际光照下决定。
提示:如果原图里还有别的红色物体干扰,比如场地贴纸,加一个宽高比约束当第二道过滤,比盲目调HSV省时间。
如果目标轮廓的宽高比在0.5到2.0以外,直接判为无效目标。只依赖颜色阈值,遇到多目标干扰时很难收场,几何约束是最便宜的抗干扰手段。
3. 从像素坐标到机器人坐标:标定与透视变换
3.1 为什么必须做坐标转换
识别主循环输出的中心像素坐标,是图像平面上以像素为单位的量,它只描述目标在画面里的位置。机器人底盘运动需要的是场地坐标系里的坐标,单位是毫米,原点是场地某个固定角点。直接把像素坐标喂给移动命令,机器人会走错方向,因为摄像头装在机器人上有安装角度,画面里的“前方”和底盘正前方并不一致。
把像素坐标转换成场地坐标,常见做法有两种:一是用相机内参外参做完整标定,二是用单应矩阵做一次透视变换。RoboWork这类比赛场地是平面,摄像头高度固定、角度固定,单应矩阵方案足够,而且标定步骤比完整标定少得多。
也有队伍用三角函数算角度,再按距离换算偏移。这招在摄像头垂直地面、纯俯视角度下能跑,但摄像头一旦带俯仰角,误差会随距离放大。单应矩阵把角度关系吃进矩阵里,不要求摄像头严格垂直,落地性和容错性都好得多。
3.2 四点标定法:用场上标记计算单应矩阵
单应矩阵描述的是“场地上一个平面点”到“图像上对应像素点”的投影关系。求它至少需要四个点对,实际操作里我选场地上四个已知坐标的特征点,比如四个角或者铺在场地上的标记纸,记录物理坐标和图像坐标,再用getPerspectiveTransform解出矩阵。
import cv2 import numpy as np # 场地坐标系下的四个点,单位毫米,按顺时针对应 pts_field = np.array([ [0, 0], # 场地左上角 [3000, 0], # 场地右上角 [3000, 2000], # 场地右下角 [0, 2000] # 场地左下角 ], dtype=np.float32) # 这四个点在图像中的像素坐标,需在画面上手工点出 pts_pixel = np.array([ [156, 98], [582, 87], [619, 428], [112, 441] ], dtype=np.float32) H = cv2.getPerspectiveTransform(pts_pixel, pts_field) def pixel_to_field(px, py): p = np.array([px, py, 1.0]).reshape(3, 1) result = H @ p scale = result[2, 0] # 齐次坐标缩放因子 return result[0, 0] / scale, result[1, 0] / scale fx, fy = pixel_to_field(300, 250) print("机器狗场地坐标: ({:.1f} mm, {:.1f} mm)".format(fx, fy))getPerspectiveTransform的两个输入数组顺序必须一一对应,第一个是场地坐标,第二个是像素坐标,对应关系一错,算出的矩阵会把坐标映射到完全错误的位置。四点顺序我习惯统一按顺时针从左上角开始,这样不容易乱。
pixel_to_field里把像素点构造成齐次坐标,和H做矩阵乘法,结果是一个三行一列的向量。因为单应矩阵可能带缩放,结果向量的第三项是缩放因子,必须先除再用前两项。漏掉这一步,坐标会整体偏差一个倍数,表现就是机器人毎次都走过头或者走不到底。
手工选点是标定精度最大的来源。像素点选得越准,坐标精度越高。一个可用的技巧:把摄像头画面截图放大,用画图工具读像素坐标,别在代码里用鼠标点几下就算了。鼠标选择误差十几个像素,在3000mm场地上会被放大成几十毫米偏差。
3.3 单目相机的高度与角度陷阱
单应矩阵默认场地是一个平面,这是赛用代码能成立的前提。但机器狗是三维立体模型,摄像头会同时看到它的顶部和侧面。识别取的是外接矩形中心,对应成像平面上“剪影”的中心,不是它底部触地点的中心。从顶部斜着看,这个中心会偏向摄像头一侧。
这个现象在机器狗识别赛里特别典型。机器狗立在场地上,高度二三十厘米,摄像头装在机器人上,高度六七十厘米,两者高度差导致成像中心偏到机器狗面向摄像头的一侧。要处理,常见做法是改用目标底边中点作为定位点,底边对应的是机器狗触地点,在单目视觉里比矩形中心稳定得多;或者加一个固定像素偏移补偿,偏移量靠实验测定。
还有一个陷阱:摄像头安装角度的测量误差会直接变成定位误差。与其手测角度写进代码做三角函数补偿,不如重新做一次四点标定。单应矩阵在标定时已经把角度信息隐式包含进去了,只要标定做完,摄像头具体斜了多少度,代码里根本不用关心。
3.4 坐标发布链路:识别结果怎么传给移动系统
识别和坐标转换只是视觉机器狗识别赛的上半场,下半场是机器人执行动作。常见做法用串口或UDP把坐标传给移动模块。比赛没强制用ROS时,串口是最轻量的选择,延迟低、不丢包,但要注意数据格式约定。
串口一次可以发16字节定长帧:2字节帧头、4字节float X坐标、4字节float Y坐标、1字节目标存在标志、1字节校验和,其余填零。定长帧的好处是解析端不用按行切字符串,坏处是排错时不如文本直观,需要先dump一帧数据出来对照协议逐字节检查。
用ROS的队伍就把场地坐标发布到话题,移动节点订阅。这种链路的好处是视觉进程崩了不连累移动进程,但话题通信有延迟,机器狗如果一直在移动,视觉发布出去的坐标实际已经过期。比赛场景里机器狗通常是静止的或缓慢移动,延迟影响不大,如果是动态追狗的场景,就要考虑加时间戳补偿了。
这里就回到了标题里“视觉引导机器人”的核心:引导的前提是感知和运动在一个坐标系下对齐。到2025年,机器人视觉已经和激光SLAM融合出更成熟的方案,但固定场地比赛里,静态标定加透视变换依然是性价比最高的选择,没有之一。
4. 赛用代码的整体组织:从 zip 解压到现场可复跑的运行流程
4.1 目录结构与启动顺序:拿到赛用代码先看这三层
拿到一份赛用代码的zip包,第一步不是急着打开主程序,而是看目录结构。我一般按三层拆:配置层、识别层、调度层。配置层放颜色阈值、场地坐标、通信参数;识别层放摄像头取流、目标检测、坐标变换;调度层放主入口和各模块的启动顺序。这样分层的好处是现场改参数不用碰代码,比赛时手忙脚乱,改的是配置文件里的数字,风险小得多。
大多数赛用代码的问题在于把三层搅在一两个Python文件里。识别参数写死在函数里,场地坐标散落好几处,现场调参只能开着编辑器全局搜索。我再怎么强调都不为过:参数必须集中到配置文件。
启动顺序上按“先配置、再识别、最后联动验证”跑。第一步启动摄像头预览,确认画面正常;第二步加载配置文件并打印关键参数;第三步用一个标定好的地面网格图验证坐标变换是否准确;全部确认无误后,才把移动模块加进来做闭环测试。直接从完整流程跑起,一旦识别不准,你没法判断是摄像头问题、标定问题还是底盘响应问题。
4.2 参数配置:把颜色阈值和坐标参数拆成独立文件
参数集中管理的推荐做法是建一个config.yaml或config.json。用YAML的好处是支持注释,现场几个人围着改参数可以直接写备注;用JSON的好处是不需要额外装依赖。RoboWork赛用代码大多是Python,PyYAML是常见依赖,装一个不费事。一张配置结构表列出来,基本覆盖机器狗识别赛里所有会变的量。
| 配置块 | 示例键 | 用途 |
|---|---|---|
| camera | width, height, index | 摄像头分辨率与索引 |
| color | h_lower, h_up, s_lower, s_up, v_lower, v_up | 目标颜色HSV上下限 |
| morph | open_size, close_size, min_area | 形态学核大小与最小轮廓面积 |
| field | field_width, field_height | 场地物理尺寸,毫米 |
| transform | homography_path | 单应矩阵文件路径 |
| comm | protocol, ip, port, serial_port | 通信方式与端口 |
特别注意field里的边界尺寸。比赛时识别结果可能落在场地外,这种情况多半是误识别,把坐标判为无效比派机器人冲到场地外面去安全得多。我习惯在坐标转换后加一个边界检查,超出场地尺寸就丢弃这一帧,宁可丢目标不可乱指挥。
4.3 运行结构:断点日志与状态机让全流程可复跑
全流程跑起来后,一个容易忽视的问题是程序卡在某一步,你不知道卡在哪。视觉机器人在比赛现场最常见的卡点:摄像头被占用没有释放、标定文件路径写错、串口打不开。这些错误如果不打印,程序要么直接崩溃,要么静默卡住。
我会在关键步骤加日志输出,日志不是用来解释代码的,是让现场人员能定位问题。一个有效的断点日志打印当前模块、当前状态、关键变量值。比如坐标转换第一步打印“homography loaded, shape=3x3”,第1帧识别打印“no contour found”,这样问题一出现就能缩小范围。
import time import cv2 import numpy as np class DogVisionPipeline: def __init__(self, config): self.config = config # 单应矩阵文件不存在时直接报错退出,不静默 self.H = np.load(config["transform"]["homography_path"]) self.log("homography loaded, shape={}".format(self.H.shape)) def log(self, msg): print("[{}] {}".format(time.strftime("%H:%M:%S"), msg), flush=True) def run_once(self, frame): # 识别步骤 hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv, np.array([self.config["color"]["h_lower"], self.config["color"]["s_lower"], self.config["color"]["v_lower"]]), np.array([self.config["color"]["h_up"], self.config["color"]["s_up"], self.config["color"]["v_up"]])) contours, _ = cv2.findContours( mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: self.log("no contour found") return None max_contour = max(contours, key=cv2.contourArea) if cv2.contourArea(max_contour) < self.config["morph"]["min_area"]: self.log("contour area too small") return None x, y, w, h = cv2.boundingRect(max_contour) cx, cy = x + w // 2, y + h // 2 # 坐标转换 p = np.array([cx, cy, 1.0]).reshape(3, 1) res = self.H @ p fx, fy = res[0, 0] / res[2, 0], res[1, 0] / res[2, 0] # 边界检查 if fx < 0 or fx > self.config["field"]["field_width"] or \ fy < 0 or fy > self.config["field"]["field_height"]: self.log("field out of range: ({:.1f}, {:.1f})".format(fx, fy)) return None return fx, fy这个类把识别、坐标转换、边界检查串到同一次调用里,run_once每处理一帧返回场地坐标或None。这种结构方便赛前做单帧回放:录一段视频,跑一遍程序,看每一帧输出是否合理。
日志设计按“关键点必打、普通帧少打”的原则。每一帧都打印检测结果会刷屏,但对排查“时好时坏”的检测问题很有用。现场调参阶段临时打开详细日志,参数稳定后关掉。整个流程可以按状态机理解:未检测到目标时,不输出坐标,移动模块保持待机;检测到目标后,连续三帧坐标一致,才允许下发移动指令,避免单帧误检导致机器人乱跑。
5. RoboWork 机器狗识别的 5 个踩坑与排查记录
5.1 现象:静止目标识别框狂跳,中心坐标来回抖
机器狗放着一动不动,识别框却像抽风一样跳来跳去。出现这个现象,先看是不是形态学处理不够,二值化后的轮廓边缘有毛刺。另一个高发原因是HSV阈值正好卡在目标颜色临界值上,同一块区域有时被识别有时没被识别,第一帧框大,第二帧框小。
解决分两步。第一步加大开运算核,把边缘毛刺修平,从3x3提到5x5一般就够。第二步看打印出来的HSV值,目标颜色值如果落在阈值边界附近,把H和S范围放宽留出余量。还有一个非常关键的因素:USB摄像头的自动白平衡和自动曝光在动,画面亮度随场景变化,导致同一块颜色的HSV值漂移。赛用代码里应把摄像头的自动曝光关掉,固定曝光值,这是解决抖动最有效的一招。
5.2 现象:黑色机器狗和环境融为一体,完全找不到
机器狗如果是黑色涂装,HSV的V通道会非常低,黑色物体的S通道噪声也大,黑白相间的涂装会让轮廓断裂成好几块,和场地深色地板混在一起后根本分不开。
解法不是死磕HSV,而是换识别策略。黑色目标在HSV里不靠谱,就改识别它身上最亮的区域,比如背部的白色贴纸或者四肢的彩色标记,用亮点作为锚点反推整个目标位置。另一个做法是转灰度图做边缘检测,再用外接矩形拟合,目标轮廓明显时这个方法比颜色阈值更稳。颜色识别到了一定程度要懂得止损,换特征比硬调阈值快得多。
5.3 现象:坐标算出来,机器人走过去却偏差 20 厘米
识别框在画面上对准了目标,坐标也输出了,机器人却停在偏离目标20厘米的位置。这种问题先看偏差方向。偏差方向固定,几乎可以断定是标定点选得不精确;偏差方向随机,再怀疑通信延迟或者底盘响应不一致。
固定方向偏差的解法是重新做四点标定,把标定点选在场地中间区域,而不是四个角落。单应矩阵在标定点围成的区域内精度高,外推区域误差会放大。摄像头视野的角落通常存在边缘畸变,畸变让像素坐标偏离理想投影位置,四个角的误差会直接影响全图映射精度。随机方向偏差的话,检查坐标是不是按毫米发出的,有的队伍把float坐标按字符串发送,解析端没转类型就截断了数字。
5.4 现象:实验室调好的颜色阈值,上赛场开灯就废
这是最常见的翻车现场,没有之一。实验室是自然光加日光灯,赛场是大功率照明,色温和照度完全不同。HSV的H受色温影响,S和V受照度影响更大,实验室调好的阈值到赛场识别率骤降。
解法不是赛前多调几次,而是把阈值参数做成可随时改的配置文件,到赛场后先用现场画面重新校准。更省事的方案是在代码里加自动色感适应逻辑:程序启动后取画面固定区域的HSV均值,以此为基准偏移阈值范围。这个方法能应大多数场景,但注意取均值区域必须是场地本身,不能让目标恰好处在画面正中,否则基准会被目标带着跑偏。
5.5 现象:笔记本跑识别 CPU 拉满,帧率掉到 5
赛用代码是Python写的,性能瓶颈就三个:摄像头分辨率太高、形态学核太大、每帧没有缩放直接处理。不少队伍喜欢把分辨率设成1920x1080,觉得画面清楚,但对颜色阈值识别来说,640x480和1920x1080的识别结果几乎没有差别,处理速度差好几倍。
解法是按需降分辨率,在主循环里加帧率监控。目标离摄像头不远的话,320x240都够用。形态学核不要盲目调大,开运算核从3x3变5x5能滤噪点,变15x15会把目标轮廓整个磨掉。帧率掉到个位数时,先看一眼机器狗在画面里的像素宽度,如果只有几十像素,降低分辨率对识别结果影响很小,降下来换帧率完全值得。
6. 一个保命的调试技巧:可视化调参加参数自动扫描
6.1 滑块条实时调参:让阈值不再靠猜
改配置文件里的HSV参数有个麻烦,改完必须重启程序才生效。更快的做法是用cv2.createTrackbar做六个滑块条,分别控制H、S、V的上下限。拖动滑块的同时,程序实时刷新两个窗口:左边是原图,右边是二值化mask。阈值合不合适一眼就能看出,比反复改文件重启快一个量级。
createTrackbar最后一个参数必须传一个回调函数,OpenCV要求必须有,写成lambda x: None即可,直接传None在某些版本会报错。运行起来后,拖动滑块的同时看mask窗口,目标区域是白色、背景是黑色、没有大面积空洞,这组参数就基本可用。这个工具建议单独存成一个脚本,别和正式赛用代码混在一起。
6.2 参数自动扫描:把试错交给脚本
滑块调参虽然快,但比赛前一夜一群人围着屏幕试组合还是太低效。更省事的做法是写自动扫描脚本,让程序自动尝试一组HSV参数,用评分函数选出最优组合。这等于把“人为猜阈值”变成“网格搜索加评分”,对现场调参非常实用。
import cv2 import numpy as np from itertools import product def score_params(frame, h_low, h_up, s_low, v_low): hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask = cv2.inRange( hsv, np.array([h_low, s_low, v_low]), np.array([h_up, 255, 255]) ) contours, _ = cv2.findContours( mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return 0.0 max_cnt = max(contours, key=cv2.contourArea) area = cv2.contourArea(max_cnt) perimeter = cv2.arcLength(max_cnt, True) if area == 0: return 0.0 return area - perimeter * 2 frame = cv2.imread("scene.jpg") best_score, best_params = -1.0, None for h_low, h_up, s_low, v_low in product( range(0, 180, 10), range(10, 180, 10), range(30, 200, 30), range(30, 200, 30)): if h_low >= h_up: continue score = score_params(frame, h_low, h_up, s_low, v_low) if score > best_score: best_score = score best_params = (h_low, h_up, s_low, v_low) print("best params: H {}-{}, S>={}, V>={}, score={:.1f}".format( best_params[0], best_params[1], best_params[2], best_params[3], best_score))评分函数单独说明一下。如果只看轮廓面积,阈值范围放得越宽,误检的背景像素越多,面积分反而越高。所以我加了惩罚项:轮廓周长乘2后减去,轮廓越碎、越不规则,评分越低。这是一个启发式评分,不严谨,但比赛场景里效果足够。实际扫描时H每10度一步、S和V每30一步,组合数量两三千组,跑完只要几十秒。
扫描用的必须是真实比赛现场的画面,不能拿实验室拍的图替代,否则选出来的参数不适合现场光照。拿到最优参数后,把值手工写回配置文件,再跑一遍识别主循环确认目标框稳定、坐标输出合理,这一轮调参才算真正收尾。
我带队时会坚持一个习惯:每一轮现场调试结束,都以配置文件备份收场,用日期做后缀保存一份副本。比赛当天如果参数被误改,还有后悔药可以吃。调参这件事,最大的敌人不是光照变化,是现场手忙脚乱之后的不可复现。滑块和扫描脚本把“手感玄学”变成了可查、可回退的工程操作,希望这个思路帮到你。
本文还有配套的精品资源,点击获取