第21届智能车竞赛结束后,笔者整理调试日志时发现,真正让队伍止步于赛区二等奖的,不是识别算法不够先进,而是大量基础环节没有形成闭环。走马观碑赛项的难点并不在某个单独模块,而在高速运动状态下,图像采集、识别、决策和控制必须稳定配合。这篇文章以我们队伍的赛后复盘为基础,记录从方案选型到现场调车的完整过程,重点写哪些参数真正影响结果、哪些问题每次比赛都会出现,以及下届队伍可以提前避免的坑。
1. 从“走马观碑”赛项看视觉智能车的技术主线
1.1 “走马观碑”到底考什么
“走马观碑”原本形容骑马行进中还能看清碑文,是一种极快的观察能力。放在智能车竞赛里,这个赛项的核心场景是:车模不能减速太多,同时又要在赛道指定区域准确识别标牌或字符信息,并根据识别结果完成后续动作。难点不在于单独跑得快,也不在于单独识别得准,而在于两者同时满足。
车辆高速运动时,摄像头画面会产生动态模糊、过曝或欠曝、赛道元素快速切换,控制周期也会因为算法耗时被拉长。只要其中一个环节出现几十毫秒的异常,整车就会偏离赛道或漏识别。在实际备赛过程中,我们最容易犯的错误是把它当成“视觉识别项目”来做,花大量时间训练模板,忽略了机械、供电、控制周期这些基础因素。等到实车测试时才发现,实验室里准确的识别在赛道上完全不稳。赛后复盘得出的第一个结论是:走马观碑是一个系统工程问题,不是单点算法问题。
1.2 技术主线:图像采集、识别、决策、控制的闭环
整车的软件任务可以拆成一条链路:
- 图像采集:摄像头按固定帧率输出图像,主控读取并完成预处理。
- 赛道与标识识别:从图像中提取赛道边界、中线,并在识别区找到目标字符或图形。
- 决策:根据当前赛道状态和目标识别结果,决定是否减速、直行、转向或停车。
- 控制:将决策转换成舵机角度和电机转速。
这条链路上的每部分都会消耗时间。我们在赛前用逻辑分析仪大致测过各环节耗时,摄像头曝光一般在几毫秒到十几毫秒,图像预处理和数据传输约几毫秒到几十毫秒,识别算法则可能从几毫秒到上百毫秒,取决于模板数量和图像尺寸。最终整个循环如果超过控制周期,车模就会“一步一顿”,出现转弯滞后。走马观碑对实时性的要求比普通电磁循迹高很多,因为既要“看得见”,又要“记得住”,还要“反应得过来”。
所以本文后面的所有内容,都是围绕同一个目标:在保证识别可靠的前提下,把整车闭环延迟压到可控范围,并把现场不稳定因素提前排除。
1.3 复盘范围与阅读建议
这篇文章不是官方赛题解析,而是工程复盘。我们使用的主控是竞赛中常见的单片机平台,传感器以灰度摄像头、编码器和 IMU 为主。如果你用的是其他平台,代码不能直接拷贝,但调试流程、参数影响和踩坑点可以复用。
建议正在备赛的队伍重点关注硬件装配、参数整定和现场排查三章,这三个部分最容易让一支看起来配置很高的车模在赛场上跑出意外成绩。
2. 赛前方案选型:为什么我们选择了“摄像头+编码器+陀螺仪”组合
2.1 常用感知方案对比
智能车竞赛里常见的感知方案可以归为几类。做选型时不能只看检测精度,还要看实时性、开发成本和赛项匹配度。
| 方案 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| 电磁巡线 | 抗光照干扰,计算量小,实时性高 | 只能感知路径,无法识别字符或图形 | 纯循迹、基础竞速 |
| 灰度摄像头 | 分辨率低但帧率高,适合高速场景 | 对曝光敏感,需要处理光照变化 | 赛道边界提取、简单符号识别 |
| 彩色摄像头 | 颜色信息丰富,便于识别色块和图形 | 数据量大,处理耗时,受光线影响更明显 | 需要颜色区分的视觉组 |
| 摄像头+辅助定位 | 通过二维码、色标或反射点辅助定位 | 需要额外布置标记,赛道改动后要重标定 | 固定场景下的精确停车、识别区定位 |
我们最终选择“灰度摄像头+编码器+IMU”的组合,原因很直接:赛项要求在行进过程中识别目标信息,电磁方案天然不具备能力;彩色摄像头在当时的算力条件下很难稳定跑满帧率;灰度摄像头虽然信息量少,但帧率高、处理快,配合固定安装角度和良好曝光,足以完成赛道边界和标识识别。
2.2 主控与传感器选型注意点
主控选择主要看三点:图像处理能力、外设接口数量和团队熟悉度。常见的 TC264、TC377、STM32H7 和 i.MX RT 系列都可以完成这类任务。我们队对 TC264 比较熟悉,所以最后使用它作为主控。要注意的是,单片机的图像处理能力和 PC 完全不同,不要在 PC 上写完识别算法就直接移植,要提前确认内存占用和单帧耗时。
传感器方面,我们使用了:
- 灰度摄像头:分辨率设置为 188×120 左右,这个分辨率对赛道边界足够,也能降低二值化耗时。
- 编码器:安装在驱动电机输出轴上,用于测速和速度闭环。
- IMU:用于陀螺仪角速度辅助转向,特别是在高速进入弯道时,可以提前感知车模姿态变化。
选型时还有一个容易忽略的点:传感器之间的时间对齐。摄像头图像是“某一瞬间”的画面,编码器和 IMU 数据是“持续更新”的值,如果直接用当前编码器值去匹配上一帧图像,控制误差会增大。我们的做法是在摄像头场中断触发时锁存一次编码器计数,并读取一次 IMU 数据,让这一帧图像和同一时刻的速度、姿态绑定。
2.3 为什么不能只靠“离线识别”
备赛初期,我们尝试过在 PC 上对图像做高精度识别,效果很好,但移植到单片机后速度直线下降。原因是模板匹配和特征提取的耗时没有控制住。走马观碑场景要求在车辆运动过程中识别目标,也就意味着每一帧图像只有一次机会,错过之后车可能已经越过识别区。
赛前我们最终确定了一条原则:识别算法必须保证单帧处理时间低于控制周期,如果做不到,就把识别区目标速度降低,用多帧投票换取可靠率。这个取舍很重要。与其追求“每一帧都识别”,不如把识别区变成一个小状态机:车辆识别到进入区域后,主动将目标速度从高速降到低速,连续抓取几帧进行识别,然后对结果做投票。这样虽然平均速度降低了,但整场比赛的稳定性更高。很多队伍就是因为不愿意在识别区减速,导致漏识别后被判失败。
3. 硬件与机械装配的细节复盘
3.1 摄像头安装:高度、俯仰角与前瞻距离
摄像头安装是最能体现“差之毫厘,谬以千里”的地方。如果摄像头装得太低,视野只能覆盖车头前方很近的区域,高速时转向根本来不及;如果装得太高,虽然看得远,但图像抖动会被放大,弯道时容易丢失近处的赛道边界。
我们最终把摄像头高度控制在 20cm 到 30cm 之间,俯仰角在 15 度到 20 度之间,让视野近端和远端都有赛道信息。这里的具体数值不是标准,不同车模和赛道比例需要重新标定。
前瞻距离是另一个核心参数。它的含义是摄像头图像远端对应的实际地面距离。相同分辨率下,前瞻越远,车辆能越早看到弯道,但远端图像放大倍数大,像素对应的实际距离变大,边界识别精度会下降。我们的做法是先用一块白板在地面标记出 0.5m、1.0m、1.5m 三条线,通过调整俯仰角让图像中的远端边界落在目标位置。高速方案可以选择 1.2m 到 1.5m,但前提是机械刚性足够,摄像头不会在急加速时抖动。
3.2 重心、轮胎与机械刚性的影响
视觉车的重心位置比电磁车更敏感。车模加速和急弯时,重心如果偏前,容易造成前轮负载过大;如果偏后,加速时车头会明显抬起,导致摄像头视野突然变远。我们把电池尽量靠近车模中心偏后位置,并使用扎带和魔术贴固定,避免电池在急刹时移动。
轮胎方面,要检查胎面是否均匀着地。很多车模使用软质轮胎,但新胎和旧胎的抓地力差别很大。比赛前一定要用酒精清洁胎面,赛道上的灰尘和轮胎脱落的橡胶颗粒会让抓地力在几分钟内持续变化。机械结构上的螺丝也要定期检查,摄像头支架使用尼龙件时尤其容易在长时间震动后松动。我们在现场发现过两次摄像头角度偏移,都是支架螺丝松动导致,后续直接用螺丝胶固定关键位置。
3.3 供电、共地与图像干扰
电机和大功率舵机是图像干扰的主要来源。现象是图像上出现周期性横纹或雪花点,有时还伴随舵机抖动。这类问题不是算法能解决的,需要从供电走线入手。我们的处理方式包括:
- 电机电源和主控电源分开走线,避免大电流回流路径穿越摄像头信号线。
- 摄像头、主控、电机驱动之间保证“可靠共地”,地线不要形成环路。
- 在电机两端并联吸收电容,舵机电源入口增加滤波电容。
- 摄像头数据线尽量使用短排线,并且避开电机线和舵机线。
这里要强调的是,用示波器观察供电电压比反复调曝光参数更有效。我们曾经花了两天时间调整二值化阈值,结果发现是舵机启动瞬间把摄像头电源拉低,画面整体变暗。解决好供电后,原来的二值化阈值甚至不需要改。
3.4 线材整备与现场维护
比赛现场最怕的不是代码 bug,而是线材接触不良。我们在赛前把所有排线、电源线都做了标签,并在接口位置点了一点热熔胶,防止震动中脱落。现场维护时,第一件事不是改参数,而是检查每一根线是否牢固,尤其是摄像头排线、编码器线和电池插头。这个习惯看起来非常基础,却能避免大量“灵异故障”。
4. 软件系统架构与关键模块实现
4.1 用状态机管理不同赛道阶段
走马观碑的赛道不会只有一种路况,一般会包含直道、弯道、十字、坡道或识别区。我们不能用同一套参数跑全程,否则会出现直道速度太慢、弯道转向太猛的问题。比较稳妥的做法是设计一个驾驶状态机,让不同阶段使用不同控制策略。
下面是一个简化的状态机设计,只用于说明思路,具体状态要根据赛题定义:
typedef enum { STATE_START = 0, // 启动等待 STATE_RUNNING, // 正常赛道行驶 STATE_APPROACH, // 接近识别区 STATE_RECOGNIZE, // 识别区低速识别 STATE_ACTION, // 根据识别结果执行动作 STATE_STOP // 停车 } CarState; CarState current_state = STATE_START;状态切换条件要尽量简单,避免因为识别抖动导致状态频繁跳变。比如从 RUNNING 切到 APPROACH,可以同时依赖赛道元素标志和历史帧计数,连续 5 帧判定进入识别区后再切换,而不是看到一帧特征就切换。
4.2 图像预处理与赛道中线提取
图像处理的第一步是灰度化和二值化。灰度摄像头输出本身是灰度图,二值化需要选择一个阈值,把赛道和背景分开。固定阈值虽然在实验室好用,但赛场的灯光和投影亮度变化会导致整幅图像亮度整体偏移。我们最终采用动态阈值方法:统计当前帧图像的灰度直方图,用大津法计算阈值。这个方法计算量不大,但能明显提高光照变化下的稳定性。
二值化之后,需要从图像底部向上扫描,找到每一行的赛道左边界和右边界,然后计算中线。一个常见的坑是:图像最底部几行会因为车头遮挡或视野过近出现大面积黑边,直接扫描会得到错误边界。我们的做法是从图像底部向上找一个有效起始行,跳过头几行和接近消失点处的不可靠区域。
下面是一段示意代码,用于说明中线提取的循环结构,不是完整工程代码:
#define IMAGE_W 188 #define IMAGE_H 120 uint8_t binary[IMAGE_H][IMAGE_W]; int left_bound[IMAGE_H]; int right_bound[IMAGE_H]; int center_line[IMAGE_H]; int valid_rows = 0; for (int row = IMAGE_H - 20; row >= 20; row--) { int left = -1; int right = -1; for (int col = 0; col < IMAGE_W; col++) { if (binary[row][col] == 1) { left = col; break; } } for (int col = IMAGE_W - 1; col >= 0; col--) { if (binary[row][col] == 1) { right = col; break; } } if (left >= 0 && right >= 0 && right - left > 5) { left_bound[row] = left; right_bound[row] = right; center_line[row] = (left + right) / 2; valid_rows++; } }这段代码的实际问题是:如果赛道元素不是简单黑底白线,而是“白底黑线”,二值化逻辑需要反过来;左右边界搜索也可能把阴影当成赛道。所以在真实工程里,建议先在 PC 上保存几帧图像,把处理结果可视化,确认边界提取的正确性,再移植到单片机上。不要直接在车上调像素级逻辑,效率太低。
4.3 识别区目标识别:多帧投票代替单帧判断
走马观碑赛项最特殊的部分,是车辆需要在运动过程中识别预设的目标信息。我们的方案是在识别区前提前减速,然后连续采集多帧图像,对目标区域做识别,最后用多帧投票决定结果。这样可以避免单帧模糊、遮挡或反光造成的误判。
识别流程可以分成四步:
- 定位:通过赛道状态机判断已经进入识别区,在图像中裁剪出目标区域。
- 预处理:对目标区域做缩放、灰度均衡或二值化,统一送入识别函数。
- 识别:使用模板匹配或简单特征匹配,输出候选结果和置信度。
- 决策:维护一个长度为奇数的结果队列,比如 7 帧,每帧得到一个候选结果,最终取出现次数最多的结果作为最终输出。
示意逻辑如下:
#define VOTE_FRAMES 7 char vote_buffer[VOTE_FRAMES]; int vote_index = 0; char recognize_zone(uint8_t *roi, int w, int h) { // 返回候选结果字符,例如 'A'、'B',或 0 表示无法识别 return match_template(roi, w, h); } void update_vote(char result) { vote_buffer[vote_index % VOTE_FRAMES] = result; vote_index++; } char get_vote_result(void) { int count[8] = {0}; for (int i = 0; i < VOTE_FRAMES; i++) { if (vote_buffer[i] != 0) { count[vote_buffer[i] - 'A' + 1]++; } } int max_count = 0; char best = 0; for (int i = 1; i <= 4; i++) { if (count[i] > max_count) { max_count = count[i]; best = 'A' + i - 1; } } return best; }这段代码的关键不是模板匹配本身,而是用历史帧结果对“单帧误判”做抑制。实际比赛中,车辆通过识别区的时间可能只有几百毫秒,7 帧投票在低速下是可行的。如果速度降不下来,可以减少投票帧数,改为“连续 3 帧同一结果即确认”。这里的取舍是:帧数越多越稳定,但需要识别区距离更长;帧数越少反应越快,但误判率上升。建议备赛时用录制的图像序列离线测试不同帧数下的准确率。
4.4 转向控制与速度控制
整车控制可以拆成两个环路:转向环和速度环。转向环的输出是舵机角度,速度环的输出是电机 PWM。
转向控制最常用的是 PD 控制:
float error = target_center - current_center; float diff = error - last_error; float steering = kp * error + kd * diff; last_error = error;error 可以取图像中线偏差,也可以综合 IMU 角速度。实际调车时,kp 决定入弯的快慢,kd 决定转向的阻尼。kp 过大容易出现高频抖动,kd 过大会导致转向反应迟钝。我们的初值一般设 kp 在 0.3 到 0.8,kd 在 0.5 到 1.5,但不同车模的舵机行程和机械结构差异很大,必须以实车表现为准。
速度控制使用 PI 控制:
float speed_error = target_speed - current_speed; integral += speed_error; if (integral > INTEGRAL_LIMIT) integral = INTEGRAL_LIMIT; if (integral < -INTEGRAL_LIMIT) integral = -INTEGRAL_LIMIT; float motor_pwm = kp_speed * speed_error + ki_speed * integral;在识别区和坡道,目标速度要单独设定。我们会在进入识别区前将目标速度从 2.5m/s 降到 1.2m/s 左右,具体值取决于识别区长度和摄像头帧率。速度降低后,识别成功率明显提升,代价是平均速度下降。这里不要贪快,因为一次漏识别造成的罚时或判负,远大于降速损失的时间。
4.5 数据记录:让系统可回溯
赛后复盘最有价值的基础设施是数据记录。我们在车上加入了 SD 卡记录功能,每 50ms 记录一帧包含时间戳、车速、舵机开度、当前状态、图像二值化结果摘要和识别结果的数据块。现场出问题时,直接下载日志定位是哪一帧开始偏离、识别是否丢帧、控制量是否饱和。没有日志,一切失败原因都只能靠猜。
5. 参数整定与调试方法总结
5.1 分级调试:从静态到动态
我们最开始的错误是直接在赛道上全速调试,出了问题不知道是机械、图像还是控制。后来改成四级调试流程:
- 静态检查:车模静止时检查摄像头画面是否清晰、二值化是否稳定、舵机左右行程是否一致。
- 低速直道:验证图像中线提取和速度闭环是否正常,观察车模是否能沿直线稳定行驶。
- 低速弯道:逐步增加转向 PD,观察入弯和出弯是否流畅。
- 高速组合:加入识别区、坡道等组合元素,验证状态机切换和识别流程。
每一级通过后再进入下一级。如果低速直道都走不稳,就不应该继续调高速。很多队伍在紧张备赛时试图“一步到位”,结果反而浪费更多时间。
5.2 关键参数速查表
下面这张表整理了我们调试过程中影响最大的参数,以及参数调大调小时的表现。参数默认值只是参考,不同车模和赛道环境需要重新标定。
| 参数 | 作用 | 参考初值 | 调大表现 | 调小表现 |
|---|---|---|---|---|
| 二值化阈值 | 区分赛道和背景 | 动态阈值 | 赛道变细或断线 | 背景噪点增多 |
| 摄像头前瞻距离 | 决定图像远端视野 | 1.2m | 提前发现弯道,但远端抖动大 | 入弯反应慢 |
| 转向比例系数 Kp | 决定转向强度 | 0.5 | 入弯快,容易抖动 | 弯道转不过去 |
| 转向微分系数 Kd | 决定转向阻尼 | 1.0 | 转向钝 | 高频抖动 |
| 速度比例系数 | 决定加速响应 | 经验值 | 响应快,容易超调 | 加速慢 |
| 速度积分上限 | 限制积分项 | 略小于最大 PWM | 消除稳态误差但易过冲 | 低速爬行无力 |
| 识别区目标速度 | 决定识别时的车速 | 1.2m/s | 识别时间短 | 通过时间过长 |
| 投票帧数 | 决定识别稳定度 | 7 | 更稳但需要更远识别区 | 反应快但误判多 |
这里的每一项都不能单独死调。例如调大前瞻距离后,转向 Kp 可能需要同时调整,因为远端误差变化率不同。调试时每次只改一个参数,记录前后行车表现,不要同时改三个参数,否则无法判断是哪个改动产生了影响。
5.3 用“失败样本库”复现问题
智能车调试最怕“时好时坏”。如果同一段赛道有时能跑过,有时会冲出,说明存在未覆盖的不稳定因素。我们后来建立了一个失败样本库:每次调试时,只要车模出现异常,就保存当时的图像序列、速度和控制量。修改算法后,再跑同一段赛道,用旧样本回归验证。这个习惯大大减少了“改完 A 问题出现 B 问题”的情况。
建立失败样本库的关键是“能稳定复现”。如果一个问题没有固定触发条件,就先收集多组数据找共同点。比如我们发现高速右弯时经常丢线,回放图像后发现是远端灯光在弯道出口形成高亮区域,导致二值化把赛道和背景混在一起。后来在图像处理里增加了针对高亮区域的抑制逻辑,问题消失。如果没有日志回放,这个原因可能永远找不到。
5.4 学习环境与赛场环境的差异
实验室和赛场环境的差异远比想象中大。赛场的灯光通常是顶部大面积灯带,光线方向、亮度和色温都和实验室不同;赛道表面可能有反光;现场其他队伍的无线设备可能造成干扰。所以最晚在赛前一周,就要按照预计赛场条件调整图像曝光和阈值策略。
一个实用的做法是让程序支持两种模式:实验室标定模式和赛场快速标定模式。赛场快速标定模式下,发车前先让车模在起跑线上静止拍摄几帧图像,程序自动计算当前环境的动态阈值和亮度补偿参数。这样即使现场光照变化,也不用手动改代码。
6. 现场犯规、掉线与失误的排查清单
6.1 高频失误现象和处理方案
比赛现场出现的问题通常集中在几个固定场景。下面这张表是我们队伍和其他队伍交流后整理的常见问题,处理方式偏工程经验。
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 发车后立刻冲出赛道 | 摄像头没对准、阈值错误、舵机中值漂移 | 检查图像画面和舵机行程 | 重新标定中值,确认二值化正常 |
| 识别区没有识别结果 | 车速太快、图像模糊、提前减速不够 | 回放日志中识别帧 | 降低识别区目标速度,增加投票帧数 |
| 运行中突然复位 | 电压跌落、看门狗超时、程序异常 | 检查供电波形和日志 | 增加滤波电容,检查死循环或内存越界 |
| 图像花屏或横纹 | 供电干扰、排线松动 | 示波器看电源,晃动线材 | 重新走线,短排线固定 |
| 舵机左右抖动 | 转向 Kp 过大、机械间隙大 | 低速直道观察 | 降低 Kp,检查舵机连杆 |
| 无线串口断开 | 现场干扰、串口线松动 | 重插线或更换模块 | 备用有线串口,重要数据走 SD 卡 |
这些问题的共同特点是,它们都不需要在算法层面做复杂修改,大部分是工程约束没做好。
6.2 现场三分钟调试顺序
正式比赛前,可以按固定顺序检查车辆,避免遗漏。我们最终固定的顺序是:
- 检查电池电压和插头是否牢固。
- 上电后确认指示灯状态和无线串口连接。
- 查看摄像头图像是否清晰,二值化是否正常。
- 手工推动车模,观察舵机是否跟随转向。
- 缓慢加速测试,观察速度闭环和转向是否稳定。
- 检查所有螺丝、排线和轮胎清洁度。
这个顺序总共大约三分钟。如果三分钟内发现问题,优先解决机械和供电问题,不要急着调识别参数。很多队伍在赛场上一紧张,就反复修改 Kp 和阈值,实际上车模本身已经存在机械松动。
6.3 规则确认与备份
比赛规则要提前确认,尤其关于识别区、允许使用的传感器和停车方式。不要因为规则理解偏差导致被判无效。现场需要准备至少两套程序备份,一套是“常用参数”,另一套是“保守参数”,当常用参数在赛场上不稳定时,可以临时切换保守参数,保证完赛率。保守参数的核心是降低目标速度、提高投票帧数、减少激进控制。
7. 赛后复盘:建议下届队伍优先做的五件事
7.1 尽早搭建数据回放系统
这是所有建议里投入产出比最高的一项。哪怕只是一个能保存图像和关键参数的简单模块,也能让调试从“碰运气”变成“查证据”。建议在备赛第一周就把日志功能写入工程,而不是等到比赛前才补。
7.2 固定机械状态,再调代码
机械结构要尽量固定下来,不要每天既改摄像头角度又改 PID。机械参数一旦变化,之前所有调试结论都会失效。每周固定一天做机械检查和标定,其他时间保持同一状态。
7.3 把识别失败当成正常分支来设计
走马观碑赛项里,识别算法不可能保证 100% 成功。程序必须默认“这一帧可能识别失败”,并设计回退方案。比如投票结果不明确时,让车辆按保守策略继续行驶或停车,而不是产生一个未定义状态。
7.4 参数配置外置化
把二值化阈值、转向 PID、目标速度、投票帧数等参数放到外部配置文件中,而不是写死在代码里。现场调参时可以快速修改,不需要重新编译。这个工程习惯能让调试效率提高不少。
7.5 学会做减法
比赛比的不是谁代码花哨,而是谁在规定赛道内稳定完赛并更快。如果高速方案经常冲出赛道,就降低目标速度;如果识别模块过于复杂,就简化识别流程。先把一个稳定方案跑完,再逐步提升速度,是最稳妥的备赛路线。
这篇文章里的所有参数和代码都是工程思路,不是标准答案。真正的走马观碑调试,需要结合自己的车模、传感器和赛道环境重新验证。希望这篇赛后总结能帮助下一届队伍少走一些弯路,把精力花在真正影响成绩的环节上。