最长白列法:MT9V03X摄像头组赛道中线提取实战
2026/9/24 11:43:01 网站建设 项目流程

做智能车摄像头组也有一段时间了,从第一版到处乱窜的破车,到后来能在赛道上稳稳跑完一圈,回头看坑不少,但真正卡住我一周的,是图像处理第一步:怎么把眼前的画面变成一条能用的赛道中线。如果你也在用 MT9V03X 系列灰度摄像头,每天对着满屏灰度图不知道从哪下手,我强烈建议你先试试最长白列法。它不需要透视变换,不需要复杂的找边算法,一个简单的统计循环就能让你在半小时内拿到稳定的中线偏差。这篇内容主要面向刚入坑的小白,但有经验的调车佬也可以看看,后面几个参数调整的坑,八成你也踩过。

1. 先想清楚:赛道边线提取到底在解决什么问题

1.1 摄像头组和电磁组的最大差别

电磁组靠电感感应赛道上的交变电流磁场,拿到的是一个相对粗糙的横向位置信息,只要能稳住偏差,车就能走。摄像头组完全不一样,眼前是一幅完整的图像,包含的信息量非常大,但单片机能用的计算资源又很有限。赛道边线提取的核心任务,就是把一大堆像素“压缩”成赛车控制真正需要的东西,也就是赛道中心线与车身位置的偏差值。

很多新手一开始会把精力放在“怎么让图像更好看”上,比如调曝光、调对比度、做滤波,折腾很久之后发现车还是不走直线。原因就是没有把目标想清楚:图像处理不是目的,拿到稳定、及时、可用的控制量才是目的。最长白列法的好处就在这里,它跳过了繁琐的逐行找边线,直接用一个全局统计量估算赛道中心,代码量小、逻辑清晰、非常容易调通。

1.2 MT9V03X 拿到的是什么图像

MT9V03X 系列是一颗灰度 CMOS 传感器,常见型号有 MT9V032、MT9V034,很多智能车队伍叫它“鹰眼”。这颗传感器本身分辨率能做到 752x480,但在单片机上跑全分辨率既不现实也没必要,实际工程里一般会开窗口模式,只取中间一块,比如 188x120 或者 160x120。分辨率降下来之后,每帧数据量小,DMA 传输快,留给算法的时间就多。

它输出的是 8bit 灰度图像,0 代表纯黑,255 代表纯白,中间是各种灰。灰度图在单片机上直接做分析很费劲,所以第一步通常要做二值化,把灰度变成 0 和 1:1 代表赛道白色区域,0 代表背景。注意有的摄像头模组内部直接输出二值化结果,但我个人建议用灰度输出,自己在单片机上做二值化,因为这样可以根据现场光线随时调整阈值,灵活性高很多。

1.3 常规边线提取思路对比

新手刚接触图像处理时最常听到的方案有三种:逐行跳变法、列投影法、最长白列法。我整理了一个简单的对比表:

方法基本思路优点缺点
逐行跳变法每行从左到右找黑到白的跳变点,得到左右边线,再算中点能得到完整赛道轮廓,便于识别十字、环岛对噪声敏感,处理逻辑复杂,调试工作量大
列投影法统计每列白色像素总数,取最大值所在列作为赛道中心思路简单、抗孤立噪点容易被大面积反光干扰,不支持多段判断
最长白列法从图像底部向上统计每列连续白色长度,取最大列极简、稳定、实时性好十字路口和起跑线场景需要额外处理

最长白列法本质上可以看成列投影法的一个改进版本。普通列投影统计的是整列里的所有白点,哪怕你在画面最远处有一小块反光也会被统计进去;而最长白列只关心从近处向上连续不断的白色段,近处的赛道信息起主导作用,这更符合智能车控制的实际需求。

2. 最长白列法的核心原理

2.1 最长白列到底在找什么

可以这样理解:你站在一条白色跑道上,跑道从脚下一直延伸到远处,两边是深色地面。在摄像头的图像里,越靠近车头,赛道在画面中的宽度越大;越往远处,赛道看起来越窄。从图像底部向上逐行扫描,只要这一列一直踩在白色赛道上,它的颜色就一直是白色。当这一列偏离赛道中心足够远时,很快就会碰到赛道边缘,白色就断了。

所以结论很简单:在一幅二值化图像中,从底部向上连续白色像素数量最长的那一列,就是“能看赛道看得最远的那一列”。在直道上,这一列通常落在图像中心附近;在弯道里,这一列会偏向弯道内侧。通过比较最长白列位置和图像几何中心的偏差,我们就能估算出车身相对赛道中心线的横向误差,这个误差直接送给转向环控制,车就能跟着赛道走了。

2.2 算法流程拆解

最长白列法就三步,没有更多。

第一步,二值化。把灰度图像里大于阈值的像素标记为白,其余标记为黑。阈值可以手动固定,也可以用大津法自动选择。

第二步,逐列扫描。对图像里的每一列,从最底行开始往上走,遇到白色像素就计数加一,遇到黑色像素就立刻停止。这里停止很重要,它保证了找的是“连续”白列,而不是把断断续续的白点加起来。

第三步,找最大值。比较所有列的连续白点长度,记录最大长度和它对应的列号。这个列号就是我们的核心输出。

伪代码描述就是:

for col in 0..W-1: len[col] = 0 for row in H-1 down to 0: if image[row][col] == WHITE: len[col]++ else: break max_len = max(len[0..W-1]) max_col = argmax(len[0..W-1])

这里有一个关键细节,很多刚接触图像数组的同学容易搞混:图像数组下标是 image[row][col],第一维是行,第二维是列。按常见规则,image[0][0] 是图像左上角,也就是画面远处;image[IMG_H-1][0] 是图像左下角,也就是车头正前方最近处。所以从 row = IMG_H-1 递减向上扫描,就是在从近处往远处数连续白点。

2.3 不同赛道元素下的表现

直道时,赛道居中,最长白列会稳定在图像中心附近,偏差值很小。这时候哪怕你什么控制算法都不用,把方向盘固定在中位,车也能走一小段直线。

左弯时,赛道向左拐,最长白列会跟着向左偏移,偏差值为负。弯道越急,最长白列偏移量越大。右弯则相反。这个偏差值的变化趋势天然适合当前瞻控制来用,反应比逐行找边线更快,因为它统计的是整幅图像的整体趋势,不是单行局部的边界。

遇到十字路口或者起跑线时,画面里会出现大块连续白色区域,最长白列位置可能跳变。比如起跑线横杠会让很多列的白列长度同时变长,最大值对应的列可能在几帧之间来回跳。这种情况下,需要加上一阶低通滤波或者限幅处理,避免转向值突变导致车身甩动。

2.4 使用边界和失效场景

最长白列法不是一个万能算法,它有几个明显的适用前提。

赛道背景与赛道本体要有足够的对比度。如果赛道是白底黑边,背景是深色,效果最好;如果背景也是浅灰色,二值化后白色区域连成一片,这条方法基本失效。

赛道上不能有大面积的反光。反光会把远处赛道照成一片亮白,二值化后白色区域可能会出现空洞,导致最长的连续白列突然断掉,偏差值大幅跳变。

起跑线、十字、环岛这些特殊元素出现时,单靠最长白列法不够。我的建议是把它作为基础速度阶段的兜底方案,后续再叠加特殊元素识别。先用它能跑起来,把转向控制闭环调通,然后再去补充更复杂的边缘判断,这个学习曲线是最平滑的。

3. 实战代码实现

3.1 图像采集与二值化

MT9V03X 的底层初始化各家库都不一样,山外、逐飞、龙邱都有现成例程,这里不展开。假设你已经在 DMA 中断里拿到了一帧灰度图,存到二维数组 gray 里,接下来做的就是二值化。

#define IMG_H 120 #define IMG_W 188 #define WHITE 0xFF #define BLACK 0 uint8_t gray[IMG_H][IMG_W]; // 原始灰度图,由 DMA 填充 uint8_t bin[IMG_H][IMG_W]; // 二值化结果 void binaryImage(uint8_t threshold) { for (int row = 0; row < IMG_H; row++) { for (int col = 0; col < IMG_W; col++) { if (gray[row][col] > threshold) { bin[row][col] = WHITE; } else { bin[row][col] = BLACK; } } } }

固定阈值在光线稳定的室内场地完全够用,一般取 60 到 100 之间。如果现场光线变化大,可以改用大津法自动计算阈值,核心逻辑是遍历 0 到 255 所有可能阈值,找到使前景和背景类间方差最大的那个值。注意大津法要统计整幅图的灰度直方图,好在 188x120 分辨率并不大,在单片机上跑一轮耗时完全可接受。

3.2 最长白列法核心函数

这里给出一个带防护逻辑的版本。除了找最大值,还做了两个额外处理:一是如果最长白列长度小于某个阈值,认定当前画面没有有效赛道,保留上次结果;二是把长度达到最大长度 90% 的列全部取平均,避免单一列上的孤点噪声造成偏差跳变。

#define MIN_VALID_LEN 20 int lastValidCol = IMG_W / 2; int findLongestWhiteColumnSafe(int *maxLenOut) { int len[IMG_W]; int maxLen = 0; for (int col = 0; col < IMG_W; col++) { len[col] = 0; for (int row = IMG_H - 1; row >= 0; row--) { if (bin[row][col] == WHITE) { len[col]++; } else { break; } } if (len[col] > maxLen) { maxLen = len[col]; } } if (maxLen < MIN_VALID_LEN) { return lastValidCol; } int sumCol = 0; int count = 0; int minAccept = (int)(maxLen * 0.9f); for (int col = 0; col < IMG_W; col++) { if (len[col] >= minAccept) { sumCol += col; count++; } } if (maxLenOut) { *maxLenOut = maxLen; } if (count > 0) { lastValidCol = sumCol / count; return lastValidCol; } return lastValidCol; }

注意 len 数组长度是 IMG_W,188 个 int 在栈上完全没问题。如果你把图像改成 160x120 甚至 320x240,记得同步调整数组大小。这里用栈数组不用全局数组,是避免多个函数共用全局导致逻辑耦合,但如果你在中断里调用该函数,要注意栈空间分配是否足够。

3.3 从白列位置到转向控制量

拿到最长白列的列号之后,偏差计算很简单,就是中心列减去图像几何中心列:

int maxLen = 0; int centerCol = findLongestWhiteColumnSafe(&maxLen); int error = centerCol - (IMG_W / 2);

这个 error 就是横向偏差,正值表示赛道中心在图像中心右侧,负值表示在左侧。直接送给比例控制就能跑,但建议加一个简单的一阶滤波,防止单帧跳变:

static int lastError = 0; int filteredError = (error * 3 + lastError * 7) / 10; lastError = filteredError;

滤波系数可以根据实际情况调,如果觉得转向太肉,就增大新误差的权重;如果觉得车子拐弯时抖得厉害,就减小新误差的权重。最终给舵机的输出可以这样写:

float steer = KP * filteredError + KD * (filteredError - lastError);

KP 和 KD 的整定因人而异,我最开始用 KP = 0.3、KD = 0.05 起步,然后根据甩尾情况和响应速度慢慢往上加。注意这只是基础框架,真正上赛道还要结合速度环做协调。

3.4 调试辅助方法

调图像算法,看不到图像等于瞎调。我强烈建议你在开发阶段把二值化结果通过串口发到上位机,山外调试助手、逐飞上位机、VOFA+ 都支持自定义协议。

一个比较省事的做法是降采样输出。完整 188x120 的 ASCII 图像在串口里刷屏刷到怀疑人生,我一般把图像缩小到 47x30,也就是每 4 个像素取一个,然后把白色像素用字符“1”表示、黑色用“0”表示,输出到调试面板。这样一眼就能看出最长白列算法提取的结果对不对,比对着二维数组猜靠谱得多。

调试时记得把当前的最长白列列号、偏差值、所用的阈值一起打出来。有一次我跑着跑着突然失控,查了半天代码没问题,最后看日志才发现当时光线变了,固定阈值把整个赛道全判成了黑色。有日志回放,这种问题几分钟就能定位。

4. 参数调优与常见问题排查

4.1 二值化阈值怎么选

固定阈值最省心,但有个致命缺陷:场地光线一变,之前调好的阈值就不准了。我建议现场比赛时至少准备三个固定阈值,分别对应强光、正常、偏暗环境,通过拨码开关或者按键切换。虽然听起来很土,但确实有效。

如果想要更自动化,那就上大津法。注意大津法在赛道占比特别小或者特别大的时候效果会变差,因为类间方差最大的阈值可能偏向背景或偏向前景。我的经验是,大津法适合光线变化中等、赛道占比在 20% 到 60% 之间的场景,超出这个范围建议直接固定阈值。

还有一个小技巧:不要在比赛前一天才去调阈值。最好是提前把不同时间段的场地光线情况记录下来,标出一天中哪个时段光线变化最剧烈,针对性做阈值切换。很多队伍在正式比赛时翻车,就是因为场地灯光和训练场地差别太大,又没准备阈值预案。

4.2 反光、阴影和光线变化怎么办

反光是最常见的问题。白色赛道材质本身是反光的,灯光一打,某些角度会形成高亮区域。高亮区域在灰度图里非常亮,灰度值接近 255,这时候如果把排水沟一样的暗色纹理也看成了黑点,最长白列就会被切短。

解决方案有几个。第一个是降低曝光时间,这是最直接有效的做法。MT9V03X 的曝光时间可以通过寄存器配置,曝光时间变短后,高光区域灰度下降,赛道和背景的对比度反而更清晰。第二个是降低增益,增益太高会放大噪声和反光。第三个是在二值化之后做一次形态学膨胀,把细小的黑色空洞填掉,但膨胀会稍微增加计算量,在 120MHz 主频下跑完整幅图的 3x3 膨胀耗时大约 0.2ms,还可以接受。

阴影的情况稍微复杂一点。如果阴影落在赛道上,会把白色的赛道区域截断成两段。这时候最长白列法会误判,因为从底部往上扫,扫到阴影处就停了,白列长度远小于真实值。我建议在做完全局统计之前,先对二值化图像做一个列方向的闭运算,也就是先膨胀再腐蚀,把阴影造成的窄缝填起来。注意腐蚀和膨胀的核别开太大,3x3 就够了,否则会把赛道边线的真实信息也吃掉。

4.3 常见问题排查速查表

现象可能原因解决方法
车在直道上方向左右摆动滤系数太小,噪声直接作用于输出加大低通滤波权重,或减小 KD
弯道入弯迟钝前瞻距离不够,最长白列偏向位置滞后适当增大图像上部使用范围,或提高扫描行权重
图像全是黑的曝光时间太短或阈值设得过高调低阈值,或增加曝光时间
图像全是白的曝光时间过长或阈值过低缩短曝光时间,提高阈值
十字路口突然甩头最长白列在起跑线横条上跳变加限幅逻辑,限制单次偏差变化范围
光线一变就失效固定阈值不适应新环境切换阈值或改用大津法
帧率明显下降二值化和白列扫描耗时太长检查是否每次都在循环里做了全图扫描,考虑放到定时中断中

4.4 调试心得和注意事项

一定要先让车在低速下跑通,再谈提高速度。我见过太多人上来就全油门冲,结果车飞出去,然后怪算法有问题。实际上在低速下,最长白列法的稳定性是完全可以保证的,先把转向控制调稳,再逐步加速,你会发现很多问题会在高速下自己暴露出来,而不是一开始就乱成一锅粥。

代码层面有一个容易被坑到的地方:图像数组一定要加上 volatile 修饰。如果 DMA 和主循环同时访问 gray 数组,编译器可能会优化掉你看到的像素更新。另一个是喂狗,智能车主控一般开了看门狗,如果图像处理函数耗时突然增加,可能触发看门狗复位。建议把图像采集、二值化、白列扫描、偏差计算的总耗时打到一个变量里,通过串口观察,正常情况下 120MHz 主频跑 188x120 的图像,这一整套流程应该控制在 1ms 以内。

最后再分享一个我自己的体会:最长白列法不是万能的,但它绝对是入坑智能车摄像头组最平滑的起点。我第一次跑通它的时候,那种“车子真的会看路了”的感觉,比调出来任何复杂算法都有成就感。后面无论你是要继续做逐行找边线,还是上透视变换和动态前瞻,这套东西都能当做一个稳定可靠的兜底方案。先把车跑起来,再去追求跑得好,方向肯定不会错。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询