
简介面向目标检测开发者和无人机视觉学习者这份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 filenameDJI_20240701_001.jpg/filename size width1920/width height1080/height depth3/depth /size object namecar/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin438/xmin ymin621/ymin xmax712/xmax ymax783/ymax /bndbox /object /annotation读取时三个关键点第一filename字段只写了文件名不写路径如果图片和xml不在同一层需要自己拼目录如果文件名里带中文或空格尽量在预处理时一并改掉。第二difficult标记表示这个目标是否难以辨认很多训练框架在读取VOC数据时默认把difficult1的样本过滤掉如果你的数据集里这个标记比较随意训练集和验证集会不一致最后评估mAP时就会对不上账。第三bndbox里的坐标允许略微超出图像边界转YOLO之前最好做一次clip否则归一化后可能出现大于1的坐标值。2.2 COCO的jsonimage_id、category_id和[x,y,w,h]的对应关系COCO格式把整个数据集的标注塞进一个json文件核心是三个数组images、annotations、categories。images里存每张图的id、file_name、width、heightannotations里每条记录通过image_id关联到图片通过category_id关联到类别bbox字段存的是[x, y, width, height]注意这是左上角坐标加宽高不是中心点。无人机目标检测数据集里的COCO标签有两个常见坑。第一category_id不一定是连续的可能从1开始、中间还有跳号而YOLO格式要求类别ID必须是从0开始的连续整数转换时不能直接拿category_id当YOLO类ID用必须重新映射。第二segmentation字段在检测任务里经常是一段多边形坐标如果直接忽略没问题但如果用现成脚本转换时没跳过iscrowd1的标注可能把一些群体目标当普通框算进来导致训练时标签噪音变大。另外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每张图一个xmlxmin, ymin, xmax, ymax左上角右下角字符串类名如car否COCO整个数据集一个jsonx, y, width, height左上角宽高整数可能不连续否YOLO每张图一个txtx_center, y_center, width, height中心点宽高整数从0开始连续是一句话记忆VOC是「两边」COCO是「一角加宽高」YOLO是「中心加宽高」。后面做转换脚本时所有换算都是围绕这三者之间的差异展开的。3. 标签转换脚本类名表统一voc、coco、yolo一个表错全盘错既然三种格式各有差异拿到压缩包后第一件事不是直接开训而是先验证三种格式的标签内容是否一致。尤其是类名顺序它是整个转换流程的中枢神经——VOC里存的是字符串类名COCO里存的是任意整数IDYOLO里存的是连续整数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, encodingutf-8) as f: data json.load(f) return {cat[name] for cat in data[categories]} def scan_yolo_classes(label_dir, class_num6): 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转yoloxml解析、归一化与坐标越界处理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_okTrue) for f in os.listdir(annotations): if f.endswith(.xml): voc_to_yolo(os.path.join(annotations, f), labels_yolo)逻辑说明脚本先取size里的宽高作为归一化分母再遍历每个object节点把bndbox的四个绝对坐标读出来做clip修正最后换算成中心点加宽高的归一化值。difficult1被直接跳过这一步是否执行取决于你的数据质量——如果标得没问题跳掉更干净。参数说明.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, encodingutf-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], keylambda 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把散落的标注按图聚合避免每张图重复开文件。iscrowd1的标注是群体目标边界框代表一群人不适合当独立检测目标转的时候直接过滤。参数说明sorted(data[categories], keylambda 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_okTrue) os.makedirs(os.path.join(OUT_DIR, labels, part), exist_okTrue) 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训练划分阶段可以只处理图片和txtCOCO留到需要提交官方评估时再单独生成。参数说明移动用copy不用move保留原始数据作为后悔药。等你确认划分结果没问题、训练也跑通了再删原始目录不迟。5000张图的复制在本地磁盘上也就几十秒这个成本值得花。5. 训练过程避坑小目标漏检、loss为nan、mAP虚高一次说完数据准备好了真正开始yolov8训练自己的数据集时问题才开始冒出来。这一章写的是我在这类无人机数据集上反复踩过的坑每一条都按现象、原因、解决来写你也可以直接当作排查清单用。5.1 小目标占比高先从imgsz、mosaic和切片推理入手现象训练loss下降正常验证mAP也不算低但把训练好的模型拿去检测一张无人机俯拍图几辆小车和一个行人全漏了只剩大卡车被框出来。如果检测结果全是这种「大地物检出、小目标全丢」基本可以断定是小目标问题。原因无人机视角的目标普遍只占图像面积的千分之一甚至更小。yolov8默认imgsz640原图resize到这个尺寸时几十像素的小目标缩到几个像素特征图上的信息几乎消失。另外默认开启的mosaic增强会把四张图拼在一起再随机裁剪小目标在拼接后的图上经常被裁掉一半。解决先试imgsz1024甚至1280这是最直接有效的办法代价是显存占用翻倍把mosaic关闭或延后训练命令里加mosaic0或者保留mosaic但把close_mosaic10改成更大的值让最后10个epoch用干净图微调。如果显存不够还有一个常见做法是保留640输入用SAHI这类切片推理工具把大图切成小块分别检测再合并结果这是保底方案不改变训练但能明显提升小目标召回。5.2 loss变成nan八成是标签数据出了问题现象训练到几十个batchloss突然变成nan然后一直nan下去loss曲线直接掉出图外。很多人第一反应是学习率太大调低lr0重新跑结果照旧。原因数据里存在非法标注。最常见的三种txt里归一化坐标出现负数或大于1VOC转YOLO时没做clip宽度或高度为0xmaxxmin或者ymaxymin类别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格式里difficult1的标注如果转换时没过滤评估时又把difficult样本算进去验证集的mAP会虚高或虚低和真实场景完全对不上。解决划分阶段按第4.2节的前缀分组方法重做保证验证集在场景上独立同时在VOC转YOLO阶段就把difficult1过滤掉。还有一个容易忽略的动作训练完成后找几张完全不在这5000张图里的照片或者一段实拍视频用yolo predict跑一遍肉眼看检测效果。这个动作叫「野测」比任何指标都真实。5.4 类别不均衡少样本类别被大类别吃掉现象数据集里car有3000个目标boat只有200个。训练完car的召回率0.9boat的召回率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某些版本在处理这类路径时会异常变慢甚至报错第二cacheTrue参数想把数据缓存到内存5000张原图加上增强后的中间结果直接把内存打爆第三workers设得过高数据加载成了瓶颈。解决数据集路径改成纯英文的绝对路径比如E:/uav_dataset这种不要放在带空格的目录下第一次训练先不开cache跑通后再考虑cacheTrue加速batch显存不足时优先调batch8或batch4配合ampTrue混合精度训练默认开启能省下不少显存。还有一个容易被忽略的配置把workers从默认值调低到2或4避免数据加载进程之间互相抢占CPU资源。6. 用yolov8把这份数据跑通从data.yaml到验证曲线的一站式命令前面的格式转换和划分都做完后训练本身反而简单。这一章把所有命令串起来给出一份可以照抄的最小闭环。6.1 统一目录结构和data.yaml的写法yolov8要求的目录结构是images/train、images/val、labels/train、labels/val四个目录。按第4章的划分脚本执行后应该已经得到这样的结构。然后写data.yamlpath: ./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 \ modelyolov8n.pt \ imgsz1024 \ batch16 \ epochs100 \ close_mosaic10 \ patience20 \ project./runs \ nameuav_detmodelyolov8n.pt是官方预训练权重第一次运行会自动下载如果你离线环境没有这个文件可以换成yolov8n.yaml从零训练但收敛速度会慢不少。imgsz1024是按第5.1节的小目标结论设置的。close_mosaic10表示最后10个epoch关闭mosaic增强让模型在正常分布上微调。训练结束后验证yolo detect val \ data./split/data.yaml \ model./runs/uav_det/weights/best.pt \ batch16输出的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, conf0.25, iou0.45) results[0].save(test_flight_out.jpg)conf0.25是置信度阈值低于这个值的框会被丢掉如果漏检多就降到0.15如果误检多就提高到0.4。iou0.45是NMS的IoU阈值用于去重无人机密集排列的小目标场景下这个值调低到0.3可能更合适。打开输出图看框的位置和类别是否合理这一步通过后这份YOLO无人机目标检测数据集就算真正被你消化了。我自己每次拿到新数据集都会先跑这一步单图验证再决定要不要回头调数据。这个习惯帮我避开了不少「指标好看、落地翻车」的尴尬。希望帮到你。本文还有配套的精品资源点击获取