简介:一份面向目标检测学习者的YOLOv11零售场景实战文档,聚焦货架商品识别与库存动态管理两大核心任务,系统讲解从需求分析、算法原理到系统搭建与实验评估的完整链路。文档基于单阶段检测算法的高效优势,针对零售行业人工盘点效率低、库存数据不准、缺货与积压并存等实际痛点,给出可落地的技术方案,涵盖YOLO系列算法演进、网络结构与损失函数、数据采集与标注、模型训练与部署、库存预警与补货策略等关键环节,并包含实验结果对比与系统整体性能分析,适合具备一定深度学习基础、希望将YOLOv11应用于实际业务的开发者参考。资源包为单个PDF文件,大小1.89MB,共35页,支持目录章节跳转及阅读器大纲快速定位,结构清晰、图文完整。目前已有77人浏览学习,内容编排贴近真实项目流程,读者可直接复现系统搭建思路,节省自行摸索的时间。
1. 零售货架识别:为什么 YOLOv11 值得拿来重新做一遍
把 YOLOv11 用到零售货架商品识别上,多数团队的第一版方案是拿通用预训练模型直接扫货架照片,然后数框。实际跑下来会撞上两个问题:货架上的商品又密又小,几十个 SKU 挤在一层隔板里,通用模型的漏检率居高不下;而且“识别”只是第一步,库存动态管理要的是缺货率、排面数这类能对账的业务数字,不是一串坐标框。
这篇笔记按一条能落地的路径来写:货架数据怎么标注成 YOLOv11 能学透的格式,训练参数怎么为小目标优化,模型怎么在 Jetson Nano 这类端侧设备上完成部署,最后又如何把检测框换算成库存系统敢直接用的数字。适合正在做门店巡检、无人货柜或自动盘点的工程师参考。
2. 先定任务边界:货架数据怎么标、库存指标怎么算
2.1 SKU 粒度标注:货架识别和通用检测的第一个分叉点
货架识别项目首先要回答一个业务问题:识别一个“商品”,还是识别一个“SKU”?这是它跟通用目标检测最大的区别。通用检测里“一瓶可乐”是一个目标,但在零售盘点里,“500ml 塑料瓶可乐”和“330ml 罐装可乐”是两个 SKU,同框出现时模型必须区分开。凡是打算把识别结果接进库存系统的,标注就要做到 SKU 粒度,否则后续所有统计都是错的。
做法上,我一般先做 SKU 清单冻结。把一张货架图上出现的所有商品编号列成表,标注类别名直接用 SKU 编号,而不是中文名或商品俗名。这样做有三个好处:类别数量稳定、标签文件干净、后续和 ERP 系统的 SKU 主数据能直接通过编号 JOIN。如果是新开门店,SKU 清单还在变动,就先跳过饮料、零食这类高频换品区,等清单冻结后再标,避免返工。
标注尺度上定两条硬规则。第一,一个商品不论被遮住多少,只要它的可辨识中心区域(瓶标、包装正面)露出来,就按一个目标标注;中心区域被完全盖掉的不标。这样训练出来的模型不会在严重遮挡时输出一堆半框。第二,同一排面相邻的同 SKU 商品,如果两个外接框之间有肉眼可见的间隙,拆成两个独立框;完全紧贴的,宁可合并成一个框。这个选择直接影响后面排面数计算的准确性,值得在标注规范里写清楚。
2.2 从检测框到业务指标:排面数、缺货率、SKU 分布
货架检测模型的直接输出是坐标框和置信度,库存管理系统要的不是这些。实际项目里,我用检测结果换算三个核心指标:排面数、缺货率、SKU 位置分布。
排面数指某层货架上某个 SKU 占几个商品位。人工盘点时数排面是核心动作,模型输出后按货架行分组,同一行内同 SKU 的框数就是排面数。缺货率的计算稍复杂一些:同一行内两个相邻框的水平间隙超过一个排面宽度时,判定为一个空排面,空排面数除以该行总排面数就是缺货率。SKU 分布则用来做动销判断,同一 SKU 的排面数在两次巡检之间明显减少,多半意味着售罄或者下架,可以自动推送补货任务。
这些指标有一个共同的前置条件:知道哪几个检测框属于同一层货架。常见两种做法,一是人工在画面里标行分隔线(ROI),把每层隔板的高低位置存成 JSON;二是用检测框的纵向中心点做聚类自动分行。固定点位摄像头我建议用人工 ROI,一次标定长期有效;移动巡检设备上用聚类更省事,代价是偶尔分行错误需要人工复核。
| 业务指标 | 检测输出的换算方式 | 数据依赖 |
|---|---|---|
| 排面数 | 同一货架行内,同一 SKU 的框数 | 行 ROI、SKU 类别 |
| 缺货率 | 行内相邻框水平间隙大于阈值的空排面数 / 总排面数 | 行 ROI、间隙阈值 |
| SKU 分布 | 全图按 SKU 类别汇总框数及位置 | SKU 类别 |
2.3 为什么选 YOLOv11:结构和算力上的取舍
先回答一个常被问到的问题:货架场景为什么值得升到 YOLOv11。从工程角度看,YOLOv11 延续了 Ultralytics 的生态,一个包解决训练、导出、推理全套流程,团队成员上手成本低。结构上它去掉了 anchor,用 C3k2 模块替换部分 C3,显存占用更低,推理速度在同类模型里不落下风,这对端侧部署非常关键。货架场景目标密、尺寸小,模型需要的是小目标召回率上的表现,而不是极端精度,YOLOv11 的参数量优势刚好落在实用区间。
算力选型要跟巡检方式绑定。固定点位(门店天花板摄像头、通道尽头工位)可以用带 GPU 的工控机,愿意牺牲一点功耗换取大模型精度;如果是店员推着巡检车,或者做轨道机器人,端侧设备功耗受限,典型的是 Jetson Nano 这类板子。选型时先算一笔账:单张货架图的像素宽度、每张图平均目标数量、要求单图推理时延,再反推用哪个尺寸的权重。
实际经验是,货架巡检不需要视频流级别的实时推理,每隔几十秒取一帧完全够用,这个节奏下 Jetson Nano 的算力是够的。项目启动时先把算力预算定下来,再回头决定训练时的 imgsz 和模型尺寸,能避免后续部署阶段模型切不动、反复重训的尴尬。
3. 用 YOLOv11 训练货架识别模型:参数设置与小目标优化
3.1 环境配置与最小训练命令
环境配置本身不难,货架项目里真正的麻烦是训练机上同时装了多个深度学习框架,torch 版本冲突是常见翻车点。我的建议是新建独立 conda 环境,Python 用 3.9 或 3.10,先装 torch,再装 ultralytics。
conda create -n yolo11 python=3.9 conda activate yolo11 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics第一行创建独立环境,第二行激活。第三行装 torch 时显式指定 CUDA 11.8 的预编译包,避免 ultralytics 依赖自动拉取最新版 torch 和训练机 CUDA 版本不匹配。如果训练机的 CUDA 是 12.x,把 cu118 改成 cu121。装完先跑一句python -c "import torch; print(torch.cuda.is_available())",确认 GPU 可见再继续,不要跳过这一步。
数据格式按 YOLOv11 的要求组织,一份 data.yaml、一个 images 目录、一个 labels 目录。一个容易踩的坑是标注工具导出的标签文件里会有空行或空目标,训练时报错,标注完先用脚本过滤一遍。
yolo detect train data=retail_shelf.yaml model=yolo11n.pt epochs=100 imgsz=640 batch=16这是最小训练命令。model=yolo11n.pt是官方预训练权重,第一次跑通用流程用 nano 级就行,正式训练再按显存升级到 s 或 m 级。retail_shelf.yaml里的 path 指向数据集根目录,train 指向训练图片目录,names 按 SKU 编号顺序列出。这套命令跑通之后,再开始调参。
3.2 三个必调参数:imgsz、batch、epochs
货架图片和自然场景图最大的差异是信息密度。一张 2048x1536 的门店照片里,一个 SKU 可能只有 40x60 像素,imgsz=640 会把商品缩到大约 15x25 像素,基本糊成一团。所以货架项目我一般把 imgsz 提到 1280 或 1536,然后看显存决定 batch 能撑多大。这不是让模型变强,而是让模型在训练时能看见原来的目标尺寸,小目标检测的第一原则就是别让目标在输入图上缩水。
batch 的设定没有玄学,就是看显存。8GB 显存跑 imgsz=1280,batch 从 4 开始试,OOM 就降到 2;显存占用一直在 90% 以上,也往下降一档。另一个有用的小开关是cache=True,把图片缓存到内存,省去训练中反复读盘的时间,配合 batch 调参体验提升明显。
epochs 方面,货架数据量通常不大,几千张图是常见规模。我一般先用 100 轮跑基线,观察 val_box_loss 在第几轮开始不再下降,然后按这个拐点乘 1.5 做正式轮数。有预训练权重在手时,50 到 100 轮之间基本都能收敛,关键是开着早停patience=20,训练曲线已经平了还空转是常见浪费。
3.3 小目标优化:SAHI 切片推理与注意力机制怎么选
当 imgsz 拉满后 mAP 仍然上不去,下一个有效手段是切片推理。开源库 SAHI 做的就是这件事:把大图切成小 patch 分别推理,再合并结果。这对货架图特别合适,单层隔板上的商品可能只有几十像素高,但整图很大,直接缩小全图会让小目标不可识别。流程是训练时仍用 1280 输入,推理时把原始大图按 640 切片,加上重叠区,推理后把边界框映射回原图坐标系。
from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction detection_model = AutoDetectionModel.from_pretrained( model_type="ultralytics", model_path="best.pt", confidence_threshold=0.25, image_size=1280, device="cuda:0", ) result = get_sliced_prediction( "shelf_01.jpg", detection_model, slice_height=640, slice_width=640, overlap_height_ratio=0.2, overlap_width_ratio=0.2, postprocess_type="NMM", postprocess_match_threshold=0.5, ) result.export_visuals(export_dir="output/")这是典型的 SAHI 切片预测。slice_height 和 slice_width 设成与训练输入一致,保证每个 patch 里目标的相对尺寸和训练时接近。overlap 比例 0.2 是为了防止目标恰好落在切片边缘被切掉一半。postprocess_type用 NMM 而不是 NMS,因为同一个目标在不同 patch 里重复出现时,NMM 对这类重叠框的抑制更稳定。跑完后 result 里的 object_prediction_list 包含每个目标的框、类别和置信度,可以直接存 JSON。
注意力机制是另一个改进方向。YOLOv11 自带 C2PSA 注意力,对长条形货架行的语义理解有帮助;想更激进一点,HCA Net 这类外挂注意力网络也可以集成,但训练复杂度和显存占用都会上升。我的经验是先跑通 SAHI,确认切片增益,再决定要不要动模型结构,注意力改进的收益有时不如多标两百张难例图片来得实在。
3.4 训练验证:置信度分布和排查顺序
货架模型的验证不能只看 mAP50。货架图片里不同 SKU 的目标数量极不均衡,mAP 被大类平均之后容易掩盖小类的问题。训练完我建议跑一次验证集预测,输出每张图的预测结果,按置信度区间画出目标数量分布:如果大量真实目标落在 0.2 置信度以下,说明模型对这类目标的特征没学好,优先去看是哪一类;如果分布在 0.5 以上,问题在业务阈值设定而不是模型。
排查顺序固定成一条线:先看数据标签有没有错,最常见的是类别串号;再看 imgsz 是不是把目标缩没了;然后看训练曲线的收敛情况;最后才调 NMS 阈值和置信度阈值。这个顺序能省掉大量无效调参。
4. 在 Jetson Nano 上部署 YOLOv11:导出、加速与推理的详细步骤
4.1 从 best.pt 到 TensorRT engine:两次转换与版本匹配
训练机的 PyTorch 权重不能直接拿到 Jetson Nano 上跑。标准做法是先在训练机导出 ONNX,再把 ONNX 拷到 Jetson 上用 TensorRT 转成 engine 文件。Ultralytics 的 export 命令把第一步做得很简单:
yolo export model=best.pt format=onnx opset=12 dynamic=Falseopset=12 是因为 Jetson 上的 TensorRT 版本通常不再更新到更高 opset 的解析能力,保守的 12 兼容性最好。dynamic=False固定输入尺寸,推理时省去动态 shape 的开销。导出成功后把 best.onnx 拷贝到 Jetson Nano,用 TensorRT 自带的 trtexec 转 engine:
/usr/src/tensorrt/bin/trtexec \ --onnx=best.onnx \ --saveEngine=best.engine \ --fp16--fp16在 Jetson Nano 上能带来明显的速度提升,货架识别对精度不是极端敏感,可以接受。如果你的板子是 NX 或 Orin,也可以试 INT8 量化,但需要准备校准数据集,工作量是另一个量级,不建议第一期就上。转完建议保存一份 engine 文件和对应的 ONNX、训练配置参数,方便追溯。
4.2 端侧推理脚本:加载 engine、保存结果、性能压测
engine 文件准备好之后,直接用 ultralytics 的 Python API 加载。注意 Jetson 上也要装 ultralytics 包,PyTorch 版本要和 JetPack 自带的 CUDA 匹配。下面的脚本覆盖加载、推理、保存结果三个动作:
from ultralytics import YOLO model = YOLO("best.engine", task="detect") results = model.predict( source="shelf_01.jpg", conf=0.25, iou=0.45, imgsz=1280, save=True, project="runs/detect", name="shelf_result", save_txt=True, save_conf=True, ) for r in results: for box in r.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) xyxy = [round(v, 2) for v in box.xyxy[0].tolist()] print(f"SKU {cls_id}, conf {conf:.2f}, box {xyxy}")model.engine是 TensorRT 引擎,推理在 GPU 上执行。save=True把标注后的图片存到runs/detect/shelf_result/;save_txt=True生成与图片同名的 txt 标签文件,格式是class x_center y_center width height;save_conf=True让 txt 里多一列置信度。这个组合是货架巡检里最常用的结果保存方式,后续脚本直接读 txt 做业务换算。
提示:Jetson Nano 上首次加载 engine 会慢,因为要做显存分配和上下文初始化,这不是故障。压测性能时用 100 张图的目录做循环推理,统计平均每张耗时,不要拿单张图刚加载完时的数据当基准。
4.3 定时巡检与结果落盘:货架场景的合理推理节奏
货架管理不需要视频流级别的帧率。常见做法是做成定时任务,每隔半小时或一小时抓一帧,送进模型推理,结果写 JSON 或 txt,传到门店本地服务器或云端。这个节奏下 Jetson Nano 的算力不但够用,还留有余量。
部署形态上有一个容易忽略的点:相机的自动白平衡和曝光。货架灯光会在一天内变化,同一位置上午和下午的照片色温不同。如果推理脚本固定从摄像头抓帧,需要把相机的自动曝光关闭,锁定曝光参数,否则模型性能会随天气一起漂移。这个坑在下一章细说。
5. 货架识别落地中的五个坑:现象、原因与解决办法
5.1 光照漂移导致全线漏检,模型扛不住傍晚
现象:训练集里 mAP 到 0.9,部署到门店,白天效果尚可,傍晚开始大量漏检,补光灯亮起后误检激增。
原因是训练数据大多来自办公室或白天门店采集,样本里的光照分布是单峰的,模型学到的是“这个亮度下的商品长什么样”,而不是“商品本质长什么样”。解决分三步:采集阶段刻意覆盖早晚两个时段和灯光模式,各占至少 15%;部署阶段关闭相机自动曝光,锁定固定参数;条件允许时做一次灰度增强训练的对比实验,确认模型对亮度变化的依赖程度。这个坑不解决,后面所有指标都是空中楼阁。
5.2 邻架商品串框,NMS 拦不住
现象:A 货架最上层的最右商品,检测框跨到了旁边区域;或者同一排面的商品被输出成两个上下重叠的框,排面数莫名翻倍。
原因是全局 NMS 只按 IoU 抑制同一个类别,相邻货架之间同 SKU 的框在图像上不重叠或重叠很少,不会被抑制。货架行本身的结构边界没有被模型感知。解决方式是在业务逻辑里加一道行内过滤:先按检测框的 cy 坐标映射到行 ROI,同一行内按 x 坐标排序,对相邻框做二次判断,类别相同且水平间距小于一定像素时,保留置信度高的那个。这道后处理不增加训练成本,却是排面数计算稳定的前提。
5.3 SKU 换包装后识别率骤降,类别体系设计缺陷
现象:某个 SKU 换了新包装,识别率从 90% 掉到 40%,误检集中在旧包装样本附近。
原因是目标检测模型记忆的是包装外观,不是商品本身。换包装等于在这个类别上失去了新样本,模型只能把相似包装归给旧类。解决方式是把 SKU 主数据与模型类别解耦:换包装的 SKU,旧包装样本继续留在训练集里,新增的包装图片单独加一类,类别名按 SKU 编号加后缀区分,比如 SKU1234_v2,用新类别做增量训练。上线后前两周重点盯这个 SKU 的置信度分布,波动大就拉出来复核。
5.4 Jetson Nano 推理忽快忽慢,不是模型问题
现象:单张图推理时间从 0.5 秒到 3 秒波动,平均耗时远高于预期,怀疑模型没转好。
原因是 Jetson Nano 的 GPU 采用共享功耗和散热设计,模块温度到 80 度以上会降频;另外推理进程和其他系统进程抢内存也会造成卡顿。解决方式:跑推理前用jetson_clocks锁高频,推理循环里检查显存占用;同时打开风扇或使用带散热底座的套件。压测性能时用连续 100 张图的循环推理平均耗时来评判,不要拿单张图刚加载完时的数据当基准。
5.5 推理结果目录覆盖,保存路径的约定要早定
现象:脚本运行无报错,输出目录是空的,或者多次运行结果互相覆盖,找不到对应货架的图片。
原因是 Ultralytics 的 predict 在 project 相同且 name 相同时会自动递增目录名,第一次是 shelf_result,第二次是 shelf_result2,脚本如果写死路径读旧目录,就会读错或覆盖。解决方式:每次运行传入带时间戳的 name,比如shelf_result_20250115_1530,并把返回结果的save_dir直接作为后续读取路径,不要自己拼路径。这个习惯能省掉很多巡检脚本的数据对账时间。
6. 从检测框到库存动态管理:排面计算、验证节奏与数据回流
6.1 把检测结果换算成排面数的脚本
货架行 ROI 标好后,排面数计算就是纯几何问题。下面的脚本读推理保存的 txt 结果,按行 ROI 分组,组内按 SKU 计数:
import json from pathlib import Path roi = json.loads(Path("shelf_roi.json").read_text()) det_path = Path("runs/detect/shelf_result_20250115_1530/labels") def row_of(cy, roi): for row_id, row in roi.items(): if row["top"] < cy < row["bottom"]: return row_id return None sku_facing = {} for txt_file in det_path.glob("*.txt"): for line in txt_file.read_text().strip().splitlines(): cls_id, xc, yc, w, h, conf = map(float, line.split()) row_id = row_of(yc, roi) if row_id is None: continue key = (row_id, int(cls_id)) sku_facing[key] = sku_facing.get(key, 0) + 1 print(sku_facing)逻辑是遍历每个标签文件,把每行按空格拆成 6 列(因为推理时开了 save_conf),用 y 中心点判断属于哪一行 ROI,再按(行号, SKU 类别)计数。用 yc 归属而不是框 IoU 归属,是因为行 ROI 是水平条带,实现简单,对轻微斜视角度也能容忍一定误差。
6.2 先用一个货架跑通验证:三天内的验收流程
不建议一上来就铺全店。这个项目值不值得继续投入,先用三天做一个验证:选一个 SKU 种类 20 到 40 的货架,固定相机位,拍 10 张不同角度和时段的照片,标注训练后部署到 Jetson Nano,输出排面数,和人工盘点表逐行对一遍,统计排面数误差和缺货识别率。
验证通过的标准建议定两条:排面数识别准确率 90% 以上,缺货位置识别准确率 80% 以上。达不到就先别扩量,回到数据采集和标注环节补样本。这个验证结果也是向业务方要资源的最好材料。
6.3 数据回流与模型版本管理:让识别结果反过来修数据集
落地运行一段时间后,容易被忽略的是数据回流。推理结果里置信度低但真实存在的目标,以及置信度高但实际不存在的目标,都是下一轮训练的金矿。做法是给巡检脚本加一个保存困难样本的分支:当一张图的置信度分布出现异常,比如平均置信度低于 0.3,或者某 SKU 框数骤减,自动把原图存到 inbox 目录,每周人工检视一次,挑出有代表性的样本加入训练集。
这看起来是额外工作,但货架场景的 SKU 变化和灯光变化是常态,没有数据回流机制的模型会在上线后一个月内悄悄退化。我自己做这类项目的一个习惯是:训练集里永远保留每个 SKU 最早期的 10 张图和最近期的 10 张图,防止模型被新样本带偏;同时把模型版本号和训练集版本号写进每次推理结果的 JSON 文件头,复盘时能知道某天的结果是用哪版模型跑出来的。
货架识别这类项目的难点从来不在模型精度本身,而在于让模型精度在真实门店的噪声里保持稳定。从数据标注到排面数计算再到样本回流,这套流程能把 YOLOv11 的识别结果稳定地变成库存系统可用的数字。希望帮到你。
本文还有配套的精品资源,点击获取