
简介这份资源包面向K12阶段学生与AI入门教师提供基于YOLOv5与大疆教育无人机Tello TT的目标识别、检测、追踪与测距完整方案将深度学习理论落地到真实飞行场景帮助学习者在动手实践中理解目标检测与无人机编程。包内共1667个文件约269.13MB以758张jpg图像与694个txt标注构成“旗”“圈”两类目标数据集另含46个py脚本、94个yaml配置、9个pt权重文件及说明文档覆盖数据准备、模型训练、推理部署与无人机控制全流程。资源已吸引679人学习下载训练好的模型可直接调用省去从零训练成本配套文档讲解环境配置、模型加载、视频流处理与飞行控制等环节便于快速复现并二次开发适合作为STEM课程项目或竞赛实践素材。1. 从 Tello TT 到 YOLOv5一套能飞起来的目标识别追踪测距方案到底长什么样很多人第一次听到「无人机 YOLOv5」的组合脑子里浮现的是那种电影里自动锁定、自动跟随的炫酷画面但真正上手才发现卡点根本不在模型精度而在「怎么让飞机看到的东西实时喂给模型、模型算完的结果又怎么变成飞控能懂的指令」。这套基于 YOLOv5 加大疆教育无人机 Tello TT 的方案解决的正是这条链路Tello TT 负责采集前视画面并执行飞行指令YOLOv5 负责在画面里框出目标中间再加一层追踪与测距逻辑把像素坐标换算成实际距离和偏航修正量。它适合两类人一类是想把 YOLOv5 从「跑通 demo」推进到「部署到真实移动平台」的算法工程师另一类是想用 Tello TT 做教学或竞赛项目、需要完整闭环而不是零散代码的学生和老师。整套东西包含源码、数据集、训练好的模型和说明文档意味着你不需要从零标注、从零训练可以直接站在一个能跑通的起点上去调参数、改逻辑、换场景。但「能跑通」和「跑得稳」之间还有一大段距离后面几章我会把这段距离拆开讲清楚。2. Tello TT 与 YOLOv5 的链路拆解为什么不是「模型一接就能飞」2.1 Tello TT 的图传与指令通道到底给了你什么Tello TT 和普通 Tello 最大的区别在于它支持教育场景下的扩展编程接口但底层图传依然是通过 UDP 把 H.264 视频流推到本地指令通道则是另一套 UDP 端口。这意味着你不能像接 USB 摄像头那样直接cv2.VideoCapture(0)就完事中间必须有一个解码环节。常见做法是用djitellopy这个库来封装指令和视频流它内部帮你处理了streamon、streamoff和帧读取但帧率并不稳定实测在 720p 下大概 25 到 30 帧遇到 Wi-Fi 干扰会掉到 15 帧以下。这里第一个容易翻车的地方是很多人把djitellopy的get_frame()直接塞进 YOLOv5 的推理循环结果发现延迟越积越大。原因是get_frame()是阻塞式的而 YOLOv5 推理本身也要时间两者串行之后每一帧的处理时间等于「取帧 推理 后处理 指令发送」一旦超过 40 毫秒画面就会明显滞后。我一般会开一个独立线程专门取帧用一个长度为 1 的队列做缓冲保证推理端永远拿的是最新帧而不是排队等旧帧。import threading import queue from djitellopy import Tello frame_queue queue.Queue(maxsize1) def frame_producer(tello): while True: frame tello.get_frame() # 阻塞式读取放在独立线程 if frame_queue.full(): frame_queue.get_nowait() # 丢弃旧帧只保留最新 frame_queue.put(frame) tello Tello() tello.connect() tello.streamon() threading.Thread(targetframe_producer, args(tello,), daemonTrue).start()这段代码的关键参数是maxsize1它决定了缓冲策略是「只留最新」而不是「先进先出」。如果你把maxsize设成 5 或 10延迟会肉眼可见地增加因为推理端会按顺序消费旧帧。另一个参数是daemonTrue保证主程序退出时取帧线程不会卡住进程。取帧线程里不要做任何图像处理只做get_frame()和入队处理逻辑全部放到推理端否则线程之间会互相拖慢。2.2 YOLOv5 推理端的输入尺寸与置信度阈值怎么定YOLOv5 默认输入是 640×640但 Tello TT 的图传分辨率通常是 960×720。如果你直接把 960×720 的帧 resize 到 640×640长宽比会变形小目标检测会受影响。常见做法是保持长宽比做 letterboxYOLOv5 的datasets.py里已经有现成实现你只需要在推理脚本里调用letterbox函数。但 letterbox 会引入灰边灰边区域如果被误检后处理时要按原始比例裁剪回去。置信度阈值conf_thres和 NMS 的iou_thres是两个必须根据场景调的参数。默认conf_thres0.25、iou_thres0.45在通用数据集上还行但在无人机视角下目标往往偏小且背景复杂conf_thres设 0.25 会引入大量误检设 0.5 又容易漏掉远距离目标。我一般会先跑一段离线视频把conf_thres从 0.3 到 0.6 按 0.05 步进扫一遍看误检和漏检的平衡点。iou_thres在单目标追踪场景下可以适当调低到 0.4因为同一类目标重叠不多低一点能减少漏合并。import cv2 import torch from models.experimental import attempt_load from utils.general import non_max_suppression, scale_coords from utils.datasets import letterbox model attempt_load(best.pt, map_locationcpu) model.eval() img frame_queue.get() img_letterbox, ratio, pad letterbox(img, new_shape640, autoFalse) img_tensor torch.from_numpy(img_letterbox[:, :, ::-1].transpose(2, 0, 1)).float() / 255.0 img_tensor img_tensor.unsqueeze(0) with torch.no_grad(): pred model(img_tensor)[0] pred non_max_suppression(pred, conf_thres0.4, iou_thres0.4) if pred[0] is not None: det pred[0] det[:, :4] scale_coords(img_tensor.shape[2:], det[:, :4], img.shape).round()letterbox的autoFalse表示固定尺寸填充不自动计算最小矩形这样每次输入尺寸一致推理时间更稳定。scale_coords把检测框从 640×640 映射回原始帧尺寸这一步不能省否则后续测距用的像素坐标全是错的。conf_thres0.4和iou_thres0.4是我在室内光照稳定场景下的常用值室外强光下我会把conf_thres提到 0.5因为过曝区域容易产生虚假高置信度。2.3 追踪与测距从像素框到实际距离的换算逻辑追踪部分常见做法是「检测 简单匹配」不一定非要上 DeepSORT。如果场景里只有一个目标直接用上一帧的框和当前帧的框做 IoU 匹配IoU 大于 0.3 就认为是同一个目标然后取当前帧的框中心作为追踪点。这样做的好处是计算量极小在 Tello TT 这种算力有限的平台上更现实。如果目标会频繁遮挡或交叉再考虑加卡尔曼滤波预测下一帧位置。测距是这套方案里最容易出玄学的地方。单目测距没有绝对深度只能靠「已知目标实际尺寸 像素尺寸 相机焦距」来估算。公式是距离 (实际宽度 × 焦距) / 像素宽度。Tello TT 的相机焦距官方没有直接给但可以用已知距离和已知尺寸的物体标定出来。我一般会拿一张 A4 纸宽 0.21 米放在离飞机 1 米、2 米、3 米的位置分别记录像素宽度然后反推焦距取平均值。KNOWN_WIDTH 0.21 # A4 纸宽度单位米 FOCAL_LENGTH 620.0 # 标定得到的焦距单位像素 def estimate_distance(pixel_width): if pixel_width 0: return -1 return (KNOWN_WIDTH * FOCAL_LENGTH) / pixel_widthFOCAL_LENGTH这个值因分辨率而异如果你把图传分辨率从 960×720 改成 1280×720焦距要重新标定。estimate_distance返回 -1 表示像素宽度无效调用方要处理这个异常不能直接拿 -1 去算飞行指令。实际使用中测距误差在 1 米内大概 ±0.15 米3 米外会扩大到 ±0.5 米所以这套测距只适合做粗略避障或跟随距离保持不能用来做精确降落。3. 把 YOLOv5 训练到能认你的目标数据集、超参与模型导出3.1 数据集标注与 YOLO 格式转换的四个边界坑标题里带了数据集意味着你拿到的是已经标注好的数据但如果你想加自己的目标类别就得自己标。常见工具是 LabelImg 或 Roboflow导出格式选 YOLO 格式每张图对应一个.txt每行是类别索引 中心x 中心y 宽 高全部归一化到 0 到 1。这里第一个坑是归一化分母用错有人用图像宽度归一化 x 和宽用高度归一化 y 和高结果训练时框全偏了。正确做法是 x 和宽除以图像宽度y 和高除以图像高度。第二个坑是类别索引从 0 开始还是从 1 开始。YOLOv5 要求从 0 开始如果你从 1 开始训练时不会报错但推理时类别名会整体错位。第三个坑是空标注文件有些图没有目标LabelImg 会生成一个空.txtYOLOv5 默认会跳过空文件但如果你手动删了又建可能留下 0 字节文件训练时会被当成背景图影响召回。第四个坑是图像和标注文件名不一致比如图片叫img_001.jpg标注叫img_001.JPG.txtYOLOv5 找不到对应标注会直接报错。# 检查标注文件与图像是否一一对应 for img in images/*.jpg; do base$(basename $img .jpg) if [ ! -f labels/$base.txt ]; then echo 缺失标注: $base fi done这段脚本遍历images目录下所有.jpg检查labels目录下是否有同名.txt。如果有缺失训练前必须补齐或删除对应图像否则 YOLOv5 在构建数据集缓存时会中断。basename的第二个参数.jpg表示去掉扩展名如果你的图像是.png要相应改成.png。3.2 超参数怎么调从 yolov5s 到自定义数据集的迁移策略标题里带了训练好的模型大概率是基于 yolov5s 或 yolov5m 在自定义数据集上微调的。如果你想复现或改进第一步是确认data.yaml里的nc类别数和names与你的数据集一致。nc不对会直接导致模型输出维度错位训练时 loss 不下降。第二步是选择预训练权重如果自定义数据集和 COCO 差异大用yolov5s.pt做迁移学习冻结前几层训练如果差异小直接全量微调。学习率lr0默认是 0.01但在小数据集上少于 2000 张我一般会降到 0.001否则容易过拟合。batch-size在显存允许的情况下尽量大Tello TT 场景下图像分辨率不高batch-size16在 8GB 显存上能跑。epochs默认 300但小数据集 100 到 150 就够了看mAP0.5不再上升就可以停。hyp.yaml里的mosaic增强默认开启它会把四张图拼成一张对小目标检测有帮助但如果你的目标本身就很大mosaic 可能让目标被裁切这时候可以关掉。python train.py \ --img 640 \ --batch 16 \ --epochs 150 \ --data data/custom.yaml \ --weights yolov5s.pt \ --cfg models/yolov5s.yaml \ --hyp data/hyp.scratch.yaml \ --name tello_exp--img 640是训练输入尺寸要和推理时保持一致否则精度会掉。--weights yolov5s.pt指定预训练权重如果本地没有脚本会自动下载。--hyp指定超参数文件hyp.scratch.yaml是默认增强配置如果你想关掉 mosaic就在这个文件里把mosaic: 1.0改成mosaic: 0.0。--name是实验名训练日志和权重会保存在runs/train/tello_exp下。3.3 训练完的模型怎么导出成 Tello TT 能用的格式训练完的best.pt是 PyTorch 格式在 PC 上推理没问题但如果你想部署到树莓派 5 或 Jetson 这类边缘设备通常要转成 ONNX 或 TensorRT。YOLOv5 自带export.py一条命令就能转 ONNX。转的时候注意--img要和训练时一致--batch设 1 用于推理。转完的 ONNX 可以用onnxruntime加载推理速度比 PyTorch 快 20% 到 30%。python export.py \ --weights runs/train/tello_exp/weights/best.pt \ --include onnx \ --img 640 \ --batch 1--include onnx表示导出 ONNX 格式如果要 TensorRT 就改成--include engine但 engine 只能在有 GPU 的机器上生成。--batch 1是推理时的批大小Tello TT 场景下每次只处理一帧设 1 最合适。导出后的 ONNX 文件在best.onnx用onnxruntime.InferenceSession加载即可输入名和输出名可以用session.get_inputs()查看。4. 避坑与排查Tello TT YOLOv5 联调时最容易翻车的五件事4.1 图传延迟越跑越大画面像幻灯片现象是刚开始几秒还正常跑半分钟后画面明显滞后指令发出去要等一两秒才响应。原因通常是取帧和推理在同一个线程里串行执行帧队列积压。解决方法是把取帧放到独立线程队列长度设为 1推理端每次取最新帧。另外检查tello.get_frame()是否在循环里被多次调用有些人为了「多取几帧选最好的」反而加剧了积压。4.2 检测框位置整体偏移目标明明在中间却框在左上角现象是框的大小看起来对但位置整体偏。原因多半是scale_coords的输入尺寸传错了比如把 letterbox 后的尺寸当成原始尺寸传进去。解决方法是确认scale_coords的第一个参数是推理输入尺寸如 640×640第二个参数是原始帧尺寸如 960×720顺序不能反。另外检查 letterbox 的pad值是否在缩放时被正确使用。4.3 测距值跳变严重同一距离下读数忽大忽小现象是飞机悬停不动测距值在 1 米到 1.5 米之间反复跳。原因是检测框的像素宽度每帧都在变尤其是目标边缘模糊时。解决方法是对像素宽度做滑动平均取最近 5 帧的中位数而不是均值中位数对异常值更鲁棒。另外可以把conf_thres提高一点减少低置信度框对宽度的干扰。4.4 模型在 PC 上跑得好导出 ONNX 后精度掉一大截现象是 PyTorch 推理正常ONNX 推理框全乱。原因通常是导出时没有指定--dynamic或--simplify导致某些算子不被 ONNX 支持。解决方法是导出时加--simplify它会调用onnx-simplifier优化计算图。如果还不行检查输入归一化是否一致PyTorch 里是/255.0ONNX 里也要做同样的预处理不能依赖模型内部归一化。4.5 Tello TT 指令发出去没反应但图传正常现象是能收到画面但takeoff、move_forward等指令无效。原因可能是指令端口被占用或者djitellopy的连接没有正确初始化。解决方法是先tello.connect()再tello.streamon()顺序不能反。如果还是不行检查防火墙是否拦截了 UDP 端口Tello TT 默认用 8889 发指令、11111 收状态这两个端口要放行。5. 进阶技巧用滑动窗口和卡尔曼滤波把追踪测距做稳如果你已经跑通了基础版本下一步大概率会遇到「目标稍微被遮一下就跟丢」或者「测距值抖得没法用」的问题。我自己的习惯是在检测框后面加一层轻量追踪用卡尔曼滤波对框的中心点和宽高做预测当 YOLOv5 某一帧漏检时用预测值顶替等下一帧检测恢复再重新关联。这样做的好处是不需要引入 DeepSORT 那种重量级方案计算量增加不到 5%但在 Tello TT 这种算力有限的平台上足够用。卡尔曼滤波的状态向量我一般设成[x, y, w, h, vx, vy, vw, vh]前四个是框的中心和宽高后四个是它们的变化率。观测向量就是 YOLOv5 输出的框。过程噪声Q和观测噪声R需要根据实际抖动调Q太小会导致预测跟不上真实运动R太大会导致滤波过度平滑、响应变慢。我的经验值是Q0.01、R0.1在室内慢速移动场景下比较平衡。import numpy as np from filterpy.kalman import KalmanFilter kf KalmanFilter(dim_x8, dim_z4) kf.F np.eye(8) for i in range(4): kf.F[i, i4] 1.0 # 位置由速度积分得到 kf.H np.eye(4, 8) # 只观测位置和宽高 kf.Q np.eye(8) * 0.01 kf.R np.eye(4) * 0.1 kf.P np.eye(8) * 10.0 def update_tracker(detection): kf.predict() if detection is not None: kf.update(detection) return kf.x[:4] # 返回平滑后的框kf.F是状态转移矩阵前四行后四列设 1 表示位置按速度线性变化。kf.H是观测矩阵只取前四个状态。kf.Q和kf.R是过程噪声和观测噪声调这两个值就是调「信任预测」还是「信任检测」。kf.x[:4]返回平滑后的框可以直接拿去做测距和指令生成。如果某一帧detection是Nonekf.update不调用只靠predict顶一帧下一帧检测恢复后再update这样追踪不会断。测距部分我还会加一个滑动窗口中位数滤波窗口大小取 5。中位数比均值抗异常值尤其是当检测框突然跳变时中位数不会跟着跳。窗口大小不要超过 7否则响应会明显滞后飞机已经飞近了但测距值还没跟上。from collections import deque distance_window deque(maxlen5) def stable_distance(pixel_width): d estimate_distance(pixel_width) if d 0: distance_window.append(d) if len(distance_window) 0: return -1 return float(np.median(distance_window))deque(maxlen5)自动维护最近 5 个值超出就丢弃最旧的。np.median取中位数比np.mean更抗跳变。如果distance_window为空返回 -1 让调用方知道当前没有有效测距值。这套组合拳下来测距值的抖动能从 ±0.5 米压到 ±0.2 米以内追踪在短暂遮挡下也能保持连续。最后说一个我踩过的坑卡尔曼滤波的初始状态kf.x不要设成全零否则前几帧预测会严重偏离。我一般用第一帧检测值初始化位置速度设零kf.P设大一点表示初始不确定性高让滤波器快速收敛。这套逻辑我前后改了三四版才稳定希望帮到你。本文还有配套的精品资源点击获取