简介:YOLOv8小型固定翼无人机检测项目提供可直接使用的训练权重和完整配套工程,面向无人机视觉应用开发者、目标检测学习者及需要快速集成固定翼识别功能的项目团队。压缩包共2000个文件,约160.8MB,核心为1892个txt格式的YOLO标注文件,另有90个Python脚本(含PyQt界面、训练与推理代码)、6个shell部署脚本、4个yaml配置、5个说明文档及3个PDF教程;数据集已按train/val/test划分,data.yaml中类别为FixedWing-Drone,可直接用于YOLOv5/v7/v8训练。内置约2000张无人机图像的标注数据和已训练好的权重,推理即可用;PyQt界面支持图片、视频和摄像头实时检测,操作选项完整,教程覆盖从环境配置到运行的每一步,便于快速验证、二次开发或迁移到其他固定翼目标。已有204人学习浏览,适合希望快速上手固定翼无人机检测的开发者。
1. 一套能落地的YOLOv8固定翼无人机检测方案:从2000张数据集到PyQt界面
把YOLOv8、小型固定翼无人机检测、2000张数据集、PyQt界面这四样东西拼在一起,其实是在解决一个很具体的工程问题:你没有足够的算力去折腾大模型,也没有精力从零标注数据,但你需要一个能实时框住天空中小型固定翼无人机的系统,并且要给人用一个像样的界面,而不是黑乎乎的命令行。我去年用这套组合做过一个低空安防的验证项目,结论是可行,但坑比想象中多——数据只有2000张,标注质量参差不齐;权重训练出来容易,真正稳定跑起来难;PyQt界面写起来很快,现场长时间运行才暴露问题。这篇文章就是把这条完整链路拆开讲清楚,适合正在做毕设、竞标演示或有真实巡检需求的人,照着做能少走我走过的弯路。
2. 2000张固定翼无人机数据集:质量评估比数量更重要
2.1 小型固定翼与四旋翼的差异:数据集的“目标”长什么样
拿到一个号称“2000张小型固定翼无人机”数据集,第一步不是急着训练,而是先搞清楚这些图里的目标到底长什么样。小型固定翼无人机和常见的四旋翼(比如大疆的Phantom系列)在外观上有明显区别:固定翼通常有展弦比较大的机翼、水平尾翼、前置或后置螺旋桨,飞行时姿态是水平滑翔的,而四旋翼是悬停式。这意味着你的检测模型要学会的不是“一个十字形的旋翼机”,而是“一个带翅膀的滑翔体”,在图像里往往只有几十个像素甚至更少。
我一般会先写一个脚本把数据集的图片尺寸、目标框面积、每张图的标注数量统计出来。小型固定翼的典型情况是:图像分辨率在1080P到4K之间,但目标框的长边很少超过80像素,大部分集中在15到50像素。这个分布决定了后续所有训练参数——比如输入分辨率不能盲目设成640,增强策略里的mosaic和random_perspective要格外小心,因为它们会进一步把小目标缩得更小。
2.2 检查标注格式与标签一致性:VOC转YOLO的四个边界问题
很多公开数据集给的是VOC格式(XML标注),而YOLOv8训练需要YOLO格式的TXT标注,归一化中心坐标加宽高。转换脚本很好写,但边界问题往往出在这些地方:
第一,XML里有些目标框的xmin和ymax或ymin和xmax写反了,或者坐标超出了图像边界。第二,有些图片被标注了但对应的TXT文件缺失,或者反过来。第三,类别标签的索引和类别名称对不上——比如XML里写的是“uav”,你的classes.txt里把它排在第二个位置,但训练脚本默认第一个位置是背景,这会导致标签错位。第四,小目标框的宽或高在归一化后趋近于0,YOLOv8训练时可能会直接忽略或者报loss为NaN。
我处理这些问题的做法是,先做一次全量检查,把异常数据清洗掉,不把“脏”数据带进训练集。转换脚本里至少要有坐标越界裁剪、空标注过滤、标签索引重映射这三步。以下是我常用的检查脚本片段:
import os import xml.etree.ElementTree as ET img_dir = "images" xml_dir = "xmls" out_dir = "labels" class_map = {"fixed_wing_uav": 0, "bird": 1} for xml_name in os.listdir(xml_dir): if not xml_name.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, xml_name)) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_map: continue bbox = obj.find("bndbox") x1 = float(bbox.find("xmin").text) y1 = float(bbox.find("ymin").text) x2 = float(bbox.find("xmax").text) y2 = float(bbox.find("ymax").text) # 裁剪越界坐标 x1 = max(0, min(x1, img_w - 1)) x2 = max(0, min(x2, img_w - 1)) y1 = max(0, min(y1, img_h - 1)) y2 = max(0, min(y2, img_h - 1)) if x2 - x1 < 2 or y2 - y1 < 2: continue cx = (x1 + x2) / 2 / img_w cy = (y1 + y2) / 2 / img_h bw = (x2 - x1) / img_w bh = (y2 - y1) / img_h lines.append(f"{class_map[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") with open(os.path.join(out_dir, xml_name.replace(".xml", ".txt")), "w") as f: f.write("\n".join(lines))这段代码的核心是三个动作:类别重映射(把XML里的字符串类别转成模型需要的整数索引)、坐标越界裁剪(防止归一化后出现负值或大于1的值)、过小目标过滤(少于2像素的框直接丢弃)。很多人会跳过最后一步,结果训练时发现loss里出现奇怪的波动,其实就是这些几像素的噪声框在干扰。
2.3 数据集划分与增强策略:2000张怎么分配才不浪费
2000张图不算大,我建议按8:1:1划分训练集、验证集、测试集。关键在于划分时不能随机乱切——同一个场景或者同一段视频连续帧的图片要放进同一个集合,否则验证集里出现和训练集几乎一样的图,mAP会虚高。可以先用视频文件或者图片文件名的时间戳前缀做分组,再在组级别随机划分。
增强策略方面,YOLOv8默认的mosaic增强对小目标其实是一把双刃剑。mosaic把四张图缩放到一张图里,目标的绝对尺寸更小了,如果数据集里本来就有大量小目标,模型的召回率会被压低。我的做法是mosaic用默认开启,但把mosaic的拼接图数量控制在4,同时把scale的随机范围从默认的0.5-1.5收窄到0.8-1.2,减少目标被过度缩小的概率。另外,针对天空背景的检测,我加了一点HSV色域增强,让模型对黄昏、逆光场景更鲁棒。
2.4 做一个标签分布统计脚本:训练前最后一道质检
在开始训练之前,最后一个质检步骤是统计标签分布。这一步能帮你发现很多问题,比如某个类别在训练集里只出现了10次、某张图有50多个框但其他图平均只有1个框、某个目标长期只出现在图像左上角导致模型学偏位置。把统计脚本跑完,输出的表格看一眼,比训练完再回头看loss曲线要省几个小时。
import os from collections import defaultdict label_dir = "labels" stats = defaultdict(lambda: {"count": 0, "area_sum": 0.0, "imgs": set()}) for txt_name in os.listdir(label_dir): if not txt_name.endswith(".txt"): continue with open(os.path.join(label_dir, txt_name)) as f: lines = f.readlines() for line in lines: parts = line.strip().split() cls_id = int(parts[0]) bw, bh = float(parts[3]), float(parts[4]) area = (bw * bh) * 100 stats[cls_id]["count"] += 1 stats[cls_id]["area_sum"] += area stats[cls_id]["imgs"].add(txt_name) for cls_id, s in stats.items(): avg_area = s["area_sum"] / s["count"] if s["count"] else 0 print(f"class {cls_id}: boxes={s['count']}, imgs={len(s['imgs'])}, avg_area%={avg_area:.2f}")输出里重点看两个指标:每个类别的目标总数和平均归一化面积占比。固定翼无人机场景下,avg_area%在0.5到5之间都算正常,如果低于0.2就要考虑提高输入分辨率或者做切图推理。
3. 训练YOLOv8权重:选型、参数与loss曲线判读
3.1 为什么从YOLOv8n或s起步,而不是l或x
2000张数据集撑不起大模型。用YOLOv8l或x训练,参数量大、过拟合风险高,而且在GTX 1660 Ti这一档显卡上训练时间会拉到让人崩溃的程度。我做这个项目时用的是YOLOv8s,输入分辨率设定为640,batch size取8,大概40分钟一个epoch,50轮下来一个晚上能跑完。如果你用的是nano模型,速度还能再快一倍,但精度会下降明显——在验证集上s和n的mAP50差距通常有3到5个点。
选s还有一个考虑:后续如果要部署到RK3588这类边缘设备,s模型经过TensorRT INT8量化后,在NPU上能跑到20到30 FPS,nano能跑更快但漏检也会更多。所以模型选型不是一个纯粹的精度问题,而是算力、精度、部署平台三者之间的平衡。我一般建议先用s跑通全流程,确认精度和速度都满足需求后再去考虑nano或l。
3.2 环境配置与最小可跑通的训练命令
环境配置网上教程一大堆,但最容易翻车的三个点其实是:CUDA和PyTorch版本不匹配、ultralytics包的依赖冲突、显卡驱动太老导致CUDA跑不起来。我习惯用Python 3.10加PyTorch 2.0以上版本,ultralytics直接用pip最新版。装完以后先跑一段代码确认GPU可用:
import torch print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_name(0))输出为True且能看到显卡型号,再进入训练。训练命令我习惯写成一个bash脚本,把关键参数都显式写出来,不依赖config文件里的默认值:
yolo detect train \ model=yolov8s.pt \ data=uav_fixed_wing.yaml \ imgsz=640 \ batch=8 \ epochs=60 \ optimizer=SGD \ lr0=0.01 \ lrf=0.01 \ momentum=0.937 \ weight_decay=0.0005 \ warmup_epochs=3.0 \ warmup_momentum=0.8 \ box=7.5 \ cls=0.5 \ dfl=1.5 \ hsv_h=0.015 \ hsv_s=0.7 \ hsv_v=0.4 \ mosaic=1.0 \ scale=0.8 \ patience=20 \ project=uav_train \ name=exp_fixed_wing这里有几个参数对小型固定翼检测影响很大:scale设为0.8而不是默认的0.5,目的是减少mosaic拼接时目标被过度缩小的概率;patience设为20是因为数据集不大,早停阈值给宽松一点,避免模型在验证集上稍微抖动就被停掉;optimizer用SGD而不是AdamW,在小数据集上SGD的收敛稳定性普遍更好,AdamW容易在训练后期出现震荡。imgsz用640是平衡速度与精度的默认选择,但如果你的验证集里大量目标只有20像素甚至更小,可以试一下imgsz=960,不过显存占用会涨到接近两倍,1660 Ti的8G显存可能吃不消,需要把batch降到4。
3.3 loss曲线怎么判读:不只看train_loss下降
训练结束后,ultralytics会在runs/detect/exp下生成results.csv和一堆曲线图。很多人只看训练loss下降了就认为模型收敛了,其实这是陷阱。我更关注验证集上的box_loss和cls_loss是否也跟着下降,以及val loss回升的那个epoch在哪里——那个点就是最佳权重。
一个小技巧:训练结束后,把results.csv用pandas读进来,画出每个loss曲线的平滑版本,然后用loc找到val/box_loss最小的epoch,把那个epoch的权重复制出来单独保存。ultralytics默认会保存best.pt,它选的也是val loss最小时的权重,但你得确认这个best.pt对应的mAP确实是你想要的。
另一个常见问题是loss在训练早期出现NaN。现象是训练到第5到10个epoch时,box_loss突然变成nan,然后所有指标全部失效。原因一般是学习率过大或者标注数据里出现了极端的归一化坐标(比如某个框的坐标写成了负数)。解决方法是先检查数据,再把lr0从0.01降到0.005,同时把warmup_epochs从3.0拉到5.0。
3.4 权重导出与推理验证:不要只看验证集mAP
训练完以后不要急着做界面,先做一次独立的推理验证。我习惯找10到20张训练集和验证集之外的图片,最好是不同时间段、不同天气、不同拍摄角度的,用下面这段代码跑一遍,把检测结果可视化出来:
from ultralytics import YOLO model = YOLO("runs/detect/exp_fixed_wing/weights/best.pt") results = model.predict( source="test_imgs", conf=0.25, iou=0.7, imgsz=640, save=True, save_txt=True, save_conf=True, )conf和iou这两个参数对小型固定翼检测影响很大。conf设太高,小目标被过滤掉;设太低,界面里到处都是误检框。我一般先设0.25做彻底检查,看误检情况,再逐步调到0.4到0.5之间。iou设0.7是常规NMS阈值,如果你发现同一个目标周围出现两三个框,说明iou可以适当调到0.75,或者模型本身对小目标的重叠预测不稳定。
这一步跑出来的可视化结果,比任何mAP数字都更能说明问题。如果你看到满屏的误检把云彩、飞鸟、远处高楼的窗户都框了,那就要回到数据层面去补负样本——比如加入一些只有云彩、只有飞鸟、只有地面建筑物的图片,并标注为空图。2000张数据集里千万不要全是“有目标”的图,否则模型会学到“见到天空就想框东西”。
4. 用PyQt把权重封装成可操作的检测界面
4.1 界面架构:推理线程必须和UI线程分离
PyQt做检测界面的最大坑是:把模型推理放到UI主线程里,界面一动就卡死。模型前向推理一次要80到150毫秒,如果放在主线程,鼠标拖动窗口、点击按钮都会卡顿,如果再接上视频流,界面基本就废了。正确的架构是:UI主线程负责绘制和响应操作,QThread子线程跑推理,检测结果通过信号回传到主线程更新画面。
整体结构分三层:输入层(图片文件、视频文件、摄像头RTSP流)、推理层(YOLOv8模型封装在QThread里)、显示层(QLabel或QGraphicsView绘制画面和检测框)。这三个层次之间的交互用Qt的信号槽机制完成,不共享可变数据,避免锁问题。
4.2 用QThread封装YOLOv8推理:一个可以直接抄的类
下面这个推理线程类是我项目的核心,它接收一帧图像,跑模型,然后把检测结果以信号的形式发出去。注意模型初始化和推理都放在run方法内部,不要在构造函数里加载模型,否则UI启动会卡很久。
import sys import cv2 import numpy as np from PyQt5.QtCore import QThread, pyqtSignal from ultralytics import YOLO class DetectThread(QThread): frame_ready = pyqtSignal(np.ndarray) result_ready = pyqtSignal(list, np.ndarray) def __init__(self, model_path, conf=0.4, iou=0.7, parent=None): super().__init__(parent) self.model_path = model_path self.conf = conf self.iou = iou self.running = True self._model = None def run(self): # 在线程内加载模型,避免阻塞UI启动 self._model = YOLO(self.model_path) cap = cv2.VideoCapture(self.video_source if hasattr(self, "video_source") else 0) while self.running: ret, frame = cap.read() if not ret: self.running = False break results = self._model.predict(frame, conf=self.conf, iou=self.iou, imgsz=640) detections = [] if results and results[0].boxes is not None: boxes = results[0].boxes.xyxy.cpu().numpy() confs = results[0].boxes.conf.cpu().numpy() cls_ids = results[0].boxes.cls.cpu().numpy().astype(int) for box, conf, cls_id in zip(boxes, confs, cls_ids): detections.append( {"box": [float(x) for x in box], "conf": float(conf), "cls": int(cls_id)} ) self.result_ready.emit(detections, frame) cap.release() def stop(self): self.running = False这段代码的要点有三个:模型在run方法里加载,避免构造函数阻塞;while循环里用self.running做退出标志,stop方法可以安全地终止线程;每次推理结果通过信号emit出去,UI线程在槽函数里更新画面。
4.3 视频流接入与目标信息展示:从图片到实时检测的平滑切换
界面里只显示检测框是不够的,操作人员还想要目标数量、类别、置信度、帧率这些信息。我的做法是主窗口里放一个QLabel显示视频画面,旁边放一个QTableWidget实时刷新当前帧的检测列表,底部用一个QStatusBar显示FPS。
视频流接入要注意的是RTSP流的断开重连问题。现场摄像头经常出现网络抖动,cap.read()返回False后如果直接break,整个界面就停住了。我一般会在视频流失败时自动重试3次,每次间隔2秒,重试逻辑放在reconnect函数里。另外,长期现场运行时的内存增长问题很隐蔽:如果每帧都创建一个新的检测结果列表而不释放旧列表,内存会缓慢爬升。我的做法是复用detections列表,每帧清空后重新填充。
4.4 界面参数调整与稳定性:把置信度阈值和模型路径暴露给用户
界面不要写死参数。我在界面上留了几个可调项:置信度阈值滑块(0.05到0.95)、NMS IoU阈值滑块、模型文件选择按钮、视频源输入框。用户可以在现场根据实际画面调整阈值,不用每次改代码重新运行。
稳定性方面有两条血泪经验:一是QLabel显示图片时,如果图片尺寸和QLabel大小不一致,不要直接用setPixmap缩放原图,那样每次都要做一次大图缩放,CPU占用会飙升。建议先把帧缩放到界面目标尺寸再显示。二是Qt的QImage格式转换一定要带正确的通道顺序,OpenCV的BGR转QImage时要先用cvtColor转成RGB,否则画面里目标的颜色和真实场景对不上,看起来像“负片”,容易误判。
以下是主窗口中槽函数的部分实现:
from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import Qt class MainWindow(QWidget): def __init__(self): super().__init__() self.detect_thread = DetectThread("runs/detect/exp_fixed_wing/weights/best.pt") self.detect_thread.result_ready.connect(self.update_frame) def update_frame(self, detections, frame): # 画框 for det in detections: x1, y1, x2, y2 = [int(v) for v in det["box"]] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) label = f"uav {det['conf']:.2f}" cv2.putText(frame, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) # BGR转RGB再转QImage frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = frame_rgb.shape qimg = QImage(frame_rgb.data, w, h, ch * w, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qimg).scaled( self.video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) self.table.update_detections(detections)5. 常见问题与避坑:训练和界面联调阶段的踩坑记录
5.1 小型固定翼目标漏检率偏高,模型只检出大目标
现象:验证集mAP50有0.85,但实际视频画面里,小尺寸的固定翼无人机根本框不出来,只有飞到画面近处才能检测。
原因:数据集里小目标占比不足,或者mosaic增强把小目标进一步缩小,模型学到了“大框优先”的倾向。另一个常见原因是图里有大量小目标时,NMS阈值设得偏高导致邻近小目标被合并。
解决:先统计数据集里目标面积分布,把面积占比低于0.2%的框筛选出来单独看;训练时把imgsz从640提到768或960,或者在推理时做两尺度推理,用640和960分别跑一次然后把结果做加权融合;适当降低conf到0.2再观察是否漏检的是边界框置信度低还是压根没检出。我最后是提高了输入分辨率并把conf阈值从0.4降到0.3,漏检率明显下降。
5.2 PyQt界面运行十几分钟后开始卡顿,FPS缓慢降低
现象:刚启动时视频检测FPS在30左右,运行10到20分钟后逐渐掉到15以下,界面操作也变迟钝。
原因:最常见的是内存泄漏。推理线程每帧创建的numpy数组和检测结果字典没有被及时释放,Qt的pixmap缓存累积也是一个因素。还有一个隐蔽原因:QLabel的setPixmap频繁触发重绘,如果帧率太高,UI线程长期被占满。
解决:给推理线程加一个帧率上限,比如用time.sleep控制在25 FPS,Qt界面不追求过高的刷新率;每次处理完帧后,显式删除不再用的临时变量;用tracemalloc或py-spy在卡顿前抓内存快照,确认泄漏点。另外一个立竿见影的改动是,不要每帧都创建QImage和QPixmap,改为在固定尺寸的QWidget上用paintEvent画图。
5.3 训练时loss在第10个epoch后震荡,验证集mAP反而下降
现象:训练loss在下降,但验证集loss从某个epoch开始回升,mAP50在0.75到0.85之间反复震荡,不稳定。
原因:数据集只有2000张,模型在第10个epoch左右就已经开始过拟合。另一个原因是数据划分时训练集和验证集分布不均匀,比如验证集里全是低光照或远距离图片,模型没见过类似的数据。
解决:把patience从20调小到10,让早停更早介入;增大data augmentation的强度,特别是hsv色域增强和random_flip;如果验证集分布问题严重,重新做一次数据划分,确保每个集合里都有不同光照条件、不同目标大小的图片。还有一个容易被忽略的点:如果训练和验证都用mosaic增强,模型会在增强后的图像上过拟合,建议把验证集的augment关闭,只在训练集上做增强。
5.4 不同来源的数据集混合标注后,目标框出现系统性偏移
现象:训练出来的模型在A类图片上检测很好,在B类图片上检测框总是偏左或者偏上半个身位。
原因:A来源的数据标注精度较高,B来源的数据标注粗糙,边界框普遍偏大或偏小。混合训练后,模型的box回归被拉到两种标注质量的中间值,对精确标注的数据产生偏移。
解决:训练前对不同来源的数据分别做一次标注质量抽样,画出来对比边界框是否准确贴合目标边缘。如果某些来源的标注整体偏差超过3个像素,建议手动修正一批再训练,或者给这些样本分配更低的采样权重。另一个做法是把不同来源的数据按8:1:1划分时,尽量保持每个来源的分布比例一致,不要让模型只看一个来源的数据。
5.5 部署到RK3588后帧率暴跌,实时性无法满足
现象:在PC上跑PyQt界面有30 FPS,部署到RK3588边缘设备后只有5到8 FPS,完全达不到实时检测要求。
原因:直接在RK3588上用PyTorch推理,完全没有用到NPU的算力;模型也没有做INT8量化,FP32卷积在CPU上跑当然慢。
解决:把模型导出为ONNX,再用RKNN工具链做INT8量化,量化时要用一批有代表性的校准图片,不能随便拿几十张训练图敷衍过去,否则精度掉得很难看。量化后部署到NPU,通常可以把s模型的推理速度提到15到25 FPS。如果不做量化,至少也换成YOLOv8n并开启TensorRT加速,能比纯PyTorch快两倍以上。
6. 进阶:验证模型效果的两个习惯与一个数据闭环技巧
模型训练完、界面跑通只是第一步,真正要确认这套方案能不能用,我建议做一次时间跨度测试:连续一周每天固定时间段从摄像头采集10分钟视频,用界面里的检测功能跑一遍,记录每帧的检测结果和置信度。特别是早晨和傍晚的低光照时段,以及逆光角度,这是小型固定翼无人机最容易和飞鸟混淆的场景。把这一周的数据记录下来,按时间段统计误检率,你会发现白天和黄昏的表现差异巨大,然后针对性地补充该时段的训练数据。
第二个习惯是保存检测失败的回放帧。我的做法是,当检测置信度低于0.3但人工确认是目标时,按一个快捷键把当前帧保存到指定文件夹,定期把这些帧补充到训练集里做增量训练。这一步比任何调参都有效,因为2000张数据集最大的问题是分布覆盖不够,而不是模型不够强。增量训练时在原模型上继续训练5到10个epoch,学习率降到0.0001,实测一次就能把特定场景的漏检拉下来。
最后一个技巧是把PyQt界面的检测结果输出成结构化日志,比如每行记录“时间戳、目标坐标、置信度、帧号”,存成CSV或JSON。这有两个好处:一是方便做离线统计,比如计算一个固定翼目标出现在视野里的平均持续时长;二是为后续做目标跟踪提供数据基础——当你需要从“检测”升级到“跟踪”时,有逐帧坐标数据可以直接做卡尔曼滤波或IoU匹配,不用再回头补录。
我自己在这个项目上吃过最大的亏,就是把精力全花在调模型参数上,后来发现数据质量才是决定上限的那个变量。自从养成“每周回看不合格检测结果并补充数据”的习惯后,模型在真实场景的可用性提升比任何一次的调参都要大。希望这套从数据到界面再到迭代的落地路径,能帮你在做固定翼无人机检测时少走点弯路。
本文还有配套的精品资源,点击获取