比赛那天下午,太阳斜着打在赛道右侧,目标板上的数字在画面里白成一片。我们调试了三个星期的分割算法,在那条直道上一瞬间全部失效,车子直接冲出了赛道。事后复盘,问题根本不在算法,而在曝光时间——相机自动曝光被太阳骗了,目标板上的边框和字符全部过曝,HSV阈值设得再准也救不回来。这个教训让我意识到,目标板识别从来不是一个纯粹的OpenCV算法问题,而是一条从镜头到决策的完整链路,每一环都必须可控。
“走马观碑”这个队名,取自那个能一边策马疾驰一边看清碑文的典故。放在智能车竞赛里,其实就是我们对视觉系统的期望:车跑过去的同时,把路边目标板上的内容又快又准地读出来。然而现实往往很打脸,跑起来之后,算法能不能稳住完全看天吃饭。
这篇文章把我们组在目标板识别这条链路上踩过的坑、验证过有效的方法,以及一些可以直接拿来用的思路整理出来。内容围绕OpenCV,但又不只是OpenCV——相机参数、图像预处理、轮廓筛选、透视矫正、内容识别、嵌入式提速、现场排障都会讲到。如果你也在准备类似的竞赛,或者正在折腾基于OpenCV的目标检测项目,这篇文章应该能让你少走不少弯路。
1. 竞赛里目标板识别的真正难点:不是“认不出”,而是“不信任”
1.1 目标板在竞赛中的作用与“走马观碑”的含义
智能车竞赛里的目标板,一般是放置在赛道旁边的平面牌子,上面有数字、字母或者特定符号。车子识别到之后,要执行对应的控制动作,比如减速、停车、换道、切换赛道模式。它的逻辑本身不复杂,但难就难在车子是在高速运动状态下,用一颗低成本摄像头去捕捉一张可能倾斜、可能反光、可能被阴影遮挡的牌子,还要在几十毫秒内给出一个可靠结果。
“走马观碑”这个典故说的是古人骑马经过碑林,马在跑,人却能把碑文看完记下。我们组取名的时候就是看中这个“快而准”的感觉。但真正跑起来才发现,视觉系统离这个境界差得很远。目标板在画面里往往只停留几百毫秒,如果算法不能在第一时间锁定它,等车子贴到近前再识别就已经来不及了。所以这个题目的本质不是“能不能识别”,而是“在什么速度和什么环境条件下,识别结果还可信”。
1.2 “快、稳、省”三个字如何贯穿所有算法选择
整个调优过程中,我脑子里始终绷着三个字:快、稳、省。
快,是指单帧处理时间必须控制在竞赛主控可接受的范围内。我们当时的平台是一块入门级嵌入式开发板,OpenCV跑在CPU上,全图640×480分辨率做一遍完整处理,如果设计不合理,耗时能飙到80毫秒以上,直接拖垮控制频率。
稳,是指在不同光照、不同角度、不同距离下,识别结果不能忽好忽坏。赛场的阳光、阴影、反光是我们控制不了的,算法必须对这些变化有足够的容忍度。我后来定了一条规矩:任何识别结果必须带上置信度,连续两帧以上一致才允许触发控制动作。
省,是指资源和时间的节省。嵌入式平台的内存和算力都有限,能不做高分辨率处理就不做,能用单通道绝不用三通道,能在小范围ROI里搜索就绝不全图扫描。省下来的算力可以用来提高帧率,帧率上去了,运动模糊和漏检问题也会相应缓解。
这三个字听起来像口号,但后面每一个具体方案的选择,其实都是拿这三个字去衡量之后的结果。
2. 从相机采图就要开始较真:曝光、帧率与图像质量
2.1 OpenCV读相机的工作原理与帧缓冲
很多队伍拿到相机第一件事就是cv2.VideoCapture(0),然后直接进入图像处理循环,很少去想这一行代码背后发生了什么。实际上,OpenCV的VideoCapture在Linux平台上默认通过V4L2协议读取设备。相机把光信号转换成数字信号之后,数据先进入内核的驱动缓冲区,再由OpenCV把它拷贝到用户空间,最后封装成Mat对象供我们使用。
这里有一个非常容易踩的坑:缓冲区是有队列的。如果你的处理速度跟不上采集速度,帧会在缓冲区里不断堆积,你读到的那一帧并不是“最新”的画面,而是几百毫秒前的旧帧。对目标板识别这种对实时性要求极高的场景,这是致命的。解决办法是主动清空缓冲区,或者在每次读取前设置较短的缓存模式,保证拿到的永远是最新的帧。
另外,很多人在调试时会写一个带窗口显示的循环,然后发现画面卡住不动,半天才反应过来是cv2.waitKey(0)的问题——参数写成0表示无限等待键盘输入,没有按键就卡死在那。竞赛代码里千万别用waitKey(0),要用waitKey(1)并利用返回值做按键响应,否则调试五分钟,找卡死原因三小时。
2.2 相机选型与竞赛场景的参数建议
目标板识别对相机的要求,其实和拍短视频完全不同。我们需要的是低延迟、固定曝光、尽量少的运动模糊,而不是高分辨率和高色彩还原。我们当时对比过几款相机,最终留下来的配置思路可以用下面这个表格概括:
| 参数维度 | USB UVC相机 | CSI接口相机 | 我的建议 |
|---|---|---|---|
| 开发难度 | 低,即插即用 | 中等,需要配置驱动 | 预算有限选UVC |
| 图像延迟 | 相对较高 | 较低 | 追求极限选CSI |
| 分辨率 | 可到1080P | 可到720P/1080P | 竞赛建议720P以下 |
| 快门类型 | 多为卷帘快门 | 部分支持全局快门 | 动态场景全局快门更稳 |
| 固定参数控制 | 部分支持 | 较完善 | 选支持手动曝光的型号 |
分辨率方面,我不建议为了“看得清”而盲目上1080P。目标板在画面里占的像素面积通常不大,确实需要一定分辨率才能看清内容,但720P其实已经足够。分辨率翻倍,处理时间不是翻倍,而是三到四倍地涨,因为后续轮廓查找、形态学的耗时都跟着像素数量走。更合理的做法是用640×480采集,在目标板出现时对局部区域做裁剪放大,这样既保证了细节,又控制住了整体计算量。
2.3 固定曝光与白平衡:让画面成为一个稳定输入
这是我在那个“过曝翻车事故”之后学到的第一课。竞赛场地在室内外都有,光线变化非常剧烈。相机的自动曝光和自动白平衡,在静止场景下效果还行,但车一跑起来,镜头看到的画面连续变化,自动参数就会一直来回调整,画面的亮度、色偏每帧都在飘。识别算法面对这种无规律的输入变化,再稳定也会崩。
我们的做法是把相机完全设置成手动模式:固定曝光时间、固定增益、固定白平衡。在赛道起点处对准光线最复杂的区域,采集一帧画面,反复调整这几个参数,直到目标板的颜色和对比度在画面里最清晰,然后把这一组参数写死。这样做的代价是,如果你跑到一个光线突然变暗的区域,画面会偏黑,但只要目标板的颜色特征还能从背景中分出来,颜色分割依然有效。相比之下,自动曝光带来的“每帧亮度不同”才是颜色分割算法最怕的事情。
关于曝光时间,这里可以给一个估算公式。假设车速是3m/s,目标板宽度是0.3m,目标板在图像中水平方向占500像素。如果曝光时间是1ms,车子在曝光期间前进了3毫米,换算到图像里大约是5个像素的拖影。5个像素的模糊对字符识别来说还在可接受范围;但如果你用的是自动曝光,在暗处曝光时间可能被拉到10ms以上,拖影就会变成几十个像素,整个字符糊成一团,任何算法都救不回来。所以曝光时间宁短勿长,哪怕画面稍微暗一点,也要保证没有明显的运动模糊。
2.4 畸变与标定:什么时候该做,什么时候可以不做
目标板识别需要做相机畸变校正吗?我的答案是:分情况。如果你用的是鱼眼镜头或者广角镜头,画面边缘的目标板会明显变形,矩形边框变成弧线,这时候透视矫正会出错,必须做标定。但大多数竞赛场景用的是普通视场角镜头,畸变集中在画面边缘,而目标板出现的位置往往在画面中前部——只需要算准这个区域,畸变的影响可以忽略。
我们组当时没有做完整的棋盘格标定,而是采取了一个更接地气的策略:让目标板尽可能落在图像中心附近,同时把识别算法里的几何校验条件放宽一些。比如矩形判断时,允许边缘曲率有很小的波动;面积和宽高比的容差也适当加大。这样做省去了标定环节的时间,在实际赛场上完全够用。如果你后续打算做更高精度的定位,再去补标定也不迟,它能帮你把角点坐标误差从几个像素降到亚像素级,但对字符识别来说,这个精度提升的收益并没有想象中那么大。
3. 预处理链路的重构:我为什么放弃Canny而改用HSV分割
3.1 灰度图+Canny的经典组合在赛道场景里的尴尬
很多OpenCV教程里,检测一个目标的标准流程是:转灰度、高斯模糊、Canny边缘检测、轮廓查找。这个流程在实验室环境下很漂亮,但拿到赛道上就有点水土不服。Canny的核心是找灰度梯度变化剧烈的点,可赛道场景里梯度大的地方太多了:轮胎痕、阴影边界、赛道边缘、反光高光,全都会产生边缘。你辛辛苦苦找出来一堆边缘,再从中筛出目标板的轮廓,等于先把所有候选者都拉进来,再一个个排除,误检率自然居高不下。
更要命的是,Canny的两个阈值(low_threshold和high_threshold)对光照变化非常敏感。阳光强的时候,目标板字符和边框的梯度变大;阴天的时候,梯度变小。同一组阈值,上午好用下午就失灵,每个参数都要重新调。这种不稳定性在竞赛场景里是不可接受的,因为我们在赛场上没有充足时间逐帧调参。
所以我的建议是,除非目标板本身是纯灰度、没有颜色信息,否则不要一上来就走Canny路线。把颜色当作第一道筛选条件,效果会好很多。
3.2 HSV颜色空间分割:让“颜色”成为第一道筛选
目标板的设计通常是有讲究的,要么是深色边框配浅色底,要么是特定颜色的矩形框中间放字符。这些颜色信息在BGR空间里很难直接用阈值表达,因为BGR三个通道对光照变化都很敏感。转换到HSV空间之后,H(色相)通道把颜色本身和亮度剥离开,只要色相范围选对了,画面整体变亮变暗基本不影响分割结果。
用OpenCV实现很简单,核心就是cv2.inRange。我当时是自己写了一个带滑动条的小工具,把H、S、V的最小值和最大值做成六个滑条,对着实际赛道画面一边拖动一边观察二值图效果,找到最合适的区间。有一点要特别注意:OpenCV里H通道范围是0到179,S和V范围是0到255。很多从别的资料里抄来的HSV范围值,没换算直接用,结果啥也分割不出来。
如果你的目标板用的不是纯色,而是多种颜色混合,我建议优先选视觉面积最大、颜色最稳定的那个颜色作为分割对象。比如白底黑字的板子,你就别试图分割白色,因为在强烈阳光下白色会直接曝成一片,阈值全域塌陷。更好的选择是分割深色边框,或者把字符颜色抠出来,这样即使过曝,颜色特征也还在。
3.3 滤波与形态学:在二值图上做减法
HSV分割之后得到的基本是一张二值图,里面除了目标板区域,还会有很多零散的噪点。处理噪点时,很多人的第一反应是加大高斯模糊。但要注意,高斯模糊是针对灰度图的,对于二值图更合适的操作是形态学。
我们的标准处理顺序是:先做一次闭运算,用cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)把目标板内部因为反光或阴影产生的断裂缝隙连接起来;再做一次开运算,把背景里的小噪点去掉。核的大小很关键,我推荐从3×3开始试,不要一上来就5×5甚至更大。核越大,区域边缘被腐蚀和膨胀得越厉害,最后轮廓定位的像素级精度就越差,透视矫正时角点偏移就越明显。
这个阶段还有一个容易被忽略的点:不要在每帧里重新创建核和形态学的临时Mat。嵌入式平台上内存分配是有开销的,反复创建销毁会拖慢速度。把核和临时变量定义在循环外面,一帧结束后复用。
3.4 动态ROI:把全图搜索变成局部跟踪
预处理做得再好,全图扫描始终是性能瓶颈。目标板在车前方的出现位置,通常是有规律可循的——赛道是连续的,目标板不会瞬移。所以我们的策略是:上一帧找到目标板之后,记录它的中心坐标,下一帧只在以这个坐标为中心、向外扩展一个固定边长的矩形区域里做分割和轮廓查找。车速越快,扩展区域就要越大;车速慢或者直道,可以把这个区域压得很小,计算量能降到原来的五分之一。
这个思路其实就是一个非常轻量的跟踪器。它和光流、KCF那些跟踪算法的区别是,它不预测目标板的运动模型,只是单纯缩小搜索范围。好处是几乎零成本,坏处是一旦连续几帧没找到目标板,ROI就会漂移。处理办法也很简单:如果连续丢失超过两帧,就把ROI重置为全图,重新搜索一次。等目标板重新被锁定,再切回局部模式。
4. 目标板定位与内容识别的主流程设计
4.1 轮廓筛选的“漏斗”:从面积到几何形状
拿到二值图之后,下一步是cv2.findContours。这里建议使用cv2.RETR_EXTERNAL(只检测外轮廓)和cv2.CHAIN_APPROX_SIMPLE(压缩轮廓点)。前者可以避免同一个目标板被内部字符轮廓分割成多个候选,后者可以减少后续计算的顶点数量。
轮廓筛选要设计成一个多级漏斗,每级过滤一批干扰项。第一级是面积过滤:面积太小的轮廓直接丢掉。但这里不能固定一个绝对像素值,因为目标板离车远的时候,在画面里的面积很小,近的时候又很大。我习惯用相对值,比如“面积小于当前ROI区域面积百分之二的轮廓直接忽略”。第二级是宽高比过滤:目标板通常是个矩形,宽高比固定在一个范围里,比如0.5到2.0之间,明显偏离这个区间的直接排除。第三级是矩形度过滤,也就是计算轮廓面积占其最小外接矩形面积的比例。一个规整的矩形目标板,这个比例应该在0.7以上,而树叶、轮胎印、阴影碎片这些不规则形状,比例会低很多。
经过这三层筛选之后,候选轮廓通常就剩下一两个了,后面再做内容识别就轻松很多。
4.2 最小外接矩形与透视矫正:把斜着的板子掰正
找到目标板轮廓之后,用cv2.minAreaRect求最小外接矩形,得到一个旋转矩形,里面包含了中心点、宽高和旋转角度。通过这四个角点和目标板在标准正视图中的四个角点做对应,就能用cv2.getPerspectiveTransform算出透视变换矩阵,再用cv2.warpPerspective把目标板区域矫正成一个正面视角的图像。
透视矫正在目标板识别里非常关键。车子跑到目标板侧面的时候,板子在画面里是一个梯形甚至更畸变的四边形,这会让后续的字符模板匹配非常吃力。矫正之后,不管车子是从什么角度经过,我们都能拿到一张近似正面的、固定尺寸的板内图像,模板匹配或者字符分割的难度大幅下降。
这里我还想提一个更轻量的“卡尺工具”思路。目标板边框通常是一条比较直的边缘,如果在矫正之前就能确定边缘位置,完全可以在ROI内沿法线方向做边缘投影,找到边缘在哪一行哪一列,不需要跑完整轮廓查找。这种卡尺法在OpenCV里没有现成函数,但实现起来很简单:在目标区域内按行扫描灰度突变点,做一个累加投影,峰值位置就是边缘所在。它的速度比轮廓法快,抗光照干扰能力也更强。不过它适合边缘清晰的场景,如果目标板边缘颜色和背景接近,还是得用轮廓法兜底。
4.3 板内内容识别:模板匹配与特征判断的取舍
矫正完成之后,板内内容识别就有两条路可选。
第一条是模板匹配,cv2.matchTemplate,用cv2.TM_CCOEFF_NORMED归一化相关系数作为相似度。这个方法在字符种类固定、字体固定的竞赛场景里非常实用。我们当时的做法是,拍好0到9十个模板,目标板区域矫正后,分别和这十个模板做匹配,得分最高的那个就是识别结果。注意模板大小和目标区域大小要尽量一致,或者匹配前把目标区域缩放到模板的尺寸,否则相关系数会受到尺度差异的影响。
第二条是基于轮廓的字符特征识别。把矫正后的图像二值化,找字符轮廓,然后提取每个字符的面积、宽高比、笔画密度、孔洞数量这些特征,喂给一个非常小型的分类型器。这个方案的优点是泛化能力比模板匹配强,字体变化也能识别;缺点是开发量和调参成本高很多,需要准备样本数据。竞赛场景下我建议先用模板匹配跑通链路,如果确实遇到字体不固定或者倾斜严重的问题,再考虑加特征分类。
这两种方案不是互斥的,也可以结合:先用颜色分割判断板子边框颜色,再用模板匹配确认具体字符,两个结果交叉验证,整体置信度会更高。
4.4 置信度与帧间一致性:让结果更可信
很多队伍在识别成功后就直接输出结果,这是很危险的。模板匹配的得分即使很高,也可能因为光照变化而产生一次性的误判;轮廓特征的组合也可能意外命中背景里某个相似形状。我坚持要给每个识别结果附加置信度,再设置多级处理策略。
具体来说,如果匹配得分超过0.85,直接接受;如果在0.70到0.85之间,暂时不动作,等下一帧结果来确认;如果最近三帧中有至少两帧识别出同一个字符且置信度都超过0.70,就认为这个结果是可信的,输出给控制模块。连续三帧都不满足条件,就丢弃这个目标板,让车子按默认策略行驶。这个“多帧确认”的机制,本质上是在用时间换可靠性,对低速和中高速场景都有效。
还要强调一点:识别结果必须带时间戳。车子的控制模块在拿到结果时,需要知道这个结果是什么时候看到的,因为车在跑,一帧的时间就能前进好几厘米。没有时间戳,控制模块就无法准确计算目标板的位置,触发时机就会偏差。
5. 嵌入式平台上的OpenCV提速与部署优化
5.1 算法侧的提速手段:分辨率、灰度、缓存复用
嵌入式平台上做OpenCV,第一步永远是降低分辨率。640×480采集,预处理阶段可以先缩放到320×240。目标板识别这种任务,320×240下的颜色分割和轮廓定位大部分时候都够用。只有当车已经离目标板很近、需要看清具体字符的时候,才在ROI区域内裁剪原始分辨率的局部图像,放大后再做模板匹配。这样既保证了远处快速发现目标板,又保证了近处的识别精度。
第二个提速手段是全程使用单通道。颜色分割用HSV的话,H通道和S通道可以单独提取,不需要同时处理三个通道。模板匹配本身也支持灰度图。字符识别阶段完全可以用单通道图像完成。三通道转单通道,cv2.cvtColor会消耗不少CPU时间,但要记住这个转换不是必须每次都做,很多操作可以在同一个通道上完成后再统一转换。
第三个提速手段是缓存复用。OpenCV里很多函数会输出新的Mat,如果不加控制,一秒钟跑30帧,就会产生几百次内存分配。识别循环里要尽量用cv2.Mat的复用特性,把一些中间的Mat对象声明在循环体外,利用.setTo(0)或者直接把结果写到同一个Mat上,避免new和delete的反复调用。这个优化看着不起眼,在嵌入式平台上却能让帧率提升10%到20%。
5.2 工程侧的线程与内存优化
纯算法优化是有上限的,想要榨干嵌入式平台的性能,还得在工程架构上下功夫。我强烈建议把图像采集和图像识别分成两个线程:采集线程只负责VideoCapture.read(),把帧放进一个队列;识别线程从队列里取帧做处理。这样即使识别偶尔变慢,采集线程也不会丢帧,处理完之后队列里始终有最新的数据,不会出现“读到旧帧”的延迟问题。
线程之间传递帧的时候,尽量不要直接传Mat对象,因为Mat是浅拷贝,底层数据会共享。更安全的做法是用一个固定长度的环形缓冲区,里面预先分配好N个Mat,采集线程和识别线程交替使用,通过索引来避免拷贝。如果平台内存足够,直接传一份浅拷贝再做深拷贝也可以,但要确认不会出现数据竞争。
嵌入式Linux下面还可以调整CPU的调度策略和频率。比如把识别线程绑到某个CPU核心上,设置实时优先级,可以减少调度延迟。另外,如果OpenCV是编译安装的,可以在CMake配置里只保留用到的模块,比如imgproc、imgcodecs、videoio、calib3d,去掉那些用不到的视频分析、高GUI等功能,库的体积和内存占用能小很多。启动速度也会有明显改善。
5.3 离线回放与参数整定:OpenCV调参不要在现场进行
这是我最想强调的一条经验:所有参数调整都应该在离线回放阶段完成,而不是到了赛场再改代码。赛场上几分钟的调试时间,根本不够你判断一组参数在多种光照下是好是坏。
具体操作是,比赛前用真实赛道录制多段原始视频,每段覆盖一种典型场景(顺光、逆光、阴影、傍晚)。录制时不要用视频压缩,或者用低压缩率编码,避免压缩噪声影响分割质量。然后在电脑上把这套识别代码跑在录制视频上,输出每一帧的识别结果、耗时、目标板中心坐标,保存成日志。再写一个离线评估脚本,统计正确识别率、误检率、漏检率和平均处理耗时。
参数整定可以做得更科学一点。HSV范围、形态学核大小、面积过滤比例这些参数,组合起来是一个高维搜索空间。手动调参效率很低,可以用网格搜索或者粒子群优化算法在离线环境中自动搜索一组最优参数。粒子群在我看来很适合这个场景:参数维度不多,目标函数可以直接定义成“识别正确率减去误检惩罚”,迭代几十轮就能收敛到一组稳定参数。这比人工在现场抱着一台电脑试半天要靠谱得多。找到最优参数后,写进配置文件,比赛时主程序启动时读取,现场只需要加载,不再改代码。
6. 赛场实测中遇到的疑难杂症与排查记录
6.1 高亮过曝与反光:白成一片怎么救
回到开头那个场景,太阳斜射导致目标板过曝,整个板面白成一片。这种问题靠调HSV阈值是解决不了的,因为过曝区域所有颜色都被压缩到白色附近,H通道已经失去意义。我的处理优先级是这样的:第一步,降低曝光时间,让画面整体暗下来,目标板的颜色特征重新出现。第二步,如果降低曝光导致暗部细节丢失,就加一块偏振片,滤掉地面的反射眩光。第三步,改分割策略,不分割白底,而是分割深色边框或字符,因为深色区域在过曝时不会立刻饱和,还能保留下轮廓信息。
如果这三步都来不及,还有一个临场应急的手段:在预处理阶段先把高亮区域的亮度值做一个非线性压缩,比如对V通道做一次幂次变换,把接近255的高光区域压回200以内,让过曝区域重新出现一点点层次感。这个方案对字符边缘的恢复有帮助,但对整体识别率的提升有限,只能算是救急。
6.2 运动模糊与拖影的应对
运动模糊是速度上来之后一定会遇到的问题。最直接的解决办法是缩短曝光时间,但曝光时间太短,画面会变暗,颜色分割的信噪比又不够。这组矛盾需要根据赛道光线条件在赛前提前测试,找到一组“拖影不明显”和“颜色可分”的平衡点。
另一个容易被忽视的因素是卷帘快门。卷帘快门在拍摄快速运动的物体时,会产生竖直方向的空间畸变,目标板在画面里可能变成斜的或弯曲的。这个畸变不是透视导致的,而是传感器逐行曝光的时间差造成的,透视矫正也无法完全修正。如果条件允许,尽量选择全局快门的相机;如果只能用卷帘快门,那么识别时对轮廓宽高比和矩形度的容差要适当放宽,不要因为目标板被拉长了一点就把它丢掉。
处理完硬件层面,再在算法层面对症下药。可以在预处理阶段添加一个轻量的锐化卷积核,让模糊的字符边缘重新变清晰。但锐化不能做太狠,过度锐化会产生强烈的振铃效应,导致轮廓周围出现假边缘,反而干扰矩形度判断。我用的是类似于[[0, -1, 0], [-1, 5, -1], [0, -1, 0]]的核,跑起来基本没有额外开销。
6.3 误检与漏检的平衡艺术
目标板识别的过程中,误检和漏检是此消彼长的关系。阈值放得宽,漏检少但误检多;阈值收紧,误检少但漏检增加。在我个人的实践中,漏检比误检更容易处理,因为控制逻辑可以允许“这一帧没看见目标板”,这样车最多晚一点做出反应,大概率还在安全范围;但误检的代价很高,如果车把背景里一块相似颜色的牌子当成了目标板,可能提前急停或者错误换道,整场比赛就毁了。
因此,阈值设置要偏保守,宁可偶尔漏检,也不要误检。结合前面说的多帧确认机制,这个策略在实际运行中非常稳定。还有一个技巧:不同赛道区域可以加载不同灵敏度配置。比如在高速直道上,识别阈值放宽一些,好让车更早地看到目标板;在急弯附近,把阈值收紧,避免因为路面标志干扰产生误检。这些配置通过一个简单的状态机来切换就行,代码上不复杂,但效果立竿见影。
6.4 调试现场的小习惯
最后分享几个让我省了大量现场时间的调试习惯。
第一,日志必须带时间戳。每一帧识别结果、置信度、耗时、目标板坐标都要写进日志文件,用chrono或std::chrono标好时间点。赛后翻日志定位问题,比在现场盯着屏幕猜要高效太多。第二,保存关键帧截图。触发动作的那一帧和连续丢失目标板的几帧,自动保存原始图和预处理后的二值图。这样即使赛后在棚里,也能根据保存的图片还原当时的场景,找到算法失效的准确原因。第三,在调试画面里实时叠加识别结果,把目标板轮廓、中心点、字符识别结果、置信度直接画在帧上,用窗口显示出来。这个画面不仅方便自己调试,也方便队友快速理解当前视觉状态,排查问题时能少很多沟通成本。
这些习惯看似琐碎,但碰到疑难杂症时,它们是让你从“一头雾水”到“定位根因”最快的一条路。
目标板识别做到最后,拼的不是什么高深算法,而是对图像链路每一环节的掌控。把曝光固定住,把预处理想清楚,把轮廓筛选设计成漏斗,把置信度机制加上,这套组合拳在竞赛里已经足够稳定。如果你也在备赛,我的建议是赛前一周就不要大改算法了,只调参数、跑回放、稳定心态。等上了赛道,车子真的能像“走马观碑”一样,刷地一下跑过去,目标板上的内容一次就读出来,那种感觉确实比拿奖还爽。