用树莓派+OpenCV打造机器视觉循迹小车:从图像处理到PID控制

📅 发布时间:2026/9/9 10:57:24
用树莓派+OpenCV打造机器视觉循迹小车:从图像处理到PID控制
1. 项目背景与整体设计思路1.1 为什么做一台“看得见”的循迹小车先聊个挺现实的问题市面上几百块一套的循迹小车大多用的还是红外对管方案靠几组传感器检测黑线反射光的强弱来判断路径。这种方案不能说不能用但调试的痛点谁试谁知道——对环境光极其敏感LED灯的频闪会让传感器数值飘忽不定对赛道颜色也挑深色背景配浅色线就很难调阈值更麻烦的是前瞻距离只有两三厘米车速稍微拉起来就冲出赛道根本来不及转向。我在做这个项目之前已经用51单片机玩过好几版红外循迹小车每次调完阈值都能用但总感觉天花板太低。真正让我决定转向机器视觉方案是有一次在学校体育馆地板上做测试——场地用的是浅黄色木地板黑线是普通电工胶带下午四点钟太阳从西侧窗户照进来红外传感器的ADC读数在200到600之间狂跳完全没有稳定区间。那一刻我意识到探测距离太近、信息量太小是红外循迹的天生缺陷。机器视觉循迹小车解决的正是这个问题。用摄像头代替红外对管等于给小车装了一双“眼睛”能看到前方几十厘米、甚至一两米的赛道信息提前预判弯道和岔路。摄像头的分辨率再低输出的也是几百乘几百的像素矩阵信息量是开关量传感器完全没法比的。整个项目做下来核心架构是树莓派或Jetson Nano负责图像采集和路径识别输出转向和速度指令下位机单片机负责执行底层运动控制。这个项目适合谁来做简单分类一下如果你已经玩过51或STM32小车想进阶到视觉层面这是个很自然的跳板如果你对机器视觉感兴趣但不想一上来就做目标检测、人脸识别那种大而全的任务循迹是一个边界清晰、反馈即时、出效果快的入门级视觉应用另外这套系统里涉及的图像处理、串口通信、PID控制算法基本都是工业机器人、AGV小车岗位面试的标配考点做完整个项目的收获远不止“小车会跑”这么简单。1.2 技术选型背后的取舍逻辑先说摄像头。这个项目的图像处理任务相对单一核心是从场景里提取出跑道线对分辨率的要求并不高。市面上常见的USB摄像头640x480分辨率30帧就够用没必要上高分辨率——分辨率越高单帧图像处理时间越长控制频率就越低小车动态响应越差。这是一对儿矛盾看得清和看得快在这个场景里应该优先保证后者。然后是主控。很多教程喜欢用Jetson Nano跑视觉循迹性能确实过剩但价格和功耗也高。我的选择是树莓派4B4GB版本理由有三第一树莓派生态成熟OpenCV、Python的库支持几乎零障碍第二性能对640x480图像的实时处理完全够用实测单帧灰度图二值化加透视变换大约20到30毫秒第三GPIO、串口这些外设接口齐备和STM32下位机通信非常方便。如果你手头是树莓派3B性能稍弱但也能跑建议把图像分辨率降到320x240。下位机我用的是STM32F103C8T6也就是传说中的“蓝丸”最小系统板。这块板子在两轮平衡小车、四轴无人机里被用烂了资料满天飞。主控负责的运动解算、PWM输出、编码器读取这些实时性要求高的任务放到底层单片机做是合理分工树莓派只管“看和想”不用操心“怎么走”这样即使图像处理偶尔卡顿几十毫秒小车的电机控制也不会出现明显抖动。舵机转向方案还是差速转向方案大多数教程会选两轮差速——两个后轮独立驱动靠两轮转速差实现转向结构简单但转向半径不够灵活。我这次选了四轮小车底盘配舵机前轮转向动力后置也就是常见的“RC遥控车”布局。转向舵机响应快转向半径可以做到很小视觉循迹时过弯会更流畅。代价是底盘贵一点控制模型多一个舵机角度输出。两者没有绝对优劣看你想练哪块——差速方案练的是双轮PID同步舵机方案练的是转向角度的连续控制。1.3 整体系统架构从像素到车轮整台小车的信号链路一句话概括就是摄像头采集图像 → 树莓派识别赛道中线和转向角 → 通过串口发指令给STM32 → STM32输出舵机角度和电机速度。树莓派和STM32之间的通信协议我定义得尽量简单一帧数据三个字节帧头0xAA第二个字节是舵机角度范围0到180映射到前轮左右极限第三个字节是速度档位0到100。不用校验位因为串口波特率115200丢包率极低而且即使偶尔丢一帧控制周期只有50毫秒下一帧马上就能纠正过来。这里要特别强调一个设计原则视觉处理是慢任务运动控制是快任务。树莓派处理图像一帧20多毫秒STM32的PWM控制周期只有1毫秒两者绝对不能直接同步。下位机必须有一个独立的控制循环持续接收串口指令、更新目标值同时保持自身的速度闭环。即使树莓派因为CPU调度偶发卡顿小车也不会因此突然失控。2. 硬件搭建与关键部件选型2.1 摄像头安装高度和角度决定识别效果摄像头安装是整个项目中第一个“魔鬼细节”。我最初把摄像头直接固定在车头平视前方结果画面里大量背景信息——墙根、桌椅腿、行人——全都拍进来了赛道线只占画面底部窄窄一条二值化之后噪点爆炸处理难度大增。后来参考了AGV自动导引车的摄像头安装方式改成前悬支架结构摄像头装在车头前方伸出约10厘米位置高度离地约20厘米俯角大约30度。这样画面里绝大部分区域都是正下方的地面赛道线占比大幅提升目标检测极度简化。不要小看这个角度它决定了后续图像处理的所有难度——装得好识别就是找个亮线装得差识别就是在噪点里找规律。固定方式也有讲究别用什么热熔胶一圈糊死。我用的是一个摄像头支架加两颗M3螺丝锁在3D打印的底座上底座槽位做成可调的能小范围调整俯仰角。调焦方面普通免驱USB摄像头大多是定焦镜头出厂对焦在1米左右近距清晰度反而一般。实测下来把摄像头镜头拧松半圈让焦点落在大约20到30厘米这个工作距离上图像锐度明显改善。如果你的摄像头不能调焦也可以把安装高度适当增高拉大工作距离。2.2 主控板、电机驱动与电源分配树莓派的供电是最容易翻车的地方没有之一。树莓派4B满负载运行时的电流可以到1.2A以上再加上舵机在转向瞬间的电流冲击如果用同一个5V电源又带驱动板又带树莓派电压跌落会导致树莓派降频甚至重启。我的方案是双电源隔离一块3S锂电池11.1V给电机驱动板和舵机供电一块2000mAh的充电宝专供树莓派两者只共地不共电。运动控制部分的选型比较常规STM32F103C8T6最小系统板L298N电机驱动板MG996R舵机金属齿轮扭矩够大两个带霍尔编码器的N20减速电机1:30减速比配合TB6612FNG驱动芯片效果更佳但L298N胜在皮实耐操、便宜大碗新手先用它跑通逻辑问题不大。编码器是必须的没有它STM32无法实时感知车轮速度所谓PID闭环就是空谈。电机和编码器接线看起来繁琐实际规律性很强。N20电机自带6根线两根粗的接电源四根细的是编码器输出。编码器AB相分别接STM32的PA0和PA1开启定时器编码器模式STM32硬件自带正交解码一个定时器就能读出转速和方向不需要外部中断一个个数脉冲。一个容易忽略的问题是STM32和树莓派通信要共地。两块板子各用各的电源如果不把GND连在一起串口信号的电平参考点不一致数据会乱码。我在调试早期就吃过这个亏串口收上来的数据全是0xFF和0x00交替折腾了半天才发现是地没接。2.3 底盘组装顺序从方形铝板到完整底盘底盘我用的是某宝买的铝合金底板四轮布局前轮舵机带动转向拉杆后轮电机直驱。拧螺丝的顺序有讲究先把电机座和电机装上底板装编码器线再装驱动板和主控板最后才能装摄像头支架。如果先装支架再装电机螺丝刀会被支架挡住有些位置的螺丝死活够不着。舵机的安装是所有装配里需要点耐心的部分。MG996R需要一组舵机臂和转向拉杆配合拉杆两端是球头扣长度可调。调整时先把舵机臂拆下来通电让舵机回中位再把舵机臂装上让前轮处于正前方位置最后接拉杆。这个顺序错了的话转向角度会左右不对称调试时就会发现“哎左转最大20度右转怎么有35度”电源线有个容易被忽略的安全细节电机驱动板上的电源线建议用14AWG以上的粗线电流大时细线发热明显。我摸过一次导线表面温度大概有60度手能感觉到烫。有条件的话在电源输入端加一个470uF电解电容吸收电机换向时的尖峰电压能显著减少驱动板复位现象。3. 视觉处理管线设计与核心实现3.1 从BGR到二值化不是所有颜色信息都需要OpenCV读进来的图像默认是BGR三通道但对于循迹任务我们需要的信息只有“哪里是赛道线”颜色本身不重要。这种情况下直接把三通道彩色图转成灰度图再做二值化是性价比最高的路径——灰度图数据量是彩色图的三分之一处理速度大幅提升后续所有算法都建立在“亮/暗”两个状态上。赛道线的颜色选择会直接影响二值化效果。我用的是白色电工胶带贴灰色地面亮暗对比强烈二值化阈值非常好找。如果你只能用深色线就要考虑反色处理或者改用固定的浅色场地否则算法和阈值都要重写。灰度变换之后是二值化。OpenCV里最常用的是cv2.threshold的THRESH_BINARY关键在于阈值。固定阈值能跑但光线稍微变化就废更稳妥的是OTSU自适应阈值。OTSU的原理一句话能讲清楚遍历所有可能的阈值找到某两个值使得“前景类内方差加背景类内方差”最小。这个算法在光照均匀且前景背景灰度差异明显时效果极好。我用的是cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)一行代码搞定。如果你用HSV色彩空间稳定性会更好。HSV把颜色的色调H、饱和度S、明度V分开你只需要提取H通道的范围就能锁定特定颜色不受明暗影响。比如白色线在V通道很亮提取V通道重建掩码再叠加灰度二值化做“与”运算能扛住一定程度的光照波动。这个方法留给有余力的读者尝试篇幅有限就不展开代码了。3.2 透视变换把“近大远小”变成“俯视图”摄像头是斜向下装的拍到的赛道是梯形的——近处宽远处窄这对后续中线拟合不友好。解决办法是透视变换也就是把原始图像中赛道所在的梯形区域映射成一个固定大小的矩形相当于把相机“抬”到赛道正上方得到俯视效果。实现步骤分为三步第一步选取四个坐标点。在原图中框出一个梯形四个点分别对应赛道远端两个角、近端两个角。这个选取纯靠肉眼用鼠标回调函数在窗口里点就行保存成配置文件不需要每次启动重新标定。第二步定义目标矩形的四个点。我惯用输出320x240尺寸四个目标点分别是(0,0)、(320,0)、(0,240)、(320,240)。第三步调用cv2.getPerspectiveTransform(src, dst)算出变换矩阵M再调用cv2.warpPerspective把原图投影过去。这里有一个经验值分享给第一次做透视变换的朋友。不是梯形区域选得越大越好实际测试发现梯形区域近端宽度占画面总宽的80%左右远端占30%左右时变换后赛道线在图像中占比最均匀。选得太宽变换后赛道容易超出视野两侧选得太窄会丢掉近处信息转向反应迟钝。这个比例不是圣旨但第一次调试不知道从哪下手的话照着这个数来能少走弯路。3.3 赛道中线提取滑动窗口还是直接重心法透视变换之后画面里就是一张俯视的、以赛道线为主体的二值图。提取赛道中线的思路很多按复杂程度从低到高排有单纯的行像素平均法、滑动窗口法、以及基于边缘检测的拟合方法。项目实战我建议你从行像素平均法起步原理极为简单对图像每一行分别遍历所有列找到白色像素点的列坐标取平均值得到“这一行的赛道中线位置”。全部行做完就得到了一条从画面底部延伸到顶部的曲线。这个方法在赛道干净、没有岔路和遮挡时稳定可靠代码不到十行运行速度极快。如果场地复杂一些会出现线间断、岔路等情况单纯平均法会把中线拉到错误方向上。此时升级为滑动窗口法也就是把图像按行分成若干层从最下方已知赛道位置开始每一层只在上一层面位置附近的一个窗口内寻找白色像素中心窗口宽度我通常设为80像素。这样做的好处是即使赛道有个小断点窗口机制也能强行续上不会因为某一行的信息缺失导致全面崩溃。滑动窗口的缺点是它的“记忆”特性——一旦跟错左侧的分叉线后面所有窗口都会沿着左边一直走无法跳回正确赛道。解决方法是增加一个置信度判据窗口内白色像素数低于某阈值时认为该层识别不可靠中线位置取上一层位置加一个固定偏移保持前进直到重新找到赛道。这个逻辑简单但有效防丢线的效果明显。3.4 转向角计算前轮转角和偏差的映射关系拿到当前行的赛道中线偏差之后怎么把它变成舵机角度这是整个视觉循迹链路里最“控制理论”的环节也是不少人栽跟头的地方。我说一下我的做法。先选一个“参考行”一般取画面垂直方向约2/3处的那一行这个位置的赛道信息代表车前约10到15厘米处的路径是前瞻距离的折中选择。取得这一行的中线x坐标和图像中心x坐标320/2160做差就得到了横向偏差。但是这个偏差的量纲是像素舵机的量纲是角度需要建立映射。最简单的线性映射舵机输出 中位角度 横向偏差 × 比例系数K中位角度实验中我标定在约78度比例系数K我一开始拍脑袋设成0.2意味着100像素偏差就是20度转向——实测结果是过弯直接冲出赛道响应太快。后来逐步减小最终稳定在0.08左右即大约125像素的偏差对应10度转向。不同底盘和舵机特性不同这个系数必须实车调试别指望抄作业抄不了。线性映射够用一段时间但在大弯道和直线衔接处有明显瑕疵——直线区偏差小转向太灵敏车子蛇行大弯时偏差大转向角度却因为比例常数不变而显得不足。改进方案是分段映射或引入非线性函数比如偏差小时用较小的K偏差大时用更大的K本质上是给转向控制器加了一个“死区”和一个“饱和区”。代码实现就是一个if-else但效果立竿见影小车直线行驶的稳定性大幅提升。4. 下位机控制串口数据解析与PID调速4.1 串口接收协议解析与状态机树莓派通过串口发出三字节指令STM32要把它安全地解析出来。解析最忌讳简单粗暴地一个字节一个字节往变量里赋值——没有帧头校验一旦串口数据错位后面所有帧都会错乱且永远无法自行恢复。正确姿势是状态机。定义三种状态STATE_IDLE空闲等帧头、STATE_HEADER收到帧头等角度字节、STATE_ANGLE收到角度等速度字节。代码骨架如下uint8_t rx_state STATE_IDLE; uint8_t rx_angle 0; uint8_t rx_speed 0; void USART1_IRQHandler(void) { uint8_t byte USART_ReceiveData(USART1); switch(rx_state) { case STATE_IDLE: if(byte 0xAA) rx_state STATE_HEADER; break; case STATE_HEADER: rx_angle byte; rx_state STATE_ANGLE; break; case STATE_ANGLE: rx_speed byte; rx_state STATE_IDLE; // 到这里说明完整收到一帧指令 servo_set_angle(rx_angle); target_speed rx_speed; break; default: rx_state STATE_IDLE; break; } }状态机的好处是即使中间丢字节也知道丢在哪一步不会长期处于错乱状态。我实际测试中长时间运行后偶尔会因为树莓派侧的串口缓冲被系统抢占导致一帧数据残缺但下一帧到来时状态机自动复位控制输出只在极短时间内保持旧值不产生明显影响。4.2 编码器数据读取与速度PID速度控制我用的经典增量式PID。先解释一下编码器的使用N20电机的霍尔编码器一般每转输出几十个脉冲经过1:30减速箱后对应到轮子转速。STM32的定时器编码器模式可以直接对AB相脉冲进行4倍频计数精度更高。我设定的控制目标是——让左右轮的实际转速尽量逼近同一个目标值。这个目标值是根据树莓派传来的速度档位换算出来的换算公式是目标转速RPM 速度档位 * 0.3也就是说档位100对应30RPM。PID三个参数我调试了很多轮最终的整定值跟多数常规电机接近Kp12Ki0.1Kd0。是的Kd直接设成0多数电机调速场景微分项容易放大噪声去掉反而更稳。整定的方法说句大实话别去背一堆“临界比例度法”的条条框框先用Kp从0开始慢慢加直到电机转速出现持续、等幅的轻微振荡记下这个Kp作为临界值再把Kp取这个值的40%到60%Ki从0逐步加到静态误差消除Kd默认给0需要抑制超调再加一点点。这个经验流程比任何书本都实用二十分钟能完成一轮整定。4.3 转向舵机的PWM信号与限幅保护MG996R舵机的PWM控制频率是50Hz也就是周期20毫秒脉宽0.5毫秒到2.5毫秒分别对应0度和180度。STM32用定时器的PWM输出模式比较寄存器值换算成角度即可。我为了省事写了个通用函数void servo_set_angle(uint8_t angle) { // 角度范围 0~180, 对应 PWM 比较值 500~2500 uint16_t pulse 500 (uint16_t)angle * 2000 / 180; TIM_SetCompare3(TIM2, pulse); }这里必须点名一个新手常踩的坑。STM32定时器的ARR值要设置成20000预分频值要设置成72才能得到20毫秒周期和1微秒精度的组合。如果ARR写成了200PWM频率变成5kHz那舵机会发出尖锐的啸叫声舵机臂疯狂抖动怎么调角度都没用。这个坑我当年至少卡了一个下午最后拿示波器看波形才醒悟过来。另一个注意事项是舵机限幅。保护机制必须在下位机做不能在树莓派端做。万一树莓派视觉程序因为某种bug算出一个极端角度比如18度如果不做限幅舵机就会硬顶到机械极限位置轻则舵机齿轮崩裂重则拉杆弯掉。我的做法是if(angle 40) angle 40; if(angle 140) angle 140;40到140这100度的范围对应安全机械行程留出至少20度的余量这样舵机即使收到极端指令也不会撞墙。5. 联调过程、常见问题与避坑技巧5.1 巡检调试的顺序和节奏先静态后动态联调阶段最忌讳的是“一步到位”——树莓派、STM32、电机、摄像头四样东西同时上电跑起来出问题了根本没法定位是哪个环节的锅。我的调试顺序固定为五步第一步单独调摄像头。装好OpenCV打开窗口看实时画面确认图像分辨率、帧率正常对焦清晰没有花屏和延迟。这一步不写任何算法就是看画面干不干净。第二步单独调透视变换。用鼠标标定梯形区域确认变换后的俯视图赛道线笔直没有畸变和变形。第三步单独调二值化和中线提取。在画面中把识别出的中线用红线画出来确认在没有动力的情况下赛道中线提取效果稳定。第四步把树莓派和STM32通过串口连起来树莓派定时发送固定的角度、速度数据看STM32是否收到、舵机是否转到指定角度、轮子转速是否稳定。这一步完全不涉及视觉纯粹验证通信链路。第五步才是视觉与控制合流。把小车架起来四个轮子悬空观察它“看着”赛道时舵机和轮子的响应方向是否正确——左偏时前轮是否左转然后放到地面先用低速速度档位30跑直线慢慢加速再试弯道逐步调整PID和转向比例系数。很多初次做这个项目的朋友上来就把小车放到地上全速跑结果撞墙、翻车、冲出赛道然后陷入“改参数-再跑-再撞”的无限循环。整个下午下来毫无进展。老老实实按这个顺序来大部分问题能在低速度、低风险环境下暴露并解决。5.2 常见问题速查表3类典型故障与处理办法Q1二值化后的图像全是噪点或者赛道线断成一截一截的。检查优先级先看光源环境是不是有窗外直射光在赛道表面形成反光反光区域会被误判为白色。对策是调整摄像头位置让它避免直接面对光源方向或者把白色电工胶带换成哑光材质减少镜面反射。如果光源没问题再考虑阈值问题OTSU也不是万能的你把OTSU多跑几帧看直方图如果前景背景峰不够分离说明对比度本身就不够得换赛道贴纸颜色。在较暗走廊做测试时我的经验是多加一盏匀光LED灯板作为辅助照明效果直接提升一个档次——机器视觉这行灯光比算法还重要这句话是真金白银换来的。Q2舵机转向时好时坏有时根本不转。用手去摸舵机外壳如果发烫到烫手大概率是舵机堵转了——转向拉杆卡在极限位置舵机一直在发力但转不动。解决办法是先断电手动把前轮掰到居中位置重新上电看舵机是否正常接受指令转动如果还是堵转检查舵机臂是否卡到底盘边缘。另外还有一个电气层面的原因MG996R舵机在堵转时电流可到2A如果电源线太细或者电源容量不够电压跌落会导致舵机复位表现就是“突然死了又突然活了”。量一下舵机电源端电压是否稳定在5V以上。Q3小车走直线时像喝醉了一样画龙。这是典型的转向过度。原因一般是转向比例系数K太大加上速度PID存在滞后。处理办法先把K降下来四分之一看蛇行幅度是否有明显减小如果减小了但还不够继续降。如果降K后直线稳了但弯道转不过来说明不是K的问题而是前瞻距离太远转弯响应太慢。此时缩短参考行的位置或者减小透视变换中的远端距离。我发现多数蛇行问题的根源是期望用比例转向解决所有问题其实低速起步时转向更从容写代码时加一个“起步限速”逻辑会舒服很多。5.3 提高稳定性的几条“野路子”经验下面几条内容很多正规教程不会写但都是实测有效、成本极低的做法。摄像头镜头用UV滤镜或者普通透明片挡住灰尘。地面环境跑久了镜片上全是灰图像清晰度下降30%以上不说关键是二值化的阈值会被灰尘投影干扰。清理镜片是日常维护里最容易被忽略但效果最明显的动作。赛道两侧贴上约2厘米宽的低反光黑色电工胶带相当于给赛道加了“路肩”。视觉算法的鲁棒性立刻提升因为二值化后黑色路肩和白色赛道线之间多了一层高对比缓冲区中线的提取更稳定。这个方法是我在一次比赛中偷师学来的效果立竿见影。运行环境的光照如果没法保证恒定就写一个跟踪阈值的自适应逻辑。每隔一段时间比如20帧在画面角落里取一块认为不会出现赛道线的区域计算该区域的平均灰度作为基线再在这个基线上做偏移得到二值化阈值。这个方案能扛住一部分缓慢的光照变化比如太阳在傍晚逐渐变暗的情况。最后关于调试时的数据可视化不要小看“把程序跑的中间过程用窗口显示出来”这件事。我在树莓派上开了一个名为debug的窗口左上角小图显示原始灰度图右上角显示二值化结果图左下角显示透视变换后的俯视图右下角显示画了赛道中线和参考行的最终结果图。四个小窗口同时展示什么环节出了问题一目了然。树莓派的HDMI接一个小显示器或者用VNC远程看都非常方便。6. 一步到位的完整代码树莓派视觉主程序树莓派端的代码是整个项目的大脑放在这里的是我实测可运行的核心版本。依赖库只有OpenCV、NumPy和serial安装命令pip install opencv-python numpy pyserial坐标标定部分有些机器相关的常数会以变量形式给出标注请根据自己的场地重新标定。import cv2 import numpy as np import serial import time # 串口初始化根据实际设备号修改 ser serial.Serial(/dev/ttyUSB0, 115200, timeout0.01) # 透视变换的4个源点需要根据实际标定修改 SRC_POINTS np.float32([[130, 40], [190, 40], [320, 240], [0, 240]]) DST_POINTS np.float32([[0, 0], [320, 0], [320, 240], [0, 240]]) M cv2.getPerspectiveTransform(SRC_POINTS, DST_POINTS) # 前视参考行取高度240图像的160行附近 REF_ROW 160 # 转向比例系数 STEER_K 0.08 # 舵机中位角度需实测标定 SERVO_MID 78 cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) servo_angle SERVO_MID speed_level 30 while True: ret, frame cap.read() if not ret: continue # 1. 裁剪感兴趣区域减少计算量 roi frame[240:480, 0:640] # 2. 灰度化 高斯模糊去噪 gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) gray cv2.GaussianBlur(gray, (5, 5), 0) # 3. OTSU自适应二值化 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # 4. 透视变换 warped cv2.warpPerspective(binary, M, (320, 240)) # 5. 提取赛道中线行平均法 center_line [] for row in range(0, 240, 10): row_data warped[row, :] white_indices np.where(row_data 0)[0] if len(white_indices) 5: center int(np.mean(white_indices)) center_line.append((row, center)) # 6. 找到参考行的赛道中心坐标 error 0 found False for row, center in center_line: if row REF_ROW: error center - 160 found True break # 7. 未找到赛道时保持上次角度 if found: delta_angle error * STEER_K servo_angle int(SERVO_MID delta_angle) # 角度限幅 servo_angle max(40, min(140, servo_angle)) # 8. 串口发送指令 cmd bytes([0xAA, servo_angle, speed_level]) ser.write(cmd) # 9. 调试可视化 vis cv2.cvtColor(warped, cv2.COLOR_GRAY2BGR) for row, center in center_line: cv2.circle(vis, (center, row), 2, (0, 0, 255), -1) cv2.line(vis, (0, REF_ROW), (320, REF_ROW), (0, 255, 0), 1) cv2.imshow(debug, vis) # q键退出 if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()代码里值得提几个细节。一是裁剪ROI我只取原始帧的下半部分因为赛道基本只出现在车前方下部上半部分全是墙壁和远景处理了只会增加噪声和计算量。二是参考行选160这个位置的赛道信息大约对应车前10到15厘米处配合舵机转向的响应速度效果比较均衡。三是未找到赛道时的处理很多新手直接跳过发送旧角度实际上这个“保持”逻辑必须有否则一旦连续几帧找不到赛道小车就会因为收不到指令而停在原地。串口指令发送频率由主循环自然决定大约30到50帧每秒。实测中树莓派4B跑这段代码的CPU占用率在30%到40%之间不会过热降频连续运转一小时以上稳定性有保障。值得注意的是树莓派的串口默认分配给系统控制台需要先用raspi-config把串口功能改为可用状态否则serial模块打不开串口设备。7. 从“能跑”到“跑得好”确定性的进阶路线图7.1 第一阶段稳定直线与缓弯目标小车在干净的直线赛道上保持居中行驶不会画龙弯道半径大于1米时能基本顺利过弯。这个阶段要解决的核心问题是基础控制逻辑是否正常。我的经验是把速度固定在最低档20到30把转向比例系数调到一个较小值追求“稳”而非“快”。此阶段用时大约半天到一天。7.2 第二阶段直角弯、U形弯和连续S弯直角弯的难点在于——当参考行已经压在弯道内部时转向已经晚了。解决方法是动态调节参考行的位置根据赛道最近几帧的平均曲率可以用当前帧中线各列位置的离散程度估算如果曲率变大说明进弯了参考行下移看更近处的赛道信息及早转向。U形弯则需要配合降速逻辑检测到前方近距离没有赛道线时速度降一半同时舵机打死方向否则循迹必漂。7.3 第三阶段岔路选择与路口识别这属于拓展玩法了。在岔路口行平均法会把两条分支的平均值当成中线小车直接冲着路口中间去了空中转向。处理思路是加一个判断某一行的白色像素如果分成两个明显的聚类用直方图/连通域分析判断且两个聚类中心距离超过某阈值说明遇到岔路。此时可以根据预设策略选择左分支或右分支——选择方式是设置一个“偏好标记”在下一帧强制只看偏好侧的窗口区域。这个实现有意思但工作量不小适合在基础循迹跑顺后当进阶课题去练。7.4 第四阶段极端环境适应性到了这个阶段可以挑战晚上灯光条件差的环境、反光严重的瓷砖地面、有落叶和纸屑杂物的赛道。每一步都是对二值化阈值自适应、连通域去噪和目标跟踪算法的强化训练。这一阶段做完一遍你对“为什么会误检”“为什么会有噪点”的理解会超过很多只调过参数的人。8. PID参数整定全记录从震荡到稳定的详细过程既然文章里反复提到PID参数这片从现场记录整理出来的整定过程直接看会更直观。第一次上电我用的是Kp5、Ki0、Kd0的初始猜测。电机转速在空载时听起来就像呼吸一样忽快忽慢声音规律性波动轮子转速在大部分时间围绕目标值上下摆动。这个现象说明比例项太小系统处于欠阻尼状态。把Kp从5提到12后声音的波动频率变快振幅减小但仍然可见。这已经接近临界状态我继续每次加2Kp到18时出现了一个新现象电机轻微的高频“嗡嗡”声转速开始以极小幅度持续振荡。根据齐格勒-尼科尔斯经验法这就是临界振荡Kp临界值记作18。然后我把Kp减到临界值的一半Kp9再加Ki。Ki从小到大从0.01起步逐步到0.1发现转速的静态误差在一两秒内能收敛到设定值附近响应足够快。继续加大Ki到0.3电机出现了低频“喘振”——速度周期性大幅波动这是积分项过大的典型症状果断退回0.1。最终参数定在Kp9、Ki0.1、Kd0。小车上实际跑出来的效果是直线段轮速平稳声音均匀转弯时因为负载变化引起的短暂转速跌落能在半秒内恢复。这组参数模板可以直接套用但不同底盘、电池电压、轮胎磨损状态下会有差异最终还是要实车微调。9. 写在最后项目做完之后的一点体会这个项目做完前后改了七个版本从最初摄像头平视、透视变换标不准、串口乱码到最后稳定跑完S形赛道的全过程收获最大的是对“系统思维”有了明确概念。循迹小车看着简单但由于涉及的环节多——任何一环出问题都会导致最终表现大打折扣。树莓派系统供电不稳定会导致画面卡顿排查半天发现问题是充电宝功率不够摄像头俯仰角拧歪了1度结果二值化噪声大了几倍串口地线没接好视觉再准控制信号也传不下去。每修一个问题对整套系统的理解就深一层。如果这个文章给了你一些启发想动手试试我的建议是不要一上来就追求高配置——一台树莓派4B、一个60块的USB摄像头、一套蓝丸STM32板子加N20电机底盘足够完成全部环节的开发和调试。控制在四五百块钱以内的预算就能把整个技术链路完整打通。真把这条链路里的每一个环节都弄明白后面去做JSON数据解析也好、做图像分类也好、做更复杂的运动控制也好你会发现自己看问题的视角都变得更全局了。最后再分享一个小经验调试时备一张纸和一支笔每改一次参数、每调一次阈值都记录当时的现象和结果。项目做到后期你会疯狂感谢这些实验记录因为同一个问题可能在不同阶段以不同形式重复出现有记录才能在十分钟内定位到旧坑而不是重新踩一遍。循迹小车的魅力就在于它给每个人的反馈极其真实直接——调得好不好跑一圈赛道就知道了。