简介:面向智能零售结算场景的商品识别检测数据集,适合从事计算机视觉目标检测的开发者、学生及算法工程师,用于训练和评估自动结算系统中的商品识别模型。数据集共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 - xmin,h = 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_ratio=0.2, copy_images=True): img_dir = Path(img_dir) xml_dir = Path(xml_dir) out_dir = Path(out_dir) # 读取类别映射 with open(classes_file, "r", encoding="utf-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(parents=True, exist_ok=True) lbl_out.mkdir(parents=True, exist_ok=True) 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(f"train: {len(train_samples)}, val: {len(val_samples)}") voc_to_yolo("JPEGImages", "Annotations", "yolo_dataset", "classes.txt", val_ratio=0.2)逻辑上先读类别表,再逐 XML 解析,做边界裁剪和有效性检查,最后按比例划分并写出。参数val_ratio=0.2对 5000 张来说验证集约 1000 张,够用;如果类别极不均衡,建议改成按类别分层抽样,否则小类可能全被分到训练集。copy_images=True会占双倍磁盘,但路径干净,适合第一次跑通。
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 张零售柜数据的常用配置。imgsz=640是 YOLOv8 的默认输入尺寸,零售柜商品通常占画面比例不大,640 够用;如果小目标漏检多,可以提到 960,但显存和耗时都会涨。batch=16是 24G 显存下的稳妥值,你显存小就降到 8。epochs=100对 5000 张来说通常能收敛,但要看mAP50曲线,如果 80 轮后还在涨就加到 150。
yolo detect train \ data=retail.yaml \ model=yolov8n.pt \ imgsz=640 \ epochs=100 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ patience=20 \ device=0 \ project=runs/retail \ name=exp1参数说明:model=yolov8n.pt是最小模型,适合先跑通链路;如果精度不够换yolov8s.pt或yolov8m.pt。lr0初始学习率 0.01 是默认值,数据量小可以降到 0.005 防止震荡。patience=20表示 20 轮没提升就早停,省时间。device=0指定第一块 GPU,CPU 训练会慢到怀疑人生。
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 分辨率下,batch=16在 8G 显存上会炸。解决办法是降batch到 8 或 4,同时开amp=True混合精度。如果还不行,把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(parents=True, exist_ok=True) 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 和光照变化上,数据集只是起点,持续补数据和迭代标注才是常态。希望帮到你。
本文还有配套的精品资源,点击获取