☰
VOC转YOLO格式牛只数据集:转换、划分与校验实战指南
2026/9/28 18:48:45 网站建设 项目流程

简介:一套面向牛只检测模型训练的VOC/YOLO双格式数据集,包含241张JPG原图、241个XML标注文件和241个TXT标注文件,共725个文件,压缩包约84.43MB。所有标注均由labelImg工具人工绘制,类别统一为Cattle,共286个矩形框;数据集不包含分割路径,仅保留图像与对应标注,结构简洁,适合农业养殖监控、畜牧个体识别等场景,可直接用于YOLO、Faster R-CNN等主流目标检测框架。文件中还额外包含2个说明性txt文件,整体文件类型为243个txt、241个xml、241个jpg;目录按原始图像、VOC标注、YOLO标注分组,便于直接划分训练集与验证集。目前已有102人浏览学习,适合熟悉Pascal VOC与YOLO格式的开发者直接下载使用;免去自行标注的繁琐流程,数据量适中,单类别目标便于快速验证算法效果,还能以该数据为基础进行数据增强、迁移学习或模型评估等后续实验。

1. 拿到牛数据集VOC格式+yolo格式241张1类别的压缩包,先别急着训练

一个zip解压出来,images目录里躺着牛的图片,旁边是成套的标注:VOC格式的XML和YOLO格式的txt,一共241张、1个类别。这种命名风格在公开数据集里很常见,它解决的问题也很直接——你不需要从零标注,双份标注随取随用。但很多人第一反应是“格式都给好了,直接用就行”,真上手才发现,要么xml和txt对不上,要么类别ID顺序和预期不一样。这篇文章把前置功夫一次讲透:先看懂两份格式的区别,再手动转换、划分、校验,最后跑通一条最小训练命令。适合正在做智慧牧场、牛只盘点,或者刚入门目标检测、想找一份干净数据练手的人。

2. VOC格式和yolo格式,一张牛的图片两套标注哲学

任何目标检测标注,本质都在回答两个问题:图里有什么,在哪。VOC格式和YOLO格式给出的答案形式不同,但都能一一对应回去。搞清楚两套体系,后面转换和排错才有依据。

2.1 VOC格式的XML:人眼友好,像素坐标记录

PASCAL VOC比赛确立的XML标注结构,到今天仍被大量工具箱使用。打开这份牛数据集里任意一个xml文件,看到的基本是这个样子:

<annotation> <folder>images</folder> <filename>cattle_001.jpg</filename> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <object> <name>cattle</name> <bndbox> <xmin>215</xmin> <ymin>98</ymin> <xmax>1104</xmax> <ymax>684</ymax> </bndbox> </object> </annotation>

filename告诉你对应哪张图,size里的width和height是图片的真实宽高,depth固定为3表示RGB三通道。object节点就是每一个目标:name是类别名,bndbox里xmin、ymin、xmax、ymax是边界框的左上角和右下角像素坐标,坐标系原点在图片左上角,x向右增大,y向下增大。

VOC格式对人非常友好,因为你能直接读出“牛在画面哪个位置、框有多大”。不少VOC标注里还带difficult、truncated、pose这类字段,用来标记难例、截断和姿态,很多开源数据导出时直接省略或置0。这份数据只保留bndbox也正常,你写解析脚本时不要默认这些字段一定存在,否则容易KeyError。

2.2 yolo格式的txt:归一化到0-1的四位数字

同样一张图,打开对应的txt文件,只有一行:

0 0.515234 0.543056 0.695313 0.813889

五个数字用空格分隔:第一个是类别ID,这份数据集只有1个类别,所以全是0;后面四个是归一化坐标——x_center、y_center、width、height。用2.1里XML的数值换算一下就能对上:

x_center = (215 + 1104) / 2 / 1280 = 0.515234 y_center = (98 + 684) / 2 / 720 = 0.543056 width = (1104 - 215) / 1280 = 0.695313 height = (684 - 98) / 720 = 0.813889

注意yolo格式的txt里没有图片尺寸信息,尺寸被“归一化”这三个字吃掉了。正因为所有坐标都被缩放到0到1之间,同一份标注在不同分辨率图片上可以直接迁移,这也是YOLO系框架图片输入尺寸可以随意resize的前提。代价是txt必须和图片配套使用,单独拎出来看不出任何含义。

2.3 为什么数据集要同时给两份格式

VOC系工具链(mmdetection、Detectron2、LabelImg这些)直接吃XML,而YOLO官方系列框架只认txt。一份数据集同时导出两种格式,拿到手就可以切进任意工具链,省掉换框架时重写转换的功夫。对学习者来说,双格式还是一份天然的对照样例:你可以拿同一张图,自己手算一遍VOC转YOLO,再和zip里现成的txt比对,验证自己理解对不对。

对比项VOC格式yolo格式
文件扩展名.xml.txt(与图片同名)
边界框表达xmin/ymin/xmax/ymax 像素坐标x_center/y_center/w/h 归一化浮点
类别表达name 字符串类别ID整数
是否依赖图片尺寸是是(换算才可还原)
常见工具链mmdetection、Detectron2、LabelImgYOLO全系、Roboflow导出

提示:判断一份txt是否合法,先看第一列类别ID是否小于names长度,再看后四列是否都在0到1之间并满足width/height为正。这两条能过滤掉大部分脏数据。

3. 把VOC格式转成yolo格式:转换脚本与坐标换算的三个关键参数

拿到双份格式,第一反应是直接用txt。但实际项目里经常出现txt缺失、txt内容错乱、或者你只需要其中某些类别的情况。与其赌数据没问题,不如自己写一次VOC转yolo的转换脚本,能手动生成标注,后面任何格式问题都能自己修。

3.1 为什么还要写转换脚本

常见做法是先用一份现成脚本跑通,再根据数据特点改参数。这份牛数据集只有1个类别241张图,转换逻辑很简单,但背后有三步核心操作绕不开:解析XML拿bndbox、读图片拿真实尺寸、按类别字典把name映射为ID后做归一化。任何一步出错,生成的txt都是废的。

自己写脚本的另一个好处是可控性。zip里给的txt如果本身就是错的呢?你拿它训练出问题,连排查方向都没有。而用脚本从VOC重新生成一遍yolo标签,整个过程可复现、可审计,出问题能定位到具体是哪张图哪个框。

3.2 一份能直接跑通的convert_voc2yolo.py

下面这个脚本是我处理这类双格式数据集时最常用的版本,兼容jpg/jpeg/png混合目录,并且强制用PIL读图片真实尺寸,不信XML里的size节点:

import os import argparse import xml.etree.ElementTree as ET from PIL import Image CLASS_DICT = {"cattle": 0} # 类别名 -> 类别ID,单类别固定为0 IMG_EXTS = (".jpg", ".jpeg", ".png") # 需要探测的图片后缀 def find_image(xml_file, img_dir): base = os.path.splitext(os.path.basename(xml_file))[0] for ext in IMG_EXTS: path = os.path.join(img_dir, base + ext) if os.path.exists(path): return path raise FileNotFoundError(f"cannot find image for {xml_file}") def convert_one(xml_file, img_dir, label_dir): tree = ET.parse(xml_file) root = tree.getroot() img_path = find_image(xml_file, img_dir) with Image.open(img_path) as im: W, H = im.size # 用真实读到的尺寸,不信任XML里的size lines = [] for obj in root.findall("object"): name = obj.findtext("name").strip() if name not in CLASS_DICT: 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 / W y_center = (ymin + ymax) / 2.0 / H w = (xmax - xmin) / W h = (ymax - ymin) / H lines.append(f"{CLASS_DICT[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") if lines: # 没有目标就不生成空txt label_path = os.path.join( label_dir, os.path.splitext(os.path.basename(xml_file))[0] + ".txt" ) with open(label_path, "w") as f: f.write("\n".join(lines) + "\n") def main(): parser = argparse.ArgumentParser() parser.add_argument("--xml_dir", required=True) parser.add_argument("--img_dir", required=True) parser.add_argument("--label_dir", required=True) args = parser.parse_args() os.makedirs(args.label_dir, exist_ok=True) for xml_file in sorted(os.listdir(args.xml_dir)): if xml_file.endswith(".xml"): convert_one(os.path.join(args.xml_dir, xml_file), args.img_dir, args.label_dir) if __name__ == "__main__": main()

逻辑说明:脚本核心就是convert_one函数,解析XML拿到name和bndbox,再读图片真实尺寸做归一化,拼成一行YOLO格式文本。注意我特意写了if lines:来避免生成空txt——空标注文件在YOLO训练时会让数据加载器报警告,属于典型的“晚炸不如早炸”。如果某张图确实没有目标,让它暴露在校验阶段再处理,而不是在转换阶段悄悄生成坏文件。

参数说明三个地方:CLASS_DICT控制哪些类别进训练、映射到哪个ID;IMG_EXTS决定怎么找同名图片;输出的六位小数精度,足够训练使用。运行命令形如:

python convert_voc2yolo.py --xml_dir ./annotations --img_dir ./images --label_dir ./labels_txt

3.3 三个必调参数:类别字典、图片后缀、坐标精度

类别字典是最容易出问题的参数。这份数据标题写1类别,但解压后你最好自己确认XML里name字段到底是不是统一的“cattle”。有的公开数据集会把cow、bull、buffalo混着标,虽然生物学上都算牛,但它们不是同一个字符串。处理方式是先统计所有XML里的name集合,再统一映射,比如{"cattle": 0, "cow": 0, "bull": 0}把它们归为同一类。千万别只写了{"cattle": 0},然后发现XML里还有几十张cow被静默跳过了,训练时莫名其妙少数据。

图片后缀看似小事,但jpg、jpeg、png混用的情况十分常见。脚本里的find_image函数按顺序探测三个后缀,就是为了避免“XML存在,但同名图片找不到”的尴尬。如果你确定这份数据全是jpg,也可以只留.jpg,多探测一次只是多了几次磁盘stat,开销可忽略。

坐标精度建议保留至少6位小数。有人图省事写成整数,归一化坐标直接变阶梯状,模型预测的框会跟着抖动,尤其对小尺寸目标影响明显。浮点运算下多几位小数不占空间,没必要做取整。

4. 训练前划分与校验:241张1类别的数据集怎么喂给YOLO

转换出txt还不够,YOLO训练要求特定的目录组织方式:图片和标注分成train/val两份,标注文件和图片文件名完全一致,扩展名不同。这一步做得草率,后面训练报错、验证集泄漏都会来。

4.1 按YOLO的目录习惯组织train和val

先看目标目录结构,这是ultralytics系列框架的默认约定:

cattle_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── cattle_dataset.yaml

注意images和labels是兄弟目录,train下是图片,labels/train下是对应的txt。图片文件名是cattle_001.jpg,标注就必须是cattle_001.txt,靠文件名stem一一对应。框架读取时不会做模糊匹配,文件名对不上就直接跳过或报错。

这份数据只有241张,train/val比例我一般用8:2或者7:3。一类别检测任务类别简单,190张训练图加50张验证图已经能看出模型基本能力,没必要留更多验证集。划分前记得先建立cattle_dataset.yaml:

path: /absolute/path/to/cattle_dataset train: images/train val: images/val names: 0: cattle

注意:path推荐写绝对路径。相对路径在ultralytics不同版本里解析规则有差异,换成绝对路径一劳永逸。

4.2 固定随机种子,一次写好划分脚本

241张图,手动复制都能完成,但为了可复现,我用一段固定随机种子的脚本做自动划分:

import random import shutil from pathlib import Path random.seed(42) # 固定种子,保证每次运行结果一致 src_img = Path("images") # 转换出来的原始图片目录 src_lbl = Path("labels_txt") # 转换出来的yolo标注目录 out_img = Path("cattle_dataset/images") out_lbl = Path("cattle_dataset/labels") for sub in ("train", "val"): (out_img / sub).mkdir(parents=True, exist_ok=True) (out_lbl / sub).mkdir(parents=True, exist_ok=True) # 收集所有图片stem,按后缀补充,避免漏掉 names = sorted({p.stem for p in src_img.glob("*.jpg")} | {p.stem for p in src_img.glob("*.jpeg")} | {p.stem for p in src_img.glob("*.png")}) random.shuffle(names) split = int(len(names) * 0.8) for i, name in enumerate(names): sub = "train" if i < split else "val" img_candidates = list(src_img.glob(name + ".*")) if not img_candidates: print(f"image missing: {name}") continue img_src = img_candidates[0] lbl_src = src_lbl / f"{name}.txt" if not lbl_src.exists(): print(f"label missing: {name}") continue shutil.move(str(img_src), str(out_img / sub / img_src.name)) shutil.move(str(lbl_src), str(out_lbl / sub / lbl_src.name))

逻辑说明:先用glob按三种后缀收集所有图片stem,转成set再排序,天然去重,不会出现同一张图被分进两个集合的情况。random.shuffle(names)打乱顺序,之后按8:2切分,前80%进train,后20%进val。这里用shutil.move而不是copy,目录干净,也避免训练时扫描到重复文件。

参数说明:random.seed(42)是关键,42换成别的数字不影响结果,但固定种子能保证你调模型参数时,train/val组成不变,实验可对比。如果你希望val里有特定场景的牛(比如全都是远距离小目标),不要用随机划分,按场景分层抽样更合适——但241张的数据量通常不值得这么做。

4.3 校验脚本:坐标越界和空标签没有后悔药

划分后先别急着开训练,花两分钟跑一遍校验脚本,能拦下绝大多数翻车现场:

from pathlib import Path for txt in Path("cattle_dataset/labels").rglob("*.txt"): if txt.stat().st_size == 0: print(f"EMPTY {txt}") # 空标签文件,训练时会出问题 continue for line in txt.read_text().strip().splitlines(): parts = list(map(float, line.split())) if len(parts) != 5: print(f"BAD FORMAT {txt}: {line}") continue cls, x, y, w, h = parts if cls != 0: print(f"BAD CLS {txt}: {cls}") # 类别ID超出预期 if not (0 < x < 1 and 0 < y < 1 and 0 < w <= 1 and 0 < h <= 1): print(f"BAD BOX {txt}: {line}") if x - w / 2 < 0 or x + w / 2 > 1 or y - h / 2 < 0 or y + h / 2 > 1: print(f"OUT OF RANGE {txt}: {line}")

逻辑说明:rglob("*.txt")递归扫描labels下的所有标注,第一层检查空文件,第二层检查格式和类别ID,第三层检查坐标数值范围,第四层检查边界框是否超出图片边界。

参数理解上要注意:归一化坐标的合法条件是中心点在(0,1)内,宽高在(0,1]内。x - w/2是框左边界的坐标,不能小于0,x + w/2是右边界,不能大于1。浮点运算有极微小误差,偶尔出现1.0000001这种值可以容差处理,但如果出现x_center=0.7, w=0.8这种组合(左右边界全越界),说明转换时图片尺寸读错了,必须回头查XML和图片。

5. 避坑指南:VOC和YOLO双格式牛数据集最容易翻车的五个现场

处理过几套公开数据集之后,你会发现“人间真实”比想象中多。以下五个问题是我在高分率数据里反复踩过的,按常见程度排序。

5.1 labels里突然出现0字节文件

现象:跑完转换脚本或划分脚本,发现labels目录下有些txt是0字节,训练时数据加载器报警告,甚至直接跳过导致一个epoch有效样本变少。

原因:原始XML里有object节点但bndbox缺字段,或者某些图确实没有牛但XML为空节点。转换脚本如果无脑按“只要有XML就生成txt”,就会产出空文件。

解决:转换脚本里用if lines:判断,没目标就不生成txt。已经出现的空txt直接删除,同时删除对应的图片,避免训练时加载到不完整的样本对。删除前先统计有多少张,超过总数5%就得回头检查XML数据质量。

5.2 类别ID对不上:训练正常,预测全是错的

现象:loss正常下降,验证指标也还行,但推理出来框的类别名显示为“1”或者完全不在names里。用混淆矩阵看更是乱七八糟。

原因:VOC里name是字符串,yolo里类别是整数ID,ID的顺序由转换脚本的类别字典决定,不是自动按字母排序。比如有人字典写{"person": 0, "cattle": 1},过滤掉person后忘了重映射,所有牛的ID就变成了1,而yaml里names可能只有{0: cattle}。

解决:训练前打印所有txt首列的set,确认和yaml里的names索引完全对应。单类别数据必须全部是0,这是最保险的状态。转换脚本里直接把不需要的类别过滤掉,而不是保留ID后手动改名,否则迟早出bug。

5.3 XML的size节点不可信:坐标没错,框却偏了

现象:把VOC坐标画在图片上,框整体朝左上或右下偏移,背景被框进来,牛身反而只占一半。

原因:标注工具记录XML时的size和图片实际尺寸不一致。常见于数据被人resize过但XML没更新,或者标注时用的是一张缩放预览图,最终导出的原图分辨率对不上。

解决:转换时用PIL/OpenCV读取图片真实宽高,完全忽略XML里的size字段。这也是第3章脚本里坚持Image.open(img_path).size的原因。如果你发现很多框边缘紧贴图片边界,多半也是这个原因。

5.4 文件名对不齐:图片找得到,标注找不到

现象:划分脚本输出一堆“label missing”,或者训练时报错找不到某张图的txt。手动去目录里看,图片在,txt也在,就是名字差一点。

原因:图片是cattle_001.JPG大写后缀,而glob写的是*.jpg;或者xml里的filename字段写的是CATTLE_001.JPG,实际文件名是cattle_001.jpg。Windows和Linux大小写敏感差异也会放大这个问题。

解决:拿到数据先做一次文件名规范化,统一转小写、统一拓展名为jpg。脚本里用p.stem取文件名主体,不要依赖xml里的filename字段,因为那字段可能和实际文件名脱节。241张的数量级,手工正则一把梭都来得及。

5.5 归一化坐标的边界条件失效

现象:校验脚本报“OUT OF RANGE”,牛的框右边界算出1.03,或左边界算出-0.01。画出来就是一只被切了一半的牛。

原因:手标数据里边界框本身就可能贴着图边缘,甚至略微越界。VOC转yolo时除以图片宽高后,越界量被缩小但不会消失,边界框中心点如果等于0,x - w/2必然小于0。

解决:校验脚本发现越界后,根据越界量决定处理方式。越界小于0.01的,直接裁到0或1,保留这张图;越界大的,检查是不是XML坐标本身写错,比如xmax写成了width+20。大目标上这点越界通常不影响训练,但置之不理会让loss曲线在后期出现诡异抖动。

6. 用241张牛数据快速验证:最小训练命令与增强策略

数据准备好不代表要立刻堆大模型。241张单类别数据,最适合的路径是先用轻量模型跑出一个可用的基线,再判断要不要加数据、调增强。

6.1 训练命令与结果怎么读

yolo train data=cattle_dataset.yaml model=yolov8n.pt epochs=80 imgsz=640 batch=16

选yolov8n而不是s或m,是因为数据量小,轻量模型不容易过拟合,训练速度快,单卡几分钟一轮。epochs给80,对241张图足够模型反复看十几遍,再多就会开始死记训练集。imgsz用640,牛在画面里通常占比较大,不需要为了小目标强行拉高分辨率。batch=16在8G显存内基本够用。

跑完后重点看两个数:mAP50和mAP50-95。单类别、241张、目标是大尺寸的牛,mAP50如果能到0.6以上,说明这份数据可以支撑一个可用的检测器;mAP50-95和mAP50差距过大,则说明框的位置精度不够,优先怀疑标注框质量而不是模型。同时留意训练集和验证集的loss曲线,val loss不降反升就是过拟合信号,此时回退epochs或者增强强度。

6.2 数据增强的度与外部数据补充

241张图做数据增强是必要的,但别把增强开满。ultralytics默认的mosaic对这份数据帮助很大,因为牛的背景多样性不足,mosaic能模拟不同环境组合;而大幅旋转对牛这种四足动物就不合适——牛倒过来就不像牛了。常见的设置是轻度水平翻转、微小的HSV扰动、mosaic概率0.5就够,不需要引入复杂自定增强。

外部数据是241张数据提升性能最直接的路。常见做法是把这份VOC和yolo双格式数据,和牧场公开的牛只检测数据合并,合并前用第3章的转换脚本统一格式,再用第4章的划分脚本重新划分。这样训练出来的模型泛化性会明显好于单独训练,而且你能直观对比出“数据量从241涨到500+之后,mAP涨了多少”。把这一条经验记下来,以后看到任何“几百张1类别”的数据集,你都该先想合并策略,而不是指望单份数据出奇迹。

我在这类单类别小数据集上栽过一次跟头:当时没做第4.3的校验,x_center和y_center的位置写反,模型跑到第30轮都没发现,最后画可视化才追回。从那以后,转换、划分、校验三个脚本固定成流程,顺序执行,缺一不可。这份牛数据集最省力的路径也基本是这样三步,希望你开局就比我顺利。

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

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

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

立即咨询