颜色检测、装甲板识别、位姿解算、数字识别和位移预测这五件事,我一开始是拆成五个脚本写的,结果每个脚本单独看都挺像样,一连起来就露馅:颜色阈值在实验室灯下很稳,换到窗边就飘;数字模型离线准确率不错,上了机器人一抖动就连续误判;PnP 解出来的位置看着能用,角度却像弹簧一样来回跳;预测器更尴尬,目标一丢就自己“脑补”出轨迹。后来我把整条链路按“检测、筛选、识别、解算、跟踪”重新拆了一遍,才发现真正难的不是调用某个函数,而是让每一级的输出都能被下一级信任。
这里的“装甲板”指的是机器人对抗演示场景里的一块结构化靶标板,上面有左右两个灯条和中间的数字区域。整条链路要做的事情很明确:先从画面里找到颜色块,再把灯条配成靶标板,裁出数字区域做数字识别,然后用四个角点解算靶标板相对相机的位置和角度,最后用滤波和运动模型预测下一小段时间的位移。它适合正在做机器人视觉 Demo、想跑通一条完整感知链路的人参考,也适合已经能检测目标、但位姿和预测总不稳的读者对照排查。
我下面按实际调试顺序讲,不按教科书顺序讲。先讲整体设计为什么这么拆,再讲颜色检测和装甲板识别里最容易被忽略的参数,接着把 PnP 位姿解算、角度定义、重投影误差剔除讲透,然后讲卡尔曼预测和延迟补偿,最后给一条能直接跑起来的最小代码骨架和常见问题速查表。你如果是刚入门,可以先把第 2 节和第 3 节跑通;如果你已经能识别目标,重点看第 3.4 节和第 4 节。
1. 项目整体设计与思路拆解
1.1 从“找颜色”到“给状态”的流水线划分
这条链路我最后定成了五级:颜色分割、灯条候选、装甲板配对、数字识别、位姿与预测。颜色分割只负责把红、蓝、绿这类高饱和区域从背景里抠出来,不承担“识别靶标板”的任务;灯条候选负责把色块过滤成细长轮廓;配对负责把左右两个灯条组合成一块靶标板;数字识别负责给这块板一个类别标签;位姿与预测负责把图像坐标变成相机坐标系下的位置、角度和短时位移。
这么拆的好处是每一级都能单独评估。比如颜色分割阶段只看召回率:该亮的灯条有没有被抠出来;灯条候选阶段看误检率:背景里有多少细长色块被放进来;配对阶段看配对成功率;数字识别看的是在真实裁剪图上的准确率,而不是在干净数据集上的准确率。很多项目一上来就端到端训练一个大模型,数据不够时很难定位问题到底出在检测、分类还是位姿。传统几何流水线虽然“手工味”重,但每一级都有明确的可观测指标,调起来心里有底。
1.2 为什么先做几何筛选,再让数字模型兜底
我试过让数字分类模型直接在全图上滑动找目标,速度慢不说,背景里类似数字的纹理会产生大量误检。后来改成先用颜色和几何把候选区域压到很小,再让数字模型在小图上做分类,误检立刻降下来。原因很朴素:颜色和几何是强先验,数字模型是弱分类器,强先验负责把搜索空间砍掉,弱分类器负责在少量候选里做最后判断。
这里有个取舍要讲清楚:几何筛选不能太苛刻。早期我把灯条长宽比卡在 3.5 到 4.5,结果机器人稍微倾斜一点,灯条在图像里变短,长宽比掉到 2.8,整块板直接丢了。后来改成宽区间加评分制:长宽比、填充率、角度一致性、面积比都算分,分数过线就保留,不再要求每一项都完美。这样召回率上来了,误检靠后面的数字识别和时序投票压下去。这个思路在真实场景里比“单个阈值卡死”更耐用。
1.3 相机、镜头与软件栈的取舍
相机这块,我建议优先选全局快门、能手动锁曝光和增益的型号。卷帘快门在机器人快速转向时会让灯条产生倾斜,你后面用角度做配对就会很难受。镜头焦距要看工作距离:太广角畸变大,边缘灯条会被拉弯;太长焦视野小,目标一出画面就丢。一个常见起步配置是 640×480 分辨率、6mm 左右镜头、工作距离 0.4 到 1.5 米,先把链路跑通,再根据视场和精度换镜头。
软件栈我用的是 Python + OpenCV + NumPy 做原型,数字识别可以用小型卷积网络导出 ONNX,也可以在样本少时先用模板匹配顶着。原型阶段不要追求“全平台部署”,先把数据回放、参数调试、可视化三件事做好。尤其是可视化:把颜色掩膜、灯条轮廓、配对结果、数字裁剪图、PnP 重投影点、预测轨迹全部叠到画面上,录成视频回放,比你盯着终端打印数组高效得多。
2. 颜色检测与装甲板识别的关键细节
2.1 HSV阈值标定:固定曝光与分块采样
颜色检测第一步不是调inRange,而是把相机曝光、增益、白平衡锁死。自动曝光在画面里出现高亮灯条时会迅速压暗背景,下一帧灯条又恢复,阈值跟着忽大忽小。我的做法是先把机器人放在典型工作距离,手动调曝光到灯条不过曝、背景不过暗,然后记录参数,后面所有调试都用同一组曝光。白平衡也建议锁定,不然红色灯条在暖光下会偏橙,HSV 的 H 值直接漂移。
HSV 阈值不要从网上抄一组就用。正确做法是截几帧典型画面,把灯条区域和背景区域分别采样,统计 H、S、V 的分布,再取覆盖 90% 样本的区间。红色要特别注意 H 在 0 和 180 附近会环绕,需要两段掩膜相加。下面这组只是起步参考,实际一定要按你的灯条和光照微调:
| 颜色 | 低阈值 | 高阈值 | 备注 |
|---|---|---|---|
| 红色第一段 | [0, 80, 60] | [10, 255, 255] | 偏橙红时上限可到 12 |
| 红色第二段 | [170, 80, 60] | [180, 255, 255] | 与第一段按位或 |
| 蓝色 | [95, 80, 50] | [125, 255, 255] | 冷白灯下 S 会偏低 |
| 绿色 | [45, 80, 50] | [85, 255, 255] | 注意与背景绿植区分 |
标定时我会做一个小工具:鼠标框选灯条区域,实时打印该区域 HSV 的均值和分位数。不要只看均值,均值会被高光拉偏,重点看 10% 和 90% 分位。阈值定完后,再加形态学开运算去掉小噪点,闭运算把灯条内部断裂补上。核大小不要太大,3×3 或 5×5 足够,太大容易把相邻灯条粘在一起。
注意:HSV 的 H 是色相,不是“颜色编号”,不同相机、不同白平衡下同一颜色的 H 会变。阈值必须跟相机参数绑定,换相机或改曝光后要重新标。
2.2 灯条轮廓筛选的参数化方法
颜色掩膜出来后会有一堆轮廓,真正的灯条只是其中细长、填充率较高、面积在合理范围内的那些。我通常用findContours加minAreaRect,然后算五个量:面积、长宽比、填充率、角度、中心点。面积过滤掉远处小噪点和近处大片反光;长宽比过滤掉圆形光斑和方形色块;填充率过滤掉边缘毛糙的色块;角度用来判断灯条是否近似竖直;中心点留给配对阶段用。
参数不要写死成“面积大于 50”。工作距离变化时灯条像素面积会差很多,比较稳的做法是同时设绝对范围和相对范围:绝对范围防止把整块背景放进来,相对范围用画面高度或上一帧目标尺寸做归一化。下面是一组我常用的起步值,640×480 下工作距离 0.5 到 1.2 米:
| 参数 | 起步范围 | 作用 |
|---|---|---|
| 轮廓面积 | 20 到 2000 像素 | 去掉噪点和大块反光 |
| 长宽比 | 2.0 到 8.0 | 保留细长灯条 |
| 填充率 | 0.55 到 0.95 | 去掉毛刺和空心轮廓 |
| 角度 | 与竖直方向夹角小于 35 度 | 允许机器人倾斜 |
| 最小外接矩形短边 | 大于 3 像素 | 防止远处灯条被压成一条线 |
角度这里容易踩坑:minAreaRect返回的角度在不同 OpenCV 版本里定义不完全一样,有的返回 [-90,0),有的返回 [0,90)。我一般统一转成“长边与竖直方向的夹角”,转换后再做判断。不要直接拿原始角度做加减,否则换版本后代码行为会变。灯条左右两侧的轮廓可能因为反光断成两段,闭运算可以缓解,但如果断得太厉害,建议换更柔和的灯条材质或降低曝光,不要一味加大形态学核。
2.3 装甲板配对与数字区域裁剪
拿到灯条候选后,配对就是找“左右两个灯条,高度差不多、角度差不多、面积比不离谱、中间距离在合理范围”的组合。我常用的评分项有:两个灯条中心点的 y 坐标差、高度差、面积比、角度差、中心距与平均高度之比。每一项都归一化后加权,分数最高的组合作为候选板。这里不要只取最高分,保留前两三个候选给数字识别做二次确认,能显著降低误配。
配对时还要判断左右顺序。用两个灯条中心点的 x 坐标排序,左边是左灯条,右边是右灯条,然后按固定顺序取四个角点:左上、右上、右下、左下。这个顺序必须和 PnP 的 3D 点顺序严格一致,否则解出来的位姿会翻面。数字区域裁剪则取两个灯条内边缘之间的矩形,再向外扩 10% 到 15%,保证数字完整。裁剪后做灰度化、直方图均衡或自适应阈值,送给数字识别。
实操心得:配对阶段最怕“跨板配对”,也就是把左边靶标的右灯条和右边靶标的左灯条配成一块板。加一个中心距上限,再用数字区域的长宽比做约束,能压掉大部分跨板组合。
2.4 数字识别:小模型、模板匹配与置信度融合
数字识别不要一上来就上大模型。样本少的时候,模板匹配加时序投票就够跑通链路:把数字区域归一化到 32×32 或 48×32,做灰度化和二值化,然后和 0 到 9 的模板做matchTemplate,取最高分。模板要覆盖不同亮度、不同倾斜角度,每个数字存 10 到 20 张。模板匹配的优点是解释性强,缺点是换字体或换曝光后容易掉分。
样本攒到几百张后,可以换小型卷积网络。输入 32×32 灰度图,两层 3×3 卷积加池化,再接一个全连接层,参数量很小,CPU 上也能跑。训练时一定要混入真实裁剪图,包括模糊、过曝、欠曝、部分遮挡的样本,不要只用干净的数字图。输出层用 softmax,置信度低于阈值时不要硬判,交给时序投票:同一个跟踪 ID 连续 5 帧里出现次数最多的类别作为最终结果,置信度低于 0.6 的帧不参与投票。
这里有个细节:数字区域的对比度可能很低,尤其是灯条很亮时相机会压低整体曝光。我通常对数字区域单独做一次 CLAHE 或自适应阈值,而不是对全图做。全图增强会把背景噪声也放大,反而影响颜色检测。数字识别只在小 ROI 上做增强,成本低,效果也更稳。
3. 位姿解算:位置、角度与坐标系落地
3.1 相机模型与内参标定
PnP 解算的前提是相机内参准确。内参不准确,位置会系统性偏大或偏小,角度也会跟着歪。内参包括焦距fx、fy、主点cx、cy和畸变系数。标定用棋盘格,拍 15 到 30 张不同角度、不同距离的图,覆盖画面中心和四角。标定后看重投影误差,一般控制在 0.5 像素以内算不错,超过 1 像素就要检查棋盘格是否平整、图像是否模糊。
如果你没有标定板,也可以先用厂商给的焦距和像元尺寸估算。比如镜头焦距 6mm,像元尺寸 3.45 微米,则fx ≈ 6 × 1000 / 3.45 ≈ 1739像素。这个值只能做起步,主点和畸变仍然建议标定。水平视场角可以用2 × atan(sensor_width / (2 × f))估算,传感器宽 4.8mm、焦距 6mm 时约 43.6 度。知道视场角后,你能快速判断工作距离和镜头是否匹配。
注意:标定时的图像分辨率必须和实际运行分辨率一致。用 1280×720 标定,运行时改成 640×480,内参要按比例缩放,否则 PnP 解出来的位置会差一倍。
3.2 3D点定义与solvePnP实操
假设靶标板物理宽高是W和H,以板中心为原点,四个角点的 3D 坐标可以定义为(-W/2, -H/2, 0)、(W/2, -H/2, 0)、(W/2, H/2, 0)、(-W/2, H/2, 0)。图像上的四个点按同样顺序取左上、右上、右下、左下。顺序错一个,解出来的旋转矩阵就会翻面,位置也会跑到相机后面。
OpenCV 里平面靶标可以用cv2.SOLVEPNP_IPPE,它对平面四点比较稳。代码骨架如下:
import cv2 import numpy as np W, H = 0.12, 0.06 # 靶标板物理宽高,单位米 objp = np.array([ [-W/2, -H/2, 0], [ W/2, -H/2, 0], [ W/2, H/2, 0], [-W/2, H/2, 0], ], dtype=np.float32) def solve_pose(img_points, K, dist): imgp = np.array(img_points, dtype=np.float32).reshape(-1, 1, 2) ok, rvec, tvec = cv2.solvePnP( objp, imgp, K, dist, flags=cv2.SOLVEPNP_IPPE ) if not ok: return None return rvec, tvectvec就是靶标板中心在相机坐标系下的位置,单位和你定义的物理尺寸一致。如果你用米,tvec就是米;如果你用毫米,后面预测的速度单位也要统一。我见过最常见的问题就是 3D 点用毫米、速度用米每秒,预测器直接发散。单位统一这件事要在代码入口处写死,别靠脑子记。
3.3 角度解算、欧拉角顺序与抖动抑制
rvec是旋转向量,转成旋转矩阵后用cv2.Rodrigues。欧拉角定义必须写清楚:是先绕 Z 再绕 Y 再绕 X,还是先绕 X 再绕 Y 再绕 Z。不同定义下同一块板的 yaw、pitch、roll 数值完全不同。我一般用“相机坐标系下,靶标板绕竖直轴左右转是 yaw,绕水平轴上下转是 pitch,绕光轴滚转是 roll”这套约定,然后在代码里固定顺序。
一个常见的提取方式是:
import math import cv2 def rotation_to_euler(rvec): R, _ = cv2.Rodrigues(rvec) yaw = math.atan2(R[1, 0], R[0, 0]) pitch = math.asin(max(-1.0, min(1.0, -R[2, 0]))) roll = math.atan2(R[2, 1], R[2, 2]) return math.degrees(yaw), math.degrees(pitch), math.degrees(roll)这段只适用于特定坐标约定,换坐标轴后要重新推导。角度抖动不要靠低通滤波硬压,先看抖动来源:如果是四个角点检测抖动,先做角点亚像素优化和时序平滑;如果是 PnP 在平面目标接近正对时解不稳定,可以加一个先验约束,比如靶标板法向量大致朝向相机。角度低通滤波会带来延迟,机器人快速转向时会让预测更滞后,所以滤波系数要谨慎。
3.4 重投影误差与异常位姿剔除
PnP 解完后一定要做重投影误差检查。把解出来的rvec、tvec重新投影到图像上,和实际角点算平均像素距离。误差超过 5 像素就认为这一帧位姿不可信,直接丢弃,不要送进预测器。重投影误差能抓出角点顺序错、角点误检、靶标板严重倾斜导致解算不稳等问题。
def reprojection_error(objp, imgp, rvec, tvec, K, dist): proj, _ = cv2.projectPoints(objp, rvec, tvec, K, dist) proj = proj.reshape(-1, 2) imgp = imgp.reshape(-1, 2) return float(np.mean(np.linalg.norm(proj - imgp, axis=1)))除了重投影误差,还要看tvec的 z 值是否在合理范围。如果 z 小于 0.1 米或大于 5 米,基本可以判定误检。角度也可以做范围约束:如果解出来的 pitch 接近 90 度,而你的靶标板不可能那样装,就丢弃。异常剔除比后面滤波补救更有效,因为滤波器最怕的就是“看起来像真值”的野值。
4. 位移预测与跟踪:卡尔曼滤波不是万能药
4.1 状态量选择与匀速模型
预测位移最常用的是卡尔曼滤波,状态量取[x, y, z, vx, vy, vz],观测取[x, y, z]。匀速模型的转移矩阵按时间步长dt写,位置等于上一帧位置加速度乘dt,速度保持不变。这个模型适合目标短时间近似匀速运动,比如机器人平移或缓慢转向。目标急停急转时,匀速模型会滞后,需要靠过程噪声Q放大来跟上。
import numpy as np import cv2 dt = 1.0 / 60.0 F = np.eye(6, dtype=np.float32) F[0, 3] = dt F[1, 4] = dt F[2, 5] = dt H = np.zeros((3, 6), dtype=np.float32) H[0, 0] = H[1, 1] = H[2, 2] = 1.0 kf = cv2.KalmanFilter(6, 3) kf.transitionMatrix = F kf.measurementMatrix = H kf.processNoiseCov = np.eye(6, dtype=np.float32) * 1e-3 kf.measurementNoiseCov = np.eye(3, dtype=np.float32) * 1e-2 kf.errorCovPost = np.eye(6, dtype=np.float32)状态量不要加太多。有人把加速度也加进去,变成九维状态,结果参数更难调,观测噪声一大就发散。先把六维匀速模型跑稳,确实需要预测急转弯时再考虑交互多模型或加加速度项。预测器的输入质量比模型复杂度更重要,位姿本身跳变,换什么模型都救不回来。
4.2 Q/R参数整定与延迟补偿
Q是过程噪声,越大越相信观测,预测跟随快但容易抖;R是观测噪声,越大越相信模型,平滑好但滞后。整定顺序我一般这样:先固定Q很小,调R到画面不抖;再逐步加大Q,直到目标急停时预测能在两三帧内跟上;最后看静止时的抖动是否可接受。不要同时调两个参数,否则你根本不知道变化是谁造成的。
延迟补偿要单独算。相机曝光、图像传输、算法耗时加起来可能 30 到 80 毫秒,这期间目标已经动了。补偿方法是在预测时把dt加上这段延迟,或者更新后再多预测一步。比如实际帧间隔 16 毫秒,链路延迟 40 毫秒,预测时用 56 毫秒的转移矩阵。延迟值不要拍脑袋,用毫秒计时器从曝光触发打到输出位姿,取平均。
注意:延迟补偿不是越大越好。补偿过头会让预测“超调”,目标停下时预测还在往前冲。我的经验是补偿到实测延迟的 80% 左右,留一点余量。
4.3 多目标ID管理与丢失重捕
画面里可能同时出现多块靶标板,每块板都要有稳定 ID。简单做法是用上一帧预测位置和当前检测位置做距离匹配,距离小于阈值就继承 ID,否则新建 ID。目标少的时候贪心匹配就够,目标多时再用匈牙利匹配。ID 管理不好,数字投票会把不同板的结果混在一起,预测器也会在两条轨迹之间来回跳。
丢失重捕要设生命周期。连续丢失 3 到 5 帧先保留轨迹,用预测位置继续输出;超过 10 帧就删除。重捕时不要只看位置,还要看颜色和数字类别,三者都接近才继承旧 ID。否则两个相同颜色的靶标板交叉时很容易串 ID。这个细节在演示视频里不明显,但在实际控制里会让下游模块收到错误目标。
5. 实操流程:从零跑通一条识别链路
5.1 采集与回放:先把数据闭环搭起来
调试第一件事是录数据,不是调参数。用固定曝光录 3 到 5 段视频:一段静止,一段缓慢平移,一段快速转向,一段目标出入画面,一段有反光和背景干扰。每段 20 到 30 秒就够。录的时候把相机参数、分辨率、帧率记下来,后面所有调试都用同一批数据回放。没有回放,你每次改参数都要重新摆机器人,效率极低,而且无法比较。
回放工具要能暂停、单步、显示掩膜、显示轮廓、显示配对、显示数字裁剪、显示重投影点。我一般用 OpenCV 的imshow加键盘控制,按空格暂停,按 N 下一帧,按 M 切换掩膜显示。听起来土,但比任何可视化框架都快。数据闭环搭好后,调参就变成了“改一个参数,跑一遍回放,看指标变化”,而不是“凭感觉再试一次”。
5.2 参数调试顺序与现场记录
调参顺序我固定为:曝光和白平衡、HSV 阈值、形态学核、灯条筛选、配对评分、数字识别阈值、PnP 异常剔除、卡尔曼参数。这个顺序不能乱,因为前一级的输出是后一级的输入。颜色掩膜不准,后面灯条筛选再调也没用;配对不准,数字裁剪就是歪的;位姿野值不剔除,卡尔曼怎么调都抖。
每次调参记录三件事:改了什么、为什么改、回放指标怎么变。指标不要只看“感觉”,至少记录灯条召回数、配对成功数、数字识别正确数、重投影误差均值、位姿跳变次数。现场记录用表格最清楚:
| 轮次 | 改动 | 灯条召回 | 配对成功 | 数字正确 | 重投影误差 | 备注 |
|---|---|---|---|---|---|---|
| 1 | 初始阈值 | 82% | 70% | 75% | 3.8px | 红色偏橙 |
| 2 | 红色 H 上限 10→12 | 91% | 78% | 76% | 3.5px | 召回提升 |
| 3 | 长宽比下限 2.0 | 91% | 84% | 80% | 3.2px | 误配减少 |
| 4 | 加数字时序投票 | 91% | 84% | 93% | 3.2px | 正确率提升 |
5.3 部署与性能预算
640×480 下,颜色转换和阈值大概 2 到 4 毫秒,轮廓筛选 1 到 3 毫秒,数字识别如果用小网络在 CPU 上大概 1 到 3 毫秒,PnP 不到 1 毫秒,卡尔曼更便宜。整条链路跑 30 到 60 帧问题不大。真正拖速度的往往不是算法,而是 Python 循环和频繁的内存拷贝。能向量化的地方向量化,ROI 裁剪后立即缩小,不要把整帧大图反复转来转去。
如果性能不够,优先降分辨率而不是砍功能。640×480 降到 480×360,颜色检测和轮廓筛选会快不少,远处灯条会小一些,但工作距离近时影响不大。数字识别可以隔帧跑,位姿每帧跑,预测每帧跑。不要为了省几毫秒把异常剔除关掉,野值进预测器后造成的抖动会让你花更多时间补救。
6. 常见问题与排查技巧实录
6.1 颜色漂移、灯条粘连、反光
颜色漂移最常见的原因是自动曝光和自动白平衡。先把这两个锁死,再看阈值。如果锁死后还漂,检查光源是否闪烁,室内灯带频闪会让某些帧整体偏色。灯条粘连通常是形态学核太大或灯条本身太近,先把闭运算核降到 3×3,再检查配对阶段是否把两个灯条当成一个轮廓。反光则要看灯条表面材质,磨砂面比镜面稳,实在不行降低曝光让高光不过曝。
6.2 数字误识别与位姿跳变
数字误识别先看裁剪图,不要只看准确率。很多误识别是因为裁剪框偏了,数字只露出一半。把裁剪框向外扩,再做透视变换把数字区域拉正,准确率会明显提升。位姿跳变先看重投影误差,误差大的帧直接丢;再看角点顺序,四个点顺序错会导致解算翻面;最后看靶标板是否太正对相机,平面目标正对时 PnP 对深度不敏感,位置会沿光轴方向跳,可以加时序平滑或换更大基线。
6.3 预测滞后与目标丢失
预测滞后先量延迟,再调Q。延迟没测就调参数,等于蒙着眼睛修表。目标丢失先看检测召回,如果灯条本身丢了,预测器只能靠模型硬撑,撑几帧后必须删轨迹。重捕时用颜色、数字、位置三个条件,别只靠位置。还有一个容易忽略的点:预测器输出的坐标系要和下游一致,相机坐标系、机器人坐标系、世界坐标系之间的变换要写清楚,不然预测越准,下游错得越离谱。
6.4 常见问题速查表
| 现象 | 优先排查 | 处理办法 |
|---|---|---|
| 灯条时有时无 | 曝光、白平衡、HSV 阈值 | 锁曝光,重采样阈值,红色做双段掩膜 |
| 两个灯条粘成一个 | 形态学核、灯条距离 | 核降到 3×3,配对时按轮廓宽度拆分 |
| 配对跨板 | 中心距上限、长宽比 | 加评分制,保留多候选给数字确认 |
| 数字识别乱跳 | 裁剪框、对比度、时序投票 | 扩裁剪框,CLAHE,连续 5 帧投票 |
| 位置沿光轴跳 | 靶标板正对、角点抖动 | 加重投影误差剔除,角点亚像素优化 |
| 角度弹簧一样抖 | 欧拉角顺序、PnP 稳定性 | 固定坐标约定,加低通或先验约束 |
| 预测滞后 | 链路延迟、Q 太小 | 实测延迟,补偿 80%,适当加大 Q |
| 目标交叉串 ID | 只按位置匹配 | 加颜色、数字类别联合匹配 |
7. 可直接抄作业的最小代码骨架
7.1 颜色与轮廓检测片段
import cv2 import numpy as np def detect_light_bars(frame, color="red"): hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) if color == "red": m1 = cv2.inRange(hsv, (0, 80, 60), (10, 255, 255)) m2 = cv2.inRange(hsv, (170, 80, 60), (180, 255, 255)) mask = cv2.bitwise_or(m1, m2) elif color == "blue": mask = cv2.inRange(hsv, (95, 80, 50), (125, 255, 255)) else: mask = cv2.inRange(hsv, (45, 80, 50), (85, 255, 255)) kernel = np.ones((3, 3), np.uint8) 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) bars = [] for cnt in contours: area = cv2.contourArea(cnt) if area < 20 or area > 2000: continue rect = cv2.minAreaRect(cnt) (cx, cy), (w, h), angle = rect long_side, short_side = max(w, h), min(w, h) if short_side < 3: continue aspect = long_side / short_side if aspect < 2.0 or aspect > 8.0: continue fill = area / (w * h + 1e-6) if fill < 0.55 or fill > 0.95: continue bars.append({ "center": (cx, cy), "size": (w, h), "angle": angle, "area": area, "aspect": aspect, "fill": fill }) return bars, mask这段只是候选灯条,配对逻辑要自己补。配对时先按 x 坐标排序,再两两组合,算高度差、面积比、角度差、中心距,加权评分。分数最高的组合取四个角点,顺序固定为左上、右上、右下、左下。
7.2 PnP与卡尔曼融合片段
def update_pose_and_predict(img_points, K, dist, kf): objp = np.array([ [-0.06, -0.03, 0], [ 0.06, -0.03, 0], [ 0.06, 0.03, 0], [-0.06, 0.03, 0], ], dtype=np.float32) imgp = np.array(img_points, dtype=np.float32).reshape(-1, 1, 2) ok, rvec, tvec = cv2.solvePnP( objp, imgp, K, dist, flags=cv2.SOLVEPNP_IPPE ) if not ok: return None, None err = reprojection_error(objp, imgp, rvec, tvec, K, dist) if err > 5.0: return None, None z = tvec.reshape(-1)[2] if z < 0.1 or z > 5.0: return None, None measurement = tvec.reshape(3, 1).astype(np.float32) kf.correct(measurement) predicted = kf.predict() return (rvec, tvec), predicted实际使用时,预测和更新顺序要注意:一般先predict再correct。上面为了展示融合过程写得比较简,真实代码里每一帧先预测,拿到观测再校正,最后再用校正后的状态做输出。
7.3 调参顺序清单
- 锁曝光、锁白平衡,录回放视频。
- 调 HSV,看灯条召回,不追求零误检。
- 调形态学核,解决断裂和粘连。
- 调灯条面积、长宽比、填充率,保留候选。
- 调配对评分,保留前三个候选。
- 裁数字区域,扩 10% 到 15%,做 CLAHE。
- 数字识别加时序投票,低置信度不投票。
- PnP 加重投影误差和 z 范围剔除。
- 卡尔曼先调 R 再调 Q,最后补延迟。
- 多目标加颜色、数字、位置联合匹配。
我自己踩得最狠的一次坑,是把数字区域的上下方向搞反了,模型离线准确率很高,实车却总是把 6 和 9 混在一起。后来把裁剪图逐帧存下来看,才发现透视变换后图像翻了 180 度。这个事让我养成了一个习惯:所有裁剪图、所有角点、所有坐标系,必须可视化到画面上确认一遍,不要相信“代码看起来是对的”。现在每换一个镜头或改一次安装角度,我都会先跑十分钟回放,把颜色掩膜、配对框、数字裁剪、重投影点、预测轨迹逐项看一遍,确认没有低级错误,再去调那些更细的参数。