简介这是一份面向目标检测学习与YOLO实战的handbag类别数据集基于COCO2017提取已转换为VOC与YOLO两种标注格式可直接用于手提袋检测模型训练。本包为整个数据集的第二部分压缩包共2000个文件主要包含jpg样本图片、xml标签与txt标签三类文件整体约395MB。数据集标注目标为handbag描述信息显示样本总量为7133适合作为二分类或单类别检测项目的训练/验证数据。目前已有490人学习下载适用于刚接触YOLO标签格式、需要现成VOC/YOLO标注数据的开发者可跳过数据清洗与格式转换环节直接进入模型训练与调参。1. 手提袋检测数据集VOC 和 YOLO 两份标签到底能省多少事做目标检测的工程师拿到一份新数据集第一件头疼的事往往不是模型精度而是格式。手提袋检测数据集把 VOC 格式和 YOLO 格式的标签都整理好了等于把“做模型对比实验”和“直接跑 YOLO 训练”两条路都铺平了。VOC 的 XML 标注适合拿来喂 Faster R-CNN、SSD 这类经典检测器也方便做数据可视化核对;YOLO 的 txt 标注则是为训练阶段准备的省去在线转换坐标的麻烦。适合三类人做零售自助收银、仓储出入库方案的目标检测开发者;需要公共数据做算法评估的研究者;以及刚入门想拿着真实标注数据跑通一次完整检测流程的初学者。2. 拆开数据集看结构VOC 和 YOLO 两种标注到底差在哪2.1 顶层目录与文件构成拿到手提袋检测数据集后第一件事是看目录结构。VOC 格式那边通常保留着 PASCAL VOC 时期的经典三件套JPEGImages 放原图、Annotations 放 XML 标签、ImageSets/Main 放训练/验证划分的 txt 索引。YOLO 格式那边则把图片和标签按 train/val 分开每张图片对应一个同名的 .txt 文件。handbag_dataset/ ├── VOC/ │ ├── JPEGImages/ │ │ ├── handbag_00001.jpg │ │ └── handbag_00002.jpg │ ├── Annotations/ │ │ ├── handbag_00001.xml │ │ └── handbag_00002.xml │ └── ImageSets/ │ └── Main/ │ ├── train.txt │ ├── val.txt │ └── classes.txt └── YOLO/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml需要说明的是不同版本的数据集在目录命名上会有差异。比如 VOC 那边有的把划分文件直接放在 Annotations 同级或者用 trainval.txt 合并训练验证;YOLO 这边有人会把 labels 直接放在 images 的同级目录也有人用 .txt 索引文件代替 train/val 文件夹。遇到这种变化不用慌划分文件本身是纯文本每行一个不含扩展名的文件名用脚本重新生成即可。关键只有一条图片文件名必须和 XML、txt 的文件名严格对应否则后面所有脚本都白搭。2.2 VOC 格式XML 里有什么坐标怎么定义打开一个 Annotations 下的 XML结构长这样annotation folderJPEGImages/folder filenamehandbag_00123.jpg/filename size width1920/width height1080/height depth3/depth /size object namehandbag/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin412/xmin ymin268/ymin xmax893/xmax ymax715/ymax /bndbox /object object namehandbag/name poseUnspecified/pose truncated0/truncated difficult1/difficult bndbox xmin40/xmin ymin900/ymin xmax180/xmax ymax1010/ymax /bndbox /object /annotationVOC 的坐标是绝对像素值xmin/ymin/xmax/ymax 直接对应原图坐标左上角为原点单位是像素。xmax - xmin 就是检测框宽度ymax - ymin 就是高度。如果一张图里有多个手提袋就写多个object块。这里有两个细节容易忽略。第一difficult标签表示目标被严重遮挡或模糊到人眼都难分辨;经典检测器如 Faster R-CNN 在训练时默认忽略 difficult1 的样本而 YOLO 训练脚本不区分这也是两种格式在处理难例时的语义差别。第二XML 里size的宽高必须和真实图片一致有些标注工具导出时会写错转换脚本读出来就是错框。还有一点LabelImg 这类目标检测常用标注工具导出的 XML 基本兼容这个结构但偶尔会多出segmented之类的节点解析时不要假设字段顺序。2.3 YOLO 格式txt 标注的归一化坐标与类别编号YOLO 的 txt 标签比 XML 精简得多每一行代表一个目标五个数字依次是类别编号、归一化中心点 x、归一化中心点 y、归一化宽度、归一化高度。0 0.456000 0.375000 0.240000 0.410000 0 0.087000 0.860000 0.095000 0.110000归一化就是把绝对像素值除以图片宽高转换公式如下center_x (xmin xmax) / 2 / widthcenter_y (ymin ymax) / 2 / heightbox_w (xmax - xmin) / widthbox_h (ymax - ymin) / height类别编号从 0 开始编号对应的类别语义由 data.yaml 里的 names 列表决定。手提袋检测数据集如果是单类那所有行的第一个数字都是 0;如果数据里分出了“纸袋”“布袋”“塑料袋”等多个类别就得严格对照 names 顺序来读。这是后续最容易翻车的地方我见过不止一次因为 class_names 顺序不一致导致整个训练集标签错位的案例。YOLO 只存归一化坐标的好处是训练脚本不需要每张图都去查原图尺寸预处理速度快;原图被 resize 后标签无需改动依然自洽。代价是它丢失了原图尺寸信息想从 txt 反向知道目标在画面里的实际大小必须回到 VOC 格式或者原图。这也意味着要做数据可视化和人工复核VOC 格式的可用性远高于 YOLO 格式。2.4 两种格式在训练流程中的分工一份数据集同时给两种格式不是资源浪费而是覆盖了目标检测模型落地的两个阶段。VOC 格式适合做算法选型阶段的多模型对比——Faster R-CNN、SSD、EfficientDet 这些框架对 VOC 的支持最成熟转换成本最低。YOLO 格式则直接对应训练阶段——YOLOv5 和 YOLOv8 的原生数据组织就是 images/labels 加 data.yaml把数据放进去就能开训不用在 DataLoader 里写自定义解析逻辑。理解了这个分工你就知道拿到数据集后该怎么用先拿 VOC 格式做标注质量的随机抽检确认无误后再切到 YOLO 格式训练。如果两份标签不是同一个版本生成的那就以 VOC 为准做一次全量转换不要盲目信任 txt。后续可以重新生成YOLO标签也不要忘记先做一个数据体检。3. 从 VOC 转到 YOLO 再转回来转换脚本与参数细节3.1 用 Python 写一个干净的 VOC 转 YOLO 脚本常见做法是写一个脚本遍历整个 Annotations 目录把每个 XML 转成同名 txt。核心函数如下import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_file, output_txt, class_names): 把单张 VOC 格式 XML 转成 YOLO txt 标签 :param xml_file: 输入的 XML 文件路径 :param output_txt: 输出的 txt 文件路径 :param class_names: 类别列表列表顺序决定 YOLO 类别编号 tree ET.parse(xml_file) root tree.getroot() # 图片尺寸必须从 XML 的 size 节点读取 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] # iter(object) 比 findall 更稳能递归找到嵌套的 object 节点 for obj in root.iter(object): name obj.find(name).text.strip().lower() # 统一转小写避免 Handbag 和 handbag 导致类别漏匹配 if name not in [c.lower() for c in class_names]: continue cls_id [c.lower() for c in class_names].index(name) box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 归一化到 [0,1]保留 6 位小数足够训练使用 cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h bw (xmax - xmin) / img_w bh (ymax - ymin) / img_h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(output_txt, w) as f: f.write(\n.join(lines) \n)参数说明class_names列表的顺序就是 YOLO 类别编号的语义一旦确定就不要中途修改。用root.iter(object)而不是root.findall(object)是因为部分标注工具导出的 XML 结构里 object 节点层级不标准iter 能递归查找兼容性更好。这段脚本有一个容易被忽略的参数风险如果 XML 里的difficult1也被转出来了训练时会当作正常目标参与计算。对 YOLO 来说这不是错误但如果你希望和 Faster R-CNN 的结果公平对比可以在转换前跳过 difficult 样本。3.2 反向转换 YOLO 到 VOC 的注意点反向转换在实际工作中用得到——数据可视化、修正标注、给其他检测框架做适配。但 YOLO 的归一化坐标丢失了原图尺寸反向转换必须显式传入原图宽高def yolo_to_voc(txt_path, img_w, img_h, class_names): 把 YOLO txt 转成 VOC 格式的 object 列表 注意img_w 和 img_h 必须来自原图否则还原坐标会错位 objects [] with open(txt_path, r) as f: for line in f: line line.strip() if not line: continue parts line.split() if len(parts) ! 5: print(f跳过非法行: {line}) continue cls_id int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) # 从归一化还原成绝对像素坐标 xmin int((cx - bw / 2) * img_w) ymin int((cy - bh / 2) * img_h) xmax int((cx bw / 2) * img_w) ymax int((cy bh / 2) * img_h) objects.append({ name: class_names[cls_id], xmin: xmin, ymin: ymin, xmax: xmax, ymax: ymax }) return objects这个函数输出的 object 列表还需要拼装成 XML 字符串拼装时size节点必须和传入的img_w、img_h一致否则下一次读入又会对不上。从归一化坐标还原像素坐标本身就有 1 到 2 像素的取整误差这个误差对检测训练没有影响但如果你的任务是要做像素级裁剪最好还是回到原始 VOC 标签取数。3.3 转换后必须做的三个校验转换脚本写完不是终点校验才是避免后面翻车的关键。我每次转换完都会跑三件事。第一检查空标签。YOLO 训练时一张图如果没有对应 txt会被当作纯背景图;如果原本有目标、转换时漏写了就等于把一个正样本变成了负样本模型会学出“看到手提袋不要检测”的错误信号。第二检查标签文件是否有对应图片。第三检查归一化坐标是否越界。这三个问题可以用一个脚本一次性扫出来import os def validate_yolo_labels(label_dir, img_dir, class_num): 扫描 YOLO 标签目录输出常见问题 :param label_dir: labels 目录 :param img_dir: 对应的 images 目录 :param class_num: 类别总数用于校验编号越界 for label_file in sorted(os.listdir(label_dir)): if not label_file.endswith(.txt): continue stem label_file[:-4] # 检查是否有对应图片 has_img (os.path.exists(os.path.join(img_dir, stem .jpg)) or os.path.exists(os.path.join(img_dir, stem .png))) if not has_img: print(f[无图] {label_file}) # 检查每行字段数和数值范围 with open(os.path.join(label_dir, label_file)) as f: lines [l for l in f.read().strip().split(\n) if l] if not lines: print(f[空标签] {label_file}) for i, line in enumerate(lines, 1): parts line.split() if len(parts) ! 5: print(f[字段数错误] {label_file}:{i} - 应5个字段实际{len(parts)}) continue cls_id int(parts[0]) if cls_id 0 or cls_id class_num: print(f[类别编号越界] {label_file}:{i} - {cls_id}) vals list(map(float, parts[1:])) if any(v 0.0 or v 1.0 for v in vals): print(f[归一化越界] {label_file}:{i} - {parts[1:]}) print(校验完成)这个校验脚本本身就是一份数据集处理工具对任何 YOLO 格式数据集都适用。跑完之后检查输出的日志数量是否合理。如果空标签、无图的比例超过 5%说明数据集的划分文件很可能有错位这时候先修数据再训练不要带着问题硬训。4. 用手提袋数据集训练 YOLO 模型目录组织与关键配置4.1 YOLOv5 和 YOLOv8 的数据集目录组织与 data.yamlYOLO 格式的目录组织各家框架基本统一直接按下面的结构放就行datasets/handbag/ ├── images/ │ ├── train/ │ │ ├── handbag_00001.jpg │ │ └── ... │ └── val/ │ ├── handbag_10000.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── handbag_00001.txt │ │ └── ... │ └── val/ │ ├── handbag_10000.txt │ └── ... └── data.yaml对应的 data.yaml 配置train: datasets/handbag/images/train val: datasets/handbag/images/val nc: 1 names: [handbag]nc是类别数量names的列表顺序必须和 YOLO txt 里的类别编号一一对应。如果数据集里分了多个类别比如[paper_bag, plastic_bag, cloth_bag]那 txt 里出现数字 2 就代表cloth_bag。这个对应关系是全局唯一的任何目录调整都不该改动它。训练命令以 YOLOv5 为例python train.py --data data.yaml --weights yolov5s.pt --img 640 --epochs 100 --batch 16--img参数控制输入分辨率。这里有一个准则看手提袋在画面里的像素占比。如果数据集中大部分目标占图面积超过 10%640 分辨率就够;如果很多目标只有几十乘几十像素建议直接上 1280或者保持 640 训练、推理时用 SAHI 做切片。--batch按显存调整16 在 8GB 显存上对 yolov5s 是安全的显存小就降到 8。4.2 单类数据集的类别平衡和尺寸分布问题很多人以为单类数据集不存在类别不平衡实际上对手提袋检测来说真正的坑是目标尺寸分布和拍摄角度。如果数据集中包含了大量近距离特写和少量远距离小目标模型会偏向大目标。判断方法很简单用脚本统计每个目标归一化宽度和高度画个直方图看看小于 0.1 的目标占比。如果远距离小目标占比低常见做法是调大--img加高分辨率小目标响应而不是盲目加训练轮数。YOLO 自带的 autoanchor 会自动适配标注框尺寸分布但前提是训练前不手动关闭它。另一个容易被忽略的是角度问题。手提袋在自然场景里经常被斜挎、挂在货架侧边甚至旋转超过 45 度。标准 YOLO 检测框是水平矩形如果数据的标注来自旋转框的导出会把斜挎的手提袋真实尺寸拉大。遇到这种情况我建议先做人眼抽检随机看 50 张图的叠加可视化确认标签和物体实际轮廓匹配再开始训练。这个动作虽然费时间但能避免整个训练过程建立在一个错误的标签基础上。4.3 训练结束后的验证用 mAP 和 PR 曲线判断标注质量模型训练完不要只看 loss 曲线。对目标检测模型来说验证阶段的指标才反映真实效果。YOLOv5 在训练完会自动跑一次验证也可以用下面的命令单独验证python val.py --data data.yaml --weights runs/train/exp/weights/best.pt --img 640输出里重点关注下面几个目标检测评价指标指标含义手提袋场景解读Precision预测框中真正含目标的占比数值低说明误检多模型把背景当成了手提袋Recall真实目标被召回的比例数值低说明漏检多模型看不见部分手提袋mAP0.5IoU0.5 时的平均精度工程落地的首要指标达到 0.9 以上才可用mAP0.5:0.95多个 IoU 阈值的平均精度数值低于 0.5 说明标注框精度差边界贴合不好如果 mAP0.5 高但 mAP0.5:0.95 明显偏低多半是标注框边界贴合不精细或者物体本身遮挡严重。这类情况可以让标注工具重新校准也可以用简单的后处理把预测框向中心收缩一点。如果 PR 曲线在召回率中段塌陷说明模型对特定场景的目标一直漏检去验证集里找对应 False Negative 图大概率能看到某一类光照或角度从来没在训练集里出现过。5. 手提袋检测数据集避坑五个高频问题和排查方法5.1 现象loss 正常下降但验证 mAP 恒为 0原因训练集和验证集的标签划分错位。最常见的是 VOC 格式下用 ImageSets/Main 里的 train.txt 做划分但 train.txt 里有的图片在 Annotations 里没有对应 XML;转成 YOLO 格式后这些图变成了无标签背景图训练时模型学到了“这张图没有目标”验证时同一张图又有标签导致预测和真值对不上。解决训练前统计划分文件与 XML/txt 的对齐情况。写一个对比脚本找出 train.txt 里缺标签的图片这种图片要么补标签要么从划分文件里剔除。我一般会用上面 3.3 节的校验脚本跑一遍空标签率超过 1% 就先修数据。5.2 现象训练到一半 loss 变成 NaN原因归一化坐标越界或者某张图的检测框宽高为 0。转换脚本里如果 xmax 和 xmin 相等宽度就是 0归一化后仍然为 0训练时计算 IoU 会出现除零错误。另一个可能是分割文件里混入了损坏图片读取尺寸失败。解决训练前用 validate_yolo_labels 扫描一遍标签目录重点看归一化越界的行。对于边界超出 [0,1] 的标签可以做一个 clamp 处理但对宽高为 0 的必须删除这一行标注。这类问题是数据层面的事不要通过修改损失函数去规避。5.3 现象训练集和验证集指标都好换新场景完全失效原因类别编号错位。如果数据集的 YOLO 标签由多个批次的标注合并而来某个批次的类别顺序和主表不一致就会出现部分 txt 里数字 0 代表纸袋另一部分 txt 里数字 0 代表塑料袋;模型学了一个混合语义自然在新场景下行为不稳定。解决以 VOC 格式的 class_names 为唯一真源重新全量生成 YOLO 标签。生成后用 3.1 节的脚本跑一遍再抽 50 张图把标签画出来人工核对。这个“画框看标签”的动作虽然原始却是最有效的确认手段。5.4 现象远距离的手提袋漏检严重近距离正常原因目标尺寸分布极不均衡。数据集中大多数手提袋是近距离拍摄占图比例超过 20%;挂在货架远处的手提袋只有几十像素在 640 分辨率下经过下采样后特征所剩无几。漏检的往往是这部分小目标。解决先做尺寸分布统计归一化宽度小于 0.1 的目标占比若低于 10%说明数据集本身小目标太少。常见做法是提高输入分辨率到 1280或者在推理阶段做切片检测。训练侧可以开 mosaic 增强但要控制切片的尺度范围否则生成的手提袋比例失真。5.5 现象斜挎手提袋的检测框只框住一半原因旋转框被存成了水平框。真实场景里手提袋经常倾斜 30 到 60 度水平 bndbox 会包含大量背景或截掉袋体。如果数据集原始标注是旋转矩形导出成 VOC 的bndbox时没有做最小外接水平矩形重算就会出现框偏大或偏小。解决检查原数据集是否附带旋转框标注。如果只有水平框可以用分割掩膜重新计算最小外接矩形来修正。如果倾斜目标占比高建议直接换用支持 OBB 的检测模型比如 YOLOv8-OBB不要再在水平框上强行训练。这个坑在自助收银、货架巡检这类场景里特别常见因为相机视角和物体姿态变化大。6. 投入训练前先跑一遍数据集体检五个检查项一脚本全过最后给你一个我每次拿到手提袋检测数据集都会先跑的“体检”流程。花五分钟做这件事能省下后面几天排查训练问题的精力。import os, random def dataset_health_check(yolo_root, class_num, sample_size50): 一个脚本覆盖五个检查项标签数、空标签、越界、类别分布、样本对齐 img_train os.path.join(yolo_root, images, train) lab_train os.path.join(yolo_root, labels, train) img_files [f for f in os.listdir(img_train) if f.endswith((.jpg, .png))] lab_files [f for f in os.listdir(lab_train) if f.endswith(.txt)] print(f图片数: {len(img_files)} 标签数: {len(lab_files)}) # 1. 图片和标签数量偏差 if abs(len(img_files) - len(lab_files)) len(img_files) * 0.02: print([警告] 图片与标签数量偏差超过 2%检查数据集划分) # 2. 类别分布 class_count [0] * class_num empty_count 0 bad_count 0 for lab in lab_files: with open(os.path.join(lab_train, lab)) as f: lines [l for l in f.read().strip().split(\n) if l] if not lines: empty_count 1 for line in lines: parts line.split() if len(parts) ! 5: bad_count 1 continue cls_id int(parts[0]) if 0 cls_id class_num: class_count[cls_id] 1 vals list(map(float, parts[1:])) if any(v 0 or v 1 for v in vals): bad_count 1 print(f空标签文件: {empty_count} 非法行: {bad_count}) print(f类别分布: {class_count}) # 3. 随机抽样本核对图片和标签是否存在 random.seed(42) sample random.sample(img_files, min(sample_size, len(img_files))) for img in sample: stem os.path.splitext(img)[0] if not os.path.exists(os.path.join(lab_train, stem .txt)): print(f[缺失标签] {img})体检脚本的输出就是一张“能不能开训”的成绩单。空标签多先修划分;非法行多先修转换脚本;类别分布严重倾斜先做数据增强策略。这些验完再训模型翻车的概率会小很多。做目标检测这几年来我的一个习惯是任何数据集到手不画框直接训练的后面代价迟早会还回来。标注格式统一、越界检查、随机抽检这三个动作是投入训练前成本最低的保险。希望这份手记能帮你少踩几个坑也希望这份手提袋检测数据集能真正跑出你想要的效果。本文还有配套的精品资源点击获取