ARTICLE DETAIL

资讯详情

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

基于YOLOv5与Tello TT的无人机目标追踪与单目测距实战指南

基于YOLOv5与Tello TT的无人机目标追踪与单目测距实战指南 简介面向计算机视觉、深度学习和无人机应用方向的学习者这套资源基于YOLOv5算法并融合大疆教育无人机Tello TT完成了目标识别检测、跟踪与测距的完整流程。数据集针对旗帜和圆圈两类目标做了精细标注模型经过训练调优可直接用于毕业设计、课程设计或项目实战。压缩包内共包含1672个文件涵盖JPG图像数据、TXT标注文件、YAML配置文件、Python源码、预训练PT权重、操作说明文档以及TensorBoard训练日志等整体大小约269.18MB内容预览中的训练记录也体现了多次迭代调优过程。目前已有283人学习下载适合正在做毕业设计或需要深度学习视觉项目参考的同学。资源附带完整源码、数据集、模型和说明文档既可开箱即用也可以在此基础上做二次开发训练其他目标识别模型灵活运用于更广泛的研究场景。1. 从刷 mAP 到能飞起来追目标差的不只是模型把 YOLOv5 模型跑通到刷出 mAP和让大疆 Tello TT 无人机在教室里完成“识别旗帜→追踪→报告距离”的完整闭环是两件事。前者是离线训练问题后者是实时性、通信与边界条件问题640×640 输入在双核 CPU 上推理耗时 60~90ms无人机自旋时运动模糊会让单帧置信度掉到 0.3 以下控制指令延迟一旦超过 200ms飞行器就开始原地画圈。下面把数据集标注、训练调优、SDK 通信、追踪控制和单目测距五条线逐一拆开给出能直接落地的代码、命令与参数。这套资源正好覆盖了毕业设计最常见的需求——目标识别检测加追踪测距模型已经训练调优过剩余工作量基本都在把检测结果接进飞控逻辑。适合正在做毕设或课程设计、手上有或准备入手Tello TT 的深度学习与计算机视觉方向学习者。2. 模型选型、数据集标注与目录结构2.1 为什么首选 YOLOv5s 作为基线这套项目训练的是“旗 圈”两类目标官方 COCO 预训练权重起着决定性作用。卷积底层从 COCO 学到的边缘、纹理特征可以直接迁移过来对旗和圈这种结构相对简单的课堂目标200 张训练图在两三个小时内就能达到 mAP0.5 0.95 以上如果从无预训练权重冷启动数据量至少翻三倍且前 30 个 epoch 的 loss 基本不动。Tello TT 端侧算力有限实际部署多是把视频流传回地面计算机笔记本或 Jetson 设备再跑推理。因此模型体积与推理速度直接决定控制回路的带宽。官方几个模型的对比大致如下模型输入尺寸参数量CPU 单帧推理约适用场景yolov5n6401.9M20~35ms低功耗边缘端yolov5s6407.2M60~100msTello TT 地面站最常选yolov5m64021.2M160~250ms高精度但控制滞后明显yolov5l64046.5M300ms不适合实时追踪项目里用的是 yolov5s理由很简单每帧推理时间直接决定能否满足 Tello 的 rc 指令刷新率。60ms 左右一帧意味着可以跑到 10Hz 以上的控制循环而 Tello 对姿态指令的响应在 20Hz 左右才平滑。yolov5n 更快但误检率高教室背景复杂人脸、标牌、窗框时容易把圆形物体误判成旗子。2.2 数据集目录结构与 YOLO 标注格式项目数据集目标为两类旗flag和圈circle。采集训练图时要覆盖不同光照、不同距离与不同倾斜角度否则模型在无人机升高后会出现距离域泛化问题。最终目录结构如下datasets/ ├── flag_circle.yaml ├── images/ │ ├── train/ │ │ ├── 0001.jpg │ │ └── ... │ └── val/ │ └── ... └── labels/ ├── train/ │ ├── 0001.txt │ └── ... └── val/ └── ...YOLO 标注文件每行对应一个目标格式为class_id x_center y_center width height四个坐标都是归一化小数值。例如一张 960×720 图像里旗帜检测框左上角在 (240, 180)、右下角在 (600, 540)则标签为0 0.4375 0.5 0.375 0.5计算过程中心点 (420, 360)宽 360高 360分别除以 960 和 720。注意归一化坐标是相对整张原图的而不是相对裁剪 patch如果数据是从视频帧抽帧后做过 resize必须先确认图像尺寸再写坐标否则框位置会整体偏移。2.3 训练前的标签一致性检查常见事故标签行数不为 5、class_id 超出类别数、宽高为 0以及图像重命名后标签丢失。这些错误不会让 train.py 启动时崩溃而是把训练 loss 拉到奇怪的大值最后 mAP 一直在 0.6 附近震荡。我会在训练前用脚本把目录扫一遍import os from PIL import Image root datasets classes [flag, circle] for split in [train, val]: img_dir os.path.join(root, images, split) lbl_dir os.path.join(root, labels, split) for img_name in os.listdir(img_dir): stem os.path.splitext(img_name)[0] lbl_path os.path.join(lbl_dir, stem .txt) if not os.path.exists(lbl_path): print(f[missing] {lbl_path}) continue with Image.open(os.path.join(img_dir, img_name)) as im: w, h im.size for line in open(lbl_path, r).read().strip().splitlines(): parts line.split() if len(parts) ! 5: print(f[format] {lbl_path}: {line}) continue cls_id int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) if cls_id len(classes): print(f[class] {lbl_path}: cls{cls_id}) if not (0 cx 1 and 0 cy 1 and bw 0 and bh 0): print(f[box] {lbl_path}: {line})各分支对应独立问题[missing]是标签与图片不同名[format]是坐标列缺失[class]是类别 id 越界[box]是坐标未归一化。最容易忽略的是[box]如果标注工具导出的是像素坐标漏做除法后训练会出现大量 NaN loss。另外yaml 中的类别顺序必须与标注时导出的索引一致否则会出现旗标成了圈、圈标成了旗的标签互换。2.4 数据配置文件 flag_circle.yamlpath: ./datasets train: images/train val: images/val nc: 2 names: 0: flag 1: circlepath字段可以写绝对路径也可以写相对 YAML 所在目录的路径。YOLOv5 读取时会用path拼接train与val所以path写错是最常见的起步报错AssertionError: train: No labels in ...。确认路径通了以后可以用detect.py对单张图先出一版结果肉眼核对检测框和类别是否落在目标上。提示训练和推理分处两台电脑时数据集图像尺寸必须与标注脚本读取时的尺寸一致。视频抽帧后用 ffmpeg 或 OpenCV 做过宽高压缩就要重跑一次归一化标注否则所有框会整体偏移且无法收敛。3. 训练命令、超参数调整与事件日志解读3.1 基础训练命令与关键参数准备好flag_circle.yaml后在 YOLOv5 仓库根目录执行python train.py \ --weights yolov5s.pt \ --data flag_circle.yaml \ --img 640 \ --batch-size 16 \ --epochs 100 \ --project runs/train \ --name flag_circle_v1--img 640是速度与精度的折中。旗和圈这类边缘鲜明的目标640 输入足够提升到 1280 虽能让小目标 mAP 微涨但推理时间翻倍控制延时会从可接受变成不可接受。--batch-size视显卡显存而定12GB 显存跑 yolov5s 用 16 是安全值8GB 就降到 8。--weights yolov5s.pt表示从 COCO 预训练权重继续训练而不是随机初始化。首次可以先--epochs 30跑通流程确认 loss 正常下降后再加到 100。3.2 超参数文件与数据增强的影响默认超参数文件是data/hyps/hyp.scratch-low.yamllr0: 0.01 mosaic: 1.0 mixup: 0.0 close_mosaic: 10对旗和圈这种颜色饱和、结构简单的目标通常不需要调lr0但要注意close_mosaic10的含义最后 10 个 epoch 自动关闭 mosaic 增强。mosaic 会把 4 张图拼在一起目标是欠清晰的小块模型在最后阶段需要贴近真实单目标分布来微调。实战里如果发现 val mAP 在最后十来轮突然掉点多数不是过拟合而是数据分布切换引起此时先别急着降学习率先检查是不是close_mosaic设得过大。3.3 TensorBoard 事件文件与训练日志events.out.tfevents.*是 YOLOv5 训练时自动写入的事件文件文件名后缀里的数字包括时间戳与进程随机数分布在runs/train/flag_circle_v1/目录下。断点续训或多次重启训练都会生成新的事件文件TensorBoard 会把它们合并到同一时间线。启动可视化tensorboard --logdir runs/train/flag_circle_v1浏览器打开 http://localhost:6006 后重点看四组标量指标代表内容排查参考train/box_loss预测框与标注框的回归损失持续下降并进入平台单轮抖动不说明问题train/cls_loss类别分类损失旗与圈混淆严重时曲线会出现周期性抬升val/obj_loss目标性损失判断是否存在目标持续偏高对应背景误检如把窗框当成圈metrics/mAP_0.5IoU0.5 下的平均精度两类目标课程项目一般稳定在 0.92 以上当面板里同类曲线出现多种颜色说明累积了多次实验的事件文件可以在 SCALARS 的 Runs 面板里取消勾选只保留当前实验。最终部署用的是weights/best.pt而不是last.ptbest 依据验证集 mAP 选取如果 best 与 last 相差过大说明训练后期存在明显过拟合。3.4 训练完成后的验证与模型导出python detect.py \ --weights runs/train/flag_circle_v1/weights/best.pt \ --source datasets/images/val \ --conf-thres 0.4 \ --save-txt--conf-thres 0.4是经验值低于 0.3 会在教室背景中刷出大量假旗子高于 0.6 会在无人机运动模糊时漏检。验证通过后导出 ONNX 格式便于后续用 OpenCV DNN 或 ONNX Runtime 加载python export.py --weights runs/train/flag_circle_v1/weights/best.pt --include onnxexport.py需要额外安装onnx、onnxruntime-gpu依赖如果卡在onnxsim报错去掉--simplify参数即可。本项目部署阶段不依赖模型精简导出默认的 640 输入就足够。4. Tello TT 通信协议、视频流接收与实时推理4.1 SDK 命令模式与端口分配Tello TT 基于 Tello EDU通信走 UDP。地面站必须先向192.168.10.1:8889发一条command把飞控切换到 SDK 模式再发streamon打开视频流之后才能收图并发送控制指令。端口分配如下端口方向用途8889地面站 → 无人机飞控命令文本格式11111无人机 → 地面站H.264 视频流8890无人机 → 地面站状态数据IMU、电量等192.168.10.1是 Tello 作 AP 模式时的固定地址如果 Tello TT 改成 Station 模式接入路由器地址需要在配套软件中查询。本项目默认走 AP 模式电脑连接名为TELLO-TT-xxxxxx的 Wi-Fi 后直接按上面地址通信。4.2 最小命令与视频流接收代码import socket import cv2 import time TELLO_IP 192.168.10.1 CMD_PORT 8889 VIDEO_PORT 11111 sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(5) sock.bind((0.0.0.0, 9000)) def send_cmd(cmd_str): sock.sendto(cmd_str.encode(), (TELLO_IP, CMD_PORT)) try: resp, _ sock.recvfrom(128) return resp.decode() except socket.timeout: return timeout cap cv2.VideoCapture(fudp://0.0.0.0:{VIDEO_PORT}) print(send_cmd(command)) print(send_cmd(streamon)) time.sleep(1) for _ in range(10): ret, frame cap.read() if ret: print(frame size:, frame.shape) break cap.release()socket.bind((0.0.0.0, 9000))的作用是把地面站 UDP 源端口固定为 9000避免系统随机分配导致 Tello 回包找不到入口。Tello 的命令应答是严格的一问一答制实际工程中不要在高频控制循环里穿插takeoff、land等阻塞命令我会把低频指令与高频 rc 指令拆到不同函数并加锁。4.3 推理线程与采集线程分离主循环里直接cap.read()后跑推理视频解码等待会叠加进推理延时。我会把采集与推理拆成两个线程用deque(maxlen1)做帧缓冲保证推理线程永远拿最新帧而不是排队帧import threading import torch from collections import deque frame_buffer deque(maxlen1) def grab_loop(cap): while True: ret, frame cap.read() if ret: frame_buffer.append(frame) model torch.hub.load(ultralytics/yolov5, custom, pathbest.pt, force_reloadFalse) model.conf 0.45 model.iou 0.45 model.warmup() threading.Thread(targetgrab_loop, args(cap,), daemonTrue).start() while True: if not frame_buffer: time.sleep(0.01) continue frame frame_buffer[-1] results model(frame) boxes results.xyxy[0].cpu().numpy()deque(maxlen1)是关键设计缓冲区只保留最新一帧采集线程永远不会被旧帧阻塞。model.warmup()提前完成卷积核形状分配避免控制循环启动时白等一次上百毫秒的冷启动。results.xyxy[0]的每一行是[x1, y1, x2, y2, conf, cls]后续控制逻辑全部基于这个数组展开。4.4 输入分辨率与推理速度的取舍Tello 视频流默认 960×720模型输入 640×640YOLOv5 预处理会自动做 letterbox。如果 CPU 推理仍不到 15FPS我的做法是先把帧用 OpenCV 缩放到 640 再送入模型而不是把模型输入改成 320。320 输入虽然能跑到 25FPS但远处 30cm 高的旗几乎全部漏检追踪超过 2 米后检测框开始抖动。保证 10~15FPS 对 Tello TT 已经足够因为飞控的 rc 响应本身就有约 100ms 机电延迟更高帧率带来的收益会被执行端吃掉。5. 目标追踪控制与单目测距算法5.1 从检测框到 rc 指令的误差映射拿到 YOLOv5 输出后先算检测框中心与画面中心的归一化误差再映射到 Tello 的 rc 指令。rc 格式是rc left_right forward_backward up_down yaw取值范围 -100~100rc 0 0 0 0表示悬停def det_to_error(det, frame_w, frame_h): x1, y1, x2, y2 det[:4] cx (x1 x2) / 2 cy (y1 y2) / 2 err_x (cx - frame_w / 2) / (frame_w / 2) err_y (cy - frame_h / 2) / (frame_h / 2) return err_x, err_y def pid_control(err_x, err_y, k_yaw45.0, k_ud35.0): yaw int(max(-100, min(100, -k_yaw * err_x))) up int(max(-100, min(100, k_ud * err_y))) return frc 0 0 {up} {yaw}err_x归一到 -1~1目标偏左时err_x为负需要顺时针偏航所以乘系数后取了负号。k_yaw45表示目标偏离一个画面宽度时输出 45% 满量程偏航速度这是比较保守的经验值实测在 2 米外追逐旗帜不会甩尾。若要提升响应可以逐步加大到 60但超过 60 后画面运动模糊加重检测置信度下降反而引起振荡。5.2 单目测距的假设与焦距标定Tello TT 没有深度传感器只能走单目测距方案前提是目标物理尺寸已知。本项目旗帜规格固定按旗面宽度 0.2m 代入公式FOCAL_PX 2300 # 通过标定得到不要直接抄 def estimate_distance(known_width_m, pixel_width, focal_px): if pixel_width 8: return None return (known_width_m * focal_px) / pixel_width距离 目标实际宽度 × 焦距 / 目标像素宽度。focal_px是焦距的像素单位表达需要做一次简单标定把旗帜放到离相机 1m 处读一次检测框宽度。# 标定距离 1.0m旗帜实际宽 0.2m检测像素宽 460 distance 1.0 real_width 0.2 pixel_width 460 FOCAL_PX pixel_width * distance / real_width # 2300Tello 相机在 960×720 下的focal_px通常在 2100~2500 之间不同固件略有差异到手后建议重新标定。这个公式假设镜头畸变可忽略追踪距离超过 5m 时最好先用cv2.undistort校正否则测距误差会从 5% 涨到 20% 以上。5.3 完整追踪测距循环综合前面几节最小可用的闭环流程如下frame_w, frame_h 960, 720 while True: if len(frame_buffer) 0: continue frame frame_buffer[-1] results model(frame) dets results.xyxy[0].cpu().numpy() # 只追踪 flag 类cls0conf 最高的一个目标 candidates [(d[4], d[5], d) for d in dets if d[5] 0] if not candidates: send_cmd(rc 0 0 0 0) continue best_conf, cls_id, det max(candidates) err_x, err_y det_to_error(det, frame_w, frame_h) rc_cmd pid_control(err_x, err_y) send_cmd(rc_cmd) pixel_width det[2] - det[0] dist estimate_distance(0.2, pixel_width, FOCAL_PX) print(ftarget dist{dist:.2f}m conf{best_conf:.2f} rc{rc_cmd}) time.sleep(0.05) # 控制频率约 20Hzcandidates里过滤d[5] 0只追踪旗类目标避免模型在画面里旗和圈同时出现时来回切换控制权。每次循环 0.05s加上推理与网络往返整体控制频率可稳定在 12~16Hz。当没有目标时显式发送rc 0 0 0 0悬停而不是不发送因为 Tello 在收到过 rc 指令后如果 15 秒内没有新指令会自动降落。5.4 多目标与遮挡的边界条件d[5] 0过滤只适用于单类单目标。如果画面里同时出现多个旗且相互交错需要引入基于 IoU 的轻量跟踪器如 SORT 或 ByteTrack给每个目标分配稳定 ID。本项目资源里没有内置跟踪器模块但当目标中途被手臂遮挡后下一次检测的置信度可能瞬间从 0.9 掉到 0.4无 ID 保持时控制权会在两个相近目标间来回切换表现就是无人机左右飘摆。一个低成本缓解方案是优先选择与上一帧检测框 IoU 最大的目标IoU 直接低于 0.2 时维持悬停等待目标重现。6. 部署验证、FPS 观测与高频踩坑清单6.1 上电试飞前的三段验证第一次带模型起飞前先做三段式验证第一步不连接飞机只对保存的视频帧跑推理确认类别与置信度正常第二步连接 Tello 但不起飞通过streamon接收实时图传跑推理观察控制台打印的检测结果是否稳定第三步手动拿着飞机缓慢移动看检测框能否跟上目标同时检查estimate_distance的距离变化是否合理。第三步最容易暴露问题手晃动时检测框宽度在几十像素内跳变距离读数会在 1.2m 与 1.8m 之间乱跳此时需要对像素宽度做滑动平均history [] def smooth_pixel_width(raw_w): history.append(raw_w) if len(history) 5: history.pop(0) return sum(history) / len(history)窗口为 5 时基本能滤掉单帧抖动。窗口加大到 10 会让测距更平滑但目标快速靠近时读数滞后也会更明显控制响应变钝。6.2 高频踩坑与解决对照现象直接原因处理办法连接 Wi-Fi 后command返回 timeout地面站 UDP 源端口未固定增加sock.bind((0.0.0.0, 9000))视频流黑屏但检测图有框OpenCV udp 后端不支持 Tello 重启流先发streamoff再发streamon重建距离读数跳变检测框宽度变化过大对像素宽度做 5 帧滑动平均rc 发送后无人机反应迟钝推理线程阻塞了命令发送命令发送单独线程不与推理共用锁追踪目标中途丢失后无法找回无 ID 跟踪控制权切换按上一帧 IoU 过滤低于阈值时悬停等待6.3 用 OSD 叠加确认闭环完成最后在帧上叠加检测框、类别、距离与控制指令直接人眼确认闭环成立results model(frame) annotated results.render()[0] cv2.putText(annotated, fdist{dist:.2f}m rc{rc_cmd}, (30, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(tello-tt, annotated) if cv2.waitKey(1) 0xFF ord(q): breakresults.render()是 YOLOv5 内置的标注绘制方法会把检测框、类别与置信度画在原图上并返回新的 ndarray。把它放到循环末尾可以同时验证“采集→推理→控制→显示”四段链路。调试时直接看窗口里绿色文本框内的 rc 指令变化是否符合直觉目标往画面左侧移动时 yaw 应为正数目标变小且居中时 up 应趋于 0距离显示应随镜头靠近连续下降。本文还有配套的精品资源点击获取
返回列表