简介:这份资源面向智能零售柜商品检测方向的算法工程师与目标检测学习者,提供真实零售柜监控场景下采集的1000张高质量商品图片,覆盖罐装饮料、袋装零食等常见品类,标注类别达113个,可用于搭建商品识别项目,也可作为新零售场景通用检测数据的补充。资源包为1个PDF文件,约5.79MB,内附数据集基本情况介绍与获取方式,标签采用labelimg标注,同时提供VOC、COCO、YOLO三种格式,可直接投入YOLO等算法训练。随包附赠YOLO11一键训练脚本,支持GPU、CPU及Mac(M芯片)多平台方案,并给出博主训练结果日志供对照参考。目前已有529人学习,适合希望快速验证零售商品检测效果、减少数据准备成本的读者参考使用。
1. 智能零售柜商品检测:1000 张图、三种标签格式和一套能跑通三平台的 YOLO11 脚本
智能零售柜的商品检测,说白了就是让摄像头在货柜里认出「这一格放的是可乐还是矿泉水」。它和通用目标检测最大的区别在于:商品密集摆放、遮挡严重、同类商品外观高度相似,而且货柜场景的光照往往不均匀。你拿 COCO 预训练模型直接推理,大概率会把一排饮料全认成 bottle。所以真正能落地的做法,是拿一批贴合货柜视角的标注数据,做一次针对性微调。这个标题给的正是这样一套组合:1000 张商品图,配 VOC、COCO、YOLO 三种格式标签,外加一份能在 GPU、CPU、Mac 三平台一键启动的 YOLO11 训练脚本。它解决的是从「有数据」到「能训起来」之间那段最磨人的路——格式转换、环境适配、参数配置。适合谁?做智能零售柜、无人货柜、自动售货机视觉模块的工程师,以及想拿一个真实小数据集练手 YOLO 训练全流程的人。1000 张不算多,但足够把 pipeline 跑通、把指标调到一个可用的水平。
2. 三种标签格式到底怎么选:VOC、COCO、YOLO 的差异与转换逻辑
2.1 三种格式的结构差异与适用场景
拿到数据集先别急着训,先搞清楚三种标签格式各自长什么样,不然转换的时候一定翻车。VOC 格式是每张图对应一个 XML 文件,里面用<object>节点记录类别名和xmin/ymin/xmax/ymax的绝对像素坐标。COCO 格式是一个大的 JSON 文件,所有图片的images、annotations、categories三个数组分开存,bbox 是[x, y, width, height]的绝对坐标。YOLO 格式最简洁,每张图对应一个 txt,每行是class_id x_center y_center width height,全部归一化到 0 到 1 之间。
选哪个取决于你的训练框架。YOLO11 官方训练只吃 YOLO 格式,所以最终一定要转成 YOLO。但 VOC 和 COCO 不是白给的——VOC 方便你用 labelImg 这类工具继续补标,COCO 方便你接 mmdetection 或者做 COCO 指标评估。三种格式同时提供,本质是让你在不同环节用不同格式,不用自己写转换脚本。
| 格式 | 单文件结构 | 坐标类型 | 典型工具 |
|---|---|---|---|
| VOC | 每图一个 XML | 绝对像素 xmin/ymin/xmax/ymax | labelImg |
| COCO | 单个 JSON | 绝对像素 x,y,w,h | labelme、mmdetection |
| YOLO | 每图一个 txt | 归一化中心点+宽高 | YOLO 官方 |
2.2 从 VOC 转 YOLO 的完整脚本与参数说明
如果你手上只有 VOC,或者想验证数据集里 VOC 和 YOLO 是否一致,这段转换脚本可以直接抄。核心逻辑就三步:读 XML 拿绝对坐标,除以图片宽高做归一化,按类别映射成 id 写 txt。
import os import xml.etree.ElementTree as ET from PIL import Image # 类别列表,顺序决定 class_id,必须和训练时的 data.yaml 一致 CLASSES = ["cola", "water", "juice", "snack", "other"] def voc_to_yolo(xml_path, img_dir, out_dir): tree = ET.parse(xml_path) root = tree.getroot() # 从 XML 里拿图片文件名和尺寸 filename = root.find("filename").text img_path = os.path.join(img_dir, filename) w, h = Image.open(img_path).size lines = [] for obj in root.findall("object"): cls_name = obj.find("name").text if cls_name not in CLASSES: continue # 跳过未定义类别,避免 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) # 归一化并转成中心点+宽高 x_c = (xmin + xmax) / 2.0 / w y_c = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{cls_id} {x_c:.6f} {y_c:.6f} {bw:.6f} {bh:.6f}") out_name = os.path.splitext(filename)[0] + ".txt" with open(os.path.join(out_dir, out_name), "w") as f: f.write("\n".join(lines))逻辑说明:CLASSES的顺序就是最终class_id的来源,一旦训练开始就不能改,否则标签全错位。归一化用图片真实宽高,不是 XML 里写的size节点——有些标注工具的 size 字段是错的,用 PIL 重新读一遍最稳。:.6f保留六位小数是 YOLO 官方推荐的精度,太少会导致小目标框偏移。
参数说明:xml_path是单个 XML 路径,批量处理时外面套一层os.listdir即可。img_dir和out_dir要分开,别把 txt 写回图片目录,否则 YOLO 扫描时会混乱。如果类别里有空格或中文,建议先重命名成英文短名,YOLO 的 data.yaml 对中文类别支持不稳定。
2.3 COCO 转 YOLO 时最容易错的两个点
COCO 转 YOLO 的坑比 VOC 多。第一个坑是 COCO 的bbox是[x, y, width, height],其中 x、y 是左上角绝对坐标,不是中心点,很多人直接拿它当中心点用,框全偏了。第二个坑是category_id不连续——COCO 原始数据的 id 可能是 1、3、7、9,但 YOLO 要求从 0 开始连续编号。转换时必须自己建一个category_id到0~N-1的映射表。
import json with open("annotations.json") as f: coco = json.load(f) # 建立 category_id 到连续 id 的映射 cat_map = {c["id"]: i for i, c in enumerate(coco["categories"])} # 建立 image_id 到文件名的映射 img_map = {img["id"]: img["file_name"] for img in coco["images"]} # 建立 image_id 到宽高的映射,用于归一化 size_map = {img["id"]: (img["width"], img["height"]) for img in coco["images"]} for ann in coco["annotations"]: img_id = ann["image_id"] w, h = size_map[img_id] x, y, bw, bh = ann["bbox"] x_c = (x + bw / 2) / w y_c = (y + bh / 2) / h nw = bw / w nh = bh / h cls_id = cat_map[ann["category_id"]] line = f"{cls_id} {x_c:.6f} {y_c:.6f} {nw:.6f} {nh:.6f}\n" txt_name = img_map[img_id].replace(".jpg", ".txt") with open(f"labels/{txt_name}", "a") as f: f.write(line)逻辑说明:cat_map用enumerate保证 id 从 0 连续。size_map单独建是因为 COCO 的images数组里才有宽高,annotations里没有。用"a"追加模式是因为一张图可能有多个标注,不能每写一个就覆盖。
参数说明:如果 COCO 的categories里包含supercategory,忽略它,只取id和name。归一化后的值如果出现负数或大于 1,说明原始 bbox 越界了,这种标注要单独挑出来检查,别直接喂给训练。
3. YOLO11 一键训练脚本:GPU、CPU、Mac 三平台怎么配
3.1 三平台环境差异与依赖安装
标题里说支持 GPU、CPU、Mac 三平台,这不是噱头,是真实痛点。GPU 服务器上你装torch要带 CUDA 版本,Mac 上要用 MPS 后端,纯 CPU 机器上只能老老实实跑device=cpu。三者装错一个,脚本直接报错退出。
先看依赖安装。GPU 平台最常见的是 NVIDIA 显卡,装 PyTorch 时要指定 CUDA 版本:
# GPU 平台(CUDA 12.1 示例) pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install ultralytics # Mac(Apple Silicon,MPS 加速) pip install torch torchvision pip install ultralytics # 纯 CPU 平台 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install ultralytics逻辑说明:ultralytics是 YOLO11 的官方包,装它会自动拉取匹配的依赖。GPU 平台的关键是--index-url指向 CUDA 版本的 wheel 源,不指定的话 pip 默认装 CPU 版,训练时torch.cuda.is_available()返回 False,你会以为显卡坏了。Mac 的 MPS 不需要额外装 CUDA,PyTorch 从 1.12 起内置支持。
参数说明:CUDA 版本要和显卡驱动匹配,nvidia-smi右上角显示的 CUDA Version 是驱动支持的上限,装的 torch CUDA 版本不能超过它。Mac 上如果torch.backends.mps.is_available()返回 False,检查系统版本是否低于 macOS 12.3。
3.2 一键训练脚本的完整写法与设备自动检测
所谓「一键」,核心是让脚本自己判断当前平台该用什么设备,而不是让你手动改device参数。下面这段脚本把设备检测、路径配置、训练启动串在一起。
import torch from ultralytics import YOLO def pick_device(): if torch.cuda.is_available(): return "0" # 第一块 GPU elif torch.backends.mps.is_available(): return "mps" # Apple Silicon else: return "cpu" if __name__ == "__main__": device = pick_device() print(f"使用设备: {device}") model = YOLO("yolo11n.pt") # 用官方预训练权重做迁移学习 results = model.train( data="data.yaml", # 数据集配置 epochs=100, # 训练轮数 imgsz=640, # 输入尺寸 batch=16, # 批大小,CPU/Mac 建议降到 4~8 device=device, # 自动选择的设备 workers=4, # 数据加载线程 patience=20, # 早停耐心值 project="runs/retail", # 输出目录 name="exp1", # 实验名 )逻辑说明:pick_device按 CUDA、MPS、CPU 的优先级返回设备字符串。YOLO11 的train接口接受device参数,传"0"表示第一块 GPU,传"mps"走 Apple 加速,传"cpu"走纯 CPU。yolo11n.pt是 nano 版本,1000 张图的数据集用 nano 或 small 就够,别一上来就上 large,过拟合风险高且慢。
参数说明:batch在三平台上差异最大。GPU 显存 8GB 以上可以上 16,Mac 统一内存建议 8,纯 CPU 建议 4。workers在 Windows 上设太大会报共享内存错误,设 0 或 2 更稳。patience=20表示 20 轮指标不提升就停,小数据集上能省不少时间。
3.3 data.yaml 的写法与路径陷阱
YOLO11 训练必须有一个data.yaml,它告诉框架图片在哪、类别有几个、名字是什么。这个文件写错,训练直接起不来。
path: /home/user/retail_dataset # 数据集根目录 train: images/train # 训练集图片相对路径 val: images/val # 验证集图片相对路径 nc: 5 # 类别数 names: # 类别名,顺序必须和 class_id 一致 0: cola 1: water 2: juice 3: snack 4: other逻辑说明:path是根目录,train和val是相对path的路径。YOLO 会自动把images替换成labels去找对应的 txt 标签,所以图片和标签的目录结构必须镜像对称——images/train/xxx.jpg对应labels/train/xxx.txt。
参数说明:nc必须等于names的条目数,多一个少一个都会报错。names用字典或列表都行,但顺序就是class_id,和转换脚本里的CLASSES必须完全一致。路径里不要有中文和空格,YOLO 在部分平台上处理不了。
4. 训练参数怎么调:从 1000 张图里榨出可用指标
4.1 小数据集的过拟合信号与应对
1000 张图在目标检测里属于小数据集,最大的风险是过拟合。典型信号是:训练 loss 一路降,验证 mAP 涨到某个点后开始跌,或者震荡不升。这时候别急着加轮数,先看数据增强够不够。
YOLO11 默认开了 mosaic、mixup、HSV 抖动等增强。小数据集上我一般会把 mosaic 关掉最后 10 轮,让模型在真实分布上收尾。参数是close_mosaic=10。另外degrees旋转增强在货柜场景要慎用,商品是正立摆放的,旋转太多反而引入噪声,设 0 或 5 就够。
results = model.train( data="data.yaml", epochs=100, imgsz=640, batch=16, device=device, close_mosaic=10, # 最后 10 轮关闭 mosaic degrees=0, # 货柜场景不做旋转增强 hsv_h=0.015, # 色调抖动,模拟光照变化 hsv_s=0.7, # 饱和度抖动 hsv_v=0.4, # 亮度抖动 )逻辑说明:close_mosaic让模型在训练末期见到无拼接的真实图像,验证指标更可信。degrees=0是因为货柜商品不会倒着放。HSV 三个参数模拟货柜灯光的色温和亮度波动,这对零售场景很实用。
参数说明:hsv_h范围 0 到 1,0.015 是官方默认,改动幅度小。hsv_s和hsv_v默认 0.7 和 0.4,如果货柜光照特别稳定可以调低,反之调高。这些参数没有绝对最优,拿验证集 mAP 做网格搜索最靠谱。
4.2 学习率与优化器的选择逻辑
YOLO11 默认用 SGD,初始学习率 0.01。小数据集上我试过 AdamW,收敛更快但最终 mAP 有时不如 SGD 稳。如果你时间紧,用 AdamW 把lr0设到 0.001 能快速看到效果;如果追求最终指标,SGD 配余弦退火更稳。
results = model.train( data="data.yaml", epochs=100, optimizer="SGD", # 或 "AdamW" lr0=0.01, # 初始学习率 lrf=0.01, # 最终学习率 = lr0 * lrf momentum=0.937, # SGD 动量 weight_decay=0.0005, # 权重衰减 warmup_epochs=3, # 预热轮数 )逻辑说明:lrf=0.01表示学习率从 0.01 余弦降到 0.0001。warmup_epochs=3让学习率从很小慢慢升到初始值,避免一开始就大步长把预训练权重破坏掉。weight_decay是 L2 正则,小数据集上可以适当加大到 0.001 抑制过拟合。
参数说明:momentum只对 SGD 有效,AdamW 会忽略它。lr0如果设太大,前几轮 loss 会飙到 nan,这时候降一个数量级重来。warmup_epochs在 batch 很小时要加长,否则预热不稳定。
4.3 用验证集指标判断该不该继续训
训练跑起来不是终点,你得会看指标决定下一步。YOLO11 训练结束后会在runs/retail/exp1下生成results.csv和混淆矩阵图。重点看三个:metrics/mAP50、metrics/mAP50-95、以及每个类别的 AP。
如果某个类别 AP 特别低,比如snack只有 0.3 而其他都 0.8 以上,大概率是这类样本太少或者标注质量差。回去数一下snack的标注框数量,少于 100 个的话考虑补标或者用过采样。如果 mAP50 高但 mAP50-95 低,说明框的位置不够准,检查标注框是否贴合商品边缘。
混淆矩阵里如果cola和water互相混淆严重,说明这两类外观太像,要么合并成一类,要么在数据里增加区分度大的样本。别硬调参数,这是数据问题。
5. 避坑与排查:三平台训练时最常翻车的 5 个地方
5.1 坑一:GPU 平台报 CUDA out of memory
现象:训练刚启动就报RuntimeError: CUDA out of memory,或者跑几轮后崩。
原因:batch设太大,或者imgsz设太高。YOLO11n 在 640 尺寸下,batch=16 大约占 4GB 显存,如果显卡只有 4GB 就爆了。另外workers太多也会占显存。
解决:先把batch降到 8 或 4,再不行把imgsz降到 416。用nvidia-smi实时看显存占用,留 500MB 余量。如果还不行,开amp=False关掉混合精度——混合精度省显存但偶尔会不稳定。
5.2 坑二:Mac 上 MPS 报错或速度极慢
现象:device="mps"后报NotImplementedError,或者训练速度比 CPU 还慢。
原因:PyTorch 的 MPS 后端对某些算子支持不全,YOLO11 里个别操作会回退到 CPU,导致频繁数据搬运。另外 macOS 版本太低也会导致 MPS 不可用。
解决:先确认torch.backends.mps.is_available()为 True。如果报算子不支持,升级 PyTorch 到最新版。速度慢的话把workers设 0,Mac 上多线程数据加载反而拖慢。实在不行就切device="cpu",Mac 的 CPU 跑 nano 模型也能接受。
5.3 坑三:CPU 平台训练慢到怀疑人生
现象:一轮训练要几十分钟,100 轮跑一天。
原因:CPU 没有并行矩阵加速,YOLO11 的卷积在 CPU 上就是慢。imgsz=640和batch=16在 CPU 上是不现实的。
解决:把imgsz降到 320,batch降到 4,epochs降到 50。用yolo11n.pt而不是更大的模型。如果只是验证 pipeline,可以先用epochs=5跑通,确认没问题再上服务器用 GPU 正式训。
5.4 坑四:标签和图片不对应导致训练 loss 为 nan
现象:训练一开始 loss 就是 nan,或者 mAP 一直是 0。
原因:labels目录和images目录结构不对应,或者 txt 里的class_id超出了nc范围。也可能是归一化坐标算错,出现了大于 1 或负值。
解决:写个检查脚本,遍历所有 txt,确认每行的第一个数字在0到nc-1之间,后四个数字在0到1之间。再确认每张图都有对应的 txt,文件名(不含扩展名)完全一致。
import os def check_labels(label_dir, nc): for txt in os.listdir(label_dir): with open(os.path.join(label_dir, txt)) as f: for i, line in enumerate(f): parts = line.strip().split() if len(parts) != 5: print(f"{txt} 第{i}行字段数不对: {line}") continue cls_id = int(parts[0]) coords = [float(x) for x in parts[1:]] if cls_id < 0 or cls_id >= nc: print(f"{txt} 第{i}行 class_id 越界: {cls_id}") if any(c < 0 or c > 1 for c in coords): print(f"{txt} 第{i}行坐标越界: {coords}")逻辑说明:这个脚本把最常见的三类标签错误一次性扫出来。字段数不对说明 txt 格式坏了,class_id 越界说明类别映射错了,坐标越界说明归一化算错了。
参数说明:label_dir传labels/train或labels/val,nc传data.yaml里的类别数。跑一遍没输出就说明标签没问题。
5.5 坑五:训练完模型在货柜实拍上效果差
现象:验证集 mAP 0.85,但拿手机拍货柜照片推理,框全乱。
原因:数据集里的图片可能是特定角度、特定光照拍的,模型过拟合到了那个分布。实拍时角度、距离、光照都变了,模型没见过。
解决:在数据增强里加大scale、translate、perspective的幅度,模拟不同拍摄条件。另外推理时把conf阈值调低到 0.25 试试,有时候是阈值太高把正确框滤掉了。长期看还是要补拍实拍场景的图进训练集,1000 张里如果全是棚拍图,落地一定翻车。
6. 把 1000 张图用到极致:难例挖掘与增量补标的具体做法
1000 张图训完第一版模型后,别急着收工。真正拉开差距的是第二轮:用第一版模型去推理未标注的货柜图,把置信度低或者框错的样本挑出来补标,再训第二版。这个流程叫难例挖掘,是小数据集提指标最有效的手段。
具体操作:准备一批新的货柜实拍图(不用标),用训好的best.pt推理,把conf设在 0.1 到 0.3 之间,导出预测结果。然后人工过一遍,把漏检的、框歪的、类别错的挑出来,用 labelImg 补标成 VOC,再用第 2 章的脚本转成 YOLO,合并进原训练集。第二轮训练时把epochs设短一点,30 到 50 轮就够,因为模型已经收敛过一轮。
这里有个血泪经验:补标的时候一定要保持类别定义一致。我见过有人第一轮把「无糖可乐」和「普通可乐」分开标,第二轮图省事合并成「可乐」,结果class_id全乱,训练直接崩。类别表一旦定下来,中途改就是给自己找麻烦。
验证方法上,除了看 mAP,我习惯再做一个「逐类召回率」检查。用验证集跑推理,统计每个类别实际有多少个框、模型检出多少个,召回率低于 0.7 的类别重点补样本。这个统计用 YOLO 的val模式加save_json=True导出预测,再和真值对比就能算出来。
from ultralytics import YOLO model = YOLO("runs/retail/exp1/weights/best.pt") metrics = model.val(data="data.yaml", save_json=True) # 输出每个类别的 P、R、mAP for i, name in enumerate(model.names.values()): print(f"{name}: P={metrics.box.p[i]:.3f} R={metrics.box.r[i]:.3f} mAP50={metrics.box.ap50[i]:.3f}")逻辑说明:save_json=True会导出 COCO 格式的预测结果,方便你逐图对比。metrics.box.p、r、ap50是按类别索引的数组,顺序和model.names一致。
参数说明:data.yaml要和训练时一致,否则类别对不上。如果某个类别R很低但P很高,说明模型太保守,调低推理conf;反过来P低R高,说明误检多,调高conf或者补负样本。
最后说个习惯:我每次训完模型,都会把data.yaml、转换脚本、训练脚本、best.pt一起打包存档,命名带上日期和数据集版本。智能零售柜的项目往往要迭代好几轮,三个月后你根本记不清当时用的哪版标签。这个后悔药,提前备好。希望帮到你。
本文还有配套的精品资源,点击获取