
赛道上跑着一辆巴掌大的小车没有磁条、没有导轨、也没有人遥控它唯一的“眼睛”就是车头正上方那颗摄像头。通过实时分析画面里赛道与背景的灰度差异它自己判断该往左打还是往右打该加速还是减速——这就是基于机器视觉的循迹小车。做这个项目的初衷很朴素传统红外光电循迹小车用一排对射式传感器贴地扫线只能感知“有/无”的离散信息遇到急弯、路口、坡道就很吃力。视觉循迹本质上是用图像把“赛道形状”完整地读到控制器里能做的事多了很多门槛也高了不少。这篇博客把整个设计从方案选型、图像处理、控制算法到硬件装配和问题排查都摊开讲一遍适合正在做课程设计、电子设计竞赛或者刚入坑嵌入式视觉的读者如果你只有单片机基础、没碰过图像处理跟着走也能把这台车跑起来。1. 整体方案设计思路1.1 为什么选择“机器视觉”而不是传统传感器先聊一个经常被问的问题光电循迹和电磁循迹已经很成熟为什么还要用摄像头红外循迹小车的原理是给地面打光通过反射率差异判断黑线位置。它的优点是真的便宜、代码也简单但缺点很致命——它只能感知传感器安装点那一两个截面上的信息小车跑到弯道入口才能发现“哦这里是弯”转向响应天然滞后。电磁循迹稍微好一点通过检测赛道中心导线产生的交变磁场定位能看“远”一点但对铺设要求高、调参也比较玄学。视觉方案完全不一样。摄像头一次拿到的是一整幅图像意味着小车能提前看到前方几十厘米甚至更远的赛道走向。也就是说它能在入弯前就开始打方向弯道通过速度可以明显提高。这在算法层面叫“预瞄”本质上和人类开车时看远方的弯道是一个道理。当速度要求不高的时候红外、电磁都够用一旦你想把速度提上去视觉就是绕不开的路。另一个好处是可扩展性。图像里不止有赛道边界还有颜色、形状、哪怕是二维码。后续如果想做红绿灯识别、十字路口计数、障碍物检测底层都是同一套视觉框架而光电循迹要想加功能就得加传感器硬件复杂度会爆炸式增长。1.2 核心需求拆解把这个项目拆成几个需求点思路会清晰很多感知摄像头实时采集赛道图像识别出赛道中心线相对车体的横向偏移。决策根据偏移量计算出期望转向角度和车速。执行电机驱动按期望转向和速度输出PWM驱动小车转向、前进。稳定整套系统在复杂光照、不同赛道颜色、电池电压波动下都能可靠运行。这套架构里感知是核心决策和执行虽然传统但依然有讲究。很多初次做视觉小车的同学把精力全砸在图像识别上结果车架松松垮垮、电机响应迟钝图像再好也跑不起来。我的经验是感知、决策、执行三个环节缺一不可要当成一个系统来设计。1.3 视觉处理平台的选型视觉处理平台是整个系统的关键决策点市面上常见几类我直接按使用体验对比方案算力开发难度功耗体积适合场景OpenMV系列中等MCU级视觉低Python编程小适合车模大多数教学、竞赛场景K210Sipeed Maix系列中高带硬件加速中Micropython/C小价格便宜需要跑简单神经网络的场景树莓派OpenCV高高需搭环境大功耗高复杂识别算法、深度学习、ROS手机APP中中需做APP-快速验证算法我最终选了 OpenMV Plus STM32F103C8T6 的组合。OpenMV Plus自带OV5640摄像头图像传感器性能在同类里算好的且支持MicroPython写起图像处理代码比C语言在STM32上裸写高好几个量级。STM32F103C8T6虽然老但它承担底层控制绰绰有余读取编码器、输出PWM、解析串口数据包稳定可靠。有人问我为什么不用树莓派OpenCV性能明显更强。我的理由是树莓派跑Linux系统开机慢、掉电容易损坏SD卡装在一辆会颠簸、频繁开关机的小车上可靠性偏低。另外树莓派加摄像头模组加供电模块的体积和重量对一辆小车来说也偏大。K210是不错的替代方案但生态和调试工具链不如OpenMV成熟对新手不太友好。如果你已经有Linux开发基础、想上更高级的算法树莓派也完全可以不过这套项目的控制部分思路是一样的换平台只需要重写图像处理这一层。2. 图像处理核心从一帧图片到一条路径2.1 摄像头安装与图像采集图像处理的第一件事不是写代码而是把摄像头装对。这个环节直接决定后面所有算法的复杂程度。我的安装方案是摄像头固定在小车正前方距离地面约18厘米镜头向下倾斜约25度。这样视野范围刚好覆盖车头前方20到60厘米的赛道区域既保证了“预瞄距离”又不会因为看得太远导致赛道在画面里占比太小。安装的硬性要求是“牢”。很多小车跑高速时车架共振摄像头稍微抖一下画面模糊不说图像中赛道位置会跳变直接影响控制稳定性。我用3D打印了一个支架把OpenMV和车架刚性连接而不是用双面胶随手一粘这是后来能稳定跑到较高速度的前提。然后是图像参数设置。OpenMV的传感器支持很多参数但我强烈建议这几项必须手动锁定自动曝光必须关闭固定曝光值自动白平衡必须关闭固定色温自动增益可以保留但增益上限要限制原因是露天环境下云飘过来遮住太阳自动曝光会自动调亮画面赛道和背景的灰度差可能在瞬间反转直接导致跟丢。固定曝光从源头上消除了这类偶发问题。具体曝光值怎么定找个阴晴不定的天把小车放赛道上来回调观察画面中赛道边缘是否清晰稳定选择一个在亮处和暗处都能接受的值。这个值一旦确定基本就不用再动了。2.2 灰度图、裁剪与阈值分割OpenMV采集到的彩色图像直接转成灰度图处理这个决策是经过考量的。常见的赛道路面是浅色赛道边界线是深色或者反过来灰度图足以表达这种对比关系。彩色信息虽然能提供更多维度但RGB值受光照影响大鲁棒性反而差。转成灰度后每个像素只有一个0到255的亮度值后续处理计算量小很多。接下来的关键步骤是裁剪ROI区域。摄像头画面里上半部分通常是远处背景、墙面、人群这些信息对小车的横向控制不但没用反而是干扰。我直接从图像中点向下裁剪只保留下半部分。假设图像分辨率为320×240我就取y120到240这一块。这里为什么要“从上往下”而不是“从中间开始”是因为赛道总会出现在视野的下部而把上面的背景干扰直接裁掉能减少很多误检。阈值分割是视觉循迹最核心的一步。我的赛道是浅灰色地面加深色线所以算法任务就是把每一行像素中“深色”的部分挑出来剩下的视为背景。OpenMV里可以用阈值编辑器实时调节LAB或灰度阈值我最常用的是LAB色彩空间。一个经验不要只设一个固定阈值最好设置一个区间。比如赛道线在灰度图里的亮度值大致是30到80我设置阈值时会把上界放宽到100把下界设到0宁多勿漏。因为阴影也属于“深色”如果阈值卡得正好树影一压过来赛道线就“消失”了宁可通过后续的算法把阴影视作跟踪目标也不能允许目标直接消失。2.3 寻找赛道中心线的两种方法得到二值化图像后所有“深色”的东西都变成了白色像素点。现在要回答一个问题赛道线到底在哪这里有两种常用方法我一开始用的是“重心法”后来换成了“边缘中点法”。重心法思路最简单在每一行扫描线上把所有白点像素的x坐标求和除以个数得到这一行的“重心”x坐标。如果赛道线连续且干净重心基本就是赛道线的位置。但问题在于如果某个扫描行里出现了反光噪点、落叶阴影造成的额外白块重心会被拉偏产生一个“毛刺”控制端就会出现一瞬间的错误转向。边缘中点法更扎实在每一行扫描线上先找到最左边白点和最右边白点的x坐标取这两个值的中间位置作为赛道中心。这个方法对噪点的容忍度明显更高因为单个噪点不会同时改变左边界和右边界除非赛道线整段被截断。我最终选择的实现是边缘中点法效果稳定很多。伪代码如下实际在OpenMV中用find_blobs找色块然后在色块内逐行扫描blobs img.find_blobs(threshold, roiROI, pixels_threshold20, area_threshold20) for blob in blobs: for row in range(ROI[1], ROI[1] ROI[3], 5): left None right None for x in range(ROI[0], ROI[0] ROI[2]): if img.get_pixel(x, row)[0] 0: if left is None: left x right x if left is not None and right is not None: mid (left right) // 2 points.append(mid)这个循环每5个像素扫描一行把每行的中点记录下来。这些点连起来就是一条赛道中心线的离散采样。只取最后一行最靠近车身的那一行的中点做控制当然也可以但那样丢掉了很多信息。我的做法是取所有点的平均相当于对整条检测到的赛道线做一次“中线拟合”这样即使某一帧某个扫描行检测失误均值也不会剧烈跳变。2.4 为什么图像要“多行扫描”而不是只看一条线这里展开讲一个初学者容易忽略的原理。如果只用靠近车头的那一条扫描线相当于用“一排传感器”替代“一颗摄像头”那和传统红外循迹就没有本质区别——依然是到了弯道跟前才能反应过来。机器视觉的核心优势在于“预瞄”。多行扫描得到的赛道中线点是从近到远一条弧线近的点反映当前位置远的点反映弯道的弯曲趋势。控制算法可以利用这个趋势提前响应。用一条直线去拟合这些中点得到的斜率就是赛道在当时那个区域的整体弯曲程度。我把这个斜率叫做“前瞻梯度”梯度大说明弯道急梯度小说明接近直线。这个值后来被我用来做速度自适应控制——看到梯度大就自动降速出弯后梯度变小再提速。这是单点视觉方案做不到的也是它能让小车高速稳定过弯的关键。3. 控制策略让小车学会“看路打轮”3.1 从偏差值到转向指令图像处理输出的核心结果是一个偏差量赛道中心线与画面中心线也就是车身中轴线的横向偏移单位是像素。我把它归一化到-1到1之间-1代表线在画面最左1代表线在最右0代表居中对正。有了这个偏差值最简单的控制是比例控制转向PWM Kp × error。P大转向快P小转向迟缓。但纯比例控制的问题很明显弯道里会出现持续震荡小车会走“S”型路线。原因是转向存在延迟当车头对准赛道线时它的角速度还没减下来会冲过去然后反向打轮再冲回来。解决办法是加微分项转向PWM Kp × error Kd × (error - last_error) / dt。微分项相当于对偏差变化率做阻尼偏差变化得越快反向抑制力越大车头姿态会稳定很多。实际调试中我先调Kp让小车能跟住线再一点一点加Kd直到震荡消失。这个过程需要在直道和弯道往返测试一般十几分钟就能找到一个不错的组合。3.2 PID参数整定实录我调出来的一组参数是Kp52Kd8积分项直接不用。为什么不用积分积分项的作用是消除稳态误差但循迹系统里偏差本身就是控制目标——我们期望的稳态偏差就是0而转向机构是位置控制型不存在累计型静差。更关键的是积分项在入弯时很容易积分饱和导致出弯时转向回正特别慢表现就是冲过弯道。所以除非你的电机驱动有明显的死区导致车走不直否则循迹小车不建议开积分。调参步骤按以下顺序走效率最高先把Kp设小比如20Kd设0确认方向正确小车能跑起来但不跟线也没关系。Kp逐步加大每次加10观察是否出现振荡。出现振荡后记录Kp值并退回上一档。Kd从2开始加每次加2观察直道是否平稳、弯道是否流畅。如果弯道里出现高频抖动说明Kd偏大退一档。最后微调转向响应上下限让转向PWM限制在一个安全范围比如最大转向对应的PWM占空比不超过70%给电机留出回旋余地。这里还有一个机器视觉项目特有的问题由于OpenMV处理一帧图像需要大约30毫秒STM32收到的偏差值是一帧前的数据存在固有的控制延迟。这个延迟无法完全消除但可以通过把微分项的计算放到STM32端而不是OpenMV端来缓解——因为STM32能更频繁地采样偏差变化率而不是只在图像更新时才更新一次。我在实际实现中就是让OpenMV只发偏差值STM32算出速度更快的差分信号来做阻尼效果比在OpenMV里算D要细腻得多。3.3 用前瞻梯度做速度自适应速度控制如果固定在一个值就会出现一个尴尬的情况设慢了直道跑得窝火设快了急弯必飞出去。所以我把速度和赛道弯曲程度关联起来。前面图像处理得到的赛道中线拟合直线有一个斜率值slope它的绝对值大小和赛道弯曲程度正相关。我做了一个简单的映射float bend fabs(slope); float speed BASE_SPEED - bend * SPEED_GAIN; if (speed MIN_SPEED) speed MIN_SPEED; if (speed MAX_SPEED) speed MAX_SPEED;BASE_SPEED是我希望小车在直道跑的最快速度MIN_SPEED是过最急弯时的保底速度。SPEED_GAIN是调节灵敏度。这个策略的效果就是直道放心狂飙发现前方弯道信号了速度提前降下来等到了弯道中心速度已经降到合适值了。这段逻辑其实借鉴了车辆的弯前制动策略。把速度控制和转向控制分开来做比单纯在转向里做文章提升大得多。实测同样一段赛道固定速度跑到1.5m/s已经摇摇欲坠用了速度自适应后直道能跑2.2m/s弯道反而更稳了。4. 硬件系统搭建与迭代4.1 车体结构选型视觉循迹小车对车体的要求比普通循迹小车高主要体现在载重量和稳定性上。OpenMV板子加上一堆模块并不重但摄像头支架、电池、控制板加起来也不轻。我选的是亚克力两驱车架两个TT马达加编码器后轮驱动前轮加一个万向轮。之所以不用四驱是因为四驱转向时内外轮速差需要额外的差速逻辑对循迹这种频繁转向的场景来说两驱配万向轮在机械上更简单可靠。电机的选择很关键。TT马达便宜但转速离散性大两个马达速度不一致会让小车跑偏加大转向控制的负担。我后来换了带编码器的N20微型减速电机用编码器测两轮实际转速在STM32里做一轮简单的速度闭环保证两轮线速度一致。这一步对稳定性的提升非常明显。4.2 电路连接与供电隔离电路部分有一个必须重视的原则电机电源和逻辑电源必须分开。电机启动瞬间的电流可以达到正常工作电流的几倍如果共用一路电源电机一抽电STM32和OpenMV的电压就会被拉低甚至复位直接导致小车断电重启。我的供电结构是2S锂电池7.4V直接给TB6612电机驱动芯片供电驱动两个电机。电池经过一个5V/3A输出的降压模块DC-DC而非LDO给STM32和OpenMV供电。注意在电池母线上并联一个大容量电解电容470uF以上吸收电机换向时产生的尖峰脉冲。TB6612是推荐使用的驱动芯片比老式L298N轻得多、效率高、发热低。接线按官方原理图来注意PWM频率我设在10kHz这个频率下电机噪音小、发热可控。OpenMV和STM32之间通过串口UART通信我定了一个简单的数据帧格式帧头0xAA 0x55 0xAA 0x55 1字节偏差值归一化后乘100 1字节控制字 1字节校验和。用4字节固定帧头加校验是为了防止通信错位。曾经踩过一个坑不加帧头只发裸数据时如果某一位抖动了后面所有数据全错位小车会突然乱转。加上固定帧头后只有连续收到完整帧头才开始解析数据即使有一次错误顶多丢一帧不会累积错位。4.3 机械结构迭代记录第一版摄像头直接放在OpenMV板子上板子用双面胶贴在车尾方向镜头方向朝前。跑起来后发现车头颠簸导致摄像头跟着上下抖图像里赛道线位置跳得很厉害转向控制基本是拿噪声在控。后来打印了一个专用的摄像头支架把摄像头的重心尽量前移并压低与车架形成刚性连接拓扑结构上做到摄像头和车身“一体位移”问题才解决。另一个迭代点在底盘重心。视觉小车的电池如果放得太靠后高速转弯时后轮会侧滑甚至甩尾。我把电池放在车体中间偏后、重心尽量贴近后驱动轴的垂直面这样在弯道里车身会自然做出一点“漂移姿态”反而帮助转向更灵活。当然漂移量不能太大压着万向轮的弹簧给后轮增加一点正压力即可。5. 调试流程先让车“看明白”再让车“跑明白”5.1 静态调试盯着屏幕调图像我强烈建议调试分两步走不要在车跑起来的时候才看图像那样既危险又分不清问题到底出在视觉还是控制。把小车放在赛道多个典型位置直道、弯道入口、弯心、出弯处用OpenMV IDE的“帧缓冲区”功能把画面冻结下来逐帧检查三件事二值化图像是否干净赛道线是否完整有没有大块噪点。赛道中心线的扫描结果是否合理有没有突然跳变。输出的偏差值是否符合直觉比如线在左边时偏差值是不是负的。这三项都对了视觉环节才算是合格的。我见过太多人跳过这一步直接上参数调车结果花了一整天其实是在和“阈值没调好”作斗争。到后期我对这套图像管线有把握了会做一个“可视化调试模式”把扫描到的赛道中点和拟合线直接画到图像上通过无线图传实时看小车眼中的世界。这一步极大缩短了“看现象猜原因”的过程。5.2 动态调试从低速到高速的渐进策略动起来之后调试节奏也要循序渐进。最开始把速度设定在很慢的档位速度上限不超过0.5m/s。此时观察转向方向和施加转向的位置是否合理——很多初学者在这一步会发现转向是反的摄像头画面左偏小车却往右打。给OpenMV发负数偏差、给STM32的转向逻辑是正着写的中间有一次取反错误就会造成这种“镜像”问题。确认方向正确后再逐渐提高BASE_SPEED每提一档跑两三圈观察是否有新问题。这个过程我一直用无线串口模块实时记录偏差值和速度值事后拉曲线看趋势。比如看到“弯道中偏差值出现高频剧烈跳动”基本可以断定是D调大了导致对噪声放大看到“直道上偏差值缓慢漂移”基本是两轮速度不一致或初始对中没调好。调试要耐心但参数记录习惯绝对值得养成。我把每一组参数对应的赛道表现记录下来形成了一张对照表参数组KpKd最高速度表现问题A3001.2直道稳弯道外抛B4541.4弯道改善直道轻微S型C5281.8弯道流畅光照突变时丢线D5282.2整体最佳无这张表在后来换赛道、换场地后重新调试时帮了大忙直接给我一份“起始参考值”不用每次从零开始。6. 常见问题与排查技巧遇到的坑比想象中多挑几个典型问题和对应的排查思路写下来。6.1 图像全黑或全白刚上电时图像全黑先从两个角度排查一是摄像头排线有没有插紧二是传感器初始化是否成功。OpenMV IDE会提示具体报错。全白则大概率是曝光设置过高或者对着强光源了。这类问题先不要怀疑算法MIPI接口的传感器对排线质量比较敏感换线或者重新插拔经常能解决。6.2 直道上小车走“S”型这个现象八成是Kd太小或者Kp太大。先用滞后分析法观察小车是不是每次过度转向后要过一会儿才回正如果是说明阻尼不够加Kd。如果车头在直道上来回摆频率很快那是Kp太大先降Kp。另外也要检查一下底盘本身的机械对称性两个轮子直径是否一致、电机是否一个快一个慢视觉系统再准机械不对称的底子跑出来的车也是歪的。6.3 弯道冲出赛道弯道冲出去的原因要多维度排查。首先看图像是不是弯道过急三十ms一帧的图像帧率跟不上导致控制延迟过大。如果是这个原因可以降低摄像头分辨率比如从320×240降到256×256换取更高帧率代价是远处赛道细节变差。其次是看速度速度自适应参数是不是太迟钝车速没来得及降到弯道安全速度。最后才是调系数尽量用机械和策略解决问题不要把参数调到极限去迁就机械缺陷。6.4 光照突变导致丢线固定曝光能解决大部分光照突变问题但遇到树荫和阳光交接的场地边界依然可能出现一段时间阈值失配。我的应对办法是在OpenMV里做动态阈值统计每帧统计一下ROI区域内的灰度直方图找到波峰波谷的位置自动调整二值化阈值。具体做法是用直方图的双峰法思想若发现当前帧的灰度均值相比上一帧变化超过某个限度就在上下限范围内微调阈值。实现不复杂但对跨光照场景的稳定性提升很显著。6.5 通信偶发丢包串口通信丢包多数是干扰问题但电机PWM本身就是强干扰源所以最直接的排查方法是把电机断电给OpenMV和STM32供电连续发数据观察是否丢包。如果此时也丢包那就是串口配置或数据帧本身的问题如果只在电机运行时丢包就是电机干扰导致。针对后者除了硬件上加电容做滤波之外还可以在STM32端对偏差值做一阶低通滤波滤掉瞬时跳变的假数据效果立竿见影。7. 项目扩展从“能跑”到“跑得好”这个项目做到能稳定跑完赛道已经达到了课程设计或者竞赛入围的水准。但如果想继续深入还有几个方向值得投入时间。一是加红绿灯和停止线识别。用色块识别处理红色和绿色目标配合赛道线检测就能让小车遇到红灯自动停车。这是从“路径跟踪”跨到“交通规则理解”的起点。二是做多传感融合。在视觉为主的前提下加一组红外对射传感器作为保险当视觉丢线时用红外信号做一两百毫秒的“盲走”等视觉恢复。这个兜底策略能让小车在极端光照下也不至于完全失控。三是换更强的视觉平台。如果熟悉了OpenMV的开发逻辑脱坑到K210或者树莓派上可以跑YOLO这样的目标检测模型把“找赛道”变成“识别目标物”视野和想法都会打开很多。OpenMV对入门很友好但它的算力天花板比较低高帧率下很多复杂算法跑不动。最后说一个我个人觉得特别值的扩展用无线图传加上位机把实时图像、赛道中线拟合结果、偏差值、转向和速度控制量都画在一张仪表盘上。调试时对着这个界面任何时候出问题都能精确地知道是哪一环节的哪一项指标异常。有了这套调试手段后面对参数和算法的任何改动都变得很踏实不用再靠“盲调”猜来猜去了。