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

资讯详情

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

无人机循迹与图像识别:STM32+OpenMV串口联调核心解析

无人机循迹与图像识别:STM32+OpenMV串口联调核心解析 简介这是面向电赛无人机赛项的完整工程资料包聚焦循迹、图形与颜色识别、串口通讯三大核心功能适合参加电赛或有无人机自动驾驶学习需求的选手与开发者。工程包含19个文件共15KB以Python源码为主辅以XML配置、pyc编译文件及README说明其中main.py、imgprocess.py、uart.py等分别承担主流程、图像处理和串口通信任务结构紧凑便于快速移植到自己的飞控或视觉方案中。已有558人学习下载具备一定参考价值。资料不仅覆盖SLAM循迹、OpenCV图像处理、HSV颜色阈值筛选、UART通讯以及PID姿态控制等关键知识点还给出了智能避让规划的设计思路能够帮助读者梳理从传感器数据到飞行控制的完整链路适合在备赛阶段对照阅读、二次开发。1. 电赛第二次积分赛无人机真正要做的不只是飞起来在积分赛题目里看到“无人机”三个字很多人第一反应是先把姿态稳下来、把电机转起来。等你进场熟悉规则后才会发现裁判真正盯着的是无人机能不能沿着黑线稳定循迹在目标区识别出指定图形和颜色再把结果通过串口通讯报给上位机。整个项目因此被拆成三条主线五路循迹传感器解决“线在哪”OpenMV 或摄像头解决“图形和颜色是什么”STM32 串口解决“告诉谁”。这三条线分别涉及不同的调试方法硬件布局一错、串口协议一乱赛场上就会连环翻车。我一般会先把架构拆明白再让每个功能各自跑通最后才合到一起联调。2. 无人机循迹与图形识别系统架构和硬件分工不要指望一颗摄像头把循迹和图形颜色识别全包。赛场上阳光、反光、地面纹理和曝光变化都会影响摄像头对黑线的判断而五路灰度传感器对黑白的响应几乎是瞬时的。所以常见的做法是“灰度传感器做运动控制视觉做事件识别”STM32 通过 ADC 读循迹传感器算偏差控制电机转速OpenMV 单独负责识别图形和颜色通过串口把结果报给 STM32。两边互不占用算力也方便在电赛现场单独调试哪个环节出了问题就只改哪一块。2.1 五路循迹传感器在无人机上怎么安装才不飘循迹小车通常用三路但无人机由于机架震动和气流扰动会轻微漂移建议直接用五路。五路循迹传感器的优点在于中间三个传感器可以判断线的粗略走向外侧两个做保护性修正即使机身有一点横向漂移丢线之后也能根据两侧传感器找回黑线。安装时传感器离地高度要控制在 8 到 15 毫米太近会把地面颗粒误判成黑线太远则因红外反射光减弱而读数不稳定。传感器方向要严格垂直于前进方向左右间隔均匀这正是后面偏差计算的基础。2.2 图形和颜色识别用 OpenMV 还是普通摄像头普通摄像头加 OpenCV 在电脑上跑完全可行但赛场上没人会背着电脑让无人机飞到跟前再回传图像。更稳妥的是 OpenMV H7 或类似的嵌入式视觉模块它能在器件内部完成图像处理并直接通过 UART 发送结果。若只有普通摄像头可以把树莓派 Zero 放在机载端用 OpenCV 从 CSI 口读图像不过对积分赛这种时间紧凑的场景OpenMV 的上手成本明显更低。颜色识别和图形识别的核心是图像分割不是在原图上逐像素找规律所以先把思路放在“找到目标颜色区域”再谈“区域是什么形状”。2.3 硬件连接和引脚定义串口通讯之前先理清物理链路主控选型我一般推荐 STM32F407带多个 UART 和 ADC可以同时挂循迹传感器和 OpenMV。OpenMV 占用一个串口无线图传或串口透传占用另一个串口四个电机 PWM 再从定时器输出。接线原则是模拟信号尽量远离电机电源线否则 ADC 采集到的循迹值会随油门变化跳来跳去。// line_sensor_pins.h // 五路循迹传感器接在 ADC1 的 PC0~PC4 #define LINE_SENSOR_LEFT_OS ADC_CHANNEL_10 // PC0 #define LINE_SENSOR_LEFT_IN ADC_CHANNEL_11 // PC1 #define LINE_SENSOR_CENTER ADC_CHANNEL_12 // PC2 #define LINE_SENSOR_RIGHT_IN ADC_CHANNEL_13 // PC3 #define LINE_SENSOR_RIGHT_OS ADC_CHANNEL_14 // PC4 // OpenMV 串口USART3波特率 115200 #define VISUAL_UART USART3 #define VISUAL_UART_BAUD 115200 // 电机 PWMTIM2 的 PA0~PA3 #define MOTOR_FRONT_LEFT TIM_CHANNEL_1 #define MOTOR_FRONT_RIGHT TIM_CHANNEL_2 #define MOTOR_REAR_LEFT TIM_CHANNEL_3 #define MOTOR_REAR_RIGHT TIM_CHANNEL_4这段代码定义的是一个典型的硬件拓扑5 个 ADC 通道负责循迹1 路 UART 接收视觉识别结果4 路 PWM 构成 X 型四旋翼的电机输出。引脚名字里的 OS 表示外侧IN 表示内侧。循迹判断不是直接使用原始 ADC 电压而是先用阈值把每个通道变成 0 或 1黑线反射红外光弱数值低白色地面反射强数值高。阈值定了之后再把 5 个通道组合成一个线位置值这一步放到下一章展开。在这个硬件布局里OpenMV 和 STM32 之间最好用 3.3V 电平的 UART 直连双方电源共地。若现场需要远距离调试也可以给视觉模块加 RS485 接口使用 RS485 串口通讯时需要在软件里增加收发切换逻辑比赛时我建议主场直接用 TTL减少排错复杂度。3. 循迹算法五路传感器的阈值提取与转向控制循迹不是让无人机“看到线”就能走直线。真正决定场上是否稳定的是把五路传感器的模拟值变成偏差量再通过 PID 给出转向量。这一节只讲循迹支路不牵扯视觉识别。3.1 动态阈值让循迹不再怕光线变化每个传感器都要独立设置阈值。最简单的方法是随机上电时读一遍白底值再在胶带正中读一遍黑线值取中间数作为阈值。但比赛现场的灯光从上午到下午会变若只做固定阈值下午阳光一斜阈值就失效。常见的做法是每次上电后先让无人机静止在白色地面上程序自动采集前 100 次 ADC 采样的平均值作为白色基准再根据黑白对比度生成阈值。uint16_t white_base[5]; uint16_t threshold[5]; void calib_threshold(void) { for (uint8_t i 0; i 5; i) { white_base[i] 0; } for (uint8_t n 0; n 100; n) { for (uint8_t i 0; i 5; i) { white_base[i] adc_read(i); } osDelay(2); } for (uint8_t i 0; i 5; i) { white_base[i] / 100; threshold[i] (uint16_t)(white_base[i] * 0.5f); } }这段代码的前提是黑线的 ADC 读数显著小于白底读数所以阈值取白色基准的一半。若你的传感器输出相反把比例系数改成 1.2 或 1.5 即可。校准完成后每个通道用adc_read(i) threshold[i]转成 0 或 1。如果五个通道全是 0说明无人机在白色区域或者机身已经偏离黑线超过两个外侧传感器的距离。3.2 加权平均法计算线位置把五个二值化值映射成位置的思路是加权平均。左右两边的权重取正负中间权重为零得到的加权和归一化后就是偏差 error。常用的映射是把 5 个传感器排成一条沿横轴的坐标-2000、-1000、0、1000、2000线在正下方时 error 接近 0。int16_t calc_line_error(uint8_t *sensor) { static int16_t weight[5] {-2000, -1000, 0, 1000, 2000}; int32_t sum 0; uint8_t cnt 0; for (uint8_t i 0; i 5; i) { if (sensor[i]) { sum weight[i]; cnt; } } if (cnt 0) { return 0; } return (int16_t)(sum / cnt); }这里 cnt 是被压住的黑线通道数量。比如传感器 2 和传感器 3 同时压线位置等于 (-1000 0) / 2 -500表示黑线偏向左侧误差为负。只压一个传感器时误差就是该通道权重结果直观。算完误差后让转向量跟随误差误差越左横向修正力越偏左。若是地面巡线模式只需要左右两侧动力差若是低空飞行模式还要把误差映射到前飞和偏航。3.3 PID 输出限幅与丢线状态机循迹 PID 的执行周期固定为 10ms用定时器中断驱动。OpenMV 的串口数据只有几十毫秒一帧不能让它反客为主去打断循迹控制。很多队伍的翻车点就是在串口中断里做了太多判断导致 PID 周期抖动。PID 输出要同时做限幅和增量限制避免无人机在弯道上突然给满油门横摆。float pid_update(float error, float kp, float ki, float kd) { static float integral 0.0f; static float last_error 0.0f; integral error * 0.01f; if (integral 300.0f) integral 300.0f; float out kp * error ki * integral kd * (error - last_error) / 0.01f; last_error error; if (out 500) out 500; if (out -500) out -500; return out; }参数上我一般把 kp 设为 12 到 20ki 控制在 0.5 以下kd 预估值 2 到 5具体值要根据机架轴距调整。下面这个表是我联调时判断方向用的参数推荐范围现象kp12~20弯道修不过来时调大过大会左右摆动ki0~0.5直道有稳态偏差时调大调大后注意积分饱和kd2~5输出抖动或过冲时调大过大后会放大噪声判断循迹是否稳定可以在空地上画一个直径 80cm 的圆弧让无人机从入弯点开始跟随。若外侧传感器已经压线但仍没有修正说明 kp 太小若修正动作呈抖动则 kd 太大或者 PID 周期不稳定。4. 图形与颜色识别HSV 分割加轮廓筛选“识别图形及其颜色”在比赛中一般被拆成两步先找到目标颜色的区域再判断这块区域是什么形状。颜色识别最好的起点是 HSV而不是直接用 RGB 像素值。OpenMV 默认的 LAB 色彩空间对光照变化更鲁棒但 HSV 的语义更直观网上查到的很多 hex 颜色对照表也都是 RGB 十六进制你可以先转换成 HSV 再写进程序。4.1 为什么颜色识别要用 HSV 而不用 RGBRGB 的三个通道互相牵制红色 (255,0,0) 和暗红 (100,0,0) 的欧氏距离很远但 HSV 中它们只是 V 值不同色调 H 和饱和度 S 保持不变。比赛现场同一张卡纸随着光照角度变化RGB 读数会漂移HSV 却容易用一个窄范围框住。实际使用时先取目标色卡的十组 HSV 值求并集再给 H 加上正负 6 的裕量S 和 V 直接使用采样范围。目标颜色H 范围S 范围V 范围红色0~10 以及 170~180100~25560~255绿色45~8080~25560~255蓝色100~130100~25560~255以红色圆形为例H 在 0 到 10S 要在 100 以上V 要在 60 以上否则会把暗色阴影误识别成红色。把阈值写进程序后先不要急着识别形状先在串口终端里打印色块质心和面积确认每个目标颜色都能被稳定抓出来。4.2 OpenMV 下识别圆形、正方形和三角形的具体做法OpenMV 的find_blobs能得到每个色块的质心、面积、外接矩形宽高和像素数所以不需要复杂的轮廓算法。图形识别用宽高比加填充密度来区分圆形包围矩形的宽高比接近 1密度接近 π/4 约 0.78正方形密度接近 0.95三角形密度通常低于 0.65。import sensor, image, time, ustruct from pyb import UART sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.set_auto_whitebal(False) sensor.skip_frames(30) uart UART(1, 115200, timeout_char1000) RED_TH (0, 10, 100, 255, 60, 255) GREEN_TH (45, 80, 80, 255, 60, 255) BLUE_TH (100, 130, 100, 255, 60, 255) COLOR_ID {red: 1, green: 2, blue: 3} def classify_shape(w, h, density): ratio float(w) / h if 0.85 ratio 1.15: if 0.70 density 0.88: return circle if density 0.90: return square if density 0.68 and 0.4 ratio 1.6: return triangle return unknown while True: img sensor.snapshot() for th, name in [(RED_TH, red), (GREEN_TH, green), (BLUE_TH, blue)]: blobs img.find_blobs([th], pixels_threshold300, area_threshold300) for b in blobs: shape classify_shape(b.w(), b.h(), b.density()) if shape ! unknown: record ustruct.pack(HBB, b.cx(), b.cy(), COLOR_ID[name]) uart.write(record) time.sleep_ms(10)这段代码把颜色阈值放进列表对每个颜色分别查询色块。pixels_threshold300和area_threshold300用来过滤孤立的小噪点。b.density()是区域内有效像素与外围矩形面积的比。识别结果里同时带有质心坐标和颜色编号主控拿到后可以知道目标出现在画面哪个位置从而决定无人机是否降低高度或悬停。4.3 视觉识别结果和循迹的时序配合视觉识别不需要每帧都处理OpenMV 可以把识别速度控制在 30ms 一帧但 STM32 循迹是 10ms 一圈。若视觉结果到达时无人机已经越过目标区域那个结果就是废数据。所以串口帧里要带上图像处理完成时的顺序号或状态位主控根据当前循迹位置判断是否接收。赛场地图通常在直道后是目标识别区此时无人机应当降低前飞速度给摄像头足够曝光同时暂停循迹 PID 的积分累积否则机身抖动会直接把画面拍虚。5. 串口通讯协议设计让 STM32 和视觉模块讲同一种语言串口通讯如果只是把整条字符串丢过去测试时看着没问题一上赛场就会遇到粘包、丢帧和乱码。原因是无人机电机 PWM 带来的电磁干扰会让 UART 线出现误码尤其线长超过 10cm 时更明显。因此无论 OpenMV 和 STM32 之间是 TTL 还是 RS485 串口通讯都要设计帧格式和校验不能裸发明文。5.1 一个最小可用的串口帧格式帧格式要兼顾解析速度和扩展性。我常用的协议是三字节帧头加一字节长度加数据区加一字节校验0xAA 0x55 0x01 0x04 data0 data1 data2 data3 checksum。高两位固定用来同步长度字段代表数据区字节数checksum 为整帧异或值。字段长度值说明帧头110xAA同步字帧头210x55同步字命令10x01~0x03循迹结果、图形颜色、状态回传长度1变化数据区字节数数据区N变化按命令定义校验1XOR前面所有字节异或命令字段不能省。比赛中给上位机回传的数据可能来自循迹状态也可能来自视觉识别没有命令字段上位机就分不清这帧到底是位置还是识别结果。5.2 STM32 串口中断里的状态机解析STM32 接收时不要一字节一字节地送给主逻辑而是放进缓冲区在中断里做状态判断。下面这个函数只负责判断当前缓冲区是否收齐一帧uint8_t buf[32]; uint8_t rx_index 0; uint8_t data_len 0; uint8_t frame_done 0; void uart_rx_byte(uint8_t byte) { buf[rx_index] byte; if (rx_index 1 buf[0] ! 0xAA) { rx_index 0; return; } if (rx_index 2 buf[1] ! 0x55) { rx_index 0; return; } if (rx_index 4) { data_len byte; } if (rx_index data_len 5) { uint8_t xor_sum 0; for (uint8_t i 0; i rx_index - 1; i) { xor_sum ^ buf[i]; } if (xor_sum buf[rx_index - 1]) { frame_done 1; } rx_index 0; } }这段代码用rx_index记录已接收字符数。收到第一个字节不是 0xAA 就丢弃第二字节不是 0x55 也丢弃第四字节是数据长度等到长度加 5 个字节收齐后做异或校验。校验通过时置frame_done主循环的循迹任务看到这个标志后再取整帧数据。实际工程中还要加最大长度保护防止干扰字节把缓冲区写满。5.3 用 FreeRTOS 把串口解析和循迹任务分隔开无人机上如果跑了 FreeRTOS常见的做法是建立两个任务控制任务优先级 7串口解析优先级 4。串口任务只把完整帧投递到队列控制任务从队列读取视觉结果并执行两个任务共享数组会造成数据竞争。连续发送视觉结果时为了不让旧帧积压我会用xQueueOverwrite而不是xQueueSend保证控制任务拿到的始终是最新识别结果。// FreeRTOS 任务片段串口解析任务 static void visual_task(void *arg) { visual_msg_t msg; for (;;) { if (xQueueReceive(visual_queue, msg, pdMS_TO_TICKS(10)) pdPASS) { control_apply_visual(msg); } } }这样的任务划分能避免视觉结果积压导致 PID 响应落后半个弯道。调参时通过串口把 PID 的 error 和 output 打印出来再画成曲线比盯着无人机猜姿态靠谱得多。下一章就把这套串口联调手段直接落到操作细节上。6. 赛前串口联调用虚拟示波器验证 PID 和识别结果这里直接给赛前最实用的验证手段把循迹 PID 的误差、PID 输出和视觉识别标志放到同一个串口波形里看。不要只依赖现场跑车的肉眼观察也不要用 LED 闪烁代替量化数据。常见的做法是在调试串口里周期发送多通道数据给上位机的虚拟示波器用 Serial_Digital_Scope 一类的工具抓取波形再对比不同参数的曲线形状。6.1 串口曲线要发哪些量最少要发三个变量误差 error、PID 输出 output、识别标志位 recognition。error 反映黑线相对机身的位置output 反映修正力度。假如 error 已经回到 0 附近output 仍持续抖动说明 kd 过大假如 error 长期不归零且 output 已顶到上限说明 kp 不足或丢线状态机没有正确处理。识别标志位可以设成 0 和 1摄像头看到目标时变 1方便确认视觉结果和串口命令在同一帧里到达。# 调试帧复用第5章协议AA 55 cmd len data check import ustruct from pyb import UART uart UART(1, 115200, timeout_char1000) while True: pid_error 123 pid_output 56 recogn_flag 1 data ustruct.pack(hhB, pid_error, pid_output, recogn_flag) cmd 0x06 length len(data) xor_sum 0xAA ^ 0x55 ^ cmd ^ length for b in data: xor_sum ^ b frame bytes([0xAA, 0x55, cmd, length]) data bytes([xor_sum]) uart.write(frame) time.sleep_ms(10)注意这是调试专用帧和 OpenMV 与主控之间的业务帧分开不要混在同一个串口里。校验和计算要和接收端一致。把调通后的串口包录下来存成 .bin 或 .log下次赛前用回放功能重新导入对比不同 PID 参数的曲线走向比临时去现场试飞快得多。6.2 赛前快速判定的三个阈值决赛前我会用下面三个标准做一次快速体检。第一黑线直道段 error 的平均值应小于满量程的 5%第二过弯时 output 的峰值不应超过设定最大输出的 80%并且要在 0.3 秒内回落到接近 0第三视觉识别帧与循迹状态同步后识别标志位应至少在 200ms 内保持稳定不能忽 0 忽 1。如果识别结果一闪而过先把摄像头曝光固定、关掉白平衡再用连续 3 帧相同的形状才输出。整条串口联调链路走完一遍再把无人机放到比赛地图上完整跑一圈循迹、图形颜色识别和串口通讯这三项功能才算真正合到一起。本文还有配套的精品资源点击获取
返回列表