ARTICLE DETAIL

资讯详情

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

无人机目标跟踪实战:从YOLOv8检测到MAVSDK控制的完整闭环

无人机目标跟踪实战:从YOLOv8检测到MAVSDK控制的完整闭环 做无人机目标跟踪应用这件事听起来很酷但真正落地的时候你会发现它不是一个摄像头跟着人走的Demo那么简单。从图像里把目标框出来、持续锁定、再把像素误差换算成飞控能听懂的速度指令这是一条完整的闭环链路任何一个环节没接上飞机就只会原地打转或者干脆追丢目标。这篇文章把我自己做这套系统时的技术选型、踩过的坑、最终跑通的方案完整整理出来。内容包括检测与跟踪算法的配合方式、从像素坐标到无人机控制指令的换算逻辑、PID参数的调试思路以及实飞阶段最容易翻车的几个问题。无论你是想用Tello做入门实验还是准备在PX4/ArduPilot平台上做正经项目这篇都能给你一个可以直接参考的起点。1. 整体设计与技术选型思路1.1 需求拆解追踪应用到底在追踪什么动手写代码之前先把需求拆清楚。一个能用的目标跟踪无人机应用至少包含下面五个模块缺一个都跑不稳目标检测从图像帧里找到目标输出边界框bounding box和类别置信度。目标跟踪在连续视频帧里维持同一个目标的身份解决这一帧框出来的是不是上一帧那个目标的问题同时应对遮挡、目标短暂离开画面等场景。状态估计把目标的像素坐标换算成相对于无人机的方位角、俯仰角和大致距离这一步是控制环节的输入。飞行控制根据状态误差生成无人机的速度指令body frame下的vx、vy、vz和偏航角速度构成完整闭环。安全保护目标丢失、信号弱、电量低、GPS拒止等异常情况下的降级策略。很多人一上来就想着先用YOLO把目标检测做了结果检测框在单帧画面上画得好好的飞机一动就全乱了。原因就在于检测只是感知的一部分真正让无人机跟住目标的是检测、跟踪、控制三者的配合。我建议在写任何代码之前先在纸上把这五个模块的输入输出接口定下来后面所有工作都是往这些接口里填内容。1.2 方案选型为什么我选了这条技术路线我最终跑通的方案是Python OpenCV YOLOv8检测 ByteTrack跟踪 MAVSDK飞控通信 PID控制算法跑在地面站笔记本上通过Wi-Fi或数传链路把控制指令下发给飞控。这套方案不是唯一的但它是我测试下来性价比最高、最容易调试的路线。下面说清楚每个选择的理由。为什么让算法跑在地面站而不是机载端这是很多人第一个纠结的问题。机载端比如Jetson Nano、树莓派的优势是延迟低、不依赖通信链路但劣势也明显算力有限、散热难搞、调试麻烦。我做这个项目的目标是把跟踪逻辑跑通而不是做边缘部署优化所以把重计算放在地面站。实测下来在Wi-Fi环境良好、画面延迟能控制在200ms以内的情况下地面站方案完全够用。如果你后续要做长距离追踪或者超视距飞行再考虑往机载端迁移也不迟。为什么用检测 跟踪的组合而不直接用KCF之类的纯跟踪器纯跟踪器OpenCV里内置的KCF、CSRT等优点是快、不需要GPU但缺点也很致命目标一旦被遮挡、快速移动或者形变跟踪框就会漂移而且漂了之后没有机制拉回来。我的做法是检测器负责找目标跟踪器负责盯目标——检测器以较低的频率比如每5帧跑一次负责校正跟踪器的漂移跟踪器在检测的空档期里以高帧率维持目标位置。这样既保证了跟踪的连续性又不会把GPU算力全耗在检测上。为什么选ByteTrack而不是DeepSORT这是我实际对比后的结论。DeepSORT需要额外训练一个ReID模型来提取外观特征数据准备和训练成本都不低在中小型项目里属于过度设计。ByteTrack的核心思路是从检测结果里做高置信度和低置信度框的二次关联纯运动信息就能干活不需要额外的特征网络部署简单得多。在行人、车辆这类常见目标上ByteTrack的ID切换率也比DeepSORT低不少足够满足无人机追踪场景的需求。飞控通信层怎么选我推荐用MAVSDK支持PX4和ArduPilot。它比直接写MAVLink协议要省心得多提供了Python绑定地面站代码里直接调用send_velocity_ned()这类接口就行。如果你用的是大疆Tello这类消费级无人机就直接用Tello SDK发送RC控制指令逻辑是一样的只是接口名字不一样。下面代码部分我会给出具体示例。2. 核心细节解析与关键参数设计2.1 检测与跟踪算法的配合方式先说一个很多人忽略的原则检测和跟踪不要串行跑在同一帧上而要并行跑在不同频率上。检测器YOLOv8的推理时间取决于你的GPU或CPU在普通笔记本上用GPU大约20到50毫秒一帧CPU的话可能200到400毫秒。如果每一帧都先检测再跟踪跟踪帧率就被检测器拖垮了。我的实现里用了双线程结构一个线程持续从视频流读取画面放入一个固定长度的队列比如2到3帧的缓冲。检测线程以较低频率每N帧或每T毫秒从队列里取最新一帧跑YOLO输出目标框。跟踪线程以全帧率跑ByteTrack的在线关联逻辑用检测结果更新跟踪状态并输出当前帧中目标的中心坐标。这样做的核心收益是跟踪的响应速度只取决于视频流帧率不取决于检测器的推理速度。检测器相当于一个每隔一阵子校准一次的参考系跟踪器负责在两次校准之间保持对目标的锁定。实际测试中我把检测频率设在5到10Hz跟踪频率跑满30Hz追踪效果已经很平滑。另外一个细节是类别过滤。YOLOv8默认在COCO数据集上训练能检测80类目标。如果你只想追踪人一定要在输出端过滤掉其他类别否则画面里出现一辆车时跟踪器可能瞬间就把ID切过去了。这个操作虽然简单但我在测试中真遇到过——目标从行人旁边经过一辆车追踪框莫名其妙跳到了车上。2.2 从像素坐标到无人机指令坐标系换算这是整个项目里最容易被低估、也最容易出错的环节。很多教程把目标框画出来就结束了但无人机要执行追踪动作必须要知道目标在哪个方向、多远、多高。这一步需要完成两个坐标系换算图像像素坐标系 → 相机坐标系 → 机体坐标系。第一步像素坐标到归一化相机坐标。假设目标中心在图像上的像素坐标为(u, v)相机内参为fx、fy焦距单位像素cx、cy主点坐标那么归一化坐标为x_cam (u - cx) / fx y_cam (v - cy) / fy这里算出来的x_cam和y_cam是目标在相机坐标系下的方向向量假设z轴朝前。如果你用Tello这类自带相机的无人机内参可以用OpenCV的棋盘格标定一下或者直接参考社区里别人标定好的参数。第二步相机坐标系到机体坐标系。如果相机是固定朝前安装的并且和机体轴对齐这一步可以简化成一次旋转矩阵变换。一般相机会有一个小小的安装俯仰角比如朝下倾斜15度这会导致目标在画面里偏上或偏下如果不修正追踪时飞机会一直往前冲或往后退。旋转矩阵用OpenCV的cv2.Rodrigues()把旋转向量转出来或者直接用欧拉角手写R R_z(yaw) R_y(pitch) R_x(roll)第三步机体坐标系到NED北东地坐标。MAVSDK的send_velocity_ned()要求的输入是NED系下的速度。对于视觉追踪这种相对定位场景一个常见做法是不依赖绝对位置只输出相对目标的方向指令。也就是把机体坐标系的误差量转成期望速度然后转换成NED系下发。具体怎么转取决于无人机的当前偏航角yaw需要从飞控的姿态估计里读取。我在实际项目里做过一次简化把飞机的偏航锁定在朝向目标的初始方向追踪阶段主要用前后vx和上下vz速度配合一个很小的侧向vy修正。这样坐标系换算的复杂度会降低很多对新手也友好一些。等基础功能跑通了再逐步放开偏航通道实现始终正面朝向目标的完整跟随效果。2.3 控制环节的核心PID参数的调试思路坐标换算完成后你得到的是目标在画面上的偏移量单位可以是归一化坐标也可以是像素。接下来要做的就是把这个偏移量变成速度指令。这里用到的是最经典也最有效的PID控制。设想这样一个场景目标中心在画面中心偏右100像素你希望无人机往右飞把目标拉回画面中心。期望速度应该和这个误差成正比——误差越大速度越快误差趋近于零速度趋近于零。这就是P项的作用。但是只有P项会有问题目标在移动时纯P控制会产生稳态误差无人机始终差那么一点追不上这时候需要I项来消除稳态误差。D项则用来抑制震荡当误差快速变化时D项可以踩刹车避免无人机在目标位置附近来回振荡。实际调参时我的建议是分通道调不要三个通道一起调。先锁定yaw通道让无人机可以左右转头对准目标再调vx前后通道保持目标在画面中的大小最后调vz上下通道保持目标在画面中的垂直位置。每个通道从P项开始从小到大逐步增加直到出现轻微震荡然后回调到震荡消失的值再加一点点I项消除稳态误差D项给一个很小的值抑制超调。这里有个我在实飞中踩过的坑PID参数在仿真环境里调得再好真机也要重新调。因为真机有风、有惯性、有通信延迟仿真里完美的参数到了真机上很可能震荡得厉害。建议第一次实飞时把所有P项降到仿真值的1/3甚至1/5先让飞机飞得肉一点确认不会乱飘再逐步加大。3. 实操过程与核心功能实现3.1 环境搭建与准备工作我的开发环境是Ubuntu 22.04 Python 3.10 NVIDIA GPU无GPU也能跑就是帧率低一些。核心依赖如下pip install ultralytics opencv-python bytetrack-python mavsdk几个版本细节需要注意ultralytics是YOLOv8的官方库安装时会自动带上PyTorch。如果你有GPU提前装好CUDA版本的PyTorchCPU版本的推理速度会慢好几倍。bytetrack-python是一个第三方封装库你也可以直接跑ByteTrack官方代码仓库。两者的核心算法一样第三方库胜在接口简单适合快速集成。mavsdk需要你的飞控支持MAVLink协议。在PX4和ArduPilot上都能用。Tello用户换成djitellopy库接口逻辑类似。硬件方面我用的是自组F450四轴 Pixhawk飞控 树莓派摄像头通过Wi-Fi推流到地面站。如果你不想折腾自组机Tello是最省事的入门选择它的SDK自带视频流和控制接口一天的功夫就能把整条链路跑通。3.2 检测跟踪模块的实现检测跟踪模块的核心逻辑分三块视频帧读取、目标检测、目标跟踪。下面是我整理过的简化版核心代码。import cv2 import numpy as np from ultralytics import YOLO from bytetrack import ByteTrack # 加载YOLOv8模型用nano版本保证推理速度 model YOLO(yolov8n.pt) tracker ByteTrack() # 只追踪person类别COCO数据集中person的类别ID是0 target_class_id 0 def detect_and_track(frame): # 1. 检测跑YOLO置信度阈值设在0.4左右太低会引入大量误检 results model(frame, verboseFalse, conf0.4, classes[target_class_id]) detections results[0].boxes # 提取检测框并归一化到[y0, x0, y1, x1]格式ByteTrack接口要求: boxes [] scores [] for det in detections: x1, y1, x2, y2 det.xyxy[0].cpu().numpy() boxes.append([y1, x1, y2, x2]) scores.append(float(det.conf[0])) # 2. 跟踪用检测结果更新跟踪状态 tracked_objects tracker.update( np.array(boxes, dtypenp.float32), np.array(scores, dtypenp.float32) ) return tracked_objects这段代码里有几个关键点值得展开说置信度阈值不要设太高。YOLOv8在远距离场景下目标在画面里只有几十个像素大小置信度会偏低。我测试时发现0.4到0.5的置信度阈值比较合适太高会导致目标在远处直接消失太低又会把背景误判成人。跟踪器输入的边界框格式要规范化。ByteTrack官方实现里用的是[x0, y0, x1, y1]还是[y0, x0, y1, x1]不同版本有差异我在集成时就因为这个格式问题浪费了半天——跟踪器不报错但框的位置完全不对。如果你用的是第三方封装库建议先打印一行跟踪结果和检测结果对比一下坐标范围确认格式正确再往下写。目标中心坐标的提取是控制模块的输入这一步在拿到跟踪结果后做def get_target_center(tracked_objects): # tracked_objects每个元素为[x0, y0, x1, y1, track_id, score] if len(tracked_objects) 0: return None, None # 取面积最大的跟踪目标作为主目标避免追踪到杂物上 target max(tracked_objects, keylambda t: (t[2]-t[0]) * (t[3]-t[1])) center_x (target[0] target[2]) / 2.0 center_y (target[1] target[3]) / 2.0 return center_x, center_y, target[4] # 返回track_id用于目标一致性判断3.3 追踪控制逻辑的实现控制模块的核心是一个循环读取目标中心坐标 → 计算偏差 → 计算PID输出 → 发送速度指令。下面是我用MAVSDK写的地面站控制代码框架。from mavsdk import System from mavsdk.offboard import VelocityNedYaw, OffboardError import asyncio class DroneController: def __init__(self): self.drone System() self.pid_x PID(kp0.3, ki0.01, kd0.05) # 前后通道 self.pid_y PID(kp0.3, ki0.01, kd0.05) # 左右通道 self.pid_z PID(kp0.2, ki0.005, kd0.03) # 上下通道 async def connect(self): await self.drone.connect(system_addressudp://:14540) # 等待飞控就绪 async for state in self.drone.core.connection_state(): if state.is_connected: print(无人机已连接) break # 启动Offboard模式 await self.drone.offboard.set_velocity_ned(VelocityNedYaw(0, 0, 0, 0)) await self.drone.offboard.start() async def track_loop(self, target_center_getter): while True: center_x, center_y target_center_getter() if center_x is None: # 目标丢失悬停等待不要乱飞 await self.drone.offboard.set_velocity_ned(VelocityNedYaw(0, 0, 0, 0)) await asyncio.sleep(0.1) continue # 归一化误差以画面中心为原点范围约[-1, 1] error_x (center_x - frame_center_x) / frame_width * 2 error_y (center_y - frame_center_y) / frame_height * 2 # PID计算速度指令 vx self.pid_x.update(error_x) # 目标偏右vx为正向前飞 vy self.pid_y.update(error_y) # 目标偏下vy为负下降 # 偏航通道让机头始终对准目标 yaw_rate -error_x * 0.5 velocity VelocityNedYaw(vx, vy, 0, yaw_rate) await self.drone.offboard.set_velocity_ned(velocity) await asyncio.sleep(0.05) # 20Hz控制频率这里有一个特别重要的设计细节目标丢失时一定要悬停而不是保持上一个速度指令继续飞。我给这个项目做测试时第一次实飞就遇到了目标从画面里跑出去的情况当时代码里直接跳过了控制更新结果无人机保持着最后的速度方向直线飞出去差点撞到树上。从那之后我在所有控制循环里都加了目标丢失→立即归零速度的保护逻辑这才是真正安全的行为。还有一个看起来小但特别坑的点控制频率和视频帧率要匹配。我最初直接把控制循环跑在视频帧回调里视频帧率是30fps控制更新频率就是30Hz后来目标检测线程偶尔卡顿视频回调也跟着变慢控制频率掉到10Hz以下飞机就开始明显抖动。解决方案就是把控制循环独立成一个线程固定20Hz运行和视频帧率解耦。3.4 仿真与实飞的前置验证在真机起飞之前强烈建议先用仿真环境把整条链路验证一遍。PX4 Gazebo的组合是社区里最常用的方案MAVSDK可以直接连接Gazebo里的仿真飞控接口和真机完全一致。这意味着你可以在仿真里把检测、跟踪、控制、保护逻辑全部调通再上真机。仿真阶段一个很实用的技巧是用地面站的虚拟相机模拟跟踪场景。具体做法是在Gazebo世界里放置一个移动的假人模型通过仿真相机获取画面输入给检测跟踪模块同时把控制指令发给仿真飞控。这个环境配好之后你可以在完全安全的情况下测试各种边界情况——目标快跑、目标遮挡、目标消失又重新出现。我测试时发现遮挡重识别的问题就是在仿真里暴露出来的真机测试时大概率会撞上一次、炸一次机才能发现。4. 常见问题与排查技巧实录4.1 目标跟踪丢失与重识别这是追踪应用里最典型的痛点。我从测试数据里统计过目标丢失的原因主要集中在三类目标被遮挡、目标快速移动导致跟踪框跟不上、目标与背景对比度太低。ByteTrack这类纯运动信息的跟踪器在目标被完全遮挡几帧后会直接丢ID。解决思路有两个一是给跟踪器加记忆机制——目标丢失后在预测位置附近做小范围搜索如果目标在一段时间内重新出现就恢复追踪二是用检测器的结果做接续——目标重新被检测到时判断它是否在之前丢失位置附近如果是就复用原来的track_id。我实现里的做法是维护一个lost_target字典记录目标最后出现的位置、大小和时间戳。当当前帧没有跟踪结果时在最后位置附近扩大搜索区域用YOLO重新检测如果发现同类目标且Iou高于某个阈值就认为目标回来了。这套逻辑代码量不大但能让追踪的鲁棒性提升一个档次。4.2 视频延迟导致的控制震荡地面站方案的天然软肋是延迟。Wi-Fi图传从摄像头到地面站的延迟通常在100到250毫秒之间这个延迟反馈到控制环路上会导致一个经典问题无人机看到的目标位置是过去的而它正在根据这个过期信息调整自身姿态结果就是震荡。我的解决思路包括两方面在PID层面做补偿。适当减小P项增益增大D项增益。D项对误差变化率敏感在延迟场景下能起到预测作用抑制过冲。我是把P项降到无延迟环境下的70%左右D项提到1.5倍左右实测震荡幅度明显减小。但要注意D项对噪声非常敏感图像坐标的抖动会被D项放大成高频速度指令所以对目标中心坐标做一阶低通滤波是必须的。在系统层面降低延迟。摄像头推流优先用硬件编码而不是软件编码H.264编码延迟可以控制在几十毫秒。传输协议上RTSP比RTMP的端到端延迟更低。如果你的图传方案允许把分辨率降到640x480而不是1080p也能显著降低编码和传输延迟对检测精度的影响并不大——毕竟YOLO的输入尺寸一般是640x6401080p的画面最终也要缩放。4.3 实飞阶段的经验与安全底线实飞是把整个项目推上能用水平的关键一步但也是最容易出问题的一步。我总结几条用真金白银换来的经验第一飞场选择比技术还重要。第一次实飞一定要在空旷、无风、GPS信号好的地方周围没有树木、电线杆、行人。这些条件缺一个都不建议起飞——我见过一个小伙伴在停车场测试GPS信号被高楼遮挡无人机起飞后直接飘了差点撞车。第二从一个轴开始测不要上来就全自动追踪。我建议的测试顺序是先只测偏航通道让无人机通过旋转对准目标前后上下全部锁定跑通了再解锁前后通道让无人机可以靠近/远离目标最后解锁上下通道。每一步都验证稳定了再进下一步出问题时也容易定位。第三必须保留手动遥控器接管能力。无论你的算法写得多好物理世界永远有意外。我在代码里加了一个急停机制遥控器切换到手动模式时地面站立即停止发送Offboard指令飞控交还给飞行员。这个机制花不了多少代码量但关键时刻能救下整架飞机。第四提前记录log。实飞时把视频流、跟踪坐标、PID输出、速度指令全部记录下来。出问题时对着log回放很快就能定位是检测问题、跟踪问题还是控制问题比自己凭记忆猜靠谱得多。5. 后续可以怎么扩展这套系统跑通之后可以扩展的方向其实非常多。我自己接下来在做的几个方向供你参考目标距离保持目前追踪只控制了画面里的方位没有控制距离。加入目标在画面中的尺寸信息通过期望尺寸和实际尺寸的比值调节前后速度就能实现始终和目标保持固定距离的跟随效果这在拍摄类应用里很实用。机载端部署把检测跟踪模型用TensorRT量化后部署到Jetson Orin Nano这类边缘设备上摆脱地面站和图传链路实现更远距离的自主追踪。多目标选择界面在地面站画面里用鼠标点击任意目标就可以切换追踪目标。这个功能不算复杂但交互体验会好很多适合给演示场景使用。最后分享一个小技巧。调试PID参数的效率很大程度上取决于你能不能实时看到误差曲线。我自己写了一个简单的调试面板用matplotlib实时画出三个通道的误差和速度指令这样调参的时候不用靠感觉盯着曲线就能判断是P太大还是D不够。这个调试面板前后花了我一个多小时但后面每一次调参省下来的时间都不止这个数。做无人机跟踪这种延迟敏感的项目可视化调试工具不是可有可无的装饰而是刚需。
返回列表