简介:面向智能零售柜商品检测场景的目标检测数据集配套说明文档,主要服务于新零售算法开发者、相关专业学生及竞赛团队,解决智能零售柜场景中商品检测训练数据获取与快速上手问题。资源以 PDF 形式提供,共 1 个文件、大小 5.77MB,内附数据集基本情况介绍与网盘获取方式;该数据集包含 5000 张真实智能零售柜监控场景图片,涵盖罐装饮料、袋装零食等常见商品,标注类别多达 113 个。数据标注采用 LabelImg 完成并提供 VOC(XML)、COCO(JSON)、YOLO(TXT) 三种格式,可直接用于 YOLO 等主流算法训练。同时文档介绍了配套的 YOLO11 一键训练脚本,支持 GPU、CPU、Mac(M 芯片) 多平台运行,并提供博主训练结果日志作为参考。目前已有 413 人学习浏览,适合希望快速获得高质量商品检测数据并完成模型训练验证的中高级开发者,也可作为新零售场景算法落地的参考数据储备。
1. 智能零售柜商品检测:这套数据集和脚本把你从最劝退的三件事里解放出来
在智能零售柜项目里,算法团队接到最多的需求不是"训练一个模型",而是"在柜子里把商品认准"。柜内摄像头位置固定、隔着玻璃、有反光有遮挡,盒子里的饮料和零食还经常前后叠放,公开数据集上的模型搬过来往往直接失灵。这个标题打包了三样东西:5000张针对柜内商品检测的图片,每张图都配好VOC、COCO、YOLO三种格式的标签文件,外加一套能够在GPU、CPU、Mac三平台直接跑起来的YOLO11一键训练脚本。一句话说清楚它的价值:不教你造轮子,而是把"找数据、转格式、配环境"这三件最容易劝退的事先替你做完,让你把精力花在调模型和上线。适合手里有零售柜业务但算法刚起步的团队,也适合想完整跑一遍YOLO11训练到部署流程的开发者。
2. 5000张图怎么设计才不白拍:采集规范、类目划分与标注卡控
数据集的照片数量只是表象,真正决定模型上限的是采集时的场景覆盖和标注时的一致性。5000张图听起来不多,但零售柜是受限场景:SKU数量有限、摄像头位置固定、商品排列有一定规律,只要采集设计到位,这个量级足够把模型训到可用的程度。
2.1 采集方案:光照、角度、SKU差异怎么控制
常见做法是分批次采集,而不是一天拍完。第一批覆盖不同柜型、不同层板高度、不同摄像头安装角度;第二批专门去覆盖补货前、补货中、补货后的状态差异;第三批则针对同一个SKU的不同摆放姿态和数量组合。每批之间留出时间间隔,这样能自然引入光照变化——早上的阳光、中午的灯光、夜间的补光灯,对柜内玻璃反光和商品颜色都会产生实质影响。
相机选型上要注意分辨率与视场角的匹配。柜内摄像头常见的是200万到500万像素,如果图幅里一个饮料瓶只有十几个像素宽,那后续再怎么调模型都救不回来。我一般会先拍几张测试图,算一下最小目标在图像里的像素尺寸,目标短边小于20像素的,要么调整安装位置,要么放弃这类极端场景。
每个SKU的样本量不能平均分配。零售柜里的销量是长尾分布,爆款商品占大多数,冷门商品偶尔才出现。采集时按照"爆款多拍、冷门保底"的原则,保证每个SKU至少有80到150个标注实例,而不是机械地每类拍100张。另外一定要拍一批空柜图和只有背景的图,这部分在训练时作为负样本,能够显著降低空柜误报——零售柜的场景里,空柜误报比漏检更让运营头疼。
2.2 标注口径:框怎么打、边界怎么切、极小目标怎么处理
标注工具的选择直接影响效率。常见做法是先用LabelImg做VOC格式的初标,再用X-AnyLabeling这类支持半自动辅助的工具做二次标注。工具本身不关键,关键是标注口径要在动工前定死,否则返工成本极高。
框的边界以商品的可见轮廓为准。完整露出的商品按最小外接矩形标;被遮挡的商品只标可见部分,不凭想象把被挡住的部分补全;对于夹在两件商品之间、可见面积小于整体20%的,直接放弃不标。这里有个容易松动的细节:边缘贴边的商品,框偏移1到2个像素问题不大,但如果每张图都习惯性地往里收一点,最后框会整体偏小,影响mAP计算。
极小目标的处理需要单独定规则。柜内摄像头视角下,底层的瓶装水经常因为透视关系变得很小,这类目标如果完全放弃,模型就学不到"远处也有商品";但如果硬标,标注噪声又会很大。我的做法是:短边小于12像素的目标直接跳过,12到20像素的目标正常标注,但这类目标在质检时会被重点抽检。
质检环节至少要过两道关。第一道关是脚本自动检查,统计每张图的标注框数量、框的宽高分布、有没有越界坐标;第二道关是人眼抽检,按5%到10%的比例随机抽图,重点看遮挡和极小目标的框是否贴合。如果两道关通过率低,就让标注员返工。
2.3 类目设计与数据分布:想要模型不飘,先让分布别歪
类目设计上最常见的错误是把"品牌+口味+容量"全拼进一个类目名,导致类目数爆炸、每类样本量都不够。合理做法是先按"货道上的摆放单位"来定义类目,比如某品牌可乐330ml罐装是一个类目,500ml瓶装是另一个类目,但不同口味的同容量包装可以合并,因为模型看到的外观几乎一样。
5000张图对应的类目数建议控制在15到30个之间。类目太少,模型体现不出区分能力;类目太多,每类的平均样本量会被稀释。表格里的分布可以按这个思路设计:
| 类目类型 | 建议占比 | 说明 |
|---|---|---|
| 爆款SKU | 40% | 单类样本量最大,负责保证主体召回率 |
| 常规SKU | 40% | 每类保持均衡,覆盖不同摆放姿态 |
| 冷门SKU | 15% | 样本量少但必须保留,防止漏检 |
| 负样本/空柜 | 5% | 无目标图,压制空柜误报 |
训练时如果发现类别不均衡导致冷门类AP很低,不要急着删数据,优先试类目权重或者对冷门类做简单的mosaic增强,后面避坑章会细讲。
3. VOC/COCO/YOLO三种标签格式:转换逻辑、目录组织与四个边界坑
同一个数据集给出三种格式,听起来是重复劳动,实际上是在兼容不同的训练习惯和工具链。VOC适合配合LabelImg等工具做二次修正,COCO适合用Detectron2或mmdetection跑对比实验,YOLO是YOLO系列训练脚本直接吃的格式。多数情况下你只需要其中一种,但保底给全可以避免后续工具链切换时重新标注。
3.1 三种格式的组织形态和本质区别
VOC格式是一张图对应一个XML文件,XML里记录每个目标的name、pose、truncated、difficult和bndbox坐标;COCO格式是把所有图片和目标汇总到一个JSON文件里,通过images、annotations、categories三个数组建立关联;YOLO格式则是每张图对应一个txt文件,每行是"class_id x_center y_center width height",坐标做了归一化。三者本质上记录的是同一批框,但组织方式和坐标表达不同。
| 维度 | VOC | COCO | YOLO |
|---|---|---|---|
| 文件组织 | JPEGImages + Annotations + ImageSets | 单JSON + 图片目录 | images + labels |
| 坐标表达 | xmin, ymin, xmax, ymax(绝对像素) | bbox为x, y, w, h(绝对像素) | x_center, y_center, w, h(归一化0~1) |
| 类别表达 | 文件夹名或XML内name | categories数组的id | txt每行首列的class_id |
| 典型适用 | 目标检测教学、标注工具 | mmdetection、Detectron2 | YOLO系列训练栈 |
容易忽略的一个点是:YOLO的归一化坐标是相对于原图宽高的,如果训练时imgsz设置为640而原图是1280宽,模型内部会先做resize,标签跟着一起缩放,不需要你手动改坐标。但COCO的bbox如果不经过resize就直接用,就会和训练时的像素坐标对不上,这一点在混用工具时经常翻车。
3.2 转换脚本:一条命令把VOC转成COCO和YOLO
拿到一个VOC标注的数据集,最常见的诉求是同时产出COCO和YOLO格式。下面这个脚本可以完成转换,同时做了坐标越界检查和空标注检查。
import os import xml.etree.ElementTree as ET import json from collections import defaultdict def voc_to_yolo_and_coco(xml_dir, img_dir, output_dir, class_list): """ xml_dir: VOC格式XML文件目录 img_dir: 图片目录 output_dir: 输出目录 class_list: 类别列表,顺序即YOLO的class_id顺序 """ os.makedirs(f"{output_dir}/images", exist_ok=True) os.makedirs(f"{output_dir}/labels", exist_ok=True) class_to_id = {name: idx for idx, name in enumerate(class_list)} coco_images, coco_annotations, coco_categories = [], [], [] ann_id = 0 for xml_name in sorted(os.listdir(xml_dir)): if not xml_name.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, xml_name)) root = tree.getroot() img_file = root.find("filename").text width = int(root.find("size/width").text) height = int(root.find("size/height").text) img_id = len(coco_images) coco_images.append({ "id": img_id, "file_name": img_file, "width": width, "height": height }) # 每个带标注的XML,生成一个同名txt txt_lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_to_id: continue bndbox = obj.find("bndbox") xmin = int(float(bndbox.find("xmin").text)) ymin = int(float(bndbox.find("ymin").text)) xmax = int(float(bndbox.find("xmax").text)) ymax = int(float(bndbox.find("ymax").text)) # 越界修正:坐标不能超出图片范围 xmin = max(0, xmin); ymin = max(0, ymin) xmax = min(width, xmax); ymax = min(height, ymax) if xmax <= xmin or ymax <= ymin: continue # YOLO归一化坐标 x_center = (xmin + xmax) / 2.0 / width y_center = (ymin + ymax) / 2.0 / height box_w = (xmax - xmin) / width box_h = (ymax - ymin) / height txt_lines.append(f"{class_to_id[name]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") # COCO标注 coco_annotations.append({ "id": ann_id, "image_id": img_id, "category_id": class_to_id[name], "bbox": [xmin, ymin, xmax - xmin, ymax - ymin], "area": (xmax - xmin) * (ymax - ymin), "iscrowd": 0 }) ann_id += 1 txt_name = os.path.splitext(xml_name)[0] + ".txt" with open(f"{output_dir}/labels/{txt_name}", "w") as f: f.write("\n".join(txt_lines)) for cls in class_list: coco_categories.append({"id": class_to_id[cls], "name": cls}) with open(f"{output_dir}/annotations.json", "w") as f: json.dump({"images": coco_images, "annotations": coco_annotations, "categories": coco_categories}, f, indent=2) print(f"处理完成,共 {len(coco_images)} 张图,{ann_id} 个标注框")脚本的逻辑分三段:第一段解析XML,读取图片名、尺寸和每个目标的坐标;第二段对坐标做越界修正,同时过滤掉宽高为负的脏数据,然后分别生成YOLO格式的txt行和COCO格式的annotation记录;第三段汇总写入annotations.json。
参数说明:class_list的顺序决定了YOLO格式里的class_id,一定要和后续训练时data.yaml里的类别顺序保持一致,否则会出现"模型训完了,输出标签全错位"的诡异问题。另外代码里使用了sorted排序扫描XML文件,保证多次运行生成的image_id稳定,这对后续增量标注很关键。
3.3 格式转换中常见的四个边界坑
坐标越界是最常见的问题。标注工具偶尔会产出一个框超出图片边界,YOLO的归一化坐标因此会大于1或小于0。这类脏数据不处理,训练时loss会突然跳一个尖峰,然后模型开始震荡。上面的脚本用clamp方式做了裁剪,但如果越界特别严重的框,裁剪后宽高变得很窄,也不是好样本,建议直接过滤。
空标注文件会让训练中断。有些图确实没有目标,但YOLO训练时读到空的txt文件会报"no labels found"。处理方式是在转换后加一个检查逻辑,把所有空txt文件记录到exclude列表中,在data.yaml里用exclude配置跳过这些图。
浮点输出精度是隐蔽问题。YOLO的标签文件用float保存归一化坐标,如果只保留3位小数,对于4000像素宽的图,误差会达到几个像素,训练时影响不明显,但转回VOC做人工校验时框会明显偏移。我在设计上保留6位小数,就是这个原因。
类别顺序漂移是最难排查的坑。很多人把VOC转YOLO后只改了数据路径,没有重新生成data.yaml,训练跑起来loss正常下降,但预测结果却张冠李戴——名字是可乐,框里是薯片。排查方式就是检查labels文件夹里txt文件第一行的数字范围,并用脚本把class_id映射回类别名,逐一核对。
4. YOLO11一键训练脚本:GPU/CPU/Mac三平台的环境准备与参数调优
YOLO11的训练入口收敛在Ultralytics框架上,官方API暴露得比较干净,但"一键训练"真正要解决的是环境探测和参数默认值的问题。三个平台的坑完全不一样:GPU踩的是CUDA和torch版本匹配,Mac踩的是MPS后端兼容性,CPU踩的是慢到让人怀疑人生。
4.1 三平台环境差异:CUDA、MPS、CPU fallback怎么选
NVIDIA GPU平台,第一步不是装ultralytics,而是确认torch能不能调用显卡。很多人在Windows上装完torch发现CUDA不可用,症结在于pip默认安装了CPU版本。常见做法是先去PyTorch官网用conda或者pip命令安装对应CUDA的torch,再回来装ultralytics。如果你的显卡比较新,报错"requires device with capability <= (9, 0) but your GPU has capability (12, 0)",说明torch版本太老不认识新卡,升级torch即可,不用怀疑显卡坏了。
Mac平台的Apple Silicon芯片支持MPS加速,torch可以直接利用,但注意MPS后端的内存管理和CUDA不太一样,batch size开得比同显存GPU小。检测是否可用的标准写法如下:
import torch def get_device(): if torch.cuda.is_available(): return "cuda" elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): return "mps" else: return "cpu" device = get_device() print(f"当前训练设备:{device}")这段代码的核心是探测顺序:优先CUDA,其次MPS,最后CPU。hasattr(torch.backends, "mps")是必要的,因为老版本torch在非Mac平台上根本没有mps这个属性,直接访问会报AttributeError。
CPU平台跑YOLO11不是不能跑,而是要做好心理准备。5000张图、640分辨率、100个epoch,在主流CPU上大概需要几十个小时。建议只在环境验证或小数据调试时用CPU,正式训练还是找一台GPU机器。
4.2 一键训练脚本骨架:一份配置同时管住数据和模型
训练脚本的常见做法是拆成train.sh和train.py两个文件,shell负责探测平台并设置并行参数,Python文件负责加载数据和模型。核心逻辑是自动探测设备、自动设置worker数、自动选择预训练权重路径。
#!/bin/bash # train.sh - 一键训练YOLO11 # 用法: ./train.sh set -e # 自动探测设备类型 if python -c "import torch; torch.cuda.is_available()" 2>/dev/null | grep -q "True"; then DEVICE="cuda" elif python -c "import torch; print(torch.backends.mps.is_available())" 2>/dev/null | grep -q "True"; then DEVICE="mps" else DEVICE="cpu" fi echo "使用设备: $DEVICE" # Mac与CPU平台建议减少worker数,避免内存占满 if [ "$DEVICE" = "cpu" ]; then WORKERS=0 else WORKERS=4 fi python train.py --device "$DEVICE" --workers "$WORKERS"# train.py import argparse from ultralytics import YOLO def main(): parser = argparse.ArgumentParser() parser.add_argument("--device", default="cuda") parser.add_argument("--workers", type=int, default=4) parser.add_argument("--imgsz", type=int, default=640) parser.add_argument("--epochs", type=int, default=100) parser.add_argument("--batch", type=int, default=16) args = parser.parse_args() data_yaml = "data.yaml" # 里面标注train/val路径和类别列表 model = YOLO("yolo11n.pt") # n/s/m/l/x按显存和精度选择 model.train( data=data_yaml, epochs=args.epochs, imgsz=args.imgsz, batch=args.batch, device=args.device, workers=args.workers, patience=20, # 验证集20轮不提升就早停 lr0=0.01, # 初始学习率 weight_decay=0.0005, project="runs/retail_cabinet", name="exp1", exist_ok=True, ) if __name__ == "__main__": main()这里有一个值得强调的参数:patience早停。5000张图的训练集不算大,如果在基础学习率下硬跑满100个epoch,后半段大概率在过拟合。设patience=20后,验证集mAP连续20个epoch不提升就会自动停止,省下来的时间可以做AB实验。
workers参数在Mac上很敏感。MPS模式下dataloader开太多worker会导致内存吃紧,甚至训练中途被杀进程。CPU平台则直接设0,用主进程加载数据,速度慢但稳定。batch参数需要根据显存调整,16是一个在8GB显存下比较稳妥的起点,显存不够就往下调到8或4。
4.3 训练参数怎么调:针对柜内商品的关键项
imgsz优先设640而不是1280。柜内摄像头图片往往超过1000万像素,但商品检测吃的是局部纹理,640分辨率下饮料瓶的图案已经足够分辨。直接设1280会让显存占用翻倍,训练时间拉长,mAP提升却可能不到1个点。如果一定要提高小目标召回,优先用YOLO11自带的tile切图推理,而不是把整体分辨率拉高。
预训练权重的选择上,yolo11n.pt速度最快、显存需求最低,适合先跑通流程;yolo11m.pt或yolo11l.pt在精度上有明显提升,但Mac上跑m系列基本会把内存吃满。建议先拿n系列确认数据集没问题,再换m系列正式训。
学习率参数在5000张图这种规模下不要乱动。lr0=0.01是Ultralytics的默认值,配合默认的cosine衰减即可。如果你发现loss前期下降太慢,先检查数据,再检查标签——八成是类别id错位或者有脏标签,九成不是学习率的问题。
5. 智能零售柜场景的五大坑:小目标、反光、遮挡与误检排查手记
零售柜场景的检测难点和通用目标检测不太一样:场景受限、目标种类固定、但拍摄条件很恶劣。这一章把实际调试中最常见的五类问题按现象、原因、解决的顺序拆开。
5.1 小瓶饮料总是漏检,置信度还在0.3以上徘徊
现象:底层的330ml罐装饮料经常漏检,或者检出来了但置信度很低,训练日志里该类别的AP明显低于其他类。
原因:小目标在YOLO11的深层特征图里已经丢失了空间细节。模型下采样32倍后,一个30像素的小罐子只剩下不到一个特征点,自然无法区分品牌。
解决:先在数据集层面确认这类目标的数量是否足够,不够就补充采集。如果数量够了,训练时开启YOLO11的增强参数,特别是scale和translate,让模型见过更多小尺寸的变体。仍然不行的,换yolo11m或更大模型,大模型的浅层特征图尺寸更大,对小目标更友好。直接改模型结构加检测头的做法,对于不熟悉源码的人来说得不偿失。
5.2 玻璃反光导致同一个商品被框两次
现象:带弧形玻璃的柜门,一处反光区域被模型识别成商品,一个真实商品旁边多出一个高置信度的虚框。
原因:反光区域的颜色和纹理与真实商品的包装高度相似,尤其是在光线角度变化时。模型学到的是"这个颜色形状像商品",而不是"商品应该在这里"。
解决:训练集里混入带反光的负样本图,让模型见到更多反光但不含目标的情况。如果反光集中在特定位置(比如柜门中部),可以在预处理时对该区域做针对性数据增强。线上的兜底手段是加入NMS优化策略,将IoU阈值调低一些,让同一商品附近重复的框被压掉。
5.3 遮挡严重时,框只框住了露出来的半边
现象:瓶装水被前排零食挡住一半,标注框只框住露出的部分,训练时模型学到的是"半个商品"的特征,预测结果框明显偏小。
原因:标注口径上"只标可见部分"的策略在遮挡角度小的时候是有效的,但当遮挡占比超过30%时,可见部分的框和完整商品的框差异会让模型无所适从。
解决:给这部分遮挡样本单独设置规则,可见面积大于50%的正常标注,可见面积在20%到50%之间的标注框稍微外扩10%到15%,贴合商品的实际物理边界。这种做法虽然牺牲了一点标注一致性,但模型学出来的框更贴近真实使用场景,因为线上预测时每个商品都有一个物理边界。
5.4 爆款类AP刷到0.95,冷门类只有0.3
现象:训练结束后看每类AP柱状图,销量靠前的几个类目接近满分,冷门类目惨不忍睹。
原因:类别不均衡导致模型把大类的特征学得过于充分,推理时倾向于把不确定的样本判给先验概率高的大类。
解决:优先尝试在data.yaml里给冷门类加weight,让loss函数对冷门类的错误更敏感。其次对冷门类做mosaic增强复制,但增强倍数控制在2到3倍,过度复制反而会让模型过拟合到特定图像上。还有一个不太起眼但很有效的手段:训练时把冷门类的图片重复采样一遍,相当于变相增加epoch。
5.5 训练到一半报OOM,或者Mac上直接被杀进程
现象:GPU训练时显存溢出,报CUDA out of memory;Mac训练时进程突然消失,没有任何报错信息;CPU训练时内存占用吃满导致系统卡死。
原因:GPU显存溢出通常是batch设太大或者开了太大的imgsz;Mac上被杀进程多数是dataloader的worker数太多,内存被多个子进程瓜分;CPU平台则是图片解码太频繁,内存没有及时释放。
解决:GPU上把batch减半,或者开启梯度累积(YOLO11的train参数里有accumulate选项)。Mac上把workers设成0,并且关闭一切后台大程序。CPU平台把batch降到4以下,同时检查数据集的图片格式,JPEG比PNG解码快得多。一个通用的调试技巧:先用小数据集(比如500张)跑通整个流程,确认硬件没问题后,再上完整5000张。
6. 从训练到上柜:指标怎么读、模型怎么导出、现场实测怎么安排
训练结束后不要急着导出模型,先看看验证集上到底谁拖了后腿。整体mAP只能说明"平均还不错",不能暴露某个SKU经常漏检的问题。我的习惯是把每类的AP列出来,再统计一个"空柜误报率"——把空柜测试图跑一遍,数一数出了多少假框。零售柜项目里,空柜误报让补货系统误判库存,比单类漏检更致命。
确认指标合格后,用下面这段代码导出ONNX格式,这一步同时完成模型精简:
model = YOLO("runs/retail_cabinet/exp1/weights/best.pt") model.export(format="onnx", imgsz=640, half=True)half=True会将权重转为FP16,模型体积缩小一半,在支持FP16推理的边缘设备上速度更快。如果部署设备是纯CPU的ARM板子,不要开half,很多CPU的NPU对FP16支持不完整,出现了掉精度再排回来很费时间。导出后建议用onnxruntime跑一遍同一张测试图,对比和PyTorch输出的差异,差异超过0.05就要怀疑导出配置有问题。
现场实测的优先级排序:第一,把柜子放到阳光直射的位置测一遍反光场景;第二,模拟高峰期货架半空状态;第三,将商品故意摆乱、倒放,看模型的鲁棒性。这三个场景各录10分钟视频,用离线视频帧跑推理,统计漏检和误检帧数,比在测试集上刷分数更接近真实效果。
按这些步骤完整跑一遍,5000张图的数据集加上一键训练脚本,可以在两到三天内让你拿到一个可评估的模型。把数据、配置、训练日志都留档,尤其是每次实验的参数变化——这东西一旦开始改,记忆完全不可靠,翻车三天找不到原因是很正常的。我自己吃过这个亏,现在每轮实验都固定存一份命令行参数和data.yaml快照,调参时才有后悔药。希望帮到你。
本文还有配套的精品资源,点击获取