简介瓶子目标检测数据集共收录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 450013500 个文件正好等于 4500×3。这说明每张图片的标注在两个体系里各有一份没有缺失。xml 是 Pascal VOC 标准txt 是 YOLO 标准两者描述同一个标注框但坐标表达方式完全不同。拿到任何数据集我第一步一定是先做这个数量核对文件数对不上后面全是坑。除了这些文件目录里通常还有一个说明.txt里面记录了标注类别、框数等元信息。训练前先把这份说明读一遍它相当于数据集的体检报告告诉你这个数据集承诺了什么、不承诺什么。2.2 VOC 的 xml 文件左上角和右下角的绝对值挑一个 xml 文件看看它的结构对做过检测的人应该不陌生。我用文本编辑器打开 xyxr_bottle_123.xml内容大致如下annotation folderxyxr_bottle/folder filenamexyxr_bottle_123.jpg/filename path/home/user/dataset/xyxr_bottle_123.jpg/path source databaseUnknown/database /source size width640/width height480/height depth3/depth /size segmented0/segmented object namebottle/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin127/xmin ymin103/ymin xmax384/xmax ymax451/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。归一化的方式是除以图片原始宽高比如 xmin127、图片宽 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_okTrue) 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 列表长度一致这里 nc1names 里只有一个 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(fcorrupted image: {path} - {e})把输出定向到文本文件里逐条看损坏图片的名字决定是删除对应的三种格式文件还是找替代图补上。注意如果真的删了图片必须把同样主文件名的 xml 和 txt 也一起删掉否则训练脚本读取图片列表时会出现索引错位这是新手最容易翻车的地方。4. 全量校验脚本验证 4500 张图的标注是否靠谱4.1 检查坐标是否越界标注质量直接决定训练结果的天花板。虽然说明文件声称提供准确且合理标注但我拿到任何数据集都不会轻信这句话一定要自己做全量校验。YOLO 格式的归一化坐标有个硬性约束中心点 x、y 和宽 w、高 h 都必须在 0 到 1 之间。越界到 1.2 这种值要么是标注时框拉出了图片边界要么是转换脚本算错了分母。我写了一个全量检查脚本遍历所有 txtimport 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, fout 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(fmissing 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(fsize 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, fbottle_{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 写到了别的位置。希望这套从验收到增量标注的流程能帮到你省下几天调试时间。本文还有配套的精品资源点击获取