简介面向智慧牧场场景的航拍牛羊检测数据集适用于远距离小目标检测模型的训练与评估目标受众为计算机视觉开发者、农业智能化研究人员及目标检测算法学习者。压缩包共2000个文件以1021个xml标注文件与979个txt标注文件为主配套jpg原图整体大小约97.58MB可同时满足VOC与YOLO两种主流训练格式需求。标注覆盖cattle、cow、sheep三个类别累计14047个矩形框其中sheep类框数高达9518个、cattle类4128个、cow类401个类别分布具有一定区分度可用于航拍视角下的牲畜计数、区域密度分析等任务。数据集按标准目录组织下载后无需格式转换即可接入训练流程省去预处理环节。目前已有379人浏览学习配套博文提供了更详细的标注规则与使用说明适合作为小目标检测算法验证或智慧农业项目落地的参考数据。1. 智慧牧场航拍牛羊检测真正卡脖子的不是模型而是数据集智慧牧场航拍牛羊检测远距离小目标数据集VOCYOLO格式1021张3类别这条路径几乎是无人机畜牧巡检所有方案的技术底座。无人机升到几十米高度后一头成年牛在画面里常常只有二十几个像素检测模型崩掉的不是网络结构而是训练数据本身——尺寸不够、标注不够准、格式不统一模型再改也无济于事。这份数据集的现实价值在于三件事基于真实航拍视角、明确覆盖远距离小目标、同时打包VOC与YOLO两种格式省去从原始影像到可训练样本之间最繁琐的整理工作。它适合做畜牧数量统计、草场载畜量评估、无人机巡牧自动化这几类项目的朋友直接拿来训练和验证。投入这份数据集之前要建立预期:它不是让模型从零学会认牛羊而是给目标检测模型提供高质量的小目标学习样本。1021张不算大,但航拍场景下图片信息密度高配合切片、拼图一类处理策略完全够跑通一个可用的检测管线。下面直接从数据格式、训练流程、小目标优化和常见翻车点讲起。2. 读懂数据集底细VOC和YOLO双格式为什么是标配2.1 先看家底1021张图、3个类别、双份标注怎么组织拿到压缩包解压后典型目录结构会包含原始图片目录和两个标注目录。VOC体系下是JPEGImages、Annotations和ImageSets/Main三个文件夹YOLO体系下是images和labels两个文件夹。VOC的标注是一个个XML文件YOLO的标注是同名TXT文件每行一条检测目标的记录。这种双格式组织方式几乎成了目标检测数据集的行业习惯原因很直接VOC格式利于阅读、编辑和跨工具交换YOLO格式利于直接喂给训练框架。你要是只在YOLO生态里做训练可以只关注labels目录要改标注、过滤类别、合并数据集则优先在VOC上操作更安全。3个类别通常是牛、羊加一个背景或干扰类。具体类别名以labels目录下的classes.txt或数据集说明为准常见做法是“cow”“sheep”外加一类“person”或“horse”因为牧场航拍画面里经常出现骑手和牧羊犬。打开classes.txt就能看到全部类别顺序这个顺序必须和TXT标注里的类别ID一一对上顺序错位是所有下游训练翻车的头号原因。转换脚本和训练配置里的类别列表永远以这份文件为唯一事实来源。判断数据质量的第一步不是看标注框准不准而是统计每张图的目标数量分布。航拍图中远景图可能只有零星几只牛羊近景图可能密密麻麻几十头。这种数量分布极度不均的数据直接训练会出现严重的漏检倾向模型倾向于学习“大多数”却把少量目标直接忽略。先用脚本查一遍分布心里有底后面设置类别损失权重时才有依据。2.2 VOC转YOLO的坐标换算从绝对坐标到归一化坐标YOLO训练要求每行标注为“类别ID x_center y_center width height”的归一化格式四个数值都除以图片宽高取值在0到1之间。VOC的XML里存的是绝对像素坐标(xmin, ymin, xmax, ymax)并且不保证左下右上顺序正确。转换的核心代码如下import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, txt_path, class_names): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) with open(txt_path, w) as f: for obj in root.findall(object): name obj.find(name).text if name not in class_names: continue class_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) # 做一次边界裁剪防止坐标越界导致训练报错 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)) # 计算归一化中心点坐标和宽高 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h f.write(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n) # 调用示例 class_names [cow, sheep, person] voc_to_yolo(Annotations/000001.xml, labels/000001.txt, class_names)这段脚本做了三件事解析XML中的图片尺寸和目标框做像素坐标边界裁剪把绝对坐标换算成归一化中心点坐标与宽高。边界裁剪不是为了好看而是因为部分标注工具在目标贴边时会给出负数或超出图像宽高的坐标不裁剪会导致训练时边界框计算异常、损失值震荡。换算后的数值保留6位小数足够精度不需要再多多了反而让文件体积变大。转换完成后必须抽样验证坐标是否正确。验证最快的方式不是看数字而是把TXT坐标画回图片上肉眼核对目标框是否贴合。你可以在OpenCV里把归一化坐标乘回图宽高再画矩形框随机抽查20张图。这个步骤虽然耗费一点时间却能挡住不止一类让人抓狂的隐蔽问题。2.3 小目标画像用像素面积说话远距离小目标不能只靠“感觉很小”要有量化指标。目标检测研究里常用两个阈值判断小目标绝对小目标定义为小于32×32像素相对小目标定义为面积小于原图的1%。航拍牛羊场景两者都适用。对数据集做一次像素面积统计能直接回答“这1021张图里的小目标到底有多少”import os import numpy as np def analyze_yolo_labels(label_dir, img_size(1280, 720)): # img_size传入时尽量用真实图片分辨率否则统计结果会失真 areas [] counts [] for f in os.listdir(label_dir): if not f.endswith(.txt): continue with open(os.path.join(label_dir, f)) as fp: lines fp.readlines() counts.append(len(lines)) for line in lines: parts line.strip().split() if len(parts) 5: continue w float(parts[3]) * img_size[0] h float(parts[4]) * img_size[1] areas.append(w * h) areas np.array(areas) print(f平均每张图目标数: {np.mean(counts):.1f}) print(f目标面积中位数: {np.median(areas):.0f} px) print(f小于32x32像素的目标占比: {(areas 1024).mean()*100:.1f}%) print(f小于原图1%的目标占比: {(areas (img_size[0]*img_size[1]*0.01)).mean()*100:.1f}%)这个统计脚本的价值在于直接告诉你后续策略往哪个方向走。如果小于32×32像素的目标占比超过六成那直接把整张图resize到640×640再训练大量小目标会被压缩到几个像素几乎不可能学出有效特征。此时你应该优先选择1280分辨率训练或切片训练。如果占比不到三成那按常规流程加一点针对性增强就能出效果不必过度投入复杂度。文件清单、格式转换、目标尺度分布三者都摸清后数据就不再是黑匣子。下一章把这套数据真正跑进YOLO训练流程。3. 把数据跑进YOLO训练自己的牛羊检测模型的完整流程3.1 数据集划分随机打乱但要有固定随机种子训练前第一步做划分目标是把全部图片按8:1:1或7:2:1分成训练集、验证集、测试集。划分最核心的原则是保证同一来源的连续帧不要同时掉进训练集和验证集。航拍视频抽帧得到的图片相邻帧几乎一模一样如果相邻帧被分到两个集合验证集就失去了评估意义——模型等于偷看了答案。这种泄漏会让训练曲线非常好看实际飞无人机时效果一塌糊涂属于典型的数据泄漏翻车。import os import random import shutil random.seed(42) # 固定随机种子保证每次划分结果一致 image_dir images label_dir labels train_ratio, val_ratio 0.8, 0.1 files [f for f in os.listdir(image_dir) if f.endswith(.jpg)] random.shuffle(files) train_n int(len(files) * train_ratio) val_n int(len(files) * val_ratio) splits { train: files[:train_n], val: files[train_n:train_n val_n], test: files[train_n val_n:], } # 创建目录结构并复制文件 for split, names in splits.items(): os.makedirs(fdataset/{split}/images, exist_okTrue) os.makedirs(fdataset/{split}/labels, exist_okTrue) for name in names: shutil.copy(f{image_dir}/{name}, fdataset/{split}/images/{name}) txt_name name.rsplit(., 1)[0] .txt shutil.copy(f{label_dir}/{txt_name}, fdataset/{split}/labels/{txt_name}) print({k: len(v) for k, v in splits.items()})random.seed(42)这一行必须保留否则每次运行脚本的划分结果都不一样后续换参数对比实验时你无法判断精度变化是模型改动带来的还是数据划分变化带来的。这个细节很多人忽略等做到第三轮实验就体会到固定划分有多重要。复制文件而不是移动文件是为了保留原始数据作为备份毕竟标注重来一遍的时间成本太高。3.2 配置YOLO数据文件与类别映射划分完成后需要写一个YAML配置文件YOLO训练脚本靠它找到图片和标签的路径同时也从这里读类别名。路径写绝对路径最省心换机器跑时候再统一改。以YOLOv8为例配置如下# data.yaml path: /home/user/smart_pasture_dataset # 数据集根目录 train: train/images # 训练集图片相对路径 val: val/images # 验证集图片相对路径 test: test/images # 测试集图片相对路径 nc: 3 # 类别数量 names: [cow, sheep, person] # 类别名称必须和classes.txt顺序一致这里最需要盯紧的是names顺序。如果labels目录里某张TXT的第一行是“0 0.5 0.5 0.1 0.1”那这框对应的就是cow而不是sheep。类别顺序错了整个模型的预测结果等于白训。YOLO系列对此极度敏感所以每次换数据集前我都先人工打开一个TXT文件对照classes.txt确认一轮这也算血泪经验换来的习惯。训练前再把txt格式的标注从左到右检查一遍永远是“类别ID 中心x 中心y 宽 高”所有坐标归一化到0-1之间。有些转换脚本会生成类别ID在后的格式训练时不报错但mAP永远上不去属于最难察觉的一类问题。3.3 训练命令与关键参数调整配置就绪后开始训练。YOLOv8训练命令如下yolo detect train \ datasmart_pasture.yaml \ modelyolov8s.pt \ imgsz1280 \ batch16 \ epochs100 \ patience15 \ projectruns/train \ namecow_sheep_v1对航拍小目标数据集imgsz从默认640提升到1280是最关键的一步。因为航拍图中大量牛羊的像素面积只有几十到几百640分辨率下再降采样就彻底丢失细节。代价是显存占用和训练时间明显增加但精度收益通常值得。batch参数受显存限制16G显存跑1280分辨率建议从batch8起步先用小batch确认程序能跑通再加到显存上限。耐心参数patience设为15表示连续15个epoch验证集指标没有改善就提前停止。这个值既不会让训练过度浪费也不至于在损失函数震荡初期就误停。epochs设100是上限配合early stopping基本不会跑满。yolov8s模型在检测速度和精度之间均衡如果目标是极致精度而推理设备性能充足换yolov8m或yolov8l可以再提升几个点但小目标场景下模型规模带来的收益远小于输入分辨率的收益这是很多人存在的认识误区——以为模型更大更强实际上对远距离小目标来说把更多的像素喂进网络比堆参数更有效。训练过程中要看的指标不是loss曲线而是验证集mAP50和mAP50-95。mAP50代表预测框与真值框IoU超过0.5时算作检测成功的平均精度mAP50-95则是在0.5到0.95多个IoU阈值下取平均。小目标数据集上mAP50-95偏低是正常的真值框只有二十几个像素时预测框差三五个像素就会让IoU掉到0.5以下。重点观察mAP50有没有持续上升以及val loss有没有在训练后期掉头向上。训练结束后runs/train/cow_sheep_v1/weights/下会生成best.pt和last.ptbest.pt是验证集指标最优的一版。但best.pt究竟可不可用还要回到真实航拍场景里去验证这个内容放到最后一章细讲。4. 远距离小目标训练优化为什么直接训练会翻车以及怎么调4.1 小目标为什么难学特征在特征图上的占比太小把一张1280×720的航拍图输入YOLO特征提取网络会不断下采样。输入经过8倍下采样得到160×90的特征图经过32倍下采样只有40×23。一个14×14像素的牛在8倍特征图上只剩不到2×2像素在32倍特征图上直接消失。这就是远距离小目标检测的核心矛盾深层特征有语义但分辨率低浅层特征有分辨率但语义弱。YOLOv5和YOLOv8原生输出三层检测头分别对应8倍、16倍、32倍下采样小目标主要依赖8倍那层而16倍和32倍层对小目标几乎没有任何贡献。常见的优化方向有三个提高输入分辨率、增加一个面向2倍或4倍下采样的P2检测头、对原图做切片训练推理。三种方案可以叠加使用但代价差异很大。用表格对比一下方案精度提升潜力训练代价推理代价适用场景提高分辨率到1280中高显存翻倍速度降一半所有小目标场景均适用增加P2检测头中显存增加约30%速度降约20%目标小于16像素时值得切片训练推理高接近翻倍显著增加原图极大、目标极小时必选我一般会先做第一种只提升分辨率不做网络结构改动。原因很简单结构改动会让损失函数和标签分配逻辑变得不确定问题排查时多一个变量。等到分辨率方案已经饱和再考虑P2层或者切片。4.2 切片推理把大图切成小块让目标在局部视图里变大航拍图像的显著特点是大画幅、小目标。与其把整张大图压缩进模型不如把图切成若干小块分别检测再把结果坐标映射回大图。这个思路在遥感目标检测里叫tiling是处理远距离小目标最可靠的手段。配合这份数据集训练出来的模型切片推理往往是精度提升最大的一步。import cv2 import numpy as np from ultralytics import YOLO model YOLO(runs/train/cow_sheep_v1/weights/best.pt) image cv2.imread(aerial_frame.jpg) H, W image.shape[:2] tile_size 640 stride 320 # 切片重叠一半避免目标正好被切缝切开 detections [] for y in range(0, H, stride): for x in range(0, W, stride): # 边界处做填充保证最后一个切片满足输入尺寸 tile np.zeros((tile_size, tile_size, 3), dtypenp.uint8) crop image[y:y tile_size, x:x tile_size] tile[:crop.shape[0], :crop.shape[1]] crop results model(tile, conf0.25, iou0.5) for box in results[0].boxes: cx, cy, w, h box.xywh[0].tolist() x1 x cx - w / 2 y1 y cy - h / 2 x2 x cx w / 2 y2 y cy h / 2 detections.append([x1, y1, x2, y2, box.conf[0].item(), int(box.cls[0].item())]) # 对切片边界的重复检测做NMS合并 from ultralytics.utils.ops import non_max_suppression import torch boxes_tensor torch.tensor([d[:4] for d in detections]) scores_tensor torch.tensor([d[4] for d in detections]) classes_tensor torch.tensor([d[5] for d in detections]) final non_max_suppression( torch.cat([boxes_tensor, scores_tensor.unsqueeze(1), classes_tensor.unsqueeze(1)], dim1).unsqueeze(0), conf_thres0.25, iou_thres0.5 )[0]切片重叠参数stride直接决定检测质量。stride等于tile_size时没有重叠速度最快但目标被切缝一分为二时两个碎片的置信度都会剧烈下降。stride设成tile_size的一半每个目标最多被两个切片重复检测漏检率显著下降。处理超大航拍图时padding区域用全零填充即可但要注意填充边不参与最终结果映射。推理速度跟切片数量线性相关一张8192宽的大图切成32个640小块单张推理耗时约是整图推理的十几倍这是精度换速度的典型场景。4.3 数据增强的取舍有些增强在航拍场景下是毒药YOLO默认开启Mosaic增强把四张图拼成一张训练这个增强对小目标有帮助——拼接后目标相对尺寸缩小模拟了更远距离的视角。但航拍场景中要关掉或调低垂直翻转增强。牛羊在航拍图中通常是“头朝某个方向”的形态垂直翻转会让牛看起来像头朝下与真实分布严重不符。水平翻转也有同样问题但影响小一些因为航拍视角下左右翻转仍然接近真实观察角度。YOLOv8中可以在augment参数里控制翻转概率yolo detect train \ datasmart_pasture.yaml \ modelyolov8s.pt \ imgsz1280 \ flipud0.0 \ fliplr0.5 \ mosaic0.8 \ hsv_h0.015 \ hsv_s0.3 \ hsv_v0.2颜色增强参数也需要克制。牧场航拍图受光照和季节影响大适度调色可以增强泛化能力但把色彩扰动调太大会让模型把颜色当成识别特征去学反而在黄昏、逆光时漏检。hsv_h控制在0.01到0.02之间hsv_s在0.2到0.4之间hsv_v同理即可。别小看这些参数它们和损失函数的交互是黑盒子没有普适最优值只能基于自己的验证集反复试。5. 排查标注与训练翻车案例的五个高频坑5.1 训练完mAP非常高实际飞无人机漏检一片现象训练集和验证集的mAP50都到了0.9以上导出模型上无人机一拍近距离有效但稍远的牛羊大量漏检。原因数据的训练集和验证集划分存在泄漏。航拍视频抽帧的图片之间有高度相似性相邻帧被分到了不同集合模型相当于变相见过验证集的图片。更隐蔽的情况是复用了同一数据集的不同格式版本比如VOC目录和YOLO目录各划分了一次训练集来自VOC、验证集却来自YOLO两边文件顺序不一致导致数据泄露。解决回到原始图片做基于时间间隔的抽帧划分保证训练集、验证集、测试集来自不同的飞行架次或时间段。至少做到同一视频序列的帧只进一个集合。5.2 训练一开始loss正常几十个epoch后突然变成NaN现象训练进行到20到30轮时loss变成NaN训练直接中断日志里能看到loss值异常波动。原因绝大多数情况是标注文件中出现了极小的目标框比如宽或高小于2个像素归一化后数值极小在损失函数计算中出现梯度爆炸。这批数据里如果包含只有几个像素的牛羊转换脚本没有过滤小目标框就会触发。解决在转换脚本里加一条过滤逻辑把面积小于4平方像素或宽高任一边小于2像素的框丢弃。这类目标在训练时提供不了有效梯度丢弃后对最终泛化能力几乎没有影响。再处理训练数据前先跑一遍数据审查统计最小目标框尺寸。5.3 验证集mAP50-95很低但mAP50很高现象mAP50有0.85mAP50-95只有0.3换任何模型都改善不大。原因标注框大概率偏松或偏紧存在系统性的边界偏移。对于只有20像素大小的目标标注框哪怕只偏3个像素IoU就从0.7掉到0.4。mAP50和mAP50-95的巨大差距是数据标注质量的直接反馈。解决用标注可视化工具对全部标注做一轮人工复核重点检查远距离小目标的框是不是正好贴住牛羊轮廓。清洗掉偏移严重的标注比换更大的模型有效得多。标注质量在这个场景里是整个项目投入产出比最高的优化点。5.4 训练时报错“assertion failed: label shape”现象训练启动时加载数据就报错提示label shape不匹配一查是某个TXT文件里的某一行的坐标值超过1或是负数。原因VOC转YOLO时没有对坐标做边界裁剪某些目标的xmax超出图像宽度归一化后宽度的比例大于1导致检测框坐标非法。解决检查所有TXT中数值超过1或小于0的行定位到对应XML文件修正标注或删除该目标。之前给的转换脚本里max和min裁剪逻辑就是为了防这个问题很多公开转换工具并不处理这种情况拿到数据后自己跑一遍边界检查脚本非常值得。5.5 导出模型推理结果大量重叠框现象单只牛被框了三四次不同大小框重叠在一起且置信度都不低。原因训练时没有正确设置NMS参数或是切片推理时切缝边的目标在多个tile里被检测到拼接后没有做全局NMS。叠框问题在密集牛羊群中尤其明显因为目标之间本来就近。解决推理脚本里保持conf0.25、iou0.5的常用配置切片推理后务必对全部检测框做一次非极大值抑制。NMS的正确使用是这类远距离小目标项目里最容易被忽视的细节它直接影响最终的可视化效果和计数统计。6. 提交评测结果而不是模型小目标场景的有效验证手段模型训练完成后最忌只看训练日志说“效果不错”。远距离小目标的验证方式跟常规场景有微妙差异要在正式交付前用对方法。先看置信度阈值。默认conf0.25在常规场景够用但航拍小目标模型的输出分数整体偏低远距离目标的置信度常落在0.2到0.4区间。如果你按默认阈值过滤会漏掉大量真实目标。我一般先在验证集或测试集上跑一遍画出置信度-召回率曲线找出曲线从平缓转陡峭的位置作为实际阈值。检漏优先场景把阈值压到0.15误检敏感场景提到0.4这个参数每次换数据都得重新调因为它高度依赖数据分布。再看指标侧重。小目标场景下mAP50-95只能作为参考主判据应放在mAP50和针对小目标单测的召回率上。如果后续业务对位置精度要求高比如要做牛羊个体识别再用分割标注做位置质量评估如果只是做数量统计与热区分布mAP50达标即可不必在mAP50-95上死磕。最后做一次端到端验证将测试图片和视频帧按切片推理跑一遍把检测结果叠加画在原图上人眼检查远中近三个距离段的目标检出情况。模型按这个流程跑出的可视化结果远比mAP指标更贴合真实业务判断。我的做法是把评估脚本和推理脚本固定成可复用的工具每次迭代数据或参数后统一跑一轮输出同一格式的报告方便横向比较。这样做的目的是建立一条可以重复执行的评测链路避免靠印象判断模型改好还是改坏。切片的步长、置信度阈值和NMS这些参数建议用一组小规模的测试集先做网格搜索找到合适区间后再全量验证。远距离小目标检测本身就有玄学成分不固定评估流程很难判断精度变化究竟来自数据改动还是随机波动。我自己吃过不少亏最深的一次体会是把大量时间花在调模型结构上最后发现标注偏差才是mAP上不去的元凶。希望帮到你少走这段弯路。本文还有配套的精品资源点击获取
