智能车竞赛走马观碑组:从目标板识别到绕行策略的完整实现
这届智能车竞赛如果你报了走马观碑组那我要先给你打个预防针这支赛题拼的不再是单纯的速度而是“边跑边看、看完了还要立刻让车做出正确动作”的综合能力。它的核心链路其实不复杂——车辆在赛道上正常行驶目标板预先放在赛道一侧板上的内容决定车辆是直行、绕行还是停车识别目标板要快绕行策略要稳两者缺一个都会在强队面前被拉开差距。这篇文章我就围绕这个完整链路来写从硬件安装、图像预处理、目标板分类、绕行状态机到最终能跑的参考代码一次讲透。无论你是第一次上车调试还是已经有一版基础逻辑想提升可靠性相信都能从中找到能直接抄作业的思路。1. 第一眼就要看懂赛题走马观碑的任务拆解与主要难点1.1 这个组别到底考什么走马观碑这个名字本身就非常形象——“走马”说明车在运动“观碑”说明你要去读赛道旁的牌子。和第二十一届许多赛区的通用设置类似目标板一般摆在赛道一侧车要通过车载摄像头在行进过程中捕捉到这块板识别出板面内容然后根据内容完成规定的通过动作。这里最容易被新人低估的一点是它不是先停车再识别。车辆全程处于运动状态摄像头看到目标板的时间窗口往往只有一两秒甚至更短。你在这个窗口里要完成目标板的发现、定位、内容分类、决策输出最后还要让底层控制执行绕行动作。这已经不是一个单纯的视觉算法题而是一个“视觉 决策 控制”的串联系统题。很多队伍死在“算法单看不差真正跑起来乱跳”这个阶段原因就是没有把它当系统来设计。1.2 “边跑边看”的容错窗口先算一笔账你就明白为什么时间窗口这么重要。假设赛车以2.0 m/s的速度行驶目标板放在赛道旁摄像头大致能在距离目标板2米左右的位置看到板的全貌。那么从首次看到板到车抵达板旁时间大约是1秒。如果摄像头跑到100fps看起来有100帧可用但实际做不了这么乐观——图像传输、算法处理、电机响应都有延迟真正可以用来做“确认识别并决定策略”的有效窗口可能只有0.5到0.7秒。在这个窗口里板在画面中会从小变大运动模糊逐渐加重再加上车本身抖动很多在静态图片上效果很好的识别算法放到连续帧里就会疯狂跳变。所以后续所有算法设计都要围绕“时间有限、单帧不可靠”这两个前提来做。稳健的连续帧确认机制比单帧跑出99%的准确率更重要。1.3 绕行不是打一把方向盘那么简单识别到目标板只是第一步。真正拉开队伍差距的是绕行策略。直行、左右绕行、停车不同目标板内容对应的动作不同车辆执行时又涉及横向偏移、返回中心线、速度控制。这里如果不设计逻辑清晰的状态机车很容易出现两种典型问题要么绕行太晚导致撞板要么绕行完成后回不来直接冲出赛道。而且走马观碑的绕行和我们平时理解的越野避障不一样。赛道宽度有限通常只有几十厘米你在高速下要完成一个可重复的横向偏移靠临时打方向根本不现实。必须提前规划好目标横向偏移量、绕行触发距离、返回中心线的时机把这些参数固化到程序里配合编码器里程计估算位置才能保证每次过板都有一致性。1.4 规则之外的隐形评分标准稳定性很多队伍忽略了一个事情竞赛只有在规定次数内稳定完成才能拿高分。车快但不稳定一次识别误判导致绕错方向轻则罚时重则直接成绩无效。我在准备比赛时给自己定的验收标准是同一个目标板连续跑20次识别和绕行动作的正确率不能低于90%。如果达不到这个数字就说明算法和策略还有隐患不要轻易上赛场。2. 硬件搭建与视野设计摄像头决定识别上限2.1 主控与摄像头选型对比我会把当前主流的方案分成三类方案优点缺点适合场景STM32H7/TC2xx 灰度数字摄像头帧率高、延迟低、功耗小、符合一般比赛限制开发量大图像处理全靠手写调试门槛高基本功扎实、追求极限帧率的队伍MCU OpenMV/数字摄像头模块开发快自带图像处理库上手容易帧率和资源相对有限复杂分类较吃力备赛时间不充裕的团队树莓派/RK系列 USB摄像头可以跑OpenCV和轻量网络调试非常方便功耗较高启动慢部分规则可能限制允许大算力平台的分组或做快速原型验证以二一届大部分赛区常见的光电或缩微平台来看使用普通单片机配数字摄像头的队伍仍然占多数。如果你跟我一样能用较低成本拿到带数字摄像头接口的MCU我建议直接走第一条路线因为比赛现场对实时性和稳定性的要求很高越少依赖复杂系统就越少出幺蛾子。2.2 安装高度、俯仰角与可视范围摄像头安装不是随便找个地方锁上就行。它的高度和俯仰角直接决定你“什么时候能看到目标板”以及“看多远不出画面”。视野拉得太远近处赛道盲区大压得太低远处目标板又来不及识别。我习惯的基准参数是安装高度2028cm俯仰角向下8°15°镜头水平朝向正前方微调让画面地平线大致在屏幕中上部。这样做的原因是让视野形成一个前宽后窄的梯形区既能覆盖车头前方1~3米的赛道范围也能让目标板在2米外就进入画面。具体参数可以这样调摄像头高度越高看得越远但车头盲区和侧向盲区同时增大俯仰角越大近处看得越清但远处目标板入画时间越晚镜头左右位置必须严格居中否则横向偏移的像素换算会产生系统误差。装好摄像头之后一定要做防松动处理。用螺丝加防滑垫片锁紧有条件的话在镜头外围加一个简易遮光罩可以减少顶部灯光直接打在镜头上产生的眩光。这个细节看着不起眼实际跑起来影响非常大。2.3 标定把像素坐标变成场地坐标识别到目标板之后程序不能只告诉主控“画面里有一块板”还要告诉主控“这块板在车的左边还是右边、大概多远”。这是绕行策略能否顺利执行的基础。所以必须做标定把图像像素坐标转换成实际场地坐标。最简单的标定方法是在场地布置一张大棋盘格或者有规则的网格线从摄像头预览画面里记录若干行和列的交叉点坐标再用多项式拟合出“图像纵坐标 → 距离”和“图像横坐标偏差 → 侧向距离”的映射关系。实际使用时直接用查找表插值计算量小完全能满足嵌入式平台的实时性要求。换算公式大致是横向偏差(cm) (目标板中心x - 画面中心x) × 比例系数其中比例系数会随目标板在画面中的纵向位置变化而改变。远处的板一个像素可能对应2cm近处的板一个像素可能只对应0.3cm。因此在做横向偏移计算时不要用固定系数一定要结合纵坐标动态查表。2.4 抗振和抗光赛事中常见的翻车原因不是算法不会写而是摄像头被电机和路面振动晃到图像发飘。镜头松动会带来每帧几像素的抖动对分类和小目标检测影响很大。建议在摄像头支架与车体之间垫一层薄橡胶垫片再用双螺母锁死。光照问题则要区分两种情况如果用的是彩色摄像头务必关闭或固定自动白平衡因为场地灯光色温变化会让同一块目标板在不同时间出现完全不同的颜色如果用的是灰度摄像头优先手工固定曝光时间把自动增益上限限制在较小范围防止画面亮度在上板瞬间突然跳变。3. 目标板识别算法从一帧图像到稳定结果3.1 图像预处理送到分类器之前原始图像一般要先做三步转灰度如果是彩色处理就用HSV颜色空间、高斯模糊去噪、增强对比度。高斯模糊的核不用太大5×5就够太大会把目标板上的细节也抹掉。赛道场景光线复杂建议使用自适应阈值或者对感兴趣区域做局部直方图均衡而不是全图固定阈值。使用彩色摄像头做颜色分割时HSV比RGB好用得多。比如目标板是蓝色板面就可以通过以下阈值把它从赛道灰黑色背景中分离出来import cv2 import numpy as np hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) low_blue np.array([100, 80, 80]) high_blue np.array([130, 255, 255]) mask cv2.inRange(hsv, low_blue, high_blue)这里的上下界必须在实际场地灯光下采样确定不要照搬网上参数否则一到真实场地就会漏检一大堆像素。3.2 候选区域提取目标板在画面中通常是色块连续的区域。基于颜色分割后的掩膜图像可以用轮廓查找拿到所有连通域再通过几个条件过滤面积下限目标板在有效识别区域内至少占一定像素面积面积太小的区域大概率是噪点或远处干扰宽高比范围目标板一般呈矩形宽高比通常在0.53.0之间超出这个范围的区域直接排除矩形度用轮廓面积除以其最小外接矩形面积比值太低的说明形状不规则不太可能是板子位置约束目标板只会出现在赛道两侧的固定范围内程序可以把搜索区域限制在整个画面中部偏左右区域既减少误检也降低计算量。经过这些过滤后如果画面中还有多个候选区域就优先保留面积最大且离赛道边缘最近的一个。处理单目标板时这个策略简单有效。3.3 透视校正与归一化这一步我强烈建议不要省。目标板在赛道上不一定是完全正对摄像头的可能是斜着摆放也可能因为车身姿态导致板面在画面中产生透视变形。如果直接对裁剪下来的梯形区域做分类识别率一定不稳定。做法是先提取目标板的四个角点再用cv2.getPerspectiveTransform做透视变换把原区域统一矫正成固定大小的正视图比如64×32像素这样可以大大降低板面倾斜对分类的影响。同时校正后的图像可以作为分类器的统一输入保证模板匹配或者神经网络的数据分布一致。下面是一个简单的透视校正代码示例def warp_plate(roi_pts, frame, size(96, 48)): dst_pts np.array([ [0, 0], [size[0] - 1, 0], [size[0] - 1, size[1] - 1], [0, size[1] - 1] ], dtypenp.float32) matrix cv2.getPerspectiveTransform(roi_pts.astype(np.float32), dst_pts) warped cv2.warpPerspective(frame, matrix, size) return warped需要注意的是角点检测本身也可能出错。如果板面颜色单一可以考虑改用板子外接矩形的四个顶点来近似角点只要板子本体相对完整效果就足够稳定。3.4 分类方法对比与选型目标板内容分类常见有两条路线传统模板匹配和轻量分类器。模板匹配的优点是代码量小、不依赖训练样本只要目标板的模板是从实际场地采集的在矫正后图像上做归一化匹配就能达到不错的准确率。缺点是它对内容形变、光照变化的容忍度有限。轻量神经网络比如一个只有两三层卷积的小模型在鲁棒性上通常更好能适应目标板在不同角度、不同光照下的变化但前提是你得提前采集一批带有标注的训练数据而且需要MCU上能跑推断。对备赛时间不太充足的队伍我的建议是从模板匹配起步先把整条链路跑通再把分类模块替换成自己训练的轻量模型。接口设计成输入矫正图像、输出类别ID和置信度即可。如果目标是识别板上的汉字或符号模板法需要注意字形是否因透视矫正而发生明显变形矫正后的汉字结构比例必须保持固定。实际项目中我会把矫正尺寸设成4:2甚至2:1而不是简单正方形这样更贴近常见板面长宽比。3.5 时间序列滤波别让单帧误判毁掉全局单帧识别结果不能直接采信。真实比赛里因车辆震动、反光、遮挡某一帧出现误判太正常了。问题的关键不是避免单帧错误而是通过时序逻辑把错误过滤掉。我常用的方案是一个三态确认器状态A没有检测到板状态B连续N帧检测到板但置信度还不稳定状态C连续M帧检测结果一致且置信度超过阈值此时才确认识别成功。确认之后还要设计一个“锁定”机制。一旦进入确认态后续几帧之内就不再轻易改变板类型除非连续多帧出现完全不同的结果并达到另一组阈值。这样可以防止车辆在靠近目标板的最后阶段因为画面模糊导致类别在几个结果间反复横跳。举个例子如果车速2m/s摄像头100fps那连续5帧确认只需要50ms对控制来说完全来得及。设太大反而会导致出决策太晚设太小又过滤不了瞬时误判。具体N和M取值可以在实测中根据稳定性来调整一般建议M不小于3。4. 绕行策略设计识别出来之后如何“优雅”地绕4.1 明确绕行目标线的几何关系走马观碑里的绕行和普通避障最大的不同是你不一定清楚障碍物的物理边界你只知道“目标板要求我往左绕”或者“往右绕”。因此绕行策略的本质是把车从当前中心线切换到一条目标偏移线上再在越过目标板区域后回到中心线。以赛道中心线为0假设规则要求左绕行时车辆整体左移1520cm右绕行时右移1520cm。这个偏移量需要根据赛道宽度和车宽来定。偏移量太小可能压到目标板或赛道边界偏移量太大容易冲出赛道或碰到另一侧。我建议在正式比赛前用实车连续测试找到能稳定通过的偏移区间然后取中间值。4.2 状态机设计四段式通过我在程序中把绕行过程拆成了四个状态状态进入条件目标横向偏移速度策略TRACK正常巡线未确认目标板0加速至巡航速度APPROACH已确认目标板板在车前方从0平滑过渡到目标偏移减速10%20%PASSING目标板进入车侧旁视觉可能丢失保持目标偏移保持低速稳定通过RETURN根据里程计判断已越过板区域从目标偏移平滑回到0逐步恢复巡航速度TRACK状态下程序正常运行赛道巡线一旦视觉给出确认结果就进入APPROACH在指定距离内完成横向偏移当编码器里程计判定车辆已经过了目标板所在位置立即进入RETURN回归中心线。这里有个容易被忽略的点PASSING阶段摄像头很可能已经看不到目标板了。所以触发RETURN不能依赖视觉必须依赖编码器里程计或者时间估算。我在代码里会记录下进入APPROACH时的总里程然后加上一个预设的通过距离当当前里程超过这个值时就自动切换状态。4.3 平滑过渡PD控制器和速度匹配绕行过程中最忌讳的就是横向偏移突然阶跃。如果你在APPROACH状态直接给一个固定的目标偏置量车的转向会变得很猛高速下极易侧滑或冲出赛道。正确做法是让目标横向偏移量随时间平滑变化再配合一个PD控制器输出转向量。一个简单有效的目标偏移生成方式把横向偏移规划成一条梯形曲线先线性增大保持一段时间再线性减小。对控制环来说目标点必须是连续变化的不能跳变。转向控制用PD就能满足多数情况比例项负责纠正横向偏差微分项负责抑制横向速度的震荡。基本公式是steer Kp × (target_lateral - current_lateral) - Kd × lateral_speedKp和Kd的整定建议从Kp较小开始在实车上逐步增加直到车辆过板过程没有明显振荡。也可以用一阶低通滤波把目标横向偏移做平滑后输入给PID效果类似但更容易调试。速度策略同样要配合。以我跑过的赛道为例巡航2.0m/s时APPROACH阶段降到1.7m/s左右PASSING阶段保持1.51.6m/sRETURN阶段再缓慢加回2.0m/s。任何速度变化都不能突然要在几十毫秒内斜坡过渡。4.4 识别置信度低时的兜底策略比赛中总有那么几次目标板反光、被阴影遮挡、识别置信度达不到确认阈值。这时不能什么都不做让车直奔目标板。我建议给程序设置一个stale状态在接近目标板但未确认类别的最后时刻触发一个保守策略。保守策略怎么选取决于规则和风险偏好。如果目标板上“绕行”与“直行”的概率各占一半那保守策略一般是“执行偏移量较大的绕行”因为如果实际要求是直行你多偏一点可能只是压线但如果实际要求是绕行而你直行了可能直接撞板或者冲出赛道。相比之下默认绕行的容错空间更大。如果规则允许停车且停车不扣太多分把“未知类别”映射成停车也是一个稳妥的备选方案。这个兜底策略建议做成可在配置文件中切换的项比赛现场根据试跑情况随时调整而不是重新烧录代码。5. 可运行的实战代码与移植要点5.1 代码结构和运行环境下面这套代码我用Python OpenCV在笔记本电脑上跑通主要拿来验证识别和状态机逻辑。你移植到MCU时完全可以把调试显示部分去掉只保留颜色阈值、轮廓筛选、透视分类以及状态切换逻辑。代码的思路是“调通再移植”不是直接嵌入式部署。5.2 主循环和目标板检测import cv2 import numpy as np LOW_COLOR np.array([100, 80, 80]) HIGH_COLOR np.array([130, 255, 255]) MIN_AREA 120 MIN_W 12 MIN_H 12 ASPECT_RANGE (0.5, 3.0) def detect_board(frame): hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, LOW_COLOR, HIGH_COLOR) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((5,5), np.uint8)) contours, _ cv2.findContours( mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) best None best_area MIN_AREA for c in contours: area cv2.contourArea(c) if area MIN_AREA: continue x, y, w, h cv2.boundingRect(c) if w MIN_W or h MIN_H: continue aspect w / h if not (ASPECT_RANGE[0] aspect ASPECT_RANGE[1]): continue rect_area w * h rect_ratio area / rect_area if rect_ratio 0.6: continue if area best_area: best_area area best (x, y, w, h) if best is None: return None, None x, y, w, h best roi_pts np.array([[x, y], [x w, y], [x w, y h], [x, y h]]) roi_warped warp_plate(roi_pts, frame, size(96, 48)) return roi_warped, (x, y, int(w), int(h))这里用到了warp_plate就是前面透视校正那段函数。形态学闭运算的作用是把目标板表面细小的反光裂缝补上避免板区域被分割成好几块。5.3 模板分类与状态机class State: TRACK 0 APPROACH 1 PASSING 2 RETURN 3 class BoardID: STRAIGHT 0 AVOID_LEFT 1 AVOID_RIGHT 2 STOP 3 UNKNOWN 4 def classify_board(roi, templates): if roi is None: return BoardID.UNKNOWN, 0.0 gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) best_id BoardID.UNKNOWN best_score -1.0 for b_id, tmpl_gray in templates.items(): res cv2.matchTemplate(gray, tmpl_gray, cv2.TM_CCOEFF_NORMED) score res.max() if score best_score: best_score score best_id b_id return best_id, best_score状态机的主逻辑长这样class AvoidStateMachine: def __init__(self): self.state State.TRACK self.confirm_count 0 self.target_offset 0.0 self.entry_mileage 0.0 def update(self, board_id, conf, mileage, low_conf_trigger): if self.state State.TRACK: if board_id in (BoardID.AVOID_LEFT, BoardID.AVOID_RIGHT, BoardID.STRAIGHT): self.confirm_count 1 else: self.confirm_count 0 if self.confirm_count 3: self.state State.APPROACH self.entry_mileage mileage if board_id BoardID.AVOID_LEFT: self.target_offset 0.18 elif board_id BoardID.AVOID_RIGHT: self.target_offset -0.18 elif board_id BoardID.STRAIGHT: self.target_offset 0.0 else: self.target_offset 0.0 elif self.state State.APPROACH: # 当行驶距离达到预设值后认为已进入过板区 if mileage - self.entry_mileage 0.35: self.state State.PASSING elif self.state State.PASSING: if mileage - self.entry_mileage 1.2: self.state State.RETURN elif self.state State.RETURN: self.target_offset 0.0 if mileage - self.entry_mileage 1.8: self.state State.TRACK self.confirm_count 0代码里我直接用了AVOID_LEFT、AVOID_RIGHT、STRAIGHT三种类别实际操作时你可以把模板字典换成自己采的样本。横向偏移0.18m是一个参考值必须结合赛道宽度和规则里的板位置来调整。5.4 参数调试清单下面这些参数是实际调试最容易改的建议全部做成变量放到程序开头集中配置曝光时间固定值不要自动HSV阈值上下界根据现场灯光采样形态学核大小反光严重时可从3×3调到7×7MIN_AREA目标板在有效距离下占的像素面积太小容易误检模板匹配的模板尺寸统一为矫正后的尺寸确认帧数N建议35APPROACH/PASSING/RETURN的里程阈值需要实车测试后标定。6. 实车调试中的坑与对应方法6.1 实验室能识别、场地疯狂误判这是最气人的一幕。原因通常有两个实验室灯光均匀场地灯光可能出现大面积高光或频闪实验室没有电机振动干扰场地上一开机整个画面都在抖。解决办法是把所有可能自动调整的图像参数全部固定包括曝光、增益、白平衡。同时颜色分割的HSV阈值一定要从场地实拍图像里重新采样不要用实验室里调的阈值。可以录制一段20秒场地视频离线调好阈值再烧进程序效率高很多。6.2 目标板反光导致识别对象断裂板面反光会让一部分像素被判成白色高光原本完整的色块被打碎轮廓提取自然不稳定。形态学闭运算是常规手段但光靠它还不够。更有效的办法是在进入分类之前先对矫正后的图像进行直方图均衡压制高光的绝对亮度让板面内部灰度分布更平均。如果用的是彩色摄像头可以在HSV空间里提高saturation的低阈值让高光部分即使亮度高也因饱和度低而被排除在外。6.3 车身振动和运动模糊运动中图像模糊是必然现象只是程度不同。处理思路有三个缩短曝光时间把曝光从常见的几毫秒压到1ms以内能大幅度缓解拖影加固摄像头支架用橡胶垫片吸收一部分高频振动调整识别时机提前在距离较远、车速还没起来的时候就完成首次识别利用APPROACH阶段余下的时间做确认。运动模糊不能靠后处理算法完全消除最好的办法就是不给它出现的机会。6.4 误报成板的干扰源场边观众、调试电脑、反光地面都可能被当成目标板。最有效的限制方式是把识别ROI缩小。程序只允许目标板出现在画面中固定的几条竖带区域位置明显不合法的候选区域直接忽略。同时利用“板子不会连续多帧都出现在同一位置”这个特点用时序确认器过滤单帧误报。连续5帧都同一个位置出现相似颜色区域才能进入确认流程基本能挡住偶发干扰。6.5 调试工具日志回放与图像标注比赛现场和实验室最大的区别是你不能在场地中央停下来看变量。所以我强烈建议程序在跑车时保存原始图像数据并在每一帧上叠加目标框、识别结果、状态机状态和转向输出跑完一版后导出成视频回放。很多现场偶发问题靠回放逐帧看可能一眼就能定位到是曝光跳变还是状态机提前切换。7. 参赛节奏与团队协作的建议7.1 先保通再优化我的建议是第一版程序不要追求高帧率、不要追求复杂分类网络。用最简单粗暴的颜色阈值加模板匹配先把“识别到目标板并完成绕行”这个闭环跑通。哪怕速度只有1m/s哪怕绕行距离很保守也比一个连板都找不到的半成品强。链路通了以后再逐渐提高速度、优化直线段效率。7.2 数据驱动调参很多队伍在场地调试时是“拍脑袋调参”一个参数一个参数试折腾一晚上也找不到问题。更好的做法是提前录制不同光照、不同角度、不同距离下的目标板图像和负样本在电脑上批量回放用脚本统计识别率。这样每次改动参数都能得到一组数据而不是靠感觉。负样本尤其重要至少要包括赛道空景、有人经过、地面反光、其他车的车影这些常见干扰场景。7.3 备份方案与现场心态现场试跑和平时调车永远是两码事。天气、灯管老化、电池电压下降都会导致识别质量变化。我的做法是在程序里预留两套参数一套针对稳定光照一套针对强烈反光。现场如果发现第一套参数表现不佳通过串口或按键一键切换不用重新编译。正式比赛的时候宁可把车速降到平时练习的85%90%也要把识别和绕行动作的稳定性保住。一个完成所有动作但速度稍慢的成绩往往比冲得飞快但识别失败的策略拿分更高。根据我个人连续几场比赛的经验走马观碑这类赛题真正的技术分水岭不在某个算法的花哨程度而在整条链路的稳定性。识别目标板能提前、绕行策略能闭环、单帧噪声能被时序逻辑挡住比赛大概率就稳了。希望这篇从硬件到算法再到策略的复盘能让你在备赛路上少走几个弯路。