ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

航拍牛羊小目标检测实战:从VOC/YOLO数据集到YOLOv8训练

航拍牛羊小目标检测实战:从VOC/YOLO数据集到YOLOv8训练 简介面向智慧牧场航拍场景的牛羊检测任务这份数据集聚焦远距离小目标识别难题适合计算机视觉学习者、算法工程师及农业智能化项目开发者用于模型训练与算法验证。数据集中包含1021张航拍图像每张均配套Pascal VOC格式的xml标注文件和YOLO格式的txt标注文件标注类别为cattle、cow、sheep共3类总标注框数达14047个其中sheep框数最多可为目标检测和小目标专项优化提供充足样本。资源包共2000个文件以xml标注文件和txt标签文件为主压缩包大小约97.58MB数据量适中标注工具采用labelImg矩形框标注规则统一已按VOC与YOLO两种主流格式整理能快速接入常用检测框架。目前已有379人学习下载。对于需要构建智慧牧场巡检系统、开展航拍小目标检测研究的读者这是一份标注准确、结构清晰、开箱即用的数据集资源。1. 拿到航拍牲畜数据集第一件事不是训练而是想清楚“小目标”这件事做智慧牧场的人基本都卡在同一个地方无人机飞上去画面里草场占 90%牛羊只占几百甚至几十个像素模型要么漏检、要么把石头和树影当羊。这份「智慧牧场航拍牛羊检测远距离小目标数据集」看起来只是 1021 张图、3 个类别但真正值钱的是它背后那个“远距离小目标”的设定——它直接决定了你该用什么模型、什么输入尺寸、什么推理策略。目标检测里有个不常被提起的底线同样的模型在近距离大目标上可能 mAP 能到 0.9换到远距离小目标马上就跌到 0.4 以下这不是调参能救的是数据分布本身就决定了难度。这篇笔记就按“数据集长什么样 → 怎么把它喂给模型 → 参数怎么设 → 坑在哪 → 怎么榨干它”的顺序展开适合刚拿到 VOC/YOLO 格式数据集准备训练的从业者也适合已经在无人机场景里翻过车的熟手对照排查。2. 先看 1021 张图里有什么数据集的目录结构、标签格式与类别盘点2.1 拿到.7z压缩包先检查自校验和解压目录这份数据集打包成.7z格式不少人在这一步就踩坑直接双击用系统自带解压工具解压解压到一半报错“头部损坏”。实际上.7z有多种压缩模式如果你的系统和工具版本不匹配很容易解压失败。我一般会这样处理# 在 Linux 下用 p7zip 解压 7z x 智慧牧场航拍牛羊检测远距离小目标数据集VOCYOLO格式1021张3类别.7z # 如果没有安装 p7zip先安装 sudo apt install -y p7zip-full # 解压后先看作目录大小和文件是否完整 du -sh 智慧牧场航拍数据集解压后大概率能看到两个核心目录VOC和YOLO分别对应两种标注格式。万一7z命令报错提示“cannot find header”就不要犹豫换 WinRAR 或者用7z t先测试压缩包完整性。压缩包传输过程中丢字节这种事并不罕见遇到这种问题优先重新下载少在修复上花时间。2.2 VOC 子目录和 YOLO 子目录各自怎么组织的VOC 格式的目录结构是 Pascal VOC 惯例这一点先记牢VOC/ ├── JPEGImages/ # 原始航拍图片通常是 .jpg ├── Annotations/ # 与图片同名的 .xml 标注文件 ├── ImageSets/ │ └── Main/ │ ├── train.txt │ ├── val.txt │ └── trainval.txt而 YOLO 格式目录相对自由常见的组织方式如下YOLO/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml这两个格式的区别是本质性的VOC 的 XML 里保存的是目标的像素坐标而 YOLO 的 TXT 标签里保存的是归一化后的中心点坐标和宽高值域在 0 到 1。这份数据集同时给出两种格式意味着你拿到手既可以跑 YOLO 系列模型也可以用 Faster R-CNN 这类基于 VOC 格式的检测框架。注意先确认 JPEGImages 里的文件名和 Annotations 里的 XML 文件名是否一一对应。常见的数据集翻车现场是图片 1021 张但标签只有 1020 个或者有 3 张图没有对应的 XML这直接导致训练时报FileNotFoundError。2.3 三个类别怎么确认类别名里的坑标题里说 3 类别但没说具体是哪三类。拿到数据集后第一步就是用脚本扫描一遍 XML 里的name标签看类别到底是什么是大类分成cow、sheep、horse还是分得更细。这一步很重要决定你后面在data.yaml里的类别顺序。# 在 YOLO 目录下直接看 data.yaml cat YOLO/data.yaml如果没有data.yaml只有一个classes.txt那也是正常现象。类别顺序至关重要YOLO 的标签文件里每个目标的第一个数字就是类别 ID它对应data.yaml里的names索引。如果类别顺序和你的交叉验证划分不一致哪怕差一个位置训练出来都是错乱。另外注意一种情况VOC 的 XML 里同一个类别可能有两种写法比如Sheep和sheep大小写混用。你的脚本如果不做strip().lower()归一化就会被当成两个类别——但在 YOLO 的 TXT 里它们的类别 ID 却混在一起。这种情况我遇到过结果是损失函数看着正常但混淆矩阵里全是乱象。3. 把 VOC 转成 YOLO 格式转换脚本与四个边界坑3.1 为什么要保留两种格式很多教程不告诉你很多人拿到 VOC YOLO 双格式数据集后第一反应是“YOLO 都有了VOC 目录删掉省空间”。我的建议是留着别删。VOC 格式对你的意义不只是兼容 Faster R-CNN 等架构。当你需要排查一个模型的失误样本时XML 里的difficult、truncated、pose字段能告诉你这个目标是不是本身被遮挡、边界不完整或者标注有争议。而 YOLO 的 TXT 格式把这些信息全丢了只留下类别 ID 和归一化坐标。你后面做错误分析时VOC 的 XML 就是你的后悔药。如果你需要自己在两种格式之间转换下面这段脚本是可靠的从业方案我在多个数据集上都跑过import os import xml.etree.ElementTree as ET from pathlib import Path # 配置路径 voc_annotations Path(VOC/Annotations) yolo_labels Path(YOLO/labels/train) yolo_labels.mkdir(parentsTrue, exist_okTrue) # 类别列表顺序必须与 data.yaml 保持一致 class_names [cow, sheep, horse] def voc_to_yolo(xml_path, out_path): tree ET.parse(str(xml_path)) root tree.getroot() # 读取图片尺寸 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) # 检查图片尺寸是否有效 if img_w 0 or img_h 0: print(f警告{xml_path.stem} 图片尺寸为 0跳过) return lines [] for obj in root.findall(object): name obj.find(name).text.strip().lower() if name not in class_names: print(f警告{xml_path.stem} 包含未知类别 {name}跳过) continue # 跳过 difficult 目标 difficult obj.find(difficult) if difficult is not None and int(difficult.text) 1: continue cls_id class_names.index(name) 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) # 计算 YOLO 归一化坐标 x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h width (xmax - xmin) / img_w height (ymax - ymin) / img_h # 越界检查防止坐标超出 0~1 if not all(0 v 1 for v in [x_center, y_center, width, height]): print(f警告{xml_path.stem} 存在越界框已裁剪) x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) width min(max(width, 0.0), 1.0) height min(max(height, 0.0), 1.0) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) # 写入 YOLO 标签空文件也保留 with open(out_path / (xml_path.stem .txt), w) as f: f.write(\n.join(lines)) # 批量转换 for xml_file in voc_annotations.glob(*.xml): voc_to_yolo(xml_file, yolo_labels)这段脚本的逻辑是从 XML 里读取object节点按类别名称映射到类别 ID将bndbox的像素坐标转为归一化后的中心点坐标和宽高最后写入 TXT 文件。其中有三个参数直接影响下游训练class_names的顺序必须和YOLO/data.yaml完全一致difficult目标默认跳过是为避免把标注本身有歧义的框送进训练集越界裁剪则是为了修正手滑标注出来的负数坐标。3.2 坐标系搞错等于训练全白费这里要重点说一个高发错误XML 里的bndbox坐标到底是 0-based 还是 1-basedPascal VOC 的官方定义里xmin和ymin是 1-based 的即最小坐标从 1 开始算。而 YOLO 归一化坐标是 0 到 1 的浮点数本身不关心这个偏移。但在转换时如果图片的宽度是 800某个框的xmin1, xmax800归一化后是(1800)/2/8000.500625和(800-1)/8000.99875看起来没什么问题。可如果你的脚本忘了除以img_w而是除以图片数组的shape[1]——大多数情况下这俩一样但万一读取图片的库做了 resize 或者有 padding整个坐标系就全部错乱了。更隐蔽的是XML 中存的坐标和 JPEGImages 里实际图片的分辨率不一致。航拍数据集中经常出现同一型号无人机拍摄但图片被后期压缩成不同分辨率的情况。转换前务必随机抽查 3~5 张图把img_w和img_h和实际图片用PIL或cv2读取的宽高对比不一致的话立刻停下排查。from PIL import Image import random voc_annotations list(Path(VOC/Annotations).glob(*.xml)) sample random.sample(voc_annotations, 5) for xml_file in sample: # 从 XML 读尺寸 tree ET.parse(str(xml_file)) size tree.getroot().find(size) xml_size (int(size.find(width).text), int(size.find(height).text)) # 从图片读尺寸 img_path Path(VOC/JPEGImages) / (xml_file.stem .jpg) with Image.open(str(img_path)) as img: img_size img.size print(f{xml_file.stem}: XML{xml_size}, 图片{img_size}) assert xml_size img_size, 尺寸不匹配3.3 空标签文件要不要删航拍大面积草场上有的图片可能一个目标都没有转换后的 TXT 文件就是 0 字节。这种情况要不要删掉取决于你用的训练框架。YOLOv5 和 YOLOv8 的训练管线允许空标签文件存在但会在日志里提示一些 warnings并且这些图片被当作纯背景参与训练有助于减少误检。但如果你用的是 Detectron2 这类框架空标签会直接报错需要过滤掉。我的习惯是保留空标签但在数据集的images和labels目录里同步保留对应图片文件然后用filter_empty_labels.py在训练前动态过滤而不是直接删文件。这样做的好处是后续如果换框架或者你决定将部分空图和三类别目标合并做难例挖掘还能找回来。4. 用 YOLOv8 跑通这份航拍数据集最小命令、关键参数和训练配置4.1 训练环境的搭建要避开的两个坑YOLOv8 的环境配置本身不复杂但很多人死在 PyTorch 版本和 CUDA 版本不匹配上。最典型的报错是UserWarning: CUDA initialization: The NVIDIA driver on your system is too old (found version 11080)这是因为 PyTorch 版本要求的最低 CUDA 版本与你机器上驱动的 CUDA 版本不一致。对于这份航拍数据集推荐用ultralytics包来训练兼容性最好# 创建干净的环境 conda create -n yolo python3.10 -y conda activate yolo # 安装 PyTorch注意根据你的 CUDA 版本选择对应的 index-url pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装 ultralytics pip install ultralytics # 验证 GPU 是否可用 python -c import torch; print(torch.cuda.is_available())第一次跑完后建议立刻检查torch.cuda.is_available()返回的是不是True这一步堵住了后续一半的报错。如果你的显卡是 30 系以上CUDA 11.8 是下限20 系可以用 cu117。4.2 用 YOLOv8s 跑第一次训练的最小命令和 data.yaml 配置跑通的最小命令如下模型用yolov8s起步不要一上来就上yolov8x那只会让你的迭代周期变成动辄几十分钟cd YOLO目录/../ # 假设 data.yaml 已经在 YOLO 目录下 yolo detect train \ dataYOLO/data.yaml \ modelyolov8s.pt \ epochs100 \ imgsz1280 \ batch16 \ workers4 \ device0命令里的imgsz1280是我特别标注的因为航拍远距离小目标直接关系到输入分辨率。如果你沿用默认的imgsz640画面里一只羊可能只占 10×10 像素骨干网络下采样到 1/32 特征图时这只羊在特征图里连 1 个像素都不到——模型根本看不见它。imgsz1280是起步理论上 1536 或 2048 效果更好但显存占用和训练时间会指数级别上升。4.3 小目标场景下必调的 5 个超参数imgsz第一优先级从 640 提到 1280。前提是显存能撑住。batch结合imgsz一起看显存不够就调小 batch不要降 imgsz。批大小尽量是 2 的幂。patience早停耐心值默认 100。小目标训练不稳定前 30 个 epoch 可能 mAP 纹丝不动别开太小的 patience 让训练早停。mosaicYOLOv8 默认开启 mosaic 数据增强对正常场景效果很好但对航拍小目标场景会带来一个问题多次拼接后目标被裁掉大半模型反而学到残缺特征。建议在训练中期将mosaic0.5或更低。close_mosaic在最后 10 个 epoch 关闭 mosaic让模型在接近真实分布的输入上微调这是 YOLOv8 的官方推荐。# YOLO/data.yaml 示例 train: YOLO/images/train val: YOLO/images/val nc: 3 names: [cow, sheep, horse]4.4 训练中实时盯着哪几个指标翻车信号长什么样训练过程中box_loss和cls_loss曲线是主要观察目标。正常情况是两条曲线稳定下降并趋于平滑。如果box_loss降到某个值后停止下降但cls_loss还在降说明定位已经到头了这时需要用更深的模型或者更大的输入尺寸来突破。另一个常见翻车信号是验证集 mAP 在第 20 个 epoch 冲到 0.8然后开始剧烈抖动。这不是模型过拟合而是航拍数据里某些图片光照变化大、小目标被云影遮挡模型在个别难样本上反复震荡。此时不用急着停止训练把patience调大让训练多走 30 个 epoch 看看有没有收敛。# 完整训练命令包含小目标优化的参数设定 yolo detect train \ dataYOLO/data.yaml \ modelyolov8s.pt \ epochs150 \ imgsz1280 \ batch16 \ mosaic0.5 \ close_mosaic10 \ cacheTruecacheTrue会把图片提前加载进内存航拍数据集单张图较大第一次加载会等一会儿但之后每个 epoch 的速度会快很多。显存 8G 以下不建议开容易内存溢出。4.5 训练完先看混淆矩阵别急着部署训练结束后runs/detect/train/confusion_matrix.png是你最该先看的图。对三类别数据集来说横轴和纵轴分别是真实类别和预测类别。如果你的sheep类别被大量误检成cow说明这两类在目标较小的情况下特征高度重合这时方案有两个一是补拍带有明显纹理差异的高空图像做数据增强二是在损失函数中加大难例权重用class weights调整类别失衡。YOLOv8 的yaml配置里可以单独为每个类别设置损失权重比如将少样本类别的cls损失权重提高。航拍数据集里三类数量可能不均衡数一数标签文件里每个类别的目标个数如果某个类只有另外类的三分之一就在 loss 权重上做倾斜。5. 避坑/常见问题/排查航拍小目标训练中你躲不开的五个坑5.1 训练时 loss 为 NaN现象前几个 epoch 的box_loss和cls_loss数值正常第 10 个 epoch 左右突然变成nan之后所有指标都显示nan。原因最常见的是学习率设置过大导致的梯度爆炸。小目标检测的特征图稀疏梯度更新不稳定加上航拍图片中目标极小跨 batch 梯度方差大很容易让参数跳到数值溢出区。另一种原因则是标签里出现了inf或极端值比如归一化坐标算出来是1.000001或-0.000001在后续损失计算中累积出错。解决将lr0从默认的 0.01 降到 0.001同时把label_smoothing开启到 0.01。如果是因为标签越界回头跑一遍第 3 章的转换脚本加上坐标裁剪逻辑。5.2 验证集 mAP 很高但实测航拍视频里全是误检现象验证集 mAP 到 0.85 以上模型拿到实际航拍视频里跑草地的纹理、围栏的影子、甚至无人机自身的机翼都被框成羊。原因验证集划分和训练集来自同一批拍摄条件模型学到的是“和训练图像分布相似的地面纹理”而不是“羊的语义特征”。这是航拍数据的通病——草地、灌木、岩石在不同光照下和羊的颜色纹理分别不明显。解决重新划分验证集确保同一个架次拍摄的图片不跨 train/val 划分。如果数据集的ImageSets/Main目录里已有trainval.txt和val.txt先检查它们是不是放的是同一个批次的图片。是的话按文件名前缀通常航拍图片命名含拍摄日期或架次号手动重分。另外在真实场景部署时适当将推理置信度阈值调高从默认的 0.25 提升到 0.4宁可漏检也别误检——牧场场景里误检一个围栏桩比漏检一只羊更让人头疼。5.3 小目标重合框 NMS 后消失现象同一只羊在检测结果里出现了两个框一个置信度 0.8 覆盖了羊的全身一个置信度 0.6 只框了羊的头部。NMS 之后置信度低的框被删除最终标注框只剩一个。但如果你分析的是目标数某些场景下两只羊挨得很近合并成一个框后模型会丢失一个目标直接表现为 F1 下降。原因YOLO 系列模型在网格级别的特征图上对目标中心点感知不够精细航拍俯视图中牛羊群聚集NMS 的 IoU 阈值影响了相邻框的最终保留情况。解决训练时将nms_iou从默认的 0.7 下调到 0.5允许相邻目标保留更多候选框同时使用agnostic_nmsTrue防止跨类别间误抑制。调参前先在验证集上跑一遍测试观察被抑制的置信度区间再决定是改阈值还是补标注数据。5.4 训练到一半报显存溢出现象CUDA out of memory报错常见于开启imgsz1280后 batch16 的情况下。原因显存占用不是线性增长的输入分辨率翻倍特征图尺寸翻倍激活值内存占用约翻 4 倍。8G 显存开 1280 分辨率本就吃紧。解决降低 batch 到 8 或者 4尽量不动imgsz。另外开启ampTrue混合精度训练能让显存占用降低约 30%。5.5 数据集中存在大量重复或相似图片现象训练集图片数量很大但 val 集上的 mAP 始终上不去画一下训练集的 embedding 分布大量图片重叠在同一区域。原因无人机航拍是连续的同一块草场被多帧覆盖标注结果在相邻帧里高度相似。这些重复图片在训练集里反复出现模型在训练时见过的“有效样本”远小于 1021 张。这一点可以说是航拍类数据集的通病需要自己排查。解决按时间戳或地理坐标抽帧去重也可以对相似图片做哈希比对删除重复项。删除时注意同时删除images和labels中对应文件别只删图片。6. 进阶玩法SAHI 切片推理和多尺度预测把数据集的剩余价值榨干训练完成后如果你觉得 mAP 和实际飞行中的检测率依然差一口气不需要急着重新标注数据先试试 SAHI 切片推理。SAHI 的全称是 Slicing Aided Hyper Inference思路很简单把大图切成一堆有重叠的小图分别推理后再合并结果。无人机航拍图通常是 4000×3000 级别的分辨率如果直接把整图缩放成 1280×1280 输入羊基本不可能被检测到。SAHI 的核心参数就是切片尺寸slice_size和重叠率overlap_ratio。from sahi.model import Yolov5DetectionModel from sahi.predict import get_sliced_prediction # 用训练好的权重初始化模型 detection_model Yolov5DetectionModel( model_pathruns/detect/train/weights/best.pt, confidence_threshold0.3, image_size1280, devicecuda:0, ) # 切片推理 result get_sliced_prediction( test_images/aerial_frame_001.jpg, detection_model, slice_height640, slice_width640, overlap_height_ratio0.2, overlap_width_ratio0.2, )slice_height和slice_width为 640 时显存开销小但切片数量多推理速度慢设为 1024 则速度更快但小目标检测力下降。我的经验是从 640 起步重叠率 0.2因为小目标刚好落在切片边界时没有重叠覆盖就会漏检。如果目标大小在 20×20 像素左右切片 640 和 1280 的差异不大硬选大切片反而可能让每张切片里目标数量过少背景占比过高。这里要提一个很多人忽略的细节SAHI 的切片推理结果要在最终预测框上再做一次 NMS不然切片重叠区域的目标会被重复识别最终框数量虚高。get_sliced_prediction默认会做全图 NMS不用自己额外处理但如果你改成自己写切片逻辑这一步千万别漏。最后用验证集跑一次类别别的 AP50 和 AP-small 对比看看模型到底在哪个尺寸段上掉链子yolo detect val \ dataYOLO/data.yaml \ modelruns/detect/train/weights/best.pt \ imgsz1280 \ plotsTrue输出结果里的metrics/small/AP50就是小目标面积小于 32×32 像素的平均精度。如果这个值和整体 AP50 差距超过 30%说明你的模型对小目标几乎没有感知SAHI 切片推理基本是必选项而不是可选项。这个数据集的真正价值不在于 1021 张图本身而是它把“远距离小目标”这个光照、海拔、季节都变数极大的问题给你提供了一个相对干净的可复现起点。我的习惯是先跑通流程拿到基准线然后再用自己的航拍素材做增量训练而不是直接把模型丢到现场用。数据集的边界条件摆在那里理解它比消费它更值钱。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进