news 2026/9/28 16:57:51

瓶子数据集双格式解析:VOC与YOLO标注转换及训练校验全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
瓶子数据集双格式解析:VOC与YOLO标注转换及训练校验全流程

简介:瓶子目标检测数据集共收录4500张真实场景图片,提供Pascal VOC与YOLO两种格式标注,标注类别仅bottle一个,总标注框数12790个,由labelImg人工绘制矩形框完成,标注规则简洁且框位准确。面向需要训练瓶子检测模型的计算机视觉开发者、算法工程师与科研人员,既适用于检测模型入门实践,也可作为格式转换与算法对比的基准数据集。下载资源为7z压缩包,共约2000个文件,其中以1999个VOC格式XML标注文件为主,并附说明文档,整体体积约664MB。标注信息已经规范化整理,可直接导入YOLO、SSD等主流目标检测框架,省去手动标注和格式转换的时间。目前已有590人学习下载,适合需要高质量瓶子样本进行模型训练、验证与部署应用的场景。

1. 拿到这个瓶子数据集,先别急着喂给 YOLO

做目标检测绕不开数据集。前几天我拿到一套瓶子数据集,总共4500 张jpg 图片,每张图都配了VOC 格式的 xml 标注文件和YOLO 格式的 txt 标注文件,标注类别只有 bottle 一类,框数统计下来是12790 个。乍看是套能直接开工的数据,但我清点完文件后反而提醒自己:这种双格式数据集,最怕的就是 xml 和 txt 对不上、类别编号错位、或者图片尺寸和标注框坐标不一致。这套数据集用 labelImg 画矩形框标注,类别单一、框数充足,特别适合做检测入门练手、做迁移学习的底料,或者拿来验证自己的训练流程是否跑得通。本文我会把目录结构、标注格式、训练配置、质量校验和踩坑记录都拆开讲,读完后你能照着走一遍完整的验收流程。

2. VOC 与 YOLO 双格式标注:先搞懂文件里的坐标体系

2.1 数据集目录长什么样

这套数据集解压后,根目录下就是一堆 jpg 图片、xml 文件和 txt 文件混在一起。文件名像 xyxr_bottle_2847.xml、xyxr_bottle_3111.xml 这样,图片和标注用同一个主文件名,只是扩展名不同。我用 Python 跑了一遍统计,得到这样的文件结构:

$ ls | wc -l 13500 $ ls *.jpg | wc -l 4500 $ ls *.xml | wc -l 4500 $ ls *.txt | wc -l 4500

13500 个文件,正好等于 4500×3。这说明每张图片的标注在两个体系里各有一份,没有缺失。xml 是 Pascal VOC 标准,txt 是 YOLO 标准,两者描述同一个标注框,但坐标表达方式完全不同。拿到任何数据集,我第一步一定是先做这个数量核对,文件数对不上后面全是坑。

除了这些文件,目录里通常还有一个说明.txt,里面记录了标注类别、框数等元信息。训练前先把这份说明读一遍,它相当于数据集的"体检报告",告诉你这个数据集承诺了什么、不承诺什么。

2.2 VOC 的 xml 文件:左上角和右下角的绝对值

挑一个 xml 文件看看,它的结构对做过检测的人应该不陌生。我用文本编辑器打开 xyxr_bottle_123.xml,内容大致如下:

<annotation> <folder>xyxr_bottle</folder> <filename>xyxr_bottle_123.jpg</filename> <path>/home/user/dataset/xyxr_bottle_123.jpg</path> <source> <database>Unknown</database> </source> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <segmented>0</segmented> <object> <name>bottle</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>127</xmin> <ymin>103</ymin> <xmax>384</xmax> <ymax>451</ymax> </bndbox> </object> </annotation>

重点看<size>和<bndbox>这两块。<width>和<height>是图片的原始像素尺寸,<bndbox>里四个值是矩形框的左上角坐标(xmin, ymin)和右下角坐标(xmax, ymax),全部是整数像素值,相对于图片左上角为原点。这张图里瓶子框占了 127 到 384 像素的横向区间、103 到 451 像素的纵向区间,是一个偏大且靠近画面中间的框。一个 xml 里可以有多个<object>,每个 object 对应一个矩形框,所以 xml 文件数等于图片数,但框数远大于图片数,平均一张图约 2.84 个框。

把这个 xml 读进 Python 也很直接,用 xml.etree.ElementTree 遍历所有 object 就行。关键是把 xmin、ymin、xmax、ymax 四个整数取出来,同时把 size 里的宽高也取出来,后面转 YOLO 格式要用。

2.3 YOLO 的 txt 文件:归一化中心点加宽高

再看同名的 txt 文件,YOLO 格式每行描述一个框,共五列。用 cat 命令直接看:

$ cat xyxr_bottle_123.txt 0 0.399219 0.576042 0.401562 0.725000

第一列是类别编号,因为只有一个类别 bottle,所以这里恒为 0。后面四列分别是归一化后的中心点 x 坐标、中心点 y 坐标、归一化宽 w、归一化高 h。归一化的方式是除以图片原始宽高,比如 xmin=127、图片宽 640,那中心点 x 就是(127 + 384) / 2 / 640 = 0.39921875,约等于 0.399219。y 方向同理,用高度 480 做分母。这套坐标系是 0 到 1 的相对坐标,不依赖图片具体像素尺寸,训练时不管输入图片被缩放到多大,标注框都能跟着等比缩放。

我每次拿到双格式数据集,都会随机抽几个文件、手工验算一遍这个转换关系,确认 xml 和 txt 描述的是同一个框。这个验算很快,但能挡掉很多后续训练时的玄学问题,比如 loss 异常大、mAP 为 0 之类。

2.4 两种格式互相转换的计算逻辑

既然有双格式,最常做的事就是相互转换,或者校验两边是否一致。VOC 转 YOLO 的公式是固定的,我一般用一个短脚本做全量转换并校验:

import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, txt_path): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.findall('object'): cls_name = obj.find('name').text # 数据集只有 bottle 一类,映射到编号 0 cls_id = 0 if cls_name == 'bottle' else -1 bnd = obj.find('bndbox') xmin = int(bnd.find('xmin').text) ymin = int(bnd.find('ymin').text) xmax = int(bnd.find('xmax').text) ymax = int(bnd.find('ymax').text) 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 lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(txt_path, 'w') as f: f.write('\n'.join(lines)) # 用法示例:对指定文件做转换 voc_to_yolo('xyxr_bottle_123.xml', 'xyxr_bottle_123.txt')

这段代码的核心是先取图片宽高,再对每个 object 计算中心点和宽高的归一化值。f-string 里保留六位小数,精度足够 YOLO 训练使用。如果你要换成自己的类别映射表,把cls_id那里改成你数据集的类别字典就行。写完后随机抽三个文件,把生成的 txt 和原 txt 对比一下,浮点数误差在1e-4以内都算正常,超过这个范围说明两边标注本身对不上。

3. 训练前的一顿操作:划分数据集与生成配置

3.1 按 8:1:1 划分训练、验证、测试集

拿到数据集不能直接整个丢给训练脚本,要先按经验比例切分。我用 80% 训练、10% 验证、10% 测试,这是检测任务里比较稳的默认值。划分的原则是按主文件名来分,同一张图的 jpg、xml、txt 必须进入同一个集合,不能拆散。

反例是有人直接把所有图片和标注拷进一个目录,然后随机分配文件名,结果验证集和训练集数据泄漏,评估指标虚高。正确的做法是从完整文件列表里随机挑出名字,再按名字去找三种格式的文件。我用 Python 脚本实现:

import os import random random.seed(2024) all_files = [f for f in os.listdir('.') if f.endswith('.jpg')] random.shuffle(all_files) n = len(all_files) train_names = all_files[:int(n * 0.8)] val_names = all_files[int(n * 0.8):int(n * 0.9)] test_names = all_files[int(n * 0.9):] for split, names in zip(['train', 'val', 'test'], [train_names, val_names, test_names]): os.makedirs(split, exist_ok=True) for fname in names: base = fname.replace('.jpg', '') for ext in ['.jpg', '.xml', '.txt']: src = base + ext dst = os.path.join(split, base + ext) os.rename(src, dst)

seed 固定成 2024,保证每次划分结果一致,复现实验时不会因为数据分布变动导致指标对比失真。划分比例可以按实际需要调,如果样本少,可以减少测试集比例,把这部分并到验证集里。我一般保证训练集占比不低于 70%,验证集不低于 10%,太少的话评估结果的抖动会很大。

3.2 类别文件与训练入口配置

YOLO 系训练需要一份类别文件,文件名通常叫 classes.txt 或 dataset.yaml,内容就是把所有类别名列出来,一行一类。这个数据集只有一个类别,文件里就是一行:

$ cat classes.txt bottle

注意类别顺序和编号的对应关系。训练脚本读取 classes.txt 时,第一个名字对应编号 0,第二个对应编号 1,依次类推。这个数据集只有 bottle 一个类,它一定是 0,但如果你以后往里面加类别,必须保证新增类别接在最后,不能插到中间,否则已标注的 txt 编号就全乱了。

在 YOLOv5 或 YOLOv8 的配置里,YAML 文件这样写:

# dataset.yaml path: ./dataset train: images/train val: images/val test: images/test nc: 1 names: ['bottle']

nc必须和 names 列表长度一致,这里 nc=1,names 里只有一个 bottle,写反了或者多写了都会在训练时报维度错误。如果你把图片放到了 train/images 这样的子目录,path 和 train 的拼接要确认正确,我习惯的做法是让 path 指向最外层目录,train、val、test 用相对路径,避免绝对路径换机器后失效。

3.3 不用做数据增强但最好做预处理

这个数据集一共 12790 个框、平均每张图快 3 个框,密度不算低,训练时一般不需要强行加马赛克增强来凑样本。但预处理有一件事必须做:检查所有图片能否正常解码。有些下载来的数据集里混着损坏图片,训练跑到一半读图失败,整个进程中断。我在划分完集合后,会先用 PIL 或 OpenCV 扫一遍:

import os from PIL import Image for root, dirs, files in os.walk('.'): for f in files: if f.endswith('.jpg'): path = os.path.join(root, f) try: img = Image.open(path) img.load() except Exception as e: print(f'corrupted image: {path} -> {e}')

把输出定向到文本文件里,逐条看损坏图片的名字,决定是删除对应的三种格式文件,还是找替代图补上。注意如果真的删了图片,必须把同样主文件名的 xml 和 txt 也一起删掉,否则训练脚本读取图片列表时会出现索引错位,这是新手最容易翻车的地方。

4. 全量校验脚本:验证 4500 张图的标注是否靠谱

4.1 检查坐标是否越界

标注质量直接决定训练结果的天花板。虽然说明文件声称"提供准确且合理标注",但我拿到任何数据集都不会轻信这句话,一定要自己做全量校验。YOLO 格式的归一化坐标有个硬性约束:中心点 x、y 和宽 w、高 h 都必须在 0 到 1 之间。越界到 1.2 这种值,要么是标注时框拉出了图片边界,要么是转换脚本算错了分母。

我写了一个全量检查脚本,遍历所有 txt:

import os bad_files = [] for f in os.listdir('.'): if not f.endswith('.txt'): continue with open(f) as fh: for line in fh: parts = line.strip().split() if len(parts) != 5: bad_files.append((f, 'column count not 5')) continue cls_id, xc, yc, w, h = map(float, parts) if not (0 <= xc <= 1 and 0 <= yc <= 1 and 0 <= w <= 1 and 0 <= h <= 1): bad_files.append((f, f'out of range: {parts}')) if bad_files: for item in bad_files[:20]: print(item) else: print('all txt annotations are valid')

这个脚本把越界和列数不对的文件都揪出来。列数不对的情况其实不少见,有人导出 YOLO 格式时带了多余空格或者换行符异常,导致 split 后字段错位。坐标越界最常见的原因是标注框超出了图片边缘,labelImg 里画框时可以拖到图片外,保存时框的像素坐标超出图片宽高,转成归一化坐标后就会大于 1。

4.2 交叉核对 jpg 和 xml 的尺寸一致性

YOLO 坐标是归一化的,即使图片实际尺寸和 xml 里记录的 size 不一致,正好可以跨格式验证:xml 里记录的宽高应该等于 jpg 的实际宽高,否则说明 xml 是从别的图片复制过来的,或者图片被二次裁剪过没同步标注。我通常把两边的信息一起读出来比对:

import os import xml.etree.ElementTree as ET from PIL import Image for f in os.listdir('.'): if not f.endswith('.xml'): continue base = f.replace('.xml', '') jpg = base + '.jpg' if not os.path.exists(jpg): print(f'missing jpg for {f}') continue img = Image.open(jpg) real_w, real_h = img.size tree = ET.parse(f) root = tree.getroot() xml_w = int(root.find('size/width').text) xml_h = int(root.find('size/height').text) if real_w != xml_w or real_h != xml_h: print(f'size mismatch: {base}, jpg=({real_w},{real_h}), xml=({xml_w},{xml_h})')

凡是打印出来的文件,后面都要人工复查一遍。有的是因为图片本身被截图工具改过尺寸,有的是 xml 是上一个版本的标注,图片已经被替换了。这两种情况都必须修好再训练,不然隐性影响很大,轻则 mAP 偏低,重则训练直接发散。

4.3 可视化抽查:肉眼检查标注位置

脚本只能查数值合法性,框是不是准确贴着瓶子,脚本看不出来。这一步没有捷径,只能把标注画到图上,抽查几十张看一眼。我用 OpenCV 把 YOLO txt 里的归一化坐标还原成像素坐标,用矩形画到图片上:

import cv2 img_path = 'xyxr_bottle_123.jpg' txt_path = 'xyxr_bottle_123.txt' img = cv2.imread(img_path) h, w = img.shape[:2] with open(txt_path) as f: for line in f: parts = line.strip().split() cls_id = int(parts[0]) xc, yc, bw, bh = map(float, parts[1:]) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, f'bottle_{cls_id}', (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite('check_xyxr_bottle_123.jpg', img)

注意我用的转换公式是反过来从归一化到像素:x1 = (xc - bw/2) * w,也就是用中心点减半宽,再乘以图片宽。和小节 2.3 的公式互为逆运算。抽查时重点关注三类现象:框是不是明显偏大、把瓶子旁边的背景也框进去了;框是不是偏移了,只框住瓶口没框住瓶身;以及小目标瓶子的框是不是过小。这类问题脚本检测不出来,只能靠看。

我一般从划分好的测试集里随机抽 20 张画图,一张一张快速扫过。如果 20 张里超过 3 张有明显错标,说明整个数据集的标注一致性有问题,需要加大抽查比例,甚至全部过一遍。

5. 避坑指南:5 个标注数据集的常见翻车记录

5.1 现象:训练时 loss 正常,但 mAP 一直为 0

原因:YOLO 的 txt 类别编号从 0 开始,而有些教程或工具导出时类别编号从 1 开始,导致所有标注框被当成类别 1 处理,而模型只有类别 0,两者永远对不上。 解决:训练前用脚本扫描所有 txt 的 cls_id 最大值,如果出现大于等于数据集类别数的值,立即全量检查转换脚本。单类别数据集 cls_id 应该全为 0,出现任何 1 都要追查来源。

5.2 现象:验证集和测试集指标虚高,换张新图就废

原因:划分数据集时没有做随机均匀切分,或者同一张图片的增强版本同时进入了训练集和验证集,造成数据泄漏。我见过有人按文件名字母顺序直接切前 80% 作训练集,恰好这个数据集按时间排序,前 80% 全是同一场景的瓶子,后面全是另一场景,结果训练出来只认识前半段场景。 解决:严格按 3.1 的随机划分方式来做,设定固定 seed。划分后把 train、val、test 里是否有重叠文件名打出来检查一遍,重名了立即重新划分。

5.3 现象:坐标越界但脚本没报错,训练跑完指标很差

原因:YOLO 格式的坐标是归一化浮点数,某些错误标注恰好落在 0 到 1 之间,比如 xml 里 xmax 小于 xmin,或者框的宽高算出来是负数。数值上 0.05 到 0.95 看起来都合法,但实际标注内容完全不对。 解决:不能只查范围,还要查逻辑关系,比如xmax > xmin、ymax > ymin。在 VOC 转 YOLO 的脚本里加一个条件判断,读取 bndbox 时直接抛出异常,保证转换过程和校验过程同步。

5.4 现象:用 labelImg 重新打开数据集,xml 和 txt 反复对不上

原因:labelImg 默认以 PASCAL VOC 格式保存,如果你在软件里改了标注画了个新框,保存后只更新了 xml,而 txt 还是旧版本,除非专门用导出功能重新生成 YOLO 格式,否则两边就再也不一致了。 解决:我的习惯是定一个规则,数据集只在 VOC 格式下维护,也就是永远用 labelImg 编辑 xml 作为唯一真源,需要 YOLO 格式时跑一次全量转换脚本,统一生成 txt。不要在两种格式间来回手工编辑。

5.5 现象:图片被误删或没配对,训练脚本报错找不到文件

原因:为了清理磁盘空间,有人把看起来是重复的 jpg 删了,但 xml 和 txt 还留在目录里,或者反过来只删了标注没删图。训练脚本通常通过文件名前缀把三种格式关联起来,任何一边缺失都会在 dataloader 里报错。 解决:每次操作完文件后,跑一次完整性检查,比对 jpg、xml、txt 三个集合的文件名是否完全一致。我写了一个快速脚本,用 set 求差集,几秒钟就能把缺失文件列出来。

6. 把新瓶子并进数据集:增量标注的正确姿势

拿到数据集的下一步往往是往里加自己的数据。比如你手头有 200 张自己拍的瓶子照片,想并进这 4500 张里一起训练。这个操作用到一个很重要的工作流:新增数据全部使用 VOC 格式标注,再统一转成 YOLO。我比较推荐的做法是把新照片按同样命名规则放进一个独立目录,用 labelImg 打开目录,在 class_list 文件里只写一个 bottle,标注时自动从列表里选这个类,不会标错成别的名字。每个瓶子都画矩形框,框多大多小尽量和原数据集保持相近的风格,特别是瓶盖和瓶身的边界,如果原数据集里框住了整个瓶子,你也必须框住整个瓶子,混用不同标注风格会让模型学到的特征方差变大。

标注完成后,把新图片连同 xml 一起拷进主数据集目录,重新跑一遍小节的转换脚本,生成对应的 YOLO txt。然后重新划分训练验证测试集,因为新增数据后原有的 8:1:1 比例变了,重新划分才能保证验证集里既有原数据也有新数据。划分完后再跑一遍第四章的全量校验脚本,重点看新图片有没有越界或尺寸不匹配。

我踩过一次印象很深的坑:新拍的照片用手机拍的,分辨率 3000×4000,而原数据集是 640×480,混合训练时 dataloader 会把所有图缩放到统一尺寸,小图上本来清晰的瓶子纹理被压缩得几乎看不见,导致模型在新图上的表现明显变差。从那以后,我所有新增图片都先统一缩放到与原数据集相近的分辨率,再走标注流程。缩放时注意不要改宽高比,直接做 resize 会把瓶子拉变形,标注框同样会被拉歪。正确做法是先按比例缩放到目标尺寸内,多余部分填充灰色,和检测任务里常见的 letterbox 同理。

这套流程走完,新老数据就真正融在一起了。训练脚本不再区分哪张图来自原数据集、哪张图是你自己标的,统一读取 images 和 labels 目录,跑出来的模型瓶子检测能力既继承了原数据集的多样性,又补上了你场景下的新样本。我每次合并完,都会把分类别框数统计再打印一遍,确认自己新增的框数真的进了统计里,如果数字没变,多半是路径配错了,txt 写到了别的位置。希望这套从验收到增量标注的流程能帮到你省下几天调试时间。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 16:57:13

DeepSeek Harness 0.1.5-rc插件兼容性升级实战指南

1. 项目概述&#xff1a;一次真实发生的DeepSeek Harness升级踩坑实录 DeepSeek Harness这个工具&#xff0c;我从去年底开始用&#xff0c;最初是0.1.3版本&#xff0c;搭了个本地知识库问答小系统&#xff0c;跑得挺稳。今年三月看到官方发了0.1.5-rc的预发布通知&#xff0…

作者头像 李华
网站建设 2026/9/28 16:57:11

Agent-Native架构实战:从AI功能到智能体驱动的工程重构

去年秋天我接手了一个客户运营后台的改造&#xff0c;需求听起来极其朴素&#xff1a;把用户咨询自动识别后转成工单。团队里所有人最初的判断都是“接一个大模型接口就能搞定”。真正做完第一版&#xff0c;我才意识到自己把AI焊死在了流程里&#xff1a;模型只负责给文本打个…

作者头像 李华
网站建设 2026/9/28 16:56:50

昆虫识别数据集处理:从XML标注到YOLOv8训练完整指南

简介&#xff1a;一套面向深度学习图像识别任务的昆虫分类数据集&#xff0c;涵盖6种昆虫的217张真实图片及对应XML标注&#xff0c;并按7:2:1划分为训练集、验证集和测试集。压缩包为ZIP格式&#xff0c;共438个文件&#xff0c;其中包含217个JPG图像文件、217个XML标注文件及…

作者头像 李华
网站建设 2026/9/28 16:56:24

STM32F4外扩DAC8562实战:16位双通道模拟输出的SPI驱动与校准全解析

做模拟量输出的时候&#xff0c;很多人第一反应就是用MCU自带的DAC&#xff0c;但真到了项目里要出16位精度、双通道、低噪声的电压信号&#xff0c;STM32F4片内那两路12位DAC往往不够看。这个项目就是在F407上外扩了一片TI的DAC8562&#xff0c;通过SPI接口实现双通道0到5V可调…

作者头像 李华
网站建设 2026/9/28 16:56:21

FPGA实战:L9110H电机驱动模块与PWM调速控制设计

1. 模块概述与方案选型1.1 L9110H是个什么样的芯片做嵌入式开发这些年&#xff0c;电机驱动模块我用过不少&#xff0c;从最开始的L298N到TB6612&#xff0c;再到今天要聊的L9110H&#xff0c;每个模块都有自己的脾气。L9110H本质上是一颗低压直流电机驱动芯片&#xff0c;内部…

作者头像 李华
网站建设 2026/9/28 16:55:56

CLI-Anything:用终端与AI CLI打造高效开发工作流

如果你和我一样&#xff0c;每天醒来的第一件事不是点开各种图形界面&#xff0c;而是直接敲两下终端命令&#xff0c;那你应该早就明白&#xff1a;CLI 从来都不是过时的玩具&#xff0c;而是真正高效的生产力工具。这个叫 CLI-Anything 的项目&#xff0c;其实就是我把自己这…

作者头像 李华