十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

智能售货柜商品数据集:VOC标注格式转YOLO实战指南

智能售货柜商品数据集:VOC标注格式转YOLO实战指南 简介面向智能售货柜商品识别这一实际落地场景这份目标检测训练集采用Pascal VOC标准格式用户拿到后无需清洗或坐标转换即可直接接入YOLO、SSD、Faster R-CNN等主流检测框架。压缩包内共2000个文件全部为xml标签文件整体约993MB配合对应训练图像可快速构建标准数据集讲解有序的命名规则有助于按文件名快速定位图像与标注的对应关系。适合算法工程师、科研人员及计算机视觉方向学生用于模型训练、性能对比和论文实验。数据集中包含类别名称txt便于训练时高效完成标签映射减少额外编码工作数据集划分上训练集与验证集、测试集分卷发布更有助于开展严谨的模型评估与调参。目前已有289人学习浏览尤其适合零售智能化、无人售货柜场景下的商品检测与计数任务既可用于从零训练也可作为预训练模型微调的数据基础。1. 智能售货柜商品训练集VOC标注格式下能直接跑的目标检测起点上周有个朋友让我搭售货柜视觉结算 demo第一个问题不是算法而是数据2950 张训练图像加 2950 个 VOC 格式 XML 标签压缩后约 1GB附带一个类别 txt。这个体量做商业级识别不够但把“图像采集 → 标注解析 → 模型训练 → 边界排查”的完整链路跑通足够了。很多人拿到这种智能售货柜目标检测数据集后习惯性先去 PaddleDetection 这类框架里找现成配置其实 VOC 标注格式已经是目标检测里最通用的交换格式YOLO、SSD、Faster R-CNN 都有成熟工具做转换。这篇文章围绕这份训练集拆开讲XML 里每个字段到底在说什么、怎么转成 YOLO 能吃的标签、训练时在类别分布和数据划分上会踩到哪些坑。2. VOC标注格式解析从annotation到bndbox的坐标体系2.1 XML的五个关键节点size、object、bndbox与difficult标准 VOC 标注文件以annotation为根节点里面至少包含图像尺寸size、文件名filename和若干object目标块。真正决定检测框位置的是bndbox里的四个像素坐标xmin、ymin、xmax、ymax。这份售货柜数据集的 XML 结构基本遵守这一套规范因此解析难度不大但有几个字段在实际处理时必须留意。节点含义本项目可能出现的问题size/width、size/height原图宽高是坐标归一化的分母如果与实际 JPG 分辨率不一致转换后框会漂移filename标注对应的图片文件名历史工程里常与 XML 文件名不一致不建议直接当索引object/name商品类别字符串注意空格、中文或全角字符必须与 classes.txt 对齐bndbox/xmin...ymax目标框左上角和右下角的像素坐标少数框可能出现越界或xmax xminobject/difficult难易标识1 表示该目标比较难是否丢弃会直接影响训练样本质量拿到 XML 后我建议先做一个最基础的动作解压后不要急着改格式先看一个 XML 的内容确认filename指向的图片是否存在。常见做法是用grep或者文本编辑器打开一个文件观察size的宽高和同目录图片的实际分辨率是否一致。若来自不同采集批次某些 XML 可能没有folder或segmented节点但只要object和bndbox完整就不影响训练。2.2 用ElementTree批量解析2950个XML并统计类别分布转换前先做一份全局体检统计每个类别出现多少次、有多少 XML 没有目标、有没有非法坐标。这里用 Python 标准库xml.etree.ElementTree即可不需要引入笨重的标注软件依赖。import xml.etree.ElementTree as ET from pathlib import Path from collections import defaultdict xml_dir Path(./Annotations) xml_files sorted(xml_dir.glob(*.xml)) stats defaultdict(int) empty_files [] for xml_path in xml_files: root ET.parse(xml_path).getroot() objs root.findall(object) if not objs: empty_files.append(xml_path.name) continue for obj in objs: name obj.find(name).text stats[name] 1 print(XML 总数:, len(xml_files)) print(类别统计:, dict(stats)) print(空标注文件数:, len(empty_files)) print(示例:, empty_files[:5])这段代码先把所有 XML 路径排序保证输出顺序可复现然后逐文件解析object节点。stats用defaultdict(int)累积每个类别的实例数空标注文件单独记录。输出的类别统计要和 resources 里的 classes.txt 逐项对比重点看 classes.txt 里的类别是否都覆盖到了以及有没有 XML 中出现了 classes.txt 之外的名称。如果出现缺失说明部分 XML 是旧版标注需要单独清洗。统计之后一定要处理空标注文件。YOLO 训练允许一张图没有目标但验证集评估时会被算作漏检而且如果随机采样时正负样本比例失衡训练早期 loss 会异常高。我的通用做法是把空标注 XML 对应的图片从训练目录移到exclude/文件夹而不是直接删除原数据方便后续回溯。2.3 归一化坐标的换算逻辑VOC 保存的是左上角和右下角的绝对像素坐标而 YOLO 系列要求每行标签是class x_center y_center width height并且所有数值归一化到 01。归一化公式很简单但要搞清楚一个概念分母是size里的原图宽高不是某个固定值比如 640。否则缩放后被检测框会整体偏移。def voc_to_yolo_norm(x1, y1, x2, y2, img_w, img_h): x_center ((x1 x2) / 2.0) / img_w y_center ((y1 y2) / 2.0) / img_h box_w (x2 - x1) / img_w box_h (y2 - y1) / img_h return x_center, y_center, box_w, box_h这里除法必须用浮点数Python 3 中/已经是浮点除法img_w、img_h建议从 XML 里读取而不是用别的途径硬编码。中心点加宽高的表示方式天然不受图像左右翻转的影响这也是 YOLO 在做 Mosaic、随机裁剪增强时可以直接对标签做仿射变换的原因。如果你后续要跑 SSD需要的是 VOC 格式或 LMDB 格式则不需要做这一步但理解这个映射关系能帮你排查为什么某个类别所有框都偏到左下角。实际排查时我会写一个简单脚本从 XML 中随机抽 5 张图在图上画框并打印归一化后的值肉眼确认坐标方向正确再去批量转换。3. 训练集迁移实战VOC格式转YOLOv8的images/labels目录3.1 目录规划训练集、验证集与labels同级这份智能售货柜商品数据集官方拆成了训练集、验证集和测试集三个资源当前拿到的是训练集。因此本地目录要先按 YOLO 约定铺好后面训练时就不需要再改路径配置。推荐结构如下dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── classes.txt └── dataset.yamlUltralsytics 的 YOLOv8 默认会去images/的同级目录找labels/比如图片在dataset/images/train/a.jpg标签必须在dataset/labels/train/a.txt。目录层级和文件名前缀要一一对应否则启动训练时会报大量 image 无标签的 warning。由于当前只有训练集验证集需要先从训练集里按一定比例切出来用于训练过程中的 loss 监控和 early stop而不是直接用官方验证集避免训练时偷偷看到评测数据。我会在转换脚本里把 10% 的图片划到val/剩余 90% 留在train/。3.2 完整转换脚本从XML生成YOLO标签并划分验证集下面是一个可以直接落地的转换脚本假定 XML 放在./Annotationsclasses.txt 放在脚本同级目录。脚本会把生成的标签按 9:1 比例随机拆到labels/train/和labels/val/同时打印每个类别在 train 和 val 中的实例数方便抽查切分是否均匀。import xml.etree.ElementTree as ET from pathlib import Path import random xml_dir Path(./Annotations) out_dir Path(./labels) train_dir out_dir / train val_dir out_dir / val train_dir.mkdir(parentsTrue, exist_okTrue) val_dir.mkdir(parentsTrue, exist_okTrue) with open(classes.txt, encodingutf-8) as f: class_names [line.strip() for line in f if line.strip()] def convert_xml(xml_path, out_file): root ET.parse(xml_path).getroot() size root.find(size) if size is None: return 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_names: continue cls_id class_names.index(name) bbox obj.find(bndbox) x1 float(bbox.find(xmin).text) y1 float(bbox.find(ymin).text) x2 float(bbox.find(xmax).text) y2 float(bbox.find(ymax).text) x1 max(0.0, min(x1, img_w)) y1 max(0.0, min(y1, img_h)) x2 max(0.0, min(x2, img_w)) y2 max(0.0, min(y2, img_h)) if x2 x1 or y2 y1: continue x_center (x1 x2) / 2 / img_w y_center (y1 y2) / 2 / img_h box_w (x2 - x1) / img_w box_h (y2 - y1) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(out_file, w, encodingutf-8) as f: f.write(\n.join(lines)) random.seed(2025) xml_files sorted(xml_dir.glob(*.xml)) for xml_path in xml_files: if random.random() 0.10: out_file val_dir / (xml_path.stem .txt) else: out_file train_dir / (xml_path.stem .txt) convert_xml(xml_path, out_file) print(转换完成。train:, len(list(train_dir.glob(*.txt))), val:, len(list(val_dir.glob(*.txt))))脚本核心是convert_xml函数它把 XML 里的像素坐标先裁剪到图像范围内再转成归一化中心点格式。裁剪这个动作很关键因为售货柜监控画面边缘经常有半个商品标注时可能写出xmax大于img_w的越界框不裁掉会导致负宽度或超出特征图范围。类别名必须和 classes.txt 严格一致顺序也不能乱YOLO 标签里只存类别 id一旦 classes.txt 顺序调整所有标签都要重新生成。随机划分时设置random.seed(2025)保证每次执行结果一致如果二次执行脚本后写的 txt 会覆盖旧文件不会重复累积。3.3 生成dataset.yaml并启动YOLOv8训练标签转换完成后还要生成一个dataset.yaml。这里要特别注意 classes.txt 的读取顺序names列表必须和脚本里的class_names完全一致否则模型训练时类别对不上框。path: /your/absolute/path/dataset train: images/train val: images/val nc: 2 names: - cola - water示例中的nc和names需要按实际 classes.txt 修改。path建议写绝对路径YOLOv8 在 Windows 上偶尔会因为相对路径解析不出图片目录而直接报内存错误。类别少的情况下train和val都可以简单用相对路径但方便多机复现我习惯统一绝对路径。然后启动训练yolo detect train datadataset.yaml modelyolov8s.pt epochs150 batch16 imgsz640 patience30参数建议值说明epochs150如果类别少且数据量小100 轮左右就能收敛设大一点配合 early stopbatch816售货柜图像多为 1080pbatch 过大容易显存溢出imgsz640 或 768商品在柜内占比往往不大低于 640 会把小目标越缩越小patience30验证集 mAP 连续 30 轮不提升就停止节省训练时间如果你的目标是拿现有权重做对比实验从yolov8s.pt开始比从头训练更容易收敛。若想验证 VOC 格式到 SSD 的迁移思路也类似先把 XML 转成标准 VOC 并整理成JPEGImages、Annotations、ImageSets/Main三个目录再用ssd_pascal.py里的create_data_lmdb脚本生成 SSD 需要的 LMDB。但实际项目里从数据量上看YOLOv8 更省事SSD 可以作为后续对比基线而不是首选。4. 训练集排坑类别不平衡、difficult标记与数据切分4.1 用awk与Python统计类别分布训练集转换完成后第一件事是看类别分布是否均匀。直接用 awk 统计 YOLO 标签中每类出现次数比看 XML 更快cat labels/train/*.txt | awk {c[$1]} END {for (i in c) print i, c[i]} | sort -k2nr这条命令把labels/train/下所有 txt 文件拼到一起按空格切分后取第一列类别 id 计数最后按数量降序排列。输出结果如果某个类别数量是另一个的 20 倍训练时模型会偏向多数类少数类 AP 可能掉到 0.3 以下。售货柜商品里常见的是矿泉水、可乐这类流量商品的样本远多于口香糖这时我不会盲目加样本而是先给少数类配上更低的学习率或使用类别平衡采样。具体实现上可以在 YOLOv8 训练时对包含少数类的图片做重复采样或者轻量一点直接用 PIL 统计每个类别的框面积分布看是不是小目标占比过高。4.2 difficult1的商品框该丢弃还是保留标准 VOC 里difficult表示目标不清晰、遮挡严重或极小官方 VOD 评测时这些框不参与 AP 计算。但这套售货柜数据采集自真实监控环境货柜边缘的商品经常被柜门边框截断如果全部丢弃相当于主动去掉一批难样本模型的泛化能力会打折。我的建议是分场景处理策略适用场景代价直接丢弃difficult1类别干净、目标完整度高对截断商品漏检严重保留且不特殊处理大部分框质量尚可低质量框可能干扰收敛保留但降低 loss 权重想学边缘商品又不想被带偏需要改 loss成本高如果你经过观察决定丢弃只需在上一章的转换脚本中增加一个判断for obj in root.findall(object): diff obj.find(difficult) if diff is not None and diff.text.strip() 1: continue这段代码在遍历object节点时先检查difficult文本为 1 就跳过。注意difficult节点不一定存在于所有 XML 中所以用is not None保护。从实际经验看售货柜识别宁可保留一部分 difficult 样本因为用户把商品拿出再放回时大量动作都发生在遮挡状态下。4.3 按文件名时间戳切分训练验证集避免同一商品泄漏售货柜视频流每秒产生大量近似帧如果直接把 XML 列表按 9:1 随机切同一个商品的连续帧会同时出现在训练集和验证集里最后得到的 mAP 会虚高。这里关键在于按文件名里的时间戳前缀分组。项目中的文件名类似ori_XYG2020122914213602358911-1_0.xml中间的2020122914213602358911可以看作采集时间戳后面的-1_0可能是相机编号或帧序号。按时间戳前缀分组后再把整个组放入训练集或验证集能最大程度避免“同物同帧”泄漏。from collections import defaultdict import random, re def timestamp_key(xml_name: str): m re.match(rori_XYG(\d{17})-\d_\d, xml_name) return m.group(1) if m else None groups defaultdict(list) for xml_path in xml_files: key timestamp_key(xml_path.stem) if key: groups[key].append(xml_path) group_items list(groups.values()) random.Random(42).shuffle(group_items) split_idx int(len(group_items) * 0.9) train_groups group_items[:split_idx] val_groups group_items[split_idx:]这段代码先把同一时间戳下所有 XML 聚到同一个列表再打乱组顺序切分而不是打乱单张 XML。random.Random(42)显式指定随机种子方便不同机器复现结果。切分完成后训练集图片数可能和官方训练集总数不完全一致因为一个时间戳组里通常包含多张近似帧这是合理的。后续如果下载了官方验证集和测试集资源不要再执行这里的二次切分直接使用官方划分即可。切分后还建议做一次文件名核对labels/train里的每个 txt 是否能和images/train里的图片一一对应。常见做法是执行ls images/train | wc -l和ls labels/train | wc -l对比数量若有差异用comm -23找出缺失的项避免训练时大量跳过。5. 提升售货柜商品小目标检测的进阶技巧5.1 增强策略与多尺度训练参数售货柜监控视角固定但商品尺寸跨度很大底层货架的商品在画面里可能只有 40×60 像素。YOLOv8 默认的增强参数偏通用目标检测对这类小目标场景可以针对性调整。训练时在命令行追加增强参数即可覆盖默认配置yolo detect train datadataset.yaml modelyolov8s.pt epochs150 batch16 imgsz640 hsv_h0.015 hsv_s0.7 hsv_v0.4 scale0.5 fliplr0.0 mosaic1.0 mixup0.2hsv_h、hsv_s、hsv_v控制色调、饱和度和明度的扰动幅度售货柜不同批次商品外包装颜色会有差异给一点 HSV 扰动有助于模型不依赖特定色相。fliplr0.0是这里的关键饮料瓶和零食包装上的文字、logo 做水平翻转后会变成镜像推理时反而增加误检。scale0.5控制图像缩放扰动配合 Mosaic 增强能让模型看到更多尺寸下的商品。如果发现小目标表现还是很差可以把骨干网络换成yolov8s-p2.yaml让模型输出 P2 层更大分辨率的特征图。5.2 用预测结果反向验证难样本训练完成后不要只看验证集 mAP。售货柜结算场景更关心漏检和重复计费误差所以我一般会在验证集上跑一轮预测把置信度阈值调到 0.25 输出所有框再和 XML 标签对比重点看模型漏掉的是不是difficult1的截断商品。yolo predict modelruns/detect/train/weights/best.pt sourceimages/val conf0.25 save_txtTrue这条命令会为每张验证图生成一个对应 txt保存到runs/detect/predict/labels。对比模型输出和 ground truth 时使用 IoU 0.5 作为判定阈值不要用 0.75因为密集场景下商品框本身目视差异就很小。如果某个类别的漏检集中在画面左下角则优先考虑在原始图像上做区域感知增强或者干脆把摄像头角度往后拉减少边缘畸变对目标框的影响。本文还有配套的精品资源点击获取
返回列表