尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于机器视觉的循迹小车设计与实现

基于机器视觉的循迹小车设计与实现 1. 循迹方案的代际切换从红外传感器到机器视觉做了几年智能车和机器人竞赛相关的项目我带过不少学生团队也帮企业调试过AGV产线。说实话这两年再回头看循迹小车这个经典项目感受最深的一点是方案分水岭已经很明显了。传统的红外对管循迹、电磁循迹虽然还在一些入门教学里存在但一旦赛道复杂度上来——比如有交叉路、有大弧线弯、有光影变化的地面、甚至有坡道——光电传感器的部署密度和抗干扰逻辑就会变得非常痛苦。而基于机器视觉的循迹方案本质上就是把过去“靠多个传感器点判断有没有线”的思路替换成“靠摄像头看一整片地面、理解线的走向”信息量完全不在一个量级。这个项目标题是“基于机器视觉的循迹小车设计”听起来好像很常见但真正把它做好、做稳涉及的知识点是相当杂的图像采集与预处理、二值化阈值处理、开闭运算的参数调优、透视变换、中线/边界提取、PID闭环控制再加上车体机械结构的匹配。任何一个环节松了车跑起来就拉胯。这篇博文我打算完整记录一套可复现的设计方案。硬件上不用太高配最亲民的组合就是树莓派或任意Linux小板子加一个USB摄像头加STM32或Arduino做电机底层控制性能足够支撑工程落地。软件走OpenCV路线算法流程从图像采集一直到偏差计算和控制量输出我会把每一步的“为什么这么做”和踩过的坑一并写出来。适合正在准备智能车竞赛、做课程设计或者单纯想把手里的底盘跑起来的人参考。需要先建立的一个认知是视觉循迹不是让摄像头“看见”赛道再机械地转向而是通过图像处理把二维像素信息换算成“小车相对赛道的横向偏差”和“航向偏差”再通过控制器实时修正。理解了这个闭环你就不会被那些看起来很玄的视觉算法吓到。2. 整体设计思路与方案选型2.1 摄像头凭什么替代一排红外传感器传统循迹小车通常在车头底部装一排红外对管靠反射率差异判断黑线位置。它的本质是对几个离散点进行采样赛道信息被压缩成一组开关量。你说它简单确实简单但它的物理上限也很明显采样点数有限弯道大、速度快时极易丢线环境光变化、地面反光、线变脏都会导致误判。遇到十字路口这排传感器的“视角”更是直接失效。机器视觉循迹则完全换了一个思路。摄像头把一个连续画面映射为矩阵像素每一个像素都携带灰度、颜色信息你相当于一次性获得了前方几十厘米甚至更远范围内、整个视野中的赛道分布情况。它不再是一组开关而是一张图——一张可以直接告诉你“赛道在哪里拐弯、以多大角度拐弯、还剩多少直道”的图。这也是我为这个项目选定“摄像头 图像处理 PID”这套骨架的根本原因。它不依赖特定颜色的胶带也不依赖传感器距地面的精确高度只要把图像处理管线调好适应能力就有质的提升。2.2 两级架构上位机负责看下位机负责跑选型上我见过不少初学者试图用STM32直接采集摄像头数据并做图像处理。这个方向不能说完全不行但对大多数人的开发效率来说是不划算的。视觉处理涉及大量浮点运算、矩阵操作在MCU上跑OpenCV或者裸写图像算法很痛苦调参效率极低帧率也上不去。更合理、也更容易调试的方案是两级架构上位机树莓派 4B / 香橙派 / 电脑负责图像采集、处理、计算偏差和期望转角/速度。下位机STM32F103 或 Arduino负责读取控制指令驱动电机完成转向和加减速。上位机和下位机之间用串口通信波特率设置到 115200 基本够用。上位机跑视觉算法帧率稳定在 20~30 FPS下位机跑电机控制控制周期可以到 50~100 Hz。这样既避免了MCU算力瓶颈又保留了实时控制能力。如果手头预算非常有限更轻量一点的方案是直接用 OpenMV 或 K210 这类带视觉处理能力的开发板。这类板子内置了视觉加速器或专门的图像处理库开发门槛更低但灵活性不如树莓派 OpenCV尤其是后续想加复杂图像处理算法时受限较多。2.3 为什么选OpenCV而不是自己造轮子图像处理环节我用的是 OpenCV。这不是因为它“热门”而是它把灰度化、滤波、阈值分割、形态学运算、透视变换这些高频操作都封装成了高度优化的函数底层有SIMD和硬件加速Python调用方便C性能更优。对小白最容易产生误导的一点是图像处理的核心不是你调用哪个函数而是这些函数背后的参数为什么这么设。比如开运算为什么用7×7的核二值化为什么选OTSU而不用固定阈值ROI为什么要截到某个高度——这些参数单独看都很简单但组合在一起恰恰决定了整套系统在真实赛道上的稳定程度。所以接下来我会先把图像处理管线完整拆开讲再给出一套能直接在OpenCV里跑的代码链路。这中间包含很多“调参教训”是我实际跑赛道时一点一点磨出来的。3. 图像处理管线把“看到”变成“看懂”3.1 第一次面对原始图像时先别急着二值化摄像头拿到手的原始图像是BGR三通道彩色图。你当然可以直接对它做颜色阈值比如只保留白色赛道里的黑线但这么做的第一直觉往往会在实战中碰壁不同时刻的环境光色温不一样同样一条黑胶带上午拍出来是偏暖的灰黑下午阴天拍出来是偏冷的蓝黑。依赖固定色域范围做检测鲁棒性很差。所以我在实际项目中始终采用“灰度化 → 滤波 → 二值化”的链路。灰度化的作用是把三通道信息压缩成单通道亮度信息大幅降低计算量也让后续处理专注于“亮暗对比”而不是“颜色”。这一步就是cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)没什么玄学但它是整条管线的基础。灰度化之后画面里会混有噪点。摄像头传感器在低照度下尤其明显画面上会出现孤立的亮/暗像素点它们是二值化之后产生“椒盐噪声”的主要来源。这时候就需要做滤波。我优先推荐高斯滤波核大小选5×5即可。高斯滤波和均值滤波的区别在于它按高斯分布给核内每个像素分配权重中心像素权重最高越往边缘权重越低能更好地保留边缘轮廓。均值滤波会模糊边缘在赛道边界提取阶段对精度有负面影响。3.2 二值化阈值OTSU到底好在哪滤波之后下一步是把灰度图转成黑白二值图——让赛道区域和背景尽可能清晰地分离。这里最常踩的坑就是直接用固定阈值比如cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)。为什么不要用固定阈值因为127这个数字是你拍脑袋定的可实际赛道亮度是动态变化的。光照强时赛道背景可能整体偏亮黑线灰度值可能在80左右光照弱时同样的黑线灰度值可能已经飘到150以上。固定阈值一旦定死系统就失去了对光照变化的适应能力。正确的做法是用OTSU大津法自动计算阈值cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)。OTSU的核心逻辑是遍历所有可能的阈值找到一个值使得用这个值分割出来的前景和背景两类像素的类内方差最小、类间方差最大。直白说就是让“黑”的更黑、“白”的更白分割得最干净的那个阈值就是它选出来的。OTSU不是什么高深算法但工程效果非常稳定。在室内灯光环境、白底黑线的常规赛道上OTSU基本不需要额外调节。如果场景光照变化极端还可以再引入自适应阈值cv2.adaptiveThreshold用局部邻域均值动态计算阈值不过它的缺点是计算量偏大而且对纹理细节敏感容易产生碎块需要配合后续形态学操作清理实际项目中我只有在光照极不均匀的场地才会考虑它。3.3 开闭运算为什么是这对组合核大小怎么定二值化之后图像不是完美的黑线内部可能有白色空洞线反光造成的断裂线边缘有毛刺背景里可能有独立的小块噪声区域。这时候就要用形态学操作来“修图”。形态学操作里最常用的一对组合就是开运算和闭运算。开运算 先腐蚀后膨胀。腐蚀是拿一个结构元素比如3×3或7×7的方形核去扫图像只有结构元素范围内全是白色中心像素才保留为白色膨胀则是只要结构元素覆盖范围内有一个白色像素中心像素就置为白色。所以开运算的效果是消除图像中比核更小的白色噪点同时保持大块白色区域的整体大小不变。闭运算 先膨胀后腐蚀效果正好相反填充比核更小的黑色空洞把断裂的线条连接起来同时不改变大区域尺寸。循迹场景里开运算负责清除背景里的孤立噪点闭运算负责修复黑线内部的反光空洞和细小断裂。两者配合图像质量明显提升。核大小怎么定这是很多人忽略的问题。核太小去噪能力不足核太大线的细节被抹平弯道处圆形边缘会被修成“钝角”影响后续中线提取精度。我的经验是当图像的ROI宽度约640像素、黑线在画面中约占30~50像素宽时7×7的核是安全和通用的选择。一个快速的估算方法是核的大小应该略大于噪声块的尺寸但不超过目标线条宽度的1/3。如果线宽90像素、噪点普遍在5×5左右那核就选15×15左右比较合适。核大小与图像分辨率强相关缩放到320×240处理时就要对应缩小到3×3或5×5。核心代码如下kernel cv2.getStructuringElement(cv2.MORPH_RECT, (7, 7)) # 先开运算去噪再闭运算补洞 img_open cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations1) img_close cv2.morphologyEx(img_open, cv2.MORPH_CLOSE, kernel, iterations1)这里有两个细节值得注意。第一个是iterations参数默认1就够了不要盲目加大迭代次数否则线条边缘会被反复腐蚀膨胀导致变形严重。第二个是结构元素的形状我用的是矩形核。理论上椭圆核在抹平边缘时更圆滑但实际循迹场景中赛道边缘本身是直线或大曲率弧线矩形核计算速度更快效果上没有明显劣势。3.4 透视变换把梯形视野拉成俯视图摄像头安装在小车前上方以一定俯角拍摄地面这时画面里的赛道是“近大远小”的梯形。直接把这个梯形画面拿去提取中线会带来一个严重问题近处的赛道在画面里宽度大、远处的赛道宽度小计算出来的中线在远处会向中间偏移这种畸变会让小车在高速入弯时感知到的赛道几何与实际不符。解决办法是透视变换。透视变换的本质是建立原始图像上四个点和目标图像上四个点之间的单应性映射把梯形区域“拉”成一个矩形相当于用软件做了一次鸟瞰视角的矫正。操作上就是选四个控制点画面中赛道梯形区域的左下、右下、左上、右上然后映射到一个固定宽高的矩形图上。我用cv2.getPerspectiveTransform和cv2.warpPerspective完成。透视变换的效果非常直观变换之后远处赛道的宽度和近处接近一致弯道的曲率也不再因为透视关系而变形。这一步对后续的PID控制至关重要。如果省掉这一步直接提取偏差直道上可能还好一旦进入大弯系统的反馈会出现非线性失真控制参数调了也白调。需要注意两点。第一四个源点必须选在同一个平面——也就是地面平面上。如果地面有起伏、坡道这个变换就会失效。第二源点坐标一旦确定整个运行过程中不能变所以摄像头必须固定死不能震动偏移。我见过不少同学调好了透视变换结果车跑起来摄像头松动画面抖动所有标定点全部失效排查半天才发现是螺丝没拧紧。4. 赛道识别与偏差计算从像素到控制量4.1 提取赛道中线质心法还是边缘法图像修好、透视变换完成之后我们得到了一张干净的俯视二值图。接下来要回答一个核心问题这条线现在在车子的哪个位置偏差多少我见过两种主流做法。第一种是“边缘法”先提取赛道左边缘和右边缘然后取中点作为赛道中线。边缘法的精度高但前提是能把两条边缘稳定提取出来。在光照复杂、线边缘有毛刺的情况下边缘提取很容易产生抖动对后续控制影响很大。第二种是“质心法”直接把二值图上白色像素或者黑色像素取决于你用的是白底黑线还是黑底白线在ROI内的坐标求平均作为赛道中心位置。质心法对边缘毛刺不敏感整体抗噪性好计算量也小非常适合入门和稳定优先的场景。我实际采用的是质心法但不是在整个画面范围求质心而是把画面纵向切成若干条横带对每一条横带单独求质心再把质心序列交给控制算法。为什么切条带因为单纯求全图质心会丢失“赛道在哪个位置拐弯”的信息。切条之后你可以看到从上到下每一段的质心位置——如果上段质心偏左、下段质心居中说明前方是一个左弯而且你已经能看到它的趋势。这个信息对后续PID的前馈控制非常有价值。横带的划分没有硬性标准通常我喜欢切5~8条每条大约20~30像素高覆盖ROI的有效区域。太少则丢失趋势信息太多则会引入单行噪声效果反而差。4.2 偏差值是怎么算出来的质心算出来之后要转换成偏差值。最朴素的做法是拿质心横坐标cx减去图像中心横坐标mid得到一个像素级的横向偏差。但这个偏差是“像素域”的值不同分辨率下数值变化很大不利于PID参数迁移。我习惯做归一化处理把偏差除以图像宽度的一半得到范围大约在 [-1, 1] 之间的无量纲偏差。这样不管你用640分辨率还是320分辨率PID参数可以基本复用。举例说明图像宽度640中心点横坐标320当前质心横坐标264那么偏差 (264 − 320) / 320 −0.175。负号表示赛道中心在图像中心左侧也就是车头偏右需要向左修正。除此之外我还会计算“质心斜率的估计”——也就是最近几条横带质心连线的方向。这个方向可以直接作为航向偏差的近似。它有什么用当车进入S弯时横向偏差可能还很接近0但质心连线已经倾斜了说明即将进入弯道。把航向偏差和横向偏差同时送入PID控制器的反应就会提前转弯更平滑而不是等横向偏差大了才猛打方向。4.3 PID参数的整定思路视觉循迹的底层控制器我还是用PID具体场景用PD较多——因为纯积分项在快速变化的赛道中容易造成超调和振荡。控制对象是转向舵机或差速转向中两侧电机的速度差输出量是一个舵机角度或差速量。标准的增量式PD长这样def pd_control(target, current, prev_error, kp, kd, dt): error target - current derivative (error - prev_error) / dt output kp * error kd * derivative return output, error调参的经验顺序是先只保留P项从很小的Kp开始让车在直道上能缓慢修正回中线然后加大Kp直到车在直道上开始出现轻微振荡接着加Kd抑制振荡让车既跟手又稳定。我见过不少新手上来就给很大的Kp结果小车在直道上左右来回晃越调越急。慢下来一项一项来是最快的路径。有一个容易被忽视的点PID的输出要映射到下位机驱动指令。如果你用的是舵机输出要映射到舵机角度范围如果你用的是差速底盘输出要映射到左右电机的PWM差。映射比例不合理控制周期再快也白搭。4.4 丢线和满线这两种极端情况实物跑起来之后你最常遇到的两种情况第一是“丢线”。当赛道急弯超出摄像头视野或者黑线因光线问题断裂严重时图像中可能找不到任何有效的赛道像素。这时候如果继续用默认的质心计算结果车会瞬间失控。我的处理策略很简单但很有效当检测到有效像素过少比如低于阈值时不更新质心数据而是沿用上一帧的偏差值同时输出一个记忆性的强制转向指令朝上一次有效偏差的方向持续打方向直到重新找到赛道。这是一种非常“暴力”但实战中很管用的兜底逻辑。第二是“满线”。在十字路口或起跑线处整幅图像的横带全被白色像素覆盖质心会被拉回中心看起来像是“没有偏差”。如果你用的是纯质心法这个时候车就会直行但这是对的——十字路口嘛直行本来就没问题。但因为全白区域的存在质心法很容易误判为“已经对准中线”进入路口后反而容易跑偏。我的处理方式是当某一横带内白色像素占比超过90%时将该横带视为无效带不参与质心计算或者直接对这种满带宽做特殊标记让控制逻辑知道“当前是路口场景采用直行策略”。这两类极端情况在代码里就是几个条件判断但在赛道上的价值极高——它决定了你的车是“跑完一圈”还是“每个弯都像在赌命”。5. 小车底盘与底层控制轮子跟不上眼睛照样白搭5.1 底盘选型三轮还是四轮差速还是舵机视觉处理做到位了偏差计算也有了但如果底盘响应跟不上整个系统的上限还是被卡死。我在这个项目里最常用的是四轮差速底盘——两个后轮驱动、两个前轮随动或者四轮驱动差速转向。这种底盘的优点是结构简单、控制直接。左转就是左轮减速/反转、右轮加速转向半径可控性强。三轮底盘前轮舵机转向后轮驱动的循迹手感更像真实汽车转向更平滑但前轮舵机的响应延迟和转向半径限制对视觉系统的实时性要求更高一些。不能说哪个更好只能说视你的目标而定。如果追求速度建议四轮差速因为差速转向在小半径弯道里可以做出更激进的入弯姿态如果追求平稳和真实感舵机版三轮底盘会让你调PID更省心。5.2 下位机控制周期与串口协议上位机算出偏差和控制输出后通过串口把指令发给下位机。这里有一个工程细节上位机帧率视觉处理频率和下位机控制频率最好解耦。我习惯的做法是让上位机以尽量高的帧率跑视觉但控制指令按固定周期下发比如每50ms一帧。下位机则独立运行在一个10ms的定时器中断里不断读取最新的串口指令并更新PWM输出。这样做的好处是即使某一帧图像处理超时下位机的PWM输出也不会中断小车不会因为一帧的延迟而出现“一顿一顿”的现象。串口协议我推荐用固定长度的ASCII文本帧比如$PWM,50,80#表示左轮PWM为50、右轮为80。解析简单、调试方便——你可以在串口助手里直接看到上位机发出来的指令格式排查问题时比二进制协议好使太多了。等性能需要再升级为校验更严格的二进制帧。5.3 转向映射从控制量到PWM把PID输出的控制量映射到PWM时务必要实测车的物理特性。比如你的底盘在PWM值为60时刚好能够克服静摩擦起步在PWM值为90时速度比较理想。那你的转向差值就最好设定在起步值和理想值之间不要轻易给出低于起步值的PWM。否则视觉系统算出了一个小幅修正量但电机根本没动到下一个控制周期偏差又变大了车就会出现“一步一卡”的抖动感。我常用的映射方式是设置基础速度base_speed左轮PWM base_speed - delta右轮PWM base_speed delta。delta就是PD输出映射到PWM域的结果并且对它做限幅比如限制在±20以内。这样即使视觉偏差瞬间很大小车也不会原地打转失去前进动力。5.4 电源与稳定性视觉系统最怕电压跌落这块很多人不重视但它坑人坑得最多。摄像头和上位机的供电一定要单独稳定不能和电机共用一个电源。电机启动瞬间电流很大会把母线电压拉低导致树莓派或摄像头瞬间掉电重启。重启一次系统就废了。所以我的电源方案是双电源一路给电机和驱动板另一路给上位机和摄像头。两路共地即可。如果你只打算用一个电池那也必须加一个DC-DC稳压模块把电机电源隔离出来之后再给上位机供电。千万别图省事直接把树莓派的5V接在电机电源上否则等着你的就是各种匪夷所思的随机重启。6. 常见问题与排查技巧实录6.1 反光导致检测线断成“虚线”这是视觉循迹最经典的问题。赛道胶带如果是哑光的还好如果是光滑的黑色电工胶带在灯光直射下会出现一条高光反射带二值化后黑线中间被“掏空”看起来就像虚线。我排查这个问题的思路是分三步走第一步检查二值化后的图像确认断裂区域是反光还是阴影。第二步优先用闭运算去修复核大小可以从7×7试到15×15直到断裂被填上。第三步如果闭运算填不上说明反光区域的灰度和背景已经非常接近二值化本身已经无法区分这时候需要回到源头调整摄像头的安装角度避免光源直接反射进镜头或者给镜头加偏振片。这里补充一个实用技巧在实际调试时把二值化结果实时显示出来看比盯着原图看有效得多。你看到的就是算法看到的所有问题一目了然。6.2 弯道过急丢掉赛道画面一片黑急弯丢线的原因通常是摄像头视野范围不够。解决方法有几个方向第一抬高摄像头的安装位置增大视野范围。但不要抬太高否则透视变换后的图像分辨率下降远处赛道像素太密提取精度反而变差。第二加大透视变换中源点选取的范围把更远的赛道纳入变换区域。第三也是我觉得最关键的——做好丢线时的“记忆转向”逻辑让车在看不到线的瞬间也能按上一帧的轨迹趋势继续转弯。在调试丢线逻辑时我把丢线判断阈值设在有效白色像素面积小于总ROI面积的5%时触发。这个阈值不能太小否则已经彻底看不到线了才触发“丢线保护”往往来不及救回来。6.3 PID调参越调越晃最后总结出一套顺序每次带新人调PID我都会让他们按这个顺序来先固定一个很慢的基础速度把P值调到直道上能稳定跟线再逐步加D值消除弯道超调最后才提速度。如果先提速再调PID偏差变化速度太快你会分不清是哪个参数出了问题——到底是P不够跟手还是D过大导致转向被抑制很难判断。还有一个小技巧在代码里把误差值和控制输出通过串口或者日志实时打出来。跑一圈赛道回放日志你能精确看到弯道处误差从多大开始、控制量在那个时间点给了多少。这比盯着小车看效率高太多了。很多“神秘”的抖动问题一看数据和曲线就清楚了。6.4 常见问题速查表现象可能原因处理方向二值化后黑线断裂成虚线反光、污渍、线本身磨损闭运算修复调整摄像头角度加偏振片画面出现大量孤立白点环境噪点、地面纹理开运算去噪加大滤波核弯道处转向不足冲出去PID响应不够或视野太小加大Kp增大透视变换视野检查丢线保护逻辑直道上左右振荡Kp偏大或Kd不足减小Kp增加Kd上位机随机重启电源电压跌落双电源供电加DC-DC稳压控制指令迟缓、一顿一顿串口下发频率不稳定或下位机没做缓存上位机固定周期下发下位机独立中断更新PWM同一套参数换个场地就失效ROI标定、OTSU受环境光照影响检查灰度直方图必要时改用自适应阈值7. 从能跑到能赢一点个人体会视觉循迹这个项目看起来是“做一个车”实际上是把图像处理、嵌入式控制、传感融合、自动控制原理这几个方向打穿。做一次你收获的不只是一个能跑的小车更是一套“拿到问题先想物理边界、再想算法方案、最后逐层调优”的工作习惯。我个人做这个项目最大的感受是真正难的从来不是某个单点算法而是让整条链路稳定地协同工作。图像处理得再好PID没调好车照样跑偏车跑得再快电源供电不稳全盘崩掉。每一步都要稳扎稳打。最后再分享一个小习惯——强烈建议给摄像头画面加上实时窗口显示功能在调试阶段把二值化图、中线结果、偏差数值直接画在画面上。看着实时图像调参你会比只看代码快得多。等系统稳定后再加个开关把它关掉避免开销影响帧率。这篇文章把机器视觉循迹小车从硬件架构、图像处理、赛道识别、控制到底盘调试的核心链路都过了一遍。内容偏工程向没有堆太多理论公式但每一个环节都指向“照做能跑”的目标。如果你正在做类似的课程设计或者比赛任务建议先按这套思路把链路跑通再针对你的具体赛道特性去优化细节。那样比一上来就研究论文算法要快也会有成就感得多。
返回列表