ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

视觉循迹小车实战:从图像处理到PID控制的全流程解析

视觉循迹小车实战:从图像处理到PID控制的全流程解析 1. 方案选型与整体架构1.1 为什么最终选了视觉循迹这个方向先说结论如果你只是想让小车“能跑赛道”红外循迹和电磁循迹都能做到甚至成本更低、调试更快。那我在做这个项目时为什么还是选了机器视觉方案核心就三个字前瞻性。红外循迹的问题在于探头贴地安装只能感知车头正下方那一小段黑线等于边走边低头看速度一快就冲出去遇到十字路口、断线和急弯更是直接懵。电磁循迹的感应范围大一些但对赛道元素特别是电磁线铺设有严格要求而且在真正接近比赛级赛道时处理多弯连续切换的极限工况也容易跟不上。机器视觉则完全不同——摄像头装在高处视野是“往前看”的相当于司机抬头看路而不是低头找线。这条路前方是直道、急弯、还是十字提前一到两个车身位就能判断控制策略能提前布局。当然视觉方案的挑战也很明显数据处理量大、实时性要求高、受光照影响明显、调参维度多。所以这个项目本质上不是一个“搭电路”项目而是一个软硬结合、以软件调试为主的项目。更直白点说这是一个“80%时间在调算法、20%时间在调车”的项目。正因为如此它对学习机器视觉基础、图像处理、PID控制都有很强的带动作用非常适合作为课程设计、毕业设计或者自我进阶的入门项目。这个项目适合谁我觉得三类人最合适一是刚开始接触机器视觉、想在真实场景里跑通一套视觉算法的同学二是已经在做循迹小车但想从红外/电磁升级到视觉方案的竞赛党三是对嵌入式实时系统性能优化感兴趣的工程师视觉循迹练的是“在有限资源下把算法跑稳”这个能力非常值钱。1.2 整体系统框架与模块划分我采用的方案是典型的“前端采集、中端处理、后端执行”三层结构整体分四个模块模块作用我选型时考虑的核心指标图像采集模块采集赛道图像帧率、分辨率、接口方式、视角大小图像处理模块提取赛道信息、计算偏差CPU/NPU算力、内存大小、算法库支持运动控制模块根据偏差输出PWM控制电机/舵机定时器资源、中断响应速度、PWM输出路数执行机构模块驱动轮子转向/前进电机响应、减速比、驱动芯片持续电流在图像采集端我用的是一颗普通的OV2640摄像头模组200万像素30fps通过DVP接口连接主控。为什么没有去追求更高像素因为循迹任务根本不缺“清晰度”缺的是“稳定的帧率”。我做实验时把分辨率从800×600降到320×240处理速度提高了一倍而赛道识别精度几乎没有下降。很多时候高分辨率反而会让光照不均和噪点更明显处理压力也更大。对循迹小车来说像素够用就行比起分辨率帧率和动态范围更重要。图像处理端我选了内置图像处理加速单元的低功耗主控在保证处理速度的同时尽量压低功耗和体积。因为视觉算法里二值化、膨胀、腐蚀等操作都是逐像素点计算量很大的操作如果用纯CPU跑帧率很难拉起来。带有加速单元的主控可以直接对图像做统一的算术运算和矩阵操作效率高出好几倍。运动控制端是独立的一颗控制器它专门负责读取图像处理端算出来的偏差值然后跑转向控制算法、生成PWM波形。把图像处理和控制分离是为了避免同一颗芯片既要跑算法、又要处理中断、又要生成PWM导致互相干扰。这样即使图像处理一帧耗时偶有波动也不会直接导致控制时序抖动。至于执行机构常见方案是“电机编码器”或者“舵机电机”。我做的是三轮结构前方一个舵机控制方向后方两个电机驱动前进这种结构在校园竞赛里最常见控制逻辑也相对直观。电源部分要注意电机和舵机这种大电流负载必须和逻辑供电分开否则一启动电机主控电压跌落摄像头就开始花屏——这个坑我后面细说。2. 硬件设计与装车要点2.1 摄像头选型与安装位置的决定性影响很多人做视觉循迹把精力全压在算法上结果车体一跑起来画面就糊、路面阴影一多就误判最后失败在硬件上。我的经验是摄像头选型与安装直接影响后续80%的算法难度。选好了图像处理是“锦上添花”选不好你就是请来调参神仙也救不回来。选型上优先考虑支持手动调焦的镜头。自动对焦在静态场景里好用但小车跑起来之后画面内容高频变化自动对焦会导致画面反复拉风箱帧率再高都没用。固定焦距镜头选定之后把焦点调到赛道平面装车后基本就不用动了。分辨率方面200万像素足够我甚至建议直接把输出分辨率降到VGA640×480以下优先保证帧率。视角和控制逻辑之间的关系容易被忽略但理解它很重要镜头视角越宽看到的赛道范围越大前瞻越远但赛道在图像中占的面积越小像素损失越严重视角越窄赛道成像越清晰但弯道一急赛道很容易跑出视野之外。我实测下来的经验值是90°到120°的水平视角比较合适。120°视角在过急弯时优势明显90°视角在直线区域测距更准。安装位置比选型更讲究。摄像头高度我装在离地30cm左右太高了俯视角度大、近处盲区变大太低了又容易拍到车头。俯仰角建议让画面中心大约落在前方60到80cm处这样在图像中“远看趋势、近看细节”。“远看趋势”就是提前知道前面是直道还是弯道“近看细节”就是车到近处时能精确找到赛道边线。很多人的车过弯发飘、切内弯就是因为画面看得不够远总是在快进弯了才反应过来。还有一个小细节摄像头要减震。小车跑赛道时电机振动、路面颠簸都会传导到摄像头画面一旦出现高频抖动二值化边缘就会产生大量毛刺干扰中线提取。我最初直接把摄像头锁死在金属支架上跑起来直线都抖得厉害后来在支架和车体之间加了一层硅胶减震垫画面稳定了很多。这个细节不要省调试时能给你省下大把时间。2.2 主控选型、电机驱动与电源设计的坑一提到“机器视觉”很多人的第一反应是拿一块高性能开发板直接上。但我要泼一盆凉水工业级视觉循迹小车核心从来不是“算力堆多高”而是“电控稳不稳”。算力不足最多是帧率低电源不稳直接是复位、花屏、舵机抽搐一套组合拳。主控方面我建议视觉处理和控制分开跑这在前文提过。如果你用的是带硬件图像加速的单片机一颗就能承担处理和控制但整体负载会偏高调参和调试时会比较难受。我更推荐“视觉主控 运动控制MCU”的双芯片架构视觉主控专注跑图像算法计算出偏差值后通过串口或IIC发送给运动控制MCUMCU再根据偏差跑转向控制生成PWM这样责任边界清晰也方便后续单独升级其中一端。电机驱动是很多人忽略的重灾区。选驱动芯片或模块时我建议注意持续电流和散热而不仅仅是峰值电流。小车正常直线行驶电流不大但起步、堵转、上坡时的瞬时电流可能是正常行驶的3到5倍。如果驱动模块实际持续输出能力不足轻则过热保护重则烧毁芯片。驱动模块和主控板之间的信号线尽量短且要避开电机线否则电机通断瞬间产生的反电动势会通过信号线干扰主控导致跑着跑着突然失控。电源设计是整个硬件部分最重要的环节务必把“功率地”和“逻辑地”分开走线最后在一点汇合。电池电压经过稳压后给主控和摄像头供电电机直接通过驱动模块从电池取电。我的第一次装机就吃过这个亏当时电机地和逻辑地简单共地一上电机摄像头画面出现条纹干扰二值化后的赛道边缘全是噪点我还一直怀疑是摄像头坏了排查了半天才发现是电源干扰。PCB排布方面如果你要打板我会提醒三点一是电机驱动部分走线要宽至少按1A/1mm的铜宽标准来两根主电源线尽量短而粗二是在电机驱动芯片的电源引脚附近加一个大容量的电解电容比如470uF/16V和一个小容量的陶瓷电容0.1uF滤掉高低频噪声三是摄像头排线座尽量靠近板边方便排线走向同时摄像头排线内部信号频率较高不要把它和电机PWM线绑在一起走否则信号串扰会让你怀疑人生。3. 图像处理从一帧画面到一条赛道中线3.1 预处理流程与开闭运算参数原理图像处理流程是整个项目的核心如果把整套流程拆开大致可以分成五步灰度化 → 二值化 → 滤波/形态学处理 → 透视变换和ROI裁剪 → 赛道中线提取。灰度化没什么好说的就是把彩色图加权转换成灰度图权重一般是R0.299、G0.587、B0.114。之所以不用彩色信息是因为赛道颜色受光照影响太大同样的绿色跑道晴天和阴天拍出来色相完全不同但灰度的“明暗关系”相对稳定。当然如果赛道颜色和背景在彩色空间里差异极大也可以保留色彩通道来做掩膜但那是另一种处理思路了通用性不如灰度。二值化的目的是把“赛道”和“背景”分开。这里我选用大津法Otsu自动计算阈值而不是固定阈值。原因很简单小车在跑的过程中车体会把光挡住一部分路面的明暗在不断变化固定阈值很容易在拐弯时因为光照变化而失效。大津法的原理是遍历所有可能的灰度阈值找到一个值使得“按这个阈值分割后前景和背景两类的类内方差最小、类间方差最大”也就是让两堆像素离得尽可能远。这个算法在赛道和背景灰度差异明显时效果非常好而且计算量不大在SOPHON BV 帧率宽裕的情况下完全跑得动。二值化之后图像上会出现很多离散噪点和毛刺。之前做红外循迹时没有这个概念转到视觉方案后才发现二值化后的图像实在太脏了。这时候就轮到形态学操作登场也就是开运算和闭运算。很多人只记住了开运算“先腐蚀后膨胀”、闭运算“先膨胀后腐蚀”但不理解为什么要在循迹场景里用这两个操作更不理解核大小怎么选。我先讲原理再讲实操。腐蚀操作会让白色区域边界往里收缩膨胀操作会让白色区域向外扩展。开运算是先用腐蚀去掉小块的白色噪点再用膨胀恢复原来白色物体的大小。闭运算是先膨胀填补白色区域内部的黑色小洞再腐蚀恢复原大小。那循迹场景里到底该用开运算还是闭运算我实际的经验是先用开运算去掉噪点再用闭运算填补赛道内部孔洞。比如赛道是白色的背景是黑色的二值化后赛道内部因为灰度和光照不均可能出现细小的黑色断点这时闭运算能把这些断裂连起来。但如果你用的是黑色赛道、白色背景那就要反过来理解腐蚀和膨胀的对象了。总之开运算去外噪声、闭运算填内空洞这两个操作组合起来效果比任何单一滤波都好。核的大小直接决定处理效果。核越大去除噪点越彻底但同时也把赛道的细节磨没了——小弯道、细边线、路口分叉会被抹平。我调试下来最常用的核是3×3到5×5。前面提到主板自带图像处理加速单元形态学操作可以用Opencv 中的 getStructuringElement 快速实现核尺寸改起来也非常方便我一般先设到7×7看效果往回收直到赛道边缘细节刚好保留、噪点刚好去除为止。迭代次数也是一个关键参数默认做1次开1次闭如果噪点明显就做2次。千万不要无脑多次迭代我见过有人迭代5次开运算结果细赛道被抹成了虚线。3.2 赛道中线提取算法与代码实现形态学处理做完之后图像已经很干净了下面要做的是从这幅干净的图里提取出“小车应该往哪走”的关键信息。最经典、最适合初学者的算法是“按行扫描找边界法”。思路很简单在画面的下半部分定义一个感兴趣区域到ROIROI的下边界贴近车头上边界设在画面中偏上的位置。然后从ROI的底部逐行向上扫描每一行从左向右遍历像素找到第一个白色像素点作为左边界再从右向左遍历找到第一个白色像素点作为右边界左右边界的平均值就是这一行赛道中心点的横坐标。这个算法看起来简单但坑不少。第一个坑是ROI区域里可能只有一条边界比如车已经偏向赛道一侧另一侧出了画面这时如果你取左右边界的平均值会直接算出一个偏离实际中心的数值。我的处理办法是如果某一行只找到单边边界就用上一行中心点作为参考做外推如果连续好几行都找不到边界就判定为丢线保持上一次有效的偏差值并让小车减速直到重新找到赛道。第二个坑是十字路口在十字路口左右扫描会同时遇到横向赛道边缘导致边界判断混乱。我的做法是限制单行扫描的最大有效宽度——如果左右边界的间距突然大于赛道在图像中的最大像素宽度就判定为干扰把这一行的扫描结果丢弃。下面是我在视觉处理主控上跑的核心代码按项目实际使用的环境做了简化但逻辑是完整可用的import cv2 import numpy as np def process_frame(frame): # 1. 裁剪ROI只保留画面下半部分排除天空、场外杂物 height, width frame.shape[:2] roi_top int(height * 0.45) # 上边界在画面45%高度处 roi frame[roi_top:height, 0:width] # 2. 灰度化 高斯模糊降噪 gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (5, 5), 0) # 3. 大津法二值化 _, binary cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 4. 形态学先开运算去噪点再闭运算填充孔洞 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) opened cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations1) closed cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel, iterations1) # 5. 按行扫描提取每行赛道中心点 center_points [] scan_start_row closed.shape[0] - 1 # 从ROI底部开始扫 scan_end_row 0 # 扫到ROI顶部 step 3 # 每隔3行采样一次减少计算量 for row in range(scan_start_row, scan_end_row, -step): row_data closed[row, :] # 找左右边界 left_edge -1 right_edge -1 for col in range(width): if row_data[col] 0: left_edge col break for col in range(width - 1, -1, -1): if row_data[col] 0: right_edge col break if left_edge 0 and right_edge 0: # 宽度过滤防止十字路口横向干扰 if right_edge - left_edge width * 0.9: center (left_edge right_edge) // 2 center_points.append((row, center)) # 6. 用N行有效中心点求加权平均作为最终输出偏差 if len(center_points) 0: return None # 丢线 # 越靠近图像底部的位置越可信给更高的权重 total_weight 0 weighted_sum 0 frame_center width / 2 for row_idx, center in center_points: weight 1.0 (row_idx / closed.shape[0]) * 2.0 weighted_sum (center - frame_center) * weight total_weight weight deviation weighted_sum / total_weight # 正值偏右负值偏左 return deviation这段代码里有几个细节我想强调一下。第一ROI上边界设到画面45%高度是因为小车前方稍远处的赛道在画面中占比很小处理价值有限还容易把背景杂物圈进来干扰计算。裁掉上半部分之后数据量少了接近一半帧率自然就上来了。第二扫描步长设为3每3行采样一行对赛道这种平滑曲线的识别精度影响很小但计算量直接降为原来的三分之一。第三加权平均那里越靠近车头即ROI底部的扫描行越可信因为透视关系下近处的赛道在图像里更宽、更清晰所以给底部扫描线更高的权重这样算出来的偏差更稳。多说一句这个流程是整个循迹算法的核心调好它会让你后续省很多事。我见过不少人一上来就直接用霍夫变换找线、用Canny边缘检测思路很高级但在实际赛道上很容易被复杂背景干扰调参调到头秃。对于这个项目来说“简单、稳定、可解释”比“高深、炫技”重要得多。4. 运动控制让小车走得稳4.1 偏差量与PD控制原理图像处理得出偏差值之后剩下的问题就变成了“偏差怎么映射成转向角度和车速”。这一步如果处理不好小车会在赛道上左右画龙或者过弯时直接冲出去。控制部分我选用的是位置式PD控制不是PID。为什么不用积分项因为循迹系统本身没有稳态误差——只要偏差为零转向角就是零没有需要靠积分消除的累积误差。而积分项的副作用却很明显赛道弯道多偏差反复变化积分值容易积累过头导致转向滞后甚至超调。PD控制的数学形式很简单output Kp * error Kd * (error - prev_error) / dt其中error是上一节算出的偏差prev_error是上一次控制周期里的偏差dt是控制周期。直观理解起来P项的作用是“现在错多少就纠正多少”Kp大反应快但太大会振荡D项的作用是“错误变化得越快就要越早刹车”Kd大能让转向更柔顺但太大会让响应变迟钝。我在实际调试时的做法是先让Kd等于0只调Kp把车放到直道上跑慢慢加大Kp直到车能稳定走直线。然后进入弯道观察过弯表现逐渐加大Kd直到车在连续弯道中的姿态变稳。反复迭代多轮直到跑圈时小车既不抖也不飘。值得强调的一点是调Kp和Kd一定要先固定控制周期比如图像处理帧率稳定在25fps那么控制周期就是40ms不要一边调参一边改帧率否则所有参数都失去参考意义。转向执行端用的是舵机。舵机的PWM频率一般是50Hz对应周期20ms对应的脉宽范围一般是0.5ms到2.5ms中位是1.5ms。PD输出的转向量需要映射到这个脉宽范围区间我一般限制最大转向角度在正负30度以内既能保证过弯能力又不会因为转向过度导致车身侧翻。4.2 速度自适应与直道弯道切换策略很多人做完PD控制之后就收工了但实际上真正让小车跑得又快又稳的是“速度自适应”策略。在小车高速行驶时如果入弯速度不降光靠转向控制是救不回来的——就像你跑步速度太快时突然要转弯身体一定会失控。我的做法是根据“赛道曲率”来实时调整目标速度。曲率可以通过中线偏差的梯度来估算即连续若干帧偏差变化率的绝对值偏差变化得越快说明弯道越急。当偏差变化率低直道或缓弯时把目标速度提高偏差变化率高急弯时提前把目标速度降下来。这个策略听起来复杂实现起来其实很直观# 伪代码单位刻度需要根据你的车实测调整 max_speed 100 min_speed 40 follow_distance 20 curvature abs(deviation - prev_deviation) / dt speed_target max_speed - int(curvature * follow_distance) speed_target max(min_speed, min(max_speed, speed_target))这个策略的核心逻辑是“提前量”不是等车子已经到弯道才减速而是从图像中看到弯道趋势就提前减速。摄像头安装得越高能够提前发现弯道的距离就越远速度就可以稍微放开一些。这也是摄像头的“前瞻”价值在控制层面的体现。另外还有一个需要注意的点是“起步和停车”策略。小车刚上电时舵机需要时间自检归零如果立即给油门很可能以一个歪斜的姿态冲出去。我的做法是上电后延迟1秒先让舵机回到中位再启动缓慢加速让车速从零平滑提升到目标速度。这样做还能避免电机突然全速启动瞬间拉低系统电压。5. 调试中的典型问题与解决实录5.1 图像层面的常见故障调试视觉循迹小车绝大多数时间不是在写代码而是在和图像质量问题作斗争。我把自己踩过的坑和排查方法整理成一个速查表你可以直接对照排查现象可能原因排查方法与解决办法二值化后赛道断裂、出现大块空洞光照不均、路面纹理干扰先加大闭运算核尺寸或迭代次数仍无效则改用局部自适应阈值二值化后背景噪点密集自动阈值受大面积阴影影响优先用开运算去除仍无效则手动微调阈值不必迷信Otsu画面边缘发暗、中间亮广角镜头边缘通光量不足开启摄像头自动曝光后手动锁定曝光值不要用自动曝光画面模糊、赛道边缘毛刺多车体振动导致高频抖动检查摄像头固定支架增加减震垫检查帧率是否过低图像颜色整体偏色白平衡漂移固定白平衡参数不要用自动白平衡过弯时赛道大面积出现过曝强光直射路面调整摄像头角度避开直射光在镜头上加偏振片滤掉反光这中间最让人头疼的就是路面反光。刚开始我用的是固定阈值二值化晴天下午在操场测试白色赛道在阳光下直接变成一片白光大津法也救不回来。试了很多方案后发现最有效的是改变摄像头安装角度让镜头稍微向下倾斜避开阳光的反射角。其次是在镜头前加一片线偏振片能滤掉大部分路面反射光。如果你只是室内环境跑这些问题会轻很多但提前了解还是好的。5.2 控制层面的振荡与失稳控制层面的问题表现上就是小车跑起来抖、画龙、冲弯。其中最常见的两个问题就是转向振荡和过弯失控。转向振荡表现为小车在直道上左右画龙且幅度越来越大。这种一般是Kp过大导致的过冲。处理步骤先把Kp降低30%观察振荡幅度是否收敛如果还抖继续降如果是高频小幅抖动可能是Kd过小或者控制周期不稳定可以适当增大Kd、锁定帧率。还有一种隐蔽原因是舵机响应滞后——如果PD输出频繁在高频抖动舵机跟不上指令也会表现为车体“抖”。这时需要在PD输出端加一个低通滤波让转向指令变化变平滑我用的是一阶低通滤波filtered alpha * new_value (1 - alpha) * filteredalpha取0.3左右。过弯失控表现为入弯速度太快直接冲出赛道或者转向不足。这种一般是速度自适应的减速量不够或者减速时机太晚。我的处理办法是调大曲率到减速量的映射系数让小车在刚看到弯道趋势时就明显减速同时降低最大速度保证“宁可慢一点也不要冲出去”。5.3 硬件层面的稳定性问题硬件层面的问题通常比较隐蔽也很难从表面看出来。我在调试中遇到的频率最高的问题是“电压跌落带来的系统复位”电机瞬间大电流把电池电压拉低主控直接复位重启然后你就看到小车突然停下来或者乱跑。排查方法很简单用万用表和示波器实时监测主控供电电压在电机启动瞬间看电压跌落幅度。如果跌落超过0.3V就需要检查电源布局。我最后是通过两个措施解决了问题一是在电池输出端增加一个大容量电解电容我用的1000uF/16V起到缓冲作用二是把逻辑电源和电机电源完全分开走线只在电池总输入端做单点汇合。另一个硬件问题是“PWM信号线串扰”舵机PWM信号线和电机电源线在同一个线束里绑着走电机一启动舵机就抽搐。解决方式很简单——PWM信号线改成带屏蔽层的双绞线并且远离电机电源线。这个细节在布线和装车时多花10分钟调试时能省下一整天。5.4 一套稳定的参数整定流程调试过程中我总结了一套比较稳定的参数整定流程按这个顺序走能大大减少盲目调参的时间先把图像处理端输出调试好。用电脑或屏幕实时显示二值化后的图像确认赛道中线提取无误。固定一个较低的恒定速度比如40%最大速度在直道上调Kp直到小车走直线不画龙。在缓弯上调Kd直到过弯流畅、无明显侧倾和振荡。逐步提高最大速度观察急弯表现同时调速度自适应的减速力度参数。最后跑完整循环记录每圈用时和失败位置针对性微调参数。调参最重要的原则一次只改一个参数。改完记录效果再动下一个。改完Kp再去调速度往往会掩盖真实问题让整个系统变成一团乱麻。写在最后这套视觉循迹小车做完之后我个人最大的体会是真正难的从来不是某一个环节而是所有环节咬合在一起时的稳定性。图像、控制、电源、机械任何一个环节出问题都会通过“小车跑不稳”这个表象暴露出来而你要做的是穿透表象定位到到底是图像脏了、控制参数不合理还是电源在捣乱。最后再分享一个小技巧每次调试之前先把摄像头固定好、电池充满、光线环境确认一遍把变量控制住再开始调参。否则你永远不知道刚才那圈跑得好是因为参数对了还是因为刚好云把太阳挡住了。还有如果条件允许把每一次调参后的记录写下来——时间、光线、参数、现象——坚持记录十几次之后你对自己小车的“脾气”会了如指掌。这个过程虽然琐碎但恰恰是做一个真实项目最宝贵的收获。
返回列表