基于机器视觉的循迹小车:从图像处理到PID控制全解析
做循迹小车这件事我从研究生阶段刚开始接触单片机一路折腾到现在前后做了三个不同方案。最早那版是几路红外对管贴在车头说白了就是“贴地摸黑走路”后来换成基于机器视觉的循迹小车方案第一次在屏幕前看到摄像头把白底黑线变成一条清晰的中线时那种感觉完全不一样像是给车装上了眼睛。基于机器视觉的循迹小车核心思路其实不难理解用摄像头采集赛道图像经过灰度化、二值化、形态学处理把赛道线从背景里单独提出来算出车体相对赛道的横向偏差再把这个偏差换算成左右轮的速度差通过差速转向把车稳稳拉回赛道中心。它解决的是传统红外循迹“只能看到车头前两三厘米”、弯道拐角稍大就跟丢的问题。这篇总结适合刚学完单片机、正准备把摄像头加入小车的同学也适合做课程设计、毕业设计或电子竞赛项目的人参考我会把硬件选型、图像处理、PID控制、排坑实录全部摊开讲一遍。1. 先想清楚方案摄像头循迹到底要怎么玩1.1 机器视觉循迹和传统红外循迹的本质区别很多第一次接触摄像头循迹的人会下意识把它理解成“把红外传感器换成摄像头”实际上这两个方案的思路完全不一样。红外循迹用的是反射式光电传感器发射管和接收管紧贴赛道黑色胶带吸收红外光、白色地面反射红外光传感器输出的就是高低电平本质上只有“有/无”两种信息。这种方案的优点是成本低、逻辑简单缺点也极其明显传感器离地高度一般只有几厘米弯道曲率一大车头还没偏就已经冲出去了遇到日光直射、地面反光、赛道上有脏污判断就会乱套。摄像头循迹则是完全不同的玩法。摄像头能把车前方二三十厘米甚至更远的赛道整体拍下来相当于提前看到了前方的路况。有了这幅图像你不仅能知道“现在有没有线”还能知道“线在图像左侧还是右侧、离中心偏了多少、前方是不是一个急弯”。这个信息量比红外大得多控制策略也可以从“开关式纠偏”升级成“连续比例式纠偏”转弯的平滑度完全不在一个量级。用机器视觉做循迹真正要解决的核心问题就两个一是怎么从图像里稳定地提取出赛道线二是怎么把提取到的偏差变成合适的转向力和速度。从学习路线的角度看这个项目是非常典型的一个分水岭。它前面接的是单片机基础和数字图像处理入门后面可以自然延伸到车道线检测、目标识别、SLAM这些更深的领域。很多人在机器视觉学习路线上卡住就是因为一上来就啃数学公式和理论书反而忘了拿一个具体的小项目练手。循迹小车刚好提供了一个低成本的试验场画面没有复杂语义特征明确处理链路短非常适合把采集、预处理、特征提取、控制决策整个闭环跑通。1.2 主控与视觉模块选型对比硬件选型是第一个容易纠结的地方。摄像头循迹的主流方案大概有三类我整理过一个对比表把最核心的差异标出来了。方案类型算力开发语言与难度成本典型场景OpenMVMicroPython中低自带图像处理库Python上手快中课程设计、入门学习STM32/ESP32 OV2640/OV7725低需自行优化C/C难度高低电子竞赛、进阶自研树莓派 OpenCV高可跑完整视觉算法Python/C生态强高科研验证、高阶扩展如果你是想尽快看到效果、把主要精力放在控制调参上OpenMV这一类自带图像处理库的方案是典型的省心选择它内置了灰度化、二值化、膨胀腐蚀、寻找色块这些算子几行代码就能把赛道中线算出来。如果目标是竞赛或者想深入理解图像处理每一步在做什么我会建议用单片机加摄像头自己写处理流程虽然代码量大一些但对硬件资源的把控能力会明显提升面试和答辩的时候也能讲出更多细节。我的建议是不要过度纠结平台重要的是把“图像处理流水线”这个概念跑通。无论哪种方案最终你都会用到同一套逻辑采集图像、转灰度、二值化、形态学运算、算偏差、输出PWM。树莓派方案虽然算力高但对一个循迹任务来说其实有点浪费而且启动慢、体积大、供电要求高装在小车上反而增加稳定性风险。我自己最后用的是“单片机低分辨率摄像头”的组合主频不高但合理降分辨率之后完全够用这个思路在竞赛里也很常见。2. 硬件平台搭建从摄像头安装到电源布线2.1 摄像头选型与安装角度摄像头选型上新手最容易踩的坑是盲目追求高分辨率和高帧率。循迹小车对图像的需求其实很朴素能看清赛道线、处理得够快就行。OV7725、OV2640这类常见摄像头模组输出RGB565或灰度图分辨率支持到QVGA甚至更低完全够用。实际项目里我建议把采集分辨率降到160×120或者120×160左右不要贪高。这个尺寸下赛道特征一点不缺但像素量一下子减少到VGA的几十分之一处理速度和处理稳定性都会好很多。分辨率高了一定帧率上不去处理一帧要几十毫秒小车速度稍快一组有效数据还没来得及算完车已经冲过弯道了。摄像头安装是整个硬件环节里最不能马虎的一步它对后续图像处理的影响远超你想象。高度、俯仰角、左右摆正这三个量必须仔细标定。一般是把摄像头支架装在车体前部镜头中心高度保持在8到15厘米镜头略微下俯让画面主体对准车头前方20到40厘米的赛道区域。这个时候视野里能同时看到近处和远处的赛道线车辆就有提前量去规划转向。安装时可以用一块硬纸板在赛道上摆一摆通过屏幕确认图像中赛道线是否居中和水平左右有没有歪斜。如果镜头装歪了图像里的赛道线天然就是斜的后面所有偏差计算都会带一个固定误差调PID的时候怎么调都别扭。还有一点很多人容易忽略摄像头模块要固定牢靠。小车跑起来马达振动很大支架如果是一个容易晃动的尼龙柱或者用双面胶粘的图像每帧都会抖动特征提取结果忽左忽右再好的算法也扛不住。我后来用的是铝合金支架加螺丝锁紧调试时用手指轻轻敲击摄像头画面纹丝不动才算合格。2.2 驱动、供电与PCB布局要点电机驱动和供电是循迹小车“跑得稳”的基石。驱动模块我用过L298N也用过TB6612FNG对比下来强烈建议选后者。L298N压降大、发热严重两个通道全开的时候降压特别明显电池电压稍微低一点摄像头画面就开始出现雪花噪点严重的直接重启。TB6612FNG是MOS管结构内阻低、体积小逻辑电压兼容3.3V和5V十几克的小车用起来非常舒服。如果你的项目在学校竞赛里对车体重量和尺寸有限制优先考虑贴片式的驱动芯片。供电结构上有一条铁律电机电源和逻辑电源必须分开走最终单点共地。电机启动和反转的瞬间电流很大电池电压会被瞬间拉低如果摄像头和单片机直接并联在这条线上画面很容易闪烁甚至掉电复位。我的做法是电池正极先接到电机驱动板从驱动板引出一路经LDO或DC-DC降压到5V给摄像头和逻辑电路两个电源在电池负极处单点相连。这样做之后图像稳定性明显改善。如果项目到了要画PCB板子的阶段布局上有几个细节值得注意电机大电流的回流路径要短而粗尽量避免和摄像头信号线平行走线电机驱动芯片旁边放一个470uF到1000uF的电解电容吸收反向脉冲摄像头排线要远离电机线交叉时尽量垂直否则画面里会出现周期性横条纹干扰。热词里很多人搜“内存条SPD参数要不要配合PCB调整”“MIPI线在线设计”本质都是在讲信号完整性和阻抗匹配小车项目虽然没到那个复杂度但“大电流避开敏感信号”这个思想是通用的提前养成习惯对后面做更复杂的板子很有帮助。电源稳压芯片的散热盘要舍得铺铜我之前用AMS1117给摄像头供电长时间跑下来芯片烫到不敢碰后来换成带有大面积散热焊盘的方案才解决。2.3 一张完整的硬件连接清单给第一次做摄像头循迹的同学列一份可以直接照抄的清单方便备料和接线部件型号/规格数量连接说明车架两驱或四驱小车底盘1预留摄像头支架孔位主控STM32F103C8T6 / ESP32 / OpenMV1核心逻辑处理摄像头OV7725 / OV2640 灰度输出1接DCMI或SPI/并口电机驱动TB6612FNG1AIN/BIN接主控PWMAO/BO接电机直流电机3-6V 减速电机2左右轮独立控制电池7.4V 2S 锂电池1经驱动板供电稳压模块5V LDO 或 小功率DC-DC1给摄像头和主控供电显示屏可选调试用1显示二值化效果接线顺序上我习惯先把电机驱动和电源部分调通再接摄像头。电源接反是这片电路里最容易烧东西的操作买个防反接模块或者自己做一颗二极管串联在电源入口成本几毛钱能救回一堆模块。3. 图像处理链路把“看到”变成“明白”3.1 采集、灰度化与ROI裁剪摄像头原始数据通常是一帧RGB565或YUV422的彩色图像但循迹任务中颜色信息几乎用不到。处理彩色图像的运算量是灰度的三倍以上而且赛道线无非是黑色胶带贴在浅色地板上灰度信息足够充分。所以第一步永远是转灰度图。灰度化的标准公式是Y 0.299R 0.587G 0.114B实际嵌入式项目里很少直接用浮点运算我一般用整数近似替代Y (77R 150G 29*B) 8把系数放大到256以内用移位搞定速度提升好几倍。OpenMV这类平台自带sensor.set_pixformat(sensor.GRAYSCALE)底层已经做好了就不用自己操心。转完灰度之后不要急着对整幅图像做处理。画面里除了赛道线还可能包含车体自身、周围环境、远处墙壁等干扰区域。这时候ROI裁剪就派上用场了。ROI的意思是只截取图像中我们关心的矩形区域比如图像下半部分2/3或者左中右三条纵向窄带后续所有处理都只针对这个区域。ROI选得好等于直接把赛道外的干扰全部丢弃处理速度更快算法鲁棒性也更高。整个图像160×120时二值化和开闭运算全图跑一遍可能要8到10毫秒ROI缩成110×80之后可以压到2毫秒这个性能收益在高速小车上非常关键。3.2 二值化阈值怎么定才稳灰度图拿到之后下一道工序是把赛道线和背景彻底分开。黑色赛道线的灰度值低浅色地板灰度值高如果环境光恒定我们可以取一个固定阈值比如gray_value 50当成赛道其他当成背景。问题在于这个“恒定”根本不成立阳光从左边照过来画面左右两半的亮度能差出一大截教室日光灯和窗外自然光的色温也随时在变。固定阈值在这种环境下惨不忍睹要么把阴影当成赛道要么把黑线照成灰色后直接漏掉。我的做法是使用动态阈值最简单的版本是取ROI区域内全部像素的灰度均值再乘一个经验系数作为阈值。白色地板在灰度图上通常接近200黑色胶带通常低于80均值落在120到160之间系数取0.65到0.7基本能把两者分开。更稳的方案是大津法OTSU它的核心思想是在0到255的灰度直方图上穷举一个阈值使得分割后两类像素的类间方差最大可以理解为“让黑和白彼此差异最大的那个分界点”。OTSU在嵌入式上实现也不复杂直方图统计并累加出来的两重循环扫一遍只有256次耗时几乎可以忽略。实测下来动态阈值比固定阈值在小车上的稳定性高很多基本能做到从黑暗走廊跑到阳光底下都不用重新标定。3.3 开闭运算参数原理与踩坑记录二值化之后画面里依然会有不少噪点。地面灰尘、翻新过的赛道边沿、反光点都会变成白色区域里的小黑块或者黑色区域里的白点。直接拿这个结果去提取中线偏差计算会隔三差五跳一下小车走起来就会一顿一顿的。清除这些噪点靠在图像处理里的形态学运算也就是膨胀和腐蚀的组合。腐蚀运算的数学含义是用结构元素在原图上滑动只有结构元素覆盖的区域全部为1时输出才为1等效于把白色区域边缘“啃”掉一圈。膨胀反过来结构元素覆盖区域内只要有一个1输出就是1白色区域边缘向外长一圈。开闭运算是它们的组合开运算先腐蚀再膨胀。效果是去掉白色区域里独立的小噪点但整体形状和尺寸基本不变。可以理解为先用砂纸把白色颗粒磨掉再把物体恢复原大小。闭运算先膨胀再腐蚀。效果是填充黑色区域里的小孔洞把断开的白色区域拼接起来。相当于先用腻子把小坑填平再把外形修回原样。这里的关键是结构元素kernel尺寸的选择。热词里“机器视觉开闭运算参数原理”被搜得很多说明大家在这里确实容易卡住。用OpenMV举例常见的写法是image.erode(2)这个“2”就是腐蚀强度表示腐蚀半径。kernel太小噪点清不干净kernel太大赛道线会被改得面目全非。我在白色地板、两厘米黑胶带的赛道上实测过腐蚀和膨胀强度各取1到2也就是3×3到5×5的结构元素效果最合适。超过3较细的赛道线会被腐蚀得断成好几截闭运算都补不回来不够1远处的小噪点又会残留。一个容易忽略的细节是开闭运算的顺序经常要根据场景调整。如果主要问题是黑色赛道上有白色反光点那应该先做闭运算把点状的白色噪声填掉如果主要问题是浅色地板上有零散黑色颗粒或者赛道线内部被噪声切成小格子那开运算优先级更高。很多人上来不管三七二十一先做一遍开再做一次闭效果也能凑合但排查到最后往往发现是运算顺序和实际噪声类型不匹配。我习惯先在屏幕上把二值化结果停留住用代码逐帧叠加不同强度的形态学处理肉眼对比哪一种组合最干净确定下来再固化进主循环。3.4 中线提取与偏差计算形态学做完画面里剩下的应该是比较整洁的二值图白色背景、黑色赛道。接下来要回答的问题是“赛道线在哪里”。最简单也比较稳的方法是按行扫描黑白的跳变边缘。对二值图的每一行从左边向右扫描找到第一个从白变黑的列坐标记为left_edge从右边向左扫描找到第一个从黑变白的列坐标记为right_edge。如果两个边缘都存在把它们的均值作为该行的赛道中心点即center (left_edge right_edge) / 2。对ROI内每一行都做这个操作就得到一条纵向分布于画面的中线点集。当然边缘提取不会每次都很完美。天空处的光斑、赛道外的深色物体都可能让某一行找不到有效的黑白跳变。处理原则是“坏行丢弃不硬猜”。比如一行里只找到了一个边缘或者左右边缘之间宽度明显异常直接抛弃这一行不用强行填充。整个画面上有效行数只要超过ROI行数的一半偏差计算依然可靠。偏差的具体计算方法我用的是底部有效中心线的列坐标与画面目标中心的差再归一化到[-0.5, 0.5]。假设画面宽度是160目标中心在80列底部赛道中心在60列那么error (60 - 80) / 160 -0.125。这个值就是后面PID控制器的输入。为什么用底部赛道中心而不是全图中线的平均位置因为车体当前姿态最有参考价值的是离车最近的那一段赛道线它直接反映了车头相对赛道的偏移。远端的赛道线主要是用来预判弯道不该和近端信息混在一起加权。当然如果想让小车在进入急弯前就开始转向可以额外算一个“远端偏差”并给它较小的权重这属于进阶的差分权重策略基础版本用底部偏差就足够了。4. 控制逻辑把偏差换算成方向盘和油门4.1 从偏差到差速的完整公式图像处理输出的误差是一个连续值控制端就要把它转成左右轮的PWM占空比差。最基本的思路是差速转向偏差为正说明赛道中心在画面中心右侧车需要右转那就让右轮减速或左轮加速偏差为负则相反。直接用一个比例系数放大偏差就能实现最简单的P控制PWM_left PWM_base - KP * errorPWM_right PWM_base KP * error这里的PWM_base是直行时的基础占空比KP是比例系数。写出来看似简单实际运行会有几个隐藏问题。第一个是输出限幅问题电机PWM占空比不可能小于0也不可能大于100%所以左右轮计算完之后必须做一次限幅处理。第二个是执行器死区问题电机驱动在小占空比下可能完全不转尤其是当PWM低于某个占空比阈值时所以最好做一个最小占空比保证。第三个问题是比例控制天然会有静态偏差当偏差很小的时候两侧PWM都非常接近基础值小车可能停在一个轻微偏移的位置上不动这时需要引入积分项帮忙消除残余偏差。行得通的完整公式我会写成这样PWM_left PWM_base - KP * error - KD * (error - last_error)PWM_right PWM_base KP * error KD * (error - last_error)这样是PD控制器在循迹场景里比PID更好用。微分项起的是一个阻尼作用当偏差变化很快时它产生一个反向的修正力矩抑制小车来回摆动。积分项在普通循迹中用处不大而且容易积分饱和调试时反而添乱我自己只加了微小的积分上限大部分项目干脆设成0。4.2 PID调参与实际效果PID参数怎么调是每个做小车的人都会头疼一阵的问题。我梳理出一套简单粗暴但是有效率的调参顺序照着做基本能收敛第一步先把PWM_base设得比较保守比如占空比40%保证小车在直道上能稳定起步。第二步KP从0.5开始递增试。观察弯道响应如果小车过弯时转向不足从赛道外侧冲出去说明KP太小如果小车在直道上走S形、来回甩头说明KP太大。第三步KP合适之后加入KD。KD从0.2开始试主要用来抑制蛇形。如果小车过弯后回正出现明显过冲逐步加大KD。第四步观察直道和弯道交叉处尤其是出弯瞬间如果小车有明显顿挫感检查PWM输出是否在某一段跳变太剧烈。实际项目中我最终用的参数一般是KP在1.2到2.0之间KD在0.5到0.9之间具体数值和车架重心、电机响应速度关系很大。调参时有一个重要技巧每修改一次参数只改一个量其他保持不动记录下小车在这一版参数下的表现再决定下一步怎么调。如果同时改KP和KD出了问题根本定位不了是哪边的问题。PID调到最后小车的表现应该是直道轻微摆动但不发散弯道能提前渐入渐出不会猛打方向。如果程序里还能实时地把error值通过串口发回上位机画成波形观察调试效率会进一步大幅提升。之前我踩过一个坑小车过弯时老是往一边偏PID参数怎么调都没用后来通过串口波形发现是一个轮子的PWM输出线接触不良导致某一边时转时不转。所以调参之前先检查机械和电气部分牢不牢靠不然后面每调一步都是白费力气。4.3 实时性与速度控制的取舍摄像头循迹小车最怕的是“反应不过来”。一帧图像从采集到处理完再算出PWM输出这个过程有一定耗时。如果帧率是30fps一帧的理想间隔大约33毫秒但如果图像处理代码写得低效处理时间超过这个周期实际控制频率就会掉到十几赫兹小车每秒钟只能做十几次转向决策。速度稍快这个响应速度根本跟不上弯道变化。提升实时性的手段除了前面提到的降分辨率、用ROI裁剪之外还有一个重要思路是降低红绿通道损耗。灰度转换时不要做浮点运算二值化时不要对每个像素跑复杂函数形态学运算尽量用平台自带的优化库。如果你的主控足够强可以考虑把图像采集和处理分开到两个线程连续采集模式下用帧中断触发处理而不是一边采一边算。实测中把处理耗时控制在5毫秒以内整体控制频率稳定在30Hz以上小车跑起来就非常跟手了。速度控制上一个很实用的策略是“直道快、弯道慢”。怎么判断前方是不是弯道可以通过中线点集在图像中的水平偏移程度来估算。如果近端赛道中心基本在画面中央远端却明显偏向一侧说明前方是个弯道这时候主动把基础速度降下来等出了弯再恢复。这个策略不需要额外传感器完全复用已有的中线计算结果实现成本很低但对过弯稳定性的提升非常明显。我在调车的时候发现只要速度控制在“弯道内轮不打滑”的范围内即使KP没有调到最优小车也能比较平滑地过弯。5. 常见问题与排查技巧实录5.1 图像侧高频故障图像侧的毛病最多而且大多能通过一张调试图直接看出来。我把自己遇到过的典型问题整理了一张速查表方便查阅。现象可能原因解决办法画面全黑或全白摄像头曝光设置错误、镜头未对准、二值化阈值偏得太极端先用灰度图预览确认曝光正常再调阈值赛道线断成一段段开运算kernel过大、阈值偏高把暗线部分切成碎块缩小腐蚀强度阈值向低处调整反光区域被识别成赛道固定阈值在强反光下失效改用OTSU或动态阈值调整摄像头俯角避开光源差速转向时画面抖动电机大电流干扰图像信号逻辑电源独立供电摄像头线远离电机线近处清楚远处模糊镜头对焦固定在近处或远处手动调焦镜头把焦点调到车头前30cm左右其中反光问题最难缠。如果赛道是光滑的PVC地板灯光一照黑胶带表面会反出一条白亮的光带二值化之后正好把黑线从中间劈成了两半。我的处理方式组合拳一是把摄像头俯角调大一点让反射光偏到画面外二是在形态学处理里加大闭运算强度把反光产生的白裂缝填掉三是适当降低阈值宁可让少量浅色噪点混进背景也别让黑线变成白线。三种手段叠加基本能压住大多数反光场景。5.2 控制侧高频故障控制侧的问题往往表现成“图像明明处理对了小车还是不走正路”。这时候别急着乱调PID先分清楚是执行器问题还是控制器问题。执行器问题最常见的包括左右两个电机的特性不一致同一个PWM占空比下转速有明显差异电池电量下降后基础速度曲线整体漂移驱动板的PWM频率设置过低电机发出异响的同时扭矩不足。我之前有一次整车跑直线却越跑越偏左右轮都分别测试过转速静态时一致装上车架后就不行了。排查到最后发现是车轮装配不一样一边轮胎摩擦地面过紧一边过松导致负载状态下转速差被放大。后来给两个轮胎的轴承上了油重新调整轮距和轴套间隙问题一下子消除。所以遇到跑偏先用最简单的方法排除机械问题再做软件调整。控制器角度有一个很隐蔽的点值得提醒微分项对噪声非常敏感如果图像偏差有细微抖动KD稍微大一点输出就会出现高频振荡表现为电机的“滋滋”声和车身抖动。可以在微分项前面加一个低通滤波或者对偏差做一次滑动平均效果立竿见影。5.3 我的独家调试顺序调试摄像头循迹小车最忌一上来就全速跑完整赛道。我的调试顺序固定为三步静态图像调试、直道动态调试、弯道完整测试。静态图像调试阶段把小车架空或者用手拿着在赛道上不同的位置摆出左偏、右偏、正对三种姿态观察屏幕里提取出的赛道中线和偏差数值是否符合预期。这个阶段的重点是确认整个图像处理链路输出是可信赖的。直道动态调试阶段把小车放在直道起点速度调慢让它走一段两米长直道观察是否跑偏、是否摆动如果有问题就在这个阶段把参数基础调好。弯道完整测试阶段先在单个大半径弯道里跑确认转向能力再逐步加入连续S弯和直角弯不停微调KP、KD和弯道减速逻辑。这套顺序的好处是每一步都能快速定位问题范围避免把所有变量搅在一起。如果静态阶段就发现中线提取不稳定那就别浪费时间调控制参数如果静态很稳定动态却不稳定那问题大概率在控制环路或机械部分。按这个顺序排查下来我基本没有遇到过卡住超过半天的问题。最后再分享一个小技巧在调试过程中把摄像头捕捉到的二值化中间结果实时显示到屏幕上或者回传到上位机比盯着小车本身看更有用。因为很多问题光看车体运动轨迹根本判断不了来源但一看中间图像是环境光干扰还是形态学参数不对一眼就能看出来。这个小习惯帮我省下了大量重复调参的时间。如果你也在做摄像头循迹先把摄像头装稳先把图像里那条线提干净后面的控制和水到渠成没有区别。