YOLOv11零售货架识别实战:从模型训练到库存动态管理
简介这是一份面向零售技术开发者与AI学习者的YOLOv11应用型文档重点解决货架商品识别与库存动态管理两大业务痛点。内容从零售行业现状与需求分析切入系统讲解YOLOv11的算法原理、网络结构与训练方法并逐步展开数据采集标注、模型训练评估、系统部署测试以及库存数据整合、实时监控、补货策略等完整实施路径。文档共35页整包为1个PDF文件大小仅1.89MB但目录层级清晰支持章节快速跳转与大纲定位便于按需查阅。已有77人学习浏览。读者可从中获得一套可参考的零售场景目标检测落地框架包括数据集构建技巧、模型优化策略、系统集成与安全保障方案适合作为从理论到工程实践的入门与进阶参考。1. 货架识别不是跑通 YOLO而是把识别结果接进库存账同一瓶饮料换个包装就是另一个 SKU同一 SKU 被货架层板挡住一半时模型认得出来但盘点数量会被“吞掉”。零售场景里碰过这类项目的人都有同感YOLOv11 训练好只是第一步真正难的是把逐帧检测结果稳定地换算成「哪个货架、哪个 SKU、还剩多少」再往下游补货流程推送。这套流程的目标不是做到单张图 mAP 高而是做到门店晨检、定时巡店、理货确认时视觉盘点结果能和库存系统对得上。这篇文章按一条实战线索展开先说明为什么选 YOLOv11、改哪些结构收益高再落数据集标注与训练参数最后给出一套推理、目标跟踪和 SKU 映射的代码骨架以及「库存动态管理」背后必须处理的时间维度问题。适合需要落地门店巡店、无人货柜、供应链盘点的工程师阅读有五年以上经验的读者也能在参数边界和时序计数部分找到值得确认的细节。2. YOLOv11 做货架商品识别选型理由、网络结构与可改的层2.1 为什么是 YOLOv11生态统一推理链路短YOLOv11 的默认检测头仍然是 anchor-free 解耦头主干端到端导出到 ONNX、TensorRT 时改动小这对货架场景很重要——门店摄像头通常几十路盘点服务要部署在边缘盒子或一台 GPU 服务器上模型结构越常规运维成本越低。很多人纠结过是否换 DETR 或 RT-DETR但货架商品识别本质是密集小目标检测不是开放词汇检测DETR 的全局注意力收益主要体现在复杂上下文理解上落到固定 SKU 列表上优势不大反而推理时延和显存占用更高。YOLOv11 的另一层优势是训练与部署的代码路径统一。同一套 YOLO 命令既能训模型也能做 predict、track、export不需要像老项目那样把 detect 和 tracking 拆成两套代码。零售巡检场景里最常见的动作是「每天定时扫一遍货架照片」这要求推理脚本能批量处理视频帧同时输出结构化 JSONYOLOv11 的结果对象直接提供 boxes、masks、probs 属性省掉大量胶水代码。做这类项目还要看算力约束。YOLOv11s 在 Jetson Orin 这类边缘设备上跑 1280 分辨率大约能到 30 到 50 毫秒一帧YOLOv11m 则适合放在机房 GPU 上统一处理门店回传的图片。选型策略我一般默认「摄像头端用 s服务端用 m」先保证离线批量盘点一天能跑完再考虑实时视频流。2.2 货架是典型小目标场景改 P2 层、注意力与 IoU 损失的收益排序货架拍摄图和自然场景不一样。摄像头俯拍时一瓶 500ml 饮料在 1080p 图像里可能只有 40×80 像素属于典型的小目标。YOLOv11 默认从 P3 层开始输出预测小目标的特征在多次下采样后已经丢失不少边缘细节。因此 YOLOv11 小目标优化最先该动的是检测层而不是盲目加深主干。常见做法是在 YOLOv11 的 Neck 上增加一个 P2 检测头让模型在更高分辨率特征图上回归小目标。这样做的代价是训练显存上涨约 20%推理速度下降约 5 到 10 毫秒但对货架 SKU 数量统计的召回率提升明显。若不想动结构更省事的方法是 SAHI 切图推理把原图切成带重叠的瓦片分别检测再合并实测在 4K 货架图上能提升 3 到 5 个点 mAP缺点是推理时间成倍增长。除了 P2注意力机制也值得试。YOLOv11 主干末端已有 PSA 注意力但在浅层特征上加自注意力往往更能区分外观接近的 SKU——比如不同口味的同系列薯片。我一般会在 C3k2 模块之后插入一层轻量自注意力而不是堆在检测头附近因为检测头附近加注意力容易拉慢收敛。另一个低成本的改进是替换 IoU 损失piousv2 这类对密集框回归更友好的变体在货架层板重叠场景下能减少误框改动只涉及 loss 文件里的几个类名。下面给一张按性价比排序的改动表供训练前做决策改进手段适合的货架问题训练成本推荐时机imgsz 提到 1280商品像素小于 60×60无代码改动显存翻倍第一批实验就做增加 P2 检测头小目标漏检集中在最底层货架中需改 yaml1280 后仍漏检再上SAHI 切图推理4K 巡检大图、计算资源充足低只改推理脚本服务器批量盘点场景轻量自注意力插到 C3k2 后同一品牌不同口味误检中预训练权重需重置该层训练中期发现混检时IoU 替换为 piousv2层板边缘框回归不准低改一行配置框偏差影响 NMS 合并时2.3 先量化任务边界别一上来就改结构货架商品识别可以拆成两个子任务一个是「这格货架上有哪些 SKU」另一个是「每个 SKU 摆了几件」。前者是纯检测问题后者必须依赖位置聚类和时序跟踪。如果盘点目标是固定的 80 到 300 个 SKUYOLOv11 的闭集检测足够如果每周都上新品那么类别数会持续膨胀训练数据也要跟着迭代这时候才需要考虑把分类头换成度量学习或者动态 embedding。多数零售项目不需要后者。另一个边界是检测视角。货架正视图、俯视图、手持设备近景三种视角下的同一 SKU外观差异远大于不同 SKU 间的差异。训练集必须按视角分桶管理否则模型会拿「角度」当「类别」学。我在项目里通常把三种视角作为三个独立数据集分别训练再在服务端按摄像头位姿路由到对应模型比硬塞进一个模型效果好。3. 准备零售数据集SKU 粒度标注、YOLO 格式与目录组织3.1 公开数据集与自采数据怎么组合零售检测领域有几个公开数据集可以当预训练和验证用SODA 分为 A/B 两个子集覆盖超市货架与收银台场景类别以商品大类和部分品牌 SKU 为主RPC 数据集包含大量同类商品的细粒度实例适合验证相似外观区分能力MVD 数据集则是多视角货架图片能帮助模型适应摄像头角度变化。这些数据集的类别体系通常和实际业务 SKU 对不上但用它们做预训练可以让模型先学会「商品 vs 背景」的边界再用业务数据微调。自采数据的规模建议按每个 SKU 至少 200 到 500 个实例来规划并且每个实例要有空货架与满货架的对照。货架场景有个容易被忽略的问题商品被取走一半后剩余商品的排列方式和满货时完全不同遮挡关系变化很大。只标满货图片会导致模型在缺货状态下漏检严重。采集时要覆盖不同时段自然光、LED 灯与混合光源。零售门店的灯光色温会影响整体颜色分布而颜色又是 SKU 区分的重要特征。建议每个货架在早中晚各拍一轮并保留一部分不开补光灯的图片做对抗样本。3.2 把 COCO JSON 标注转成 YOLO 格式的脚本多数标注工具导出的是 COCO 格式而 YOLO 训练需要每张图对应一个同名 txt 文件。转换逻辑不复杂读 JSON 里的 images 与 annotations 字段将 bbox 的 xywh 除以图片宽高得到归一化坐标再按类别 id 映射写入 txt。import json import os from pathlib import Path # 映射业务 SKU 到训练类别 id与 data.yaml 中 names 的顺序保持一致 SKU2ID {coca_cola_500ml: 0, pepsi_500ml: 1, lays_original: 2} def coco_to_yolo(coco_path: str, out_dir: str) - None: with open(coco_path, r, encodingutf-8) as f: coco json.load(f) # 建立图片 id 到文件名的索引避免每张图都遍历全量标注 img_map {img[id]: img for img in coco[images]} annotations coco[annotations] for ann in annotations: img img_map[ann[image_id]] img_name Path(img[file_name]).stem txt_path os.path.join(out_dir, img_name .txt) cat_id ann[category_id] if cat_id not in SKU2ID: continue x, y, w, h ann[bbox] # COCO 的 bbox 是左上角坐标转成中心点坐标再归一化 xc (x w / 2) / img[width] yc (y h / 2) / img[height] w_n w / img[width] h_n h / img[height] line f{SKU2ID[cat_id]} {xc:.6f} {yc:.6f} {w_n:.6f} {h_n:.6f}\n with open(txt_path, a, encodingutf-8) as f: f.write(line) if __name__ __main__: coco_to_yolo(datasets/retail/annotations/train.json, datasets/retail/labels/train)脚本的三个要点。第一类别 id 的映射必须和后续data.yaml的names列表一致顺序错了模型训练出来类别全是乱的。第二注意 COCO 与 YOLO 的 bbox 表示差异COCO 是[x, y, width, height]且原点在左上角YOLO 是中心点坐标加宽高都要除以图片宽高归一化。第三脚本用追加写模式重复运行前要先清空目标目录否则会出现同一个标注写两遍。3.3 训练集目录与类别文件的最小结构一个可复现的零售数据集目录应该长这样datasets/retail/ ├── images/ │ ├── train/ # 按货架分区均衡采样 │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── data.yaml └── classes.txtdata.yaml是训练入口至少包含path、train、val和names四项。classes.txt不是训练的必需文件但保存一份能避免后续推理时类别顺序混乱。划分训练集时有一条 рекомендуемое 规则同一个货架同一时间点的连续拍摄帧只能进 train 或 val不能在两侧同时出现。零售场景中相邻帧高度相似混入验证集会虚高 mAP部署到新门店时准确率立刻打回原形。图片尺寸方面建议训练图原始宽度不低于 1080 像素即使后续训练时缩放到 1280 或 640原始分辨率高一些也能给增强留出裁剪空间。数据增强策略也要按零售场景调整。货架图像不会翻转但商品被取出和放回会产生水平位移因此fliplr0.0、scale控制在 0.5 到 1.5 之间比较合理。颜色增强要保守hsv_h设 0.01避免把不同口味的包装颜色增强成同一种。4. 训练自己的 YOLOv11 模型命令、参数表与模型选择4.1 用 YOLO 包跑通最小训练命令YOLOv11 环境配置比较省心pip install ultralytics装完即可训练命令可以直接走 CLI。下面的命令用 yolo11m 做迁移学习训练集是上一章建的零售数据目录yolo detect train \ modelyolo11m.pt \ datadatasets/retail/data.yaml \ epochs120 \ imgsz1280 \ batch16 \ device0,1 \ optimizerSGD \ lr00.01 \ close_mosaic10 \ projectruns/retail \ namesku_v1命令各参数含义yolo11m.pt是预训练权重首次运行会自动下载imgsz1280对应货架小目标的输入分辨率batch16在双卡环境下每张卡 8 张图如果你的显存只有 12G可以降到总 batch 8 并把imgsz降到 960close_mosaic10表示最后 10 个 epoch 关闭马赛克增强这一步不可省否则模型在训练尾声一直看到拼接图推理时对真实遮挡的适应反而变差。用多卡训练时YOLO 包会自动做 DDP 同步不需要手动设置torch.distributed。训练日志实时打印在终端runs/retail/sku_v1/下会生成权重文件和指标曲线图。训练中如果 loss 长时间不降优先看train/box_loss和val/box_loss的差距差距过大说明过拟合需要加大数据增强或增加 val 图像采样。4.2 货架场景的 8 个必调参数参数调整不能照抄通用目标检测的默认值货架场景的密集、小目标、相似外观三个特征决定了某些参数必须改。参数推荐值调整理由imgsz1280货架商品像素小640 下小目标特征几乎消失batch8~16由显存决定优先保证 imgsz 不降optimizerSGD小数据集上用 AdamW 收敛快但泛化差lr00.01m 模型从预训练权重微调时不要超过 0.02close_mosaic10让模型最后适应真实遮挡分布fliplr0.0货架商品不存在镜像语义翻转会造成类别混淆hsv_h0.01包装颜色是 SKU 核心特征过度增强等于毁特征degrees0.0~5摄像头安装有轻微倾斜过强旋转会破坏文字方向optimizerSGD这个选择容易有争议。Ultralytics 默认用 AdamW收敛更快但零售数据集通常只有几千到几万张AdamW 在小样本上更容易记住训练集的灯光、角度特征。SGD 配合 120 epoch 能获得更平滑的决策边界货架场景里外观极其接近的 SKU 对决策边界质量要求很高。另外注意cacheTrue这个参数。零售图片分辨率高磁盘 IO 容易成为训练瓶颈如果机器内存充足加上cacheram能省下大量数据加载时间。但如果数据集超过 20GRAM 缓存可能撑爆内存此时用cachedisk折中。4.3 训练结果怎么看跨类别 mAP 比总 mAP 重要训练完成后runs/retail/sku_v1/下会生成results.csv、confusion_matrix_normalized.png、PR_curve.png等文件。大多数人只看总的mAP50但在零售场景里这个数字会骗人——如果长尾 SKU 只占样本的 5%它们被识别错对总 mAP 影响很小却恰恰是库存差异的主要来源。建议单独跑一次验证并输出每个类别的指标yolo detect val \ modelruns/retail/sku_v1/weights/best.pt \ datadatasets/retail/data.yaml \ imgsz1280 \ batch8 \ save_jsonTrue验证结束后YOLO 包会生成predictions.json里面是每张图每个框的类别、置信度、坐标。用下面的代码快速找出哪些 SKU 召回率低import json from collections import defaultdict with open(runs/retail/sku_v1/predictions.json) as f: preds json.load(f) # 假设已知每类的真实数量这里统计每类的置信度分布 conf_by_cls defaultdict(list) for p in preds: cls_id p[category_id] conf_by_cls[cls_id].append(p[score]) for cls_id, confs in sorted(conf_by_cls.items()): avg_conf sum(confs) / len(confs) low_conf len([c for c in confs if c 0.35]) print(fSKU {cls_id}: 平均置信度 {avg_conf:.3f}, 低于 0.35 的框 {low_conf} 个)这套验证的价值在于把「模型整体好不好」换成「哪些 SKU 会被库存系统漏掉」。平均置信度低且低置信度框集中在某个 SKU 时优先补齐那个 SKU 的训练样本而不是整体加数据。另一个要检查的文件是confusion_matrix_normalized.png如果某两个 SKU 之间存在稳定的互相误判说明它们的特征在模型看来太接近需要考虑增加区分性标注或单独训练一个二分类器。5. 从识别结果到库存动态管理推理保存、目标跟踪与 SKU 映射5.1 YOLOv11 预测后保存推理结果的结构化方式货架识别的下游不是给人看图而是给库存系统喂数据。因此推理脚本的核心任务是把每一帧的检测结果保存成结构化格式并保留与原图画面对应的可视化文件以便排查。利用 YOLO 包提供的回调能力写一个轻量推理器import json from pathlib import Path from ultralytics import YOLO model YOLO(runs/retail/sku_v1/weights/best.pt) def infer_and_save(source_dir: str, output_dir: str, conf: float 0.35): source_dir Path(source_dir) output_dir Path(output_dir) output_dir.mkdir(parentsTrue, exist_okTrue) for img_path in sorted(source_dir.glob(*.jpg)): results model.predict( sourcestr(img_path), confconf, imgsz1280, devicecuda:0, verboseFalse, ) r results[0] # 提取结构化数据 boxes r.boxes.xyxy.cpu().numpy() cls_ids r.boxes.cls.cpu().numpy().astype(int) scores r.boxes.conf.cpu().numpy() records [] for box, cls_id, score in zip(boxes, cls_ids, scores): records.append({ frame: img_path.stem, bbox: box.tolist(), sku_id: int(cls_id), confidence: float(score), }) # 保存 JSON Lines库存服务逐行读取做聚合 with open(output_dir / f{img_path.stem}.jsonl, w, encodingutf-8) as f: for rec in records: f.write(json.dumps(rec, ensure_asciiFalse) \n) # 同时保存带框可视化图供人工抽检 annotated r.plot() # 返回 BGR 的 numpy 数组 import cv2 cv2.imwrite(str(output_dir / f{img_path.stem}_vis.jpg), annotated) if __name__ __main__: infer_and_save(data/round1, runs/retail/infer_round1, conf0.35)代码里面有两个细节值得说明。第一r.plot()和model.predict(..., saveTrue)都能保存可视化结果区别在于saveTrue的路径由 YOLO 内部管理文件名可能被自动改写而手动cv2.imwrite可以完全控制输出路径。生产环境建议用plot()方式便于按门店、轮次分目录管理。第二conf0.35这个阈值是针对货架场景调的。通用检测里常用 0.25但零售识别宁可漏检几个也不希望误检混入库存统计因为漏检可以由下一轮补拍兜底误检会直接污染库存账。批量推理时先看耗时表现。YOLOv11m 在 1280 分辨率下每张图约 15 到 30 毫秒取决于 GPU一个 200 张图的货架照片集一分钟内能跑完完全满足门店定时巡店的节奏。5.2 用目标跟踪去重同一 SKU 多次出现只计一次静态货架图不会出现同一商品移动的问题但门店摄像头常常拍摄视频流同一件商品会因为镜头抖动、行人遮挡而在连续帧中反复被检测到。直接对每帧计数会把 5 件商品数成 12 件。YOLO 包内置了目标跟踪能力为每个检测框分配稳定的 track idyolo track \ modelruns/retail/sku_v1/weights/best.pt \ sourcestore_cam_03.mp4 \ trackerbytetrack.yaml \ conf0.35 \ imgsz1280 \ saveTruetracker 参数可以换botsort.yaml或bytetrack.yaml。货架场景我更推荐 bytetrack因为它的关联策略对低置信度框更宽容而货架上的目标经常处于半遮挡状态低置信度检测框恰好是目标进入遮挡前的最后信息。BOT-SORT 在行人跟踪里表现好但在密集静止目标上容易因为外观特征计算导致 track id 漂移。实际落地时跟踪不能替代计数逻辑而是为计数服务提供去重依据。正确做法是维护一个「当前货架目标表」用 track id 作主键不断更新每个 id 最近一次检测到的 SKU 类别和置信度在一段观察窗口结束后再对同一 SKU 的 track id 集合做去重计数。如果两个 track id 在时空上持续重叠则判定为同一目标被重复跟踪只取置信度更高的那个。5.3 SKU 映射与库存状态判定从像素到库存账检测框类别 id 只是模型内部的数字库存系统需要的是 SKU 编码、商品名称、单位包装规格。这层映射可以放在一个独立的配置文件里避免因模型重新训练导致业务字段丢失{ sku_mapping: { 0: {sku_code: 6901234567890, name: 可乐 500ml, unit: 瓶}, 1: {sku_code: 6901234567891, name: 可乐 330ml, unit: 罐}, 2: {sku_code: 6901234567892, name: 薯片原味 70g, unit: 袋} } }映射表要单独管理不要硬编码在推理脚本里。零售场景的 SKU 会频繁上下架映射表变动只需要发布配置不需要重新跑训练和推理链路。有了 SKU 映射和跟踪去重后的计数库存状态可以按下面的规则判定SKU最近 3 轮盘点数量货架额定容量库存状态可乐 500ml28低库存可乐 330ml06缺货薯片原味55正常状态判定逻辑是连续两轮视觉盘点计数低于额定容量的 30% 时标记低库存计数为 0 时进入缺货状态并生成补货任务。这里要设置一个去抖窗口单轮因遮挡引起的漏检不能直接触发补货必须等到下一轮确认。6. 进阶给「动态」加上时间维度——平滑计数与重扫验证6.1 为什么逐帧计数结果不能直接用货架视频里计数抖动的原因不只是检测误差还有商品被顾客拿起查看再放回的瞬时遮挡。摄像头以 20 到 30 帧每秒采集一次遮挡可能造成连续 5 到 10 帧漏检直接取最小计数或最后一帧计数都会产生大幅波动。库存系统要的是「每 30 秒重扫一次货架得到一个稳定数字」而不是跟踪每个商品的生命周期。我一般用一个轻量的 EMA指数移动平均平滑器来输出最终计数。对每个 SKU 维护ema_count新一帧的有效计数到来时按权重更新class SkuInventoryTracker: def __init__(self, alpha: float 0.3, min_confidence: float 0.5): self.alpha alpha self.min_confidence min_confidence self.ema_count {} self.last_sighting {} def update(self, frame_id: str, sku_counts: dict): from datetime import datetime now datetime.utcnow().isoformat() for sku_id, count in sku_counts.items(): prev self.ema_count.get(sku_id, count) # 丢弃明显异常的跳变防止瞬时误检污染计数 if abs(count - prev) max(3, prev * 0.5): continue new_val self.alpha * count (1 - self.alpha) * prev self.ema_count[sku_id] round(new_val, 2) self.last_sighting[sku_id] now return self.ema_countalpha控制平滑强度alpha0.3表示新计数占 30% 权重历史占 70%。货架场景推荐 0.2 到 0.4 之间太小则响应慢商品被大量取走时库存数字迟迟不跌太大则抖动明显缺货状态会反复横跳。跳变丢弃逻辑的核心是防止把一次误检当真实变化写入库存。比如前一帧统计 5 瓶下一帧突然变成 12 瓶14 秒后变回 5 瓶绝大多数情况是画面里出现了路人遮挡或模型幻觉不是真的补了 7 瓶货。6.2 用定时重扫验证平滑结果是否可靠平滑器解决了抖动但也可能把真实的快速变化掩盖掉。建议在推理管线里加一个「重扫回测」开关记录每一轮原始检测计数和EMA 输出计数在下一次人工盘点时对比两端数据的偏差。具体技巧是在门店盘点结束后把人工计数当作 ground truth与 EMA 输出做逐 SKU 的差值统计差值落在 ±1 以内视为正确并输出偏差最大的前 10 个 SKU。这样既验证了模型也验证了平滑参数是否合理。如果某个 SKU 持续偏差 2 以上通常不是计数问题而是该 SKU 存在相同外观的竞品或同款不同规格混放需要在训练数据里补充区分性样本。把重扫窗口比如 30 秒和 EMA 的alpha做成配置文件后「动态管理」就不再是每帧变化的瞬时值而是一个带置信度的时间序列。库存系统消费这个序列时只有当计数连续两轮低于阈值才创建补货任务避免因为顾客暂时拿起商品而产生无效工单。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →