简介:智慧牧场航拍牛羊检测数据集,聚焦无人机远距离拍摄中的小目标识别,面向智慧农业、畜牧信息化及目标检测算法研究者。内含1021张航拍图像,覆盖cattle、cow、sheep三个类别,共14047个标注框,其中sheep框数9518个,数据贴近真实养殖中羊群密集分布的特点。压缩包共2000个文件,以Pascal VOC的xml标注和YOLO的txt标注为主,xml便于可视化校验,txt可直接接入YOLO训练流程,从数据查看、标注检查到模型训练全链路可用。资源包约97.58MB,体积适中,便于下载与本地部署,全部标注由labelImg工具画框制作,类别划分合理、位置准确,适合智慧牧场牛羊识别、航拍远距离小目标检测等模型的训练与算法验证。目前已有379人学习使用,可为构建畜牧检测数据集或扩充小目标样本的研究者提供可靠基础。
1. 智慧牧场航拍牛羊检测:为什么远距离小目标数据集要先过三关
无人机在牧场上空 40 米巡航,一头牛在 4K 画面里往往只有 30×40 像素,这种远距离小目标检测,拿通用 COCO 预训练模型直接上基本抓瞎。智慧牧场航拍牛羊检测数据集这份 .7z 压缩包,把 1021 张真实航拍图像处理成 VOC 与 YOLO 双格式,三类别标注,省掉了从零标注和格式转换的重复劳动。但拿到手不等于能直接用:要过三关——确认压缩包完整、核对双格式标注一致、再把目录调整成训练框架认的样子。这篇文章按我实际处理这类数据集的操作顺序写,覆盖解压、检查、划分、训练参数和踩坑清单。适合正在做智慧牧场的算法工程师,也适合需要小目标基准数据跑实验的团队。
2. 拿到 7z 先别解压:完整性检查与 VOC/YOLO 双格式核对
2.1 解压前先测压缩包完整性:Linux 命令与 Windows 操作
在 Linux 服务器上,我习惯先用7z t校验压缩包完整性再解压,这一步能避免解压到一半发现文件损坏,还有补救机会。
# 测试 .7z 完整性 7z t 智慧牧场航拍牛羊检测远距离小目标数据集VOC+YOLO格式1021张3类别.7z # 确认无报错后解压,-o 指定目标目录,避免把文件名散落在当前目录 7z x 智慧牧场航拍牛羊检测远距离小目标数据集VOC+YOLO格式1021张3类别.7z -o./pasture_data7z t输出末尾会显示「Everything is Ok」,看到这个再解压。-o参数后不能加空格,直接跟目录名,这是 7-Zip 和 tar 不一样的地方。解压前顺手df -h看一眼磁盘,1021 张航拍图加标注文件,解压后通常要到几个 GB,预留 2 倍压缩包大小的空间比较稳妥。
Windows 机器上更简单,装好 7-Zip 后右键压缩包选「解压到当前文件夹」。如果在 PyCharm 的终端里操作,Windows 下执行7z命令的前提是 7-Zip 已加入 PATH;没加的话我一般直接右键解压,不再折腾环境变量。
提示:解压完先别删压缩包。后续训练若发现个别图解码失败或标注缺失,压缩包是唯一的后悔药。
2.2 核对目录结构:图像、XML、TXT 三份文件能不能对上
解压后不要急着看标注内容,先确认整体结构。航拍数据集常见组织方式是 images、Annotations、labels 三个目录分别放图片、VOC 的 XML、YOLO 的 txt。
from pathlib import Path root = Path("pasture_data") for d in root.iterdir(): if d.is_dir(): files = list(d.rglob("*.*")) print(f"{d.name}: {len(files)} 个文件")这段脚本把每个子目录的文件数打出来,第一轮就能发现有没有目录是空的,或者 images 下混进了 .xml 文件。正常的数量关系是:图片数 = XML 数 = txt 数(少数负样本图只有图没有标注,后面单独处理)。1021 张图,如果 images 目录出现 1021 个文件而 labels 只有 800 个,说明有 221 张图的标注丢了,需要回压缩包检查确认是不是原始数据就是如此。
2.3 看懂 VOC 标注:XML 里的 bndbox 是像素坐标
VOC 格式用 XML 描述每个目标,核心是bndbox里的四个值,单位是像素,取目标框的左上角和右下角。
<annotation> <filename>IMG_0001.jpg</filename> <size> <width>3840</width> <height>2160</height> </size> <object> <name>cattle</name> <bndbox> <xmin>120</xmin> <ymin>85</ymin> <xmax>152</xmax> <ymax>118</ymax> </bndbox> </object> </annotation>这个框宽 32 像素、高 33 像素,在 3840×2160 的画面里属于典型远距离小目标。用 VOC 标注训练时,框架自己会读 XML,但你要是想把这份数据和其他标注来源合并,就要统一命名:name字段的拼写必须一致,别一个写cattle一个写cow,类别数会莫名翻倍。
还需要留意<size>里的宽高是否和实际图片分辨率一致。航拍相机偶尔会带旋转信息,有的工具导出标注时把宽高写反,结果<xmax>大于<width>,这种错位在 VOC 里很隐蔽,后面转 YOLO 时直接算成大于 1 的坐标。
2.4 读懂 YOLO 标注:归一化中心坐标与类别 ID 是训练真正吃的格式
YOLO 系列训练要求 txt 每行一个目标,格式是“类别 ID + 归一化中心 x + 归一化中心 y + 归一化宽度 + 归一化高度”。
0 0.0354 0.0456 0.0089 0.0157 0 0.2134 0.5678 0.0123 0.0189 1 0.4321 0.1234 0.0456 0.0678第一个数字是类别 ID,3 个类别对应 0、1、2。后面的四个小数因为除以了图片宽高,范围应在 0 到 1 之间。把上面第一行还原,对应 3840×2160 的图,目标中心在 x=136、y=98,宽约 34 像素、高约 34 像素,边界框接近正方形——航拍视角下牛羊的框普遍接近正方形,因为俯拍没有侧视角拉长。
拿到数据集后,我一定做一次快速转换验证:随机抽一个 XML,手工算 YOLO 值,和 txt 里对一下。差一个像素以内是舍入误差,差出 0.1 以上就是格式转换脚本有问题,这份数据不能直接拿来训练。
2.5 脚本找出空标签样本、超界框与微小框
这一步很重要,用小脚本扫一遍全部 labels。
from pathlib import Path label_dir = Path("pasture_data/labels") verify_ok, empty_cnt = 0, 0 for txt in label_dir.rglob("*.txt"): lines = txt.read_text(encoding="utf-8").strip().splitlines() if not lines: empty_cnt += 1 continue for line in lines: parts = line.strip().split() if len(parts) != 5: continue _, x, y, w, h = map(float, parts) if not (0 <= x <= 1 and 0 <= y <= 1 and w > 0 and h > 0): print(f"越界: {txt.name} -> {line}") print(f"空标签文件数: {empty_cnt}")逻辑说明:空标签(txt 文件内容为空)影响不大,YOLO 会当作背景负样本,但如果空标签数量占比超过 5%,就要考虑是不是标注遗漏。越界坐标几乎一定是格式转换 bug,训练时坐标归一化会出 NaN 或错位,必须修。微小框不报错,但对训练结果影响最大,一个 3×3 像素的框在缩放后可能直接消失。
远距离小目标数据集里,我自己的经验是把宽或高小于 5 像素的框单独统计出来,多数航拍数据集这部分约有 2% 到 5%。它们不一定是错的,但若是标注误差导致的随机微小框,会拉低 mAP,后面换损失函数时这种噪声会被放大。
3. 把 1021 张图整理成 YOLOv5/v8 的目录结构:划分脚本与 data.yaml
3.1 YOLO 系训练框架为什么认「images 与 labels 分目录」这一套
YOLOv5、YOLOv8 这类框架默认用images/train、images/val存图,labels/train、labels/val存标注,二者靠文件名对应。这样设计的用意是效率:训练时按图片路径找同名 txt 比解析一张 XML 再转坐标快得多,而且图片加载管线不需要额外维护格式转换逻辑。
这也意味着训练前要做的不是格式转换,而是目录搬运——把上一节检查合格的 txt 按图片的 train/val 划分同步复制到标签目录。这份数据集标题里写了 VOC + YOLO 双格式,一般不用再转,但验证一下不会亏。如果某些框架不认这套规则,常见做法是写一个软链接目录,把 images 指到实际存储位置,避免大文件复制两份。
3.2 训练集/验证集划分脚本:按拍摄批次划分,别纯随机
随机划分 20% 做验证集是最省事的方式,但航拍视频相邻帧极度相似,纯随机会把同一批连续帧拆到训练和验证里,导致验证指标虚高。常见做法是像 PHM2012 这类工业预测数据集一样按序列分组:如果文件名含拍摄批次或时间戳,就按批次分。
import random import shutil from pathlib import Path random.seed(42) img_dir = Path("images") label_dir = Path("labels") train_img = Path("images/train") val_img = Path("images/val") train_label = Path("labels/train") val_label = Path("labels/val") all_imgs = sorted(img_dir.glob("*.jpg")) val_cnt = int(len(all_imgs) * 0.2) # 按单张随机划分;若文件名带 seq01 之类前缀,改成按前缀分组 random.shuffle(all_imgs) val_imgs = set(all_imgs[:val_cnt]) for img in all_imgs: is_val = img in val_imgs target_img = val_img if is_val else train_img target_lab = val_label if is_val else train_label target_img.mkdir(parents=True, exist_ok=True) target_lab.mkdir(parents=True, exist_ok=True) shutil.copy2(img, target_img / img.name) label = label_dir / f"{img.stem}.txt" if label.exists(): shutil.copy2(label, target_lab / label.name) print(f"train: {len(train_img.glob('*.jpg'))}, val: {len(val_img.glob('*.jpg'))}")逻辑说明:random.seed(42)保证每次运行划分结果一致,val_cnt取 20% 共 204 张做验证。真正的关键在划分粒度:用shutil.copy2不仅复制图片,也保留原文件时间戳等元信息。如果文件名带seq01_、seq02_这类批次前缀,应先去重批次号再按批次划分,宁可验证集略少,也不能让验证集里混进训练集的近邻帧,否则线上表现会狠狠打脸。
3.3 写 data.yaml 并核对 names 顺序
YOLO 训练入口是 data.yaml,框架按这里面的配置找路径,按nc和names定义类别数。
path: ./pasture_data train: images/train val: images/val nc: 3 names: 0: cattle 1: sheep 2: otherpath填项目根目录的相对路径,train和val填相对于根目录的路径。最容易踩的点是names里的顺序必须和 txt 文件的第一个数字严格对应,不能看到类别多就乱排。一个快速验证方法:找一张只含单一类别目标的图,打开它对应的 txt,确认该类别 ID 与names里的顺序一致,比如 txt 里第一行开头是2,那names[2]就要是那个类别名。
4. 远距离小目标的训练策略:分辨率、锚框与损失怎么调
4.1 训练与推理尺寸:为什么小目标数据优先 imgsz=1280
航拍图分辨率通常很高,3840×2160 很常见。若直接缩到 640×640 训练,一个 32×32 像素的目标在缩放后只剩 5×5 像素,经过网络下采样到特征图,可能就剩一个点。这个问题不是调参能解决的,得从输入尺寸下手。
# YOLOv5 训练时指定 python train.py --data data.yaml --img 1280 --batch 16 --epochs 100 # YOLOv8 训练时指定 yolo detect train data=data.yaml imgsz=1280 batch=16 epochs=100把--img或imgsz调到 1280,相当于让网络看到原始画面的 1/3,而不是 1/6。代价是显存翻倍,batch 往往要从 32 降到 16 甚至 8。如果 GPU 显存只有 8G,常见做法是先按 640 把模型结构和超参跑通,再开 1280 做正式训练,别一上来就崩显存。
推理阶段同样要保证输入尺寸和训练一致。拿训练好的权重做 TensorRT 部署时,若换成 640 输入而训练用的是 1280,小目标召回会掉得厉害,这不是模型坏了,是分辨率不匹配。部署到 Jetson 这类边缘设备时,工程上更稳妥的方案不是整体提高输入,而是把高分辨率图切块推理,后面第 5 章展开讲。
4.2 锚框参数:自动聚类只对中大型目标友好,小目标要手动干预
YOLOv5 训练时会默认跑一次 k-means 聚类,基于当前数据集的标注框重新计算锚框,这比用 COCO 预设锚框更贴合数据。但对远距离小目标,聚类结果容易被少数大框带偏,9 组锚框里大部分落在中等尺寸区间,小目标分配到的那一两组反而失准。
# 在模型 yaml 里手动约束锚框的尺寸范围 anchors: - [5, 6, 8, 10, 12, 14] # P3 层,小目标 - [18, 22, 30, 36, 48, 60] # P4 层 - [80, 100, 140, 170, 220, 280] # P5 层逻辑说明:YOLO 的 P3 层特征图分辨率最高、步长最小,适合检测小目标,所以第一组锚框要往小里压。上面第一组的 6 个值对应 3 组宽高比,单位是像素——注意这是输入尺寸下的像素,不是原图。若训练尺寸是 1280,锚框值可等比放大;若是 640,锚框值要相应缩小。如果你用的是 YOLOv8,它自带自适应锚框逻辑,一般不用手动改,但建议训练完看一眼anchors日志里聚类出的平均 IoU,低于 0.85 就得手动干预。
4.3 损失函数:CIoU 对小目标不友好,常见替换思路
小目标框的像素误差在总损失里占比很小,CIoU 这类基于 IoU 的损失对绝对像素偏移不敏感:一个 8×8 的框偏了 2 个像素,IoU 掉得不多,但相对误差其实很大。这也是航拍小目标训练收敛慢的根源之一——不是模型笨,是损失函数给不了足够的梯度信号。
常见做法是把回归损失换成 NWD(Normalized Wasserstein Distance)或 Inner-IoU。NWD 的核心是不直接算框的交并比,而是把框建模成二维高斯分布,用 Wasserstein 距离度量分布差异,对微小偏移更敏感。YOLOv8 里改造点集中在回归分支的损失计算:
# 伪代码示意:替换 loss 里的 IoU 计算 # 原:iou = bbox_iou(pred, target, CIoU=True) # 改:nwd = normalized_wasserstein_distance(pred, target) # loss_reg = 1 - nwd改成 NWD 之后小目标召回通常有明显改善,但中大型目标的框精度可能回退,因为 NWD 对大框的约束不如 IoU 严格。稳妥的做法是加权融合:loss = alpha * ciou_loss + (1 - alpha) * nwd_loss,alpha 取 0.5 到 0.7,按验证集表现调。别指望一次到位,这个超参就是拿验证集试出来的,没有捷径。
5. 航拍小目标数据集训练的四个常见坑与排查方法
5.1 高分辨率图直接缩到 640,目标彻底消失
现象:训练 loss 能降,但 mAP@0.5 一直在 0.1 以下徘徊,验证集上的检测框零星几个。
原因:航拍原图 3840×2160,缩到 640 时一个 30×30 的牛羊框只剩 5×5 像素,经过网络下采样后在特征图上不到一个点,模型根本无从学起。
解决:把训练尺寸提到 1280,batch 相应减小。若显存不够,用滑窗推理——把原图切成 4 块 960×1080,分别推理再合并结果。我一般先跑 1280 训练确认模型有效,再在部署侧用切图处理,两头兼顾。
5.2 坐标转换漏了归一化,Loss 直接变 NaN
现象:训练刚开始没几步 loss 变成 nan,或者验证 mAP 恒为 0,控制台报出大量小于 0 或大于 1 的坐标。
原因:VOC 的 bndbox 是绝对像素坐标,YOLO 要求归一化。转换脚本里如果直接用 xmin 当中心坐标,或者忘了除以图片宽高,就会产生越界值。
解决:回到 2.5 节的脚本全量扫描一遍标签,把越界行过滤出来看比例。如果只有少数行越界,手写脚本修正;如果大面积越界,说明转换脚本有系统性 bug,别打补丁,重写转换函数再用 2.4 节的手工验证核对。
5.3 7z 解压后目录嵌套过深,训练读取报错
现象:PyCharm 或服务器终端跑训练时,报FileNotFoundError或OSError: [Errno 22],但文件明明存在。
原因:.7z 压缩包内部往往带多层目录,解压后路径可能长达一百多个字符。Windows 路径默认限制 260 字符,Linux 下部分工具对超长路径也敏感。
解决:解压时直接用短根目录,比如C:\data或/data/pasture,别把文件名带进嵌套层级。遇到已经解压坏的情况,回压缩包重新解压到短路径,不要手动改文件路径——手动移动容易破坏 images 和 labels 的对应关系。
5.4 验证集误用近邻帧,指标虚高
现象:训练时验证集 mAP 高达 0.9,部署到真实航拍视频里掉到 0.5,怀疑是模型过拟合。
原因:航拍视频相邻帧几乎相同,随机划分时近邻帧同时进入训练集和验证集,模型相当于“看到过答案”再考试。这是数据集划分最隐蔽的坑。
解决:按拍摄批次划分,同批次的图全部进训练集或验证集。划分逻辑常见做法是先提取文件名前缀(比如flight01_、flight02_),按前缀分桶,再以桶为单位切分。验证指标虚高比偏低更危险,因为它会误导你提前结束调参。
6. 验证数据集值不值得投入:两小时跑完热力图与快速测试
6.1 画一张目标尺寸分布图,先看清小目标占比
不用急着训练,第一步把标注框的宽高分布画出来,明确这份 1021 张的数据里小目标占比多少。小目标一般指相对原图小于 1% 面积的目标,航拍牛羊数据里这类往往超过一半。
import matplotlib.pyplot as plt from pathlib import Path label_dir = Path("labels") areas = [] for txt in label_dir.rglob("*.txt"): for line in txt.read_text().splitlines(): parts = line.strip().split() if len(parts) == 5: _, _, _, w, h = map(float, parts) areas.append(w * h * 100) # 相对面积百分比 plt.hist(areas, bins=50) plt.xlabel("relative area %") plt.ylabel("sample count") plt.title("target size distribution") plt.savefig("size_dist.png")如果直方图峰值集中在 0.1% 以下,这份数据就是高强度的小目标场景,直接套用常规训练参数大概率翻车;如果峰值在 1% 以上,说明“远距离”主要体现在某一部分图上,训练策略可以中庸一些。
6.2 用最小模型跑一轮 50 epoch 的 smoke test
正式调参前先用 YOLOv5s 或 YOLOv8n 这种最小模型,在默认参数下跑 50 epoch,目的不是拿指标,而是验证数据管线是否通畅、loss 是否下降、标注是否有系统性错误。
yolo detect train data=data.yaml imgsz=1280 model=yolov8n.pt epochs=5050 epoch 在 1021 张图上,单卡 3090 大概半小时到一小时。这轮跑完看两点:loss 下降曲线是否平滑,验证集上能不能看到牛羊的预测框。如果框位置明显偏移,回到标签检查;如果能检出但召回很低,再动 4.2 和 4.3 节的锚框与损失函数。
6.3 用三个指标判断数据集的投入产出
我习惯把结果压成一张对比表,和自建标注的基线做对照。三类别各自看 mAP@0.5 和 mAP@0.5:0.95,再加一个平均框尺寸的召回率。远距离小目标数据集里 mAP@0.5:0.95 低很正常,预测框和真实框只要偏几个像素,IoU 就掉到 0.5 以下,这是小目标检测的通病,不代表数据白做了。
自己建一个高精度小目标标注集,成本按人力和时间算很容易破万;数据集的 1021 张图如果能让基础模型跑到 mAP@0.5 在 0.6 以上,后续用自采数据微调就能用,投入产出比很划算。我自己的习惯是留下解压后的原始目录和这份划分脚本,每换一个框架版本就重跑一遍管线,省得下次还得从头调。希望帮到你。
本文还有配套的精品资源,点击获取