1. 为什么做机器视觉循迹小车,而不是继续用红外对管
做循迹小车,很多人第一反应是红外对管方案,四个、六个或八个灰度传感器一字排开,沿着黑白分明的赛道识别。这方案确实经典,51单片机就能跑,代码逻辑简单,调起来也不费劲。但我自己在做完第一版红外循迹之后,很快碰到了几个绕不过去的坎:环境光一强,传感器读数就飘;赛道颜色稍微脏一点、有反光,误判率直线上升;到了连续S弯,靠几个离散点判断走向,控制起来非常生硬。说白了,红外对管采到的信息太少了,几个点根本描述不了赛道的形状。
后来我把方案整个推倒,换成了摄像头加机器视觉的思路。核心变化是感知维度的升级:不再纠结于“某几个位置是黑是白”,而是直接从图像里还原出完整的赛道轮廓,小车道在哪、弯道多急、偏离了多少,全都一目了然。这篇文章就围绕这个“基于机器视觉的循迹小车设计”展开,把我从选型、搭硬件、调算法到实车跑通的全过程梳理一遍,重点讲清楚每一步为什么这么选、怎么落地。
这套方案适合谁?一个是准备参加智能车竞赛、想从传统传感升级视觉方案的学生团队,另一个是对机器视觉入门感兴趣、想做点有挑战性项目的嵌入式爱好者。做这个项目需要的基础大概是:会一点C语言、用过STM32或树莓派、了解最基础的PID控制概念。都不用精通,边做边补完全来得及。
2. 方案选型:三种循迹路线对比与机器视觉的定位
2.1 红外、电磁、视觉三套方案的真实差异
我见过不少团队在选择循迹方案时摇摆不定,所以先把三种主流路线的优劣摆开来说清楚。
红外循迹胜在便宜和简单,一组灰度传感器几十块,代码几百行就能跑通,适合入门练手。但它的上限也很明显:信息量太少。传感器的数量和间距决定了空间分辨率,哪怕排了八个探头,也只能知道八个点的灰阶值,赛道曲率、丢线后的趋势判断只能靠插值猜。而且红外对黑色和蓝色的区分能力很差,遇到深色背景赛道基本就得改阈值。
电磁循迹在竞赛圈也很流行,原理是在赛道中心线铺设通有交变电流的导线,车上的电感线圈感应磁场强度,依据左右线圈的差分信号判断车相对于导线的位置。好处是不受光照影响,可靠性很高,但它的使用场景被严格限定在“有预设电磁线”的场地上,出了赛场就没有意义了。而且电磁信号容易受到环境中的金属物体、大功率电机干扰,布线也需要额外小心。
机器视觉循迹走的是另一条路:通过摄像头实时采集赛道图像,用图像处理算法从中提取赛道边缘和中线,然后传给控制端。它的开放性最强,换一个场景,只要重新训练识别模型或调整阈值就能适应;信息量也最大,可以看到前方一两米的完整路况,提前入弯、切弯、减速都有依据。缺点也实打实存在:计算量比单片机传统程序高一个量级,对硬件平台有要求;处理链路长,摄像头采图、算法处理、控制输出每个环节都可能有延迟;光线变化是最大变量,逆光、阴影、反光都会让识别效果打折扣。
三套方案放在一起,我最后选机器视觉的原因很简单:我不想在传感器数量和赛道条件上继续做妥协,想做一个真正“看得见路”的车。即便它前期调试工作量更大,这个投入是值得的,因为一旦视觉识别稳定了,后续扩展避障、标识牌识别、赛道元素分类都非常顺手。
2.2 算力平台的平衡点:STM32还是树莓派或其他
定了视觉方向,接下来就是主控选型。这个决策直接决定了你的开发方式。
低端选择是STM32F4系列配摄像头模块,比如OV7725,图像分辨率通常是QVGA(320×240)甚至更低。这个组合的优点是实时性强、启动快、功耗低,底层直接操作寄存器,适合对执行频率有严格要求的场景。缺点是算法空间极其有限,想在单片机上跑稍微复杂一点的处理,例如透视变换、边缘拟合,内存和算力都捉襟见肘,很多团队做出来的实际效果是“能开会抖”,但谈不上稳。
中高端选择是树莓派或Jetson Nano这类带Linux系统的板子。OpenCV直接装,Python或C++随你写,图像处理算法的实现成本断崖式下降。缺点是系统实时性不如裸机,进程调度、驱动层、USB传输都可能引入不确定的时延,而且上电启动到系统就绪需要好几秒,不适合需要即时响应的场合。
那有没有中间路线?我最终采用的是“STM32 + 树莓派”分工协奏的组合:树莓派负责图像采集和视觉算法,识别出赛道中线的偏差值后,通过串口以固定协议发给STM32;STM32负责执行控制,接收偏差数据、跑PID、驱动电机。这个架构的好处是把视觉和控制两件事解耦:视觉部分出问题,控制端至少还能按上一帧数据兜底;控制算法要调参,也不需要反复动视觉代码。
这个分工模式也接近工业界很多真实视觉项目的前后端分离思路,做完这个项目之后,再去接触实际生产线的视觉定位系统,你会发现逻辑是相通的。
2.3 摄像头与镜头选择的经验之谈
很多初学者忽略摄像头本身对效果的影响,我踩过这个坑。一开始我用的是免驱USB广角摄像头,视角大概120度。装上车之后发现问题很明显:画面边缘的赛道被严重拉伸,弯道曲线畸变得厉害,算法提取出来的中线跟实际路径偏差很大。
后来换了视角在60到90度之间的普通工业USB摄像头,畸变控制在可接受范围。如果你用的树莓派,直接用官方的Camera Module其实就够,驱动成熟,文档也全。传感器的选择上,黑白或者彩色都行,关键是性能要支持至少30帧的全局曝光——卷帘快门在车身震动时会产生果冻效应,图像拉斜,识别就废了。
镜头焦距方面,小车的底盘低,摄像头安装离地面大约20到30厘米,俯视角度25到45度比较合适。角度太小,视野近,看不到远处的弯道,来不及减速;角度太大,虽然看得远,但近处的赛道线占画面比例太大,又容易在贴弯时跟丢。我调了一个多星期才找到“既能看近、又能保远”的平衡角度,大概35度左右,视野里赛道呈梯形展开,既保留了足够的纵深信息,又不容易丢线。
还有一个容易被忽视的点:镜头固定。车身行驶时的震动会让镜头位置发生微小的偏移,导致图像的透视关系漂移,算法标定好的参数就会失效。我用热熔胶把镜头底座彻底固定死之后,这个问题就消失了。
3. 硬件设计与系统搭建:看起来简单,实际暗坑不少
3.1 整体硬件架构和每个模块的选型理由
机器视觉循迹小车的硬件可以分为三大块:图像采集端、主控与算法端、底层运动控制端。我用的具体组合如下:
- 图像采集:200万像素USB免驱摄像头,输出YUV格式,最大支持60帧,实际跑30帧。
- 视觉算法端:树莓派4B(4GB版本),安装Python3 + OpenCV。
- 运动控制端:STM32F103C8T6。很多人说这个板子性能不够,但我只让它跑PID和电机控制,资源完全够用。
- 电机驱动:TB6612FNG。相比老掉牙的L298N,它的压降低、发热小、响应快,体积也小得多,非常适合这种小尺寸车模。
- 电机:N20微型减速电机,带霍尔编码器,用于测速形成闭环。200转版本比较合适,太快了视觉跟不上,太慢了体现不出优势。
- 电源:两节18650锂电池串联(7.4V)给电机供电,再通过降压模块给树莓派(5V/3A)和STM32(3.3V)供电。
这套系统的数据流是单向的:摄像头采集图像 → 树莓派运行图像处理算法提取赛道中线偏差 → 通过串口(波特率115200)发送偏差数据到STM32 → STM32跑PID计算电机PWM → 电机驱动输出(编码器测速回传给STM32形成闭环)。
3.2 电源布局和信号隔离是稳定运行的前提
推车之前,先说你如果偷懒跳过电源设计会遇到什么。我用过一套劣质降压模块,树莓派一旦有USB摄像头数据并发传输,电压就有明显跌落,轻微的表现是WiFi断连,严重的直接重启。还有一个团队的同学,车高速跑的时候STM32频繁复位,查了好久才发现是电机换向产生的反向电动势串到了单片机电源轨上。
所以电源分配的原则是“强电和弱电完全分开铺”。电机的电直接走电池正负极,不要和逻辑电共用一根长线;降压模块的输出端要加低ESR的电解电容;所有的地线在电池负极处单点汇合,不要形成环形地回路。电机驱动TB6612的VM和VCC要分开接,VM接电池正极,VCC接5V(给逻辑电路)。板上每一路电源都对地并联一个100uF电解电容和一个0.1uF陶瓷电容,大电容稳低频,小电容滤高频。
这个细节没人提醒你的话,可能要花好几个晚上排查“车为什么老是莫名其妙重启”这种玄学问题。
3.3 树莓派与STM32之间的通信协议设计
两个主控之间的通信是系统的一个潜在瓶颈,如果协议设计得不好,后面调试时有你受的。我的做法是定义了一个固定长度7字节的协议帧:
帧头(2字节) + 数据类型(1字节) + 数据内容(2字节) + 校验(1字节) + 帧尾(1字节)帧头用0xAA 0x55,避免被误判;数据类型区分是中线偏差值、速度指令还是心跳包;数据内容直接发偏差值的int16,单位是像素;校验用简单的异或校验,STM32每次收到先验证,错误就丢弃等待下一帧。帧尾固定为0x0D。
这里要特别提一个容易被忽略的问题:通信双方的数据包频率必须匹配。树莓派视觉帧率设定为30fps,串口发送也按这个节奏来;STM32那边的PID控制周期固定为10ms,它每次都去取“最新的一帧偏差”,而不是“积压了多久的偏差”。这样即使视觉偶尔卡顿,控制端也永远用的最新数据,不会因为旧数据导致转向滞后。
数据协议里最好带一个“丢线标志位”。当摄像头完全看不到赛道时,偏差值失去意义,这时协议里额外发送0x01表示丢线,STM32收到后执行预设的车速保持策略,而不是用随机偏差去猛打方向。这个字段帮我把车从冲出赛道边缘的尴尬场景里救回来好多次。
4. 图像处理:从摄像头画面到赛道中线的完整流程
4.1 图像预处理,先把脏东西洗掉
摄像头直接输出的原始图像直接用来寻线是不可行的,噪声太大、亮度不均匀、透视变形太严重。我按顺序做了四步预处理:
第一步是灰度化。如果摄像头输出的是RGB图,可以先取G通道代替灰度图,因为在光照变化剧烈的环境下绿色通道的信噪比通常更好。也可以用OpenCV的cvtColor函数转灰度,效果接近。彩色赛道可能需要用HSV颜色空间提取特定颜色分量,但一般黑白赛道灰度图就够了。
第二步是高斯模糊。卷积核取5×5或7×7,目的是去除图像噪声和孤立噪点,让梯度和边缘检测更稳定。模糊核太大容易模糊掉赛道边缘细节,太小又起不到降噪效果,5×5是实践中的平衡点。
第三步是感兴趣区域。我的相机安装角度决定了图像上半部分是墙壁、空地和远处无关物体,直接裁掉。具体做法:透视变换前,先把图像上方的无用区域切掉,只留下包含赛道从近到远的区域。
第四步是光照补偿。这一步容易被忽略,但我强烈建议在正式比赛或长时间调试时加上。简单方法就是用自适应直方图均衡化(CLAHE),对光照不均的赛道有奇效。完整处理代码大致是这样的:
import cv2 import numpy as np def preprocess(frame): # 转为灰度 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 高斯降噪 gray = cv2.GaussianBlur(gray, (5, 5), 0) # 感兴趣区域裁剪,假设分辨率640x480 roi = gray[240:480, 0:640] # 自适应直方图均衡化,增强对比度 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) roi = clahe.apply(roi) return roi4.2 透视变换:把“梯形赛道”拉成“俯视图”
相机俯视赛道时,图像里近处的赛道宽、远处的窄,形成梯形。直接用这个画面计算偏差,远处的赛道信息几乎没用。解决方法是做透视变换(鸟瞰图变换),把梯形区域“拉伸”成矩形俯视图。
我在标定透视变换参数时,先让车停在直道上,在图像中手动选了四个点:左近、右近、右远、左远,对应实际赛道四条边界上的位置。然后映射到一个矩形区域,宽320像素、高480像素。变换矩阵通过cv2.getPerspectiveTransform计算:
def bird_eye_view(binary): h, w = binary.shape # 原图中的四个顶点(按实际赛道位置调整) src = np.float32([[80, 400], [560, 400], [600, 200], [40, 200]]) dst = np.float32([[80, 480], [560, 480], [560, 0], [80, 0]]) matrix = cv2.getPerspectiveTransform(src, dst) result = cv2.warpPerspective(binary, matrix, (w, h)) return result这儿一个关键点是首次标定时车必须停在直道正中央。否则变换出来的俯视图存在系统性偏转,之后所有距离估算都会出错。标定完,画面里的赛道应该是两条近似平行的直线带,弯道也是平滑的曲线,不再有透视收缩。
4.3 二值化与边缘提取:别迷信大津法
灰度图转二值图是决定识别成败的关键一步。赛道底色通常是浅色的,赛道线/边界是深色的,理想情况下阈值分开就行了。但光照变化会让同一场景不同区域的亮度差很多,固定阈值就没法用。
我会用两种方式混合处理。第一是自适应二值化(adaptiveThreshold),对每个像素邻域单独计算阈值,对光照不均匀有天然抗性,但速度稍慢,噪声也偏多。第二是大津法(Otsu),它自动计算全局最优阈值,速度快,但整体光照剧烈变化时适应性差。
我的实测经验是:在均匀光照下用大津法,速度最快、效果也干净;在极端灯光的室内环境下用自适应阈值更有保障。你可以在上位机界面里做两个按钮,一个“自动阈值模式”,一个“手动阈值模式”,调试时随时切换,非常方便。手动模式的阈值滑块是我那段时间调得最多的控件之一。
二值化做完之后,下一步是找赛道边缘。我用的方法是逐行扫描鸟瞰图:对每一行,从左往右找到第一个像素值为255的点的x坐标(记为left),从右往左找到第一个白色点(记为right),中心点就是这两个坐标的平均值。把每一行的中心点连起来,就是整条赛道的动态中线。代码示意:
def find_center_line(binary): h, w = binary.shape left_points = [] right_points = [] for row in range(h): row_data = binary[row, :] left = np.argmax(row_data > 0) if np.any(row_data > 0) else -1 right = w - 1 - np.argmax(row_data[::-1] > 0) if np.any(row_data > 0) else -1 if left >= 0 and right >= 0 and right > left: left_points.append((row, left)) right_points.append((row, right)) centers = [(row, (left + right) // 2) for (row, left), (row, right) in zip(left_points, right_points)] return centers如果某行找不到左右边缘,说明这一行丢线了,需要做“丢线恢复”:用上一行找到的中线位置作为先验,向两边扩展搜索范围;仍然找不到就标记为失效行,让后面的平滑算法去处理。
4.4 中线平滑与曲率估计
直接拿逐行扫描出来的中心点连线给控制端,车会抖得没法看。因为二值化的边缘会有微小抖动,中线的抖动被直接放大成转向量的抖动。所以必须做拟合和平滑。
我试过直线拟合、二次多项式拟合、三次样条拟合三种方式。直线拟合最简单,但在弯道上误差极大;三次样条拟合最精确,但计算开销大,对树莓派的CPU不友好(实测平均一帧多花4到5毫秒);最终我选了二次多项式拟合,既保留了弯道曲率信息,又够轻量。在鸟瞰图上,赛道中线大致是对称的抛物线或直线段,用最小二乘法拟合一个二次函数y = ax² + bx + c,a就代表曲率,b代表方向。一个巧妙的控制点:如果只看车前部固定距离处的中线与图像中心的横坐标偏差,就能把控制量简化为一个像素差值。
平滑我当时用的是“滑动窗口平均+限制相邻帧变化率”。限制变化率的意思是:如果当前帧的偏差值与上一帧相比超过一个阈值(我设的40像素),就认为这一帧是异常,改为采用上一帧的值加上一个最大步进量。这个方法比单纯用卡尔曼滤波简单得多,但对震动跳帧非常有效。
4.5 图像处理链路的技术选型复盘
整套图像链路选用的是OpenCV。再多聊一点为什么是OpenCV而不是自己硬写像素循环:虽然树莓派4B的CPU不算强,但OpenCV底层大量使用SIMD指令优化,处理一张640×480的图像做上述全部步骤,实测平均耗时12毫秒左右(包括高斯模糊、自适应阈值、透视变换和逐行扫描),帧率能稳定到30fps以上,完全满足小车实时控制需求。你用Python写循环做同样的事,可能一帧就要几百毫秒,那车早就冲出赛道了。
结论是:机器视觉的应用关键不一定在于算法多高级,而在于把一套性价比足够高的算法组合优化到能跑实时。先把这层地基打牢,再上深度学习模型也不迟。
5. PID控制与循迹策略:视觉给了方向,控制决定成败
5.1 控制需求拆解与PID结构设计
视觉端给出的核心输出是“路径偏差”,单位是像素:中线和图像中心的横向差值。正值表示赛道偏右,负值表示偏左。但这个偏差值不能直接丢给电机,需要经过控制算法转化为合适的转向PWM和行驶PWM。
小车的运动控制有两个关键输出:方向环(转向)和速度环(油门)。方向环我用的是PD控制,位置式PD非常适合这种以偏差为输入的转向任务,因为转向不需要累积误差,反而需要“阻尼”来抵抗震荡。速度环我用的是增量式PI控制,根据编码器测得的实际转速和期望转速的差来计算PWM增量,避免积分饱和问题。两个环分开设计,互不干扰。
// STM32方向环PD控制 float direction_pid(float error) { static float last_error = 0; float p_term = 0, d_term = 0, output = 0; p_term = KP_DIR * error; d_term = KD_DIR * (error - last_error); output = p_term + d_term; // 限制输出 if (output > MAX_STEERING) output = MAX_STEERING; if (output < -MAX_STEERING) output = -MAX_STEERING; last_error = error; return output; }PID系数我最终调试出来的参考值:方向环KP=0.6,KD=1.8(对单位像素误差输出合理PWM增量),速度环KP=0.05,KI=0.08(对单位转每秒误差输出合理PWM增量)。这个数值只能做参考,因为不同车的底盘、电机、电池电压差异很大,一定得在自己车上重新整定。
5.2 弯道减速与直道加速的策略
纯PID控制能解决直道和缓弯,但高速过连续S弯时容易翻车。这是因为视觉路径预瞄距离远,车到了弯心才发现偏差太大,转向已经来不及了。所以我在速度环上叠了一层“弯道感知”策略。
利用二次拟合曲线的系数a来判断弯道曲率。a绝对值大表示弯急,速度目标值就低;a接近0表示直道,速度目标值可以提到最高。实现:
# 视觉端识别弯道曲率后调整发送目标速度 curve = poly_fit[0] # 二次项系数 if abs(curve) < 0.001: target_speed = 120 # 直道高速 elif abs(curve) < 0.003: target_speed = 80 # 缓弯中速 else: target_speed = 50 # 急弯低速另外还有一个重要策略:入弯减速,出弯加速。这个需要把弯道看作一个连续事件。我的做法是在视觉端实时计算“弯道标志位”,当连续三帧的曲率都大于阈值时,判定进入弯道并降低全局目标速度;当连续两帧曲率都很小时,判定出弯、恢复高速。不要只看单帧曲率,否则直道上的微小噪声会误触发减速。
5.3 路径规划思路:跟随中线还是内切过弯
这个是最有讨论价值的点。很多人做循迹默认就是“跟踪中线”,也就是车始终沿着赛道中线跑。这个策略在直道上没问题,在连续弯道里就很吃亏:不断大角度转向,速度被迫降得很低,动能损失大,跑出来的圈速也难看。
我实验过一种“延迟前瞻”(Late Apex)策略:不跟踪中线的全段,而是把车前特定距离外的一个点作为参考目标,朝那个点行驶。这个点离车越远,车的路径越“切弯”;离车越近,越贴近中线。这个策略配合弯道曲率调整前瞻距离,可以达到一种类似车手内切过弯的效果。
实际效果对比很明显:同一段有连续S弯的赛道,纯中线跟踪最高速度只能到30%(归一化速度),切内线策略能到50%甚至60%,过弯也更顺滑,车身侧倾感减少。代价是策略调试稍微复杂一些,并且要求视觉给出的中线位置质量极高——如果中线抖动大,前瞻点也会乱跳。建议先把中线稳定跟踪做到位,再考虑切弯优化。
5.4 控制频率与延迟分析
延迟是这个系统最致命的敌人。我实测链路的各端延迟大概是这样:
| 环节 | 参考延迟 |
|---|---|
| 摄像头采集 | 约5ms(30帧时) |
| 树莓派图像处理 | 约10~15ms |
| 串口传输 | 约1ms |
| STM32 PID计算 | 约0.1ms(10ms周期任务) |
| 电机响应 | 约5~10ms |
| 总延迟 | 约25~35ms |
这就意味着车看到前方弯道后,大约要等30毫秒才做出转向动作。以车速1.5m/s计算,这30毫秒里车已经前进了将近5厘米。看起来不多,但赛道宽度可能总共才40厘米,这点延迟足以让车在高速急弯中冲出去。所以控制端周期必须是“最新偏差优先”,视觉端的图像帧率也要高于实际控制周期,否则视觉输出的每一帧对控制来说都是滞后的。这也是为什么我坚持用30fps视觉去匹配10ms控制周期,相当于控制端每3帧图像做一次决策,延迟可控。
6. 上位机调试与C#视觉监控:没有调试工具的视觉项目寸步难行
6.1 为什么需要C#上位机
很多做小车的人调试方式就是盯着终端看printf打印的偏差数字。数字看几十行还行,看几百行眼就花了,而且摄像头图像是不是正常、阈值设得合不合理,终端上完全看不出来。
后来我花了两个晚上写了个C#上位机。这个上位机通过WiFi接收树莓派发来的图像帧(用TCP传输,压缩成JPEG格式)和识别结果(中线位置、曲率、丢线状态),侧边栏用滑块调节阈值、曝光等参数,滑块改动实时通过串口指令下发给树莓派并即时生效。画面的中间层叠加显示蓝色识别出的赛道边缘、绿色中线,调试效果直观到让人感动。写这一小段上位机的收益远超投入,我现在强烈建议每个做视觉项目的团队都做一套类似工具。
6.2 上位机功能设计与协议实现
上位机的核心功能我拆成了四个面板:
第一块是视频实时画面,可以叠加跟踪结果。实际上这个就是把你图像处理的可视化结果转发到电脑上,比如原图、二值化结果、鸟瞰图、带中线的原图,四个画面切换。你能清楚地看到“算法眼中”的世界,而不是猜。
第二块是参数调节面板。阈值、高斯模糊核大小、透视变换坐标、目标速度,全部做成滑块。调参数时不用反复改代码重启程序,这个对找“阈值窗口”的帮助非常大,我原来改一次代码要花一分钟,现在滑动滑块一秒钟就能试一个新值。
第三块是曲线示波器,显示偏差值、曲率、目标速度和实际速度的实时曲线。排查PID震荡和视觉帧率不稳的时候,这个面板提供的信息远超一堆日志。
第四块是数据记录与回放。把每一帧识别结果保存成CSV,跑完可以复盘数据,特别是看哪个时间点车开始抖、哪个时间点电机响应滞后。
协议设计上,上位机和树莓派之间是TCP JSON格式,树莓派每帧发一个JSON消息:
{ "type": "telemetry", "seq": 1024, "timestamp": 12345.67, "image_base64": "...", "center_line": [120, 124, 130, 142, ...], "curve": 0.0021, "lost": 0, "target_speed": 80, "actual_speed": 75.2 }上位机解包渲染。树莓派上还要开一个TCP服务端接收上位机的参数变更请求,解析后写入全局变量。这套架构写起来不复杂,直接兼容任何Linux板子,不局限于树莓派。
6.3 摄像头标定与图像调试的一个实操技巧
上位机调参时有一个小技巧很管用:在画面里画一个“阈值分布直方图”。每次调阈值前,先把当前ROI的灰度直方图画出来,你就能看到赛道和背景的两个峰分别在哪,选阈值就是选两峰之间的谷底。这个直方图视图加上滑块,实测能让阈值调试时间缩短一半以上。
另外,所有标定参数(透视变换的四个点、ROI上下界、阈值上下限)都不要硬编码进主程序里,放进一个config.json文件,上位机里改完参数后一键同步到树莓派。否则每次调参都要SSH上去改文件,效率极低。
7. 常见问题与排查技巧实录:这些坑我先踩为敬
7.1 光线变化导致的识别漂移
最大的坑就是光线。白天窗户边、晚上开灯、赛道上方的LED灯牌,光照强度变化范围很大,一个固定阈值不可能适应所有情况。排查思路是用直方图工具先看灰度的峰谷分布,如果在不同时间点峰谷位置漂移明显,就要启用自适应阈值模式,或者给车加一个光照传感器,根据环境亮度动态切换阈值。
我还尝试过用曝光锁定:调整摄像头的自动曝光,让它固定在一个较保守的EV值,减少抢光和过曝现象。你再打开直方图,会发现图像整体亮度稳定多了。
7.2 直道正常、弯道丢线
这是视觉循迹最经典的故障。直道上中线识别没问题,进弯时画面里赛道边缘突然消失。常见原因有三个。第一是车身侧倾时摄像头视角变化,赛道线跑到ROI之外去了;第二是弯道外圈边缘和背景颜色接近,二值化后边缘断裂;第三是速度太快,图像运动模糊导致边缘糊掉。
我当时的解决办法是:ROI区域外扩一些,把弯道处的边缘也纳入搜索;对二值化结果做形态学闭运算,把断裂的边缘连接起来;速度上限调低一点,同时把曝光时间调短,减少运动拖影。这些组合拳下来,弯道丢线率从一弯一次降到几十圈一次。
7.3 车左右摆动不止(转向振荡)
振荡的根源几乎都是PID参数没有整定好,具体一点说就是P太大、D太小。转向P过大时,偏差稍微有点波动,转向输出就猛地变化;D过小导致没有足够的阻尼来抵消这个冲击。我的整定方法是:先只保留P,从小到大逐步增加,找到“轻微摆头”的临界值,然后回调20%;再加入D,逐渐增加直到摆头明显衰减、但不至于高频抖动。每次修改参数后,实际跑一圈看效果,不要只看静态响应。
还有一个我栽过的跟头:PID输出没有限制。极端大偏差时,转向PWM可能直接满偏,电机猛打方向的同时还会让车偏航。最后我加了饱和限制输出值,并且给转向PWM加了斜率限制,让方向变化不会超过每10ms 5个PWM单位——这个限制值也需要实车调,不过基本能控制住车身的稳定。
7.4 树莓派掉帧导致控制卡顿
树莓派在处理图像时偶尔掉几帧,表现为图像处理线程卡了一两个周期,偏差值还是旧值,但控制端已经按新偏差算了一轮。控制端必须设计成“最近一帧有效”模式,每次控制任务从“最新偏差缓存”里取数,而不是从串口FIFO队列里取积压的旧数据。
串口发送的视觉端代码也要做节流,不要在每帧图像处理完都发,只有当偏差值变化超过设定阈值时才发送,否则高频发送的微小抖动会在控制端引起持续微调。实测下来这个“变化阈值发送”策略既降低了串口负载,也减少了控制端的无效运算。
7.5 电池电压掉电影响的排查记录
还有一个特别隐蔽的问题是电压跌落带来的行为不一致。跑第一圈时电池满电,车明显更快、转向更猛;跑到第三圈电压下降,车变得“温柔”了,PID参数不再匹配,原本稳定的路径也开始抖。电机驱动PWM相同占空比下,实际电压不同导致输出功率不同,速度环和方向环性能都受影响。
解决方案是给控制端引入“电压前馈补偿”:STM32通过ADC实时采集电池电压,算出一个衰减系数,当电压下降时自动增加PWM占空比,抵消电压下降带来的功率衰减。实现起来不复杂,但对长跑稳定性帮助极大。
8. 实测效果与扩展思考:视觉循迹只是第一步
整套系统跑下来,最终效果是:在标准室内赛道(宽度40到50厘米、包含直道和连续S弯)上,稳定运行时速可以达到1.2到1.8m/s。丢线率在正常光照条件下,一百圈不超过两三次;加了防护策略后,即便丢线也能按上一帧方向平缓滑行,不会冲出赛道。
这个项目最大的收获不是“车会循迹”本身,而是让我彻底打通了一套视觉感知到运动控制的完整链路。红外循迹时,我以为控制是重点,算法聊胜于无;做完机器视觉版本后,我才真正理解了一个视觉系统的每一环都缺一不可:硬件平台决定下限,图像处理决定上限,控制策略决定最终是否跑得稳,上位机决定你能不能快速迭代调优。
如果继续扩展,可以加一块GPU推理卡跑轻量级深度学习模型,做赛道元素识别,比如斑马线、十字路口、限速标志;也可以把视觉里程计和惯性导航融合,实现对小车位置的实时估计,不再依赖全局定位系统。这套从红外到视觉的升级路径,同样适用于仓库AGV、生产线循迹机器人和教学实验平台,核心思路完全一致。
最后再分享一个经验之谈:做这类项目,别一上来就幻想跑得多快、算法多炫。先把摄像头图像稳定地“看懂”,再谈控制优化。视觉端多花一天时间调稳定,后面所有环节都省心;视觉端凑合,后面每跑一圈都是煎熬。稳扎稳打,你的小车才能真正跑起来、跑得远。