ARTICLE DETAIL

资讯详情

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

智能零售柜商品检测实战:VOC/COCO/YOLO格式转换与YOLO11训练全攻略

智能零售柜商品检测实战:VOC/COCO/YOLO格式转换与YOLO11训练全攻略 简介包含5000张真实智能零售柜监控场景商品图像的数据集覆盖罐装饮料、袋装零食等常见品类标注113个商品类别由labelimg软件精细标注同时提供VOC(XML)、COCO(JSON)、YOLO(TXT)三种格式标签覆盖主流目标检测框架的使用习惯可直接用于YOLO等算法训练省去自行转换标注的麻烦适配实际项目落地。资源附赠YOLO11一键训练脚本支持GPU(GPUs)、CPU、Mac(M芯片)三种平台运行并提供博主训练结果日志作为参考便于快速完成训练、验证与调参。资源包共1个文件为PDF格式约5.77MB内含数据集基本情况介绍及百度网盘下载方式。已有413人学习适合智能零售柜商品检测项目、新零售场景数据集补充以及目标检测方向的开发者获取高质量训练数据可用于毕业设计、算法竞赛或实际工程中的快速迭代。1. 智能零售柜商品检测为什么5000张图比公开数据集更值得信任做智能零售柜的同行应该都有同感商品检测和通用目标检测是两套逻辑。通用检测关心“路面上有什么车、图片里有没有人”而零售柜要解决的是“这层货架上哪个SKU被拿走了、剩余数量够不够、补货员该补哪一格”。同一个可乐换个角度、换个光照、被前面商品挡住一半模型就可能把“可口可乐”认成“百事可乐”。公开数据集在场景分布上和柜内实拍差异很大直接用公开权重做迁移往往在真实柜内场景翻车。这份5000张图的智能零售柜商品检测数据集配上VOC/COCO/YOLO三种标签格式和跨平台YOLO11训练脚本正好是“场景数据格式配套训练工具”的完整方案适合正在做零售柜识别、货架监控、自动结算这类项目的开发者。2. 拆开数据集5000张图里藏着哪些训练决策点2.1 场景分布与标注策略决定模型上限拿到数据集先别急着训练先花半小时搞清楚图片是怎么拍的。零售柜商品检测的最典型难点有三个密集遮挡、类间相似、尺度变化。密集遮挡指的是货架上商品一个挨一个边缘框互相重叠检测框稍微偏几个像素就可能框到旁边商品。类间相似是指不同SKU的包装颜色接近比如红色包装的可乐和红色包装的果茶模型很容易混淆。尺度变化是指同一个商品在顶层货架和底层货架成像大小差别很大靠前的商品可能占几百个像素靠后的可能只有三十几个像素。这份数据集有5000张图实际训练时要把这5000张按场景切成几个子集。我的建议是按照“单层货架平拍”“整柜广角”“补货手持拍摄”三类去划分训练集、验证集、测试集而不是随机划分。零售柜最终部署往往是固定视角的整柜图像如果训练集里整柜图太少训练出来的模型在实际柜内场景会明显退化。标注策略上要确认标注框是否包含被遮挡部分。有的标注工具画框习惯按可见部分画有的会画完整物体框。对YOLO系模型来说可见部分框更合适因为训练时模型学到的是“露出来的部分对应这个类别”推理时被遮挡的SKU也能被正确召回。如果用的是完整物体框被遮挡物体的框大部分区域是另一个物体学习信号会被污染。2.2 三种标签格式的字段差异VOC/COCO/YOLO不只是后缀不同VOC、COCO、YOLO三种格式背后的组织方式差异很大训练脚本读标签的方式也完全不同。VOC格式是每张图对应一个XML文件文件名与图片名一致。XML里最关键的是object节点每个object包含name类别名和bndbox坐标坐标是绝对值单位是像素格式为xmin, ymin, xmax, ymax。VOC格式的优点是肉眼可读、方便排查缺点是文件数量多、解析稍慢。COCO格式是整份数据集打包成一个JSON文件。JSON里有images数组、annotations数组、categories数组三个顶层结构。annotations里每个标注对象用bbox字段表示坐标但格式是[x, y, width, height]且是绝对值坐标需要把左上角坐标和宽高转换成xmin, ymin, xmax, ymax才能和VOC互通。此外segmentation字段、area字段、iscrowd字段在转换时也要处理否则容易在训练时报警告甚至崩掉。YOLO格式是每张图对应一个TXT文件每行一个目标格式为class_id x_center y_center width height。这里的坐标全部是归一化值即像素值除以图片宽高。类别用整数id表示不直接存类名所以需要一个单独的classes.txt文件按索引顺序记录类名。YOLO格式最紧凑训练加载最快但肉眼排查困难框画歪了不画出来根本看不出来。三种格式相互转换时最容易出错的地方是坐标系的换算。VOC的xmax在转COCO时要减1吗严格来说COCO的bbox宽高是xmax - xminVOC的xmax如果标注工具是按像素索引存的转过去应该用xmax - xmin 1。这类“差1像素”的误差在几百个框时看不出来但训练大模型时边界回归的损失会受到细微干扰。我的习惯是转换后抓几张图做可视化对比确认框的位置准确后再开训练。2.3 拿到数据集先做的不变量检查从数据集拷到本地先跑一遍数据完整性检查脚本确认三件事第一图片和标签文件是否一一对应。VOC/YOLO格式下某张图缺XML或缺少TXT文件训练时会跳过但不会报错导致实际参与训练的图数比预期少。第二所有坐标是否都在图片尺寸范围内。过期标注工具生成的框偶尔会有坐标超出图像边界的情况YOLO训练时会报all box coordinates are out of bounds然后跳过该图。第三类别id是否连续。COCO和YOLO格式的类别id必须从0开始连续递增如果中间跳号类别映射就会错位。这三项检查可以在几十行脚本里完成但能省掉后面排错的两小时。检查坐标边界这一项尤其重要因为智能零售柜图片常由不同门店的不同设备拍摄分辨率不一致老设备产出的历史数据可能混着奇怪的标注坐标。3. 把VOC/COCO/YOLO串起来格式转换脚本的三个关键实现数据集自带三种格式实际使用时并不需要每次转换。但数据增强、筛选坏样本、合并新数据时往往只能处理到某一种格式这时就需要自己写转换。这里给出三段我常用、可以直接抄的转换代码。3.1 从VOC XML到COCO JSON的坐标换算import xml.etree.ElementTree as ET import json import glob import os from PIL import Image def voc_xml_to_coco(xml_dir, img_dir, output_json, categories): categories: {sku_name: 1, ...} 从1开始 images [] annotations [] ann_id 1 cat_id_map categories for xml_path in sorted(glob.glob(os.path.join(xml_dir, *.xml))): img_name os.path.splitext(os.path.basename(xml_path))[0] .jpg img_path os.path.join(img_dir, img_name) img_w, img_h Image.open(img_path).size images.append({ id: len(images) 1, file_name: img_name, width: img_w, height: img_h }) tree ET.parse(xml_path) root tree.getroot() for obj in root.findall(object): name obj.findtext(name) if name not in cat_id_map: continue bndbox obj.find(bndbox) xmin float(bndbox.findtext(xmin)) ymin float(bndbox.findtext(ymin)) xmax float(bndbox.findtext(xmax)) ymax float(bndbox.findtext(ymax)) w xmax - xmin h ymax - ymin annotations.append({ id: ann_id, image_id: len(images), category_id: cat_id_map[name], bbox: [xmin, ymin, w, h], area: w * h, iscrowd: 0 }) ann_id 1 coco { images: images, annotations: annotations, categories: [{id: v, name: k} for k, v in cat_id_map.items()] } with open(output_json, w) as f: json.dump(coco, f, indent2) print(fimages: {len(images)}, annotations: {len(annotations)})这段代码做了两件容易被忽略的事一是用Image.open读取真实图片尺寸而不是从XML里的size节点取原因是部分数据集图片被压缩过但XML没同步更新二是bbox的宽直接用xmax - xmin面积同理。如果XML里坐标是xmin0, xmax99实际框住了100个像素严格算w应该是100但我按xmax - xmin99处理这是为了和多数COCO格式训练代码保持一致那些代码内部不再做加1处理。如果你后续要跑COCO官方的评估脚本建议把w改成xmax - xmin 1。3.2 从COCO JSON到YOLO TXT归一化与类别重排import json import os def coco_json_to_yolo_txt(coco_json_path, img_root, out_label_dir, category_order): category_order: [cola, juice, snack, ...] 索引即YOLO class id从0开始 with open(coco_json_path) as f: coco json.load(f) cat_id_to_idx {} for cat in coco[categories]: name cat[name] if name in category_order: cat_id_to_idx[cat[id]] category_order.index(name) img_id_to_meta {img[id]: img for img in coco[images]} os.makedirs(out_label_dir, exist_okTrue) for img_id, img in img_id_to_meta.items(): img_w img[width] img_h img[height] lines [] for ann in coco[annotations]: if ann[image_id] ! img_id: continue idx cat_id_to_idx.get(ann[category_id]) if idx is None: continue x, y, w, h ann[bbox] x_center (x w / 2.0) / img_w y_center (y h / 2.0) / img_h w_norm w / img_w h_norm h / img_h lines.append(f{idx} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}) if lines: img_name os.path.splitext(img[file_name])[0] out_path os.path.join(out_label_dir, img_name .txt) with open(out_path, w) as f: f.write(\n.join(lines)) # 写出类别文件 with open(os.path.join(out_label_dir, classes.txt), w) as f: f.write(\n.join(category_order))这里的关键是category_order参数。COCO格式里类别id是随意指定的比如某个数据集的“可乐”id是7而YOLO格式要求类别从0开始连续所以必须先做一次映射。如果漏掉这个映射训练时会出现标签全错的诡异现象——模型训练没报错但损失不下降。另一个细节是归一化计算用x_center (x w / 2.0) / img_w许多人会误写成(x w) / 2 / img_w两种写法数学上等价但前者少一次括号嵌套不容易写错。3.3 转换脚本里容易出错的细节转换脚本最常踩的坑有三个都是实际调试中遇到过的问题。第一个是宽高比不一致。同一批图片里偶尔混了旋转90度的图VOC的XML里存的是旋转前的宽高COCO转换时从图片读到的宽高却是旋转后的标签坐标全部错位。解决办法是在转换前先做一轮图片朝向校验检查所有图片的width/height和XML里的size节点是否一致不一致的单独挑出来人工看。第二个是标注框面积为0的脏数据。有些标注工具的拖拽操作不当会生成xmin xmax或ymin ymax的框YOLO训练遇到这种框直接跳过而COCO训练则会报AssertionError: bbox should have positive area。转换脚本里应该显式过滤w 0 or h 0的标注。第三个是类别名的硬编码问题。零售柜商品类目经常调整比如把“低糖可乐”从“可乐”里拆出来单独成一类如果转换脚本里类别映射写死每次改类目都要改代码。我一般把所有类别映射放在一个独立的config.py文件里转换脚本只读配置不写配置。4. YOLO11三平台一键训练脚本参数拆解与Mac/CPU生存指南4.1 场景自适应配置输入尺寸、类别数、batch与epoch的初始取值YOLO11是YOLO系列的新版本它的训练脚本在ultralytics框架里统一管理支持GPU、CPU和Mac的MPS加速。这个数据集配套的一键训练脚本核心价值不是把ultralytics的命令包装了一遍而是针对零售柜场景把参数预设好了。零售柜商品检测的推理场景通常是固定摄像头位置商品在画面中的尺度相对稳定不像自动驾驶那样远近变化剧烈。输入尺寸不用拉到YOLO默认的640设成512或416就够了训练速度提升明显精度损失在密集商品场景下通常小于1个点的mAP。如果目标是做边缘设备部署输入尺寸设成320也能跑但小商品漏检会明显增多。初始batch设成关键参数。GPU上8G显存可以跑batch16配640输入尺寸换成512输入可以提到32。CPU上batch设4最稳Mac上MPS加速时batch设8。epoch初次训练设100轮数据集5000张不算大100轮足够看到收敛趋势不用一上来就跑300轮。类别数要从classes.txt里自动读取不要在脚本里手写数字。零售柜类目经常微调手写类别数会导致训练脚本和数据集对不上。4.2 GPU/CPU/Mac三平台环境检测逻辑训练脚本在开头会做一次运行平台检测按优先级选择设备。逻辑如下import platform import torch def detect_device(): if torch.cuda.is_available(): return cuda, torch.cuda.get_device_name(0) elif platform.system() Darwin and torch.backends.mps.is_available(): return mps, Apple MPS elif platform.system() Windows and torch.backends.mps.is_available(): return mps, Apple MPS # 仅理论分支 else: return cpu, CPU检测部分有三个关键判断点。一是torch.cuda.is_available()为False时不要强行to(cuda)否则直接AssertionError。二是Mac上torch.backends.mps.is_available()在较老版本的torch里不存在这个属性脚本里要用getattr(torch.backends, mps, None)做防御式判断。三是CPU训练时torch.set_num_threads要设置成CPU核心数减1留一个核给数据加载线程否则CPU资源争抢导致训练速度反而变慢。很多用户会手动指定model.to(mps)但ultralytics框架在训练内部会自动调用model.to(device)手动指定反而可能造成设备不一致。更可靠的做法是把设备字符串直接通过命令行传给YOLO11的train方法。4.3 训练脚本的主干结构一份能直接跑通的最小训练脚本如下from ultralytics import YOLO import os import yaml # 运行平台检测 device, device_name detect_device() print(fusing device: {device_name}) # 读取类别 with open(datasets/retail_cabinet/classes.txt) as f: classes [line.strip() for line in f.readlines() if line.strip()] # 构造数据集yaml data_cfg { path: os.path.abspath(datasets/retail_cabinet), train: images/train, val: images/val, test: images/test, names: {i: name for i, name in enumerate(classes)}, nc: len(classes), } with open(retail_cabinet.yaml, w) as f: yaml.dump(data_cfg, f, allow_unicodeTrue) # 加载预训练权重并训练 model YOLO(yolo11s.pt) results model.train( dataretail_cabinet.yaml, epochs100, imgsz512, batch16 if device cuda else 8, devicedevice, patience20, save_dirruns/retail_cabinet, )这份脚本的关键参数意义patience20表示20轮mAP没有提升就早停可以避免在数据量不大的情况下过拟合后白白耗时间。allow_unicodeTrue是yaml库的转义开关类别名里如果有中文必须开这个选项否则生成的yaml文件里中文变成乱码训练时读类别名全错。yolo11s.pt是最小的可用预训练权重零售柜数据集5000张不需要追求yolo11m或yolo11l从yolo11s起步训练如果mAP不理想再往上加模型尺寸这是更经济的做法。4.4 首次训练该盯的5个指标训练不是挂着让百分之百跑完就行首次训练至少每20轮看一次这几个指标。第一个是box_loss的下降曲线。正常情况下前20轮快速下降之后缓慢下降。如果box_loss全程一条水平线说明学习率设置有问题或者标签坐标根本没读对。第二个是cls_loss的下降速度。类别数多且类间相似的零售柜场景cls_loss会比其他任务下降慢但如果40轮后还是高于2.0基本可以判断是标注类别本身有歧义。第三个是mAP50和mAP50-95的差距。零售柜密集遮挡场景两个指标差距大于15个点是正常的但如果差距超出20个点说明框回归精度不够要调大输入尺寸或换更深的模型。第四个是训练集和验证集的loss差。差越来越大说明过拟合零售柜场景5000张图100轮一般不会严重过拟合除非数据增强参数被改小了。第五个是dfl_loss——这是YOLO系列专门衡量框边界精度的损失零售柜场景里商品边缘和包装文字多这个值偏大说明边界预测不稳定。5. 智能零售柜场景的避坑清单数据与训练脚本的五条血泪记录5.1 类别不平衡被“数量”骗了mAP很高但特定商品完全检不出现象训练完mAP50有0.93看起来能上线但现场测试时某款饮料干脆检不出来损失曲线也没异常。原因数据集中热门SKU的图占了大半冷门SKU只有几十张图。mAP是各类别取平均还是按样本数加权不同框架处理方式不同。ultralytics对IoU阈值和类别做平均时冷门类看作一个整体参与计算热门类精度高会拉高整体指标把冷门类的失败掩盖掉。解决统计classes.txt里每个类别的样本数量对样本数低于总数2%的类别在训练配置里提高这些类别的损失权重。具体做法是给model.train()传cls3.5默认通常是0.5到1.0或者用class_weights参数指定每个类别的权重列表。如果冷门类实在太少优先做法是补充这部分数据而不是硬调权重。5.2 相似商品混检标注框的包含关系引发大坑现象“可乐”和“低糖可乐”在包装上只有一行小字区别训练后模型总是把低糖可乐也检成可乐。看标注数据时发现部分图上同一个商品被标了两个框——一个“可乐”框套着一个“低糖可乐”框。原因标注人员对SKU认知不统一导致同一商品标注了两层标签。YOLO模型在训练时对同一区域收到两种类别的监督信号特征被拉向两个方向的折中最终边界模糊。解决写一个查重脚本统计同一图片内不同类别框的IoU把IoU大于0.85的框对全部列出来人工复核。这个操作看似繁琐但处理一次能避免后续整个训练周期被污染。这类数据问题用模型调参解决不了只能在数据层面处理干净。5.3 Mac上训练卡在“背景”检测MPS的玄学现象同一份数据和代码在Linux GPU上训练正常在Mac上训练到第8轮时训练卡住loss不再更新看起来像死循环。原因MPS加速在部分YOLO11算子上存在已知兼容问题尤其是SiLU激活函数和DFL模块的某些计算图在MPS后端会触发同步等待。这不是数据集的问题而是框架层面的适配问题。解决遇到这个情况在model.train()里显式设置devicecpu速度虽然慢一半但是稳定跑完。优先用yolo11s而不是yolo11nn版本太轻量在MPS上的算子路径更容易触达未优化的分支。5.4 数据划分不随机导致验证集失效现象验证集mAP一直在0.95以上模型看起来完美但真实柜内场景测试掉到0.6。反复调整训练参数都没用。原因数据集文件在目录里是按时间顺序排列的脚本用文件名排序后取了前90%做训练、后10%做验证导致验证集和训练集的拍摄时段、光照条件完全不同。训练集学到的是上午的柜内光线验证集恰好都是下午的图。解决划分数据时用随机种子打乱文件名再做划分。零售柜这类场景还要考虑按门店划分同一家店的图尽量全放训练集或全放验证集避免验证集和训练集混着同一家店的数据导致模型记住了店的特征而不是商品的特征。5.5 训练脚本改了batch却忘了同步学习率现象手动把batch从16改到32后训练loss剧烈震荡前10轮完全不下降。原因YOLO11里的学习率和batch大小是耦合的脚本里传了自定义lr0参数时框架不再自动适配batch变化。batch翻倍而学习率不变梯度的噪声被放大参数更新出现震荡。解决如果自定义了lr0batch从16改到32时把lr0也翻倍。更稳妥的做法是不要传lr0让ultralytics用默认的自适应调度框架内部会按batch大小自动调学习率。6. 验证与进阶用热力图和测试集切片判断模型是否可用训练完成后不要只盯着测试集的mAP指标mAP是整体统计量掩盖了位置相关的误差。零售柜场景有一个天然验证方法把柜内图像按网格切块统计模型在每一格的检出率。import cv2 import numpy as np def grid_check(model, img_path, grid(3, 4)): img cv2.imread(img_path) h, w img.shape[:2] gh h // grid[0] gw w // grid[1] results model(img_path, verboseFalse) boxes results[0].boxes.xyxy.cpu().numpy() cell_counts np.zeros(grid, dtypeint) for x1, y1, x2, y2 in boxes: cx int((x1 x2) / 2 / w * grid[1]) cy int((y1 y2) / 2 / h * grid[0]) cy min(cy, grid[0] - 1) cx min(cx, grid[1] - 1) cell_counts[cy, cx] 1 print(每个格子检出数量:) print(cell_counts)运行后会看到一个直观现象靠近柜门边缘的格子检出数量远低于中间区域的格子。原因通常是广角镜头边缘畸变导致商品拉长变形训练数据里这类边缘样本偏少。针对这种情况我会把边缘格子对应区域的图裁出来做二次标注加到训练集覆盖这部分场景比调整任何参数都有效。第二类验证是热力图。用results[0].plot()输出带热力层的检测图观察模型注意力集中的区域。如果热力图高亮区域集中在商品的文字标签而不是整体包装说明模型学到的特征是文字特征后续换包装设计时模型会失效。我自己的习惯是每次训练完固定输出三张图一张整柜原图一张格子检出热力图一张置信度直方图。三张图放进同一个对比目录连续几次训练的效果对比一目了然。数据集的三种标签格式在这个阶段会再派上一次用场——验证不同格式训练出的模型时用这些可视化工具确认不是标签格式转换带来的精度差异。做零售柜检测这几年最大的感触是模型结构和训练参数带来的精度提升有限真正决定上限的是数据分布覆盖度。5000张图做完网格抽取和热力图验证如果门店覆盖足够、各类别数量均衡这台模型的表现在真实场景里基本就稳了。希望这些经验能帮你少踩几个坑。本文还有配套的精品资源点击获取
返回列表