
简介一套面向牛只检测模型训练的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 folderimages/folder filenamecattle_001.jpg/filename size width1280/width height720/height depth3/depth /size object namecattle/name bndbox xmin215/xmin ymin98/ymin xmax1104/xmax ymax684/ymax /bndbox /object /annotationfilename告诉你对应哪张图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(fcannot 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, requiredTrue) parser.add_argument(--img_dir, requiredTrue) parser.add_argument(--label_dir, requiredTrue) args parser.parse_args() os.makedirs(args.label_dir, exist_okTrue) 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控制哪些类别进训练、映射到哪个IDIMG_EXTS决定怎么找同名图片输出的六位小数精度足够训练使用。运行命令形如python convert_voc2yolo.py --xml_dir ./annotations --img_dir ./images --label_dir ./labels_txt3.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.yamlpath: /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(parentsTrue, exist_okTrue) (out_lbl / sub).mkdir(parentsTrue, exist_okTrue) # 收集所有图片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(fimage missing: {name}) continue img_src img_candidates[0] lbl_src src_lbl / f{name}.txt if not lbl_src.exists(): print(flabel 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(fEMPTY {txt}) # 空标签文件训练时会出问题 continue for line in txt.read_text().strip().splitlines(): parts list(map(float, line.split())) if len(parts) ! 5: print(fBAD FORMAT {txt}: {line}) continue cls, x, y, w, h parts if cls ! 0: print(fBAD CLS {txt}: {cls}) # 类别ID超出预期 if not (0 x 1 and 0 y 1 and 0 w 1 and 0 h 1): print(fBAD BOX {txt}: {line}) if x - w / 2 0 or x w / 2 1 or y - h / 2 0 or y h / 2 1: print(fOUT OF RANGE {txt}: {line})逻辑说明rglob(*.txt)递归扫描labels下的所有标注第一层检查空文件第二层检查格式和类别ID第三层检查坐标数值范围第四层检查边界框是否超出图片边界。参数理解上要注意归一化坐标的合法条件是中心点在(0,1)内宽高在(0,1]内。x - w/2是框左边界的坐标不能小于0x w/2是右边界不能大于1。浮点运算有极微小误差偶尔出现1.0000001这种值可以容差处理但如果出现x_center0.7, w0.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里类别是整数IDID的顺序由转换脚本的类别字典决定不是自动按字母排序。比如有人字典写{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时除以图片宽高后越界量被缩小但不会消失边界框中心点如果等于0x - w/2必然小于0。解决校验脚本发现越界后根据越界量决定处理方式。越界小于0.01的直接裁到0或1保留这张图越界大的检查是不是XML坐标本身写错比如xmax写成了width20。大目标上这点越界通常不影响训练但置之不理会让loss曲线在后期出现诡异抖动。6. 用241张牛数据快速验证最小训练命令与增强策略数据准备好不代表要立刻堆大模型。241张单类别数据最适合的路径是先用轻量模型跑出一个可用的基线再判断要不要加数据、调增强。6.1 训练命令与结果怎么读yolo train datacattle_dataset.yaml modelyolov8n.pt epochs80 imgsz640 batch16选yolov8n而不是s或m是因为数据量小轻量模型不容易过拟合训练速度快单卡几分钟一轮。epochs给80对241张图足够模型反复看十几遍再多就会开始死记训练集。imgsz用640牛在画面里通常占比较大不需要为了小目标强行拉高分辨率。batch16在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轮都没发现最后画可视化才追回。从那以后转换、划分、校验三个脚本固定成流程顺序执行缺一不可。这份牛数据集最省力的路径也基本是这样三步希望你开局就比我顺利。本文还有配套的精品资源点击获取