☰
目标检测小样本实战:猪数据集VOC转YOLO全流程指南
2026/10/1 13:50:43 网站建设 项目流程

简介:猪只目标检测数据集面向计算机视觉学习者、算法工程师及智慧养殖领域研究者,聚焦猪只个体识别、群体计数与行为分析场景,提供约634张猪图片及人工标注,标注类别统一为pig,可直接用于YOLO、Faster R-CNN、SSD等主流目标检测框架的训练与评估。资源共1903个文件,包含jpg原图634份、VOC格式xml标注634份、YOLO格式txt标注635份,压缩包整体229.59MB,按图片、xml、txt三个文件夹分类存放,解压后无需解压密码即可对照原图与标注文件快速使用。标注由labelImg工具完成,遵循准确框选目标边界、尽量覆盖图像中全部目标、标注后进行一致性检查的原则,有助于降低标注噪声并提升模型泛化能力。已有110人学习,既适合入门者熟悉VOC/YOLO两种格式,也可作为算法实测、课程设计或毕业设计的基准数据集。

1. 只看634张图的量级:猪数据集的VOC与YOLO格式意味着什么

一份猪数据集,VOC和yolo格式的目标标注,634张左右。这个数字在目标检测里属于典型的小样本,但它并不是不能用的废料。做猪只盘点、出栏计数、行为识别或者栏舍异常预警的起步阶段,634张足够把完整训练流程跑通,并验证算法在真实场景里的表现。真正会拖垮你的往往不是图片数量,而是标注格式不统一:VOC的XML里写的是绝对坐标,yolo的txt里写的是归一化中心点,接口一错,模型训练出来的框就会乱成一团。这篇文章从数据检查、格式转换、训练参数、常见坑位一路讲到验证技巧,适合正准备入坑目标检测的从业者,也适合手头已有标好数据但不知道怎么喂进训练框架的工程师。

2. 先吃透VOC和YOLO两种标注结构:目录、字段、坐标换算

2.1 两种格式长什么样:XML的绝对框与txt的归一化中心点

VOC格式来自Pascal VOC比赛,是目标检测领域最通用的“交换格式”。它的标准布局是:JPEGImages目录放原始图片,Annotations目录放同名XML文件,ImageSets/Main目录放训练和验证的图片名清单。每个XML文件用object标签描述一个目标,bndbox子节点里存的是xmin、ymin、xmax、ymax四个值,单位是像素,坐标原点在图片左上角。

我一般拿到数据集后会先找一个XML打开看,判断它是不是标准Pascal VOC结构:

<annotation> <filename>pig_00001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>pig</name> <bndbox> <xmin>245</xmin> <ymin>310</ymin> <xmax>868</xmax> <ymax>704</ymax> </bndbox> </object> </annotation>

yolo格式则是训练框架直接读取的“运行时格式”。每张图片对应一个txt文件,每一行描述一个目标,五个值依次是:class_id、x_center、y_center、width、height。注意,中间的四个数值全部是以图片宽高做分母的归一化值,不再是像素。比如上面这个猪框,图片1920乘1080,那么 yolo 的 txt 里应该写:

0 0.290104 0.469444 0.324479 0.364815

换算逻辑是:中心点x = (245+868)/2 / 1920,中心点y = (310+704)/2 / 1080,宽 = (868-245)/1920,高 = (704-310)/1080。这个换算并不复杂,但它是后续所有训练问题的最常见源头:只要有人手动改过XML坐标,或者标注工具缩放显示时记错了像素值,归一化后的数字就会超过1或者变成负数,模型训练直接崩。

还有一个容易忽略的点:yolo的class_id不是类别名字符串,而是从0开始的整数。因此任何yolo训练项目都必须要有一份类别映射文件,常见做法是data.yaml里用names列表,或者在训练目录下维护一个classes.txt。拿到数据先看这两个东西在不在,比急着训练更重要。

2.2 数据集一致性三查:图片数、标注数、类别数对得上吗

把634张的底子摸清楚,我一般会按三个步骤做一致性检查,每一步都花不了两分钟,但能省掉后面一整天的排查时间。

第一步是看数量对不对。统计JPEGImages和Annotations两个目录下的文件数,最直接的方式是bash命令:

find JPEGImages -name "*.jpg" | wc -l find Annotations -name "*.xml" | wc -l

如果图片数和XML数不一致,多半是标注过程中漏存了文件,或者同一张图被重复导出。对不上时不要急着补,先找出具体缺哪个,用文件名差集定位。

第二步是检查图片和XML的一一对应关系。很多时候数量对上了,但文件名对不上,训练时照样报错找不到标注。用一段Python脚本做差集最省事:

from pathlib import Path img_dir = Path("JPEGImages") xml_dir = Path("Annotations") imgs = {p.stem for p in img_dir.glob("*.jpg")} xmls = {p.stem for p in xml_dir.glob("*.xml")} print("图片总数:", len(imgs)) print("XML总数:", len(xmls)) print("缺标注的图片:", len(imgs - xmls)) print("缺图片的标注:", len(xmls - imgs))

这段脚本的原理是:把两个目录下的文件名取出来,去掉后缀后分别存成集合,然后求差集。jpg和xml后缀不同的情况不影响,因为stem只看主文件名。如果输出显示缺了某个编号,就去原标注目录里找人工补导出对应文件。

第三步是统计类别名和出现次数。这一步最关键,因为猪数据集的类别可能是单一pig,也可能是母猪、育肥猪、仔猪等多个类别,类别名字串不一致会直接导致class_id映射错位:

import xml.etree.ElementTree as ET from collections import Counter counter = Counter() for xml_file in xml_dir.glob("*.xml"): root = ET.parse(xml_file).getroot() for obj in root.iter("object"): counter[obj.findtext("name").strip()] += 1 print(counter.most_common())

输出可能是[('pig', 1200)],也可能是(('sow', 600), ('piglet', 400))。看到多种类别名时,先想清楚训练任务是单类还是多类。如果业务上并不需要区分母猪和仔猪,直接把类别名统一成pig,能显著降低训练难度。类别名有拼写差异的,比如pig和Pig同时存在,也要在转yolo之前先合并。

提示:这三查做完,数据集的“底子”才算真正立住。数量一致只能说明文件齐全,类别一致才说明标注语义可用。

3. VOC转YOLO:一个能跑的脚本,加转换后必查的三个位置

3.1 为什么两个都要留:标注工具写VOC,训练框架读YOLO

现在绝大多数开源标注工具,比如LabelImg、labelme的DET模式,默认导出的是XML或者JSON格式,而不是yolo的txt。原因很简单:标注工具面对的是人,人在看一张图的时候更容易理解“这个框是从左上角245像素到右下角868像素”,而不是“中心点在0.29、0.47,宽高是0.32、0.36”。yolo格式是给模型看的,它不关心像素,只关心相对位置。

常见做法是XML永远作为原始底稿保留。标注阶段只用XML,训练阶段不直接吃XML,而是先跑一遍转换脚本生成txt。好处有两个:第一,XML可读性强,发现标注错了可以直接改坐标;第二,换个训练框架时,只需要重新生成一次txt,不用重新人工标注。634张图的转换脚本跑一次只要几秒钟,没必要手动去改txt。

3.2 手写VOC转YOLO转换脚本:从XML到txt的完整过程

下面这段Python脚本是我在实际项目里常用的最小实现,不依赖第三方库,Python 3.6以上就能跑:

import xml.etree.ElementTree as ET from pathlib import Path def xml_to_yolo(xml_path: Path, class_map: dict, img_w: int, img_h: int, out_dir: Path): root = ET.parse(xml_path).getroot() lines = [] for obj in root.iter("object"): name = obj.findtext("name").strip() if name not in class_map: print(f"未映射类别: {name}, 已跳过") continue box = obj.find("bndbox") xmin = float(box.findtext("xmin")) ymin = float(box.findtext("ymin")) xmax = float(box.findtext("xmax")) ymax = float(box.findtext("ymax")) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) w = min(max(w, 0.0), 1.0) h = min(max(h, 0.0), 1.0) lines.append( f"{class_map[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}" ) out_dir.mkdir(parents=True, exist_ok=True) (out_dir / (xml_path.stem + ".txt")).write_text("\n".join(lines), encoding="utf-8")

逻辑说明:脚本的核心是从bndbox里读出左上角和右下角的绝对像素坐标,然后换算成中心点加宽高的归一化值。这里有两个关键设计。第一,class_map必须是一个预先定义好的字典,比如{"pig": 0},不能直接用字符串类别名,否则yolo训练时无法识别类别顺序。第二,换算出结果后特意加了clip限制,把x_center、y_center、w、h全部钳制在0到1之间。

为什么要clip?因为标注工具在鼠标拖拽越界时,xmax可能大于图片实际宽度,ymax可能大于高度。不处理的话,训练时标签坐标超出边界,软件会报错或直接把这些样本当坏数据丢掉。clip的代价是边界框位置略有误差,但至少训练不会中断。严格的做法是在转换前自动提示越界框,让人工确认是修改标注还是保留。

调用脚本时,需要传入每张图的宽高。不推荐从XML的size节点读,因为有些标注工具写进去的size和原图不一致。最稳的方式是在转换前用PIL批量读取图片尺寸,存成字典再传入:

from PIL import Image from pathlib import Path img_dir = Path("JPEGImages") size_map = {} for img_path in img_dir.glob("*.jpg"): with Image.open(img_path) as im: size_map[img_path.stem] = im.size # (w, h) class_map = {"pig": 0} out_dir = Path("yolo_labels") for xml_path in Path("Annotations").glob("*.xml"): w, h = size_map[xml_path.stem] xml_to_yolo(xml_path, class_map, w, h, out_dir)

参数说明:size_map的作用是避免每转一个XML就打开一次图片,634张图用字典缓存尺寸后,转换速度会快很多。class_map按训练需求调整,如果业务里区分sow和piglet,就写成{"sow": 0, "piglet": 1},顺序就是训练时的类别编号,这个顺序不能随便换,否则之前标注的所有txt都要重新生成。

3.3 转换后必查的三个位置:类别ID、坐标越界、空标注

脚本跑完不等于转换成功。我每次转换完都会做三个验证,用不了五分钟,但能避免后面训练到一半才发现数据有问题。

第一个位置是类别ID。检查生成的所有txt里,每一行的第一列是否都在0到类别数减1之间。可以用一行命令快速完成:

awk '{print $1}' yolo_labels/*.txt | sort -n | uniq -c

如果输出的编号里有超出预期的值,说明class_map写错了,或者XML里有未纳入映射的类别名。此时重新检查XML里的object name,把漏掉的类别名补进class_map,再重新跑一遍脚本即可。

第二个位置是坐标越界。虽然脚本里做了clip,但clip只能防止数值超过1,不能保证数值和原图视觉位置吻合。抽3到5个txt,把归一化值乘回图片宽高,和原XML里的bndbox对比:

python -c " from pathlib import Path p = Path('yolo_labels/00001.txt') w, h = 1920, 1080 for line in p.read_text().strip().splitlines(): cid, cx, cy, bw, bh = map(float, line.split()) xmin = (cx - bw/2) * w ymin = (cy - bh/2) * h xmax = (cx + bw/2) * w ymax = (cy + bh/2) * h print(cid, int(xmin), int(ymin), int(xmax), int(ymax)) "

对比时重点看xmax是否接近原XML里的值,ymax是否超出图片高度。如果明显对不上,问题多半出在size_map里读到的尺寸和拍摄原图不一致,需要检查数据集是否存在压缩后的副本。

第三个位置是空标注。有些XML里object为空,或者所有类别名都被class_map跳过,这种情况下生成的txt是空文件。yolo训练框架对空标注文件的态度各家不一,有的直接忽略,有的报错。稳妥起见,转换脚本里加一行判断,如果lines为空就跳过,不生成txt文件,同时打印警告。这样在后续检查时,可以用图片数减txt数定位到哪些图缺标注,便于人工补标。

4. 用634张启动yolo训练:划分、预训练权重与参数设定

4.1 训练集和验证集怎么分:按猪舍与拍摄时间,别全随机

634张图如果全部扔进训练集,模型在训练集上会表现得很好,但validation一旦暴露真实场景,mAP会惨不忍睹。问题不全是数据量少,而是划分方式不对。全随机划分最常见的毛病是:同一个猪舍、同一台摄像机、几乎同一个角度连续抽帧得到的图片,被同时分进了训练集和验证集。模型在验证集上看到的是“训练时见过的那头猪的另一个角度”,分数虚高,部署到新栏舍时直接露馅。

我一般会先看文件名携带的信息。猪场数据集的文件名通常带栏舍编号或拍摄时间,比如A1_20240318_091230.jpg。按文件名前缀分组,再整组划分,能保证同一个场景的图片不会横跨训练集和验证集:

from pathlib import Path import random files = list(Path("images").glob("*.jpg")) groups = {} for f in files: prefix = f.name.split("_")[0] # 按栏舍编号分组 groups.setdefault(prefix, []).append(f) all_groups = list(groups.values()) random.Random(42).shuffle(all_groups) train_ratio = 0.8 val_ratio = 0.1 n_train = int(len(all_groups) * train_ratio) n_val = int(len(all_groups) * val_ratio) train_files = [p for g in all_groups[:n_train] for p in g] val_files = [p for g in all_groups[n_train:n_train + n_val] for p in g] test_files = [p for g in all_groups[n_train + n_val:] for p in g] print(f"train: {len(train_files)}, val: {len(val_files)}, test: {len(test_files)}")

参数说明:这里的核心是按文件名前缀分组,而不是对634个独立文件做随机洗牌。random.shuffle用的是固定种子42,保证每次运行结果一致,方便复现。如果文件名没有可分组的前缀,退而求其次的做法是每隔N帧抽一张做验证,这也能降低相邻帧的相似性。

需要注意,634张分成8:1:1后,验证集只有60张左右,测试集更少。验证指标会有一定抖动,所以后面看mAP时不能只看一次数值,要结合loss曲线一起判断。

4.2 预训练权重、输入尺寸和batch size:小数据往稳里调

634张的小数据集,从头训练yolo几乎必翻车。没有预训练权重时,网络前几十个epoch全靠随机初始化,感受野还没稳定,loss下降非常慢,甚至直接发散。常见做法是下载官方yolo预训练模型权重,在自己的数据上做迁移学习。对于猪只检测这种目标清晰、场景单调的任务,用轻量级模型就够,不需要一上来就上最大规格的模型。

数据配置方面,先建一个data.yaml:

path: dataset train: images/train val: images/val names: 0: pig

names列表的类顺序必须和转换脚本里的class_map一致。如果class_map里猪是0,names里第一项就是pig。yolo训练框架会用names列表去匹配txt里的class_id,顺序错了模型就会把猪学成别的类。

训练命令我一般这样起:

yolo train \ data=data.yaml \ model=yolov8n.pt \ imgsz=640 \ batch=16 \ epochs=100 \ lr0=0.01 \ patience=30

参数说明:model=后接yolo官方预训练权重文件,yolov8n是轻量级网络,634张数据下比大模型更容易收敛。imgsz=640是输入尺寸,如果原图是1920乘1080,训练时会等比缩放到640,这个值决定了小目标的检测下限,猪只体量大,640足够。batch=16是单卡常见设置,显存不够就减到8,但低于8时BN层统计不稳定,容易出问题。lr0=0.01是初始学习率,小数据刻意调低,防止前几个batch把预训练权重冲坏。patience=30表示30个epoch验证集没提升就提前停。

4.3 训练命令与损失曲线判读:出现BN崩溃时先查这三样

训练跑起来后,我基本都是盯三个loss曲线:box_loss、cls_loss、dfl_loss。yolo的损失函数不是单一值,box_loss衡量预测框和真实框的位置误差,cls_loss衡量类别判断误差,dfl_loss负责边界框的分布建模。三者下降速度不一样,box_loss下降最快,dfl_loss次之,cls_loss最后趋稳,这都正常。

需要警惕的异常情况有两种。第一种是loss完全不降,前10个epoch基本是平线。原因通常是学习率过大或者预训练权重没加载成功。检查方式很简单,看命令行输出的model字段是不是显示from=yolov8n.pt,如果显示从头训练,就去把权重文件路径补齐。

第二种更隐蔽,也更容易在猪只数据集上出现:loss在某一个epoch突然跳到几十甚至NaN,之后再也回不来。这种叫BN崩溃,常见诱因是训练后期某个batch里含有极端标注,比如框的归一化坐标刚好卡在0或者1,导致BN层的均值和方差统计被污染。遇到这个情况时,先做三件事:第一,把batch size提到16或32,增加统计稳定性;第二,把lr0从0.01降到0.001;第三,重点检查clip到0或1的边界框,这些框在数据增强时很容易被裁掉一半,形成“半个猪”的极端样本。

注意:BN崩溃不是模型不行,是数据里有一个或几个坏样本在捣乱。先排查那个导致loss跳变的epoch之前新增的模型预测输出,再配合逐张抽检标注框,不要一上来就换模型结构。

5. 训练过程中的常见坑:标注、转换、训练三个环节的血泪排查

5.1 猪只重叠导致标注互相吞并,模型学出一团糊框

现象:训练出来的模型在单头猪的场景下框得很准,一旦出现两三头猪挤在一起,就输出一个大框把好几头猪全包进去,或者两头猪中间生成一个无意义的框。

原因:标注阶段,标注员在猪只重叠时图省事,只标了最外层的轮廓,把好几头猪画成一个框。更常见的情况是,先给A猪画了框,画B猪时鼠标稍微拖大了一点,直接把A猪的框吞掉,B猪的框覆盖了A猪大部分区域。模型从这些样本里学到的不是“猪的轮廓”,而是一个模糊的“猪群范围”。

解决:回到XML里找出所有高重叠度的框,计算两两之间的IoU,超过0.5的样本单独抽出来人工复核。复核时把图片放大到200%,先标视觉上最完整的猪,再标被遮挡的猪,允许被遮挡的猪框只有一小块可见区域。这类样本对模型学习遮挡鲁棒性非常重要,宁可框小一点,也不要框大一整片。

5.2 坐标越界或类别ID错位:转换后模型无法收敛

现象:训练到十几个epoch时,cls_loss下不去,输出类别总是集中在某一个ID上,比如所有预测都是类别0。或者训练前置检查直接报错,提示某个标签的坐标在0到1之外。

原因:坐标越界最常见的原因是转换脚本读图片尺寸时读到了EXIF信息里的旧尺寸,或者原图被压缩后XML的bndbox没有重新生成,xmax还是压缩前的像素值。类别ID错位的根源是class_map的字典顺序和data.yaml里的names列表顺序不一致,转换时用的class_map把pig设为0,训练时data.yaml里第一项却写了别的类。

解决:坐标越界用前面第3章的抽样脚本,把归一化坐标乘回当前图片实际宽高,逐张对比。发现越界统一改成clip处理,并在转换日志里输出警告,警告数量超过总样本数5%就说明原图有批量替换,最好重新标注。类别ID错位没有捷径,只能统一class_map和data.yaml的顺序,改完重新转一遍txt,不要手动改txt里的数字。

5.3 损失曲线不掉或NaN:BN崩溃与学习率过高

现象:box_loss一开始停在0.1附近不动,或者训练到第20个epoch突然跳到NaN,之后全部输出变为无意义数值。

原因:BN崩溃的触发点通常是某个batch里有一个极端标注框,它的宽或高趋近于0,或者中心点坐标刚好等于0。这样的框在数据增强的随机裁剪里几乎被完全裁掉,模型被迫学习预测一个不存在的目标,梯度瞬间爆炸。学习率过高则是另一种情况,小数据集上lr0=0.01配合batch=8时,BN统计量波动大,很容易在初期就把权重冲飞。

解决:先把lr0降到0.001,batch提到16,如果还是NaN,就把数据增强里mosaic和mixup的比例调低,或者暂时关闭其中一个。这些增强虽然对提升小样本泛化能力有帮助,但它们会把标注框做随机拼贴,坐标一旦拼接出错,NaN就来了。跑通一遍后再逐步加回增强。

5.4 验证mAP低但训练loss很低:样本不平衡与过拟合

现象:训练集和验证集的数据分布差异大,或类别不完全。例如训练集全部来自母猪舍,验证集却包含一些仔猪图片;更典型的是,训练时验证集上loss停止了下降,但mAP只有0.5左右,而训练集上的loss很漂亮。

原因:634张的小数据集很容易过拟合到具体场景。模型记住的不是“猪的通用特征”,而是“训练集里那几头猪身上的花纹、栏舍背景、光照角度”。验证集如果恰好是另一个栏舍、另一种光照,模型就表现得很差。

解决:验证集一定要按场景留出“没见过的猪”。前面按栏舍分组划分就是为了这个。其次,在训练时提高HSV饱和度、亮度的增强强度,让模型不依赖具体的皮毛颜色。最后,如果mAP依旧低,检查验证集里猪只的尺度分布。母猪舍的图片猪占画面很大,仔猪舍的图片猪很小,模型训练时大多数学到的是大尺度特征,小尺度检测自然翻车。这种情况可以试试把imgsz从640调到768或832,给模型更多像素去分辨小目标。

5.5 混淆矩阵总和看着不对:行归一化的正常现象

现象:训练结束后打开confusion_matrix.png,每一行的数值相加不是100,甚至不是整数,有人会误以为是训练出错或者类别统计漏了。

原因:yolo混淆矩阵按目标框计数,并且按行归一化。每一行代表某个真实类别,行内各列表示该类别被预测成各个类别的比例,因此每行加起来是100%左右。但由于测试图片里目标数量不同,不同图片的目标数差异会让数量级的感知产生偏差;显示的百分数保留小数值时,行内数字相加也可能在99或101附近,这都不是错误。

解决:看混淆矩阵时重点看对角线值和最容易混淆的类别对。比如矩阵显示pig被预测成background的比例过高,说明漏检严重;pig被预测成sow的比例偏高,说明两类外观太接近。对于634张的小数据,如果两类之间混淆超过30%,最实际的做法是把它们合并成一个pig类,先保证单类检测的准确率,再考虑细分。矩阵“总合不唯一”本身不是问题,不要因为数字不好看而去修改测试集。

6. 样本不够时的验证技巧:先过拟合,再看混淆矩阵

6.1 先做20张的过拟合冒烟测试,再谈优化

训练前我习惯先做一个冒烟测试:从训练集里随机抽20张图,不设验证集,直接训练30到50个epoch。如果loss能一路降到很低,说明数据读取、标注坐标、类别映射、模型配置这条链路是通的;如果连20张都过拟合不了,问题一定在数据或配置层面,而不是模型容量不够。这个测试不要跨epoch早停,让它跑就行,10分钟就能出结果。

冒烟测试通过后,再回到634张的全量训练。看指标时别只盯着mAP,先看验证集上的混淆矩阵,确认漏检和错检集中在哪。小样本的验证集可能只有60张图、几百个目标,混淆矩阵数值会有抖动,所以更信IoU@0.5的mAP曲线是否平稳上升。如果某两类始终互相混淆,就按第5章的思路合并类别。

数据增强在小样本场景下的作用比模型结构改动更明显。我一般保持yolo默认的mosaic和HSV增强,额外把scale打开,让模型见过不同尺度的猪。634张数据量下,模型结构越复杂越容易过拟合,花力气改transformer结构不如先把标注质量和数据划分做好。吃过几次亏之后,我养成了一个习惯:任何模型跑全量训练之前,先抽20张过一遍,把可能性都验证过再谈参数。这个习惯治好了我大半的“玄学调参病”,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询