十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

OpenMV巡线小车实战:从图像处理到PID调参全流程

OpenMV巡线小车实战:从图像处理到PID调参全流程 很多第一次接触开源小车的朋友拿到的第一个任务往往就是“巡线”。程序看起来也确实不难传感器读到黑线位置电机按偏移量调整差速小车就能沿着线跑。但真正把车放到地面上你会发现另一件事——它要么在直道上画龙要么在弯道直接冲出赛道要么走几步就疯了一样原地转圈。巡线之所以被大家当作机器人入门的经典项目是因为它把视觉感知、串口通信、运动控制三条链路完整地串在了一起。任何一环出了问题车就跑不直、过不了弯。这篇文章准备用“OpenMV 主控小车”这套最典型的开源组合把巡线从图像处理到 PID 调参的完整工程过程拆开讲清楚。你会看到巡线的重点不是“找线”而是如何把找线的结果变成稳定、实时的控制信号。1. 为什么“看上去简单”的巡线其实是组合工程如果只追求“能跑”巡线的门槛确实不高。灰度传感器小车甚至可以只用几行判断语句最中间的传感器看到白底就认为线在偏左方向左轮减速。这种代码放在干净的地面上跑效果相当不错。但往工程深处走一步问题就来了。首先是传感器本身的局限它只能看到车头下面很小的一个范围车速稍微快一点弯道前瞻就不够其次是环境干扰光线一变、地面颜色不同、黑线宽度不一样固定阈值全部失效再往后是控制层面即使传感器能准确读到线电机响应、转向执行、车体惯性这些因素也会让小车在目标线两侧反复震荡。这就要回到一个基础判断上巡线不是单点技术而是感知、通信、控制三件事的组合工程。感知负责回答“线在哪里”通信负责把答案传到主控控制负责让车身真正跟随线走。三层任意一层不稳定整台车就无法工作。对于学习机器人的人来说这恰恰是最好的训练场。它不像无人机那样需要复杂的动力学知识也不像机械臂那样对精度要求苛刻却能让你快速理解“传感器数据 → 数据处理 → 执行器指令”这条最基本的机器人工作链路。这篇文章以 OpenMV 摄像头为感知前端、以 Arduino 或 STM32 为主控走的正是工程开发里最常用的一条路径。2. 巡线小车的三种主流方案与选型判断在动手接线之前先花点篇幅把主流方案理清楚。很多人在选型阶段被“哪个更好”困住其实每种方案都有自己的适用场景不存在绝对最优。2.1 灰度传感器阵列这是最早的方案也是很多比赛小车的首选。常见的是 5 路或 8 路灰度传感器并排安装在车头底部每个传感器通过红外发射管 接收管检测地面反射光强度。黑色吸光、白色反光所以传感器可以直接输出一个数字信号黑线为低电平白底为高电平。这种方案的优点是成本低、响应快、程序简单几乎没有图像处理延迟。但它有三个明显局限只能识别“黑或者白”无法感知更复杂的赛道元素检测范围受安装高度和排布宽度限制前瞻距离很短对光照和地面颜色变化非常敏感换一个场地往往要重新标定。如果目标是入门最快、代码最简洁灰度传感器阵列是稳妥的选择。但如果后续想让小车识别十字路口、斑马线、停车区这些复杂元素它很快就会不够用。2.2 OpenMV 视觉方案OpenMV 本质上是一个带摄像头的 MicroPython 开发板可以在板端直接运行视觉处理脚本。它通过摄像头采集画面在嵌入式处理器上完成颜色阈值分割、特征提取然后把结果通过串口发给主控。相比灰度传感器视觉方案最大的优势是信息量。它能在一帧画面里同时看到线、障碍物、目标颜色块后续要做扩展时不需要增加太多硬件其次摄像头安装在车体前方能看到更远的路面这给高速循迹留出了前瞻距离。但它的代价也很直接需要调摄像头参数、调颜色阈值、处理曝光问题还要保证图像处理流程足够快。很多新手第一次接触 OpenMV 巡线往往卡在阈值标定上——环境光一变黑线就识别不出来了。2.3 两种方案核心对比对比维度灰度传感器阵列OpenMV 视觉方案复杂度低几行代码即可中高需要图像处理脚本前瞻距离短受安装位置限制长可看到前方较远路面环境适应性对光照敏感需重新标定固定曝光后可适应多数场景扩展性弱适合固定赛道强可识别复杂元素成本低中调试难度简单直观需要工具和经验这篇文章选 OpenMV 方案展开不是因为它比灰度传感器“高级”而是因为它在工程上的延展性更好。即使你最终做的是比赛小车视觉方案的调试经验也能直接迁移到更多场景中。3. OpenMV 巡线原理与图像处理流程把 OpenMV 接到小车之前先理解它在做什么。整个巡线图像处理流程可以拆成两步第一步是把彩色画面变成“黑白分明”的二值图第二步是从二值图里提取线的位置或角度。3.1 OpenMV 到底是什么OpenMV 是一块集成了摄像头、微控制器和 MicroPython 解释器的视觉模块。它不需要像树莓派那样跑完整操作系统而是直接在单片机上执行 Python 脚本。它提供了sensor、image等模块封装了摄像头采集、图像滤波、颜色识别、直线提取等底层操作。巡线场景里最常用的 API 是这几个sensor.reset()重置摄像头sensor.set_pixformat(sensor.RGB565)设置像素格式为 RGB565sensor.set_framesize(sensor.QVGA)设置画面尺寸为 320x240find_blobs()按颜色阈值查找色块image.get_regression()在感兴趣区域里做线性回归返回一条直线对象OpenMV 还能直接通过串口输出结果这一点对和主控配合来说非常重要。3.2 二值化把“黑白”变成程序能处理的数字OpenMV 默认输出的是彩色图。要让程序找到黑线需要先把彩色图转换成二值图黑色为 0白色为 1。这个转换依赖 LAB 颜色空间下的亮度阈值。LAB 是由亮度 L 和两个颜色通道 a、b 组成的色彩模型。对纯黑线来说L 通道的值会非常小对白色地面L 通道的值则接近 100。所以巡线阈值通常写成类似(0, 20)这样的元组表示只保留 L 通道在 0 到 20 之间的区域。在 OpenMV IDE 里有一个专门的颜色阈值编辑器可以打开摄像头实时画面用鼠标调整每个通道的上下限然后把生成的阈值元组复制到代码里。这个工具是调参效率最高的方式强烈建议先用它做标定。3.3 线性回归为什么优于质心法拿到二值图后有两种常见方式计算线的位置一种叫质心法把画面里所有黑色像素点做平均得到黑线的质心坐标。这个方法的缺点是一旦画面里出现噪声点或断线质心位置会明显跳变导致小车抖动。另一种叫线性回归法对二值图中所有黑色像素点拟合一条直线返回直线的角度和位置。image.get_regression()就是在做这件事。即使黑线上有几个小缺口回归结果也不会剧烈变化抗干扰能力明显更强。对巡线小车来说线性回归产生的角度信息可以直接映射到转向量控制起来更平滑。这也是 OpenMV 官方例子推荐的方式。4. 硬件环境与接线准备开始写代码前先把硬件准备好。以下是一套典型的 OpenMV 巡线小车组合不涉及具体品牌只描述功能模块的选型和接线思路。4.1 硬件清单模块作用说明小车底盘提供车架、电机和轮子常见两驱或四驱底盘均可电机驱动模块驱动直流电机如 TB6612、L298N推荐 TB6612主控板负责接收视觉结果并控制电机可选 Arduino、STM32、ESP32OpenMV 摄像头模块负责图像采集和巡线算法推荐带 RGB 摄像头的基本款即可电池为整个系统供电航模电池或充电宝组合需匹配电压降压模块为 OpenMV 提供稳定电压可选视电池电压而定这里不写死具体型号和版本原因是硬件市场迭代很快不同批次的可视化参数差异较大。实际购买时以卖家提供的文档为准本文重点演示通用思路。4.2 通信接线OpenMV 通过 UART 串口和主控通信。接线规则是OpenMV 的 TX 接主控的 RXOpenMV 的 RX 接主控的 TX两边的 GND 必须共地。共地是新手最容易漏掉的一步漏掉之后最常见的现象就是串口数据乱码或者完全收不到。以 OpenMV Arduino 为例典型接线参考OpenMV TXD - Arduino RX (D0) OpenMV RXD - Arduino TX (D1) OpenMV GND - Arduino GND Arduino VIN - 电源模块正极注意电压匹配 Arduino GND - 电源模块负极电机驱动和主控这边以 TB6612 为例典型接线参考TB6612 PWMA - Arduino D5西电机PWM TB6612 AIN1 - Arduino D6 TB6612 AIN2 - Arduino D7 TB6612 PWMB - Arduino D9东电机PWM TB6612 BIN1 - Arduino D8 TB6612 BIN2 - Arduino D10 TB6612 STBY - Arduino 5V TB6612 VM - 电池正极 TB6612 VCC - Arduino 5V TB6612 GND - 电池负极和 Arduino GND 共地4.3 软件环境OpenMV 的编程和调试使用官方 IDE下载安装对应版本即可版本以官网当前发布为准。写代码时在 IDE 里连接 OpenMV运行脚本后可以看到摄像头画面也可以在串口终端里查看输出。主控侧的环境则取决于选型。Arduino 使用 Arduino IDE 安装对应开发板支持包STM32 可以使用 STM32CubeIDE 或 PlatformIO 的 HAL 工程。本文核心逻辑放在 OpenMV 侧主控侧用 Arduino 写法演示串口接收和电机控制STM32 读者可以对照移植。5. OpenMV 侧巡线代码与阈值标定OpenMV 侧是整个巡线系统的感知核心它的任务是在每一帧画面里找到黑线并把“线的角度/位置”通过串口发给主控。下面给出完整代码。5.1 第一步用 IDE 的阈值编辑器标定实际操作中不要先把代码写完再调参。正确顺序是先用 OpenMV IDE 的“工具 → 机器视觉 → 阈值编辑器”打开摄像头画面然后用鼠标把黑色线条在画面上标出来IDE 会自动生成一组 LAB 阈值。以黑色线为例生成的阈值大概长这样# 在 IDE 的阈值编辑器里复制出来的结果 THRESHOLD (0, 30, -20, 20, -20, 20)这个元组的含义依次是 L 最小值、L 最大值、A 最小值、A 最大值、B 最小值、B 最大值。不同光照下数值差别很大一定要在实际场地里现场标定。5.2 完整代码OpenMV 巡线输出# 文件路径main.py OpenMV 侧 import sensor import image import time from pyb import UART # 初始化摄像头 sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QQVGA) # 160x120帧率更快 sensor.skip_frames(time2000) # 等待摄像头稳定 sensor.set_auto_gain(False) # 关闭自动增益固定曝光 sensor.set_auto_whitebal(False) # 关闭自动白平衡减轻光照波动 sensor.set_auto_exposure(False, exposure_us20000) # 固定曝光时间 # 初始化串口TXP4, RXP5波特率115200 uart UART(3, 115200, timeout_char1000) # 黑线阈值务必在现场用阈值编辑器重新标定 THRESHOLD (0, 30, -20, 20, -20, 20) # ROI只处理画面下方一半减少上方环境干扰 ROI (0, 60, 160, 60) # 帧标志方便主控判断数据是否更新 frame_id 0 while True: frame_id (frame_id 1) % 100 img sensor.snapshot().lens_corr(1.0) # 在线性回归前先把 ROI 内的画面转成二值图 img.binary([THRESHOLD], invertFalse, roiROI) # 对二值图中的黑色像素做线性回归得到直线对象 line img.get_regression(thresholds[(0, 0, 0, 0, 0, 0)], roiROI, area_threshold20, pixels_threshold20, robustTrue) if line: # line.theta() 返回角度范围 0-180减去90后转为带方向的偏差 angle int(line.theta() - 90) # line.rho() 返回直线到原点的垂直距离这里不做深度修正 rho int(line.rho()) # 发送格式帧号,角度,rho uart.write(f%d,%d,%d\n % (frame_id, angle, rho)) print(angle:, angle, rho:, rho) else: uart.write(lose,%d\n % frame_id) print(line lost) time.sleep_ms(1)这段代码的关键逻辑分四步关闭自动曝光、自动增益和自动白平衡避免环境光线波动导致画面明暗突变用binary()函数把 ROI 区域内的黑线变成纯黑区域用get_regression()对黑色像素做线性回归得到代表黑线的直线把角度和帧号通过串口发送出去主控据此计算转向。需要说明的是line.theta()返回的角度是“直线在图像坐标系中的倾角”直接减去 90 后正值表示线偏右负值表示线偏左主控处理起来会比较直观。如果不想用角度而是想用“线中心相对画面中心的水平偏移”可以在binary()之后用find_blobs()求所有黑色色块的重心再把重心 x 坐标减去画面中心 x 坐标效果也类似。实际工程中两种方式都有我倾向于用线性回归角度因为它在断线和噪声场景下更稳定。5.3 定时发送还是循环发送上面的代码采用了“每帧都发送”的轮询方式简单直接。另一种做法是使用定时器固定频率发送比如每 30ms 发一次。固定频率的好处是主控侧可以按节奏处理不会因为偶尔的丢帧导致数据混乱。对于绝大多数开源小车项目轮询发送已经够用因为主控端处理 PWM 指令的周期通常是 10~20ms和 OpenMV 的帧率基本匹配。6. 主控侧串口接收与 PID 控制OpenMV 把角度发给主控之后真正决定小车“会不会走稳”的是主控侧的电机控制。这里采用的方案是增量式 PID 控制核心思路是根据角度误差调整左右轮速差。6.1 串口协议设计上一节的 OpenMV 代码发送的是字符串格式数据例如f1,15,120 f2,-8,118 lose,3主控侧要做的第一件事是把这串文本解析成可计算的数值。建议加一个简单的帧头判断比如f开头代表正常数据lose开头代表丢线。这样即使偶发乱码程序也能通过字符过滤避免误判。6.2 Arduino 侧代码串口解析与 PID 控制// 文件路径arduino_controller.ino // 功能接收 OpenMV 串口数据用增量式 PID 控制差速 #include SoftwareSerial.h // 设置软件串口OpenMV 接 D2 和 D3 SoftwareSerial mySerial(2, 3); // RX, TX // 电机驱动引脚 const int PWMA 5; // 左轮PWM const int AIN1 6; const int AIN2 7; const int PWMB 9; // 右轮PWM const int BIN1 8; const int BIN2 10; // PID 基础参数先用 P稳定后再加 I 和 D float Kp 0.35; float Ki 0.0; float Kd 0.0; float error 0; float lastError 0; float integral 0; // 基础速度 int baseSpeed 80; void setup() { Serial.begin(115200); // 调试串口 mySerial.begin(115200); // 与OpenMV通信 pinMode(PWMA, OUTPUT); pinMode(AIN1, OUTPUT); pinMode(AIN2, OUTPUT); pinMode(PWMB, OUTPUT); pinMode(BIN1, OUTPUT); pinMode(BIN2, OUTPUT); pinMode(10, OUTPUT); digitalWrite(10, HIGH); // TB6612 STBY 置高使能电机 } void loop() { if (mySerial.available() 0) { String raw mySerial.readStringUntil(\n); raw.trim(); if (raw.startsWith(f)) { // 解析f帧号,角度,rho int firstComma raw.indexOf(,); int secondComma raw.indexOf(,, firstComma 1); if (firstComma 0 secondComma 0) { int angle raw.substring(firstComma 1, secondComma).toInt(); controlMotor(angle); Serial.print(angle: ); Serial.println(angle); } } else if (raw.startsWith(lose)) { // 丢线先停止转向保持滑行 stopWithBrake(200); } } } // 增量式 PID 差速控制 void controlMotor(int angle) { error angle; // 角度偏差正为偏右负为偏左 float P Kp * error; integral error; float I Ki * integral; float D Kd * (error - lastError); float turn P I D; lastError error; int leftSpeed constrain(baseSpeed - turn, 0, 255); int rightSpeed constrain(baseSpeed turn, 0, 255); setMotor(0, leftSpeed); // 左电机 setMotor(1, rightSpeed); // 右电机 } // 简单电机控制函数左右电机使用同一方向 void setMotor(int motorIndex, int speedVal) { speedVal constrain(speedVal, 0, 255); bool forward speedVal 0; int absSpeed abs(speedVal); if (motorIndex 0) { digitalWrite(AIN1, forward); digitalWrite(AIN2, !forward); analogWrite(PWMA, absSpeed); } else { digitalWrite(BIN1, forward); digitalWrite(BIN2, !forward); analogWrite(PWMB, absSpeed); } } void stopWithBrake(int durationMs) { digitalWrite(AIN1, LOW); digitalWrite(AIN2, LOW); digitalWrite(BIN1, LOW); digitalWrite(BIN2, LOW); delay(durationMs); }这段代码里需要留意几个地方SoftwareSerial是 Arduino 的软件串口适合调试用对 STM32 或更高频率的板子建议直接使用硬件 UARTbaseSpeed不建议一开始就设太高先让小车低速跑起来稳定后再加turn变量由 PID 输出负值代表向左转正值向右转配合baseSpeed加减形成差速丢线时先刹车 200ms 再重新接收数据能防止小车冲出去太远。如果你用的是 STM32思路完全一致只是把 HAL 库的HAL_UART_Receive_IT和定时器 PWM 输出替换进来即可。核心的 PID 算法不需要改动。6.3 PID 参数初调方法PID 调参是巡线项目里最消耗耐心的环节但有一套还算可靠的经验顺序先把Ki和Kd设为 0只保留 P 项。从小数值开始比如 0.2观察小车是不是来回摆动如果小车在直线上左右摇摆说明 P 太大减小 P如果小车过弯时明显跟不上回的太慢可以适当增加 P但幅度不要一次超过 20%当直线行驶基本稳定后再开始加 Kd 来抑制过冲Kd 的初始值可以从 Kp 的 5 倍左右试探最后才考虑加 Ki。对巡线小车Ki 不是必需的定得不好反而会让小车越来越偏向一侧。调参时务必遵循“每次只改一个参数”的原则同时记录当前数值和车的表现否则很难定位问题。7. 运行验证与调参实录代码写完不代表小车就能跑。下面的顺序是我在调试开源小车时习惯采用的流程能帮你快速定位问题出在感知层还是控制层。7.1 先验证 OpenMV 输出再让电机转起来把 OpenMV 用 USB 连到电脑打开 IDE 的串口终端观察画面和串口输出。先用手遮住黑线、移开黑线确认角度输出是否随线的位置变化而变化。如果这一层都不稳定后面的 PID 调参毫无意义。判断标准很简单线在画面中间角度接近 0线在画面右侧偏转角度为正值线在画面左侧偏转角度为负值线完全移出视野输出lose。如果角度输出方向和实际相反说明你把左右方向搞反了。解决方式是调整 OpenMV 的安装方向或者在主控代码里把turn的符号翻转。7.2 空载测试电机差速不把小车放地上把车轮悬空。运行主控代码观察左右轮转速是否随角度变化而变化。可以用Serial.print打印 PWM 值来确认。常见问题是左右电机转速不一致可能是电源电压不足也可能是地面摩擦不同导致负载不同。最简单的缓解办法是给左右电机分别设置基准速度偏置而不是让两者完全一样。7.3 上地低速测试第一次上地测试建议把baseSpeed设到 40 左右满档大概是 255这个值只相当于 15% 左右的占空比。先跑一段直道观察小车有没有抖动、有没偏离。直道稳定后再在赛道里放一个缓弯看它能不能顺利通过。如果过不了弯先检查是“线突然丢了”还是“转向不足”。前者要去改 OpenMV 的阈值和 ROI后者才是 PID 参数的问题。7.4 效果判断标准一个调得比较好的巡线小车应该具备这样的表现直道上保持稳定前进不出现明显的左右摆动缓弯能平滑切过不会冲出赛道偶发断线时能依靠当前角度偏差和惯性短时间维持方向全油门到底时系统依然可控。这里说的“全油门”对于开源小车来说往往不现实。真正的比赛级巡线要在机械结构、电机响应、视觉前瞻上做大量优化不是简单改 PID 就能实现的。对初学者来说先追求低速稳定再逐步提速是更现实的目标。8. 常见问题与排查思路巡线小车跑不起来时问题往往藏在细节里。以下是我在实际工程中遇到最多的几类问题整理成表格方便查阅。问题现象可能原因排查方式解决方案串口乱码或收不到数据TX/RX 接反、未共地、波特率不一致检查接线确认两端波特率一致交叉接线确认 GND 相连统一波特率OpenMV 画面过暗或过亮曝光参数不适合当前环境打开 IDE 实时图像查看亮度关掉自动曝光手动设置曝光时间黑线识别不稳定阈值标定不合适在 IDE 阈值编辑器里观察黑白区域重新在现场标定阈值小车直线摇摆P 值过大观察小车运动幅度减小 Kp先保证直道稳定过弯时冲出赛道前瞻不够或转向不足观察出弯时是否丢失线增大 ROI 高度提高 Kp降低车速停顿时小车一直朝一侧转电机基准速度不一致打印 PWM 值看左右轮输出给电机增加速度偏置偶尔丢线后直接停车丢线处理逻辑太激进检查丢线时的刹车逻辑增加“持续丢线 N 帧才刹车”的逻辑电池电压下降后性能下降电源管理不足测量电机端电压更换稳压模块或加装电源模块丢线处理值得单独强调。在简单的循环代码里一旦get_regression()返回None程序就刹车这会导致小车在遇到短线障碍、反光噪声时频繁顿挫。更稳妥的做法是维护一个“连续丢线计数器”只有连续 5~10 帧找不到线才判定真正丢线期间保持上一次的控制量输出。# OpenMV 侧连续丢线计数伪代码 lose_count 0 while True: line img.get_regression(...) if line: lose_count 0 # 正常发送角度 else: lose_count 1 if lose_count 8: uart.write(lose,%d\n % frame_id)这个细节不复杂但对稳定性的提升非常明显。9. 工程化建议与后续进阶方向巡线小车跑通只是开始把它当成一个工程系统来打磨才能真正积累经验。以下几项是我认为值得投入时间的点和可以继续深入的技术方向。9.1 优先固定摄像头视野与曝光参数OpenMV 侧最容易踩的坑是环境变化导致识别失效。解决这个问题的最简单手段就是把曝光、增益、白平衡全部关闭在场地实际光照下标定一次阈值。如果场地光照会变化可以定期重新标定也可以后续引入“自动寻找背景亮度”的逻辑但那属于更进阶的内容。安装摄像头时要保证画面里只有赛道和路面不要把远处的墙壁、标志牌拍进来。ROI 区域也应尽量只覆盖实际赛道范围。这样既能减少干扰也能提高帧率。9.2 串口协议尽量带帧头、帧尾和校验很多调试点位到后面会发现问题出在数据错乱上。与其事后排查不如一开始就设计一个简单的协议格式。比如0xAA 0x01 angle_high angle_low checksum 0x55或者继续用文本协议但每个字段用分隔符隔开并在末尾加一个校验字段。文本协议的好处是调试直观坏处是解析稍慢但 115200 波特率下传输几个字符的文本协议完全够用。9.3 先机制后参数做任何机器人项目都应该先确认“机制正确”再调“参数最优”。你永远无法用一个合理的 PID 参数去弥补一个错误的方向符号或者一个漏发的串口帧。在调试时先把代码里的数据结构、方向符号、协议解析全部确认一遍再上地调参会省非常多的时间。9.4 从小车控制扩展到完整机器人开发巡线本质上是一个最简单的移动机器人问题。把这条链路走通之后后续的扩展方向至少有这几个赛道地图构建让小车记录路径分段规划最优速度曲线多传感器融合视觉 灰度传感器 编码器互相校正转向控制优化在 PID 基础上引入模糊控制或 LQR 控制目标检测与追踪用 OpenMV 的色块识别实现追踪移动目标接入 ROS 或 ROS 2把感知、决策、控制在软件架构层面解耦为更复杂的机器任务做准备。这篇文章的内容到这里算是把 OpenMV 巡线的整个闭环讲完了。你不需要在第一次调试时全部做到完美只需要让小车成功在赛道上跑完一圈然后把那些“差点能跑”的失败场景记录下来。这些记录比任何标准答案都更值钱。下一次当你再遇到“机器人走不直”的问题时你会比今天的自己多一层判断力。
返回列表