简介:一套面向ROS无人机二维码精准降落场景的V2版本代码包,适合熟悉ROS基础、正在调试自主起降功能的开发者与研究者。其核心改进在于降落过程中持续进行无人机位置动态调整,避免二维码脱离视野后出现偏移;针对摄像头盲区,可设置离地0.6米等合适高度再执行最终降落,同时依据相机视角与安装位置灵活调整判定高度。压缩包共27个文件,大小约970KB,涵盖launch启动文件、yaml参数配置、c++源代码、lua脚本、pgm地图及相机驱动等类型,资源内还划分了视觉追踪、飞控启动、相机接入等多个功能模块,便于按需替换和快速定位修改点。已有797人学习下载。使用者可得到一套完整的二维码降落代码骨架,并能够参照其中启动、参数和驱动配置,将方案迁移至自己的无人机平台,依据实际机型和传感器情况微调高度阈值。
1. V2版本做了什么:把“能降”改成“降得准”
第一次拿到这个ROS无人机二维码降落项目时,我其实是不太信的:二维码降落听着不难,不就是飞控收到视觉定位然后落下去吗?真正跑过一遍才知道,V1版本能做到“落在二维码附近”,V2版本才敢说“落在二维码正中”,而且是在降落过程中根据相机画面实时修正位置,不是预先算好一条固定航线。这个项目适合两类人:一是想给无人机做视觉精准降落却总被偏移问题折磨的开发者,二是做ROS机器人课设或竞赛、需要一个能落地的视觉闭环参考的初学者。整个工程把图像采集、二维码识别、坐标变换、位置修正、飞控通信串成了一条完整的控制链路,我拆完以后最大的感受是:这项目真正值钱的部分不是识别,而是“边降边修”的那套动态调整逻辑。
2. 系统架构拆解:消息链路与节点分工
2.1 硬件分层与话题拓扑
这套系统从硬件上分成三层:底层是飞行控制器,负责姿态控制和电机输出;上层是一台机载计算单元,跑ROS节点和视觉算法;中间通过串口或USB转串口连接,走MAVLink协议,由飞控桥接节点负责转发。视觉部分是一台朝下安装的单目相机,常见装法是固定在机架底部云台板上,光轴尽量垂直于机体平面。除了相机,项目还保留了一路测距传感器接口,用于低空补高度数据,因为二维码解算出来的高度在低于0.5米后误差会明显放大。
ROS侧的话题拓扑其实不复杂,核心链路是单向的:相机驱动节点发布原始图像,视觉检测节点订阅图像后发布二维码位姿,解算节点把视觉位姿换算成机体坐标系的偏移量,控制节点读取偏移量并输出位置设定点,最后通过飞控桥接发送给底层飞控。我在拆代码时观察到,很多偏移问题不是控制不给力,而是前面某一环的坐标系方向搞错了。V2版本在这条链路上加了一个强制约定——所有中间话题统一使用机体前置坐标系,视觉位姿一旦进入控制节点就必须先被转到这个坐标系。
2.2 核心消息链路的节点与话题
下面这张表格把我在工程里看到的节点和话题整理了出来,方便你对照自己的工程修改:
| 节点名称 | 订阅话题 | 发布话题 | 作用 |
|---|---|---|---|
| 相机驱动 | 无 | /camera/image_raw | 输出原始图像帧 |
| 视觉检测 | /camera/image_raw | /qr/detected_pose | 检测二维码角点和中心点 |
| 坐标解算 | /qr/detected_pose | /qr/body_offset | 把像素坐标换算成机体偏移 |
| 位置修正 | /qr/body_offset, /mavros/local_position | /mavros/setpoint_position | 生成动态降落目标点 |
| 飞控桥接 | /mavros/setpoint_position | MAVLink指令 | 把设定点发给飞控 |
这套链路里最容易被忽略的是飞控桥接节点的消息频率。位置修正一般跑10Hz到20Hz,视觉检测最多跑到30Hz,但飞控的本地位置话题往往有50Hz左右,如果直接订阅转发的设定点不设缓存队列,很容易丢消息,表现为无人机修正动作一顿一顿的。我看工程里所有订阅端都把队列长度设成了1,宁可拿到最新一帧丢弃旧的,也不要把视觉滞后帧累积起来。
2.3 与V1版本相比改了哪三处
V1版本常见的设计是:起飞后先飞到二维码正上方某个预置点,然后垂直下降,下降过程中不处理视觉反馈。问题很明显,只要有风或者预置点没打准,飞机落地时就是歪的。V2版本的改动集中在三处。
第一处是把“目标点”从静态变成了动态。控制节点不再使用下采样时的目标点,而是以二维码解算出来的位置为基准,每收到一帧视觉反馈就重新计算一次目标点,再叠加飞控本地位置做限幅。第二处是加了降落状态机。V1把所有高度段混在一起控制,V2则把降落过程拆成“高位粗对准”和“低位精对准”两个阶段,不同阶段用不同的修正系数。第三处是统一坐标系。V1的视觉方向经常因为相机安装角度问题产生偏置,V2在启动时用TF静态变换把相机坐标对齐到机体坐标,后续所有解算都基于这个标定值,而不是在代码里反复手动改符号。
3. 二维码坐标解算:从一帧图像到机体偏移量
3.1 图像订阅、转换与预处理
视觉解算第一步是把ROS图像消息转成OpenCV能处理的格式,这个环节我用的是cv_bridge。相机发布的图像通常是原始BGR或灰度图,而二维码检测更适合直接处理灰度图,所以我在回调函数里做了一个判断:只在图像编码变化时才转换,避免每帧重复分配内存。
import cv2 import numpy as np from cv_bridge import CvBridge from sensor_msgs.msg import Image bridge = CvBridge() image_msg = None def image_callback(msg): global image_msg try: cv_img = bridge.imgmsg_to_cv2(msg, desired_encoding="bgr8") except Exception as e: rospy.logwarn("图像转换失败: %s", e) return gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) # 这里直接送入二维码检测器,返回角点和中心点 corners, ids, _ = detector.detect(gray)这段代码里需要注意两点。第一,desired_encoding="bgr8"会强制转换格式,如果原图已经是灰度图,这个转换会消耗额外CPU,实际场景中我会改成直接判断msg.encoding,是灰度图就不转。第二,检测器返回的corners顺序不一定一致,二维码的四个角点需要按照固定的左上、右上、右下、左下顺序重排,否则后续坐标解算会出现90度的偏差。这个坑我在第一次试飞时就踩过,后文避坑章节会详细说。
3.2 相机到机体的坐标系对齐
相机安装角度不可能绝对水平,哪怕只偏两度,在3米高度也会带来 10 厘米以上的水平误差。V2项目在启动阶段通过TF静态变换把相机坐标系对齐到机体坐标系,方向由标定值指定,通常写成launch文件参数。
<node pkg="tf2_ros" type="static_transform_publisher" name="camera_tf_broadcaster" args="-0.08 -0.02 -0.15 0.03 0.0 0.0 base_link camera_link" />这行参数的含义是:相机在机体坐标系下的安装位置是x方向负8厘米、y方向负2厘米、z方向负15厘米,旋转量是绕x轴0.03弧度。实际安装时,我会先量出相机光心相对飞控机体质心的距离,填进前三个数字;旋转量则需要通过试飞校准,简单做法是让飞机悬停在一个已知二维码上方,调整旋转量直到视觉输出与本地位置偏差一致。这个步骤虽然麻烦,但能省掉后面所有控制代码里的“魔法数修正”。
3.3 像素坐标转机体偏移
拿到二维码中心点的像素坐标后,需要把它换算成机体坐标系下的水平偏移量。核心公式是利用相机内参和当前高度做投影逆变换,高度可以由测距传感器或视觉估算得到。
def pixel_to_body_offset(cx, cy, fx, fy, height): # cx, cy 是图像中心的像素坐标,fx, fy 是相机内参焦距 # height 是当前高度,单位米 dx_pixel = detection_center_x - cx dy_pixel = detection_center_y - cy offset_x = dx_pixel * height / fx offset_y = dy_pixel * height / fy return offset_x, offset_y说明一下关键参数:fx和fy由相机标定得出,如果拿不到精确内参,可以用标称焦距算,但误差会直接放大到修正量上;height建议优先使用激光或光流测距值,而不是气压计,因为室内环境下气压计高度抖动非常大。dx_pixel和dy_pixel需要注意图像坐标系方向:图像x轴向右,y轴向下,所以在飞控位置控制里,offset_y往往要取反再发给位置修正节点,否则飞机会沿错误方向飞走。V2代码里我见过直接把这两行输出打印出来和遥控器动作对比的调试做法,非常推荐。
4. 动态调整控制策略:边降边修的工程实现
4.1 降落状态机设计
动态调整的核心是降落状态机。V2不再无脑下降,而是把降落过程拆成四个状态:搜索、粗对齐、精对齐、下降。搜索状态下飞机保持当前高度进行小幅平移扫描,直到二维码连续N帧被检测到才进入粗对齐;粗对齐负责把飞机水平位置修正到二维码正上方,但修正系数取得比较大,允许一定超调;精对齐发生在高度低于1米时,修正系数减小,避免低空过度摆动;下降状态则是在水平误差小于设定阈值后启动垂直下降,同时保持视觉修正继续工作。
def descent_state_machine(state, height, offset_x, offset_y): if state == "SEARCH": if detection_count > 5: return "COARSE", None return "SEARCH", hover_target() elif state == "COARSE": if height < 1.0: return "FINE", clamp_target(offset_x, offset_y, max_speed=0.4) return "COARSE", clamp_target(offset_x, offset_y, max_speed=0.8) elif state == "FINE": if abs(offset_x) < 0.03 and abs(offset_y) < 0.03: return "DESCENT", current_target() return "FINE", clamp_target(offset_x, offset_y, max_speed=0.2)这段代码有几个关键参数需要实际调。detection_count > 5是稳定帧数,设太大会让搜索阶段响应慢,设太小容易把误检当成有效目标。max_speed在不同阶段不一样,粗对齐我用0.8 m/s,精对齐降到0.2 m/s,防止飞机在低空来回摆动撞地。0.03是精对齐判定阈值,也就是3厘米,实际验证时如果场地风大,我会放宽到5厘米,不然飞机会一直处于“修正-超调-修正”的循环里下不去。
4.2 位置修正节点的增量代码
动态调整不只是简单地把视觉偏移加到位置设定点上,那样会引入一个固定偏置。V2采用增量式修正,把视觉偏移当作期望位置变化量的一部分,再与飞控本地位置叠加。
def position_correction_callback(qr_offset_msg): global target_pos, last_correction_time now = rospy.Time.now() dt = (now - last_correction_time).to_sec() last_correction_time = now if dt <= 0 or dt > 0.5: return # 视觉偏移是相对量,叠加到上一次目标点上 target_pos.x += qr_offset_msg.x * k_visual target_pos.y += qr_offset_msg.y * k_visual # 限幅,防止单帧修正量过大导致震荡 max_step = 0.15 * dt target_pos.x = clamp(target_pos.x, target_pos.x - max_step, target_pos.x + max_step) target_pos.y = clamp(target_pos.y, target_pos.y - max_step, target_pos.y + max_step) # 高度直接读取测距值,不做增量融合 target_pos.z = current_height() send_setpoint(target_pos)k_visual是视觉修正系数,V2默认值是0.6,表示视觉偏移只按60%比例叠加。为什么不是1.0?因为视觉检测本身有噪声,完全跟踪会把噪声也放进去,飞机就会表现出高频抖动。max_step限制的是单次修正步长,0.15乘以时间间隔意味着每秒钟最多修正15厘米,这个值在室内足够,在室外有风的环境可以增大到0.25。还有一点很容易忽略:dt必须做上限保护,如果视觉线程卡顿超过0.5秒,这一帧直接丢弃,而不是用旧数据继续修正,否则飞机会被一个突然出现的旧偏移量带走。
4.3 参数表与调参顺序
下面是我从工程里整理出的关键参数表和推荐调参顺序,方便你直接对照检查。
| 参数名 | 默认值 | 作用 | 调参建议 |
|---|---|---|---|
| k_visual | 0.6 | 视觉修正叠加比例 | 先0.4试飞,稳定后逐步加大 |
| max_step | 0.15 | 单次最大修正步长 | 风大时上调到0.25 |
| detection_count | 5 | 进入粗对齐的稳定帧数 | 误检多就加到8 |
| coarse_speed | 0.8 | 高位粗对齐最大水平速度 | 场地小就降到0.5 |
| fine_speed | 0.2 | 低位精对齐最大水平速度 | 太低会延长下降时间 |
| align_threshold | 0.03 | 精对齐完成判定阈值 | 风大环境放宽到0.05 |
我个人的调参顺序是固定顺序,先把align_threshold放到最大,让飞机能正常降下去,再逐步缩小;然后调k_visual,同时观察飞机在悬停时有没有明显抖动;最后才碰max_step。如果发现飞机总朝一个方向偏,不要先调参数,先回去检查坐标系和像素方向,参数调不出系统性问题。
5. 避坑记录:二维码降落试飞中的四个坑
5.1 低空二维码突然出画
现象:飞机高度降到0.8米以下后,二维码明明在正下方,视觉话题却突然丢失,修正停止,飞机带着侧向偏移继续下降。
原因:相机是固定视角,底部相机视场角一般在60度到90度之间,高度越低,地面可视范围越小。二维码尺寸如果偏小,在低空时反而会超出画面边界,或者因为太靠近图像边缘产生畸变导致检测失败。
解决:V2里我加了一个高度切换逻辑——低于0.8米后不再依赖单目视觉做水平修正,而是把最后一次有效的偏移量保持为恒定目标,同时降低下降速度。如果场地条件允许,把二维码尺寸做大一点更直接,我实测过,40厘米见方的二维码比25厘米的在低空存活率高很多。
5.2 逆光和反光导致检测边抖动
现象:二维码贴在有反光的亚克力板上,或者光线从侧面照射时,检测框在图像里一跳一跳的,位置修正也跟着来回摆。
原因:二维码边缘角的二值化受光照影响很大,顺光时黑色块对比度高,逆光时黑色块可能被识别成灰色,角点提取就会漂移。
解决:V2代码里的做法是预处理阶段先用自适应阈值替代固定阈值,保证每个局部窗口的对比度都被拉伸。另一个很实用的办法是换哑光材质打印二维码,避免塑封膜。飞行方向也尽量安排成顺光,如果场地允许,把太阳位置放在机尾方向,这个问题会在根源上缓解。
5.3 落地瞬间飞控提前切了电机
现象:降落过程看起来一切正常,二维码对准了,但飞机刚接触地面,电机突然停转,机身直接砸地,甚至弹起来。
原因:飞控自带的落地检测通常基于油门输出和高度变化率,视觉降落时水平方向非常稳定,垂直速度又偏慢,一些飞控会把这种缓慢下降误判为“已经被卡住”,从而提前触发解锁和切油。
解决:我给降落指令加了一个最低持续时间保护,在状态机进入下降状态后,即使收到落地信号,也至少保持油门输出0.5秒再执行切油动作。这算是一个比较保守的处理方式,后续如果想要更顺滑着陆,可以改为检测电机电流突变来辅助判断真实触地。
5.4 坐标系方向反了,飞机越修正越偏
现象:飞机悬停在二维码正上方,视觉显示偏差为负,但修正后飞机往反方向移动,而且每次修正都让偏差变大。
原因:像素坐标系和机体坐标系的y轴方向没有对齐。图像y轴向下,机体坐标系y轴通常指向机头左侧或右侧,如果直接把像素偏差值发给位置控制器,就等于把一个反向的值送进闭环。
解决:在坐标解算函数里手动加了一次符号变换,视频画面的“右”和机体的“右”必须通过实际拨杆确认一致后再写死。我一般会用一个笨办法验证:把二维码放在飞机偏左的位置,看视觉输出数值是正还是负,再决定要不要翻转y轴。这个坑排查起来最耗时,因为代码逻辑看起来完全合理,问题出在物理方向定义上。
6. 二次开发思路:把固定阈值改成自适应策略
6.1 让修正系数随风速自适应
V2默认的k_visual和max_step是固定值,在室内无风环境很好用,但拉到室外就发现修正量不够,飞机被风吹偏后要很久才能拉回来。我建议把这两个值改成根据水平位置误差动态变化:误差大时用大系数快速拉回,误差小时用小系数防止震荡。实现上不需要引入复杂算法,一个线性分段函数就够了,误差超过0.5米时k_visual设为1.0,误差小于0.1米时降到0.3,中间线性插值。注意加上时间平滑,避免系数突变导致控制量跳变。
6.2 增加降落航向自动对齐
原项目只修正水平位置,无人机降落时的机头朝向是固定的。但二维码贴纸不一定和机头方向对齐,如果机头偏了几十度,就算落在二维码中心,视觉上也会觉得“没对准”。我在自己的版本里加了一个偏航修正支路:检测二维码角点后计算两条边的方向角,与当前无人机偏航角做差,得到偏航修正量,叠加进位置设定点一起发送。偏航修正系数一般取0.3左右,且只在精对齐阶段启用,因为高位时角点检测噪声大,修正偏航反而会转来转去。
6.3 验证链路与参数记录习惯
二次开发最忌讳直接改参数上真机。我惯用的流程是先录一段rosbag,包含图像、视觉话题、飞控本地位置和设定点话题,然后写一个简单的离线回放脚本,把修正节点的输入输出打印出来,对比视觉偏移和控制指令是否同向。这个方法能发现大部分坐标系和符号问题。参数记录方面,我每次试飞后会把当天的参数文件拷贝一份,用日期命名保存。到目前为止,我花时间最多的都是坐标系标定,而不是控制算法本身。
从那以后我每次改动视觉相关代码,都会强制走一遍“录包回放验证”流程,哪怕只是改一个阈值参数也照做,这套习惯替我省下了不少试飞时间。希望帮到你。
本文还有配套的精品资源,点击获取