简介:面向智慧牧场航拍场景的牛羊检测任务,该数据集提供了一千零二十一张航拍图像及对应的Pascal VOC与YOLO双格式标注文件,覆盖牛、母牛、羊三个类别,合计一万四千零四十七个标注框。得益于完整的标签数据,开发者可以省去从原始视频中筛选并裁剪目标的工作,直接用于远距离小目标的模型训练与精度评估。压缩包内文件总数为两千个,以一千零二十一个XML标注文件和九百七十九个TXT标签文件为主要组成,整体体积约为九十七点五八兆字节,解压后即可按需划分为训练集与验证集,使用起来非常便捷。目前已有三百七十九人学习下载。对于智慧农业、无人机视觉或目标检测方向的研究者,这一数据集既适合用于YOLO系列、Faster R-CNN等主流检测器的效果对比,也可支撑牧场牛羊的个体识别、数量统计与密度估计等实际应用,是验证算法在小目标场景下鲁棒性的实用资源。
1. 智慧牧场航拍牛羊检测:这份远距离小目标数据集能直接拿去训练吗
无人机航拍数牛羊,难点从来不是"认得出牛羊",而是几百米高空看下去,一群牛只占几十个像素,人眼都要眯半天,模型更是容易漏检。这份智慧牧场航拍牛羊检测数据集就是针对这个场景来的:1021张jpg图片,cattle、cow、sheep三个类别总共14047个标注框,同时输出Pascal VOC和YOLO两种格式,既能拖进labelImg复核标注,也能直接喂给YOLOv5/YOLOv8训练。做智慧牧场、无人机巡检或任何航拍小目标检测项目的人,值得先对照这份数据的标注分布看看自己的数据差在哪。它不保证训练精度,但那14047个框是准确且合理的,这是训练能起步的前提。
2. 数据集结构拆解:VOC与YOLO双格式、三类别的标注分布
2.1 文件构成与解压后的目录组织
拿到手的压缩包是7z格式,Linux下用p7zip解压,Windows下用7-Zip就行。解压之后是一个平铺目录,jpg图片、xml文件、txt文件各1021个,文件名一一对应。xml是Pascal VOC格式标注,txt是YOLO格式标注,两份标注描述的是同一批框。解压命令里把压缩包名换成你实际下载的那个文件:
7z x pasture_animal.7z -o./firc_animal7z x解压并保留压缩包内目录结构,-o后面接输出目录,注意-o和目录名之间没有空格。解压完成后先确认三类文件数量都是1021:
ls firc_animal/*.jpg | wc -l ls firc_animal/*.xml | wc -l ls firc_animal/*.txt | wc -l三行命令分别统计三类文件数量,如果哪个数量对不上,大概率是压缩包损坏或解压中断,需要回退到完整性校验那一步。从文件名看,编号并不连续,firc_animal_467、479、481、482、484、489、490、491、503、504……中间跳过不少序号,说明数据是从更大规模航拍素材里筛出来的,原作者去掉了模糊、过暗、目标过小或完全无目标的帧。这种筛选习惯对训练是正向的,模型不用在废图上浪费学习能力,验证集也比较干净。
每张图片平均有13.7个目标框(14047/1021),这个密度在航拍场景里属于中等偏上。一群羊围在一起时,框与框之间大量重叠、紧挨,标注上没问题,但训练时会给NMS带来压力,后面第5章的增强设置会专门处理。
2.2 类别分布与不均衡问题
三个类别的框数差异相当大,直接看表:
| 类别 | 框数 | 占比 | 平均每张图框数 |
|---|---|---|---|
| cattle | 4128 | 29.4% | 4.0 |
| cow | 401 | 2.9% | 0.4 |
| sheep | 9518 | 67.7% | 9.3 |
sheep接近七成,cattle接近三成,cow只有401框,占比不到3%。这个分布带来两个直接问题:第一,模型对sheep的拟合会很好,对cow的召回可能很低,而且很难靠单纯加epoch解决,因为梯度更新被多数类主导;第二,cow和cattle语义上都是牛,cattle是牛的总称,cow特指母牛,标注者把一部分牛标成cattle、一部分标成cow,实际使用前必须先想清楚业务到底需要几个类别。如果系统只需要"牛/羊"二分,把cattle和cow合并是最理性的做法,不用让模型浪费参数去学一个航拍视角下本身就难分清的语义差异。
xml里记录的是标准VOC信息,每个object节点包含name、pose、truncated、difficult和bndbox坐标。对这份数据来说name只有三种,bndbox给出xmin、ymin、xmax、ymax四个像素坐标;txt每行是"类别id 中心x 中心y 宽度 高度",归一化到0到1。两套坐标描述同一个框,训练框架一般只需其中一种,同时保留两份的价值在于方便交叉验证和切换工具链。
2.3 远距离小目标成像对标注框尺寸的影响
航拍场景下目标像素小是常态,这也反映在标注框上。把全部xml里的框宽高归一化统计一遍,就能量化小目标占比:
import xml.etree.ElementTree as ET import os xml_dir = "firc_animal" widths, heights = [], [] for name in os.listdir(xml_dir): if not name.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, name)) root = tree.getroot() size = root.find("size") w_img = int(size.find("width").text) h_img = int(size.find("height").text) for obj in root.findall("object"): box = obj.find("bndbox") xmin = int(box.find("xmin").text) xmax = int(box.find("xmax").text) ymin = int(box.find("ymin").text) ymax = int(box.find("ymax").text) widths.append((xmax - xmin) / w_img) heights.append((ymax - ymin) / h_img) small = sum(1 for w, h in zip(widths, heights) if w < 0.05 and h < 0.05) print(f"框总数: {len(widths)}, 相对尺寸小于5%的框: {small}")这段代码遍历所有xml,把框宽高归一化到图片尺寸。对一张1920x1080的航拍图来说,5%意味着宽度不到96像素、高度不到54像素,实际很多目标比这个还小。这个占比直接决定训练时imgsz怎么选,小目标比例越高,imgsz越要往1280以上提。
3. 目录组织与格式衔接:从7z压缩包到可训练数据集的落地路径
3.1 重建标准VOC目录结构
解压后的平铺目录虽然能直接用,但不同训练框架对数据集目录有自己的约定,YOLO系一般要求images和labels分开,MMDetection习惯读VOC结构。我拿到任何VOC+YOLO混标数据集后的第一步,都是先重建标准VOC目录,给后续操作留余地:
mkdir -p VOCdevkit/VOC2024/{JPEGImages,Annotations,ImageSets/Main} mv firc_animal/*.jpg VOCdevkit/VOC2024/JPEGImages/ mv firc_animal/*.xml VOCdevkit/VOC2024/Annotations/JPEGImages放原图,Annotations放xml,ImageSets/Main放train.txt和val.txt这些划分列表,VOC工具链和大量开源脚本默认认这三层路径。txt文件先不动,YOLO训练时通常有自己独立的labels目录,等目录结构稳定后再统一按类别放好。
3.2 VOC坐标与YOLO坐标的换算验证
VOC坐标是像素级绝对坐标,YOLO坐标是归一化相对坐标,两者定义不同但描述同一个矩形。这份数据的txt已经生成好,理论上不需要自己转换,但直接信任不是好习惯。抽查几个文件,把txt反算回像素坐标再和xml里的bndbox比对:
import xml.etree.ElementTree as ET xml_file = "VOCdevkit/VOC2024/Annotations/firc_animal_491.xml" txt_file = "firc_animal/firc_animal_491.txt" tree = ET.parse(xml_file) root = tree.getroot() size = root.find("size") w_img = int(size.find("width").text) h_img = int(size.find("height").text) xml_boxes = [] for obj in root.findall("object"): box = obj.find("bndbox") xml_boxes.append(( int(box.find("xmin").text), int(box.find("ymin").text), int(box.find("xmax").text), int(box.find("ymax").text), )) with open(txt_file) as f: lines = [line.strip().split() for line in f if line.strip()] print(f"图片尺寸: {w_img}x{h_img}, xml目标数: {len(xml_boxes)}, txt目标数: {len(lines)}") for line in lines: cls = int(line[0]) cx, cy, bw, bh = map(float, line[1:]) xmin = int((cx - bw / 2) * w_img) xmax = int((cx + bw / 2) * w_img) ymin = int((cy - bh / 2) * h_img) ymax = int((cy + bh / 2) * h_img) print(f"类别{cls}: 反算像素框({xmin},{ymin},{xmax},{ymax})") print("xml第一个框:", xml_boxes[0] if xml_boxes else "无")脚本把txt的归一化中心点和宽高反算成像素坐标,输出后和xml的第一个框对比,数值差1到2个像素属于正常,因为归一化小数保留位数会带来取整误差。如果差得多,说明txt和xml不是同一批标注,或者类别顺序被改过,必须停下排查。随机挑三到五张做抽检就够了,目的是确认双格式一致性,不是重新校验每一行。
3.3 划分训练集与验证集:注意分层抽样
训练需要train和val目录,但这份数据集没有预划分。按常见做法8:2切分,但我建议用分层抽样而不是纯随机,因为cow只有401框、分布在少量图片上,随机切分极易把带cow的图片全分到某一侧:
import os import random from collections import defaultdict random.seed(42) xml_dir = "VOCdevkit/VOC2024/Annotations" class_to_files = defaultdict(list) for name in os.listdir(xml_dir): if not name.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, name)) root = tree.getroot() has_cow = any(obj.find("name").text == "cow" for obj in root.findall("object")) key = "cow" if has_cow else "no_cow" class_to_files[key].append(name[:-4]) train_files, val_files = [], [] for key in ["cow", "no_cow"]: files = class_to_files[key] random.shuffle(files) split = int(len(files) * 0.8) train_files.extend(files[:split]) val_files.extend(files[split:]) random.shuffle(train_files) random.shuffle(val_files) with open("VOCdevkit/VOC2024/ImageSets/Main/train.txt", "w") as f: f.write("\n".join(train_files)) with open("VOCdevkit/VOC2024/ImageSets/Main/val.txt", "w") as f: f.write("\n".join(val_files)) print(f"train: {len(train_files)}, val: {len(val_files)}")先按"这张图里有没有cow"分组,组内再按8:2切,保证验证集里也有cow样本。seed固定在42,让每次复现实验用同一个划分,否则换模型调参时训练集都不一样,对比结果没有说服力。统计cow的代价是一次全量xml遍历,几秒钟的事,别嫌麻烦。
4. 常见问题避坑:解压异常、标签映射、类别漏标的排查手册
4.1 7z解压报错或文件数量对不上
现象:解压到一半提示CRC校验失败、文件末端错误,或者解压完统计少了十几个文件。
原因:网络传输导致压缩包损坏,下载工具多线程分段下载时某段写坏是重灾区。
解决:先别反复试解压,用测试模式做完整性校验:
7z t pasture_animal.7zt是test模式,只校验不释放,几秒就能出结果。输出里有error就直接重新下载原文件,用单线程或带断点续传的下载方式。损坏的7z在部分解压软件里能"强行解压"出部分文件,但缺失和损坏的部分不可预知,这种半拉子数据喂给训练就是定时炸弹。第一次用损坏包训练时,loss曲线很正常但验证集mAP稀烂,查了一下午才发现是图片和标签错位。
4.2 txt里的类别id和训练yaml的names顺序对不上
现象:训练能跑,但验证结果里cattle的框对应到羊的类别名,或者loss一直不降。
原因:txt第一列是类别id,id的含义由训练yaml的names顺序决定。如果txt是用某个脚本重建的,脚本里的classes列表顺序和yaml不一致,id就全乱了。
解决:训练前固定一份权威classes列表,比如["cattle", "cow", "sheep"],所有txt生成、yaml编写都从它派生。启动训练前随机看几个txt的第一列:
head -5 firc_animal/firc_animal_491.txt看到大量2开头,yaml里2: sheep,那说明对应关系正确。一旦中途改过类别顺序,所有txt必须同步重生成,不能只改一半。
4.3 某个txt文件行数比xml里的object少
现象:xml里明明有4个object,对应txt只有2行,或者labelImg打开显示正常但训练时这个样本的损失异常。
原因:labelImg在VOC和YOLO模式之间切换时最后保存动作没完成,YOLO格式txt可能是旧版或空文件,标注者中途切换保存格式也会导致部分标签丢失。
解决:以xml为准重建txt。下面的转换脚本全量重建,输出到独立labels目录,避免覆盖原始文件:
import xml.etree.ElementTree as ET import os os.makedirs("labels", exist_ok=True) def voc_to_yolo(xml_path, txt_path, classes): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") w_img = int(size.find("width").text) h_img = int(size.find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in classes: continue cls_id = classes.index(name) box = obj.find("bndbox") xmin = int(box.find("xmin").text) ymin = int(box.find("ymin").text) xmax = int(box.find("xmax").text) ymax = int(box.find("ymax").text) x_center = ((xmin + xmax) / 2) / w_img y_center = ((ymin + ymax) / 2) / h_img w = (xmax - xmin) / w_img h = (ymax - ymin) / h_img lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(txt_path, "w") as f: f.write("\n".join(lines)) classes = ["cattle", "cow", "sheep"] xml_dir = "VOCdevkit/VOC2024/Annotations" for name in os.listdir(xml_dir): if not name.endswith(".xml"): continue base = name[:-4] voc_to_yolo(os.path.join(xml_dir, name), f"labels/{base}.txt", classes)重建后再做一次全量比对,把xml的目标数和txt行数对不齐的文件揪出来:
for base in [f[:-4] for f in os.listdir(xml_dir) if f.endswith(".xml")]: tree = ET.parse(os.path.join(xml_dir, base + ".xml")) n_obj = len(tree.getroot().findall("object")) txt_path = f"labels/{base}.txt" if not os.path.exists(txt_path): print(f"{base}: 无txt") continue with open(txt_path) as f: n_txt = sum(1 for line in f if line.strip()) if n_obj != n_txt: print(f"{base}: xml={n_obj}, txt={n_txt}")输出里任何xml不等于txt的文件都要单独处理。注意过滤空白行,否则统计会误报。
4.4 图片与标注文件对应不全
现象:训练时提示找不到图片或没有标签,DataLoader直接跳过样本或报错。
原因:从大目录筛选素材时删了jpg没删xml,或者拷贝过程中某个扩展名文件没拷全。
解决:用集合差快速定位:
import os jpg_set = {f[:-4] for f in os.listdir("VOCdevkit/VOC2024/JPEGImages") if f.endswith(".jpg")} xml_set = {f[:-4] for f in os.listdir("VOCdevkit/VOC2024/Annotations") if f.endswith(".xml")} print("有图无标注:", len(jpg_set - xml_set), list(jpg_set - xml_set)[:5]) print("有标注无图:", len(xml_set - jpg_set), list(xml_set - jpg_set)[:5])有图无标注的图片建议直接从训练列表剔除,不要自己补标,补的框画不准反而成为干扰样本。有标注无图的xml直接归档到backup目录,留着审计用。
5. 用YOLOv8训练这份数据集:参数设置与小目标检测调优
5.1 数据yaml与目录准备
训练前准备数据yaml,指定数据根路径、图像目录、标签目录和类别名:
path: /data/pasture/firc_animal train: images/train val: images/val names: 0: cattle 1: cow 2: sheeppath用绝对路径最稳妥,相对路径在不同机器上解析结果不一样,换机器训练时排查成本高。names顺序必须和txt第一列对应,0是cattle、1是cow、2是sheep,这就是第4章反复强调的对应关系。目录方面,需要把图片按train/val分放,标签也按相同命名空间组织:
mkdir -p images/train images/val labels/train labels/val对照第3章生成的train.txt和val.txt,把jpg和txt分别拷入对应目录,不要手拷,写个三四行的脚本或者直接用rsync按列表同步,避免漏文件。
5.2 训练命令、输入尺寸与损失函数相关参数
基础训练命令:
yolo detect train data=sheep.yaml model=yolov8n.pt epochs=100 imgsz=1280 batch=16 device=0 patience=20关键参数逐个说:model=yolov8n.pt用nano预训练权重起步,显存小优先跑通流程;imgsz=1280是这份数据集最重要的一个参数,航拍小目标在640输入下只有十几个像素,特征提取很难到位,提到1280相当于目标面积放大4倍;batch=16按显存调,OOM就降到8再不行降到4,我一般先试16,能跑就用;patience=20是早停,20个epoch验证指标不涨就停,省时间。
损失函数方面,YOLOv8的分类损失用BCE,回归部分用CIoU加DFL。小目标对回归误差更敏感,同样偏移几个像素,小框的IoU下降远大于大框。box参数控制回归损失权重,默认就是7.5,改它不是首选手段。更有效的是把close_mosaic调大,让训练后期更多epoch用完整图片:
yolo detect train data=sheep.yaml model=yolov8n.pt epochs=100 imgsz=1280 batch=16 close_mosaic=15close_mosaic表示最后15个epoch关闭mosaic增强。航拍小目标对mosaic不友好,一只100x100的羊被四张图切开拼接,模型学的是碎片特征,收官阶段用完整的自然图像精调对小目标收敛有帮助。同理,航拍牛羊不存在"翻转后语义改变"的问题,可以显式关闭随机翻转减少增强噪声:
yolo detect train data=sheep.yaml model=yolov8n.pt epochs=100 imgsz=1280 batch=16 close_mosaic=15 fliplr=0fliplr=0关闭水平翻转。最后是多尺度训练,无人机高度变化会直接改变目标像素尺寸,固定输入会让模型对高度敏感。官方默认的scale=0.5就是为这个场景准备的,显式写出来方便调:
yolo detect train data=sheep.yaml model=yolov8n.pt epochs=100 imgsz=1280 batch=16 close_mosaic=15 fliplr=0 scale=0.5scale=0.5表示图像在0.5到1.5倍之间随机缩放,等效模拟不同飞行高度。
5.3 类别不均衡的解决思路
cow只有401框,直接硬训的预期结果是sheep精度很高、cow几乎检不出。给三条路径,按业务需求选。
第一,业务只需区分牛羊,把cattle和cow合并。合并操作就是改txt第一列,cow是1改成0:
sed -i 's/^1 /0 /' labels/train/*.txt labels/val/*.txts/^1 /0 /匹配行首的"1 "替换成"0 ",cow并入cattle,同时yaml里names改成两个类别。这最省事,也最符合"数牛羊"的实际业务。
提示:改txt前先备份,
sed -i是原地修改,没有后悔药。
第二,必须区分cow,做数据集层面的过采样,把含cow的图片在训练列表里重复3到5次,让模型更多次看到cow样本。缺点是有可能过拟合那401框,需要配合早停观察。
第三,用few-shot思路单独微调。冻结backbone只训检测头,让cow在这一轮里不被sheep的梯度淹没。这条路线适合后续补数据的场景,先把流程搭好,新数据到位后直接增量训练。
6. 验证结果的小技巧:用置信度阈值和混淆矩阵看模型真实水平
6.1 低置信度先测召回上限
训练完先别急着看默认mAP,用低置信度阈值把验证集刷一遍:
yolo detect val model=runs/detect/train/weights/best.pt data=sheep.yaml conf=0.05 iou=0.5conf=0.05远低于部署阈值,正常推理用0.25或0.3。这一步的目的不是评估部署指标,而是回答一个问题:模型到底学到目标特征没有。0.05下召回也上不去,说明模型没学会小目标特征,调阈值没用,要回头改特征提取链路;0.05召回不错但0.3掉一半,说明模型对目标有感知但置信度不足,可以从增强、imgsz、损失权重着手。很多人只在默认0.25下看一眼mAP就下结论,信息量差太远。
6.2 看混淆矩阵再决定要不要合并类别
yolo detect val输出里自带confusion_matrix.png,重点看cattle和cow那两行。如果cow大量被分到cattle,说明两个类别在航拍视角下视觉特征高度重叠,标注本身的语义区分度就不够——肉牛和母牛从几百米往下看,标注者自己都要辨认半天。这种情况不要纠结模型能力,合并类别是损失最小的决策。如果sheep被分到cattle,那才是真问题,说明牛和羊的基础特征都没学到,需要回去查训练数据或增强设置。
6.3 用imgsz-recall曲线选部署参数
部署时imgsz不是越大越好,显存和延迟都是成本。把640、960、1280、1536四档分别跑验证,记录小目标类别召回率,画一条曲线贴在项目文档里。通常640到1280是明显上升段,1280之后收益陡峭下降,这个拐点就是性价比最优的部署输入尺寸,直接写进推理配置。
从那以后我每拿到一份新数据集,都强制先走一遍"目录抽检、双格式一致性校验、imgsz曲线"再开训练,这套流程帮我拦下了不少看起来能跑、实际一堆隐患的数据。希望这份航拍牛羊数据集能帮你省下这些排查时间,把精力放在真正影响精度的环节上。
本文还有配套的精品资源,点击获取