☰
ROS无人机物体识别与精准投放:从YOLO感知到MAVROS执行的全链路解析
2026/10/29 1:09:58 网站建设 项目流程

简介:面向ROS无人机开发者、高校课程设计与智能竞赛团队,这份资源解决“识别到目标后如何控制无人机精准悬停并完成定点投放”的完整闭环问题。方案基于ROS+PX4开发,利用YOLO目标检测识别物体位置,并据此发布飞行控制指令,使无人机保持在目标正上方,在最小允许误差范围内触发舵机投放,整体路径完整、工程化程度较高,实测效果良好。资源为zip压缩包,共125个文件,约1.22MB,包含h/cpp源码、launch启动文件、py脚本、xml配置、msg消息定义及样例数据等;h/cpp负责算法主体与控制逻辑,launch便于一键启动多节点,py脚本可用于识别后处理与调试,xml/msg提供参数和消息结构配置,目录便于按模块阅读。目前已有1178人浏览学习,参考热度较高。除可直接运行的工程代码外,包内还附带样例与配置说明,可帮助读者快速理解YOLO识别、PX4通信、悬停对准与舵机投放的串联实现,方便二次开发和项目复用。

1. ROS无人机物体识别与精准投放:先跑通感知到执行的闭环

第一次做“无人机识别地面目标并投放”,我以为难点在YOLO模型选得多好,后来发现整套链路里ROS节点通信、坐标转换、投放时序,每个环节都能让落点偏半米。那次飞机悬停、摄像头朝下,YOLO把目标框得挺准,投放指令也发了,投放物却落在目标旁边半米。问题不在识别,在坐标转换链路少了一环:像素坐标没换算到机体坐标系,加上悬停漂移,投放时机没补偿。这套系统用ROS做节点通信、YOLO(资源标题里yolv对应的yolov5实现)做物体识别、MAVROS走offboard控制,表面三个独立模块,实际是一条感知到执行的闭环。下面把链路每一步拆开:环境怎么搭、识别节点怎么写、投放指令怎么发、坑在哪。适合正学ROS、手头有或打算入手可编程无人机的人,照着能复现,出问题也知道先查哪。

2. 环境搭建:Ubuntu、ROS与Gazebo怎么组合最省事

2.1 为什么识别选YOLO系列而不是传统OpenCV方案

早期版本我用的方案是OpenCV颜色阈值加轮廓提取,室内固定光源下很好用,绿色地毯上找红色小球,一个HSV阈值就搞定。搬到户外立刻翻车:太阳角度一变,阴影里的目标边缘碎成一片,草地上颜色接近的物体直接融进背景,阈值怎么调都不稳定。传统视觉方案在光照可控的产线上没有大问题,放到无人机这种移动平台上,鲁棒性撑不住。

YOLO系列是端到端检测,输入整张图,输出目标框、类别和置信度,光照变化和轻微遮挡对它的影响远小于颜色阈值方案。yolov5在ROS社区里的资料最多,从训练、导出到部署都有现成脚本;yolov8精度更高,但ROS封装基本要自己写,资料少一轮。yolov5s权重只有十几兆,在GTX 1660级别显卡上单帧推理20到30毫秒,放在机载Jetson上跑TensorRT还能再快一截,悬停投放这种实时场景完全够用。选yolov5还有一个理由:它的COCO预训练权重覆盖人、瓶子、车辆等常见类别,做投放目标识别可以先迁移,再用自采数据微调。

三个方案放一起对比,选型就清楚了:

方案抗光照变化遮挡表现部署复杂度ROS资料量
OpenCV颜色阈值差,需反复调HSV差低多
HOG+SVM中中中少
YOLOv5好好中多

实机部署我用带GPU的机载电脑,比如Jetson Orin NANO或者Xavier NX,yolov5s用TensorRT推理能到30帧以上;如果飞机上只有CPU,可以退到yolov5n,权重更小,但帧率可能只有个位数。投放控制对实时性要求高,CPU方案我不建议用在室外有风的场景,识别延迟一旦超过100毫秒,落点误差就不可控了。

2.2 Ubuntu与ROS版本搭配:Noetic和Humble怎么选

版本选型决定了后面少踩多少坑。做无人机视觉识别,我推荐两条组合:Ubuntu 20.04加ROS Noetic加PX4,或者Ubuntu 22.04加ROS2 Humble加PX4。Noetic的资料最多,MAVROS、cv_bridge、image_pipeline都有预编译版本,遇到问题翻问答社区基本都有现成答案;Humble适合新项目,ROS2的DDS机制在多机通信时更稳,但PX4对ROS2的支持成熟度不如ROS1。只想先把链路跑通、尽量少跟文档搏斗,优先Noetic。

安装ROS这一步,我一般直接用鱼香ROS的一键安装脚本,脚本地址以项目仓库README为准,执行后选择对应ROS版本的选项,它会自动处理软件源、rosdep和依赖。不想用脚本的手动安装路线也有,以Ubuntu 20.04为例:

sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list' sudo apt install ros-noetic-desktop-full sudo rosdep init rosdep update echo "source /opt/ros/noetic/setup.bash" >> ~/.bashrc source ~/.bashrc

desktop-full是桌面完整版,带rviz、gazebo、tf、图像工具,做视觉开发一次装全;如果装成ros-noetic-ros-base基础版,后面还要自己补包,我一般不推荐。装完验证环境:

roscore

能在新终端正常启动master就说明环境没问题。这一步最常见的坑是新终端找不到roscore命令,十有八九是bashrc里的source行没加,或者是装了多个ROS版本互相覆盖。

rosdep init和rosdep update是编译工作空间时扫描依赖用的,不建议跳过。rosdep update偶尔会失败,这不是网络玄学,是rosdep源本身不稳定,换镜像或者多执行几次就能过去。

2.3 Gazebo仿真:相机、光照与机型三个关键配置

仿真帮你把节点逻辑跑通,但仿真和实机的差距主要在光照和相机参数,所以仿真里就要按实机配置来搭。机型我用开源社区维护的spacedrone模型,或者PX4自带的带云台相机iris模型。相机要挂在机架下方朝下看,分辨率1280x720,帧率30,视场角按实际镜头参数写,不要随便填一个120度。下面是在URDF里定义下视相机的片段:

<sensor type="camera" name="down_camera"> <pose>0 0 -0.1 0 0 0</pose> <camera> <horizontal_fov>1.0472</horizontal_fov> <image> <width>1280</width> <height>720</height> </image> <clip> <near>0.05</near> <far>50</far> </clip> </camera> </sensor>

horizontal_fov是水平视场角,单位弧度,1.0472对应60度;pose里的三个旋转角设为0,让相机光轴垂直向下,方便后面按平面投影做坐标换算。clip的near设0.05米,避免近距离物体被裁剪,far设50米覆盖常见飞行高度。

启动仿真后验证话题:

roslaunch px4 mavros_posix_sitl.launch rostopic hz /camera/image_raw

rostopic hz稳定输出30Hz说明仿真跑满;如果只有几赫兹,先检查机器GPU渲染是否被占用,或者把分辨率降到640x480。Gazebo里有两个高频翻车点:一是相机图像全黑,topic有数据但画面全黑,原因是world文件里没有光照;二是图像颜色偏蓝或泛白,是相机白平衡参数没设置好。建议把相机噪声开到接近实机水平再测识别节点,仿真里鲁棒、实机上大概率也不差。

仿真跑通后,rostopic list里能看到/camera/image_raw,用rqt_image_view能实时看到画面,环境这步就完成,可以进入识别节点开发了。

3. YOLO识别节点:从标注转换到ROS话题发布

3.1 VOC数据集转YOLO格式:转换脚本与边界处理

识别模型不能拿COCO权重直接对付所有目标。投放场景里要识别的是特定物体,比如红色锥桶或圆形靶标,需要自己标数据微调。公开数据集里很多标的是VOC格式,也就是XML文件,而yolov5训练要的是txt格式,每行是“类别 中心x 中心y 宽 高”,比例归一化到0到1,转换这一步绕不开。

转换脚本里最容易出问题的位置是坐标归一化时除以图像宽高,图像尺寸读错整批数据就废了;另一个坑是标注框超出图像边界,产生负数宽高,训练时直接报错。下面这段脚本把VOC的XML转成yolov5的txt,顺手做了边界裁剪:

import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, out_dir, class_map): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.findall('object'): name = obj.find('name').text if name not in class_map: continue cls_id = class_map[name] box = obj.find('bndbox') x1 = float(box.find('xmin').text) y1 = float(box.find('ymin').text) x2 = float(box.find('xmax').text) y2 = float(box.find('ymax').text) x1 = max(0, min(x1, img_w - 1)) y1 = max(0, min(y1, img_h - 1)) x2 = max(0, min(x2, img_w - 1)) y2 = max(0, min(y2, img_h - 1)) if x2 <= x1 or y2 <= y1: continue cx = (x1 + x2) / 2 / img_w cy = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") if lines: out_path = os.path.join(out_dir, xml_path.split('/')[-1].replace('.xml', '.txt')) with open(out_path, 'w') as f: f.write('\n'.join(lines)) class_map = {'cone': 0, 'target': 1}

这段脚本的核心是归一化:cx、cy、w、h都除以图像宽高,yolov5要求框坐标必须落在0到1区间。clip那几行最容易漏,框超出边界不裁剪,训练时yolov5会报“invalid box”直接中断。class_map要和训练配置里的data.yaml保持一致,多一个类别少一个类别都会在mAP评估时对不上。

数据集准备好后,微调训练的命令是这样:

python train.py --img 640 --batch 16 --epochs 100 --data data.yaml --weights yolov5s.pt

--img是训练分辨率,640是默认值,目标小可以调到800,代价是显存和速度;--batch按显卡显存调,16G显存跑16没问题;--epochs在自采的小数据集上100轮足够,再多容易过拟合;--weights用yolov5s.pt预训练权重做迁移学习,收敛比随机初始化快很多。训练完的best.pt放在weights目录,后面识别节点直接加载它。

3.2 用cv_bridge把ROS图像接入YOLO推理

模型训练完,要把识别做成一个ROS节点。核心问题是ROS的sensor_msgs/Image怎么转成OpenCV能处理的Mat,这一步用cv_bridge。下面是一个识别节点骨架,订阅/camera/image_raw,跑yolov5推理,发布检测结果:

#!/usr/bin/env python3 import rospy import cv2 import torch from sensor_msgs.msg import Image from cv_bridge import CvBridge from vision_msgs.msg import Detection2DArray, Detection2D, ObjectHypothesisWithPose class YoloDetector: def __init__(self): self.model = torch.hub.load('ultralytics/yolov5', 'yolov5s', pretrained=True) self.model.conf = 0.45 self.model.iou = 0.45 self.bridge = CvBridge() self.pub = rospy.Publisher('/detection_result', Detection2DArray, queue_size=10) rospy.Subscriber('/camera/image_raw', Image, self.image_cb) def image_cb(self, msg): frame = self.bridge.imgmsg_to_cv2(msg, 'bgr8') results = self.model(frame, size=640) dets = Detection2DArray() dets.header = msg.header for *xyxy, conf, cls in results.xyxy[0].cpu().numpy(): det = Detection2D() det.header = msg.header det.bbox.center.x = (xyxy[0] + xyxy[2]) / 2 det.bbox.center.y = (xyxy[1] + xyxy[3]) / 2 det.bbox.size_x = xyxy[2] - xyxy[0] det.bbox.size_y = xyxy[3] - xyxy[1] hyp = ObjectHypothesisWithPose() hyp.id = int(cls) hyp.score = conf det.results.append(hyp) dets.detections.append(det) self.pub.publish(dets) if __name__ == '__main__': rospy.init_node('yolo_detector') YoloDetector() rospy.spin()

model.conf和model.iou是两个最关键的识别阈值。conf是置信度阈值,设0.25会多出一堆误检框,无人机投放场景宁可漏检也不要误检,所以把conf压到0.45以上;iou是NMS的去重阈值,目标间距大时用默认0.45就行。torch.hub.load第一次运行会联网下载权重,建议在机载电脑联网状态下先跑一遍,把模型缓存到本地,飞行时断网也能加载。size=640对应训练分辨率,如果机载电脑性能不够,改416能明显提速,但小目标的召回率会下降,需要权衡。

ROS2环境里cv_bridge对应的包名是ros-humble-cv-bridge,API写法差异不大,订阅回调、图像转换的流程基本一致。

3.3 识别结果发布:话题设计与可视化

上面代码里用了vision_msgs/Detection2DArray,这个消息类型是ROS社区通用的视觉检测接口,下游的投放决策节点、rviz可视化节点都不需要关心上游用的是yolov5还是别的框架。发布检测框的同时,再发一个可视化Marker,在rviz里实时叠加识别框,方便确认当前识别状态:

from visualization_msgs.msg import Marker marker = Marker() marker.header.frame_id = 'camera_link' marker.header.stamp = rospy.Time.now() marker.type = Marker.CUBE marker.action = Marker.ADD marker.pose.position.x = cx marker.pose.position.y = cy marker.pose.position.z = 0.1 marker.scale.x = 0.2 marker.scale.y = 0.2 marker.scale.z = 0.05 marker.color.a = 0.8 marker.color.r = 1.0 marker.pub = rospy.Publisher('/detection_marker', Marker, queue_size=5)

这里有个容易翻车的点:Marker的frame_id必须设成检测框实际所在的坐标系,比如camera_link,不能图省事设成map。设成map后rviz会去查camera_link到map的tf树,查不到就直接不显示。识别结果消息里的header时间戳也要填原始图像的时间戳,下游节点拿它做消息时效判断,填当前时间会导致时序混乱。

我保持一个习惯:每个环节都带着源时间戳走完整条链路。识别端到这里就闭环了,接下来是更关键的下半段——拿到检测框坐标以后,怎么让无人机对准目标并精准投放。

4. 精准投放链路:坐标换算、悬停控制与投放时序

4.1 像素坐标到机体坐标:四个坐标系一次理清

识别节点输出的是像素坐标和框尺寸,投放需要的是机体坐标系下的相对位置。中间要经过四个坐标系:像素坐标系、相机坐标系、机体坐标系、世界坐标系,无人机里世界坐标一般叫NED或者ENU,具体看飞控定义。

坐标系作用常见frame_id
像素坐标系图像上的(u,v),原点在左上角无
相机坐标系相机光心为原点,Z轴朝前camera_link
机体坐标系飞机重心为原点,X轴朝机头base_link
世界坐标系起飞点为原点map / odom

像素到相机用相机内参矩阵,相机到机体用相机外参,也就是安装位置和角度。朝下单目相机为例,像素坐标u、v换算到机体坐标x、y的公式:

x = (u - cx) * z / fx y = (v - cy) * z / fy

fx、fy是焦距,cx、cy是主点,单位像素,来自相机标定;z是目标相对相机的深度,单目测不了深度,投放场景里目标在地面上,高度由无人机的高度传感器提供,z就取飞行高度。

手算矩阵容易错,代码里用tf做变换更可靠。下面这段把像素坐标转到机体坐标:

import tf2_ros import rospy from geometry_msgs.msg import PointStamped tf_buffer = tf2_ros.Buffer() listener = tf2_ros.TransformListener(tf_buffer) def pix_to_body(u, v, z, cam_info): fx = cam_info.K[0] fy = cam_info.K[4] cx = cam_info.K[2] cy = cam_info.K[5] x_cam = (u - cx) * z / fx y_cam = (v - cy) * z / fy p_cam = PointStamped() p_cam.header.frame_id = 'camera_link' p_cam.point.x = x_cam p_cam.point.y = y_cam p_cam.point.z = z p_body = tf_buffer.transform(p_cam, 'base_link') return p_body.point

cam_info来自相机的sensor_msgs/CameraInfo话题,标定完会实时发布,K矩阵的前三个元素就是fx、fy、cx、cy。z取飞行高度,从/mavros/altitude拿,气压计和激光测距融合后的值更稳。

这里要特别提醒:机身倾斜时光轴不垂直地面,还按平面投影算,目标位置就偏了,这就是开头说“投放偏半米”的主要原因。解决方法是起飞前做水平校准,或者用自稳云台保持相机始终朝下,再在代码里用机体姿态角做实时补偿。姿态角从/mavros/imu/data读四元数,转成欧拉角后修正x_cam、y_cam。

4.2 用MAVROS发布位置设定点:把悬停稳在厘米级

坐标换算完,目标在机体坐标下的位置有了,还要转成世界系下的期望位置发给飞控。MAVROS的offboard模式是最常用控制方式,飞机订阅/mavros/setpoint_position/local,发布期望位置,PX4飞控负责内部姿态控制。下面代码让飞机飞到目标点正上方,高度保持2米:

from geometry_msgs.msg import PoseStamped import rospy def goto_target(x, y, z): pub = rospy.Publisher('/mavros/setpoint_position/local', PoseStamped, queue_size=10) pose = PoseStamped() pose.header.frame_id = 'map' pose.header.stamp = rospy.Time.now() pose.pose.position.x = x pose.pose.position.y = y pose.pose.position.z = z pose.pose.orientation.w = 1.0 rate = rospy.Rate(20) for _ in range(50): pub.publish(pose) rate.sleep()

offboard模式下设定点发布频率不能低于10Hz,低于这个频率飞控会认为链路丢了,自动退出offboard,我一般用20Hz。进入offboard之前要连续发送设定点几秒钟,这是PX4的安全机制,飞控确认收到稳定指令才允许切换模式;切换命令通过/mavros/set_mode服务发出,顺序是先切offboard再解锁,反过来飞控直接拒绝。

坐标系的坑在这里最隐蔽。MAVROS里local坐标系通常和飞控本地坐标系一致,是NED北东地;而视觉识别输出的是机体坐标系下的坐标。直接拿机体系坐标当local坐标发,飞机就朝错误方向飞。正确做法是用tf先把目标点的机体坐标转到map或odom系再发出去。我在实际项目里维护一棵tf树,base_link到map的变换由飞控的里程计提供,视觉节点只负责在base_link下发布目标点,转换交给tf。

4.3 投放机构与时序控制:状态机比一堆if else可靠

投放机构我用舵机加弹簧拨片,舵机信号线接飞控的PWM输出通道,或者单独接一个PCA9685扩展板。控制动作本身不复杂:给舵机一个PWM脉宽,舵机转到释放位置。真正难的是投放时机。

识别到目标后,飞机要移动到目标正上方,悬停稳定,再执行投放。如果飞机还在漂移中就释放,投放物带着初速度,落点就偏了。我把整个流程写成状态机,比散落的if else逻辑可靠得多:

class DropStateMachine: def __init__(self): self.state = 'SEARCH' def update(self, detection, current_pose): if self.state == 'SEARCH' and detection is not None: self.state = 'APPROACH' self.approach_target = detection elif self.state == 'APPROACH' and self._reached(0.3): self.state = 'HOVER' self.hover_start = rospy.Time.now() elif self.state == 'HOVER' and (rospy.Time.now() - self.hover_start).to_sec() > 2.0: self.state = 'DROP' self.servo_trigger() self.return_home() return self.state

状态机参数按实机调。_reached的距离阈值我取0.3米,太小了飞控在目标点附近震荡,触发不了转换还好,反复跳状态很烦;悬停停留时间要大于飞机从移动状态到稳定静止的时间,2秒是保守起点,风大时加到3秒。户外有风时悬停漂移可能超过0.5米,光靠位置闭环不够,我还会在投放前检查/mavros/local_position/velocity_body的速度模长,小于0.1米/秒才允许触发舵机。

投放完成后返航,可以走一条直线回起点,也可以接全局路径规划器给的路线,这部分不影响投放精度,但多机协作时要注意话题和坐标系的统一,别把返航点发到别的机体坐标系里,多机通信配置也是同样的原则:每个飞机一套独立命名空间,避免话题互相覆盖。

5. 避坑:联调时最容易翻车的五个环节

5.1 识别节点CPU跑满GPU不工作

现象:机载电脑上运行识别节点,CPU占用冲到100%,nvidia-smi里看不到GPU进程,推理帧率掉到个位数。

原因:torch没有启用CUDA,默认走CPU推理。常见于torch装成了CPU版本,或者OpenCV是系统包安装的不带CUDA支持,图像预处理也在CPU上跑。

解决:先确认torch.cuda.is_available()返回True,为False就重装对应JetPack版本的torch。图像预处理我习惯用cv2.cuda模块,把resize、颜色转换也放到GPU上,推理和预处理统一走GPU,CPU负载能降一半。Jetson上还要记得开最大性能模式,sudo nvpmodel -m 0切到max性能,否则GPU频率被限制住,推理速度上不去。这个坑最容易在第一次上机时出现,因为开发机上GPU没问题,部署到Jetson才发现环境不一致。

5.2 Gazebo里相机图像全黑

现象:rostopic list能看到/camera/image_raw,但rqt_image_view里画面全黑,识别节点正常推理却什么都检测不到。

原因:仿真场景里没有光照。URDF里的相机传感器只是定义了相机本身,world文件里没放光源,场景就是黑的。另一种可能是光源位置在飞机上方但被机体遮挡,光线照不到地面目标。

解决:在world文件里加一个方向光,指向地面,强度设1.0左右:

<light type="directional" name="sun"> <pose>0 0 10 0 0 0</pose> <diffuse>1 1 1 1</diffuse> <direction>-0.2 -0.5 -1</direction> </light>

direction向量指向光源照射方向,要和相机朝下的视角配合,保证地面目标在照明范围内。加完光源重启Gazebo,图像还黑就检查相机clip的near/far设置,near太大会把靠近相机的物体裁掉。仿真里把相机噪声开到接近实机水平,识别节点的边界能力才能提前暴露。

5.3 识别框抖动导致投放偏移

现象:目标框在图像里左右跳,换算出目标位置跟着波动,投放落点离散,打不到一个固定位置。

原因:单帧检测不稳定,某些帧置信度略低时目标框位置跳变;相机帧率低时,飞机晃动带来的运动模糊进一步加大框的位置漂移。

解决:给目标中心位置加指数移动平均滤波,代码一行:

alpha = 0.6 filtered_x = alpha * observed_x + (1 - alpha) * filtered_x filtered_y = alpha * observed_y + (1 - alpha) * filtered_y

alpha是当前帧的权重,取值按场景调:

场景alpha取值效果
光线稳定、目标静止0.6-0.8响应快,跟踪跟手
悬停晃动、目标小0.3-0.5平滑,但延迟变大

滤波会引入延迟,悬停投放时延迟影响不大,如果是运动目标投放,需要根据速度做前馈补偿,否则落点还是偏。另一个常被忽略的原因是相机帧率设置成默认的15Hz,运动模糊严重,我把它固定到30Hz以上、曝光时间调短,框的抖动明显下降。

5.4 多节点通信话题数据时有时无

现象:识别节点跑着,飞控节点却经常收不到/detection_result,rostopic hz显示频率忽高忽低,偶尔为零。

原因:发布端queue_size设成1,处理慢时新消息把没来得及消费的旧消息顶掉;另一个可能是订阅和发布的消息类型不完全一致,类型不匹配时回调不触发,没有任何报错。

解决:发布端queue_size至少设10;用rostopic info /detection_result确认发布和订阅的类型完全一致。多机通信场景,比如机载电脑和地面站分开跑节点,还要检查ROS Master的地址配置,确保两边的ROS_MASTER_URI指向同一个master,否则话题只在各自机器上可见。这个问题隐藏得很深,优先排查网络配置,再回头看代码。

5.5 舵机投放无响应

现象:状态机已经走到DROP,舵机纹丝不动,代码层面没有任何报错。

原因:舵机信号线插错通道,或者PWM频率和舵机型号不匹配。大多数舵机要求50Hz,周期20毫秒,脉宽1到2毫秒;但部分飞控默认PWM输出频率不是50Hz,脉宽也超出舵机接受范围。

解决:先脱离飞控单独测舵机,用舵机测试工具或直接给信号线送固定脉宽,确认舵机本身能动,再查通道映射:

cd /path/to/servo_test_tool ./servo_test --channel 0 --pulse 1500

1.5毫秒脉宽是舵机中位,舵机臂应该转到中间位置。如果单独测试正常、接上飞控不动,检查飞控的PWM配置里的输出频率和通道映射,再检查舵机供电是否足够,落地后供电不足舵机会不动。还有一个血泪教训是舵机地线和飞控不共地,信号飘忽不定,时好时坏,单独测试正常,一上机就失灵,最后发现是电平参考不一致。

6. 用rosbag回放数据:验证识别与投放时机的进阶手段

实机飞行成本高,不能每次都上天调参。我的做法是每次飞行都把关键话题录成rosbag,落地后在电脑上反复回放,调识别阈值和投放参数,确认没问题再上机验证。录制时最少包含这四个话题:

rosbag record -O flight_01.bag \ /camera/image_raw \ /detection_result \ /mavros/local_position/pose \ /mavros/setpoint_position/local

回放时启动识别节点和可视化节点,然后让bag包按单倍速或二倍速播放,观察识别框和实际位置的对应关系:

rosbag play flight_01.bag --rate 1.0

回放的价值不只是看画面。我会写一个离线脚本把bag里所有识别结果导出来,统计三项指标:目标持续被发现的帧占比,正常应大于90%;每帧目标框中心位置的标准差,用来量化识别抖动;从发现目标到进入悬停的耗时,评估整个投放决策链路的延迟。数据说话,比肉眼判断靠谱得多。

调参顺序我也固定下来了:先不碰代码,回放时只调conf和iou阈值,看误检率能不能压到5%以下;再调EMA滤波强度,让目标中心位置波动小于0.1米;最后才调状态机的悬停时长和速度阈值。这个顺序能避免代码和参数互相干扰,改一个变量就只有一个变量在变。

从那以后,我每次跑完一组飞行,第一件事就是存档rosbag,文件名带日期和天气。攒了十几个包以后,很多当时没法解释的“玄学”偏差,回放时一眼就看出是光照方向变化导致识别框偏移,或者是风速变化让悬停漂移超限。这套流程帮我把投放落点误差从最开始半米收窄到20厘米以内,而且每次改动都有据可查。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询