
简介这份手提袋检测数据集源于COCO2017目前放出的是第二部分共7133张真实场景图片目标类别为handbag适合目标检测入门练习、YOLO系列模型微调及VOC格式训练流程验证。压缩包约395.33MB共约2.1万个文件包含7134个txt、7133个xml与7133个jpg其中txt为YOLO格式标签xml为VOC格式标签免去了自行从COCO抽取与格式转换的繁琐步骤可即下即用。数据已按两个主流框架格式对齐交给YOLO训练脚本或VOC读取工具都能直接解析便于快速开展训练、验证或数据增强实验适合需要干净、结构化手提袋样本的算法工程师和研究者。已有490人学习压缩包目录结构清晰图片与对应标签一一对应方便二次切分训练集与验证集使用。1. 手提袋检测数据集两种标签格式之间的转换才是落地第一关做零售场景的项目时我拿到过一份手提袋检测数据集里面是几百张商场和便利店门口抓拍的图片VOC格式的XML标签和YOLO格式的TXT标签都给了。原以为直接能开训结果第一步就卡住很多标注工具默认导出的XML路径和TXT的class_id对不上类别名一个叫bag一个叫handbagyaml文件写错一个字母训练直接崩。手提袋检测本身不复杂复杂度全在数据集格式这条暗线上。对想复现的人来说先搞清楚VOC格式和YOLO格式怎么互相转换、怎么验框比急着调模型参数更值钱。这篇笔记就是围绕这个数据集把两种标签的转换、训练配置和踩坑完整过一遍。2. VOC和YOLO的标签格式差异一条标注数据在两种标准下的生存方式2.1 绝对坐标对相对坐标XML和TXT各自记录了什么VOC格式沿用的是Pascal VOC的标注惯例。每张图对应一个同名XML文件文件里有folder、filename、source、size、object这些节点。核心信息在object节点里的bndboxbndbox下面有xmin、ymin、xmax、ymax四个子节点存的是像素绝对值。坐标系的原点在图像左上角x轴向右y轴向下。比如一个手提袋左边界离图像左边120像素右边界离左边460像素那xmin就记120xmax记460。YOLO格式走的是另一套逻辑。每张图对应一个同名TXT文件一行一个目标格式是class_id x_center y_center width height五个值用空格隔开。后面的四个值全部是归一化后的相对坐标除以了原图的宽或高取值范围理论上在0到1之间。这个设计的最大好处是标注文件不再依赖图像分辨率同一份TXT在416x416和1280x1280的训练配置下都能用模型读取时会按当前输入尺寸重新换算。对新手来说最容易出错的地方在于绝对值转归一化时忘了除以尺寸。我见过不止一次有人把XML里的xmin、ymin直接当成YOLO的中心点坐标写进TXT训练时损失从第一轮就异常大甚至直接出现NaN。YOLO格式里x_center和y_center是中心点位置不是左上角位置这两个概念混掉的后果是框全部偏移而且偏移量还不是固定值因为除以宽高以后的数值区间完全变了。2.2 选型逻辑格式跟着训练框架走不跟着感觉走手提袋检测数据集之所以要同时保留VOC格式和YOLO格式是因为实际使用它的人走的是两条不同的路线。如果你用的是老牌检测框架比如Faster R-CNN、SSD或者Mask R-CNN的常见实现数据加载器通常直接解析XMLVOC格式是原生输入。如果你走YOLO体系从YOLOv5到YOLOv8、YOLO11官方训练流程认的都是TXT标签目录而不是XML。所以选型标准很简单最后用哪个框架训练就以哪种格式为主。我自己默认以YOLO格式为主因为YOLO生态从训练到部署的链路最短转换到ONNX、TensorRT都有现成工具。VOC格式保留一份主要是为了做数据审阅和跨框架迁移毕竟XML里的结构化信息更完整调试时可读性比TXT好太多。不需要为“两种格式都有”而强迫自己混用大部分训练框架只认一种格式混放还会导致同一张图被读两次框数量翻倍loss曲线看起来正常但评估指标全是错的。提示如果数据集的VOC和YOLO标签来自同一份原始标注两者的框数值应该一一对应。发现某张图两边框的数量对不上说明数据在标注后修改过以较新的那份为准别自己脑补补全。2.3 目录组织的行业惯例三件套目录与文件对应关系拿到手提袋检测数据集后第一步是把目录结构理清楚。行业里最常见的布局是根目录下分images、Annotations、labels三个一级目录images放JPG原图Annotations放VOC格式XMLlabels放YOLO格式TXT。三个目录里的文件名一一对应只是扩展名不同。YOLO训练时data.yaml里指定train和val的图片路径训练器会自动从图片路径推导标签路径推导规则是把images换成labels把.jpg换成.txt。这个推导关系在Ultralytics框架里特别关键。它不会让你显式指定标签目录而是遍历图像文件后到同级路径去找同名TXT。如果目录命名不符合images/train对labels/train这种对应关系训练时会出现“找到图像但没找到标签”的警告框架默认跳过这些样本而你不会立刻察觉等训练完看统计数据才发现参与训练的图片数少了一大截。我惯用的结构是handbag_dataset/ ├── data.yaml ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── Annotations/ │ ├── train/ │ └── val/Annotations目录在这里是备份角色。万一TXT被人误删或者某张图的归一化坐标算错了可以从XML重新生成不用重新标注。images和labels才是真正的训练输入。这个三件套结构不限于手提袋任何检测数据集的复现实战都可以照搬。3. 手提袋数据集VOC转YOLO一个从XML到TXT的转换脚本及其边界处理3.1 转换脚本主体解析、归一化与写出VOC转YOLO没有太多黑匣子逻辑自己写一个转换脚本比找工具更可控因为数据集的XML字段偶尔会有小差异比如有的版本用bndbox有的写BndBox固定工具解析不了还得改脚本。下面这段我一直在用结构简单直接遍历Annotations目录读出每张图的宽高和所有bndbox归一化后写入对应的TXT。import os import xml.etree.ElementTree as ET def convert_xml_to_yolo(xml_path, txt_path, class_map): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w float(size.find(width).text) img_h float(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_map: continue class_id class_map[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 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 # 防止标注越界导致负值或大于1的值 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) if __name__ __main__: annotations_dir Annotations labels_dir labels # 类别名必须和数据集实际标注字符串一致多类时继续往后加 class_map {handbag: 0} os.makedirs(labels_dir, exist_okTrue) for xml_file in os.listdir(annotations_dir): if not xml_file.endswith(.xml): continue xml_path os.path.join(annotations_dir, xml_file) txt_path os.path.join(labels_dir, xml_file.replace(.xml, .txt)) convert_xml_to_yolo(xml_path, txt_path, class_map)逻辑说明脚本核心是ET.parse解析XML先取size节点的宽高再迭代所有object节点取类别名和bndbox四个坐标。归一化公式是中心点等于xmin加xmax除2再除以图像宽宽等于xmax减xmin再除以图像宽高同理。这套公式是所有VOC转YOLO工具的统一算法改任何一项都会让框偏移。输出用6位小数精度足够YOLO训练使用位数太多只会让文件无谓增大。参数说明class_map是类别名到数字id的映射必须和数据集中实际标注字符串完全一致。手提袋检测数据集如果只有一类写{handbag: 0}就够了。如果某个XML里出现了不在映射表里的类别脚本会直接跳过这样避免了脏数据写出不存在的类id也便于事后检查到底是标注问题还是映射遗漏。3.2 三个必调参数和边界状况处理逻辑第一是类别映射。类别名必须一字不差。踩坑最频繁的就是“数据集里写的是plastic_bag映射表里写的却是bag”TXT倒是生成出来了但class_id全部错位训练加载后模型学到的类别对应关系和你的认知完全不同验证时显示0分。转换前先用代码统计一遍XML里所有出现的name当作核对清单。第二是坐标钳制。代码里那四个min/max操作平时不会生效但有些标注工具允许标注框延伸到图像边缘外侧不钳制的话TXT里会出现负宽度或者大于1的宽度YOLO训练计算框面积时拿到负值轻则损失曲线异常重则直接训练中断。钳制到0到1之间只是把物理上不可能的值修掉不影响框的真实逻辑。第三是图像宽高从哪里取。一定从XML的size节点取不要用OpenCV去读原图尺寸。因为数据集在标注后可能被缩放或裁剪过XML里的size如果没同步更新就会造成错位。反过来如果原图被重新处理过而XML是旧的以XML为准也会导致框偏所以拿到数据集先做一次“图与标注一致性”检查用2.3的抽查脚本跑一遍有问题提前暴露。3.3 转换后的一分钟抽查用OpenCV画框验证转换完不等于能用。我每次转换后必做的一步是随机抽5到10张图把TXT里的坐标反算回像素值画框肉眼看一眼框贴不贴手提袋轮廓。import cv2 def show_yolo_box(image_path, txt_path, class_names): img cv2.imread(image_path) h, w img.shape[:2] with open(txt_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() cls int(parts[0]) xc float(parts[1]) * w yc float(parts[2]) * h bw float(parts[3]) * w bh float(parts[4]) * h x1 int(xc - bw / 2) y1 int(yc - bh / 2) x2 int(xc bw / 2) y2 int(yc bh / 2) color (0, 255, 0) if cls 0 else (0, 0, 255) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, class_names[cls], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imshow(check, cv2.resize(img, (800, 800))) cv2.waitKey(0) show_yolo_box(images/train/0001.jpg, labels/train/0001.txt, [handbag])反算逻辑是TXT里的归一化值分别乘以原图宽高还原出像素坐标中心点坐标减半宽得左上角x加半宽得右下角x高度同理。这一步能直观看出两个典型问题如果框整体偏移但形状正常说明归一化或反算公式有误如果框形状没问题但位置贴不紧说明标注本身就不准。这步抽查看起来是玄学实际上是成本最低的后悔药。训练一轮目标检测动辄一两个小时等跑完才发现数据格式错了浪费的时间远比抽查多。4. 手提袋训练配置怎么定YOLO系列模型与数据划分的复现指南4.1 模型选择YOLOv8对单类半刚性目标检测的适配度手提袋在数据集里通常是单类目标检测难点不在类别区分而在目标形态。纸质手提袋剪影清晰边缘稳定塑料手提袋会起皱、反光、半透明手柄和袋身的连接处还经常被误判成两个目标。单类模型的训练难度比多类低但对样本多样性的要求更高因为所有样本都属于同一个类模型必须有足够的形态差异才能学会泛化。YOLOv8在工业项目里处理这类目标已经很成熟。它的C2f结构对中小目标有一定增益而且Ultralytics框架从训练、验证到导出ONNX再到部署链路完整省去格式转换的麻烦。手提袋目标在画面里一般占中等面积不需要大感受野yolov8s作为起步模型通常足够。如果数据量超过两千张且目标形态差异大再考虑yolov8m。注意不要在首轮就直接上yolov8x。数据集在千张级别时大模型很容易过拟合验证集的mAP波动会很明显还会拖慢训练速度得不偿失。4.2 训练参数图像尺寸、batch、epoch、anchor的默认值与调整理由用Ultralytics框架训练时命令行的标准写法如下yolo train datadata.yaml modelyolov8s.pt epochs150 imgsz640 batch16 \ patience30 lr00.001 optimizerauto seed42参数说明imgsz640是多数目标检测数据集的标配训练尺寸640能保留手提袋的纹理细节同时显存占用可控。batch16在12G显存的显卡上是安全值显存不够就要降到8batch减半后学习率也要跟着小幅下探否则收敛曲线会抖动。epochs150搭配patience30表示连续30个epoch验证集没有提升就提前停止不用傻等跑满150轮。lr00.001是从预训练权重继续微调的常用起点不容易跳过最优区间。optimizerauto让框架自动选择优化器省心且结果稳定。seed固定用于复现同一个数据集不同随机种子跑出来的mAP可能差3到5个点不固定种子的话对比实验就没意义。anchor不需要手工调。YOLOv8默认在训练集上重新聚类anchor这对手提袋这种宽高比接近1比1.3的目标完全够用。除非框的长宽比出现极端情况比如超宽横幅或者极窄长条才需要手工指定anchor手提袋检测数据集基本不会走到这一步。4.3 数据划分的“同场景不跨集”原则数据划分是训练前最不该偷懒的一步。手提袋数据集的图片通常来自几个固定场景超市收银台、便利店门口、礼品包装台。如果不做干预直接随机切分同一场景的不同帧会同时出现在训练集和验证集里。模型实际学到的是场景记忆不是手提袋本身。验证集分数虚高现场换个摄像头机位立即翻车。我的做法是先按拍摄场景给图片分组保证同一个场景的所有图片只能进入train、val、test其中一个集合再在分组基础上做随机抽样。比例按7:2:1但核心约束是场景不串组。如果文件名里带场景ID可以用GroupKFold来做分组划分比如文件名前缀是store01_0001.jpg取前缀作为分组依据。import os from sklearn.model_selection import GroupKFold images sorted(os.listdir(images/train)) groups [img.split(_)[0] for img in images] gkf GroupKFold(n_splits5) for train_idx, val_idx in gkf.split(images, groupsgroups): print(ftrain: {len(train_idx)}, val: {len(val_idx)})GroupKFold保证同一个分组ID不会同时出现在训练集和验证集避免上述场景泄漏。实际落地时建议把场景ID整理成meta表格先看每个场景的图片数量和手提袋数量分布再决定哪个场景进训练、哪个进验证、哪个留作现场测试。5. 手提袋检测踩坑实录5个反复出现的血泪问题和修复5.1 小目标漏检标注框太“诚实”了现象训练完成后测试画面远处的小手提袋经常漏检近处大目标全中。验证集mAP看起来还行但实际拍到的场景里小目标几乎全丢。原因远处手提袋在画面里像素面积小标注框又贴得很紧没有留边量。模型在低分辨率特征层上看到的目标结构不完整尤其手柄部分被框切掉后整个目标的特征就被打散了。解决先把imgsz从640提升到768或者896小目标在更大输入上会有更清晰的响应。同时确认mosaic增强是开着的Ultralytics默认开启但很多复现者为了“加快训练速度”手动关掉这对手提袋这种依赖上下文信息的目标反而丢了有效样本。增强背景多样性也有帮助把部分训练图里的手提袋区域裁掉只留背景作为负样本加入训练能抑制“画面下半部分出现东西就当目标”的误检。5.2 loss下降但mAP不动数据划分没洗牌现象训练loss一路下降看起来很健康但验证集mAP从第20个epoch开始就在0.6附近波动怎么调参都上不去。原因最常见的就是数据划分方式有问题。按文件名排序后直接取前80%当训练集、后20%当验证集如果前80%全是室内灯光下的照片后20%全是有阳光直射的门口照片光照差异就是一道天然的隔离墙模型在室内学到的特征套不到室外场景。解决用第4章的GroupKFold重新划分按场景、光照、机位分组保证训练集和验证集的光照分布基本一致。同时对比train loss和val loss的差距val loss始终高一大截先怀疑分布问题再怀疑过拟合。分组划分后重新训练通常mAP能回到正常水平。5.3 验证集高分、现场失分背景过拟合现象训练时val mAP到了0.93部署到新环境一测准确率直接腰斩框全打在货架边缘和门框上。原因数据集里的手提袋大多出现在收银台、出入口这些固定位置模型学会了用位置和环境信息代替目标本身的特征。背景样本不够多样时模型记住的是“画面下方有货架的格子就可能是袋”根本不是手提袋的纹理和轮廓。解决增加背景负样本是最直接的手段。做法是把不含手提袋的店铺照片加进训练集配一个空的TXT文件让模型看到“这里没有目标”的真实样本。同时做相邻帧去重同一个场景间隔不到1秒的连续截图视觉上几乎相同重复样本会持续强化位置偏差。5.4 类名不匹配导致训练报错现象训练刚开始就报错提示类别id超出范围或者训练能跑但验证时所有类别的AP都是0。原因VOC格式XML里的类别名和data.yaml的names列表不一致。比如数据集里大部分XML写的是handbag有一小部分写的是bag转换时未匹配的被跳过TXT里丢了标注又或者data.yaml里写了三个类但TXT里只有两个类的id框架校验类别数量时直接报错。最怕的是不报错但id错位比如bag映射成0handbag映射成1实际标注全乱。解决转换前做一次全数据集的类别普查把XML里出现过的所有类别名统计打印出来和data.yaml逐字核对。这是一个5分钟的小检查能省下半天定位时间。from collections import Counter import xml.etree.ElementTree as ET import os counter Counter() for xml_file in os.listdir(Annotations): root ET.parse(os.path.join(Annotations, xml_file)).getroot() for obj in root.findall(object): counter[obj.find(name).text] 1 print(counter)5.5 手柄和褶皱导致边界框抖动现象同一个手提袋在相邻几帧里的预测框大小忽大忽小中心点稳定但框的面积跳变部署端的跟踪逻辑跟着出错。原因手提袋的形态不固定。手柄完全张开时标注框能框到整个轮廓手柄收拢时标注框只框袋身。如果同一批数据的标注规范不统一模型学到了一个模棱两可的边界推理时就在两个尺度间来回试探。塑料手提袋的透明区域也会让模型把袋内物品边缘当成袋子的边界。解决在准备阶段定死标注规范。约定框必须包含整个可见轮廓含手柄手柄被遮挡时只框到可见区域。规范本身是不是最优不重要重要的是所有人按同一个标准标注。框抖动还可以在后处理环节缓解视频流检测时对相邻帧的框做IOU平滑或者用上一帧结果对当前帧做约束但这只是治标根因还是标注一致性问题。6. 验证手提袋检测效果用PR曲线、F1曲线和bad case分析把关6.1 三张曲线定位真实问题单看mAP容易掩盖问题。手提袋这种单类检测我更看重三张曲线。第一是PR曲线如果曲线在置信度较高的区间突然掉下来说明高置信度区域还存在误检模型学了不该学的特征。第二是F1和置信度的关系曲线峰值对应的置信度可以直接指导部署阈值设置。比如F1峰值出现在置信度0.72附近部署时阈值就该设在0.45到0.55区间而不是用默认的0.25否则误检会明显变多。第三是val loss相对train loss的差距差距控制在20%以内属于正常超过30%就要怀疑过拟合。Ultralytics训练完会在输出目录自动生成这些曲线图。不看等于浪费。我习惯把每次训练的曲线图按日期归档翻车时回头对照比只看控制台输出的日志有效得多。6.2 用脚本统计bad case的分布训练完做一次推理把预测框和真值框做匹配将漏检和误检的样本分门别类落盘统计这些bad case集中在哪些场景和距离段。false_positives [] missed_boxes [] for result in results: tp, fp, fn compute_tp_fp_fn(result.gt_boxes, result.pred_boxes, iou_thr0.5) if fp 0: false_positives.append(result.image_path) if fn 0: missed_boxes.append(result.image_path) print(误检样本数:, len(false_positives)) print(漏检样本数:, len(missed_boxes))这里需要说明compute_tp_fp_fn函数要根据你实际用的框架写Ultralytics的predict结果可以直接取到框坐标和置信度用NumPy做简单的IOU匹配就行。重点不是代码本身而是把问题归类误检多去看背景负样本够不够漏检多去看小目标配置和标注精度两者都多先怀疑数据质量。做这个项目最大的教训是格式转换和目录组织带来的损失远比模型调参大。数据集自带VOC和YOLO双格式标签原本是为了方便实际落地时如果转换脚本有问题、类别映射不一致、目录结构不匹配再好的标注数据也会变成脏数据。把这套从VOC转YOLO到训练验证的流程固定下来之后换其他检测数据集也只是改类别名的事。希望帮到你少走一圈弯路把精力留给真正难调的模型和部署。本文还有配套的精品资源点击获取