ARTICLE DETAIL

资讯详情

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

智能零售柜商品检测实战:5000张图与YOLO11三平台训练脚本

智能零售柜商品检测实战:5000张图与YOLO11三平台训练脚本 简介面向智能零售柜商品检测场景的目标检测数据集资源适合从事新零售视觉算法研发的工程师、学生及竞赛选手使用可用于智能零售柜商品识别项目落地也可作为通用新零售场景商品检测数据的补充。数据集采集自真实智能零售柜监控场景涵盖罐装饮料、袋装零食等常见商品标注标签共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、MMDetectionYOLO每图一个 txtclass_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_widthy_center (ymin ymax) / 2 / img_heightw (xmax - xmin) / img_widthh (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_okTrue) 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_okTrue) # 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(imgIdsimg_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_mapCOCO 的 category_id 可能是 1、3、7 这种不连续的值直接拿来当 YOLO 的 class_id 会越界。必须建立连续映射并且把映射关系记下来评估时再映射回去。参数上instances_train.json换成你的 COCO 标注文件如果同时有 train 和 val两个都要转。4. YOLO11 一键训练脚本三平台参数怎么设、显存怎么省这套脚本的价值在于抹平平台差异。GPU 机器上走 CUDAMac 上走 MPS纯 CPU 也能跑但慢。YOLO11 是 Ultralytics 维护的训练入口就是model.train()但参数设不对要么显存爆要么训不动。下面按平台拆开讲。4.1 GPU、CPU、Mac 三平台的启动参数GPU 平台最省心装好 CUDA 版 PyTorch 后直接指定device0。CPU 平台要把device设成cpu同时把workers调小因为 CPU 上多进程数据加载反而拖慢。Mac 的 M 系列芯片走 MPSdevicemps但要注意 MPS 对某些算子支持不全遇到报错就退回 CPU。from ultralytics import YOLO import torch # 自动选设备有 CUDA 用 GPUMac 用 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( datadatasets/retail/data.yaml, epochs100, imgsz960, # 零售柜小目标多640 容易漏检 batch16, # GPU 显存 8G 以上可以到 32 devicedevice, workers4 if device ! cpu else 2, patience20, # 20 轮没提升就早停省时间 cacheTrue, # 图片缓存到内存5000 张约几个 G )参数逐个说imgsz960是因为零售柜商品小640 分辨率下小目标特征太弱batch16是 8G 显存的保守值显存够就往上加不够就降到 8patience20是早停避免过拟合后白跑cacheTrue把图片缓存进内存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这是最直接的。第二开梯度累积batch4配合accumulate4等效 batch 还是 16但显存占用按 4 算。第三降 imgsz从 960 降到 640显存占用大概降一半但小目标召回会掉。第四用更小的模型yolo11n 换成 yolo11n 已经是最小的了那就只能降分辨率。我一般按「先降 batch再开累积最后降分辨率」的顺序试因为降分辨率对精度伤害最大。model.train( datadatasets/retail/data.yaml, epochs100, imgsz640, batch4, accumulate4, # 梯度累积等效 batch16 device0, ampTrue, # 混合精度省显存还提速 )ampTrue是自动混合精度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。原因是cacheTrue把图片缓存进内存的同时数据增强还在生成大张量峰值显存超了。解决把cache设成False或disk或者降imgsz。另外检查workers是不是设太大每个 worker 都会占一份显存。5.4 Mac 上 MPS 报算子不支持现象Mac 上devicemps训练到某个算子时报错提示 not implemented。原因是 MPS 后端对某些 PyTorch 算子支持不全尤其是自定义的损失函数。解决先升级 PyTorch 到最新版很多算子已经补上了。还不行就把device设成cpu慢但能跑通。或者用PYTORCH_ENABLE_MPS_FALLBACK1环境变量让不支持的算子回退到 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( sourcetest_videos/shelf.mp4, conf0.4, # 置信度阈值零售柜建议 0.4 到 0.5 iou0.5, # NMS 的 IoU 阈值密集商品可以降到 0.4 imgsz960, streamTrue, # 视频流式推理省内存 device0, ) for r in results: boxes r.boxes # 统计每帧的类别和数量和实际货架对比 print(boxes.cls.tolist(), boxes.conf.tolist())conf0.4是零售柜的经验值太低会误检太高会漏检。iou0.5控制 NMS 合并商品密集时降到 0.4 能保留更多相邻框。streamTrue对视频推理很重要不然会把所有帧读进内存。跑完把每帧的检测结果和实际货架对比重点看遮挡区域和反光区域。连续帧稳定性看的是同一个商品在相邻帧里的框是否跳动。如果框在抖说明模型对该商品的特征提取不稳定可能是训练数据里这个角度太少。解决办法是把这个角度的图补进训练集或者开测试时增强TTA但 TTA 会拖慢推理速度实时场景慎用。最后一步是拿取动作识别。零售柜的核心逻辑是「谁拿了什么」单帧检测只能告诉你货架上有什么要结合前后帧的差异判断拿取。常见做法是维护一个货架状态表每帧检测结果和上一帧对比某个商品消失就记一次拿取。这里有个坑用户手挡住商品时检测会短暂丢失导致误判。解决办法是加一个时间窗口连续 3 帧都检测不到才认为商品被拿走。我自己的习惯是任何模型上线前先用真实柜子的视频跑一遍人工数 100 次拿取动作看模型报了多少次、漏了多少次、误报了多少次。这三个数比 mAP 实在得多。零售柜场景里漏报比误报严重因为漏报意味着丢货误报只是多记一笔。所以阈值宁可调低配合时间窗口过滤误报。这套数据集和脚本能帮你快速跑通 baseline但真正上柜子还得靠真实场景的数据反复迭代。希望帮到你。本文还有配套的精品资源点击获取
返回列表