先说个结论:循迹小车这种东西,大学实验室里十有八九都做过,但大多数人的版本还停留在红外对管、电磁探头这些“碰运气”式传感器上。真正把机器视觉塞进一辆小车里,让它靠摄像头看清赛道、自己算偏差、自己修正方向,这个跨度远比想象中大。这篇文章我就围绕“基于机器视觉的循迹小车设计”这个项目,把从方案选型、图像处理到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参数整定,我没用什么花哨的自整定算法,就是经典的试凑法:
- 先把Kd设为0,只调Kp。从小到大加,加到小车开始轻微振荡的位置,再回退20%~30%,得到稳定的基础比例值。
- 然后加Kd,同样是小幅递增,每次加完跑一段弯道观察,直到转向平滑、不带明显抖振为止。
- 我最终的参数大约在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的学习曲线平缓得多,图像处理函数封装度高,能让你把精力集中在视觉算法与控制的衔接上。等真正吃透了“图像处理后怎么变成控制量”这一层,再往高级平台迁,逻辑是共通的。
最后再分享一个经验:调试视觉小车最忌讳在光线复杂的场地反复硬调参数,正确做法是先找一个白底黑线清晰、光照均匀的场地把全链路调通,再逐步增加场地复杂度。这跟学开车先找空练场一个道理。别让环境变量和算法逻辑问题混在一起,否则你永远分不清是摄像头没看对还是算法写错了。