ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于YOLOV5的猪圈生猪检测数据集制作与训练全攻略

基于YOLOV5的猪圈生猪检测数据集制作与训练全攻略 简介面向猪圈摄像头场景的生猪检测数据集按YOLOV5目录格式整理完毕可直接用于训练。数据包共2000个文件包括933张JPG图片、1066个TXT标签文件及1个Python可视化脚本压缩包整体约97.43MB所有图像分辨率在640至1080之间类别统一为单个pig标签每张图像包含大量目标适合密集场景检测。数据已按训练集与验证集划分训练集929张图片及对应标签验证集136张图片及对应标签标签与图片同名无需额外转换附带的可视化脚本可随机读取一张图片并绘制边界框便于快速检查标注效果。目前已有608人学习适合目标检测初学者、算法工程师及生猪养殖智能化项目参考使用。1. 猪圈摄像头场景下的生猪检测单类别数据集为什么最容易翻车做猪只检测的人拿到一整套猪圈监控视频第一反应几乎都是同一个流程抽帧、标注、按 YOLOV5 目录格式摆好、开训。结果往往是这样的情况验证集 mAP 能到 0.9一到夜间低照度或者镜头溅上粪污模型就像失明一样漏检同一个栏位里明明有七八头猪最后框出来的只有一两头。问题不是网络不够深而是“单类别”这三个字把任务变得更刁钻了——所有猪都长得像目标密集、互相遮挡背景里料槽、栏杆、监控立柱和地面反光全是干扰模型没有任何类别线索可以借只能靠位置和尺度硬学。这篇笔记就把猪圈摄像头视频整理成一套靠谱的 YOLOV5 目录格式数据集的完整路径讲清楚从目录结构、标注规则、训练命令一直写到实景验证时要看的统计量给准备在自己猪场环境复现的人一条能直接照做的路线。2. YOLOV5目录格式从文件摆放到一份能直接开训的目录树2.1 目录树images 和 labels 必须对称YOLOV5 的目录格式核心是images和labels两个同级根目录下面的train、val子目录一一对应。这是一个典型的可训练数据集的最小结构piggery_dataset/ ├── images/ │ ├── train/ │ │ ├── cam01_day_0001.jpg │ │ ├── cam01_night_0001.jpg │ │ └── ... │ └── val/ │ ├── cam03_day_0101.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── cam01_day_0001.txt │ │ ├── cam01_night_0001.txt │ │ └── ... │ └── val/ │ ├── cam03_day_0101.txt │ └── ... └── dataset.yaml这套结构里最容易被忽略的是images和labels的兄弟关系。YOLOV5 在加载数据时会把图片路径中的images替换成labels来找对应标注文件所以这两个目录名不能乱改图片和 txt 文件必须同名同相对路径。很多人拿到别人的数据集后只把图片丢进去、标注文件随手放在别的目录训练时大量报WARNING: No labels found就是这个原因。有一个常见误解是“图片是 .jpg标签文件就一定要叫 .jpg.txt”。其实不需要YOLOV5 匹配的是文件主干名后缀由代码自己替换。真正要紧的是images/train里有什么图片labels/train里就必须有同样主文件名的.txt哪怕 txt 是空的也必须存在。空 txt 在 YOLOV5 里代表“这张图没有任何目标”会被当作背景负样本参与训练这一点在猪圈场景里特别有用后面会专门讲。2.2 每张图对应一个 txt五字段标签的坐标换算YOLO 格式的标注文件里每一行代表一个目标框格式固定为五个字段class_id x_center y_center width height。其中坐标全部是相对图片宽高的归一化小数取值范围在 0 到 1 之间。猪圈数据集只有猪一个类别所以class_id恒为 0。从常见的像素坐标框转成 YOLO 格式核心是一个换算函数def to_yolo_line(xmin, ymin, xmax, ymax, img_w, img_h): box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h return [0, round(x_center, 6), round(y_center, 6), round(box_w, 6), round(box_h, 6)]参数含义很直白img_w和img_h必须是图片的原始像素尺寸单位是像素。一个非常常见的翻车点是先把图片在标注工具里缩放过一轮再用缩放后的坐标去算归一化最后得到的框整体偏移模型怎么训都收敛不到好的 IoU。另一个坑是四舍五入精度归一化坐标至少保留 6 位小数否则小目标框会失真。猪圈监控里的猪远端的个体可能只有十几个像素宽精度损失直接等于框位移。LabelImg、X-AnyLabeling 这类常用标注工具导出的格式五花八门有 XML 的、有 JSON 的但最终落到 YOLOV5 目录格式里都必须转成上面这一行五列的结构。我一般会把转换脚本固定下来标注一批、转换一批、立刻用自检脚本检查一批不把问题攒到最后。2.3 data.yaml 的写法nc 与 names 是命门YOLOV5 训练时只认一个数据配置文件通常叫dataset.yaml放在数据集根目录下。单类别数据集的最小写法是train: /data/piggery_dataset/images/train val: /data/piggery_dataset/images/val nc: 1 names: [pig]train和val可以写目录路径也可以写一个 txt 文件列表的路径两种写法 YOLOV5 都接受。但无论哪种写法names后面永远建议用列表形式哪怕只有一个元素也别省方括号。曾经见过有人写成names: pig解析器看似没报错训练日志里类别名却变成了一串字符等于把类别标签搞坏了。训练启动后第一时间看两行日志一行是Number of classes: 1另一行是Names {0: pig}。这两行对上才说明 data.yaml 没写错。很多人只看第一行类别数对就开跑等到预测阶段发现类别名不对再来排查浪费的时间远不止一分钟。2.4 数据集自检脚本启动训练前先跑三秒标注数据是人手工完成的一定有错。YOLOV5 本身对坏标签很宽容加载时不会主动报错但会在训练过程中以 loss 异常、精度骤降的方式悄悄惩罚你。训练前跑一遍目录级体检是成本最低的后悔药。from pathlib import Path def check_dataset(data_root, nc1, suffix.jpg): label_dir Path(data_root) / labels / train img_dir Path(data_root) / images / train bad 0 for txt in label_dir.glob(*.txt): if txt.stat().st_size 0: continue # 空标签是合法负样本 img img_dir / (txt.stem suffix) if not img.exists(): print(f缺少图片: {img.name}) bad 1 for i, line in enumerate(txt.read_text().strip().splitlines()): parts line.split() if len(parts) ! 5: print(f{txt.name}:{i1} 字段数不等于5) bad 1 continue cls, xc, yc, w, h map(float, parts) if cls 0 or cls nc: print(f{txt.name}:{i1} 类别ID越界) bad 1 if not (0 xc 1 and 0 yc 1 and 0 w 1 and 0 h 1): print(f{txt.name}:{i1} 坐标越界) bad 1 print(异常数量:, bad)脚本逻辑分四层查文件配对、查字段数量、查类别范围、查坐标范围。坐标越界这一条在猪圈场景里特别高发因为标注时猪经常被栏杆挡住一半标注者为了把整头猪圈进去很容易把框拉出画面边缘。这类标签在训练时会让回归目标变得不可理喻轻则浪费算力重则直接把 loss 打成 NaN。3. 猪圈摄像头场景的数据处理低光、脏镜头、密叠猪怎么建模3.1 按时间段分层抽帧别把相似帧灌进数据集猪圈监控是 24 小时连续录像直接按固定间隔抽帧得到的原始样本会严重偏向某个时间段。比如只在白天抽的帧到了晚上模型就翻车只在某个猪栏前抽的帧换了栏位就失效。我一般会把一天按光照分成三段白天、傍晚、夜间分别抽帧再按摄像头编号分组保存。抽帧命令用 ffmpeg 一条就能完成ffmpeg -i night_20250115_0000.mp4 -vf fps1 -q:v 2 night_0001_%04d.jpgfps1表示每秒输出 1 帧q:v 2控制输出 jpg 质量数值越小画质越好。按 1fps 抽完的候选帧里相邻帧几乎一模一样如果全部留着训练集里会出现大量“亲兄弟”模型等于反复看同一头猪验证集的 mAP 会被刷得虚高部署时长尾场景该漏还是漏。我的做法是抽完后再按“每连续 10 帧只保留 1 帧”做一次粗去重把数据量压到原来的十分之一信息量损失却很小。这一步的目的是让数据集的光照分布和真实使用场景一致。猪圈摄像头通常有红外补光夜间画面是灰蒙蒙的和白天的色彩分布差异极大。如果训练集里白天帧占 95%模型其实只学会了“在白天的猪长什么样”这就是很多夜间漏检问题的源头。3.2 标注边界规则被挡的猪怎么画框猪圈场景里猪互相叠着睡觉、挤在料槽边抢食是常态目标边界经常模糊不清。如果标注人员凭感觉画同一个数据集里就会混入两种完全不同的语义网络学起来像是同时学两套标准。我固定下来的标注约定如下场景标注规则单独站立或行走包全身的最小外接框贴边不留空隙被部分遮挡可见面积大于 50%按完整外接框估算把被遮挡部分也包进去被严重遮挡可见面积小于 50%只标可见主体不硬补全看不见的部分画面模糊到无法判断是不是猪不标注保留为空标签背景帧目标尺寸小于图像短边的 2%不标注按背景处理这套规则的核心原则是“全身框优先宁可少标也不乱标”。单类别检测任务没有分类分支可以做语义兜底回归框学到的就是“猪的尺度先验”。如果一部分框是全身、一部分框是半个身子模型会在两种目标之间来回摇摆损失曲线长期震荡。标注完成后的质检环节不能省。YOLOV5 训练时会在runs/train目录下输出训练批次的标注预览图框画在原始图上任何错位一眼就能看出来。我习惯在每个批次的预览图里随机抽 20 张专门盯那些遮挡严重、猪只互相叠压的样本看标注框是否符合规则。这一步虽土但救过很多次训练。3.3 低光与红外画面从抽帧到三通道的预处理红外监控输出的是单通道灰度图而 YOLOV5 的预训练模型和网络结构默认按三通道输入设计。直接拿单通道图训练通常不会崩但会把模型的输入分布搞乱导致收敛变慢、精度上不去。稳妥的做法是在抽帧阶段就把单通道复制成三通道import cv2 img cv2.imread(night_raw.jpg, cv2.IMREAD_GRAYSCALE) rgb cv2.merge([img, img, img]) cv2.imwrite(night_3ch.jpg, rgb)这段代码把灰度图像的同一个通道复制三份合并成一张三通道图网络输入就合法了。三份内容完全相同不会增加信息量但能保证和预训练权重的输入分布对齐减少很多说不清道不明的玄学问题。更关键的是样本比例。我一般要求傍晚和夜间样本在训练集里占比不低于 30%否则模型对光照变化几乎没有鲁棒性。数据增强里的亮度扰动只能模拟“同一光照下的明暗变化”模拟不出红外补光带来的纹理改变、噪点形态和对比度特征。这些只能靠真实夜间样本喂出来。3.4 污渍与固定设施与其疯狂清洗不如让网络学会忽略猪圈镜头上溅到粪污、沾上灰尘是常态时间一长画面固定位置会出现一块模糊斑。网络很聪明它会学到“这个位置有一坨模糊的东西和猪很像”于是一段没有猪的视频里也能稳定输出一个框。针对这个问题常规做法是擦镜头后重新采样一段负样本视频把无猪画面抽帧后放进images/train并在labels/train里放同名空 txt。YOLOV5 会把空标签文件当作背景样本参与训练模型能明确学到“这个画面区域没有目标”。另一个手段是给训练样本加随机污渍增强模拟镜头附着物位置不固定时的场景import cv2 import numpy as np def add_stain(img, prob0.15): if np.random.rand() prob: return img h, w img.shape[:2] x, y np.random.randint(0, w), np.random.randint(0, h) r np.random.randint(15, 60) cv2.circle(img, (x, y), r, (0, 0, 0), -1) return imgprob是加污渍的概率r是污渍半径。这个增强在预处理阶段生效每次训练时污渍的位置和大小都不同网络就不会再依赖某个固定位置的纹理来判断“这里有猪”。猪圈里除了污渍还有料槽、栏杆、水管、地面阴影这些固定背景它们对模型的干扰原理完全一样统一用“负样本 随机遮挡增强”的方式处理比自己一张张去清洗数据高效得多。4. 从目录到训练YOLOV5 环境、划分脚本与超参数怎么定4.1 环境与预训练权重conda 建环境、装依赖、准备权重训练环境的搭建属于 yolov5 环境配置的标准动作命令顺序基本固定conda create -n yolov5 python3.9 -y conda activate yolov5 git clone 官方yolov5仓库 cd yolov5 pip install -r requirements.txt预训练权重用官方发布的yolov5s.pt起步最省事文件下载后放在仓库根目录即可。第一次做单类别检测不要一上来就上yolov5x先拿yolov5s跑通全流程确认数据集和标注没有系统性错误再根据精度瓶颈决定要不要换更大的模型。用 COCO 预训练权重做迁移学习时网络会把 80 类分类知识压缩成 1 类这个过程本身就是对数据集质量的检验。4.2 按时间段划分 train/val一个不泄漏的 split 脚本划分训练集和验证集时最忌讳的是全数据集随机打散。同一个视频段的相邻帧太像了随机划分会让验证集里混入大量“训练集的近亲”验证指标虚高换到真实部署场景立刻露馅。正确做法是按摄像头和时间段分组每个组内部再切分import random import shutil from pathlib import Path src_img Path(all_frames/images) src_lbl Path(all_frames/labels) dst_img Path(piggery_dataset/images) dst_lbl Path(piggery_dataset/labels) for d in [train, val]: (dst_img / d).mkdir(parentsTrue, exist_okTrue) (dst_lbl / d).mkdir(parentsTrue, exist_okTrue) groups {} for img in src_img.glob(*.jpg): cam img.stem.split(_)[0] # 文件命名形如 cam01_day_0001.jpg groups.setdefault(cam, []).append(img) random.seed(2024) for cam, imgs in groups.items(): imgs.sort() random.shuffle(imgs) val_n max(1, int(len(imgs) * 0.2)) val_imgs imgs[-val_n:] train_imgs imgs[:-val_n] for img in train_imgs: lbl src_lbl / (img.stem .txt) shutil.copy(img, dst_img / train / img.name) if lbl.exists(): shutil.copy(lbl, dst_lbl / train / lbl.name) else: (dst_lbl / train / lbl.name).touch() for img in val_imgs: lbl src_lbl / (img.stem .txt) shutil.copy(img, dst_img / val / img.name) if lbl.exists(): shutil.copy(lbl, dst_lbl / val / lbl.name) else: (dst_lbl / val / lbl.name).touch()脚本的关键是按cam分组每个摄像头单独切分保证验证集覆盖所有视角而不是恰好漏掉某个角落的猪圈。文件名里用split(_)[0]提取摄像头 ID适用于cam01_day_0001.jpg这类命名如果文件命名规则不同改这一行就行。没有标签的帧自动生成空 txt既避免训练时 WARNING 刷屏又能把无猪画面作为背景负样本送进去。4.3 训练命令与 single-cls单类别数据集的关键开关数据集目录和配置文件都就绪后训练命令长这样python train.py \ --data /data/piggery_dataset/dataset.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 200 \ --single-cls \ --cache参数里对单类别数据集最关键的只有一个--single-cls。它的作用是告诉 YOLOV5 把所有标签当同一类处理模型只学“这个位置有没有目标、框应该画多大”不学“这是什么类”。猪圈数据集天然只有 1 类开这个开关能减少分类分支对训练量的争夺收敛更快。如果以后计划扩展成“母猪/仔猪”多类别那时再重新组织数据训练不要提前把开关去掉。--img 640是输入分辨率。猪圈广角摄像头下远端猪只目标很小640 起步够用如果经过评估发现小目标漏检严重可以试试--img 1280但显存消耗会翻几倍训练时间也明显拉长。--batch按显存顶到不 OOM 为止16 是 8GB 显存左右的入门值。--cache把图片缓存进内存加速迭代硬盘空间够就开。4.4 训练日志怎么看P、R、mAP 曲线和早停时机训练开始后runs/train目录下会生成results.png里面包含train/box_loss、val/box_loss、precision、recall、mAP0.5、mAP0.5:0.95几类曲线。单类别任务里我更关注precision和recall的平衡而不是 mAP 数值。mAP 会被大量简单样本拉高猪圈里那些站得笔直、周围空荡荡的猪太好检测了几条腿的猪舍人群都能跑进高 mAP真正决定部署效果的是夜间密集遮挡场景下的 recall也就是“能不能把该看到的猪都框出来”。loss 曲线进入平台期、连续 50 个 epoch 没有明显下降时就该考虑早停了。YOLOV5 默认有早停机制但 patience 设得比较大小数据集上往往已经过拟合了还在跑。我一般把--patience参数显式设为 50节省无效训练时间。训练结束后如果准备部署到树莓派这类边缘设备还得用--img 416再训练一次或者做剪枝量化640 模型在边缘设备上推理帧率往往撑不住。5. 生猪检测数据集的 5 个常见坑现象、原因、解决办法5.1 标签坐标越界导致 loss 变成 NaN现象训练进行到十几个 epoch 时loss 突然变成 NaN或者前一秒 P/R 还在正常上升下一秒直接归零。原因标签 txt 里出现了越界坐标。最常见的是标注时把框拖出画面边缘导致归一化后的w或h大于 1或者x_center不在 0 到 1 之间。魔改过的回归目标让损失函数产生异常梯度网络权重被冲垮。解决训练前把第 2.4 节的自检脚本跑一遍对越界坐标做 clamp把x_center、y_center限制在 0 到 1 之间w、h限制在 0 到 1 之间。clamp 完后如果宽度或高度小于 0.01说明这个框本身就废了直接删除那一行。5.2 mAP 很高但夜测漏成筛子现象验证集 mAP0.5 达到 0.93感觉模型已经可用一放到夜间猪圈实拍视频检出率掉到三成以下。原因数据集划分没有按时间段分层。白天帧同时出现在训练集和验证集里验证 mAP 是“开卷考试”而夜间帧在训练集里占比极低甚至为 0模型根本没有见过这个光照域。解决按第 3.1 节的方式重新抽帧保证夜间样本占比不低于 30%同时用第 4.2 节的脚本按摄像头分组划分验证集。遇到这类问题先查数据分布不要条件反射去调网络结构或者换更大的模型问题往往不在模型容量上。5.3 镜头污渍区域固定误检位置先验比猪更像猪现象某个固定摄像头画面右下角有一块粪污污渍模型在没有猪的时候也稳定输出检测框置信度还不低。原因污渍在画面中的位置固定特征又和猪的轮廓有几分相似。网络学到了“这个位置的纹理大概率是目标”的捷径而不是真正理解猪的形态。解决擦干净镜头后重新采集一段无猪视频抽帧后放空标签文件进训练集让网络明确看到“这个位置的污渍不是猪”。同时在预处理里加随机污渍增强打破污渍位置的固定性。如果已经训练完不好回炉可以在推理后处理阶段对这个摄像头画面设置更高的置信度阈值比如从 0.25 提到 0.45压制固定位置的误检。5.4 单类别收敛困难标签的完整性比数量更关键现象--single-cls也开了数据量也有几千张但训练 loss 一直高位震荡验证框位置抖得厉害。原因标签语义不一致。一张图里有的猪标了全身框有的猪因为被遮挡只标了半截模型在同一批次里看到两种差异巨大的回归目标梯度互相打架。猪圈场景里猪只互相遮挡是常态标成什么样直接决定了模型学到的“猪的尺度分布”。解决统一按 3.2 节的标注规则执行可见面积大于 50% 标全身小于 50% 只标可见主体模糊不清的不标。严重遮挡、边界极不清晰的样本宁可删掉一张也不要交一张模棱两可的框。单类别任务里每张图的标注质量都要当作训练策略的一部分来对待。5.5 训练是俯视、部署是平视视角差异没有数据兜底现象训练集全部来自猪圈顶部俯视摄像头模型在俯视画面里表现很好换到斜角或者平视安装的摄像头漏检率大幅上升。原因不同视角下猪的体型形变、可见纹理和遮挡关系完全不同。俯视看到的是猪背轮廓平视看到的是猪头和侧腹特征分布差异大到数据增强弥补不了。解决训练集按视角分桶每个视角至少要有一两个视频段参与训练。如果部署目标是几十个猪圈建议在数据集里单独留下“部署地盲测集”——那些完全不参与训练、来自其他猪圈的样本。没有这套盲测集调参过程就是没有后悔药的赌博。6. 实景验证从四个统计量里挑出数据集的毛病6.1 按亮度给验证集分桶逐桶看召回整体 mAP 会掩盖光照短板。我的方法是把验证集按平均亮度分桶每个桶单独计算召回率精确定位模型在哪个亮度区间崩溃。核心思路如下import cv2 import numpy as np from pathlib import Path def analyze_illumination(val_img_dir, det_fn, buckets[60, 100, 140, 180]): stats {b: [0, 0] for b in buckets} for img_path in Path(val_img_dir).glob(*.jpg): img cv2.imread(str(img_path)) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) mean_brightness gray.mean() dets det_fn(img) for b in buckets: if mean_brightness b: stats[b][0] 1 stats[b][1] dets break return statsdet_fn是你的模型推理函数返回检测框数量亮度阈值按数据集实际分布调整。红外夜视图平均亮度通常只有 30 到 50白天能到 160 以上。如果模型在低亮度桶的召回明显偏低说明夜间样本不够而不是模型“不行”。6.2 中心点热图与时间抖动暴露固定误检的两张图把一段部署视频的检测结果中心点叠加到一张图上污渍误检会形成明显的热点一眼就能看出来heat np.zeros((h, w), np.float32) for det in dets: heat[int(det[1]), int(det[0])] 1 heat cv2.normalize(heat, None, 0, 255, cv2.NORM_MINMAX)把热图和原图加权叠加导出固定位置的误检区域会变成一个亮点簇。另一张关键的图是检测框的“时间抖动曲线”按帧输出检测框中心点的 x 坐标如果序列在某个段落剧烈跳动说明模型对同一头猪的定位不稳定通常是训练时遮挡样本的标签规则不一致导致的。我现在的习惯是拿到任何新数据集第一件事不是训练而是花半小时看标签长宽比直方图、夜间样本占比、空标签数量和中心点分布。数据自己会说话模型只是把数据里的问题复述出来。这套流程帮我少走了很多弯路希望也帮到你。本文还有配套的精品资源点击获取
返回列表