FEATURED · 精选文章

OpenMV光流定点+巡线+直角转弯:室内无人机视觉闭环方案

发布时间 / 2026/9/14 23:38:01
来源 / 创域科博编辑部
栏目 / 资讯中心
OpenMV光流定点+巡线+直角转弯:室内无人机视觉闭环方案 简介基于OpenMV的无人机视觉控制Python源码包面向无人机开发者、竞赛学生以及对机器视觉感兴趣的初学者系统覆盖光流定点、巡线与直角转弯三项核心功能帮助读者快速理解如何借助开源视觉模块实现无人机的实时定位与路径跟随。压缩包内共包含6个文件以四个Python脚本为主辅以说明文档和备份文件压缩包整体大小约16KB结构简洁清晰脚本按巡线、光流加寻线、十字定点、直角转弯等任务拆分方便逐一实验和二次开发。每个脚本完整展现了从摄像头采集图像、识别地面特征、计算光流信息到输出飞行控制指令的过程并针对不同场景进行了专门优化。目前已有126人学习下载特别适合希望快速搭建无人机视觉控制原型或备战相关竞赛的读者。通过学习这些示例能够掌握无人机视觉飞行的代码组织方式与关键算法落地细节为后续自主导航、避障等复杂功能打下基础。1. 为什么「OpenMV光流定点 巡线 直角转弯」是一个整体方案OpenMV光流定点、巡线和直角转弯这组任务的常见使用场景是GPS失效的室内或桥洞环境。飞控用IMU和气压计只能稳住姿态位置环一旦失去卫星信号就会持续漂移姿态再稳也救不回来。把一颗朝下的摄像头装上OpenMV用光流把像素位移积成水平位置偏差再把一个朝前摄像头输出的引导线偏差作为横向约束就构成了一架低成本无人机的最小视觉闭环。直角转弯是这个闭环里的离散事件巡线模式的线消失、方向判断和转弯后恢复必须与连续的光流积分在同一套状态机里协同。适合正在做无人机课设、比赛或PX4/ArduPilot offboard实验的工程师参考代码用MicroPython写能在OpenMV IDE里直接跑通。2. OpenMV光流定点把每秒像素位移积成分米级位置闭环2.1 光流在OpenMV里是两种算法二选一光流在本方案的职责只有一个当无人机在空中平移时地面特征在摄像头图像里产生整体位移把这种二维像素位移换算成机体水平位移。OpenMV在MicroPython环境里提供两种算法find_displacement()采用SAD块匹配或相位相关适合摄像头垂直向下、基本平飞的低速场景match_keypoints()跟踪稀疏特征点适合有倾斜角或纹理稀疏的画面但计算量明显更高。选型理由对于定点悬停我们要的是整体位移不是个别角点的轨迹相位相关对整个画面的平移估计最稳。OpenMV官方光流示例建议把分辨率降到64x32这不是性能妥协而是因为低分辨率能显著降低高频噪声同时把单帧处理时间压到十几毫秒以内保证20Hz以上的输出频率。对比项find_displacementmatch_keypoints算法类型全局平移估计SAD/相位相关稀疏特征点匹配适用姿态近似水平、无剧烈旋转有倾斜、视角变化大返回内容dx/dy像素位移、response可信度匹配特征点列表计算开销低B64X32可达实时高需控制特征点数量本方案选择主用备选如果无人机在室内有偏航旋转图像位移会被角速度污染纯光流无法区分平移和转头。飞控端的EKF里有光流加陀螺仪融合时旋转分量可以消掉如果只是OpenMV单独闭环就只能在姿态稳定后的平飞阶段使用光流积分。这点决定了定点功能的可靠性边界也决定了后续巡线转弯时为什么必须暂停积分。2.2 一张朝下的摄像头固定曝光20Hz的全局位移最小实现是B64X32灰度图关闭自动曝光和自动增益对前后两帧反复调用find_displacement()。为什么必须固定曝光光流对两帧间灰度分布的一致性敏感自动曝光会让明暗突变导致响应值骤降。地面纹理越丰富、灯光越稳response越接近1。import sensor, image, time # 朝下安装的光流模块OV7725传感器适用 sensor.reset() sensor.set_pixformat(sensor.GRAYSCALE) sensor.set_framesize(sensor.B64X32) # 64x32足够光流帧率高 sensor.skip_frames(time2000) sensor.set_auto_gain(False) # 光流最怕增益跳动 sensor.set_auto_exposure(False, exposure_us12000) sensor.set_auto_whitebal(False) # 灰度图仍然关掉白平衡 clock time.clock() old_frame sensor.snapshot() while True: clock.tick() frame sensor.snapshot() d frame.find_displacement(old_frame) # 全局位移估计 dx d.x_translation() dy d.y_translation() resp d.response() # 只有可信帧才参与积分响应值低于阈值的帧直接丢弃 if resp 0.6: print(%d,%.2f,%.2f,%.2f % (time.ticks_ms(), dx, dy, resp)) old_frame frame time.sleep_ms(20) # 强制20ms一个周期约50Hzx_translation()返回的是图像坐标系下的像素位移不是机体位移。以时间戳为最小数据单元输出飞控端才能做同步。time.sleep_ms(20)不是必须但建议加避免while空转造成帧率抖动帧率抖动对积分有直接影响。参数调整要点exposure_us从8000开始调室内灯光暗可以升到15000超过20000后运动模糊会明显增大点也更容易丢。response阈值0.6是一个起点纹理密集地毯、木纹可以放松到0.5纯色地面应提高到0.75以上宁可丢帧也不积分错帧。若dx/dy呈跳跃性正负变换优先检查固定曝光是否生效再检查skip_frames预热时间是否太短。2.3 位置闭环死区、符号与积分重置三个隐藏细节把光流位移积分成位置公式是真实位移(mm) 像素位移 × 当前高度(mm) / 焦距(像素)。焦距通过棋盘格标定获得高度来自飞控的气压计或激光定高。OpenMV本身没有测距传感器这个公式中的高度参数必须由飞控定时下发或用单独的激光测距模块读取。积分代码的关键在死区当abs(dx_mm) 2mm时OpenMV读数大概率是亚像素噪声直接累加会引出缓慢的位置漂移正确做法是低于死区的帧跳过而不是加0。另一个隐蔽问题是方向符号摄像头朝下安装时无人机向右平移画面里的特征向左移动OpenMV返回的位移表示图像内容移动方向与机体移动方向相反。不同安装方向下的符号关系应当在地面测试时确认一次。pos_x 0.0 pos_y 0.0 last_t time.ticks_ms() HEIGHT_MM 800.0 # 相对地面的高度由飞控给定 FOCAL_PX 160.0 # B64X32下等效焦距必须标定 TARGET_X 0.0 TARGET_Y 0.0 DEADZONE_MM 2.0 # 2mm死区防止积分漂移 while True: dx, dy, resp read_flow_once() # 封装2.2节的单帧读取 if resp 0.6: continue dx_mm dx * HEIGHT_MM / FOCAL_PX dy_mm dy * HEIGHT_MM / FOCAL_PX if abs(dx_mm) DEADZONE_MM: pos_x dx_mm if abs(dy_mm) DEADZONE_MM: pos_y dy_mm vx clamp_kp((TARGET_X - pos_x), K0.08, LIMIT0.4) vy clamp_kp((TARGET_Y - pos_y), K0.08, LIMIT0.4) send_pilot(vx, vy, yaw_rate0) time.sleep_ms(50)pos_x是图像坐标系累加出的机体水平位移clamp_kp用一个比例系数把位置误差映射到0.4以内的速度指令再通过串口发给飞控。起降瞬间位置误差会剧烈跳变落地后应重置pos_x/pos_y为0否则下次起飞定位环会带着旧偏移。K取0.08适合光流输出频率20Hz左右的场景调参顺序是先去掉积分环节只调比例系数K等悬浮不震荡后再加微分。飞控端如果有PX4/ArduPilot的光流加超声波EKFOpenMV可以只发原始dx/dy若飞控没有位置融合才由OpenMV直接跑位置PID。串口波特率建议至少115200数据帧里带简单校验值。3. 巡线的Python实现从LAB阈值到偏差输出3.1 选LAB而不是RGB光照鲁棒性的关键巡线需要一个结论RGB阈值在OpenMV里几乎不可用。RGB的R/G/B三个通道都混有亮度信息同一条黑线在灯光亮暗变化时RGB分量会整体飘LAB把亮度单独放进L通道颜色信息集中在A红绿和B蓝黄轴。黑色引导线的检测只需要关注L通道的低值A/B通道放全范围放过即可。对室内浅色地板上的黑色胶带推荐阈值是[(0, 40, 80, -80, 80, -80)]含义L通道0到40黑线A通道-80到80全部接受B通道-80到80全部接受。这样能天然忽略颜色较浅的影子因为阴影的L值通常在中段不会落入0-40区间。3.2 find_blobs把引导线当成最大色块来用最常见也最稳的做法是用find_blobs找出所有符合黑色阈值的连通域取面积最大的那个作为引导线。import sensor, image, time sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) # 320x240巡线分辨率要高于光流 sensor.skip_frames(time1000) sensor.set_auto_whitebal(False) # 前视画面固定白平衡避免色温漂移 THRESHOLD_BLACK [(0, 40, 80, -80, 80, -80)] BLOB_MIN_PIXELS 60 # 小于60像素的噪点直接忽略 while True: img sensor.snapshot() blobs img.find_blobs( THRESHOLD_BLACK, pixels_thresholdBLOB_MIN_PIXELS, area_threshold200, mergeTrue ) if blobs: blob max(blobs, keylambda b: b.area()) error blob.cx() - img.width() // 2 img.draw_cross(blob.cx(), blob.cy()) print(%d,%d,%d % (time.ticks_ms(), error, blob.area())) else: print(%d,line_lost,0 % time.ticks_ms())pixels_threshold是最小像素数低于该值的噪点排除area_threshold是最小面积阈值mergeTrue允许相邻色块合并防止线中间有高光缺口时被拆成两段。max(blobs, keylambda...)取最大色块在T形路口和多段线场景里必须保留这一句否则OpenMV默认返回的是列表中第一个色块而第一个色块可能是远处的小断点误差一下拉大。error是色块中心相对画面中心的像素偏差。负值表示线在左正值表示线在右直接把它送给飞控或底盘做转向。面积值blob.area()不要丢弃它在直角转弯判断线消失时是关键特征。3.3 线太细、反光断线时换find_lines替代当黑色引导线是2cm以下的细胶带俯视画面里线宽可能只有6像素左右色块面积和形状都不稳定find_blobs会受到地板纹理干扰。这时改用霍夫直线检测find_lines检测对象是边缘梯度不依赖连通域面积。lines img.find_lines(threshold1200, theta_margin15, rho_margin15) best_line None for line in lines: angle line.theta() # 角度单位度0为水平 rho line.rho() # 原点到直线的距离 if abs(angle - 90) 20: # 90度表示与画面纵向轴一致 if best_line is None: best_line line elif abs(line.rho() - img.width() // 2) abs(best_line.rho() - img.width() // 2): best_line line if best_line: error best_line.rho() - img.width() // 2 else: error Nonefind_lines返回的theta()是直线法线角度90°表示与画面纵向轴一致rho()是直线到画面中心的距离偏差越大说明航线偏离越多。threshold是累计投票门限线越明显、边缘越锐利threshold要得越大室内白墙反光比较强时建议从1600开始往上调。场景推荐算法关键参数起步值3cm以上黑胶带find_blobspixels_threshold60, area_threshold200细线加强反光find_linesthreshold1200, theta_margin15无规则纹理、断线较多find_blobs合并mergeTrue, pixels_threshold降到30曲线半径小的弯find_linesthreshold1000, theta_margin10选型结论普通黑胶带、墨绿色地面等连续纹路场景用find_blobs更快且天然带面积和中心点信息与转弯检测衔接更方便细边、反光场景优先find_lines。3.4 偏差到转向量视觉外环与飞控内环的时间间隔分工巡线输出偏差后要把它变成无人机的偏航角速度设定值。这里最常见的问题是直接拿error乘以一个很大的Kp导致转向抖动。视觉作为外环输出给飞控内环的应该是期望偏航角速度不直接控制舵机或电机。推荐做法是turn_rate tanh(error / 100.0) * MAX_RATEMAX_RATE设为0.5rad/s用tanh让偏差饱和而不是硬限幅加符号翻转。飞控内环是串级PID中的角速度环频率在100-250Hz视觉外环20Hz已经天然形成时间间隔差确保内环至少比外环快5倍以上否则外环误差生成的目标角速度变化会被内环滞后放大。调参顺序和工业PID一致先比例后微分。若悬停巡线时左右摇摆优先降低Kp而不是加滤波若转弯后回正慢适当减小死区阈值让外环在误差2cm时就恢复输出。4. 直角转弯不靠角点用线消失与方向记忆触发转向4.1 直角在机载视角下不是一个稳定的特征很多初学者会想到用角点检测识别直角在OpenMV上实现也很简单先find_lines再计算两条直线的夹角是否接近90°满足就转弯。但在实际无人机俯视或前下视角中这个方案有三处站不住一是直角在透视投影下不是90°而是受俯仰角和滚转角影响的动态角度二是室内地板纹理、地砖缝隙会产生大量假角点三是无人机转弯时滚转带来的画面旋转会让直线角度短暂抖动角点检测难以稳定触发。更工程化的做法是退一步直角转弯的本质是线突然找不到了而运动到弯心时导引线会滑出画面外侧。所以把问题转化成两个连续事件线消失加方向记忆。判断逻辑从几何识别降级为时间序列判断光照和角度影响都小得多。4.2 用线消失法触发直角转弯完整状态机片段线消失的逻辑是巡线状态下连续丢失N帧视为已经到达弯道口。方向记忆则依赖丢失前的平均error线偏左丢则左转线偏右丢则右转。STATE_LINE 0 STATE_TURN 1 state STATE_LINE lost_count 0 turn_dir 0 # 1 右转, -1 左转 last_errors [] TURN_DURATION_MS 2200 # 90度转弯时间由偏航角速度估算 while True: t time.ticks_ms() blob get_line_blob() # 封装3.2节的find_blobs结果 if state STATE_LINE: if blob is not None: lost_count 0 err blob.cx() - SENSOR_WIDTH // 2 last_errors.append(err) last_errors last_errors[-15:] # 只留最近0.3s的误差 send_turn_rate(tanh(err / 100.0) * 0.5) else: lost_count 1 if lost_count 5 and len(last_errors) 5: avg_err sum(last_errors) / len(last_errors) turn_dir 1 if avg_err 20 else -1 state STATE_TURN turn_start t send_turn_rate(0.8 * turn_dir) # 0.8rad/s转弯 elif state STATE_TURN: if time.ticks_diff(t, turn_start) TURN_DURATION_MS: state STATE_LINE lost_count 0 last_errors [] reset_position_integral() # 转弯结束必须重置光流积分 else: send_turn_rate(0.8 * turn_dir) time.sleep_ms(20)lost_count 5的原因是一两帧丢线可能是色块被临时遮挡或正好撞上高光点连续5帧约100ms丢失才是真正的线消失。last_errors保存最近15帧偏差用于判断最后线偏向屏幕哪一侧。注意转弯结束后调用reset_position_integral()这一步直接决定转弯后定点能否接得上因为转弯期间画面在旋转光流完全不可信。参数起步值说明lost_count阈值5帧约100ms太低了误触发last_errors长度15帧约0.3s方向记忆avg_err方向阈值20px小于20px按线居中处理TURN_DURATION_MS2200用yaw_rate0.8rad/s测量实际角度修正4.3 十字路口与转弯回正的两个易错点第一个易错点是十字不等于直角。在十字路口前方还有线lost_count不会增长色块中心会突然从一边跳到另一边。识别方式是检测偏差符号翻转若连续帧中err的符号从正变负且绝对值都大于画面宽度的1/4说明穿过十字。处理方法保持直行0.3秒再看两侧线状态如果又恢复巡线就继续走。第二个易错点是转弯结束的恢复条件。理想情况下飞控转过90°后前视摄像头应该重新找到线但由于角速度过快或转向不足重新找到的可能仍然是弯道内侧。实现时在转弯结束后加一个1.2秒直线搜索窗期间暂停光流积分如果连续40帧都找不到线则按同方向补转一个小角度例如0.15秒的补充偏航然后再进入巡线。若目标是沿固定几何轨迹飞完整个任务弯道之间通常是直线直角转弯后需要重新与引导线对齐。转弯结束后的第一秒误差可能很大直接把PID的误差上限放宽1.5倍可以让无人机先靠近线而不是立刻剧烈转向。5. OpenMV状态机、曝光与离线回放视觉闭环收口的细节5.1 把定点、巡线、转弯收进同一个状态机把三部分收进一个主循环用状态机切换。常见状态是STOP / HOVER / LINE / TURN / LOST其中HOVER就是光流定点LINE是巡线与光流同时工作TURN只执行转弯光流积分暂停LOST超过1.5秒进入保护性悬停等待遥控干预。状态切换时的积分重置遵循三条规则进入TURN前保存当前位置禁用位置积分TURN结束重置位置累加值到保存值视觉外环增益降低30%持续0.5秒从LOST恢复清空光流与巡线的历史队列强制重新找线。5.2 三个最容易翻车的参数曝光、白平衡、低通滤波现象根因处理光流response长期低于0.5曝光时间过长或自动曝光开启固定exposure_us回到8-12ms档巡线色块忽大忽小自动白平衡跟随色温set_auto_whitebal(False)定点漂移方向固定积分符号反了或死区过小地面测试反转dx/dy符号转弯后无法接上巡线转弯期间光流积分未暂停进入TURN前显式关闭积分还有一个容易被忽视的是帧率。OpenMV IDE调试时会拉低帧率因为图像传输占用了大量USB带宽实际装机调试应拔掉IDE改用串口日志输出关键变量帧率才能体现出真实25-50Hz性能。5.3 离线回放技巧阈值不用上机调日常调优建议用OpenMV IDE录制一段飞行视频把视频导出成连续帧在PC上用Python脚本按同样的阈值和转弯逻辑做离线回放。这样参数调整完全不占飞行时间还能对比不同曝光参数下同一段路径的检测结果。回放脚本只负责逐帧调用find_blobs和find_lines把错误帧号打印出来。绝大多数阈值、死区和N帧计数问题都能在离线阶段被发现余下的才是光照突变、强烈震动等硬件相关的问题。离线回放只能收敛参数机上仍需看串口里的三个量response、丢失帧计数、光流积分累计值。把这三个量以固定频率写入日志飞行结束后对比转弯触发时刻是否与路线形状吻合能快速定位是检测问题还是执行问题。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻