简介:面向毕业设计、课程设计及目标识别实战的深度学习资源,基于YOLOv5框架与大疆教育无人机Tello TT,完整实现目标识别、追踪与测距流程,数据集以旗帜和圆圈为识别目标,模型已完成训练和调优,可直接用于实测或二次开发。压缩包共1672个文件,大小约269MB,包含758张jpg训练图像、694个txt标注文件、94个yaml配置文件、50个Python源码脚本、9个pt模型权重,以及xml标注、Dockerfile、csv记录、Markdown说明等,类型覆盖全面。附有TensorBoard训练日志,方便查看训练曲线与调参依据。目前已有283人学习下载。整套资料提供项目源码、训练好的模型、完整数据集与操作说明文档,结构清晰,适合计算机视觉、模式识别方向的学生作为毕设或大作业蓝本;也可在此基础上修改代码训练其他目标,是一套完整可运行的实战项目方案。
1. 这套“基于yolov5+Tello TT”的方案能做什么,为什么我不建议从零搭
当你在实训课或技术预研里拿到一台大疆教育无人机Tello TT,需求往往不是“飞得稳”,而是“看到目标、跟着目标、报出距离”这三件事一口气全跑通。基于yolov5的目标识别检测配合Tello TT,是目前成本和上手难度最平衡的路线:yolov5网络结构图和预训练权重都是公开的,Tello TT 又自带完整的SDK和WiFi视频流,把检测结果换算成飞行控制量和距离估计,工程上完全可控。本文会把整套落地路径拆开讲,覆盖通信链路、数据集与训练超参数、追踪与测距实现、以及我实际踩过的坑。适合手里有Tello TT、想在几天内跑通完整应用、而不是只看理论图纸的人。下面直接进入第一个决定成败的环节:通信链路。
2. 先打通无人机通信链路:连上Tello TT,把视频流喂给yolov5
Tello 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是最稳妥的组合,patience=50表示连续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.pt,best.pt就是验证集效果最好的权重。如果整个检测推理都在带GPU的电脑上运行,直接用PyTorch加载即可:
import torch import cv2 model = torch.hub.load( "ultralytics/yolov5", "custom", path="runs/tello_exp/weights/best.pt", force_reload=True, ) 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", path="best.pt", force_reload=True ) 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_pixel=180, f_pixel=921, real_h_meter=0.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.00 | 1.12 | +0.12 |
| 1.50 | 1.63 | +0.13 |
| 2.00 | 2.28 | +0.28 |
| 2.50 | 2.79 | +0.29 |
| 3.00 | 3.41 | +0.41 |
可以看到误差在远距离时会线性放大,这时可以在输出端做一次多项式补正。还有一个容易被忽略的细节,测距用的box_h_pixel要选目标框的高度,而不是宽度,因为锥桶、行人这类目标在侧向偏移时不满足光轴垂直假设,高度的尺寸在多数姿态下更接近稳定值。
结尾说一个我自己的习惯:每次调完一套参数,我会把best.pt、转换脚本、视频回放和CSV日志存在一个以时间命名的目录里,比如exp_20250412_tello_cone。下次调试如果想回到之前某个版本,只要把对应权重和控制参数换回来就行,这就是留后悔药。正是这个习惯让我在反复调整模型和控制参数时从来没有丢失过可用状态,也希望这一点能帮到你。
本文还有配套的精品资源,点击获取