ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

智能零售柜商品识别:VOC转YOLO格式与YOLOv8训练实战

智能零售柜商品识别:VOC转YOLO格式与YOLOv8训练实战 简介面向智能零售结算场景的商品识别检测数据集适合从事计算机视觉目标检测的开发者、学生及算法工程师用于训练和评估自动结算系统中的商品识别模型。数据集共5422张已标注图片涵盖113类商品并已按训练集3796张、验证集1084张、测试集542张完成划分可直接用于PaddleDetection、PaddleX等主流框架。压缩包共8057个文件包含2685张jpg图像、2685个xml标注文件与2687个txt标签文件同时提供VOC与YOLO两种格式兼容YOLO系列、SSD、PP-YOLO等算法包体约881.5MB。已有597人学习下载。借助完整标注与标准划分读者可快速复现检测流程、对比不同模型表现相关项目最高mAP可达96.41为智能零售商品识别研究提供可靠的数据基础。1. 智能零售柜商品识别5000 张双格式数据集到底能跑出什么智能零售柜的商品识别说白了就是让柜子里的摄像头知道“谁拿了什么”。这件事听起来简单真正落地时最卡脖子的往往不是模型结构而是数据。你拿不到足够多、足够杂、标注足够准的商品图后面调参调出花来也没用。这个标题里的“5000张VOC格式和YOLO格式标签”核心价值就在于它同时给了两套标注VOC 的 XML 适合做数据审查和格式转换的中间态YOLO 的 TXT 可以直接喂给 YOLOv5/v8 这类检测器。对做智能柜、无人零售、货架盘点的人来说这是一份能直接开跑的起点数据。适合谁适合手上已经有柜体硬件、想先验证识别链路是否通的人也适合学生做课程设计或比赛 baseline。但别指望 5000 张就能覆盖你真实场景的全部 SKU它更像一块敲门砖不是终点。2. 先搞清楚 VOC 和 YOLO 两套标签的差异在哪2.1 标注坐标系不同转换时最容易翻车VOC 格式的标注文件是 XML每个目标用bndbox记录xmin, ymin, xmax, ymax这是绝对像素坐标原点在图片左上角。YOLO 格式则是每张图对应一个 TXT每行class_id x_center y_center width height全部是归一化到 0~1 的相对值。很多人第一次转换时直接拿 VOC 的坐标除以图片宽高就写进 TXT结果训练时框全飘了原因通常是忘了 VOC 的坐标是包含边界的而 YOLO 的宽高是框的绝对宽高再归一化不是右下角坐标。正确做法是先算w xmax - xminh ymax - ymin再分别除以图片宽高。2.2 类别映射表必须两边一致VOC 的 XML 里写的是类别名字符串YOLO 的 TXT 里写的是从 0 开始的整数索引。如果你直接拿 VOC 的类别名去训 YOLO第一轮就会报类别不匹配。常见做法是维护一个classes.txt每行一个类别名行号就是 YOLO 的 class_id。转换脚本里读 XML 的name去classes.txt里查索引查不到就报错并记录别默默跳过否则你会得到一份“看起来能跑但少类”的数据集。2.3 图片和标签的目录结构决定你后面省不省心VOC 常见结构是JPEGImages/放图Annotations/放 XML。YOLO 常见结构是images/train/、images/val/、labels/train/、labels/val/图片和标签同名不同后缀。如果你拿到的压缩包是混在一起的第一步不是急着训模型而是先按下面这个脚本把目录理清楚。我一般会先跑一遍统计看看有没有图片没有对应标签、或者标签里类别为空的情况。import os import xml.etree.ElementTree as ET from pathlib import Path # 统计 VOC 数据集中图片与 XML 的配对情况 img_dir Path(JPEGImages) xml_dir Path(Annotations) img_stems {p.stem for p in img_dir.glob(*.jpg)} xml_stems {p.stem for p in xml_dir.glob(*.xml)} only_img img_stems - xml_stems only_xml xml_stems - img_stems print(f图片无标签: {len(only_img)}) print(f标签无图片: {len(only_xml)}) # 检查每个 XML 的类别分布 from collections import Counter cls_counter Counter() for xml_path in xml_dir.glob(*.xml): tree ET.parse(xml_path) root tree.getroot() for obj in root.findall(object): name obj.find(name).text cls_counter[name] 1 print(类别分布:, cls_counter.most_common())这段脚本先做配对检查再做类别统计。参数上你只需要改img_dir和xml_dir两个路径。如果only_img数量很大说明你的数据里有一批图没标训练前要么补标要么删掉如果某个类别只有个位数后面训练时这类基本学不动考虑合并或补充。3. 把 VOC 转成 YOLO转换脚本与四个边界坑3.1 转换脚本的核心逻辑与参数说明下面这个脚本是我在多个零售柜项目里反复用过的版本输入是 VOC 的JPEGImages和Annotations输出是 YOLO 的images和labels同时按 8:2 划分 train/val。关键参数有三个classes_file指定类别映射val_ratio控制验证集比例copy_images决定是复制还是软链接图片。零售柜场景图片不大我一般直接复制省得后面路径乱了。import shutil import random import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image def voc_to_yolo(img_dir, xml_dir, out_dir, classes_file, val_ratio0.2, copy_imagesTrue): img_dir Path(img_dir) xml_dir Path(xml_dir) out_dir Path(out_dir) # 读取类别映射 with open(classes_file, r, encodingutf-8) as f: classes [line.strip() for line in f if line.strip()] cls_to_id {name: i for i, name in enumerate(classes)} # 收集所有有效样本 samples [] for xml_path in xml_dir.glob(*.xml): stem xml_path.stem img_path img_dir / f{stem}.jpg if not img_path.exists(): continue tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in cls_to_id: continue bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 边界裁剪防止越界 xmin max(0, min(xmin, w - 1)) ymin max(0, min(ymin, h - 1)) xmax max(0, min(xmax, w - 1)) ymax max(0, min(ymax, h - 1)) if xmax xmin or ymax ymin: continue xc (xmin xmax) / 2.0 / w yc (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{cls_to_id[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) if lines: samples.append((img_path, lines)) random.shuffle(samples) split int(len(samples) * (1 - val_ratio)) train_samples samples[:split] val_samples samples[split:] for subset, subset_samples in [(train, train_samples), (val, val_samples)]: img_out out_dir / images / subset lbl_out out_dir / labels / subset img_out.mkdir(parentsTrue, exist_okTrue) lbl_out.mkdir(parentsTrue, exist_okTrue) for img_path, lines in subset_samples: if copy_images: shutil.copy(img_path, img_out / img_path.name) else: os.symlink(img_path.resolve(), img_out / img_path.name) with open(lbl_out / f{img_path.stem}.txt, w) as f: f.write(\n.join(lines)) print(ftrain: {len(train_samples)}, val: {len(val_samples)}) voc_to_yolo(JPEGImages, Annotations, yolo_dataset, classes.txt, val_ratio0.2)逻辑上先读类别表再逐 XML 解析做边界裁剪和有效性检查最后按比例划分并写出。参数val_ratio0.2对 5000 张来说验证集约 1000 张够用如果类别极不均衡建议改成按类别分层抽样否则小类可能全被分到训练集。copy_imagesTrue会占双倍磁盘但路径干净适合第一次跑通。3.2 四个边界坑越界、空标签、同名不同类、图片损坏第一个坑是越界框。VOC 标注里偶尔出现xmax大于图片宽度的情况直接归一化会得到大于 1 的值YOLO 训练时虽然不报错但框会异常。上面脚本里做了min(xmax, w-1)的裁剪这是血泪经验别省。第二个坑是空标签。有些 XML 里object数量为 0转换后 TXT 是空文件。YOLO 默认会忽略空标签图但如果你用某些版本的数据加载器空文件可能导致报错。我的做法是直接跳过这些图不写入数据集。第三个坑是同名不同类。零售柜里经常有“可乐罐”和“可乐瓶”这种相似类别如果classes.txt里写重了或者映射错了模型会学得四不像。转换前一定用前面的统计脚本看一眼类别分布确认没有拼写不一致。第四个坑是图片损坏。5000 张里难免有几张打不开的PIL 读取时会抛异常。建议在转换前加一轮Image.open校验损坏的直接剔除并记录文件名别让它在训练中途炸掉。4. 用 YOLOv8 训练这份数据集的完整命令与参数4.1 环境准备与数据配置文件假设你已经装好了 ultralytics没装的话pip install ultralytics一行搞定。数据配置文件retail.yaml要写清楚路径和类别数。注意path写你实际的数据集根目录train和val写相对路径。nc是类别数names按classes.txt的顺序写顺序错了模型输出的类别名就全乱。# retail.yaml path: /data/yolo_dataset train: images/train val: images/val nc: 10 names: 0: cola_can 1: cola_bottle 2: water_bottle 3: chips_small 4: chips_large 5: candy_box 6: tissue_pack 7: milk_box 8: juice_bottle 9: unknown_item4.2 训练命令与关键参数怎么调下面这条命令是我在单卡 3090 上跑 5000 张零售柜数据的常用配置。imgsz640是 YOLOv8 的默认输入尺寸零售柜商品通常占画面比例不大640 够用如果小目标漏检多可以提到 960但显存和耗时都会涨。batch16是 24G 显存下的稳妥值你显存小就降到 8。epochs100对 5000 张来说通常能收敛但要看mAP50曲线如果 80 轮后还在涨就加到 150。yolo detect train \ dataretail.yaml \ modelyolov8n.pt \ imgsz640 \ epochs100 \ batch16 \ lr00.01 \ lrf0.01 \ patience20 \ device0 \ projectruns/retail \ nameexp1参数说明modelyolov8n.pt是最小模型适合先跑通链路如果精度不够换yolov8s.pt或yolov8m.pt。lr0初始学习率 0.01 是默认值数据量小可以降到 0.005 防止震荡。patience20表示 20 轮没提升就早停省时间。device0指定第一块 GPUCPU 训练会慢到怀疑人生。4.3 训练过程中看什么指标跑起来后重点盯三个box_loss是否稳定下降mAP50是否在涨cls_loss有没有突然飙升。如果cls_loss飙升多半是类别映射错了或者某类样本太少。如果mAP50卡在 0.3 以下先别调模型回去看数据——大概率是标注框质量差或者类别混淆严重。零售柜场景里透明包装和反光商品最容易让模型翻车这时候不是加数据就是加数据增强没有捷径。5. 避坑与排查零售柜数据集训练中最容易踩的五个雷5.1 现象训练 loss 正常但验证 mAP 极低原因通常是训练集和验证集分布不一致。比如你的 5000 张里某些类别只在训练集出现验证集里没有mAP 自然低。解决方法是划分时按类别分层抽样确保每个类别在 train 和 val 里都有。另一个原因是验证集的标签格式和训练集不一致比如一个用了归一化坐标一个用了绝对坐标检查labels/val里的数值范围是否在 0~1。5.2 现象模型把“可乐罐”识别成“可乐瓶”原因一般是两类样本数量差距大或者标注时边界模糊。零售柜里罐和瓶的形状差异其实不小但如果你的数据里瓶的样本是罐的 10 倍模型会偏向瓶。解决办法是过采样小类或者在损失函数里给小类更高权重。YOLOv8 不直接支持类别权重但你可以通过复制小类图片来平衡。5.3 现象推理时框位置偏移严重原因多半是转换时坐标算错了。回去检查 VOC 的xmax是否被当成了宽YOLO 的width是(xmax - xmin) / w不是xmax / w。这个错误非常常见而且训练时 loss 看起来正常只有推理时才会暴露。建议转换后随机抽 10 张图用脚本把 YOLO 标签画回图上肉眼确认框对齐。5.4 现象训练到一半报显存不足原因可能是batch太大或者imgsz太高。5000 张 640 分辨率下batch16在 8G 显存上会炸。解决办法是降batch到 8 或 4同时开ampTrue混合精度。如果还不行把imgsz降到 512但小目标召回会掉需要权衡。5.5 现象验证集图片没有对应标签文件原因通常是转换脚本里跳过了空标签图但验证集划分时又把这些图算进去了。检查labels/val目录下是否有缺失的 TXT。我的习惯是转换后跑一遍for f in images/val/*.jpg; do test -f labels/val/$(basename $f .jpg).txt || echo $f; done把没有标签的图删掉保证训练和验证的输入一致。6. 进阶技巧用 ReID 思路补足商品识别的类内差异6.1 为什么检测之外还要考虑 ReID零售柜里同一类商品可能有不同包装版本比如可乐的节日限定罐和普通罐检测模型会把它们都标成“可乐罐”但如果你要做精准库存需要区分具体 SKU。这时候检测只能给你“这是可乐”ReID 才能告诉你“这是哪一款可乐”。ReID 数据集的核心是同一目标在不同视角、光照下的多张图你可以从 5000 张检测数据里按类别裁剪出小图构建一个简易的 ReID 训练集。6.2 从检测数据构建 ReID 样本的脚本下面这段代码把 YOLO 格式的标签还原成裁剪图按类别存到不同文件夹每个文件夹里就是同一类商品的不同实例。注意这里用的是训练集图片验证集不要动否则评估会泄漏。import cv2 from pathlib import Path def crop_by_yolo(img_dir, lbl_dir, out_dir, classes): img_dir Path(img_dir) lbl_dir Path(lbl_dir) out_dir Path(out_dir) for img_path in img_dir.glob(*.jpg): lbl_path lbl_dir / f{img_path.stem}.txt if not lbl_path.exists(): continue img cv2.imread(str(img_path)) h, w img.shape[:2] with open(lbl_path) as f: for i, line in enumerate(f): parts line.strip().split() if len(parts) ! 5: continue 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) x1, y1 max(0, x1), max(0, y1) x2, y2 min(w, x2), min(h, y2) if x2 x1 or y2 y1: continue crop img[y1:y2, x1:x2] cls_name classes[cls_id] save_dir out_dir / cls_name save_dir.mkdir(parentsTrue, exist_okTrue) cv2.imwrite(str(save_dir / f{img_path.stem}_{i}.jpg), crop) classes [cola_can, cola_bottle, water_bottle, chips_small, chips_large, candy_box, tissue_pack, milk_box, juice_bottle, unknown_item] crop_by_yolo(yolo_dataset/images/train, yolo_dataset/labels/train, reid_crops, classes)逻辑是按 YOLO 标签的归一化坐标还原出绝对框裁剪后按类别存。参数上classes必须和retail.yaml里的names顺序完全一致否则类别名会错位。裁剪出来的小图可以直接喂给 ReID 模型也可以用来做数据增强把同一 SKU 的裁剪图贴回不同背景扩充检测数据。6.3 一个验证技巧用混淆矩阵定位最差的类训练完 YOLOv8 后runs/retail/exp1/下会生成confusion_matrix.png。别只看 mAP打开这张图找对角线最浅的那一格那就是模型最混淆的两个类。零售柜里常见的是“矿泉水”和“苏打水”互混因为瓶身透明、标签相似。定位到之后要么补这两类的区分性样本要么在标注时把边界画得更紧。我一般会把这个矩阵打印出来贴在工位上下一轮数据迭代就盯着这几个类补。这套流程跑下来5000 张数据从 VOC 到 YOLO 再到训练和 ReID 扩展基本能让你在半天内看到第一个可用的检测结果。但记住零售柜的真实挑战永远在长尾 SKU 和光照变化上数据集只是起点持续补数据和迭代标注才是常态。希望帮到你。本文还有配套的精品资源点击获取
返回列表