
简介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里把它排在第二个位置但训练脚本默认第一个位置是背景这会导致标签错位。第四小目标框的宽或高在归一化后趋近于0YOLOv8训练时可能会直接忽略或者报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(fclass {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或x2000张数据集撑不起大模型。用YOLOv8l或x训练参数量大、过拟合风险高而且在GTX 1660 Ti这一档显卡上训练时间会拉到让人崩溃的程度。我做这个项目时用的是YOLOv8s输入分辨率设定为640batch size取8大概40分钟一个epoch50轮下来一个晚上能跑完。如果你用的是nano模型速度还能再快一倍但精度会下降明显——在验证集上s和n的mAP50差距通常有3到5个点。选s还有一个考虑后续如果要部署到RK3588这类边缘设备s模型经过TensorRT INT8量化后在NPU上能跑到20到30 FPSnano能跑更快但漏检也会更多。所以模型选型不是一个纯粹的精度问题而是算力、精度、部署平台三者之间的平衡。我一般建议先用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 \ modelyolov8s.pt \ datauav_fixed_wing.yaml \ imgsz640 \ batch8 \ epochs60 \ optimizerSGD \ lr00.01 \ lrf0.01 \ momentum0.937 \ weight_decay0.0005 \ warmup_epochs3.0 \ warmup_momentum0.8 \ box7.5 \ cls0.5 \ dfl1.5 \ hsv_h0.015 \ hsv_s0.7 \ hsv_v0.4 \ mosaic1.0 \ scale0.8 \ patience20 \ projectuav_train \ nameexp_fixed_wing这里有几个参数对小型固定翼检测影响很大scale设为0.8而不是默认的0.5目的是减少mosaic拼接时目标被过度缩小的概率patience设为20是因为数据集不大早停阈值给宽松一点避免模型在验证集上稍微抖动就被停掉optimizer用SGD而不是AdamW在小数据集上SGD的收敛稳定性普遍更好AdamW容易在训练后期出现震荡。imgsz用640是平衡速度与精度的默认选择但如果你的验证集里大量目标只有20像素甚至更小可以试一下imgsz960不过显存占用会涨到接近两倍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( sourcetest_imgs, conf0.25, iou0.7, imgsz640, saveTrue, save_txtTrue, save_confTrue, )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, conf0.4, iou0.7, parentNone): 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, confself.conf, iouself.iou, imgsz640) 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 fuav {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 FPSQt界面不追求过高的刷新率每次处理完帧后显式删除不再用的临时变量用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匹配不用再回头补录。我自己在这个项目上吃过最大的亏就是把精力全花在调模型参数上后来发现数据质量才是决定上限的那个变量。自从养成“每周回看不合格检测结果并补充数据”的习惯后模型在真实场景的可用性提升比任何一次的调参都要大。希望这套从数据到界面再到迭代的落地路径能帮你在做固定翼无人机检测时少走点弯路。本文还有配套的精品资源点击获取