ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于机器视觉的循迹小车设计与实现:从图像处理到PID控制

基于机器视觉的循迹小车设计与实现:从图像处理到PID控制 先说个结论循迹小车这种东西大学实验室里十有八九都做过但大多数人的版本还停留在红外对管、电磁探头这些“碰运气”式传感器上。真正把机器视觉塞进一辆小车里让它靠摄像头看清赛道、自己算偏差、自己修正方向这个跨度远比想象中大。这篇文章我就围绕“基于机器视觉的循迹小车设计”这个项目把从方案选型、图像处理到PID调参、实车排障的完整链路拆开讲一遍。内容适合正在做课程设计、准备电赛或者单纯想从传统传感器方案升级到视觉方案的爱好者尤其适合那些已经玩过51/STM32基础小车、但对“机器怎么看清路”还一知半解的读者。1. 项目整体设计与方案选型1.1 为什么要选机器视觉做循迹传统循迹方案里红外对管是最常见的。一组红外发射管加接收管黑线吸光、白底反光ADC读回来的电压不一样据此判断是黑是白。优点是便宜、实时性好、逻辑简单严重缺点是对环境光敏感在强光或灯下反光一强误判率直线上升。电磁循迹稍微好一点靠感应赛道中心通电导线产生的交变磁场定位能适应弯道大前瞻但需要场地铺线本质上也是一种“被动感知”。机器视觉则完全是另一个维度——小车用摄像头获取整幅画面相当于有一双眼睛。它能看到的不只是一条线还包括弯道弧度、十字路口、斑马线、起跑线甚至多个目标物。处理得当的话前瞻距离可以达到几十厘米甚至一米速度上限比传统方案高出不少。代价是计算量大、调试维度多这恰恰是这个项目的核心难点和学习价值所在。从工程角度看这项目其实是在模仿自动驾驶的一条简化链路感知摄像头成像→ 决策找赛道中线、算偏差→ 控制PID算转向和速度。把这条链路弄明白以后玩OpenCV、玩SLAM甚至做无人车底层逻辑是通的。1.2 硬件架构怎么搭摄像头、主控与驱动我自己的方案是经典的三层结构很多电赛队伍也是这么搭的摄像头层OpenMV摄像头ST官方那一块OV7725传感器负责采集图像内置的MicroPython环境可以直接跑简单的图像处理算法比如二值化、找色块。主控层STM32F103最小系统板负责逻辑控制、PID计算、接收摄像头串口数据、输出PWM给电机驱动。驱动层TB6612电机驱动模块 两路带编码器的直流减速电机组成两轮差速底盘。前轮用万向轮支撑。选OpenMV而不是直接上树莓派USB摄像头原因很实际OpenMV的IDE里有一堆现成的图像处理函数比如find_blobs()、get_regression()几分钟就能出效果树莓派虽然算力更强但供电、散热、启动时间都是麻烦小车底盘那点电池仓根本伺候不起一个大板子。用C在OpenCV里写图像处理当然也可以但那是另一个量级的移植成本。这里插一个选型细节千万别用不带编码器的电机。编码器输出的是电机真实转速没有它PID闭环反馈就无从谈起开环控制那种“写死占空比”的做法电池电压一掉速度就飘视觉循迹根本稳不住。编码器电机多花十几块钱省下的调试时间远不止这个数。1.3 视觉方案与传统传感器的性能对比我把几个方案放在一张表里看得更直观方案核心器件前瞻距离环境适应性开发难度成本红外循迹多路红外对管2~5cm光线敏感强光反光易误判低逻辑简单很低电磁循迹电感探头信号调理10~20cm依赖通电导线环境电磁干扰敏感中等低机器视觉摄像头主控30~100cm光线变化需算法自适应较高涉及图像处理与控制中高从表里能看出来视觉方案的本质优势是“看得远、看得全”。红外对管的有效范围几乎只能贴着地面所以车一跑快弯道前隔着几厘米才发现要转弯根本来不及。视觉方案在摄像头架设角度正常的前提下可以提前一个车身甚至更远就识别到弯道趋势为PID提供提前量这也是视觉小车能跑出高速的关键。2. 图像处理链路从原始帧到赛道中线2.1 图像预处理灰度化与二值化摄像头拿到的第一张图是彩色图像每个像素RGB三个通道。直接处理彩色图会浪费大量算力所以第一步转灰度。OpenMV里一句img.to_grayscale()就完成了本质上是对RGB三通道按一定权重加权求和灰度值 0.299×R 0.587×G 0.114×B这个权重不是拍脑袋定的它是根据人眼对红绿蓝敏感度的差异选出来的经验值绿色权重最高蓝色最低。很多初学者在这里会犯一个错误直接用RGB的等权平均来做灰度化视觉上区别不大但在某些颜色复杂的赛道材质上阈值分割的稳定性会明显变差。灰度化之后是二值化。赛道是白底黑线或者反过来黑底白线灰度图上黑和白的灰度级有明显差异我需要设一个阈值把大于阈值的像素变成白色255、小于阈值的变成黑色0。关键在于阈值怎么选。两种常用思路固定阈值赛道光线不变时写死一个值比如100。优点是简单缺点是太阳一出来或者灯光一变就完蛋。自适应阈值OpenMV有find_blobs()配合LAB颜色空间来做颜色识别它本质上也是一种自适应的阈值判断基于颜色范围而非灰度值对外界光强变化相对更鲁棒。我最后用的就是LAB阈值法。2.2 提取赛道中线的两种方法拿到二值图之后核心任务变成“找到该往哪走”。两个常用方法Blob色块法。用find_blobs()把黑色赛道目标提取出来返回一个色块对象它自带cx()、cy()坐标。如果只有一个赛道色块直接把cx和图像中心点横坐标相减就得到横向偏差。这个方法最直观但问题在于如果赛道在视野里分成前后两段比如过十字路口可能出现两个色块需要自己合并或者取最大块逻辑稍复杂。线性回归法。OpenMV的get_regression()可以从ROI区域里做线性回归把所有黑色像素拟合成一条直线返回一条包含方向角度的线段。这对直线赛道和缓弯道都很好用拿回归线的theta()角度做转向效果比单纯用cx更平滑。缺点是急弯处黑线可能超出ROI拟合出一条歪线。我实际用的是两者的结合直线段用回归角度严重丢线时切到色块中心。这个策略让小车的转向动作明显比以前只用cx平稳得多——cx本质上只反映“当前车头旁边”的横向位置而回归线角度同时包含了航向信息相当于预判了趋势。2.3 关键参数计算图像分辨率与前瞻距离这里有一个很多人忽略的计算摄像头能看到多远取决于架设高度和俯仰角。我的车把摄像头架在约8cm高度向下俯视30度角视野范围大约能覆盖前方15~50cm的路面。为什么不在正上方垂直朝下看垂直朝下视野太窄只能看到车头正下方一小片称不上“视觉循迹”角度太平又会看到远处背景干扰太多。30到45度俯角是个实用经验值兼顾了前瞻和干净背景。分辨率方面我一开始用QVGA320×240跑OpenMV的帧率已经能到30帧左右。后来做抛物线插值计算中线的时候发现320宽的图像已经够用——每像素大约对应路面1.25mm对控制精度来说绰绰有余。再往上堆分辨率只会拖慢帧率纯属浪费。3. 控制策略与核心算法3.1 PID控制在循迹小车里的角色图像处理给出偏差如果把偏差直接当转向量用小车会左右甩头、严重振荡。这是PID要解决的核心问题。循迹小车最常用的形式是位置式PD控制u(k) Kp×e(k) Kd×[e(k) − e(k−1)]其中e(k)是当前帧的横向偏差像素e(k−1)是上一帧的偏差。为什么建议用PD而先不加积分项两个原因循迹过程中偏差是持续变化的动态量积分项累积历史误差容易导致过冲反而坏事。除非小车跑的是无限长的绝对直线否则I项作用有限。D项相当于一个“阻尼”它看的是偏差的变化趋势。假如车头正在朝左偏D项会给出一个反向修正提前抑制过冲这是循迹小车稳定的关键。关于PID参数整定我没用什么花哨的自整定算法就是经典的试凑法先把Kd设为0只调Kp。从小到大加加到小车开始轻微振荡的位置再回退20%~30%得到稳定的基础比例值。然后加Kd同样是小幅递增每次加完跑一段弯道观察直到转向平滑、不带明显抖振为止。我最终的参数大约在Kp0.8、Kd1.5这个量级但每个车的底盘、舵机响应速度、图像帧率都不一样这组值只能当起点参考不能直接照抄。3.2 转向与差速控制的映射OpenMV算出的偏差单位是像素而STM32的PWM寄存器是0~1000的占空比值之间需要一个映射关系。用一种相对保守的思路处理转向修正量 PID输出值左右轮目标速度 基础速度 ± 转向修正量举个例子基础速度设为80PWM计数值某帧偏差e30像素PID输出u25那么左轮 80 25 105 右轮 80 - 25 55差速效应让小车向目标方向偏转。这里有个细节转向修正量的权重通常需要加个系数衰减比如乘以0.3~0.5否则偏差稍微大一点左右轮速差就过大小车容易原地打转。我实际在代码里给PID输出乘了衰减系数并把左右轮限幅在40~160之间既保留了修正强度也避免电机瞬间进入不线性区。3.3 丢线和十字路口怎么处理丢线是视觉循迹最容易出现的致命问题——弯道太急、反光过强、或者色块被干扰物遮挡都会让find_blobs()返回空对象。处理策略分两级一级是“记忆上次方向”——上一帧还有有效偏差时如果当前帧丢线就按上一帧的方向继续转并把速度降为60%。这模拟的是人类开车时不完全确定路况先减速试探的操作。二级是“强制寻线”——连续丢线超过10帧说明大概率出问题了执行一个固定角度的转弯搜索直到重新找到黑线。十字路口则是另一种局面二值图像里通畅的十字路口看起来像一整块黑色连通域常规色块方法提取中线会跳不稳定。我的办法是检测白色区域的横向宽度当连续多帧提取到“白线断成数段”的模式时判定为路口然后设定一个直行优先策略不丢转向、匀速通过。4. 实车调试与常见问题排查4.1 照明与反光对图像的影响这是整个项目里最磨人的环节。实验室棚顶灯、阳光直射、地板瓷砖本身的镜面反射都会让二值化结果忽黑忽白。最初用固定阈值时灯光一换小车立刻失明方向乱打。最后有效的做法是换到LAB颜色空间做阈值分割。LAB的L通道是亮度色度信息全部集中A/B通道上我把黑色色块的L通道阈值范围设成(0, 50)A和B通道放宽一些这样把“黑”定义成低亮度而不是某种RGB组合反而对亮度变化不敏感。另外在OpenMV的IDE里可以实时调整阈值滑块现场对着几种不同光环境各标定一次合并成一组合适的阈值范围。4.2 电机干扰与图像抖动小车跑起来之后电机换向产生的尖峰干扰会让画面出现横纹甚至偶尔开出畸形色块。排查时用示波器量过电机端的电压波形起步瞬间纹波能到几百毫伏的毛刺相当吓人。对策是三层一是电机的正负引脚并联几颗100nF陶瓷电容加一颗100μF电解电容做吸收二是摄像头排线尽量远离电机电源线交叉走线改成垂直走线三是给OpenMV单独用了一路AMS1117稳压芯片跟电机驱动模块的电源彻底分开。处理完这三步画面稳定多了误检率明显下降。4.3 常用问题速查表现象可能原因解决办法时常丢线找不到色块曝光过高/反光干扰改用LAB阈值降低摄像头曝光时间调整镜头角度避开光源直射直线走得稳弯道冲出赛道前瞻距离不够或PID过冲增大摄像头俯角提高前瞻调大Kd抑制过冲小车左右剧烈摆动Kp过大调小Kp适当加Kd转向输出加衰减系数电机全速时画面抖动电源纹波过大电机端并联去耦电容摄像头单独稳压供电十字路口方向乱跳图像连通域分裂增加路口检测逻辑识别到路口后锁定直行固定速度跑起来有顿挫感PWM占空比非线性使用速度闭环用编码器做PID调速4.4 上位机调试的有效方法我在开发过程中最受益的是把串口输出推到一个自写的C#上位机里实时显示二值图像、赛道中线和偏差值。C#写这个不复杂用OpenCVSharp打开传统串口接收图像字节流OpenMV那边把图像编码成JPEG通过串口发帧。帧率不高大约10帧但足够用来肉眼观察算法效果。很多资料教人直接在OpenMV IDE看屏幕图像当然可以但如果想边跑边调无线图传或者串口回传比插线调试方便得多。我做了一个简单的窗体左边实时画面右边一组调试滑块PID参数和阈值都在上位机里改串口下发到STM32或OpenMV省去了反复烧录的麻烦。这套东西跑通之后整个项目的调试效率至少翻了一倍。5. 项目可以往哪些方向扩展这个项目做完如果还想继续深挖路线是清晰的。一个是把OpenMV换成树莓派或者Jetson Nano跑完整的YOLOv5或者TensorFlow Lite目标检测模型让小车识别的不只是黑线还能识别锥桶、红绿灯、行人模型这就从视觉循迹升级成了视觉避障加目标跟随。另一个方向是把单纯的前馈转向PID扩展成完整的路径规划——存一帧帧的赛道中线坐标离线生成目标路径再结合编码器里程计做局部位姿估计。这就有点SLAM的味道了。在电赛里很多队伍把“视觉循迹”作为基础模块真正拉开差距的是多目标识别、断头路处理、以及更高的直道速度下如何稳定过弯这些都可以在前面那套框架上迭代。如果只从性价比来谈我强烈建议新手第一版先做OpenMVSTM32这个组合而不是一上来就买树莓派。OpenMV的学习曲线平缓得多图像处理函数封装度高能让你把精力集中在视觉算法与控制的衔接上。等真正吃透了“图像处理后怎么变成控制量”这一层再往高级平台迁逻辑是共通的。最后再分享一个经验调试视觉小车最忌讳在光线复杂的场地反复硬调参数正确做法是先找一个白底黑线清晰、光照均匀的场地把全链路调通再逐步增加场地复杂度。这跟学开车先找空练场一个道理。别让环境变量和算法逻辑问题混在一起否则你永远分不清是摄像头没看对还是算法写错了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进