简介:面向目标检测开发者和无人机视觉学习者,这份YOLO无人机目标检测数据集围绕真实航拍场景构建,包含5000张高质量图片,场景丰富,经LabelImg精细标注,同步提供VOC、COCO、YOLO三种格式标签,分别存放在不同文件夹中,可直接接入主流YOLO训练流程。整个压缩包共包含2000个文件,以xml标签为主,配合数据划分py脚本、train_list等txt清单,以及多份环境搭建与训练教程文档,总大小301.59MB。除了标签外,还附赠训练集、验证集、测试集划分脚本,支持按需求重新分配数据;教程覆盖Windows和Linux两个平台,从环境安装到基于案例修改训练自己的数据集均有说明,可帮助学习者避开常见坑点。资源已有433人学习,适合课程设计、毕业设计及算法研究等场景,整体是一份“数据+工具+教程”一体化的实用资源。
1. 拿到YOLO无人机目标检测数据集:先别急着跑训练,把目录和标签读懂再说
「YOLO无人机目标检测数据集」这种压缩包,打开之后真正值钱的不是那5000张图,而是同一批图片同时给了VOC、COCO、YOLO三种格式的标签,外加划分脚本和训练教程。对做遥感、低空安防、电力巡检和农业植保的工程师来说,标注格式齐全意味着你不需要在xml、json、txt之间来回倒腾,能直接把精力放在「如何训练」和「目标检测模型调参」上。但这套东西有个隐性前提:三种格式的坐标语义、类别顺序、归一化方式完全不同,版本对不上,后续的yolov8训练自己的数据集大概率翻车。这篇笔记按「读数据→转格式→划分→训练→避坑」的顺序,把完整落地路径捋一遍。新手可以照着命令一步步跑通,熟手可以直接跳到第5章看边界条件和参数坑。
2. 先读懂三种标签再动手:VOC的xml、COCO的json、YOLO的txt各自怎么存坐标
很多人在这一步犯的第一个错误,是以为三种格式只是文件后缀不同,内容差不多。实际上VOC、COCO、YOLO对「一个框」的描述方式差异很大:VOC存的是左上角和右下角的绝对像素坐标,COCO存的是左上角坐标加宽高,YOLO存的是中心点坐标加宽高、而且全部做了归一化。坐标系都不同,直接混用必然出错。
2.1 VOC的xml:绝对坐标、difficult标记和容易被忽略的filename字段
VOC格式的核心是每个xml文件对应一张图片,里面用<bndbox>节点给出目标的绝对坐标。一个典型结构长这样:
<annotation> <filename>DJI_20240701_001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>car</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>438</xmin> <ymin>621</ymin> <xmax>712</xmax> <ymax>783</ymax> </bndbox> </object> </annotation>读取时三个关键点:第一,filename字段只写了文件名不写路径,如果图片和xml不在同一层,需要自己拼目录;如果文件名里带中文或空格,尽量在预处理时一并改掉。第二,difficult标记表示这个目标是否难以辨认,很多训练框架在读取VOC数据时默认把difficult=1的样本过滤掉,如果你的数据集里这个标记比较随意,训练集和验证集会不一致,最后评估mAP时就会对不上账。第三,bndbox里的坐标允许略微超出图像边界,转YOLO之前最好做一次clip,否则归一化后可能出现大于1的坐标值。
2.2 COCO的json:image_id、category_id和[x,y,w,h]的对应关系
COCO格式把整个数据集的标注塞进一个json文件,核心是三个数组:images、annotations、categories。images里存每张图的id、file_name、width、height;annotations里每条记录通过image_id关联到图片,通过category_id关联到类别;bbox字段存的是[x, y, width, height],注意这是左上角坐标加宽高,不是中心点。
无人机目标检测数据集里的COCO标签有两个常见坑。第一,category_id不一定是连续的,可能从1开始、中间还有跳号,而YOLO格式要求类别ID必须是从0开始的连续整数,转换时不能直接拿category_id当YOLO类ID用,必须重新映射。第二,segmentation字段在检测任务里经常是一段多边形坐标,如果直接忽略没问题,但如果用现成脚本转换时没跳过iscrowd=1的标注,可能把一些群体目标当普通框算进来,导致训练时标签噪音变大。
另外COCO的json里图片路径五花八门,有的带http前缀,有的带完整绝对路径,处理时最好只用os.path.basename取文件名,再和你本地图片目录拼接,否则根本找不到图。
2.3 YOLO的txt:归一化坐标与类别ID从0开始这回事
YOLO格式是目前训练阶段唯一真正会被yolov8直接读取的格式,每个txt文件对应一张图,每一行代表一个目标,格式是:
class_id x_center y_center width height五个值都是空格分隔,class_id是整数,从0开始,和训练配置里data.yaml的names列表顺序严格对应;后面四个值全部是归一化浮点数,即相对于图片宽高的比例,取值范围正常情况下在0到1之间。
这个格式最容易被误解的地方是「归一化」到底除以什么。不管原图是1920x1080还是640x480,统一除以各自图片的宽和高,而不是除以同一个固定尺寸。另外YOLO的x_center、y_center是中心点,不是左上角;如果你从COCO转过来,坐标变换公式是:
x_center = (bbox_x + bbox_w / 2) / image_width y_center = (bbox_y + bbox_h / 2) / image_height w_norm = bbox_w / image_width h_norm = bbox_h / image_height为了直观对比,把三种格式的核心差异整理成一张表:
| 格式 | 存储方式 | 坐标含义 | 类别ID | 是否归一化 |
|---|---|---|---|---|
| VOC | 每张图一个xml | xmin, ymin, xmax, ymax(左上角+右下角) | 字符串类名(如"car") | 否 |
| COCO | 整个数据集一个json | x, y, width, height(左上角+宽高) | 整数,可能不连续 | 否 |
| YOLO | 每张图一个txt | x_center, y_center, width, height(中心点+宽高) | 整数,从0开始连续 | 是 |
一句话记忆:VOC是「两边」,COCO是「一角加宽高」,YOLO是「中心加宽高」。后面做转换脚本时,所有换算都是围绕这三者之间的差异展开的。
3. 标签转换脚本:类名表统一voc、coco、yolo,一个表错全盘错
既然三种格式各有差异,拿到压缩包后第一件事不是直接开训,而是先验证三种格式的标签内容是否一致。尤其是类名顺序,它是整个转换流程的中枢神经——VOC里存的是字符串类名,COCO里存的是任意整数ID,YOLO里存的是连续整数ID,三者之间全靠一张类名表来对齐。
3.1 先定类名表:三种格式的对齐前提和校验脚本
假设这份无人机数据集的类别是person, car, bicycle, truck, bus, boat这类常见目标(以压缩包里的实际类别为准,我这里只是举例)。先把它们写成一个固定列表,所有转换脚本都用这个列表,不要一边写一边改:
CLASSES = ["person", "car", "bicycle", "truck", "bus", "boat"]VOC的xml里<name>节点必须是这个列表里的字符串;COCO的categories数组转到YOLO时,按这个列表的索引重新编号;YOLO的txt里第一列就是列表的下标。这个顺序最终还会写进data.yaml的names里,一旦中途调换位置,之前生成的txt全部作废。
所以在做任何转换之前,先用一段脚本把三种格式里实际出现的类别全量打印出来,人工核对一遍:
import json, os, xml.etree.ElementTree as ET def scan_voc_classes(xml_dir): classes = set() for f in os.listdir(xml_dir): if not f.endswith(".xml"): continue root = ET.parse(os.path.join(xml_dir, f)).getroot() for obj in root.iter("object"): classes.add(obj.find("name").text) return classes def scan_coco_classes(json_path): with open(json_path, encoding="utf-8") as f: data = json.load(f) return {cat["name"] for cat in data["categories"]} def scan_yolo_classes(label_dir, class_num=6): ids = set() for f in os.listdir(label_dir): if not f.endswith(".txt"): continue with open(os.path.join(label_dir, f)) as fh: for line in fh: ids.add(int(line.split()[0])) return ids print("VOC:", scan_voc_classes("annotations")) print("COCO:", scan_coco_classes("annotations.json")) print("YOLO:", scan_yolo_classes("labels"))逻辑说明:三个函数分别扫xml里的name节点、json里的categories.name字段、txt每行的第一列,把实际出现的类名和类ID收集起来。比对时重点看两点——VOC和COCO的类名集合是否一致,YOLO的ID最大值是否小于类表长度。如果VOC里有car而COCO里叫Car,转换脚本不会报错,但训练时两个类会被当成不同目标;如果YOLO的ID最大是6而类表只有6项(0到5),说明有越界标签,必须回去查对应txt。
参数说明:class_num是人为指定的类别总数,用来检查ID范围;如果你的数据集中间有缺失类别,YOLO ID仍应是连续的,否则说明转换时发生了跳号。
3.2 voc转yolo:xml解析、归一化与坐标越界处理
VOC转YOLO是最常见的需求,因为很多标注工具默认输出VOC格式,而训练框架只认txt。下面这段脚本可以直接用:
import os, xml.etree.ElementTree as ET CLASSES = ["person", "car", "bicycle", "truck", "bus", "boat"] def voc_to_yolo(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) filename = os.path.basename(root.find("filename").text) out_lines = [] for obj in root.iter("object"): cls = obj.find("name").text if cls not in CLASSES: print(f"未知类名 {cls} 出现在 {xml_path}") continue if int(obj.find("difficult").text) == 1: continue bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) xmin = max(0, min(xmin, img_w)) xmax = max(0, min(xmax, img_w)) ymin = max(0, min(ymin, img_h)) ymax = max(0, min(ymax, img_h)) if xmax <= xmin or ymax <= ymin: continue w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h xc = (xmin + xmax) / 2 / img_w yc = (ymin + ymax) / 2 / img_h cls_id = CLASSES.index(cls) out_lines.append(f"{cls_id} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}") if out_lines: base = os.path.splitext(filename)[0] + ".txt" with open(os.path.join(out_dir, base), "w") as f: f.write("\n".join(out_lines)) os.makedirs("labels_yolo", exist_ok=True) for f in os.listdir("annotations"): if f.endswith(".xml"): voc_to_yolo(os.path.join("annotations", f), "labels_yolo")逻辑说明:脚本先取size里的宽高作为归一化分母,再遍历每个object节点,把bndbox的四个绝对坐标读出来做clip修正,最后换算成中心点加宽高的归一化值。difficult=1被直接跳过,这一步是否执行取决于你的数据质量——如果标得没问题,跳掉更干净。
参数说明:.6f是格式化精度,保留6位小数足够,过多小数位会增加文件体积但对精度没帮助;CLASSES.index(cls)如果类名不存在会抛异常,实际工程里建议先打印再跳过,避免一个脏数据中断整个转换流程。要注意filename节点里可能带着路径,用os.path.basename取干净,保证输出的txt能和图片文件名精确配对。
3.3 coco转yolo:跳过iscrowd、换算bbox与反向校验
COCO转YOLO比VOC转YOLO多两个坑:一是category_id要重新映射,二是iscrowd标注要过滤。参考实现:
import json, os CLASSES = ["person", "car", "bicycle", "truck", "bus", "boat"] def coco_to_yolo(json_path, out_dir): with open(json_path, encoding="utf-8") as f: data = json.load(f) img_info = {img["id"]: img for img in data["images"]} cat_map = {cat["id"]: i for i, cat in enumerate(sorted(data["categories"], key=lambda c: c["id"]))} anns_by_img = {} for ann in data["annotations"]: if ann.get("iscrowd", 0) == 1: continue if ann["area"] <= 0: continue anns_by_img.setdefault(ann["image_id"], []).append(ann) for img_id, anns in anns_by_img.items(): img = img_info[img_id] img_w, img_h = img["width"], img["height"] base = os.path.splitext(os.path.basename(img["file_name"]))[0] lines = [] for ann in anns: x, y, w, h = ann["bbox"] if w <= 0 or h <= 0: continue cls_id = cat_map[ann["category_id"]] xc = (x + w / 2) / img_w yc = (y + h / 2) / img_h w_n = w / img_w h_n = h / img_h lines.append(f"{cls_id} {xc:.6f} {yc:.6f} {w_n:.6f} {h_n:.6f}") if lines: with open(os.path.join(out_dir, base + ".txt"), "w") as f: f.write("\n".join(lines))逻辑说明:cat_map把json里原始的category_id映射成按名称排序后的连续ID,这一步保证了「COCO的任意ID」变成「YOLO的0到N-1」。anns_by_img把散落的标注按图聚合,避免每张图重复开文件。iscrowd=1的标注是群体目标,边界框代表一群人,不适合当独立检测目标,转的时候直接过滤。
参数说明:sorted(data["categories"], key=lambda c: c["id"])是按原始ID排序再做映射,保证映射结果稳定可复现;如果你的data.yaml里names顺序不是按原始ID排的,要改成手动指定顺序而不是这里自动排序。area <= 0也是过滤条件,COCO里面积为零的标注通常是垃圾标注。
转换完成后必须做反向校验。随便挑20张图,把生成的txt画回原图上看框是否贴合目标:
import cv2 def draw_yolo_box(img_path, txt_path, classes, out_path): img = cv2.imread(img_path) h, w = img.shape[:2] with open(txt_path) as f: for line in f: cid, xc, yc, bw, bh = map(float, line.split()) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, classes[int(cid)], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(out_path, img)如果画出来的框位置明显偏移、宽高比例不对,多半是归一化分母用错了图——比如读取json里的width/height用的是缩略图尺寸,而实际图片是原图。这是个非常隐蔽的坑,因为不画框根本看不出来。
4. 划分训练集和验证集:无人机抽帧数据最怕随机划分
5000张图的标签再全,划分方式不对,训练评估就是自欺欺人。无人机数据大多是视频抽帧得到的,相邻帧之间背景几乎一样、目标位置只差几个像素,这种情况下随机划分会让验证集里出现和训练集高度相似的画面,mAP虚高,换到真实场景立刻露馅。
4.1 随机划分与分层划分:小数据集用哪一个
如果5000张图是从不同场景、不同航班采集的独立照片,随机划分就够了。一个标准脚本如下:
import os, random, shutil IMG_DIR = "images" LABEL_DIR = "labels_yolo" OUT_DIR = "split" RATIO = (0.8, 0.1, 0.1) random.seed(42) files = [f for f in os.listdir(IMG_DIR) if f.endswith((".jpg", ".png", ".jpeg"))] random.shuffle(files) n_train = int(len(files) * RATIO[0]) n_val = int(len(files) * RATIO[1]) parts = { "train": files[:n_train], "val": files[n_train:n_train + n_val], "test": files[n_train + n_val:] } for part, flist in parts.items(): os.makedirs(os.path.join(OUT_DIR, "images", part), exist_ok=True) os.makedirs(os.path.join(OUT_DIR, "labels", part), exist_ok=True) for f in flist: shutil.copy(os.path.join(IMG_DIR, f), os.path.join(OUT_DIR, "images", part, f)) label = os.path.splitext(f)[0] + ".txt" if os.path.exists(os.path.join(LABEL_DIR, label)): shutil.copy(os.path.join(LABEL_DIR, label), os.path.join(OUT_DIR, "labels", part, label)) else: print(f"标签缺失: {f}")逻辑说明:先把所有图片文件打乱,按比例切成三份,再把对应的标签文件复制过去。random.seed(42)保证每次运行划分结果一致,方便复现实验。标签缺失时打印出来而不是静默跳过,否则yolov8会把这张图当成没有目标的负样本,数据白白浪费。
参数说明:RATIO用8000张以下的数据集可以设0.8/0.1/0.1,样本量再大可以设0.9/0.05/0.05。验证集至少要有几百张,否则mAP曲线抖动得没法看。test集平时不参与训练也不参与验证,只留到最后做一次最终评估,这个习惯能避免你在调参时把验证集信息泄漏到模型里。
4.2 按来源分组划分:防止同一架无人机的相似帧串集
如果文件名能看出视频来源(比如DJI_20240701_001.jpg、V0001_000123.jpg这种命名),就要按前缀分组再划分。思路是:先按文件名第一段聚合,把同一来源的图片看作一个整体,按组划分而不是按单张划分:
from collections import defaultdict files = [f for f in os.listdir(IMG_DIR) if f.endswith((".jpg", ".png", ".jpeg"))] groups = defaultdict(list) for f in files: prefix = f.split("_")[0] groups[prefix].append(f) group_names = list(groups.keys()) random.shuffle(group_names) n_train_groups = int(len(group_names) * 0.8) n_val_groups = int(len(group_names) * 0.1) train_files = [] val_files = [] test_files = [] for i, gname in enumerate(group_names): if i < n_train_groups: train_files.extend(groups[gname]) elif i < n_train_groups + n_val_groups: val_files.extend(groups[gname]) else: test_files.extend(groups[gname])逻辑说明:f.split("_")[0]取出文件名第一段作为来源标识,同一段前缀的所有帧永远进同一个集合。这样做以后,验证集里的画面和训练集不再来自同一条视频流,评估结果才接近真实部署。
参数说明:前缀分隔符要按实际文件名调整,有些命名是20240701_001.jpg,有些是DJI_0001_01.jpg,先用打印的方式看几个前缀再定。如果前缀粒度太粗(整个数据集只有两三个前缀),划分结果会很不均匀,这时可以退一步用「按时间戳分桶」:把文件名里的序号切出来,每隔N帧抽一帧作为验证,近似模拟时间上的隔离。
4.3 划分后同步三种格式:一份名单管所有标签
还有一种麻烦情况:你用voc和coco格式做评估,但训练只用yolo格式。同一张图在三种格式下有三份标签,划分时就得保证三份同步。最稳的做法是先用一份划分名单,然后挨个移动:
import json, shutil def move_by_list(file_list, src_img, src_voc, src_coco, dst_dir): for f in file_list: base = os.path.splitext(f)[0] shutil.copy(os.path.join(src_img, f), os.path.join(dst_dir, "images", f)) shutil.copy(os.path.join(src_voc, base + ".xml"), os.path.join(dst_dir, "voc", base + ".xml")) # COCO是单json,需要在划分后重建json子集逻辑说明:图片、xml、txt按文件名一一对应移动,COCO因为是一个大json,不能直接按文件切,必须在划分后通过image_id过滤重建子集。实际操作中如果你不打算用COCO训练,划分阶段可以只处理图片和txt,COCO留到需要提交官方评估时再单独生成。
参数说明:移动用copy不用move,保留原始数据作为后悔药。等你确认划分结果没问题、训练也跑通了,再删原始目录不迟。5000张图的复制在本地磁盘上也就几十秒,这个成本值得花。
5. 训练过程避坑:小目标漏检、loss为nan、mAP虚高一次说完
数据准备好了,真正开始yolov8训练自己的数据集时,问题才开始冒出来。这一章写的是我在这类无人机数据集上反复踩过的坑,每一条都按现象、原因、解决来写,你也可以直接当作排查清单用。
5.1 小目标占比高:先从imgsz、mosaic和切片推理入手
现象:训练loss下降正常,验证mAP也不算低,但把训练好的模型拿去检测一张无人机俯拍图,几辆小车和一个行人全漏了,只剩大卡车被框出来。如果检测结果全是这种「大地物检出、小目标全丢」,基本可以断定是小目标问题。
原因:无人机视角的目标普遍只占图像面积的千分之一甚至更小。yolov8默认imgsz=640,原图resize到这个尺寸时,几十像素的小目标缩到几个像素,特征图上的信息几乎消失。另外默认开启的mosaic增强会把四张图拼在一起再随机裁剪,小目标在拼接后的图上经常被裁掉一半。
解决:先试imgsz=1024甚至1280,这是最直接有效的办法,代价是显存占用翻倍;把mosaic关闭或延后,训练命令里加mosaic=0,或者保留mosaic但把close_mosaic=10改成更大的值,让最后10个epoch用干净图微调。如果显存不够,还有一个常见做法是保留640输入,用SAHI这类切片推理工具把大图切成小块分别检测再合并结果,这是保底方案,不改变训练但能明显提升小目标召回。
5.2 loss变成nan:八成是标签数据出了问题
现象:训练到几十个batch,loss突然变成nan,然后一直nan下去,loss曲线直接掉出图外。很多人第一反应是学习率太大,调低lr0重新跑,结果照旧。
原因:数据里存在非法标注。最常见的三种:txt里归一化坐标出现负数或大于1(VOC转YOLO时没做clip),宽度或高度为0(xmax==xmin或者ymax==ymin),类别ID超出了data.yaml里names的数量。这些非法标签在mosaic拼接或随机裁剪增强时产生异常计算,最终导致loss发散。
解决:训练前先跑一遍清洗脚本:
import os bad = [] for f in os.listdir("labels_yolo"): if not f.endswith(".txt"): continue with open(os.path.join("labels_yolo", f)) as fh: for line in fh: parts = line.split() if len(parts) != 5: bad.append((f, "字段数错误")) continue cid, xc, yc, w, h = map(float, parts) if not (0 <= xc <= 1 and 0 <= yc <= 1 and 0 < w <= 1 and 0 < h <= 1): bad.append((f, "坐标越界")) if cid < 0 or int(cid) >= 6: bad.append((f, "类ID越界")) print(bad[:50]) print("问题标签数:", len(bad))逻辑说明:逐行检查五个字段的取值合法性:中心点必须在0到1之间,宽高必须大于0且不超过1,类ID必须在类别范围内。这套检查对voc、coco转换过来的数据都适用。bad列表里收集所有问题标签,先看前50条判断问题集中在哪,再回源数据修。
参数说明:int(cid) >= 6里的6要改成你实际类别数;如果清洗后发现空标签文件(txt里一行都没有),yolov8会把对应图片当作负样本,这在背景干净的数据集里问题不大,但若图片本身有目标,就要回到VOC或COC O检查转换是否漏了标签。
5.3 验证集mAP高但实际漏检:串帧和difficult在捣乱
现象:训练完验证mAP有0.85,兴冲冲拿一段没见过的无人机视频去测,结果漏检率很高,和mAP完全不成正比。很多人这时候会怀疑模型过拟合,但真正的原因往往在数据划分。
原因:两个主要嫌疑。第一个是划分时没按视频来源分,验证集里大量图片和训练集出自同一段视频流,画面几乎一样,模型等于见过答案再考试;第二个是VOC格式里difficult=1的标注,如果转换时没过滤,评估时又把difficult样本算进去,验证集的mAP会虚高或虚低,和真实场景完全对不上。
解决:划分阶段按第4.2节的前缀分组方法重做,保证验证集在场景上独立;同时在VOC转YOLO阶段就把difficult=1过滤掉。还有一个容易忽略的动作:训练完成后,找几张完全不在这5000张图里的照片或者一段实拍视频,用yolo predict跑一遍,肉眼看检测效果。这个动作叫「野测」,比任何指标都真实。
5.4 类别不均衡:少样本类别被大类别吃掉
现象:数据集里car有3000个目标,boat只有200个。训练完car的召回率0.9,boat的召回率0.3,且无论怎么调置信度阈值,boat都很难被召回。
原因:目标检测训练时每个batch里的样本按图采样,图里boat出现得少,模型对boat的特征学习不充分。yolov8没有直接把loss按类别加权的参数(不像分类任务那样有class_weight),所以解决思路要从数据层面下手。
解决:对包含boat的图片做重复采样,把这类图片在训练集里多复制几份,让每个epoch里boat样本出现次数增加。操作上是保留原始图片不变,只把boat相关的图片额外复制到train目录,文件名加后缀防止覆盖,同时复制对应txt。另一种做法是对少样本类别做离线数据增强,比如裁切、旋转、亮度调整,生成新的训练样本后重新标注。要注意的是,验证集不要做重复采样,否则评估结果失真。
5.5 显存溢出或训练太慢:先查路径、缓存和batch
现象:训练刚开始就报CUDA out of memory,或者一个epoch跑得特别慢。很多人第一反应是换更大显存的卡,但无人机数据集的训练卡顿往往不是显卡不够。
原因:第一,数据集路径带中文或空格,ultralytics某些版本在处理这类路径时会异常变慢甚至报错;第二,cache=True参数想把数据缓存到内存,5000张原图加上增强后的中间结果直接把内存打爆;第三,workers设得过高,数据加载成了瓶颈。
解决:数据集路径改成纯英文的绝对路径,比如E:/uav_dataset这种,不要放在带空格的目录下;第一次训练先不开cache,跑通后再考虑cache=True加速;batch显存不足时优先调batch=8或batch=4,配合amp=True(混合精度训练,默认开启)能省下不少显存。还有一个容易被忽略的配置:把workers从默认值调低到2或4,避免数据加载进程之间互相抢占CPU资源。
6. 用yolov8把这份数据跑通:从data.yaml到验证曲线的一站式命令
前面的格式转换和划分都做完后,训练本身反而简单。这一章把所有命令串起来,给出一份可以照抄的最小闭环。
6.1 统一目录结构和data.yaml的写法
yolov8要求的目录结构是images/train、images/val、labels/train、labels/val四个目录。按第4章的划分脚本执行后,应该已经得到这样的结构。然后写data.yaml:
path: ./split train: images/train val: images/val names: 0: person 1: car 2: bicycle 3: truck 4: bus 5: boatnames的顺序必须和txt里第一列的整数ID完全对应。最常见的翻车现场是:txt里类ID是0到5,但names写成了VOC里的类名字符串顺序,比如0: boat、1: car,训练不报错但模型学的东西全乱了。
6.2 训练与验证的命令和关键参数
yolo detect train \ data=./split/data.yaml \ model=yolov8n.pt \ imgsz=1024 \ batch=16 \ epochs=100 \ close_mosaic=10 \ patience=20 \ project=./runs \ name=uav_detmodel=yolov8n.pt是官方预训练权重,第一次运行会自动下载;如果你离线环境没有这个文件,可以换成yolov8n.yaml从零训练,但收敛速度会慢不少。imgsz=1024是按第5.1节的小目标结论设置的。close_mosaic=10表示最后10个epoch关闭mosaic增强,让模型在正常分布上微调。
训练结束后验证:
yolo detect val \ data=./split/data.yaml \ model=./runs/uav_det/weights/best.pt \ batch=16输出的mAP50、mAP50-95是模型在验证集上的直接表现。如果你怀疑结果虚高,去检查第4.2节的划分是否严格按来源分组,别只看数字。
6.3 用单张图推理验证闭环
训练完先别急着上视频测试,用一张不在数据集里的无人机照片做单图推理:
from ultralytics import YOLO model = YOLO("./runs/uav_det/weights/best.pt") results = model.predict("test_flight.jpg", conf=0.25, iou=0.45) results[0].save("test_flight_out.jpg")conf=0.25是置信度阈值,低于这个值的框会被丢掉;如果漏检多就降到0.15,如果误检多就提高到0.4。iou=0.45是NMS的IoU阈值,用于去重,无人机密集排列的小目标场景下,这个值调低到0.3可能更合适。打开输出图看框的位置和类别是否合理,这一步通过后,这份YOLO无人机目标检测数据集就算真正被你消化了。
我自己每次拿到新数据集,都会先跑这一步单图验证再决定要不要回头调数据。这个习惯帮我避开了不少「指标好看、落地翻车」的尴尬。希望帮到你。
本文还有配套的精品资源,点击获取