基于YOLOv5与Tello TT的无人机目标识别追踪测距实战
简介面向毕业设计、课程设计及目标识别实战的深度学习资源基于YOLOv5框架与大疆教育无人机Tello TT完整实现目标识别、追踪与测距流程数据集以旗帜和圆圈为识别目标模型已完成训练和调优可直接用于实测或二次开发。压缩包共1672个文件大小约269MB包含758张jpg训练图像、694个txt标注文件、94个yaml配置文件、50个Python源码脚本、9个pt模型权重以及xml标注、Dockerfile、csv记录、Markdown说明等类型覆盖全面。附有TensorBoard训练日志方便查看训练曲线与调参依据。目前已有283人学习下载。整套资料提供项目源码、训练好的模型、完整数据集与操作说明文档结构清晰适合计算机视觉、模式识别方向的学生作为毕设或大作业蓝本也可在此基础上修改代码训练其他目标是一套完整可运行的实战项目方案。1. 这套“基于yolov5Tello TT”的方案能做什么为什么我不建议从零搭当你在实训课或技术预研里拿到一台大疆教育无人机Tello TT需求往往不是“飞得稳”而是“看到目标、跟着目标、报出距离”这三件事一口气全跑通。基于yolov5的目标识别检测配合Tello TT是目前成本和上手难度最平衡的路线yolov5网络结构图和预训练权重都是公开的Tello TT 又自带完整的SDK和WiFi视频流把检测结果换算成飞行控制量和距离估计工程上完全可控。本文会把整套落地路径拆开讲覆盖通信链路、数据集与训练超参数、追踪与测距实现、以及我实际踩过的坑。适合手里有Tello TT、想在几天内跑通完整应用、而不是只看理论图纸的人。下面直接进入第一个决定成败的环节通信链路。2. 先打通无人机通信链路连上Tello TT把视频流喂给yolov5Tello TT对外只开放WiFi接口开机后无人机会自己开一个热点电脑连接这个热点之后通过UDP协议就能与飞控通信。官方SDK的逻辑很直接往192.168.10.1的8889端口发送一条command飞控返回ok随后就可以在这个端口上持续下发控制指令。视频流走的是另一条通道开启streamon后飞机会把H.264码流推送到本网络的11111端口地面程序需要自己接收并解码。很多人第一次接触时会觉得自己用socket几行代码就能搞定但实际做下去会发现Tello有一个不太友好的行为如果在一段时间内没收到任何新指令它会自动退出SDK模式下一次指令就失效了。这个“保持心跳”的逻辑还有视频裸流的解码都会消耗大量调试时间。我一般直接用 djitellopy 这个开源库它把这些协议层的细节都封装好了出问题时顺着库的源码看也比自己盲试快得多。2.1 用djitellopy把SDK和视频流一起封装掉安装依赖只需要两个包djitellopy负责飞控协议和视频解码opencv-python负责预览和后续的图像处理pip install djitellopy opencv-python安装完成后不要急着跑代码先梳理一下djitellopy帮我们做了什么。connect()会发送command进入SDK模式并在后台按固定间隔发送心跳指令保证链路不超时streamon()开启视频流get_frame_read()返回一个后台线程持续接收并解码的帧读取器。下面这段代码是验证链路的最小单元不需要起飞就能看到画面适合刚拿到飞机时做自检。from djitellopy import Tello import cv2 import time tello Tello() tello.connect() tello.streamon() frame_reader tello.get_frame_read() start time.time() try: while time.time() - start 30: frame frame_reader.frame if frame is None: continue cv2.imshow(tello_stream, frame) if cv2.waitKey(1) 0xFF ord(q): break finally: cv2.destroyAllWindows() tello.streamoff()这段代码里最关键的两个参数是connect()之后的链路状态和get_frame_read()的返回频率。frame_reader.frame在后台解码完成后会被持续更新所以主循环里不需要再做耗时操作。注意代码里没有调用takeoff因此也不需要land只是纯链路验证如果后续加了起降动作记得在程序退出前写tello.land()否则飞机可能悬停在空中失去控制这是新手最容易翻车的地方。2.2 起飞、降落、遥控指令的最小控制顺序链路验证通过以后可以尝试让飞机起飞并执行几秒钟的悬停。Tello的控制状态机比想象中严格必须先takeoff之后才能发送send_rc_control而且遥控指令最好保持在20到100毫秒一条的频率过于频繁会导致指令被合并过于稀疏又可能触发超时保护。from djitellopy import Tello import time tello Tello() tello.connect() tello.takeoff() # 悬停2秒再做一个原地旋转 tello.send_rc_control(0, 0, 0, 0) time.sleep(2) tello.rotate_clockwise(90) time.sleep(2) tello.land()send_rc_control的前四个参数分别是左右速度、前后速度、上下速度、偏航速度取值范围是-100到100。这里传入全0表示悬停rotate_clockwise(90)则是角度控制指令传90表示顺时针转90度。实际项目里追踪目标时更多用send_rc_control的连续速度模式角度控制反而少用因为角速度模式能更平滑地修正偏差。2.3 链路通了之后网上最容易忽略的时序问题链路通了以后常见的时序坑有三个。第一command握手完成到takeoff之间最好留出至少2秒有些飞控固件在收到第一条指令后需要时间同步状态立刻起飞容易报错。第二退出脚本前必须确保飞控收到land除了正常降落紧急情况可以直接调用tello.emergency()触发紧急停桨但这个指令会直接让电机停转只能在测试环境里用。第三电脑的无线网卡同时连接无人机热点时如果周围2.4GHz信道拥挤画面会出现周期性马赛克和卡顿这是信道干扰问题不是代码问题换到干扰少的频段或关掉周围的高带宽设备通常能缓解。3. 目标识别检测把yolov5训练成你认得的目标探测器标题里的目标识别检测落到实现上就是训练一个yolov5模型并部署到合适的算力上。如果要识别的目标是常见COCO类别以外的物体比如锥桶、标识牌、某一类工具就不能直接用预训练权重必须用目标数据集再训练一遍。这也是为什么项目里一定会附带数据集和训练好的模型数据决定了模型认得什么模型决定了后续追踪测距的上限。3.1 数据集准备与YOLO格式转换YOLO格式的标签是纯文本文件每一行代表一个目标对象格式为class_id 中心点x 中心点y 宽度 高度其中x、y、w、h都是归一化到0到1之间的浮点数。举个例子一张640x480的图里有一个宽100像素、高80像素的锥桶其左上角坐标为(10, 20)标签行就会是0 0.09375 0.1375 0.15625 0.16667。这个格式看起来很简洁但实际转换时最常出的问题就是忘记归一化把绝对像素当成比例直接写进去导致训练时框的位置完全错乱。很多公开数据集或自采数据是先做成VOC、LabelMe或Json格式的再统一转换。下面是VOC格式转YOLO格式的核心函数def voc_to_yolo(size, box): # size: 图片宽高 (w, h) # box: [xmin, ymin, xmax, ymax] dw 1.0 / size[0] dh 1.0 / size[1] x_center (box[0] box[2]) / 2.0 y_center (box[1] box[3]) / 2.0 w box[2] - box[0] h box[3] - box[1] return [ round(x_center * dw, 6), round(y_center * dh, 6), round(w * dw, 6), round(h * dh, 6), ] if __name__ __main__: bbox [10, 20, 110, 120] # 左上角(10,20)右下角(110,120) print(voc_to_yolo((640, 480), bbox))这里的核心运算就是把像素坐标除以图片宽高强制映射到0到1区间。转换完成后还要做一次越界检查凡是某一行的w或h大于1或者中心点位置为负数都需要回到标注工具里修复不要指望训练过程帮你纠错。3.2 yolov5训练自己的数据集超参数怎么调数据就绪后先要写一个数据配置文件# custom.yaml path: /home/user/tello_project/dataset train: train/images val: val/images nc: 1 names: [cone]nc是类别数量names要和标签文件里的 class_id 一一对应。训练时把yolov5代码仓库clone到本地运行train.py即可下面是我最常用的一组命令python train.py \ --data custom.yaml \ --weights yolov5s.pt \ --img 640 \ --batch-size 16 \ --epochs 200 \ --hyp data/hyps/hyp.scratch-low.yaml \ --patience 50 \ --project runs/tello_exp这组超参数里img选640是精度和速度的折中。如果后续要把模型部署到树莓派4B或RK3568这类设备我建议直接训练时就降到416推理速度会快不少精度损失对单类目标来说通常可以接受。batch-size如果大于显存容量就调小到8或4不要硬跑。epochs配patience是最稳妥的组合patience50表示连续50轮验证集指标没有提升就提前停止省时间也防过拟合。hyp文件决定数据增强的强度。hyp.scratch-low.yaml是保守策略训练数据比较干净时用它收敛更快hyp.scratch-high.yaml带了更强的随机翻转、色彩扰动和Mosaic增强适合目标出现在复杂光照背景下的场景。Tello拍摄的画面常有运动模糊和反光我更倾向于用hyp.scratch-high.yaml起步。3.3 导出与端侧部署从best.pt到树莓派和RK3568训练完成后runs/tello_exp/weights/下会有best.pt和last.ptbest.pt就是验证集效果最好的权重。如果整个检测推理都在带GPU的电脑上运行直接用PyTorch加载即可import torch import cv2 model torch.hub.load( ultralytics/yolov5, custom, pathruns/tello_exp/weights/best.pt, force_reloadTrue, ) model.conf 0.35 model.iou 0.45 frame cv2.imread(test.jpg) results model(frame) boxes results.pandas().xyxy[0] for _, row in boxes.iterrows(): x1, y1, x2, y2 map(int, [row.xmin, row.ymin, row.xmax, row.ymax]) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(result.jpg, frame)注意框架返回的框坐标是绝对像素results.pandas().xyxy[0]里每一行包含xmin、ymin、xmax、ymax、confidence、class可以直接供后续追踪模块使用。但如果要把模型放到树莓派4B、树莓派5或RK3568这类边缘设备上问题就来了。yolov5s用PyTorch在树莓派CPU上跑也就是2到5FPS很难做实时追踪。常见做法是先把best.pt导出成ONNX再转成 RKNN 或直接用ONNX Runtime推理。导出命令为python export.py --weights best.pt --include onnx --opset 11转成ONNX之后后处理就要自己写了。yolov5的输出是多个特征图层需要做坐标解码、置信度过滤和非极大值抑制这就是常说的yolov5后处理。如果板子是RK3568还要通过rknn-toolkit2做INT8量化用验证集的几十张图片做校准。量化后一般会有3%到10%的精度掉点如果掉得太多可以检查量化后输出层的置信度分布必要时保留第一层和最后一层为浮点推理。4. 追踪测距从目标框到无人机控制量的换算检测模型给的是目标框而无人机需要的是速度指令和距离数值这是两个不同的纬度。追踪的核心思路是用目标框中心点相对画面中心的偏差作为误差经过PID控制器转换成send_rc_control的前后左右速度测距则依赖相机模型用已知目标尺寸和像素高度估算深度。4.1 追踪控制像素偏差怎么变成飞行指令画面坐标系里图像左上角是原点x轴向右y轴向下。目标框中心减去画面中心得到err_x和err_y。err_x为正说明目标在画面右侧无人机需要右移err_y为正说明目标在画面下方无人机需要上升注意y轴方向要取反。PID控制器是这里最常用的闭环方案。下面这个实现足够用于Tello控制class PIDController: def __init__(self, kp, ki, kd, max_out): self.kp kp self.ki ki self.kd kd self.max_out max_out self._integral 0.0 self._last_error 0.0 def update(self, error, dt): self._integral error * dt derivative (error - self._last_error) / dt output self.kp * error self.ki * self._integral self.kd * derivative self._last_error error return max(-self.max_out, min(self.max_out, output))这里kp控制响应速度ki消除稳态偏差kd抑制震荡。Tello本身体积小、惯性弱kp超过0.2之后飞机就会在目标中心来回摆动我通常把kp定在0.12到0.18之间ki给0.001左右kd给0.02左右。max_out限制输出幅值在-0.6到0.6之间这是为底层的rc指令幅值留了缓冲避免输出直接顶满导致飞机冲过头。把模型和PID串起来的核心控制循环大概是这样的import time from djitellopy import Tello tello Tello() tello.connect() tello.takeoff() model torch.hub.load( ultralytics/yolov5, custom, pathbest.pt, force_reloadTrue ) pid_x PIDController(0.15, 0.001, 0.02, 0.6) pid_y PIDController(0.15, 0.001, 0.02, 0.6) last_t time.time() while True: frame tello.get_frame_read().frame if frame is None: continue results model(frame) dets results.pandas().xyxy[0] if len(dets) 0: tello.send_rc_control(0, 0, 0, 0) continue x1, y1, x2, y2 dets.iloc[0][[xmin, ymin, xmax, ymax]].astype(int) cx (x1 x2) / 2 cy (y1 y2) / 2 h, w frame.shape[:2] err_x cx - w / 2 err_y cy - h / 2 now time.time() dt max(now - last_t, 1e-4) last_t now vx pid_x.update(err_x / (w / 2), dt) vy pid_y.update(-err_y / (h / 2), dt) tello.send_rc_control(int(100 * vx), 0, int(100 * vy), 0) time.sleep(0.1)这段代码里dets.iloc[0]只取置信度最高的那个目标简化了多目标情况下如何选择追踪对象的逻辑。实际项目中如果画面里有多个同类目标通常按最大框或中心点最近的原则选一个这属于业务策略问题不要写在控制代码里。4.2 单目测距相机模型与近似公式Tello TT没有深度相机单目测距在数学上只有一个近似解当一个物体垂直于光轴且目标实际尺寸已知时距离与像素高度成反比distance f * real_height / pixel_height这里的f是相机焦距像素为单位real_height是目标实际高度米pixel_height是目标框的像素高度。换算代码很短def estimate_depth(box_h_pixel, f_pixel, real_h_meter): if box_h_pixel 1: return None return real_h_meter * f_pixel / box_h_pixel depth estimate_depth(box_h_pixel180, f_pixel921, real_h_meter0.30) print(depth) # 大约1.53米如果你没有做严格的内参标定f_pixel可以用相机水平视场角估算f_pixel (图像宽度 / 2) / tan(水平视场角 / 2)。拿Tello的1080p画面和标称视场角来算结果在一个合理近似范围内。不过要注意的是这个公式在目标偏离心位置或有倾斜角度时会严重失真所以测距结果最好经过一个时间滑窗平滑不要直接把单帧结果当准值显示出来。4.3 把检测、追踪、测距串成闭环完整闭环中测距的作用不只是显示一个数字它还能改变追踪行为。比如当目标距离小于0.8米时应该停止前向速度只做左右和平移控制防止无人机撞上目标当距离大于2米时可以允许低速前飞让目标维持在可跟踪范围内。这个逻辑可以用一个简单的状态判断if depth is not None: if depth 0.8: forward_speed 0 elif depth 2.0: forward_speed 0.3 else: forward_speed 0.15 else: forward_speed 0forward_speed最终会作为send_rc_control的第二个参数传入。这样检测、追踪、测距就连成了完整的自主闭环这也是整个项目标题里“目标识别检测追踪测距”最核心的交付状态。5. 避坑这套方案最容易翻车的5个位置即使链路、模型、控制都单独跑通了整合在一起还是有很多意外情况。下面这几个坑是这套方案里我自己踩过或者亲眼见过别人踩的按现象、原因、解决的顺序写方便你遇到时快速定位。5.1 现象检测正常画面上也有框但飞机在原地乱晃原因PID控制环的频率和检测帧率不一致或者某一帧检测到了错误目标置信度低的误检框被当成追踪目标导致控制量来回跳变。解决把所有检测结果先做一次置信度过滤低于0.3的直接丢弃在进入PID前对目标框中心做滑动平均例如维护最近5帧的中心点取中位数。还有一个笨办法地面测试时把无人机的电机桨叶拆掉或者放在一个固定支架上运行控制循环观察输出的速度指令曲线是否有高频抖动。这一步在起飞前做能省掉大量炸机风险。5.2 现象视频流延迟高无人机追不上目标总是慢半拍原因WiFi信道拥堵或者地面端是用CPU软解H.264导致整个循环耗时过高。Tello的实时视频流是H.264硬编码出来的编码延迟本身不高问题通常出在传输和解码链路。解决先看丢包率和帧间隔。在djitellopy里可以打印frame_reader.frame_count的变化速度如果长时间低于15FPS优先检查无线环境。软件层面可以降低yolov5推理的输入分辨率从640降到416同时关闭OpenCV的显示窗口预览这会减少主循环的阻塞。5.3 现象yolov5训练时Loss能降下来但推理时所有预测框都偏大或偏小原因标签归一化时把像素坐标和归一化比例混用了。常见做法是标注工具导出的是VOC绝对坐标而转换脚本里忘了除以宽高更隐蔽的是图片在预处理时被letterbox压到方形但标签没有跟着做相同的缩放和padding。解决在训练前写一个校验脚本打印所有标签中w或h大于1.0的行训练完成后用torch.hub.load加载模型对一张图片预测再把预测框坐标画在原图上人工检查框是否贴合目标边缘。这个检查比看mAP重要得多因为它能直接暴露标签和预处理不一致的问题。5.4 现象飞行中突然断联或者takeoff之后飞控不响应原因多半是电量过低触发了飞控保护。Tello的电量保护比较激进电量低于20%时部分姿态指令会被忽略低于10%时可能直接强制降落。如果地面程序又持续发送控制指令会显得非常混乱。解决程序启动时检查get_battery()低于40%就拒绝起飞每次降落后不要立刻换电池继续飞给飞控和电池一个冷却时间。另外注意长时间把无人机放在启动状态但不起飞电池也会持续下降测试过程中要经常看一眼电量。5.5 现象模型部署到树莓派或RK3568之后检测速度从30FPS掉到3FPS原因没有做模型转换和量化直接在CPU上用PyTorch跑原始FP32权重。PyTorch在ARM CPU上的算子优化远不如ONNX Runtime和RKNN。另一个常见问题是没有做输入分辨率封装每一帧都做letterbox、缩放和数据类型转换这些Python层的耗时会比推理本身还高。解决在边缘端选择ONNX Runtime或RKNN做推理并且用队列把图像采集、预处理、推理串成三个独立线程。预处理里尽量用cv2.dnn.blobFromImage或矩阵运算替代逐像素操作。如果板子支持NPU比如RK3568优先走RKNN的INT8量化路线。6. 最后一课用回放和误差曲线验证追踪效果调PID和调模型有一个共同点感觉会骗人数据不会。我不建议靠肉眼看着画面说“好像追上了”因为人的主观判断会掩盖掉严重的稳态误差。我的习惯是给每个测试批次写一个CSV日志文件记录帧序号、检测置信度、目标框中心偏移、PID三个通道的输出、最终发送的rc速度以及当前测距结果。等测试飞完把CSV拖进Matplotlib或者Excel里画成曲线一眼就能看出控制量有没有高频抖动、距离估计有没有跳变。具体的验证顺序我一般是这样先把目标物体放在地面静止不动操作无人机飞到目标正前方记录一组数据确认稳定悬停时误差曲线没有振荡然后让目标以缓慢速度前后移动观察无人机跟随的延迟是否小于300毫秒最后才做完整的巡航测试。调参顺序也不要乱来先调kp到响应不太迟滞为止再加一点kd消除振荡最后微调ki补偿稳态偏移。ki给大了反而会让飞机冲过头这是最常见的操作陷阱。距离测距部分可以做一个标定表在室内用激光测距仪当作参照物无人机悬停在1米、1.5米、2米、2.5米、3米这几个位置分别记录单目测距输出和真实距离做一条误差修正曲线。例如下表是我在一台Tello上记录过的典型结果真实距离(m)单目估算(m)误差(m)1.001.120.121.501.630.132.002.280.282.502.790.293.003.410.41可以看到误差在远距离时会线性放大这时可以在输出端做一次多项式补正。还有一个容易被忽略的细节测距用的box_h_pixel要选目标框的高度而不是宽度因为锥桶、行人这类目标在侧向偏移时不满足光轴垂直假设高度的尺寸在多数姿态下更接近稳定值。结尾说一个我自己的习惯每次调完一套参数我会把best.pt、转换脚本、视频回放和CSV日志存在一个以时间命名的目录里比如exp_20250412_tello_cone。下次调试如果想回到之前某个版本只要把对应权重和控制参数换回来就行这就是留后悔药。正是这个习惯让我在反复调整模型和控制参数时从来没有丢失过可用状态也希望这一点能帮到你。本文还有配套的精品资源点击获取