简介:面向智能零售柜商品检测场景的目标检测数据集资源,适合从事新零售视觉算法研发的工程师、学生及竞赛选手使用,可用于智能零售柜商品识别项目落地,也可作为通用新零售场景商品检测数据的补充。数据集采集自真实智能零售柜监控场景,涵盖罐装饮料、袋装零食等常见商品,标注标签共113个商品类别,采用labelimg标注,质量较高,并同步提供VOC(xml)、COCO(json)、YOLO(txt)三种主流格式,可直接投入YOLO等算法训练。资源包为1个PDF文件,大小约5.77MB,内含数据集基本情况介绍与获取方式说明。随附YOLO11一键训练脚本,支持GPU(GPUs)、CPU及Mac(M芯片)多平台训练方案,并给出博主训练结果日志供参考。目前已有413人学习,适合希望快速搭建商品检测基线、验证多平台训练流程的读者参考使用。
1. 智能零售柜商品检测:5000 张图、三种标签格式和一套能跑通的三平台训练脚本
智能零售柜的商品检测,说白了就是让摄像头认出货架上谁拿了什么、还剩几瓶。它和通用目标检测最大的区别在于:商品 SKU 密集、遮挡严重、同类商品外观差异极小(比如两种矿泉水只差一个瓶盖颜色),而且柜内光照固定、背景干净,模型很容易在训练集上过拟合。我手上这套数据集给了 5000 张实拍图,覆盖饮料、零食、日用品等常见品类,同时提供 VOC、COCO、YOLO 三种标签格式,省去了自己写转换脚本的麻烦。配套的 YOLO11 一键训练脚本支持 GPU、CPU、Mac 三平台,新手拿一台笔记本就能先跑通流程,有卡的机器再上量。这篇笔记按「数据集长什么样 → 三种格式怎么选怎么转 → 脚本怎么改参数 → 训练翻车怎么排查 → 怎么验证模型真的能用」的顺序讲,目标是你读完能直接复现一遍。
2. 先看清数据集:5000 张图里到底有什么、标注质量怎么判断
拿到一个目标检测数据集,第一件事不是急着训练,而是先搞清楚它的分布和标注质量。5000 张图听起来不少,但如果某一类商品只有几十个框,训出来的模型对这类就是瞎猜。智能零售柜场景还有个特点:同一张图里可能有 20 个以上的商品实例,密集程度远超 COCO 的平均水平,这对 YOLO 的 anchor 匹配和 NMS 后处理都是压力测试。
2.1 商品类别分布与实例密度
先统计每个类别的实例数,别只看图片数。常见做法是用脚本遍历标签目录,把每个类的框数累加,画一个柱状图。如果发现长尾——比如「某品牌气泡水」只有 30 个框,而「矿泉水」有 800 个框——就要考虑合并细分类别或者做重采样。零售柜场景里,我一般会把「同系列不同口味」先合并成一个大类训练,等 baseline 稳定了再拆细。
实例密度方面,用每张图的平均框数来衡量。COCO 平均每图 7 个框左右,零售柜数据集经常到 15 到 25 个。密度高意味着小目标多,输入分辨率不能太低,640 是底线,条件允许上 960 或 1280。下面这段脚本用来统计类别分布和每图框数:
import os import glob from collections import Counter # label_dir 指向 YOLO 格式的 labels 目录,每个 txt 一行一个框 label_dir = "datasets/retail/labels/train" cls_counter = Counter() box_per_img = [] for txt in glob.glob(os.path.join(label_dir, "*.txt")): with open(txt) as f: lines = [l for l in f.readlines() if l.strip()] box_per_img.append(len(lines)) for line in lines: cls_id = int(line.split()[0]) # YOLO 格式第一列是类别 id cls_counter[cls_id] += 1 print("类别实例数:", dict(sorted(cls_counter.items()))) print("平均每图框数:", sum(box_per_img) / len(box_per_img)) print("最大单图框数:", max(box_per_img))这段代码的关键点是读 YOLO 格式的 txt,每行结构是class_id x_center y_center width height,坐标都是归一化到 0 到 1 的。跑完你会得到两个判断依据:类别实例数低于 100 的类要警惕,平均每图框数超过 15 的要把输入尺寸调大。参数上,label_dir换成你自己的路径即可,如果标签是 VOC 的 XML,需要先转成 YOLO 再统计,转换方法下一章讲。
2.2 标注质量的三条快速检查
标注质量决定模型上限。5000 张图人工标完,难免有漏标和框不准。我一般做三个检查:第一,看有没有宽或高为 0 的退化框,这种框会让损失函数直接 NaN;第二,看有没有坐标超出 0 到 1 范围的框,说明标注时越界了;第三,随机抽 20 张图把框画出来肉眼过一遍,重点看密集区域有没有漏标。
import os import glob label_dir = "datasets/retail/labels/train" bad = [] for txt in glob.glob(os.path.join(label_dir, "*.txt")): with open(txt) as f: for i, line in enumerate(f): parts = line.split() if len(parts) != 5: bad.append((txt, i, "字段数不对")) continue _, x, y, w, h = map(float, parts) if w <= 0 or h <= 0: bad.append((txt, i, "退化框")) if not (0 <= x <= 1 and 0 <= y <= 1 and 0 <= w <= 1 and 0 <= h <= 1): bad.append((txt, i, "坐标越界")) print(f"问题标注共 {len(bad)} 条") for b in bad[:20]: print(b)这段脚本把三类问题一次性扫出来。发现退化框直接删掉那一行,坐标越界的要回原图确认是标注错误还是转换 bug。注意,YOLO 格式的坐标是中心点加宽高,不是左上角加右下角,转换时最容易在这里翻车,后面避坑章节会细说。
3. VOC、COCO、YOLO 三种格式:什么时候用哪个、怎么互转
三种格式不是随便给的,它们对应不同的训练框架和评估工具。VOC 格式是 XML,一个图一个文件,适合传统框架和某些标注工具;COCO 格式是单个 JSON,包含 images、annotations、categories 三个主键,是 COCO 评估指标(AP、AR)的标准输入;YOLO 格式是每图一个 txt,最简洁,Ultralytics 的 YOLO11 直接吃这个。选错格式不会报错,但会让你在评估和可视化时多绕很多路。
3.1 三种格式的结构差异与选型建议
| 格式 | 文件组织 | 坐标表示 | 典型用途 |
|---|---|---|---|
| VOC | 每图一个 XML | 左上角 xmin/ymin + 右下角 xmax/ymax,绝对像素 | 传统框架、LabelImg 默认输出 |
| COCO | 单个 JSON | [x, y, width, height],绝对像素,左上角起点 | COCO 评估、Detectron2、MMDetection |
| YOLO | 每图一个 txt | class_id + 归一化中心点与宽高 | Ultralytics YOLO 系列直接训练 |
我的建议是:训练用 YOLO 格式,评估用 COCO 格式,标注交换用 VOC 格式。这套数据集三种都给了,你只需要确认三者的类别顺序一致。常见坑是 VOC 的类别名和 YOLO 的 class_id 对不上,比如 VOC 里「矿泉水」是第 3 类,YOLO 里写成了 2,训出来的模型会把矿泉水认成别的。转换前先把类别映射表固定下来。
3.2 VOC 转 YOLO:脚本与四个边界坑
VOC 转 YOLO 的核心是把绝对坐标转成归一化中心点坐标。公式是:x_center = (xmin + xmax) / 2 / img_width,y_center = (ymin + ymax) / 2 / img_height,w = (xmax - xmin) / img_width,h = (ymax - ymin) / img_height。看着简单,但边界情况很多。
import os import glob import xml.etree.ElementTree as ET classes = ["water", "cola", "snack", "daily"] # 必须和训练配置里的 names 顺序一致 xml_dir = "datasets/retail/voc_annotations" out_dir = "datasets/retail/labels/train" os.makedirs(out_dir, exist_ok=True) for xml_path in glob.glob(os.path.join(xml_dir, "*.xml")): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) lines = [] for obj in root.findall("object"): cls_name = obj.find("name").text.strip() if cls_name not in classes: continue # 类别不在映射表里就跳过,避免 class_id 错位 cls_id = classes.index(cls_name) 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, img_w)) xmax = max(0, min(xmax, img_w)) ymin = max(0, min(ymin, img_h)) ymax = max(0, min(ymax, img_h)) if xmax <= xmin or ymax <= ymin: continue # 裁剪后退化,丢弃 x_c = (xmin + xmax) / 2 / img_w y_c = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f}") name = os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(out_dir, name + ".txt"), "w") as f: f.write("\n".join(lines))四个边界坑分别是:类别名不在映射表里导致 class_id 错位、坐标超出图像范围、裁剪后框退化、以及 XML 里 size 字段缺失或为 0 导致除零。代码里都做了处理。参数上,classes列表的顺序必须和训练时 data.yaml 里的 names 完全一致,这是最容易翻车的地方。另外注意,如果原图是灰度图或带 alpha 通道,img_w 和 img_h 要以实际读到的尺寸为准,不能盲信 XML 里的 size。
3.3 COCO 与 YOLO 互转:评估和训练各取所需
COCO 转 YOLO 时,COCO 的 bbox 是 [x, y, width, height],左上角起点,绝对像素。转成 YOLO 需要先算中心点再归一化。反过来 YOLO 转 COCO 时,要补齐 image_id、category_id、annotation id 这些字段,否则 pycocotools 评估会报错。我一般用 pycocotools 自带的 COCO 类来读写,避免手写 JSON 出错。
from pycocotools.coco import COCO import os coco = COCO("datasets/retail/annotations/instances_train.json") img_ids = coco.getImgIds() out_dir = "datasets/retail/labels/train" os.makedirs(out_dir, exist_ok=True) # COCO 的 category_id 不一定从 0 连续,要建立到 YOLO 连续 id 的映射 cat_ids = sorted(coco.getCatIds()) cat_map = {cid: i for i, cid in enumerate(cat_ids)} for img_id in img_ids: info = coco.loadImgs(img_id)[0] w, h = info["width"], info["height"] ann_ids = coco.getAnnIds(imgIds=img_id) lines = [] for ann in coco.loadAnns(ann_ids): x, y, bw, bh = ann["bbox"] x_c = (x + bw / 2) / w y_c = (y + bh / 2) / h lines.append(f"{cat_map[ann['category_id']]} {x_c:.6f} {y_c:.6f} {bw/w:.6f} {bh/h:.6f}") name = os.path.splitext(info["file_name"])[0] with open(os.path.join(out_dir, name + ".txt"), "w") as f: f.write("\n".join(lines))关键点是cat_map:COCO 的 category_id 可能是 1、3、7 这种不连续的值,直接拿来当 YOLO 的 class_id 会越界。必须建立连续映射,并且把映射关系记下来,评估时再映射回去。参数上,instances_train.json换成你的 COCO 标注文件,如果同时有 train 和 val,两个都要转。
4. YOLO11 一键训练脚本:三平台参数怎么设、显存怎么省
这套脚本的价值在于抹平平台差异。GPU 机器上走 CUDA,Mac 上走 MPS,纯 CPU 也能跑但慢。YOLO11 是 Ultralytics 维护的,训练入口就是model.train(),但参数设不对,要么显存爆,要么训不动。下面按平台拆开讲。
4.1 GPU、CPU、Mac 三平台的启动参数
GPU 平台最省心,装好 CUDA 版 PyTorch 后直接指定device=0。CPU 平台要把device设成cpu,同时把workers调小,因为 CPU 上多进程数据加载反而拖慢。Mac 的 M 系列芯片走 MPS,device="mps",但要注意 MPS 对某些算子支持不全,遇到报错就退回 CPU。
from ultralytics import YOLO import torch # 自动选设备:有 CUDA 用 GPU,Mac 用 MPS,否则 CPU if torch.cuda.is_available(): device = 0 elif torch.backends.mps.is_available(): device = "mps" else: device = "cpu" model = YOLO("yolo11n.pt") # 从预训练权重起步,小数据集别从零训 model.train( data="datasets/retail/data.yaml", epochs=100, imgsz=960, # 零售柜小目标多,640 容易漏检 batch=16, # GPU 显存 8G 以上可以到 32 device=device, workers=4 if device != "cpu" else 2, patience=20, # 20 轮没提升就早停,省时间 cache=True, # 图片缓存到内存,5000 张约几个 G )参数逐个说:imgsz=960是因为零售柜商品小,640 分辨率下小目标特征太弱;batch=16是 8G 显存的保守值,显存够就往上加,不够就降到 8;patience=20是早停,避免过拟合后白跑;cache=True把图片缓存进内存,5000 张图大概占 3 到 5 个 G,内存小的机器设成disk或False。workers在 CPU 上设 2 就够,设多了进程切换开销大。
4.2 data.yaml 的写法与类别顺序
data.yaml 是训练配置的核心,写错一个字段训练直接起不来。它要指定 train、val 的图片目录,类别数 nc,以及类别名列表 names。names 的顺序必须和标签里的 class_id 一一对应,错一位模型就全乱。
path: datasets/retail train: images/train val: images/val nc: 4 names: 0: water 1: cola 2: snack 3: daily注意path是数据集根目录,train和val是相对路径。Ultralytics 会自动把images替换成labels去找标签,所以图片和标签的目录结构要对称:images/train对应labels/train。如果标签放在别处,要在 yaml 里显式写labels字段。类别数 nc 必须等于 names 的长度,多一个少一个都会报错。
4.3 显存不够时的四个降级手段
显存爆了别急着换卡,先试这四个手段。第一,降 batch,从 16 降到 8 甚至 4,这是最直接的。第二,开梯度累积,batch=4配合accumulate=4,等效 batch 还是 16,但显存占用按 4 算。第三,降 imgsz,从 960 降到 640,显存占用大概降一半,但小目标召回会掉。第四,用更小的模型,yolo11n 换成 yolo11n 已经是最小的了,那就只能降分辨率。我一般按「先降 batch,再开累积,最后降分辨率」的顺序试,因为降分辨率对精度伤害最大。
model.train( data="datasets/retail/data.yaml", epochs=100, imgsz=640, batch=4, accumulate=4, # 梯度累积,等效 batch=16 device=0, amp=True, # 混合精度,省显存还提速 )amp=True是自动混合精度,GPU 上默认开,能省 30% 左右显存。注意 CPU 和 MPS 上 amp 支持有限,遇到 NaN 就关掉。梯度累积的accumulate参数要和 batch 配合,batch * accumulate才是等效 batch size,别设太大导致训练不稳定。
5. 训练翻车排查:损失不降、mAP 上不去、显存爆的常见原因
训练过程中出问题是常态,关键是怎么快速定位。下面五条是我在零售柜数据集上真实踩过的坑,按「现象 → 原因 → 解决」写。
5.1 损失从第一轮就 NaN
现象:训练日志里 box_loss 和 cls_loss 直接显示 nan,几轮后进程退出。原因通常是标签里有退化框(宽或高为 0)或者坐标越界,导致损失计算时出现除零或 log(0)。解决:回到 2.2 节的检查脚本,把所有退化框和越界框清掉。另一个可能是学习率太大,lr0默认 0.01,小数据集上可以降到 0.001。
5.2 损失降但 mAP 一直卡在低位
现象:训练损失平稳下降,但验证集 mAP50 卡在 0.3 左右上不去。原因多半是类别不平衡或标注漏标。零售柜里矿泉水可能有上千个框,某个小众零食只有几十个,模型倾向于预测多数类。解决:先看每个类的 AP,找出拖后腿的类,对这类做重采样或加类别权重。漏标问题只能靠人工复查,重点看密集货架区域。
5.3 显存爆在第一个 epoch 中途
现象:训练开始正常,跑了几十张图后报 CUDA out of memory。原因是cache=True把图片缓存进内存的同时,数据增强还在生成大张量,峰值显存超了。解决:把cache设成False或disk,或者降imgsz。另外检查workers是不是设太大,每个 worker 都会占一份显存。
5.4 Mac 上 MPS 报算子不支持
现象:Mac 上device="mps"训练到某个算子时报错,提示 not implemented。原因是 MPS 后端对某些 PyTorch 算子支持不全,尤其是自定义的损失函数。解决:先升级 PyTorch 到最新版,很多算子已经补上了。还不行就把device设成cpu,慢但能跑通。或者用PYTORCH_ENABLE_MPS_FALLBACK=1环境变量让不支持的算子回退到 CPU。
5.5 验证集 mAP 高但实际柜子里认不出
现象:验证集 mAP50 有 0.85,但拿真实柜子的视频去测,漏检严重。原因是验证集和训练集同分布,都是摆拍图,而真实场景有反光、遮挡、角度变化。解决:从真实柜子里再采一批图,哪怕只有几百张,加进验证集重新评估。如果掉得厉害,说明模型过拟合了摆拍分布,需要补充真实场景的训练数据。
6. 验证模型真能用:从 mAP 到柜内实测的最后一公里
mAP 只是参考,模型能不能上柜子,得用真实视频流测。我一般分三步验证:先看单帧推理的框准不准,再看连续帧的稳定性,最后看拿取动作的识别率。单帧推理用model.predict(),把置信度阈值调到 0.4 左右,零售柜场景宁可多检也别漏检,因为漏检意味着用户拿了东西没记录。
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.predict( source="test_videos/shelf.mp4", conf=0.4, # 置信度阈值,零售柜建议 0.4 到 0.5 iou=0.5, # NMS 的 IoU 阈值,密集商品可以降到 0.4 imgsz=960, stream=True, # 视频流式推理,省内存 device=0, ) for r in results: boxes = r.boxes # 统计每帧的类别和数量,和实际货架对比 print(boxes.cls.tolist(), boxes.conf.tolist())conf=0.4是零售柜的经验值,太低会误检,太高会漏检。iou=0.5控制 NMS 合并,商品密集时降到 0.4 能保留更多相邻框。stream=True对视频推理很重要,不然会把所有帧读进内存。跑完把每帧的检测结果和实际货架对比,重点看遮挡区域和反光区域。
连续帧稳定性看的是同一个商品在相邻帧里的框是否跳动。如果框在抖,说明模型对该商品的特征提取不稳定,可能是训练数据里这个角度太少。解决办法是把这个角度的图补进训练集,或者开测试时增强(TTA),但 TTA 会拖慢推理速度,实时场景慎用。
最后一步是拿取动作识别。零售柜的核心逻辑是「谁拿了什么」,单帧检测只能告诉你货架上有什么,要结合前后帧的差异判断拿取。常见做法是维护一个货架状态表,每帧检测结果和上一帧对比,某个商品消失就记一次拿取。这里有个坑:用户手挡住商品时,检测会短暂丢失,导致误判。解决办法是加一个时间窗口,连续 3 帧都检测不到才认为商品被拿走。
我自己的习惯是,任何模型上线前,先用真实柜子的视频跑一遍,人工数 100 次拿取动作,看模型报了多少次、漏了多少次、误报了多少次。这三个数比 mAP 实在得多。零售柜场景里,漏报比误报严重,因为漏报意味着丢货,误报只是多记一笔。所以阈值宁可调低,配合时间窗口过滤误报。这套数据集和脚本能帮你快速跑通 baseline,但真正上柜子,还得靠真实场景的数据反复迭代。希望帮到你。
本文还有配套的精品资源,点击获取