ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

智慧牧场航拍牛羊检测:远距离小目标数据集处理与YOLO训练实践

智慧牧场航拍牛羊检测:远距离小目标数据集处理与YOLO训练实践 简介智慧牧场航拍牛羊检测数据集聚焦无人机远距离拍摄中的小目标识别面向智慧农业、畜牧信息化及目标检测算法研究者。内含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 智慧牧场航拍牛羊检测远距离小目标数据集VOCYOLO格式1021张3类别.7z # 确认无报错后解压-o 指定目标目录避免把文件名散落在当前目录 7z x 智慧牧场航拍牛羊检测远距离小目标数据集VOCYOLO格式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 filenameIMG_0001.jpg/filename size width3840/width height2160/height /size object namecattle/name bndbox xmin120/xmin ymin85/ymin xmax152/xmax ymax118/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第一个数字是类别 ID3 个类别对应 0、1、2。后面的四个小数因为除以了图片宽高范围应在 0 到 1 之间。把上面第一行还原对应 3840×2160 的图目标中心在 x136、y98宽约 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(encodingutf-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.yaml3.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(parentsTrue, exist_okTrue) target_lab.mkdir(parentsTrue, exist_okTrue) 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(ftrain: {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 训练与推理尺寸为什么小目标数据优先 imgsz1280航拍图分辨率通常很高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 datadata.yaml imgsz1280 batch16 epochs100把--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 掉得不多但相对误差其实很大。这也是航拍小目标训练收敛慢的根源之一——不是模型笨是损失函数给不了足够的梯度信号。常见做法是把回归损失换成 NWDNormalized Wasserstein Distance或 Inner-IoU。NWD 的核心是不直接算框的交并比而是把框建模成二维高斯分布用 Wasserstein 距离度量分布差异对微小偏移更敏感。YOLOv8 里改造点集中在回归分支的损失计算# 伪代码示意替换 loss 里的 IoU 计算 # 原iou bbox_iou(pred, target, CIoUTrue) # 改nwd normalized_wasserstein_distance(pred, target) # loss_reg 1 - nwd改成 NWD 之后小目标召回通常有明显改善但中大型目标的框精度可能回退因为 NWD 对大框的约束不如 IoU 严格。稳妥的做法是加权融合loss alpha * ciou_loss (1 - alpha) * nwd_lossalpha 取 0.5 到 0.7按验证集表现调。别指望一次到位这个超参就是拿验证集试出来的没有捷径。5. 航拍小目标数据集训练的四个常见坑与排查方法5.1 高分辨率图直接缩到 640目标彻底消失现象训练 loss 能降但 mAP0.5 一直在 0.1 以下徘徊验证集上的检测框零星几个。原因航拍原图 3840×2160缩到 640 时一个 30×30 的牛羊框只剩 5×5 像素经过网络下采样后在特征图上不到一个点模型根本无从学起。解决把训练尺寸提到 1280batch 相应减小。若显存不够用滑窗推理——把原图切成 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, bins50) 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 datadata.yaml imgsz1280 modelyolov8n.pt epochs5050 epoch 在 1021 张图上单卡 3090 大概半小时到一小时。这轮跑完看两点loss 下降曲线是否平滑验证集上能不能看到牛羊的预测框。如果框位置明显偏移回到标签检查如果能检出但召回很低再动 4.2 和 4.3 节的锚框与损失函数。6.3 用三个指标判断数据集的投入产出我习惯把结果压成一张对比表和自建标注的基线做对照。三类别各自看 mAP0.5 和 mAP0.5:0.95再加一个平均框尺寸的召回率。远距离小目标数据集里 mAP0.5:0.95 低很正常预测框和真实框只要偏几个像素IoU 就掉到 0.5 以下这是小目标检测的通病不代表数据白做了。自己建一个高精度小目标标注集成本按人力和时间算很容易破万数据集的 1021 张图如果能让基础模型跑到 mAP0.5 在 0.6 以上后续用自采数据微调就能用投入产出比很划算。我自己的习惯是留下解压后的原始目录和这份划分脚本每换一个框架版本就重跑一遍管线省得下次还得从头调。希望帮到你。本文还有配套的精品资源点击获取
返回列表