ARTICLE DETAIL

资讯详情

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

猪只检测实战:VOC转YOLO格式与634张小数据集训练全攻略

猪只检测实战:VOC转YOLO格式与634张小数据集训练全攻略 简介面向目标检测与计算机视觉入门者这份猪只标注数据集提供约634张真实猪图片并同时给出VOC和YOLO两种格式的标注文件可直接用于训练猪只检测模型也可作为标注练习数据。压缩包共1903个文件包含634张jpg图片、634个xml标注、635个txt标注整体229.59MBxml适用于VOC系列工具txt可被YOLO训练流程直接读取解压后无需密码即可使用。图片为jpg格式单张大小1-500KB标注类别统一为pig由labelImg工具制作并遵循目标边界准确、尽量不漏标、标注一致性检查等原则整体标注质量相对稳定。解压后图片、xml、txt分放在三个独立文件夹目录结构清晰便于按需筛选和快速接入训练流程。目前已有111人学习下载适合需要现成数据集进行算法验证、模型微调或教学演示的开发者。1. 猪数据集的双格式真相VOC 与 YOLO 并存意味着什么做猪场视觉巡检也好做猪只盘点计数也罢拿到一份「猪数据集」的第一反应应该是先搞清楚它为什么同时给了 VOC 和 YOLO 两种格式。634 张左右的规模明显不是拿来从零训练大模型的体量但也足够把 YOLO 这类检测模型调到一个能用的状态。VOC 格式保留的是完整的标注语义YOLO 格式则是给训练脚本吃的现成饭。这份数据集真正要解决的问题不是「怎么跑通一个 demo」而是怎么在这两种格式之间安全地来回切换怎么把 634 张的样本量用到接近极限以及在转换和训练里哪些环节最容易被忽略。适合正在做猪只检测、目标计数、行为分析或者第一次把标注数据集喂进 YOLO 的工程师和学生。接下来的内容全部围绕这份数据集的实操路径展开目录结构、格式转换、数据体检、训练避坑最后聊怎么用迁移学习和损失函数调参把这 634 张的潜力榨干。2. VOC 目录与 XML双格式数据集的原始凭证2.1 JPEGImages、Annotations 与 ImageSets先做一次文件对账VOC 格式的目录约定很固定JPEGImages 放图片Annotations 放 XML 标注ImageSets/Main 放训练验证划分文件。拿到这 634 张「左右」的数据第一件要做的事不是训练而是对账。因为「左右」这个词本身就意味着数量有水分常见情况是 JPEGImages 里有 634 张图但 Annotations 里只有 610 份 XML或者反过来多了几个没有图片的孤儿标注。我一般会先跑一段文件对账脚本把两个目录的差集列出来from pathlib import Path voc_root Path(VOC2024) jpgs {p.stem for p in (voc_root / JPEGImages).glob(*.jpg)} xmls {p.stem for p in (voc_root / Annotations).glob(*.xml)} missing_xml jpgs - xmls missing_jpg xmls - jpgs print(缺 XML 的图片:, missing_xml) print(缺图片的 XML:, missing_jpg) print(f图片 {len(jpgs)} 张, 标注 {len(xmls)} 份)逻辑上就是取两个文件主名的集合差集Python 的集合运算一次就能算完。缺 XML 的图片意味着训练时这张图没有任何目标YOLO 会把它当成背景图如果数量不多可以保留因为真实场景里本来就有空栏位缺图片的 XML 则直接标注了不存在的文件这种必须剔除否则训练脚本会在读图时报错。参数上注意 glob 模式要按实际扩展名调整如果数据集里有 PNG 格式的图只匹配.jpg就会漏算。做完这一步再顺手检查一下 ImageSets/Main 里的训练验证划分文件指向的文件名是否都在 JPEGImages 里。很多双格式数据集的 VOC 部分和 YOLO 部分是分别导出的VOC 目录里的 train.txt 和 val.txt 列的文件名如果对不上 YOLO 目录里的 images后面训练时就得手动修正。2.2 单张 XML 里的关键字段bndbox 不是全部VOC 的 XML 标注里大多数人只关心 object 节点下的 bndbox 四个坐标但猪只场景里真正影响训练质量的往往是另外几个字段。一个典型的猪栏俯拍标注长这样annotation folderJPEGImages/folder filenamepig_0234.jpg/filename size width1280/width height720/height depth3/depth /size object namepig/name poseUnspecified/pose truncated1/truncated difficult1/difficult bndbox xmin410/xmin ymin230/ymin xmax590/xmax ymax340/ymax /bndbox /object /annotationtruncated表示目标超出图像边界或被栏位截断difficult表示目标难以辨认通常是密集遮挡或目标太小。在猪只数据集里这两个字段的出现频率比一般数据集高得多因为猪栏的围栏会挡掉一部分身体猪与猪之间的互相遮挡也极其常见。麻烦的地方在于YOLO 格式的 txt 每一行只有class_id cx cy w h五个数字根本没有位置存放 truncated 和 difficult 这两个标志位。转成 YOLO 格式之前你必须先决定怎么处理这些样本全部保留、全部丢弃还是按比例保留一部分。这直接决定了训练出来的模型会不会漏检被栏杆挡住的猪。我的建议在后文转换章节里展开这里先记住一个结论不要无脑丢 difficult 样本否则模型的泛化能力会明显变差。2.3 写一个 50 行的 VOC 统计脚本先摸清猪的分布在对账之后写一个统计脚本对全部 XML 做解析看每个类别的目标数、每张图的目标数分布和框尺寸分布。这个步骤很多人跳过但对 634 张的小数据集来说先摸清分布能省下后面大量试错时间。import xml.etree.ElementTree as ET from pathlib import Path from collections import Counter anno_dir Path(VOC2024/Annotations) per_image [] box_wh [] for xml_file in anno_dir.glob(*.xml): root ET.parse(xml_file).getroot() objs [obj for obj in root.findall(object) if obj.find(name).text pig] per_image.append(len(objs)) for obj in objs: b obj.find(bndbox) xmin float(b.find(xmin).text) xmax float(b.find(xmax).text) ymin float(b.find(ymin).text) ymax float(b.find(ymax).text) box_wh.append((xmax - xmin, ymax - ymin)) print(标注张数:, len(per_image)) print(单张目标数分布:, Counter(per_image)) print(框宽高示例:, box_wh[:5])这段脚本的核心是解析 XML 树按name字段过滤出 pig再提取 bndbox 的四个坐标换算成框宽高。参数上注意两点一是findall(object)会拿到所有类别的目标如果数据集里混了人、狗等其他类别过滤条件不能少二是per_image只统计了有 pig 的 XML空标注文件不会进列表。跑完你会看到类似「单张目标数分布: {1: 135, 2: 210, 3: 150, 4: 79, 5: 40}」的数据这个分布决定了后面 anchor 尺寸和增强策略怎么调框宽度主要集中在 300 像素以内还是超过 500 像素直接影响你对小目标问题的判断。我说这是很多人 yolo 入门学习时最容易跳过的环节因为大家都急着跑训练命令结果模型翻车了才回来查数据。花十分钟做这个统计后面省的可能是几天的调参时间。3. 把 VOC 转成 YOLO 格式归一化坐标脚本与三类过滤参数3.1 为什么 YOLO 的 txt 只认归一化中心点和宽高YOLO 系列的训练脚本读取标注时每一行要求class_id cx cy w h其中 cx 和 cy 是目标中心点相对于图片宽高的比例w 和 h 是目标框宽高相对于图片宽高的比例取值范围都在 0 到 1 之间。这个设计不是为了让文件好看而是因为 YOLO 把图像划分成 S×S 的网格每个网格负责预测中心点落在自己格子里的目标。网格预测的是目标中心相对于网格左上角的偏移量偏移量本身就是一个 0 到 1 的比例值。如果直接把 VOC 的绝对像素坐标喂进去不同分辨率的图片会产生完全不同的标签分布同一个目标在 1280×720 和 1920×1080 下计算出的网格偏移完全不同模型训练时会无所适从。而 634 张的数据集里如果混了两种分辨率归一化坐标让它们可以一起训练而不需要统一缩放。VOC 的绝对值坐标和像素尺寸是给人看的YOLO 的归一化坐标是给网络吃的。转换这一步没有技术难度但有一堆细节坑下面脚本就是为了把坑一次性填平。3.2 转换脚本从 XML 到同名 txt 的最小实现完整转换脚本如下可以直接保存成voc2yolo.py运行import xml.etree.ElementTree as ET from pathlib import Path VOC_ROOT Path(VOC2024) OUT_LABELS Path(YOLODataset/labels) OUT_IMAGES Path(YOLODataset/images) CLASS_MAP {pig: 0} KEEP_DIFFICULT True # 是否保留 difficult1 的样本 OUT_LABELS.mkdir(parentsTrue, exist_okTrue) OUT_IMAGES.mkdir(parentsTrue, exist_okTrue) def xml_to_yolo(xml_path: Path): root ET.parse(xml_path).getroot() size root.find(size) img_w, img_h float(size.find(width).text), float(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASS_MAP: continue diff obj.find(difficult) if diff is not None and int(diff.text) 1 and not KEEP_DIFFICULT: continue box obj.find(bndbox) xmin float(box.find(xmin).text) xmax float(box.find(xmax).text) ymin float(box.find(ymin).text) ymax float(box.find(ymax).text) cx (xmin xmax) / 2 / img_w cy (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{CLASS_MAP[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) return lines for xml_path in (VOC_ROOT / Annotations).glob(*.xml): lines xml_to_yolo(xml_path) if not lines: continue stem xml_path.stem out_txt OUT_LABELS / f{stem}.txt out_txt.write_text(\n.join(lines)) img_src VOC_ROOT / JPEGImages / f{stem}.jpg img_dst OUT_IMAGES / img_src.name if not img_dst.exists(): img_dst.symlink_to(img_src)核心逻辑分成三块。第一块是读取size节点拿到图片真实宽高这是归一化的分母拿错直接导致所有框错位。第二块是遍历每个object用 CLASS_MAP 做类别映射这里如果数据集里有多个类别务必保证 CLASS_MAP 的 id 和最终训练用的 names 列表顺序一致否则会出现类别串位。第三块是坐标换算中心点用(xmin xmax) / 2 / 宽计算宽高直接相减再除以宽高保留 6 位小数是为了避免精度损失导致的边界抖动。参数设置里最关键的是KEEP_DIFFICULT。我建议保持默认 True把 difficult 样本保留在训练集里因为猪栏实拍场景中遮挡是常态模型见过难样本才能在现场不漏检。同时我在脚本里加了if not lines: continue空标注 txt 不生成这能避免 YOLO 训练时把空标签解释成背景图而产生意外行为。转换完成后强烈建议做一次可视化抽查随机挑 5 张图把 txt 里的归一化坐标乘以图片宽高画框然后和原图对比肉眼看框对不对得上。这个动作只需要五分钟但能拦截掉绝大部分坐标错位问题。3.3 训练验证划分种子、比例与镜头泄漏的三个原则634 张左右的数据量训练验证划分建议按 85% 训练、15% 验证来验证集大概 95 张左右足够看出模型有没有过拟合。划脚本时随机种子必须固定否则每次跑出来验证集都不同调参时看到的指标变化就不是模型改进带来的而是数据集采样波动。import random from pathlib import Path img_dir Path(YOLODataset/images) random.seed(42) images sorted([p.name for p in img_dir.glob(*.jpg)]) random.shuffle(images) val_count max(1, int(len(images) * 0.15)) val_names set(images[:val_count]) train_lines, val_lines [], [] for name in images: abs_path str((img_dir / name).resolve()) if name in val_names: val_lines.append(abs_path) else: train_lines.append(abs_path) Path(train.txt).write_text(\n.join(train_lines)) Path(val.txt).write_text(\n.join(val_lines))这里有一个容易栽的坑如果原始图片来自视频抽帧或者同一场景拍了多张连续照片直接随机划分会把同一镜头前 0.5 秒和后 0.5 秒的帧分别分到训练集和验证集。验证集的 mAP 会虚高得离谱因为模型见过的猪其实是同一头猪的同一姿态。我遇到过一次验证集 mAP 0.87现场测试直接掉到 0.6 以下排查半天才发现是镜头泄漏。正确做法是按拍摄片段分组文件名如果带camera1_001.jpg这种前缀就以前缀为分组单位把整个组的帧全部划进同一侧。脚本里可以先提取文件名前缀再 shuffle 分组而不是对单帧 shuffle。最后把划分好的文件写进数据集描述配置YOLO 的 data yaml 直接引用即可path: /path/to/YOLODataset train: train.txt val: val.txt nc: 1 names: 0: pig3.4 yolov8 训练自己的数据集启动命令与关键参数格式和划分都准备好之后就可以用 YOLO 的 CLI 启动训练。这里给一个我在 634 张猪只数据集上常用的起步命令yolo detect train datapig_dataset.yaml modelyolov8s.pt epochs150 imgsz640 batch16 lr00.01 warmup_epochs3参数说明modelyolov8s.pt表示加载 COCO 预训练权重这是小数据集上的关键一步后面第 6 章会细说imgsz640是 YOLO 系列的默认输入分辨率但如果第 2 章统计时发现大量猪只框的宽高小于 32 像素就要考虑把 imgsz 调到 960 或更高batch大小根据显存调整12GB 显存跑 640 分辨率时 16 一般没问题。第一次跑建议先只训 50 个 epoch看验证集指标是否在合理区间再决定要不要加长到 150。4. 训练前的数据体检634 张里的小目标、遮挡与增强边界4.1 标签合法性校验坐标越界和空标签是最常见的坏数据转换完成后、训练启动前还需要跑一次标签格式校验。这一步是为了拦下三类问题字段数不对、坐标越界、以及类别 id 超出 names 列表。634 张左右的标注如果来自人工手工整理几乎一定会出现零星的脏数据。from pathlib import Path label_dir Path(YOLODataset/labels) errors [] for txt in label_dir.glob(*.txt): for ln, line in enumerate(txt.read_text().splitlines(), 1): parts line.split() if len(parts) ! 5: errors.append((txt.name, ln, 字段数不为 5)) continue try: cid, cx, cy, w, h map(float, parts) except ValueError: errors.append((txt.name, ln, 存在非数值字符)) continue if not (0 cx 1 and 0 cy 1 and 0 w 1 and 0 h 1): errors.append((txt.name, ln, 坐标越界或宽高非正)) if cid ! 0: errors.append((txt.name, ln, f类别 id {cid} 未在 names 中定义)) print(f发现 {len(errors)} 处异常) for e in errors[:20]: print(e)校验逻辑是逐行拆分为 5 个字段再对每个浮点做范围判断。cx 和 cy 在 0 到 1 之间闭区间w 和 h 要大于 0 且不大于 1超过这个范围的标签在 YOLO 训练时会被当成坏图轻则影响 loss 计算重则直接终止训练。这个脚本跑完如果报错返回转换流程去查对应的 XML 和图片而不是手动改 txt因为根因多半在 XML 里的坐标写错或者图片尺寸被改过。我自己习惯把这个校验脚本跑两遍一遍在转换完后立即跑一遍在训练前跑。训练前跑的那次用的是最终放进数据集目录的 txt排查的是后续是否有人动过文件。4.2 框宽高分布与 anchor 尺寸直觉统计完合法性再看一遍框尺寸分布。猪只检测的核心难点是框形比极端俯拍视角下猪只是近似椭圆宽高比大多在 0.8 到 1.5 之间侧拍视角可能出现宽高比到 2 以上的长条框。但真正影响训练的是小目标数量占比。把 VOC 坐标换算成相对 640×640 输入下的像素尺寸如果大量框的宽或高小于 16 像素这些目标经过网络下采样后可能只剩几个特征点检测难度极高。反映到训练上常见现象是 loss 能收敛但小目标召回率上不去。这种情况有两个方向一是在训练时把 imgsz 提高到 960让输入分辨率变大小目标占的像素比重跟着变大二是用切片推理或专门的小目标检测策略但 634 张的数据量首先应该考虑提分辨率而不是换模型结构。如果统计结果显示 90% 的框都在 32 像素以上640 分辨率就够用强行提分辨率只会让训练速度变慢。4.3 针对猪只场景的增强配置mosaic 与翻转的边界YOLO 内置了丰富的数据增强但对猪只数据集不能全开。问题出在猪只目标的特性上猪与猪之间粘连严重单个目标本身的纹理又不强过强的几何增强会把本就模糊的目标边界彻底破坏。我一般在 634 张猪只数据集上用的增强配置是这样的hsv_h: 0.015 hsv_s: 0.5 hsv_v: 0.4 fliplr: 0.5 flipud: 0.0 mosaic: 0.5 mixup: 0.0参数取舍的逻辑hsv 色域扰动基本不破坏目标形状猪只肤色在不同光照下的变化本来就大所以完全保留fliplr 水平翻转对猪只检测很有价值训练集里如果猪头都朝向同一个方向翻转能补上对称姿态的样本flipud 垂直翻转我不开因为俯拍场景里上下翻转会制造出不符合物理规律的样本mosaic 设为 0.5 是让拼接增强存在但不占主导mosaic 开满 1.0 时猪只在拼接边界处会被切断反而让模型学到大量残缺目标mixup 关掉是因为单类检测里 mixup 的收益很低还会叠加在密集遮挡上产生重影。这段增强配置没有绝对最优但它遵循一个原则小数据集的目标是逼模型学到目标的语义特征而不是让增强把本来就少的标注信息冲散。5. 避坑记录猪只数据集转换与训练中最常见的五个翻车现场5.1 现象训练时 mAP 只在低位震荡loss 曲线下不去原因类别 id 错位。VOC 转换脚本里用 CLASS_MAP 做了从类别名到 id 的映射但训练用的 data yaml 里的 names 列表顺序和它不一致。比如 CLASS_MAP 里 pig 是 0yaml 里把 names 写成了[person, pig]模型读到 id 0 会当成 personperson 占用了一个分支但没有任何标注模型学的目标就从猪变成了一个不存在的类mAP 自然上不去。解决全局只保留一份类别定义。我通常直接把 CLASS_MAP 和 data yaml 的 names 放在同一个配置文件里转换脚本和训练命令都从这里读取。每次改类别都要两端同步而不是靠「记得去改」。5.2 现象验证集指标很高现场测试时猪框整体偏移半个身位原因图片被二次处理过。转换脚本用的是 XML 里size节点的宽高做归一化分母但有些标注工具在标注完成后对图片做过缩放XML 里的 size 没有同步更新或者 JPEGImages 里已经被替换成了压缩重采样的版本实际分辨率跟 XML 里写的对不上。框的整体偏移大部分来自宽高比失配。解决转换前用 Pillow 或 OpenCV 读一遍真实图片尺寸强制以实际读到的宽高为归一化分母。我在前面给的转换脚本里没有加这一层是因为它依赖具体的图片读取方式但实际项目中这一步不应该省略。血泪经验永远不信 XML 里的 size 字段永远以图片本身为准。5.3 现象loss 能收敛但被栏杆挡住半截的猪几乎全漏检原因转换时把 difficult1 或 truncated1 的标注全部丢弃了。这样操作后的训练集里所有猪都是完整可见的模型没有见过半截猪样本推理时遇到被挡住的猪特征不完整置信度直接掉到阈值以下。解决保留 difficult 样本这是我在转换脚本参数里特意留KEEP_DIFFICULT True的原因。如果担心难样本框不精准可以人工复核这一小批标注把明显画偏的修正后再保留。遮挡样本是猪场实拍场景的常态丢掉它们等于放弃模型的实地可用性。5.4 现象0.5 IoU 下的 mAP 不低但实际数猪时把食槽和母猪误检成猪原因训练集里猪的姿态太单一且大部分背景是固定栏位模型学了太多背景纹理而不是猪本身的语义。验证集如果和训练集来自同一场景的不同帧mAP 看起来正常一旦换到新的栏位布局误检率立刻飙升。解决增强时把 hsv 扰动和 fliplr 打开打破背景纹理的过拟合更重要的是检查验证集是否存在镜头泄漏同一摄像头的连续帧绝不能横跨训练和验证两侧。我经历过一次验证集 mAP 0.87、换场景掉到 0.6 以下的翻车根因就是划分脚本把同一个视频片段拆散了。5.5 现象yolov8 训练自己的数据集时提示标签加载失败或训练集数量比预期小很多原因txt 里的坐标值越界或者存在空标签文件。YOLO 读取标签时会解析每一行坐标小于 0 或大于 1 的会被判定为坏图整张图从训练集剔除空 txt 文件在某些版本里也会触发警告甚至被忽略。多张坏图累积下来634 张可能只剩 400 来张在训模型效果自然打折扣。解决训练前把 4.1 节的校验脚本跑一遍逐行检查坐标范围对空 txt 做统计。坏数据宁可删掉也不保留因为一张坐标错乱的标签对 loss 的破坏远大于一张好标签带来的收益。6. 用 634 张撬动可用模型迁移学习、损失函数权重与验收技巧634 张的数据量不足以从零训练必须走迁移学习路线。YOLO 官方在 ImageNet 或 COCO 上预训练的权重可以直接当起点训练时冻结模型前 10 层跑 50 到 80 个 epoch让新头先学会表达猪只特征再解冻全部层微调 50 个 epoch 左右。冻结阶段的学习率可以给到 0.01 以上解冻后必须降到 0.001 以下否则骨干网络的预训练特征会被冲掉。阶段冻结层数学习率训练轮数冻结骨干100.0150-80解冻微调00.0005 - 0.00150损失函数权重方面我一般从box7.5, cls0.5, dfl1.5这个组合起步。猪只检测只有 pig 一个类别分类任务本身很简单cls 权重太高会让模型把精力浪费在区分「是不是猪」上box 和 dfl 负责回归框的位置和形状对密集遮挡场景的影响更直接。如果训练时发现框的定位偏松偏紧优先调 box 权重而不是去动网络结构。最后是验收技巧。训练结束不要只盯着验证集 mAP我会额外留出 20 张从未参与训练的图像做人工预测检查直接看模型的预测框和真实猪只位置是否对齐。这个检查至少能发现两类问题验证集指标虚高但实际视角变化时崩掉以及小目标漏检。小数据集训练容易出现过拟合而 20 张新鲜图像比任何指标都更诚实。我自己拿到任何双格式数据集第一件事永远是做一致性校验这个习惯替我挡掉过多次训练翻车。对标注文件的怀疑要像对模型一样认真希望这份猪数据集的实践路径能帮你在 634 张的规模上少走弯路。希望帮到你。本文还有配套的精品资源点击获取
返回列表