从YOLOV5目录格式到猪圈目标检测:数据准备实战指南
简介猪圈摄像头场景下的生猪检测数据集类别为单一pig按YOLOV5标准目录格式整理可直接接入现有训练流程减少数据预处理工作。图像均截取自猪圈监控视频分辨率覆盖640至1080每帧包含多个生猪目标对密集遮挡场景有一定覆盖适合目标检测模型训练与算法验证。数据集已划分训练集与验证集训练集含929张图片及对应txt标注验证集含136张图片及对应txt标注每张图片均配有对应的txt标签文件另附可视化Python脚本可随机读取图片并绘制边界框无需修改即可运行。资源共2000个文件以jpg图片和txt标签为主附带py工具脚本压缩包约97.43MB。目前已有600余人学习适用于农业智能化领域的生猪检测研究、目标检测课程实践及算法性能对比。1. 猪圈摄像头下的生猪检测数据准备比模型选择更值钱猪圈摄像头装完那一刻最不缺的就是画面几路监控全天都在录真正缺的是能把画面变成这栏有几头猪的模型。目标检测里YOLOV5目录格式几乎成了数据集的默认交换格式图像和同名标注并列存放、一个 yaml 描述类别结构简单到不需要解释就能读。但这个数据集特殊在源自猪圈摄像头俯视视角、漏粪板反光、猪挤成一团、耳朵互相遮挡哪怕只有一个类别标注边界也得认真定。YOLOV5目录格式决定了这份数据能被训练脚本直接读取不用改任何数据集加载代码。适合做智慧养殖的工程师、给猪场做视觉方案的乙方、以及第一次想用 yolov5 训练自己的数据集的学生。它的价值不在算法多新而在目录规范、标注策略和坑的前置规避。2. 拆开YOLOV5目录格式这套结构凭什么成为默认标准YOLOv5 官方仓库和绝大多数第三方代码默认都按同一个目录契约取数据。新手拿到一个数据集先别急着训练第一步永远是检查目录长什么样。常见做法是项目根目录建一个 dataset 目录内部按 train/val 分开看起来像这样pig-dataset/ ├── images/ │ ├── train/ │ │ ├── pig_20250101_100001.jpg │ │ └── ... │ └── val/ │ ├── pig_20250101_120001.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── pig_20250101_100001.txt │ │ └── ... │ └── val/ │ └── ... ├── train.txt ├── val.txt └── data.yaml这段骨架就是整套格式的物理载体。images 里放原图labels 里放与图片同名的纯文本标注文件train.txt 和 val.txt 只是图片路径列表data.yaml 描述数据集根目录和类别。我一般会再保留一份 test 目录用来做最终验收但基础训练只需要 train 和 val。2.1 images 与 labels 的同名对应关系为什么坐标必须归一化labels 目录里每个 txt 文件必须与 images 里的 jpg 完全同名后缀不同而已。txt 是一个纯文本文件每一行代表一个真实标注框格式是五个字段类别 ID、中心点 x、中心点 y、框宽、框高。0 0.624218 0.489181 0.338281 0.471265 0 0.187500 0.710297 0.126562 0.273973第一列是类别 ID因为本数据集只有猪一个类别所以全部是 0。后四列是归一化坐标范围在 0 到 1 之间。重点是归一化YOLOv5 训练时会自动把任意尺寸的图缩放成 640×640 或你指定的分辨率如果 txt 里存的是原始像素坐标一次 resize 之后所有框就全错位了。归一化之后坐标是相对值缩放不影响。我自己在检查数据时会算一笔账一张 1920×1080 的图某个框左上角在 (480, 270)右下角在 (720, 540)那它的归一化中心点就是((480720)/(2×1920), (270540)/(2×1080))宽高分别是 240/1920 和 270/1080。标注工具会自动算但手写脚本校验时脑子里得有这层换算关系。2.2 用 bash 搭出标准骨架再把图片对号入座很多人在这一步偷懒结果训到一半才报错找不到 labels。先花十秒钟把空目录结构搭好后面每一步都有地方放东西。# 在工程根目录下执行一次性建好所需目录 mkdir -p pig-dataset/images/train pig-dataset/images/val mkdir -p pig-dataset/images/test pig-dataset/labels/train mkdir -p pig-dataset/labels/val pig-dataset/labels/test # 确认结构 tree pig-dataset -L 2mkdir -p 的作用是递归创建目录即使父目录不存在也会先建好父目录。最后用 tree 列出两层结构目测一下 images 和 labels 下三级目录名是否完全一致。这里最常见的翻车点是images 下建了 train 和 vallabels 下却建了 train 和 valid多一个字母代码按 val 去找就找不到标注。图片放进去之后我习惯顺手统计一下 train 和 val 的图片数量保证比例合理。一个别漏find pig-dataset/images -name *.jpg | wc -l find pig-dataset/images/val -name *.jpg | wc -lfind 管道给 wc -l 统计的是匹配文件的行数。这一步不产任何训练结果但能提前暴露val 目录是空的这种低级事故。我在项目里吃过一次亏val 目录只有 3 张图训练完看 val mAP 忽高忽低还以为是模型不稳定后来才发现是验证集太小统计上没有意义。2.3 data.yaml 与 train.txt让训练器找到数据的两条线YOLOv5 读取数据有两条路径一是 train.txt 和 val.txt 里每行写一张图片的绝对路径二是 data.yaml 里用字段指到 images 子目录。老版本只认 txt新版本更推荐直接在 yaml 里写相对路径。为了让代码不去猜我习惯写成这样# pig-dataset/data.yaml path: /home/pig/projects/pig-dataset # 数据集根目录建议写绝对路径 train: images/train val: images/val test: images/test nc: 1 names: 0: pigpath 字段指到数据集根目录train 和 val 写相对于根目录的子目录名。nc 是类别数这里固定 1。names 列表只有 0 号类别 pig注意索引必须从 0 开始和标注文件里的第一列保持一致。有的工具导出类别时从 1 开始编号那份数据到了 YOLOv5 里就会整体错位这个问题在第五章展开。train.txt 和 val.txt 里的内容极其简单每行一张图的完整路径结尾不要空行/home/pig/projects/pig-dataset/images/train/pig_20250101_100001.jpg /home/pig/projects/pig-dataset/images/train/pig_20250101_100002.jpgdata.yaml 的 path 和 txt 各行路径需要指向同一个地方。如果我改过项目目录位置两处都要同步改。我见过同事把数据集从服务器拷到本地yaml 里还留着服务器的绝对路径训练直接报 FileNotFoundError这就是最典型的路径黑匣子问题。3. 标注环节标注工具选择与猪框边界判定目录结构搭好了接下来才是最花时间的部分——标注。猪圈场景的标注难度不在工具操作而在一头猪的框边界到底画在哪。猪不像人没有固定的目标轮廓它可能头朝镜头、屁股朝镜头、侧躺、把脑袋伸到食槽里甚至三头猪叠在一起只露出两个头。做目标检测数据集处理规则定得越早返工越少。3.1 用 LabelImg 按 YOLO 格式落盘两个设置别改错目标检测常用标注工具里LabelImg 是老牌选择图形界面简单适合单类别数据集中批量操作。安装和启动之后关键不是开画而是先把两个设置确认好第一个设置是保存格式。标注框画完CtrlS 保存时工具会问你存成 PascalVOC 还是 YOLO 格式。选 YOLO它才会生成上一步那种类别 ID 四个归一化坐标的 txt 文件。如果误选 PascalVOC生成的是 XML 文件放进 labels 目录后 YOLOv5 根本不会解析 XML等同于没有标注。第二个设置是默认标注目录。LabelImg 允许单独指定图片目录和标注保存目录务必把标注保存目录指到对应的 labels 子目录。比如当前打开的是 images/train 的图片保存目录就指到 labels/train。下面是 LabelImg 启动的最小命令适用于 Linux 环境# 安装 LabelImg 及其依赖 pip install labelImg # 打开标注界面第二个参数指向图片目录 labelImg /home/pig/projects/pig-dataset/images/train \ /home/pig/projects/pig-dataset/labels/train第一个参数是图片路径第二个参数是预填的标注保存路径。启动后界面上先按 W 开始画框画完一帧按 D 切下一张。右侧的标签栏里要预先创建好类别 pig不要每张图临时手输不然很容易把 pig 打成 pgi一个拼写错误就是一类标注事故。单类别项目里这个问题还不致命类别一多拼写不一致直接导致某些框类别不存在训练时报错。我实际操作时每标完 100 张会停一下随便抽查 5 张重新打开图片和 txt 对照确认框的位置和类别没问题再继续。这不是强迫症是防止标注手滑产生系统性误差。3.2 猪框到哪才收手只标可见主体别标想象出来的轮廓这是猪圈数据集里最需要达成一致的一条规则。摄像头是俯视的栏里的猪互相遮挡如果要求每头猪都框出完整轮廓几乎不可能。我的做法是只框当前画面里该头猪的可见主体部分被遮挡的部分不补全。一头猪露出头、前腿和半边身体那就框住这一整块可见区域如果两头猪叠在一起底下那头只露出一个臀部也照实框出来。这样做有几个直接好处。第一标注员不需要脑补遮挡区域多人协作时标准统一第二模型学的是画面里猪的可见像素长什么样预测时遇到同样遮挡情况也能匹配上第三框的重叠率天然降低NMS 后处理阶段不会因为两个高度重叠的框而互相压制。有的同学习惯把所有可见猪只框成一个大的猪群框想偷懒减少标注量这个我试过模型会把一头猪和一个猪群全都数成一计数任务直接废掉。还有一个容易被忽略的点猪的耳朵、尾巴不会被单独标注。猪尾巴是长条形的容易误判成小目标但猪尾巴对猪圈管理没有独立价值按一头猪的可见主体一部分带过即可。保持目标定义纯粹模型才会把精力放在数出几头猪这件事上。这里有一条关于小目标的经验。猪圈全景画面里离摄像头最远的栏位猪在图像里可能只有 20×15 像素。这种目标在 640×640 输入下会被缩得更小。小目标检测的通用补救思路是在标注时保留这些极小框不要嫌麻烦就删掉训练阶段再用 mosaic 和大分辨率输入去兜底。删掉它们模型就失去了对远距离猪栏的感知能力。3.3 标注体检脚本训练前先扫一遍坐标越界和空文件标注完成后不要直接开训。先跑一段脚本做体检检查是否有空 txt、是否有坐标越界、是否有文件名不匹配。这段脚本我每次做数据集都会复用省掉大量训练到一半才发现数据的低级事故。import os from pathlib import Path def check_labels(img_dir: Path, lbl_dir: Path): imgs sorted(img_dir.glob(*.jpg)) sorted(img_dir.glob(*.png)) bad 0 for img in imgs: label_file lbl_dir / (img.stem .txt) if not label_file.exists(): print(f[缺失标注] {img.name}) bad 1 continue with open(label_file) as f: lines [line.strip() for line in f if line.strip()] if not lines: print(f[空标注] {img.name}) bad 1 continue for line in lines: parts line.split() if len(parts) ! 5: print(f[字段数错误] {img.name}: {line}) bad 1 continue cid, xc, yc, w, h parts try: xc, yc, w, h float(xc), float(yc), float(w), float(h) except ValueError: print(f[坐标非数字] {img.name}: {line}) bad 1 continue if not (0.0 xc 1.0 and 0.0 yc 1.0): print(f[中心点越界] {img.name}: {line}) bad 1 if w 0 or h 0 or w 1 or h 1: print(f[宽高异常] {img.name}: {line}) bad 1 print(f检查完毕问题文件数: {bad}) check_labels(Path(pig-dataset/images/train), Path(pig-dataset/labels/train))脚本的逻辑是遍历 images 下所有图片为每张图定位同名 txt。缺失标注和空标注直接记一次问题正常标注则拆分五个字段检查字段数、数字合法性、中心点和宽高是否都在 0 到 1 之间。中心点越界或宽高大于 1意味着标注工具保存了错误坐标这类坏数据训练时轻则拉低性能重则让 loss 出现 NaN。参数说明img_dir 是图片目录lbl_dir 是对应的标签目录两个 Path 对象拼接时用 stem 取文件名主干这样 jpg 和 png 都能适配。体检结果里如果缺失标注数量超过千分之一一般是标注员漏存了宁可回去补标也不要抱着侥幸心理训练。4. 数据集划分与训练准备按时间抽帧别让数据自己骗自己数据和标注都齐了接下来是数据集处理里最容易被低估的一步——划分训练集和验证集。猪圈摄像头视频是连续录的相邻两帧之间的场景几乎一样。如果划分时把所有图片随机扔进 train 和 val同一个猪栏同一批猪可能同时出现在两个集合里验证指标虚高到了新场景立刻露馅。目标检测数据集处理的质量很大程度上取决于这一步是不是较真了。4.1 划分脚本按视频片段抽帧而不是全局随机打散合理的做法是先把视频按时间片段分组同一个片段的帧要么全进 train要么全进 val。下面这段脚本实现了一个简单的按文件名分组抽帧逻辑假设文件名里带着日期和帧序号例如 pig_20250101_100001.jpg。import random import shutil from pathlib import Path src Path(pig-dataset/images/full) # 所有未划分图片 src_lbl Path(pig-dataset/labels/full) dst_img Path(pig-dataset/images) dst_lbl Path(pig-dataset/labels) # 按视频片段分组这里用文件名的前15个字符代表同一段视频 video_groups {} for img in sorted(src.glob(*.jpg)): key img.stem[:15] # 例如 pig_20250101_10 video_groups.setdefault(key, []).append(img) keys sorted(video_groups.keys()) random.seed(42) random.shuffle(keys) # 取 15% 的视频片段作为验证集 val_count max(1, int(len(keys) * 0.15)) val_keys set(keys[:val_count]) def move_img_group(img_paths, dst_dir): dst_dir.mkdir(parentsTrue, exist_okTrue) for img in img_paths: label src_lbl / (img.stem .txt) shutil.move(str(img), str(dst_dir / img.name)) if label.exists(): shutil.move(str(label), str(dst_dir.parent.parent / labels / dst_dir.name / label.name)) for k, imgs in video_groups.items(): if k in val_keys: move_img_group(imgs, dst_img / val) else: move_img_group(imgs, dst_img / train)脚本先说划分逻辑把文件名前 15 个字符当作视频片段标识同一片段的图片捆绑处理要么全去训练要么全去验证。这样模拟真实部署场景——模型上线后看到的画面和训练数据来自不同的时间片段。随机种子固定为 42保证能复现划分结果15% 的验证比例对单类别任务够用数据量大的话可以降到 10%。参数说明val_count 越界保护用了 max(1, ...) 确保视频片段再多也不会出现空验证集。移动文件时同时处理图片和同名 txt避免图片过去了标注还躺在原地。我通常在划分完再跑一遍 3.3 节的体检脚本确认移动后没有丢文件。4.2 data.yaml 相邻配套单类别任务的训练参数怎么定划分完数据回到 data.yaml 关注训练参数。YOLOv5 提供不同规模的预训练权重复杂场景一般从 yolov5s 起步。猪圈场景不算难目标类别只有一个不必一上来就用大模型。# hyp.scratch-low.yaml 里的关键参数按猪圈场景调整 hsv_h: 0.02 # 色调增强幅度原默认0.015微调即可 hsv_s: 0.3 # 饱和度增强室内猪圈色彩单一可以调高 hsv_v: 0.4 # 亮度增强猪圈光照差异大适当提高 fliplr: 0.5 # 水平翻转概率摄像头固定视角不建议超过0.5 mosaic: 1.0 # 开启mosaic单类别条件下mosaic能提升小目标效果hsv_v 是亮度扰动猪圈里白天的窗户光和晚上的灯光差异大把亮度扰动从默认的 0.0 调高到 0.4相当于变相扩充了训练集光照范围。hsv_s 饱和度对漏粪板反射和猪肤色都有影响但室内场景颜色单调0.3 足够。fliplr 保持 0.5 即可猪圈摄像头是固定视角左右翻转不会破坏语义。训练命令按官方最小流程走python train.py \ --data pig-dataset/data.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 8 \ --epochs 100 \ --patience 20解释一下参数weights 用官方预训练权重的 s 版本能加速收敛batch 根据显卡显存调整显存不够就降到 4单类别任务 batch 4 也可以稳定收敛epochs 100 对单类目标检测来说够用配合早停 patience 20loss 连续 20 轮不下降就自动停止避免浪费时间。img 640 是速度与精度的折中如果离摄像头远处的小猪老是漏检可以试试 1280显存和训练时间代价要自己权衡。4.3 单类别项目的特殊注意点背景样本不可忽略类别只有一个数据分配上反而会出现一个隐蔽问题所有图片都有猪模型对空栏没有概念。这意味着模型见到没有猪的画面也倾向于输出一个框。解决办法是在数据集中加入负样本——不包含任何猪的猪圈空栏图片。负样本的 txt 是空文件一行内容都没有。我在划分时专门保留一个 empty 目录放 10% 左右的空栏图片进训练集和验证集。训练时模型知道纯背景是什么样预测空栏时才不会乱框。负样本不用太多比例太大会压偏模型让模型偏向输出背景。10% 是我常用的起点如果验证时发现空栏误检反复出现再逐步往上加。这是单类别目标检测里最容易忽略但收益最直接的一步。5. 避坑记录猪圈摄像头标注与训练的 5 次翻车数据格式本身不复杂但把摄像头画面 手工标注 YOLOv5 训练串起来至少有 5 个坑我真踩过。每一条都是现象、原因、解决三段式按出现频率排序。5.1 训练 loss 正常下降val mAP 却一直是 0现象训练曲线和 loss 都很正常每个 epoch 的 mAP 稳如直线始终是 0。 原因labels 目录里混入了 LabelImg 误保存的 XML 文件YOLOv5 默认只读 txt遇到非 txt 文件解析失败相当于模型从头到尾看到的都是空标签所有预测都被当成背景。 解决训练前跑一遍 3.3 节的体检脚本检查 labels 下每个 txt 的行格式把 XML 和 json 文件全部清理掉。这种问题从 loss 曲线上根本看不出来只能靠数据校验脚本卡住。5.2 报错 AssertionError: train: No labels in .../labels/train现象train.py 启动后立刻报错提示 train 分区的 labels 目录不存在或为空。 原因目录层级和文件名不匹配。最常见的是 labels 下的子目录叫 valid而 images 下叫 val或者标注文件的后缀是 .json。 解决统一目录命名是治本方案。用第二章节的 bash 命令重新建骨架把文件移动到正确目录再执行 tree pig-dataset -L 2 确认 images 和 labels 的子目录名完全一致。5.3 数据量够但验证集上小猪几乎全部漏检现象大猪检测效果不错离摄像头远的小猪全部漏框recall 明显偏低。 原因训练输入被统一缩放到 640×640原始图中只有 20 像素宽的小猪缩放后只占 6 个像素语义信息基本丢失。这不是模型问题是输入分辨率和标注框分布共同作用的结果。 解决两个方向。一是训练时把 img 提到 1280保留原始小目标的像素二是把图片切成若干块分别训练和推理也就是切片推理的思路。我的习惯是先把 img 提到 1280 试一版如果训练时间扛不住再考虑切片方案。5.4 白天测试 mAP 很高晚上直接全漏现象白天的验证集上 PR 曲线漂亮一到夜间摄像头画面模型几乎每帧输出空结果。 原因训练集里 90% 以上是白天样本模型对夜间低照度、红外补光下的图像特征完全没有概念。数据增强里 hsv_v亮度扰动只能模拟亮度变化模拟不了红外补光的纹理和颜色偏移。 解决补数据优先于调参。我实际做的时候是从夜间监控视频里抽了几百帧和白天数据按大约 3:1 混合重新训练。如果实在没有夜间数据数据增强阶段把 hsv_v 调高、补加高斯噪声是无奈的补救效果比纯白天模型好一点但有限。5.5 部署后每帧都在抖框同一头猪被反复计数现象模型跑在实时视频流上效果还行但相邻帧的检测框跳来跳去一头猪走进走出栅栏时被不同的框交替触发计数重复。 原因单帧检测天然不稳定猪的遮挡程度、姿态、摄像头细微抖动都会让框的置信度上下浮动。模型只对单张图片做推理没有利用时间上下文。验证集用的是静态图片评估不出这种问题。 解决部署阶段加一个简单的后处理策略同一目标的框在连续 5 帧内位置变化小于阈值就认为是同一头猪只在第一次出现时计数。精准计数任务里这个 trick 比换模型更有效。另外把推理置信度阈值从默认的 0.25 提高到 0.4 以上误框会少很多。6. 验证模型与迭代val.py 的指标别只看 mAP还要看漏检样本训练结束后用 val.py 验证是看到指标最简单的方式跑一次只花几分钟。python val.py \ --data pig-dataset/data.yaml \ --weights runs/train/exp/weights/best.pt \ --conf 0.25 \ --iou 0.45 \ --batch 8conf 是置信度阈值iou 是 NMS 的 IoU 阈值batch 与训练时保持一致即可。val.py 结束后runs/val/exp 目录下会生成混淆矩阵、PR 曲线和一张带预测框的可视化图。在猪圈场景里我优先看 recall漏一头猪比多框一个无关目标后果更严重因为漏检会影响出栏计数和异常告警。mAP 是整体质量的参考真正要翻的是可视化图里那些左下角置信度在 0.2 到 0.4 之间的灰框——它们决定了模型的下限。进阶的迭代习惯是挖掘难例。模型在白天样本上跑一遍把预测框置信度低于 0.3 且真实存在的猪导出来人工确认后补进训练集。这类 hard example 比随机抽帧补标更有价值专门针对模型弱点做标注下一版的效果提升是最明显的。另外一个我常用的验证习惯是统计单张图的检测框数量。猪圈场景里单栏最多 8 头猪如果模型在一张图上输出了 15 个框基本能断定小目标误检问题可以优先关注合并栏位和反光区域。可视化图扫一遍比看指标曲线直接得多。我最初做第一版猪圈数据集时赶进度标得急只标大猪小猪的框嫌小没标。结果模型对大猪 recall 很高小猪全漏还以为是数据增强不够反复调了几天参数都不见起色。后来重新查标注补上几百个被漏掉的小猪框recall 直接涨了十几个点。从此我每做完一版数据都先抽 50 张最难的角度人工数一遍猪头数再和模型输出对比。数据对了后面的工作才会顺。希望帮到你。本文还有配套的精品资源点击获取