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

资讯详情

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

智能零售柜商品检测实战:5000张图、三种标签格式与YOLO11训练

智能零售柜商品检测实战:5000张图、三种标签格式与YOLO11训练

简介:面向智能零售柜商品检测与通用新零售场景目标检测需求,这份资源提供真实监控场景采集的高质量商品图片数据集,覆盖罐装饮料、袋装零食等常见品类,共5000张标注图和113个商品类别。数据经专业标注工具细致处理,同时给出VOC、COCO、YOLO三种格式标签,免去格式转换步骤,可直接用于主流检测模型训练,适合算法工程师、科研人员及零售行业开发者做项目落地或算法调优。资源包为单个PDF文件,大小5.77MB,内附数据集基本情况、标注规范说明及完整网盘获取方式;实际数据体量大而置于网盘,PDF便于预览和传播。目前已有413人学习。随附YOLO11一键训练脚本,支持GPU、CPU、Mac(M芯片)三平台,并含博主训练结果日志,读者可根据自身硬件快速启动训练,有效降低环境配置和适配成本。

1. 智能零售柜商品检测:5000张图和三种标签格式能解决什么

做智能零售柜的人都知道,真正的门槛不在柜体硬件,而在“柜子里那排商品到底能不能被稳定认出来”。一张货道照片里,饮料瓶挨着饮料瓶,标签被遮挡、反光、暗光、透视形变混在一起,这时候跑目标检测模型,常规的COCO预训练权重基本直接失效,必须用零售柜自己的数据重新训练。这个项目标题给的正是这条链路的关键资产:5000张柜内商品检测图,每张图配好VOC、COCO、YOLO三种格式的标签,再附一套YOLO11训练脚本,能在GPU、CPU、Mac三种环境下跑起来。适合谁?零售科技公司的算法岗、做边缘识别设备的嵌入式工程师,以及手里有柜子但刚起步做识别的团队。5000张不是玩具规模,它足够把一个垂类检测模型训练到可上线验证的程度,前提是你懂得怎么用它。

2. 数据集内部结构:VOC/COCO/YOLO三套标签和它们的组织方式

这个数据集第一个容易误会的地方是“三种格式”三个词。它并不是三批不一样的标注内容,而是同一批5000张图片的同一份标注结果,用三种行业通用格式分别表达了一遍。为什么要做三份?因为不同训练框架对标注输入有硬性要求:YOLO系吃TXT,MMDetection和Detectron2吃COCO JSON,老项目或自定义pipeline经常吃VOC XML。拿到手就能直接喂给对应框架,省掉格式对齐的时间——但前提是你得知道这三套坐标体系本质上差在哪,否则格式转换时第一个坑就埋下了。

2.1 VOC/COCO/YOLO三种标签格式不是三份数据,而是三套坐标系

VOC的核心是每个图片对应的XML文件,目标框记录的是左上角和右下角的绝对像素坐标。COCO把全部分标注汇总到一个JSON里,每个标注的bbox字段是[x, y, width, height],同样是绝对像素坐标,但表达的是左上角起点加宽高。YOLO则完全换了一套逻辑,每张图对应一个TXT,每一行是一个目标,格式是class_id x_center y_center width height,四个数值全部除以图片宽高做归一化,取值区间在0到1之间。

这套差异直接决定了转换脚本怎么写。VOC转YOLO要做一次从(xmin, ymin, xmax, ymax)到((xmin+xmax)/2/w, (ymin+ymax)/2/h, (xmax-xmin)/w, (ymax-ymin)/h)的换算;COCO转YOLO则是把[x, y, w, h]的左上角起点改成中心点。两个转换都要除以宽高,而宽高必须严格来自图片实际尺寸,不是XML或JSON里记录的那个值——这里若偷懒,边界目标的坐标会有像素级偏差,训练时不容易发现,推理时错位特别明显。

VOC XML片段长这样:

<annotation> <filename>IMG_0042.jpg</filename> <size> <width>1920</width> <height>1080</height> </size> <object> <name>coca_cola_500ml</name> <bndbox> <xmin>312</xmin> <ymin>405</ymin> <xmax>486</xmax> <ymax>670</ymax> </bndbox> </object> </annotation>

COCO JSON里对应的一条标注是这样:

{ "images": [{"id": 42, "file_name": "IMG_0042.jpg", "width": 1920, "height": 1080}], "annotations": [{"id": 1, "image_id": 42, "category_id": 0, "bbox": [312, 405, 174, 265]}], "categories": [{"id": 0, "name": "coca_cola_500ml"}] }

而YOLO的TXT里同一目标只有一行:

0 0.2078 0.4977 0.0906 0.2454

XML和JSON的可读性在于人眼能直接看懂边界在哪,TXT则是给人眼看的反面教材,但训练速度最快。三类文件物理解释各不相同,所以做任何二次开发前,第一件事就是把坐标体系确认清楚,而不是急着跑训练。

2.2 数据集目录长什么样:从图片到标签的对应关系

拿到数据集后,先确认顶层目录组织。常见做法是图片和标签分开两棵目录树,这样按train/val/test划分时,可以同时把images和labels两边的子目录切齐。兼容YOLO训练脚本的目录一般长这样:

dataset/ ├── images/ │ ├── train/ │ │ ├── IMG_0001.jpg │ │ └── ... │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ │ ├── IMG_0001.txt │ │ └── ... │ ├── val/ │ └── test/ ├── annotations/ │ ├── train.json │ ├── val.json │ └── test.json ├── VOC_xml/ │ ├── train/ │ ├── val/ │ └── test/ └── retail.yaml

图片文件名和标签文件名必须完全同名,只是后缀不同,这是YOLO训练时的硬性匹配规则。annotations放COCO格式的JSON文件,VOC_xml目录放XML。yaml文件是给训练脚本用的入口,里面写清数据集路径、类别数量、类别名称列表,一个字符都不能错。

这里有一个容易忽略的点:如果你要自己重新划分train/val,不要在三个格式目录里分别手工移动文件,那样一定会出现某个目录图片多了一张、标签少了一张的错位。正确做法是只维护一份图片文件清单,脚本根据清单同步复制三种标签到对应子目录。

2.3 数据划分:train/val/test的分配为什么影响训练结果

5000张图的常见划分是7:2:1,即3500张训练、1000张验证、500张测试。注意,这里有一个智能零售柜特有的问题:很多采集是连续拍照得到的,同一个货道、同一个时刻的几张图在画面内容上高度相似。如果按文件名随机洗牌,同一组连续帧会同时出现在train和val里,验证集指标好看得像是广告,真到现场立刻现原形。

我一般会按“柜”维度划分,而不是按“图”维度。也就是先确认哪些图片来源于同一个柜子,再把整组柜子图片分配进train、val、test。这样验证集里出现的商品摆放方式和训练集有真实差异,评估出的mAP才可信。给到手里的数据集如果已经划分好了,先花十分钟看val里和train里有没有重复柜号的图片,这不是可做可不做的检查,而是后续一切指标的前提。

3. 标签格式互转与校验:从坐标系换算到类别映射的几个硬边界

很多人的习惯是拿到数据集直接开训,训到一半发现loss不降,回头排查才发现是标签问题。零售柜商品检测对标签质量尤其敏感,因为商品密集、类别多且外观接近,一张图上几十个框,任何一个框的坐标写错都在给模型传递错误信号。第3章做的事情,是在训练前把标签彻底体检一遍,顺便把三种格式之间互转的脚本准备好——这步做扎实,后面训练和调参才谈得上可控。

3.1 标签校验是第一关:坐标、类别、空文件

先写一个YOLO格式的校验脚本。它的职责有三类:TXT里坐标是否在0到1区间、类别ID是否落在合法范围内、以及是否存在空标签文件。空标签的意思是某些TXT文件里没有任何内容,这类文件如果大量存在,说明原标注有漏标,正常训练可能不报错,但模型会在对应图片上学不到任何目标信号,表现为推理时对该图场景稳定漏检。

import os from pathlib import Path def validate_yolo_labels(label_dir, num_classes): errors = [] for txt_path in Path(label_dir).glob("*.txt"): with open(txt_path, "r", encoding="utf-8") as f: lines = [line.strip() for line in f if line.strip()] if len(lines) == 0: errors.append((txt_path.name, "空标签")) continue for line in lines: parts = line.split() if len(parts) != 5: errors.append((txt_path.name, f"字段数不对: {line}")) continue cls_id, xc, yc, w, h = int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) if cls_id < 0 or cls_id >= num_classes: errors.append((txt_path.name, f"类别ID越界: {cls_id}")) for val, name in [(xc, "x_center"), (yc, "y_center"), (w, "width"), (h, "height")]: if not (0.0 <= val <= 1.0): errors.append((txt_path.name, f"{name}超出0-1: {val}")) return errors if __name__ == "__main__": errs = validate_yolo_labels("dataset/labels/train", num_classes=20) for name, reason in errs[:50]: print(f"{name}: {reason}") print(f"共发现 {len(errs)} 处问题")

脚本里num_classes要跟后续训练yaml里的nc完全一致。校验逻辑很简单:每个标签文件读取每一行,拆成五个字段,逐个检查边界。注意坐标范围检查用的是0.0 <= val <= 1.0,而实际允许边框紧贴图像边缘时坐标等于0或1,所以用闭区间判断,不要误报正常情况。类别ID越界是最危险的错,它不会影响loss计算,但会让模型把某个类别学成一个永远不会激活的类别,等到类别映射表对不上时才发现集体错位。

3.2 VOC转YOLO的坐标换算和COCO的anno_id陷阱

VOC转YOLO是用的最频繁的转换。注意两点:坐标换算必须用图片真实宽高,不要从XML的<size>取,最好用PIL直接读取图片尺寸,因为图片可能被预处理脚本重压过尺寸,XML里记录的是旧值;类别名转ID时,必须以统一的类别字典为唯一真源,不能每次脚本运行时重新生成一个字母序的字典,那样一旦类别列表顺序变了,所有标签的ID就全变了。

import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image def voc_to_yolo(xml_path, img_root, class_dict): tree = ET.parse(xml_path) root = tree.getroot() img_name = root.find("filename").text img = Image.open(Path(img_root) / img_name) w, h = img.size yolo_lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_dict: continue cls_id = class_dict[name] box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) x_center = (xmin + xmax) / 2.0 / w y_center = (ymin + ymax) / 2.0 / h box_w = (xmax - xmin) / w box_h = (ymax - ymin) / h yolo_lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") return img_name, yolo_lines class_dict = {"coca_cola_500ml": 0, "sprite_500ml": 1, "water_550ml": 2} img_name, lines = voc_to_yolo("sample.xml", "dataset/images/train", class_dict) with open("sample.txt", "w", encoding="utf-8") as f: f.write("\n".join(lines))

代码里.6f保留六位小数,换算精度完全够用。注意xmin + xmax这种写法依赖XML中坐标数值是整数还是小数,如果标注工具导出时做了四舍五入,框的边界会和原图有1到2像素偏差,这是VOC的老毛病,通常不需要处理,但如果你对像素精度有强迫症,建议直接以图片为准重新算一遍边界。

COCO转YOLO要特别小心image_id的对应关系。COCO JSON里images数组和annotations数组通过image_id关联,转换脚本必须先把image_id映射到文件名,再按文件名写TXT。常见做法是遍历images建一个{id: file_name}字典,然后逐条读annotations。如果忘了这一步直接按顺序读,JSON里的id不是从0开始连续排的,后果是标签张冠李戴,训练loss反而会下降,因为模型在拟合一组错位数据。

3.3 类别映射表:训练脚本里的names配置和数据集yaml

三种格式统一以后,真正的唯一真源是零售柜商品类别的ID字典。例如{"coca_cola_500ml": 0, "sprite_500ml": 1, "water_550ml": 2},这个字典要同时写进三个地方:YOLO TXT里的class_id、COCO JSON的categories数组、训练脚本的retail.yaml。三个地方只要有任意两处不一致,模型训练出来就白费。

yaml文件是YOLO11训练入口,常见长这样:

path: dataset train: images/train val: images/val test: images/test nc: 3 names: 0: coca_cola_500ml 1: sprite_500ml 2: water_550ml

这里有一个很多人翻车的点:nc和names的长度必须一致,names里的key必须从0开始连续递增,不能跳号。比如你删掉了一个类别,把2留给下一个类,但names里写的是1: sprite_500ml, 2: coca_cola_500ml,那么TXT里ID为2的标签在模型输出里就会对应一个从未参与训练的索引。模型不会报错,只会静默学错。

4. YOLO11一键训练脚本:GPU/CPU/Mac三平台跑通的参数套路

数据整理完毕,下一步就是训练。这个数据集的配套脚本核心是YOLO11训练自动化,目标是在三种环境上都能跑:NVIDIA GPU走CUDA、纯CPU机器走CPU后端、Mac走MPS。YOLO11本身是ultralytics当前稳定版框架里的主力检测模型,前身是YOLOv5/v8那条单阶段路线,对小目标、遮挡有一定内置优化,同时支持动态输入和多种导出格式,很适合智能零售柜这种既要精度又要部署灵活的场景。

4.1 为什么是YOLO11而不是YOLOv5或v8:选型理由

零售柜商品检测难点在于密集排列,货道纵深带来的小目标较多。YOLO11相对v5、v8的改进主要在backbone和C2f模块上,特征提取对稀疏小目标更友好,同时推理速度和显存占用控制得不错。YOLO11有n/s/m/l/x五个档位,n档最轻但检测密集商品时的漏检率偏高,s档对零售柜场景来说是个可接受的起步点,m档在GPU上训练性价比最高,精度和速度都占。

yolo11环境配置里最典型的坑是torch和ultralytics版本对不上。训练脚本开头加个设备检测函数,先自动探测当前机器有没有可用GPU、有没有MPS,再决定跑在哪个设备上,这样不需要用户手动改参数,也是“一键”两个字的真正价值。

4.2 三平台适配:GPU端CUDA、CPU端OpenVINO、Mac端MPS

设备自动检测代码:

import torch def get_device(): if torch.cuda.is_available(): return "cuda" if hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): return "mps" return "cpu" device = get_device() print(f"检测到设备: {device}")

GPU机器上建议显存足够时直接device=0,多卡可以写device=0,1,YOLO11会自动做数据并行。CPU和Mac跑的时候有个重要差别:CPU后端用OpenVINO或直接原生PyTorch都行,但Mac的MPS对某些算子支持不完整,batch得主动调小才能避免显存溢出。

配套的训练启动脚本:

#!/bin/bash # 训练入口:自动选择设备,训练后导出最佳权重 DEVICE="cuda" if ! command -v nvidia-smi &> /dev/null; then if [ "$(uname)" = "Darwin" ]; then DEVICE="mps" else DEVICE="cpu" fi fi yolo detect train \ model=yolo11s.pt \ data=dataset/retail.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ device=$DEVICE \ amp=True \ patience=20 \ project=runs/retail \ name=exp1

参数说明里几个值得专门解释的点。epochs=100对5000张级别的数据集够用,零售柜商品视觉差异不大,训练后期主要靠早停兜底,patience=20表示验证集指标连续20轮不改善就自动停止,省电省时间。imgsz=640是平衡速度和精度的保守选择,如果你的货道图片里小目标特别多,可以考虑imgsz=960起步,但显存占用会明显上涨。amp=True启动混合精度训练,GPU上速度提升明显,Mac上部分版本可能不稳定,如果训练中途崩了,第一件事就是把amp改成False再跑一轮。

4.3 训练脚本的可调参数:batch、imgsz、epochs、amp、patience

这五个参数里,影响排序大致是imgsz > batch > epochs > patience > amp。imgsz直接决定小目标的分辨率表现,batch决定收敛稳定性和显存占用,epochs决定最终上限,patience只负责止损,amp只影响速度和数值精度。GPU显存不够时,优先把batch减半而不是降imgsz,因为零售柜场景降分辨率会把小瓶装商品的纹理细节直接磨没了。

训练中断也不用慌,yolo detect train支持resume=True恢复上次的checkpoint,代码里的project和name命名规则要固定,resume时它会去runs/retail/exp1/weights/last.pt找存档。我一般会写一个run_train.sh、一个run_resume.sh,后者只有一行命令加resume=True,这是中断后最稳的后悔药。

Mac平台还有一个特殊注意点:MPS设备上不要把workers设太大。PyTorch在Mac上的DataLoader多进程支持和CUDA有差异,workers=4以上偶尔会出莫名其妙的挂起,设成2最稳。CPU机器同理,workers=0或1能避免很多和线程竞争有关的翻车。

5. 避坑清单:零售柜数据集和训练脚本里的高频翻车现场

这章内容全部来自实际做过零售柜项目的血泪经验。以下五个坑,任何一个都可能让你在调试上白耗两三天,按“现象→原因→解决”说清楚。

5.1 坑1:标签文件里坐标越界,训练不报错但精度崩

现象:损失函数曲线看起来正常,mAP指标在某个类别上始终不涨,或者推理时某类商品的位置偏移明显。排查时用画框脚本检查标签,发现少数几个框画在图片外面,甚至中心点落在图片外。

原因:标注工具导出时,边缘商品的扩展框超出了图像边界;或手工修正后没同步更新归一化坐标。YOLO训练时对越界坐标是能容忍的,ultralytics会做内部裁剪,但这等于给模型喂了错误监督信号,它学到的框回归目标不是真实商品边界。

解决:训练前统一跑一次坐标裁剪和过滤。把x_center、y_center裁剪到0到1,宽高裁到剩余有效区域,宽或高小于0.2就直接丢弃该框。裁剪脚本就三行:

xc = min(max(xc, 0.0), 1.0) yc = min(max(yc, 0.0), 1.0) w = min(w, 1.0 - xc) h = min(h, 1.0 - yc)

5.2 坑2:Mac上MPS显存溢出,训练跑不到第一个epoch结束

现象:Mac训练启动后几秒就报RuntimeError: MPS backend out of memory,命令行里能看到MPS设备,但显存直接爆掉。

原因:MPS走的是统一内存,imgsz=640加batch=16的组合在Mac上内存占用远超NVIDIA显存的同参数水平;另外某些老版本PyTorch的MPS后端对卷积算子支持不全,会多开一块临时内存放大占用。

解决:Mac上batch直接从8开始调,还不行就降到4;imgsz=640不动,amp=False。先确认PyTorch是支持MPS的稳定版本,老版本在MPS上边角算子容易崩溃。另外不要在Mac后台同时开着模拟器或大内存应用,统一内存被吃干净时MPS会毫无征兆地崩。

5.3 坑3:类别不平衡导致模型只认得高频商品

现象:训练完测试,可乐、矿泉水这种高频SKU检出率和定位都很好,某个只出现在几十张图里的小众饮料漏检严重,甚至完全检不出。

原因:5000张图按实际柜内商品分布采集,天然是长尾分布。高频类别几十张图里出现上百次,低频类别总共只有百来个实例,loss被高频类别主导,低频类别的梯度贡献微乎其微。

解决:先看数据分布,统计一下每个类别在训练集里的实例数。实例数低于100的类别,优先考虑在训练时给对应类别提高cls损失权重,或者在预处理里对该类图片做复制粘贴增强——把这类商品从原图抠出来,粘贴到其他空位生成新训练样本。注意增强时不要把瓶身光效和阴影丢掉,否则模型学的不是商品纹理而是贴图感,现场识别照样翻车。零售柜场景不建议用Mosaic把低频商品切碎,那会让小目标难上加难。

5.4 坑4:yaml里nc和数据集实际类别数不一致

现象:训练正常跑完,推理时所有检测框都预测成第一个类别,或者预测类别ID整体错位。检查retail.yaml发现nc写的是图片里真实类别数,但names列表某个位置留空了,或者names顺序和标签生成时的类别ID顺序对不上。

原因:这张数据集的VOC/COCO/YOLO三份标签虽然是同一份标注,但三份文件的类别顺序未必一致。VOC按XML里的<name>字符串出现顺序排,COCO按categories数组顺序排,YOLO按TXT里的数字ID排。做格式转换时如果直接复制了各自的顺序,而不以一份权威类别字典统一下发,三个格式的类别ID就会悄悄不一致。

解决:建立全局唯一的class_map.json,里面固定每个商品名的ID,三个目录的标签全部按这个map重新生成一遍。再在训练脚本里加一个启动前校验:扫描train/labels下所有TXT,取类别ID最大值,必须小于nc,并打印实际分布,发现异常就直接终止训练,不要等模型训完才后悔。

5.5 坑5:验证集mAP漂亮,但柜内实拍视频漏检严重

现象:验证集mAP50到了0.8以上,拿去做边缘设备推理演示,同一个柜子拍出来的视频里大量漏检,尤其是下面两层货道和玻璃反光区域的商品。

原因:验证集图片和训练集图片很可能来自同一批采集环境、同一个柜子、同一个时间段的连续帧,分布太接近。真实场景里玻璃反光、灯光色温变化、商品被顾客拿起后的位置偏移,这些分布外因素验证集根本没覆盖到。指标好看纯粹是自欺欺人。

解决:数据划分时就按柜子分,不要按单张图片随机分。训练结束后,从现场重新采集一条5到10分钟的短视频做盲测,统计逐帧的漏检数和误检数,而不是只看mAP。这是后面第6章要讲的正式验证流程,也是我判断模型能否上线的唯一标准。

6. 让模型真正可用:一段视频比一张图更早暴露问题

训练结束后,先不要急着导出。我的习惯是准备三个验证层级,逐级确认模型真的是为业务场景训练的。第一个层级是冒烟验证:拿测试集里单张图片跑推理,确认框的位置和类别不歪。第二个层级是现场视频逐帧推理:真实柜内画面连续帧,统计漏检率和误检率,这个指标比mAP更能说明问题。第三个层级才是导出成部署格式,比如ONNX或TensorRT,量化和后处理都放到这一步做。

冒烟验证可以直接用训练好的权重跑:

yolo detect predict \ model=runs/retail/exp1/weights/best.pt \ source=dataset/images/test/ \ conf=0.4 \ iou=0.5 \ save=True

参数里conf=0.4是按零售柜场景定的经验值,商品密集且类别相似时调低到0.3能减少漏检,但误检会上升;iou=0.5控制NMS的框合并阈值,货道纵深里商品前后叠放时,iou调到0.45更合适。如果现场画面里同一类商品一个挨一个,YOLO11默认的类别感知NMS会正常工作,不需要额外改后处理逻辑,选择判断基准就是视频漏检统计结果。

从项目整体价值看,这份数据集加脚本的定位是缩短“拿到柜内数据→产出可部署模型”的路径。数据侧的坑在格式转换和标签校验,代码侧的坑在设备和batch参数,业务侧的坑在于指标和真实场景之间那层窗户纸。5000张图能否训出上线模型,取决于你有没有把上面几个环节挨个走通。我自己的习惯是先把一份完整离线流程跑完,再谈调参——这个习惯替我避开过好几次白费一周训练的损失。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表