基于机器视觉的循迹小车设计与实现:从图像处理到PID控制
2026/9/8 14:55:40 网站建设 项目流程

先说个结论:循迹小车这种东西,大学实验室里十有八九都做过,但大多数人的版本还停留在红外对管、电磁探头这些“碰运气”式传感器上。真正把机器视觉塞进一辆小车里,让它靠摄像头看清赛道、自己算偏差、自己修正方向,这个跨度远比想象中大。这篇文章我就围绕“基于机器视觉的循迹小车设计”这个项目,把从方案选型、图像处理到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度俯角是个实用经验值,兼顾了前瞻和干净背景。

分辨率方面,我一开始用QVGA(320×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参数整定,我没用什么花哨的自整定算法,就是经典的试凑法:

  1. 先把Kd设为0,只调Kp。从小到大加,加到小车开始轻微振荡的位置,再回退20%~30%,得到稳定的基础比例值。
  2. 然后加Kd,同样是小幅递增,每次加完跑一段弯道观察,直到转向平滑、不带明显抖振为止。
  3. 我最终的参数大约在Kp=0.8、Kd=1.5这个量级,但每个车的底盘、舵机响应速度、图像帧率都不一样,这组值只能当起点参考,不能直接照抄。

3.2 转向与差速控制的映射

OpenMV算出的偏差单位是像素,而STM32的PWM寄存器是0~1000的占空比值,之间需要一个映射关系。用一种相对保守的思路处理:

转向修正量 = PID输出值

左右轮目标速度 = 基础速度 ± 转向修正量

举个例子,基础速度设为80(PWM计数值),某帧偏差e=30像素,PID输出u=25,那么:

左轮 = 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的味道了。在电赛里很多队伍把“视觉循迹”作为基础模块,真正拉开差距的是多目标识别、断头路处理、以及更高的直道速度下如何稳定过弯,这些都可以在前面那套框架上迭代。

如果只从性价比来谈,我强烈建议新手第一版先做OpenMV+STM32这个组合,而不是一上来就买树莓派。OpenMV的学习曲线平缓得多,图像处理函数封装度高,能让你把精力集中在视觉算法与控制的衔接上。等真正吃透了“图像处理后怎么变成控制量”这一层,再往高级平台迁,逻辑是共通的。

最后再分享一个经验:调试视觉小车最忌讳在光线复杂的场地反复硬调参数,正确做法是先找一个白底黑线清晰、光照均匀的场地把全链路调通,再逐步增加场地复杂度。这跟学开车先找空练场一个道理。别让环境变量和算法逻辑问题混在一起,否则你永远分不清是摄像头没看对还是算法写错了。

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

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

立即咨询