目标跟踪无人机实战:检测与跟踪双模块及PID控制全解析

📅 2026/8/26 22:23:06
目标跟踪无人机实战:检测与跟踪双模块及PID控制全解析
Object Tracking 无人机应用到底怎么做这个需求在我后台被问过很多次索性把整套开发思路和关键代码一次写清楚。这篇文章面向想自己动手做目标跟踪无人机的开发者不管你是刚入门还是已经飞过几款开源飞控都能从里面找到可以直接落地的方案。我会从架构设计、算法选型、硬件搭建、核心控制逻辑到实测调试完整走一遍。先说一个很多人容易走偏的点目标跟踪无人机听起来是个单一功能但本质上它是一个典型的感知-决策-执行闭环系统。视觉感知模块负责看到目标并确认目标位置决策模块负责根据目标在图像中的位置计算出飞行的控制指令执行模块也就是飞控系统负责把指令平滑地转成电机转速。任何一个环节出问题整条链路都会断。所以这篇文章的整个拆解逻辑就是围绕这条链路逐层展开的。1. 目标跟踪无人机的系统架构与核心模块划分动手写代码之前先得把系统架构想清楚。我做过的几个跟踪项目无论是基于大疆 Tello 这种成品小飞机还是 Pixhawk 加机载电脑的 DIY 方案整体架构都能抽象成三个子系统和一条数据链路。1.1 三大核心子系统第一个子系统是视觉感知子系统。它要完成三件事图像采集、目标检测、目标定位。图像采集依赖摄像头可能是无人机自带的也可能是外挂的目标检测解决的是画面里有没有我要追的东西、在哪个位置目标定位则要输出更精确的像素坐标甚至通过云台角度和飞行姿态估算目标的空间坐标。第二个子系统是飞行控制子系统。它接收视觉子系统给出的目标位置偏差经过控制算法换算成无人机移动的期望速度再交由底层飞控去执行。这里有两个完全不同的做法第一种是自己控制无人机的平移和旋转也就是我们常说的位置控制第二种是只控制云台的朝向保持目标始终在画面中心无人机本身不动。两种方案的技术难度差别非常大后面我会专门展开讲。第三个子系统是地面站与通信子系统。它负责显示实时视频流、记录飞行日志、调整跟踪参数以及处理紧急情况下的手动接管。很多人做跟踪无人机时容易忽略地面站的作用实际飞行测试中它几乎是救命稻草——目标跟丢了、飞机飘了、跟踪框锁错了目标这些情况如果没有地面站实时监控风险非常高。1.2 数据链路的时序关系清楚三大模块还不够还得理解数据是如何一环扣一环流动的。一次完整的跟踪循环大致是这样的摄像头采集一帧图像时间点记为 T0目标检测模型在图像上找到目标框更新目标的位置信息时间点 T1控制器计算目标中心与画面中心的像素偏差转换为控制指令时间点 T2通信链路把控制指令下发到飞控飞控解析后驱动电机时间点 T3无人机开始移动摄像头画面随之变化时间点 T4。整个流程通常要在几十毫秒内完成。如果用的是机载处理器比如 Jetson Nano 或树莓派T0 到 T2 的时间取决于模型推理速度和图像分辨率一般能控制在 30ms 到 80ms 之间如果图像传到地面站处理再回传指令来回的无线延迟就要另加 50ms 到 200ms这时候跟踪的实时性就会明显变差。这也是我推荐机载端处理的核心原因跟踪系统对延迟极其敏感每一帧的延迟都意味着无人机会继续沿着旧指令的方向飞行误差会不断累积。地面站方案虽然开发调试方便但只适合空旷环境下追踪慢速移动的目标。2. 目标检测与跟踪算法传统视觉和深度学习的实际取舍这是整个项目里技术含量最高、也最容易被带偏的部分。先说结论经过多次实测我推荐的是检测器 轻量级跟踪器双模块架构而不是直接用纯检测或纯跟踪方案。这个结论背后的原因值得花一整节来分析。2.1 为什么纯目标检测方案做不了实时跟踪很多人第一反应是用 YOLO 或者 SSD 做检测每帧检测一次把目标框提取出来不就行了吗实际上把这个方案直接应用于无人机时问题会迅速暴露出来。目标检测模型是逐帧独立工作的它没有上一帧这个目标在哪的概念。无人机飞行中画面会有剧烈抖动、运动模糊、视角变化模型很容易出现隔几帧才检测到目标的情况于是目标框会闪烁、跳动控制指令也随之剧烈振荡飞机会像喝醉了一样来回晃。此外目标检测的虚警问题也很头疼。如果你要追的目标是个人但画面里出现了一个背影轮廓相似的路牌或树干纯检测方案可能直接把框套过去。没有时序信息的约束这种错误检测无法被识别和过滤。2.2 为什么纯跟踪器方案也不行那反过来只用 KCF、CSRT 这类视觉跟踪器呢这类算法的思路是在第一帧指定一个目标框后续帧在邻域内搜索与目标外观最相似的区域并持续更新目标位置。跟踪器的优点是速度快KCF 在 CPU 上都能跑到几百帧而且它天然带有时间连续性目标框不会逐帧跳动。但它的致命短板是容易跟丢且无法自我恢复。目标一旦被障碍物遮挡几秒或者快速移动产生剧烈运动模糊跟踪器就会毫不犹豫地把框漂移到背景纹理相似的地方而且毫无察觉。2.3 检测与跟踪分离的双模块架构真正可靠的方案是把两者结合起来检测器负责周期性确认目标真的在这里并在目标丢失时重新找回跟踪器负责帧与帧之间的快速连续定位保证平滑输出。简单说就是检测器做慢而准的兜底跟踪器做快而稳的贴身跟随。实际项目中我用的是 MobileNet SSD 作为检测器用 OpenCV 的 CSRT 作为跟踪器并按如下逻辑协作启动阶段用检测器在整幅画面中搜索目标锁定后初始化跟踪器正常跟踪阶段每帧用跟踪器输出目标框以目标中心作为控制依据每 30 到 50 帧约每秒 1 到 2 次用检测器在跟踪器预测位置的邻域内做一次校验确认目标框仍然可信如果检测器连续多次确认跟踪框已经偏离目标或跟踪器反馈的置信度极低则触发重新检测流程。这套架构在跟踪稳定性和目标丢失后的恢复能力上都远优于单一方案。代价仅仅是每秒钟多跑一两次检测模型推理对机载处理器的算力要求基本可以接受。2.4 端到端代码跟踪主循环下面是一段可直接用于机载端例如 Raspberry Pi 4 或 Jetson Nano的跟踪主循环实现使用 OpenCV 的 dnn 模块加载 MobileNet SSD 模型并用 CSRT 跟踪器做帧间跟踪。import cv2 import numpy as np # 初始化检测器模型 net cv2.dnn.readNetFromCaffe( MobileNetSSD_deploy.prototxt, MobileNetSSD_deploy.caffemodel ) # 类别名称MobileNet SSD COCO 模型对应的 20 类 CLASSES [background, aeroplane, bicycle, bird, boat, bottle, bus, car, cat, chair, cow, diningtable, dog, horse, motorbike, person, pottedplant, sheep, sofa, train, tvmonitor] # 需要跟踪的目标类别这里以 person 为例 TARGET_CLASSES [person] # 初始化摄像头 cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) tracker None lost_frames 0 check_counter 0 def detect_target(frame): 在整帧中检测目标类别返回置信度最高的目标框 (x, y, w, h) 或 None h, w frame.shape[:2] blob cv2.dnn.blobFromImage(frame, 0.007843, (300, 300), 127.5) net.setInput(blob) detections net.forward() best_box None best_conf 0.0 for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence 0.5: continue idx int(detections[0, 0, i, 1]) if CLASSES[idx] not in TARGET_CLASSES: continue box detections[0, 0, i, 3:7] * np.array([w, h, w, h]) (x1, y1, x2, y2) box.astype(int) if confidence best_conf: best_conf confidence best_box (x1, y1, x2 - x1, y2 - y1) return best_box def track_frame(frame, tracker): 用跟踪器跟踪目标返回目标框或 None ok, box tracker.update(frame) if not ok: return None return box while True: ret, frame cap.read() if not ret: break # 每 30 帧执行一次检测做校验 if tracker is None or check_counter 30: box detect_target(frame) if box is not None: tracker cv2.TrackerCSRT_create() tracker.init(frame, box) check_counter 0 if tracker is not None: box track_frame(frame, tracker) if box is not None: lost_frames 0 x, y, w, h [int(v) for v in box] cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) cv2.circle(frame, (x w // 2, y h // 2), 4, (0, 0, 255), -1) else: lost_frames 1 if lost_frames 30: # 连续跟丢 30 帧重置跟踪器触发重新检测 tracker None check_counter 1 cv2.imshow(Tracking, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码在普通笔记本上就能直接跑通验证双模块逻辑的可行性。需要注意的是如果用 640x480 分辨率MobileNet SSD 的检测耗时大约在 20ms 到 50ms如果用 1280x720耗时会明显上升。我的建议是跟踪循环用 640x480 分辨率保证帧率检测校验用降采样后的图像不要在高分辨率图上跑检测性价比太低了。3. 硬件平台、通信架构与嵌入式环境搭建算法在 PC 上跑通之后下一步就是把它移植到无人机平台上。这里的选择会直接影响整个项目的复杂度和开发周期值得认真对比。3.1 无人机平台选型对比我做过的项目和拆解过的方案大致可以分成三个档位平台方案代表硬件开发难度扩展性适合场景成品无人机 SDKDJI Tello / Tello EDU低较弱学习验证、室内实验成品无人机 独立机载电脑DJI Mini 系列 Jetson Nano中中等轻量户外测试DIY 无人机 开源飞控Pixhawk Raspberry Pi / Jetson高强定制化研究、算法验证Tello 是很多初学者入门跟踪无人机的最佳选择原因很简单它自带的 SDK 开放了基础的飞行控制指令包括起飞、降落、悬停、平移、旋转甚至可以通过简单协议发送速度控制指令而且搭载了底部的视觉定位系统室内可以保持稳定的姿态。它的缺点是机身太小、抗风能力差、负载能力基本为零如果想外挂更高性能的计算板或云台就非常困难。DIY 方案的灵活度是成品机无法比的。你可以自由选择机架尺寸、飞控型号、电机功率和电池容量机载电脑的功能也不只是做视觉跟踪还可以记录完整飞行日志、运行更复杂的算法、甚至接 GPS 做自主航线规划。相应的代价是你需要自己处理飞控的参数调优、振动隔离、电磁干扰等一系列硬件问题。第一次把 Pixhawk 调到手感顺手往往就要折腾一到两周。3.2 机载处理器选型目标检测模型的推理需要一定的算力目前我用过的方案主要有这三种Raspberry Pi 4 / Pi 5性价比高生态成熟但 CPU 跑 MobileNet SSD 只能到每秒 2 到 5 帧算力紧张。适合作为通信中继或简单图像采集跑实时检测比较勉强。Jetson Nano / Orin Nano有专门针对深度学习推理优化的 GPU或者在高版本上复用 Tensor Core可以运行 TensorRT 加速后的 YOLO 系列模型是机载视觉处理的主流选择。功耗大约在 5W 到 15W 之间搭配 2S 到 4S 电池即可长期运行。Intel NUC / 迷你主机算力最强甚至能跑最新的实时分割网络但功耗和重量都大通常适合搭载在 450mm 以上轴距的大型无人机上。坦率地说如果预算有限可以先在树莓派上用 320x240 分辨率跑轻量模型验证整条链路再把代码迁移到 Jetson 上提速。迁移成本不高因为 OpenCV、PyTorch/TensorRT 的接口基本一致主要是推理引擎的替换。3.3 通信架构与视频传输通信链路的目标是满足地面站实时监控 飞行指令下发 手动接管三个需求。常用的方案如下控制链路通过 MAVLink 协议与飞控通信推荐用 915MHz/2.4GHz 的遥测数传模块波特率 57600 或 115200视频链路机载摄像头通过 USB 传到机载电脑机载电脑将编码后的 H.264 视频流通过 WiFi 或 4G/5G 图传模块发到地面站应急接管地面站可以随时发送指令暂停自动跟踪切换为手动遥控这是飞行安全的基本保障。在 Tello 平台上一个常见的做法是机载电脑通过 USB 读取摄像头画面处理后将跟踪状态和目标框信息通过 UDP 回传地面站。控制指令则通过 Tello SDK 的指令端口发送例如 rc 0 0 0 0 表示悬停实际操作中会封装成下面这类的速度控制指令。import socket tello_address (192.168.10.1, 8889) sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) def send_rc_control(left_right, forward_backward, up_down, yaw): cmd frc {left_right} {forward_backward} {up_down} {yaw} sock.sendto(cmd.encode(), tello_address) # 示例以 0.3 m/s 的速度向右平移同时逆时针旋转 20°/s send_rc_control(0.3, 0, 0, -20)这里有个比较隐蔽的坑Tello 接收的 rc 指令值的实际含义是归一化速度范围通常是 -1.0 到 1.0不同固件版本映射关系可能有差异。我建议先用小的指令值做标定比如发送 rc 0.1 0 0 0观察飞机实际移动方向和速度再根据实测结果校准增益系数。4. 目标位置到飞行控制指令的转换与核心控制逻辑视觉系统给出了目标框但无人机不会因为看到了目标框就知道往哪里飞。中间还有一层非常重要的转换像素坐标到飞行控制命令的映射。这一步做得不好前面算法再准也白搭。4.1 从图像坐标到机体坐标假定摄像头固定在无人机前方光轴方向与机体前进方向近似一致。目标中心在图像中的像素坐标为 (u, v)画面中心为 (cu, cv)那么目标相对光轴的偏差可以表示为水平偏差dx u - cu正值表示目标在画面右侧垂直偏差dy v - cv正值表示目标在画面下方。控制器的目标就是通过无人机的平移和旋转让 dx 和 dy 都趋近于 0。为了让不同分辨率、不同焦距的摄像头能通用最好把像素偏差归一化。以水平偏差为例dx_ratio (u - cu) / (frame_width / 2)这样 dx_ratio 的取值范围大约是 -1 到 1-1 表示目标在画面最左侧1 表示在最右侧。后续控制器的输入可以直接使用 dx_ratio 和 dy_ratio而不用关心具体像素数。4.2 速度控制设计对于跟踪任务最通用的控制策略是速度控制。无人机不需要精确到某个空间坐标而是期望以某个速度去靠近目标、保持在目标正前方。这里用经典的 PID 控制器作为核心水平方向速度左右平移与 yaw 角速度共同负责消除 dx 偏差前后方向速度负责保持与目标的固定距离通常依据目标框的宽度来估算距离垂直方向速度负责消除 dy 偏差保持目标在画面垂直中心附近。一个简化版本的 PID 控制逻辑如下class PIDController: def __init__(self, kp, ki, kd, output_limit1.0): self.kp kp self.ki ki self.kd kd self.output_limit output_limit self.last_error 0.0 self.integral 0.0 def update(self, error, dt): self.integral error * dt derivative (error - self.last_error) / dt if dt 0 else 0.0 output self.kp * error self.ki * self.integral self.kd * derivative self.last_error error output max(-self.output_limit, min(self.output_limit, output)) return output # 水平偏差控制器输出范围 -0.5 到 0.5 m/s pid_x PIDController(kp0.004, ki0.0001, kd0.001, output_limit0.5) # 垂直偏差控制器 pid_y PIDController(kp0.003, ki0.0001, kd0.0005, output_limit0.4) # 前后距离控制器error 为目标框面积与设定面积的偏差 pid_z PIDController(kp0.05, ki0.001, kd0.01, output_limit0.5)每次从视觉模块拿到新的目标框后更新误差并计算控制量下发到飞控。注意控制频率应该与视觉处理频率保持一致比如视觉处理是 30 帧/秒那么 PID 的执行频率也应该是 30Hz。4.3 距离估计与保持跟踪距离目标框的大小隐含着目标与无人机的距离。假设目标实际高度为 H_real例如成年人约 1.7 米在图像中目标框的高度为 h_pixel 像素摄像头的焦距为 f 像素那么距离估算公式为distance (H_real * f) / h_pixel焦距 f 可以通过相机内参标定获得一个近似的计算方法是f (图像高度像素 * 0.5) / tan(FOV_vertical / 2)。大多数摄像头会在规格书中给出垂直视场角如果没有也可以利用已知尺寸的标定板自己算。距离信息的用处有两个一是实现保持固定跟踪距离比如要求无人机始终离目标 2 米左右二是作为目标丢失时搜索动作的参考距离太远时自动向前靠近找目标距离太近时自动后退。4.4 完整控制循环代码把检测、跟踪、PID 控制组合起来就是完整的跟踪控制循环。以下代码基于 Tello 平台示例import cv2 import time import math class ObjectTrackerApp: def __init__(self, tello_sock): self.tello tello_sock self.pid_x PIDController(0.004, 0.0001, 0.001, output_limit0.5) self.pid_y PIDController(0.003, 0.0001, 0.0005, output_limit0.4) self.pid_z PIDController(0.05, 0.001, 0.01, output_limit0.5) self.tracker None self.check_counter 0 def run(self, frame): h, w frame.shape[:2] box None # 定期的检测校验与初始检测 if self.tracker is None or self.check_counter 30: box detect_target(frame) if box is not None: self.tracker cv2.TrackerCSRT_create() self.tracker.init(frame, box) self.check_counter 0 # 跟踪更新 if self.tracker is not None: box track_frame(frame, self.tracker) if box is None: # 目标丢失悬停等待重新找回 send_rc_control(0, 0, 0, 0) return frame x, y, bw, bh [int(v) for v in box] xc x bw // 2 yc y bh // 2 # 归一化偏差 dx (xc - w / 2) / (w / 2) dy (yc - h / 2) / (h / 2) # 距离偏差目标高度在实际中未知这里用目标框面积做近似让框保持在一定范围内 box_area_ratio (bw * bh) / (w * h) dz 0.018 - box_area_ratio # 期望目标框占比 1.8% # PID 计算 vx self.pid_x.update(dx, 1 / 30.0) vy self.pid_y.update(dy, 1 / 30.0) vz self.pid_z.update(dz, 1 / 30.0) # Tello SDK 指令rc left_right forward_backward up_down yaw # 视觉偏差转换为 yaw 旋转水平偏差大时旋转小时平移 yaw vx * 0.6 left_right vx * 0.4 send_rc_control(left_right, vz, -vy, yaw) # 绘制跟踪框和偏差信息 cv2.rectangle(frame, (x, y), (x bw, y bh), (0, 255, 0), 2) cv2.line(frame, (w // 2, h // 2), (xc, yc), (255, 0, 0), 1) cv2.putText(frame, fdx{dx:.2f} dy{dy:.2f}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) self.check_counter 1 return frame需要注意的一点Tello 的 rc 控制是增量式速度控制无人机在收到持续指令时会持续移动。因此在目标丢失、程序退出或切换模式时一定要显式发送悬停指令否则飞机会沿着最后的速度继续漂移。我在实际测试中就遇到过因为忘记停掉 rc 指令导致飞机撞墙的情况这个细节务必重视。5. 实际飞行调试中的典型问题与排查链路代码写完了第一次拉出去飞的时候一定会遇到各种问题。这里整理几类出现概率最高的故障以及我总结的排查思路帮你少走弯路。5.1 目标跟丢与重新找回策略目标跟丢几乎是永远无法完全避免的。它的成因主要有几类目标被障碍物遮挡、目标快速移动导致运动模糊、光照突变导致检测器失效、以及无人机姿态剧烈变化导致画面跳变。面对跟丢我的建议是分级处理当跟踪框连续 10 到 20 帧没有更新跟踪器反馈失败时切换到悬停等待模式保持当前高度和姿态防止盲目移动导致目标彻底脱离视野同时开启原地扫描模式无人机以非常慢的角速度原地旋转扩大视觉搜索范围检测器持续运行一旦置信度足够高的目标重新出现立即重新初始化跟踪器如果 10 秒内仍然无法找回目标触发返航或降落流程。这是安全底线必须设置。5.2 控制振荡与参数调节经验控制振荡的表现是无人机在目标附近来回晃动或者跟踪框在画面中心附近高频抖动。这个问题的根源几乎都在 PID 参数。我的调参顺序是先调 P再调 D最后加一点 I。具体操作是将 I 和 D 全部设置为 0只保留 P从小到大慢慢增加直到无人机开始出现轻微振荡然后加入 D 项D 负责抑制振荡让系统重新稳定下来最后加入少量 I用于消除稳态误差例如目标始终停在画面中心偏右几像素的固定偏差。一个比较实用的经验值是P 增大时系统响应变快但容易振荡D 增大时系统更稳定但噪声敏感I 只用于补偿恒定偏差。如果发现跟踪速度跟不上目标移动优先增大 P 而不是盲目加大 D。5.3 光照变化与运动模糊的应对户外场景下光照突变是目标检测模型失效的最常见原因。树荫和阳光之间切换时目标的颜色和对比度会发生剧烈变化MobileNet SSD 这类模型很容易把目标置信度降低到阈值以下。应对方法有以下几种按性价比排序降低检测置信度阈值从默认的 0.5 降到 0.3 到 0.4代价是虚警会增加增加图像预处理例如对每一帧做 CLAHE 自适应直方图均衡化提升弱光环境的对比度使用多目标跟踪逻辑在检测器短暂失稳时依靠跟踪器的时序连续性撑过去等检测器恢复后再校正。运动模糊则建议通过缩短相机曝光时间来处理。OpenCV 里可以通过设置摄像头的相关属性调整曝光时间虽然大部分 USB 摄像头对曝光控制的支持参差不齐但值得一试。5.4 安全飞行与合规注意事项最后必须强调安全。目标跟踪无人机属于自动飞行范畴风险等级比手动飞行高一个数量级。我个人的测试流程中有几条底线原则第一次测试必须在室内宽敞场地或专业飞行网笼内进行禁止在人员密集区域直接试飞始终在遥控器上保持手动模式开关随时可以中断自动跟踪接管控制设置地理围栏或者飞行限高限远防止失控后飞出视野每次飞行前检查螺旋桨、电池电量和通信链路是否正常。飞行的合法性方面不同地区对无人机的重量等级、飞行高度、空域管制有不同的管理要求特别是带自动飞行功能的设备通常需要更严格的资质和申报流程。建议在正式试飞前先向当地主管部门了解当前空域的具体管理规定确认自身设备和飞行计划符合要求后再选择合适的场地测试。我做这个项目的过程中最大的体会是目标跟踪无人机的难点从来不在于某一个单独的模块而在于如何把检测算法、嵌入式推理、控制链路和飞行安全系统地融合在一起。这套架构从最初的室内慢速追踪到后期户外复杂场景测试经历了无数次跟丢、撞机和参数重调才逐渐稳定下来。希望这篇文章能帮你把每个环节的路都铺平让你更快地享受到飞机自己盯着目标飞的那种成就感。