简介:猪圈摄像头场景下的生猪检测数据集,类别为单一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),那它的归一化中心点就是((480+720)/(2×1920), (270+540)/(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 和 val,labels 下却建了 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 是老牌选择,图形界面简单,适合单类别数据集中批量操作。安装和启动之后,关键不是开画,而是先把两个设置确认好:
第一个设置是保存格式。标注框画完,Ctrl+S 保存时,工具会问你存成 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(parents=True, exist_ok=True) 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 20,loss 连续 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 张最难的角度人工数一遍猪头数,再和模型输出对比。数据对了,后面的工作才会顺。希望帮到你。
本文还有配套的精品资源,点击获取