十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

猪目标检测数据集实战:VOC/YOLO双格式标注与YOLOv5训练全流程

猪目标检测数据集实战:VOC/YOLO双格式标注与YOLOv5训练全流程 简介猪目标检测数据集包含634张左右已标注图片同时提供VOC和YOLO两种格式面向目标检测初学者及智慧养殖场景开发者。压缩包内共有1903个文件其中jpg原图634张、xml标注文件634个、txt标签文件635个整体大小约229.59MB。标注类别统一为pig使用labelImg工具完成遵循边界框准确、目标无遗漏及一致性检查等标注规范xml与txt分别对应VOC和YOLO训练要求。目前已有110人学习下载解压后三个文件夹分工明确图片与两类标注一一对应可直接用于YOLO、MMDetection等框架的模型训练与验证。此外图片为jpg格式单张1-500KB加载和预处理速度快既能作为猪只检测算法的入门数据集也可为数据标注质量控制提供参考。1. 先说清楚这批猪数据集到底值不值得下做目标检测的人大概率都碰到过一个尴尬局面模型结构调好了、训练脚本写好了结果数据集成了拦路虎。要么网上找的公开数据集类别太杂要么标注格式跟你用的框架对不上要么图片数量虚标解压完发现一半是废图。这份634张左右、VOC和YOLO双格式并存的猪目标检测数据集解决的正是这个痛点——图片全是整理过的猪舍场景标注是labelImg逐张框出来的xml和txt各634份直接对应不用再做格式转换解压后丢进yolov5或其他框架就能开始训练。适合谁用刚跑通yolo系列想拿真实数据练手的新手以及做养殖场景检测、需要单类别数据做预研或验证的算法工程师。先别急着解压后面几章我会把标注内容、格式细节、使用前检查、训练配置、踩坑记录和优化技巧全部摊开讲你花十分钟看完可以少走两三天弯路。2. 数据集内部结构文件命名规则与两种标注格式的真实内容2.1 解压后的目录长什么样压缩包解压后是三个平级文件夹这个组织方式从官方说明看是刻意保持的一个放jpg图片一个放xml标注一个放txt标注。对应关系靠文件名强匹配比如pig_10499.jpg对应pig_10499.xml和pig_10499.txt这个逻辑和labelImg保存时的命名习惯一致。实际打开目录后你能看到dataset/ ├── images/ # 634张jpg图片 ├── annotations_xml/ # 634个xml标注文件 └── annotations_txt/ # 634个txt标注文件三个文件夹缺一不可。如果你只想跑yolo系框架txt文件夹单独拷走就行如果要跑mmdetection、detectron2这类支持VOC格式的框架xml文件夹就是你的入口。我建议不要改动这个目录结构训练脚本里直接指向三个路径方便后续做数据校验。图片大小在1到500KB之间分辨率不统一是这类手工整理数据集的常态。这意味着训练时--img参数不能拍脑袋填640得提前统计一下图片的宽高分布后面避坑章我会详细说这个。类别只有一类就是pig所以txt文件里每个标注行的第一个数字永远是0不可能出现其他值。2.2 打开一个xml标注文件看内部字段labelImg生成的VOC格式xml长这样我随机挑了一个pig_10499.xml做说明annotation folderimages/folder filenamepig_10499.jpg/filename path/home/user/dataset/images/pig_10499.jpg/path source databaseUnknown/database /source size width800/width height600/height depth3/depth /size segmented0/segmented object namepig/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin120/xmin ymin85/ymin xmax520/xmax ymax450/ymax /bndbox /object /annotation重点看object块name是类别名pigbndbox四个值是目标左上角和右下角的绝对像素坐标。每张图片里有多少头猪就有多少个独立的object块。有一点要特别提醒path是标注机器上的绝对路径你下载到本地后这个路径必然失效但不用管它——所有检测框架读取VOC标注时只依赖folder、filename、size和object这几个字段path写错不影响训练。2.3 YOLO格式txt是归一化坐标对应同一张图片的pig_10499.txt内容是一行一个目标的归一化坐标0 0.400000 0.445833 0.500000 0.608333这行的五个数字含义是类别索引 中心点x_norm 中心点y_norm 宽度_norm 高度_norm。注意它不是VOC的xmin/ymin/xmax/ymax直接除以宽高而是先算出中心点坐标再做归一化。从上面xml的bndbox值换算一下就能验证x中心(120520)/2320除以宽800得到0.4y中心(85450)/2267.5除以高600得到0.445833完全吻合。labelImg在切换成YOLO模式保存时内部就是按这个公式自动算的。你在用这份数据训练时不需要手动验证每个txt但数据敏感受个检是必要的我会在第四章给你完整的校验脚本。有一点要记住xml和txt里目标个数完全一致、坐标严格对应但如果你后续用脚本动过任一份就必须重新同步否则训练时会出现检测框和图片内容对不上的情况。2.4 多目标图片的标注组织方式前面举例子的图片可能只有一头猪但猪舍场景里大部分图片不止一头。多目标的txt文件是多行每一行一个目标行的顺序没有明确规则labelImg记录的顺序取决于你画框的先后顺序。xml文件对应着多个object块。训练时框架的collate函数会把所有gt信息打包成tensor行顺序不影响结果。如果你打开txt发现某一行全是大数字或者负数说明这张图的标注坐标有异常常见原因包括图片尺寸规范化错误。遇到这种文件建议先剔除而不是强行修正后面避坑章会详细讲这个判断逻辑。3. 训练前必须做的数据体检交叉校验脚本和坐标换算3.1 哪些文件有资格进训练集拿到手的634张图片不能直接全部丢进images文件夹就开训。我训练自己的数据集前一般会先跑一遍完整的数据体检核三件事xml和txt文件是否一一对应、标注框坐标是否超出图片边界、图片尺寸是否和xml里size记录一致。任何一个环节出问题轻则丢数据重则loss跑飞、mAP为0还找不到原因。先写一个文件对应关系的检查脚本import os from pathlib import Path img_dir Path(images) xml_dir Path(annotations_xml) txt_dir Path(annotations_txt) img_files {p.stem: p for p in img_dir.glob(*.jpg)} xml_files {p.stem: p for p in xml_dir.glob(*.xml)} txt_files {p.stem: p for p in txt_dir.glob(*.txt)} # 检查三个集合是否完全一致 img_only img_files.keys() - xml_files.keys() - txt_files.keys() xml_only xml_files.keys() - img_files.keys() - txt_files.keys() txt_only txt_files.keys() - img_files.keys() - xml_files.keys() assert not img_only, f缺少标注: {img_only} assert not xml_only, f缺少图片: {xml_only} assert not txt_only, f缺少txt标注: {txt_only} print(f校验通过: {len(img_files)} 张图片对应 {len(xml_files)} 个xml和 {len(txt_files)} 个txt)这段脚本的逻辑是先把三个文件夹里的文件按主文件名stem分别收集成集合再用集合差运算找出「独有的文件名」。如果三个文件夹的主文件名集合完全一致差集为空断言通过。这里之所以用Path.stem而不是直接比对完整文件名是因为jpg、xml、txt的扩展名本来就不一致需要先剥离掉再比。然后检查标注框是否超出图片边界同时验证xml的size和实际图片尺寸是否一致import xml.etree.ElementTree as ET from PIL import Image img_dir Path(images) xml_dir Path(annotations_xml) for img_path in img_dir.glob(*.jpg): xml_path xml_dir / (img_path.stem .xml) tree ET.parse(xml_path) root tree.getroot() # 从xml读取记录尺寸 xml_w int(root.find(size/width).text) xml_h int(root.find(size/height).text) # 读取真实图片尺寸 with Image.open(img_path) as im: real_w, real_h im.size if (xml_w, xml_h) ! (real_w, real_h): print(f尺寸不一致: {img_path.name} xml{xml_w}x{xml_h} 实际{real_w}x{real_h}) continue # 检查每个目标框是否出界 for obj in root.findall(object): bbox obj.find(bndbox) xmin int(bbox.find(xmin).text) ymin int(bbox.find(ymin).text) xmax int(bbox.find(xmax).text) ymax int(bbox.find(ymax).text) if xmin 0 or ymin 0 or xmax real_w or ymax real_h: print(f出界框: {img_path.name} bbox({xmin},{ymin},{xmax},{ymax})) if xmax xmin or ymax ymin: print(f无效框: {img_path.name} bbox({xmin},{ymin},{xmax},{ymax}))这段代码对每张图做两类检查。尺寸比对的意义在于如果xml里的记录和真实图片宽高不一致YOLO格式txt的归一化坐标就会算错。分类别框有效性的逻辑是xmax xmin或ymax ymin说明这个框宽或高为0属于误操作产生的无效标注。如果跑完这段脚本后打印出大量问题建议优先用labelImg重新打开对应图片确认而不是直接删xml。3.2 VOC和YOLO坐标转换深度理解如果你后续要增减目标框、把数据集送给别人用总得自己动手改标注。到时候你会发现真正难的不是写转换函数而是搞懂两种格式背后的坐标系逻辑。看这个对照就清楚了坐标含义VOC(xml)YOLO(txt)左上角xxmin(绝对像素)无左上角yymin(绝对像素)无右下角xxmax(绝对像素)无右下角yymax(绝对像素)无中心点x无(xminxmax)/2 / width中心点y无(yminymax)/2 / height宽度xmax - xmin(xmax-xmin) / width高度ymax - ymin(ymax-ymin) / heightVOC存的是绝对像素值YOLO存的是归一化后的中心点加宽高。两者互相转换的唯一关键变量是图片原始宽高。所以在做VOC转YOLO时你必须在同一张图片被resize之前完成读取否则归一化就失真了。一个通用转换函数示例在训练流程里很实用def voc_to_yolo(xmin, ymin, xmax, ymax, img_w, img_h): VOC绝对坐标转YOLO归一化坐标 # 计算中心点绝对坐标 x_center_abs (xmin xmax) / 2.0 y_center_abs (ymin ymax) / 2.0 # 计算框宽高绝对像素值 box_w xmax - xmin box_h ymax - ymin # 除以图片宽高做归一化 x_center x_center_abs / img_w y_center y_center_abs / img_h width box_w / img_w height box_h / img_h # 加上类别索引单类数据集恒为0 return [0, round(x_center, 6), round(y_center, 6), round(width, 6), round(height, 6)]round到6位小数是惯例做法yolov5和yolov8对超小目标的坐标非常敏感精度不足会导致边框抖动。注意这个函数必须在没有resize的原图上计算如果你开了opencv的imread默认通道顺序不影响坐标计算但确认宽高用img.shape[1]和img.shape[0]别把H和W写反了——这个错误纯属手误但极难排查。4. 用这份猪数据集跑通YOLOv5训练目录组织与配置参数4.1 把目录结构改成yolov5能直接吃的格式虽然数据集本身包含VOC和YOLO两套标注但yolov5、yolov8系列更习惯用YOLO txt标注而且对目录结构有固定要求。我一般是把它重组成YOLO官方推荐的组织方式yolo_dataset/ ├── images/ │ ├── train/ # 图片训练集 │ └── val/ # 图片验证集 ├── labels/ │ ├── train/ # txt标注训练集 │ └── val/ # txt标注验证集 ├── data.yaml # 数据配置文件重组脚本如下我用的是按8:2比例随机划分import random from pathlib import Path import shutil src_img Path(images) src_txt Path(annotations_txt) dst_img_train Path(yolo_dataset/images/train) dst_img_val Path(yolo_dataset/images/val) dst_txt_train Path(yolo_dataset/labels/train) dst_txt_val Path(yolo_dataset/labels/val) for d in [dst_img_train, dst_img_val, dst_txt_train, dst_txt_val]: d.mkdir(parentsTrue, exist_okTrue) all_stems list(src_img.glob(*.jpg)) random.seed(42) random.shuffle(all_stems) val_cnt int(len(all_stems) * 0.2) val_stems all_stems[:val_cnt] train_stems all_stems[val_cnt:] for stem in train_stems: shutil.copy(stem, dst_img_train / stem.name) shutil.copy(src_txt / (stem.stem .txt), dst_txt_train / (stem.stem .txt)) for stem in val_stems: shutil.copy(stem, dst_img_val / stem.name) shutil.copy(src_txt / (stem.stem .txt), dst_txt_val / (stem.stem .txt)) print(ftrain: {len(train_stems)}, val: {len(val_stems)})random.seed(42)是为了保证每个epoch的划分结果可复现——同一个数据集用同一种子跑出来的数据分配完全一致方便不同模型之间的效果对比。20%的验证集比例对单类别数据集是够用的如果目标数很少可以适当提高到30%。这里用的是shutil.copy保留原始文件不动避免折腾坏原始数据。4.2 data.yaml的参数怎么填yolov5训练时会读取data.yaml内容直接对应数据集的类别和组织方式train: yolo_dataset/images/train val: yolo_dataset/images/val nc: 1 names: [pig]train和val填的是images目录的路径框架会自动找同级的labels目录把images替换成labels后缀改成.txt。所以如果你改组目录只要保持images/train和labels/trainimages/val和labels/val配对框架就不会找不到标注。nc是类别总数1对应这个数据集的单一类别。names数组里的索引要和txt里的第一个数字严格对应这份数据只有0对应就是pig。有几个容易踩的坑说在前面yaml文件里的路径不要用~yolov5不会自动展开环境变量如果报AssertionError: train: No labels in ...百分百是标注文件后缀或目录配对有问题如果训练时loss一直不降优先检查是不是有大量txt文件是空的——标注图像里偶尔漏标也是正常情况。4.3 训练参数选哪种比较稳我拿这份数据实际跑过的经验推荐一组数值给你参考python train.py \ --data data.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --cache--img 640默认resize到640x640如果图片本身分辨率差异大第一轮loss会偏高训练几个epoch后会收敛不用太焦虑--batch 16634张图的规模16的batch在8G显存上够用如果显存紧张降到8--epochs 100这个数据量100轮足够收敛mAP0.5大概能到0.85以上前提是标注质量没问题--cache第一次训练前把所有图片缓存到内存634张小图总占用不到1G极大减少IO等待如果你用的是yolov8命令基本等价yolo detect train datadata.yaml modelyolov8s.pt imgsz640 batch16 epochs100。参数含义一致就是写法不同。5. 避坑与常见问题排查我在这份数据上翻过的车5.1 训练loss不降反升现象用这份数据集跑yolov5s前几个epoch的box_loss不降mAP始终为0甚至训练直接报NaN。原因大概率是txt标注中出现了空文件或全0坐标文件。空文件意味着这张图片没有任何框但框架默认会当它是一张负样本参与训练全0坐标则会让anchor计算出现无穷值。解决在训练前用下面的脚本过滤掉无标注文件。我在第3章的体检脚本里故意没写这一步因为很多新手会忽略——先跑这个过滤再谈训练。from pathlib import Path txt_dir Path(labels/train) empty_files [] for txt in txt_dir.glob(*.txt): lines [l.strip() for l in txt.read_text().splitlines() if l.strip()] if not lines: empty_files.append(txt) else: for line in lines: parts line.split() if len(parts) ! 5: print(f格式错误: {txt.name}: {line}) empty_files.append(txt) break for f in empty_files: img Path(images/train) / (f.stem .jpg) print(f移除空标注: {img.name}) img.unlink(missing_okTrue) f.unlink(missing_okTrue)之所以连图片一起删是因为只删txt会导致images目录和labels目录文件名对不上yolov5训练时照样报错。5.2 验证集mAP高但实际检测效果稀烂现象训练完val mAP0.5有0.9但拿一张没见过的猪舍照片测试要么漏检要么框偏。原因数据划分时我用了随机划分没有按场景分导致train和val里可能包含同一场景的相似图片模型等于提前记住了答案。634张图里如果有大量来自同一段视频的连续帧随机划分一定会有这个隐患。解决按文件名序号做分桶划分。这份数据文件名带编号pig_10067、pig_10499编号接近的大概率是同一场景的连续帧。建议按编号排序后每10个抽2个做验证集验证集序号取%10 in [0,1]其余做训练集。这样验证集中的场景基本和训练集不重叠。脚本只需把第4章第1节里的random.seed(42)那段改为sorted_stems sorted(all_stems, keylambda x: int(x.stem.split(_)[1])) val_stems [p for i, p in enumerate(sorted_stems) if i % 10 in (0, 1)] train_stems [p for i, p in enumerate(sorted_stems) if i % 10 not in (0, 1)]keylambda x: int(x.stem.split(_)[1])是把pig_10499按_切成[pig, 10499]取第二个元素转成整数排序。这样划分出的val约127张10%加1%一共20%场景更分散val mAP会更真实。5.3 类别名大小写不一致现象训练时一切正常但推理后txt输出全部是pig而你的后续pipeline里全是小写pig或者反过来。原因labelImg标注时如果多人协作有人填了Pig有人填了pig生成的数据集里xml的name值就不一致。我在下载的这个版本里确认过全是小写pig但你自己后续增标数据时很容易破坏这个一致性。解决统一检查所有xml文件写个极简脚本grep -rh name annotations_xml/ | sort | uniq -c输出结果里如果出现两个不同的大小写组合就用sed统一替换。比如出现Pig就执行sed -i s/namePig\/name/namepig\/name/g annotations_xml/*.xml。txt文件里对应的类别索引也要同步检查。5.4 图片尺寸差异大导致训练不稳定现象训练前期loss一直大幅震荡收敛很慢。原因官方说明指出图片大小范围为1~500KB实际宽高比例极不统一。yolov5的--img 640是固定resize宽图会被压缩变形目标框也跟着扭曲。解决先跑一个统计脚本看每张图的宽高分布如果长宽比超过2:1的数量占比不到10%直接按640训练问题不大如果占比超过30%建议--img 416并开启--rect参数让同batch内的图片保持原始比例不拉伸。--rect的原理是按图片长宽比分桶同一桶内的图片共享一个resize尺寸减少形变。副作用是训练代码需要--rect配合--bucketbatch内图片尺寸不完全一致但yolov5支持这一点。5.5 目标过小导致AP为0现象mAP0.5显示为0.01这种离谱值查了一下训练集里确实有猪但模型就是学不出来。原因某些图片里猪在远处框只有几十像素甚至十几像素宽在640x640缩放下只有原图的1/10~1/20小目标信息基本丢失。这是634张小图里最常见的问题。解决这类小目标图建议在一开始就排查出来或者单独训练一个检测头。快捷做法是把所有框宽或高小于图片宽或高5%的样本打出来from pathlib import Path import xml.etree.ElementTree as ET xml_dir Path(annotations_xml) for xml_file in xml_dir.glob(*.xml): tree ET.parse(xml_file) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) for obj in root.findall(object): b obj.find(bndbox) bw int(b.find(xmax).text) - int(b.find(xmin).text) bh int(b.find(ymax).text) - int(b.find(ymin).text) if bw / img_w 0.05 or bh / img_h 0.05: print(f{xml_file.stem}: 小目标框 {bw}x{bh} 图片 {img_w}x{img_h})如果跑出来小目标框数量不多直接在第4章划分数据集时把这些xml从训练集里摘出去避免干扰模型学正常目标。如果小目标框数量多就得考虑用SAHI切片推理方案——把图片切成多个小图分别推理再合并结果这个方案对养殖场远距离监控场景特别有效。6. 让这份数据发挥更高价值数据增强与自动标注自举6.1 634张单类数据怎么扩到够用对单类别检测任务来说634张标注图如果直接训练正常能达到可用水平但如果你想提升泛化能力尤其是光照、角度、遮挡这些场景变化数据增强是性价比最高的手段。yolov5内置的增强已经很强--augment参数开启后能覆盖随机翻转、马赛克、HSV扰动等。不过更可控的方式是先用脚本做离线增强把增强后的图片和标注一起保存这样可以人工检查质量。我一般用albumentations做离线增强因为它的bbox变换能自动同步到增强后的标注上。一个合适的增强组合如下import albumentations as A transform A.Compose([ A.RandomBrightnessContrast(p0.5), A.HueSaturationValue(hue_shift_limit10, sat_shift_limit20, val_shift_limit10, p0.5), A.RandomGamma(gamma_limit(80, 120), p0.3), A.HorizontalFlip(p0.5), A.Rotate(limit15, border_mode0, p0.4), ], bbox_paramsA.BboxParams(formatpascal_voc, label_fields[labels]))这个组合的参数是支持目标检测的RandomBrightnessContrast模拟猪舍不同方向的灯光差异Rotate限制在±15度是因为猪舍里猪的朝向基本是水平的旋转角度过大会让框的形状失真边框更可能框到猪舍栏杆之类的东西。HorizontalFlip对检测目标完全对称所以p0.5很安全。执行增强时要注意一件事A.Rotate的border_mode0表示旋转后的空白区域用0填充黑色如果一张图里猪正好在角落旋转后可能被裁出画面此时albumentations会自动把对应bbox的坐标设为空的需要过滤掉这些失效标注。增强后的数据建议控制在2~3倍即1900张左右再多容易过拟合。6.2 用预训练模型反哺数据集标注634张标注数据还有一个进阶玩法——用它训练出一个初步模型再拿这个模型去标注新的无标注猪舍图片人工只负责校验模型输出。这就是自举式标注法。具体操作分三步第一步用第4章的流程正常训练出一个yolov5s模型。第二步收集1000张无标注猪舍图片跑一次推理python detect.py --weights runs/train/exp/weights/best.pt --source new_images/ --save-txt --save-conf--save-txt会把检测结果按图片名保存成txt文件格式和数据集txt一模一样包括类别索引、中心点坐标、宽高、置信度。第三步打开这些txt文件把置信度低于0.6的框删掉或调整坐标。因为检测模型输出的txt直接能被yolov5拿来训练省去了手工用labelImg画框的体力活。我实际操作时碰到过一个细节检测模型输出的坐标是相对模型输入尺寸比如640x640归一化的你把这些txt存回去时要考虑训练时用--img参数是不是同一个分辨率。如果图片原始分辨率和检测时的输入分辨率共用一个尺寸可以直接用不一致就得先还原绝对坐标再重新归一化。6.3 多场景验证和置信度阈值调节634张猪数据集的标注遵循原说明里提到的三个原则——边界准确、全目标覆盖、一致性检查。最终模型效果验证时不建议只跑一遍测试集看个mAP就收工。更合理的做法是准备三组实际场景图片一组光线正常的猪舍一组傍晚偏暗的一组有遮挡的分别测一下检测结果统计漏检和误检数。置信度阈值的选择有一些玄学成分。目标检测的--conf-thres从0.25调到0.5误检会下降但漏检会增加。对于猪这种目标不算特别小的场景我的经验是训练好的模型如果mAP在0.85以上推理阈值设0.35左右比较均衡既不会漏掉远处的小目标也不会把栏杆阴影误检成猪。这个调参过程没有统一公式多试几个值画PR曲线对比就好。从那以后我每次拿到一个新的标注数据集都强制走一遍同样的流程先跑体检脚本再按场景分桶划分最后用预训练模型做自举标注。坚持这个习惯之后踩坑的频率大幅下降。希望这份猪数据集的完整拆解对你有帮助也祝你的模型一遍训通。本文还有配套的精品资源点击获取
返回列表