简介:这份PDF文档面向零售行业技术人员、计算机视觉学习者与门店数字化方案设计者,围绕YOLOv11在货架商品识别与库存自动化管理中的落地展开,帮助读者理解如何用单阶段目标检测替代低效的人工盘点与手工记录。资源包共1个PDF文件,大小约2.13MB,支持目录章节跳转、阅读器左侧大纲显示与章节快速定位,查阅体验完整流畅。文档共38页,内容涵盖零售业智能升级背景与需求、YOLO系列算法演进与YOLOv11网络结构原理、货架商品识别系统搭建(数据采集标注、模型训练评估、系统集成测试)、库存自动化管理系统实现(实时监控、补货提醒、盘点与销售分析)、数据与模型及架构层面的性能优化,以及大型超市、便利店、精品零售店三类案例的应用效果评估。已有95人学习,适合希望系统掌握YOLOv11零售落地路径、对照目录快速定位关键模块的读者参考。
1. 货架商品识别为什么总在"最后一米"翻车
做过零售数字化的人大多有过类似经历:摄像头装好了,模型也训了,离线测试 mAP 看着还行,一上真实货架就原形毕露——密集排列的饮料罐互相遮挡、价签反光、商品被顾客拿起来又放回去导致位置错乱,库存数字和实际对不上。零售业智能升级落到货架商品识别与库存自动化管理这件事上,核心矛盾从来不是"能不能检测出商品",而是"能不能在真实货架的拥挤、遮挡、光照变化下稳定地检测出每一个 SKU,并把结果变成可用的库存数据"。YOLOv11 作为 Ultralytics 在 2024 年推出的新一代检测框架,在骨干网络和特征融合上做了调整,对小目标和密集场景相对友好,这也是它被大量用在货架场景的原因。这篇笔记面向的是想用 YOLOv11 把货架商品识别真正跑起来、并接到库存管理流程里的工程师,从数据准备、模型训练、小目标优化到推理结果落库,把每一步的参数和坑讲清楚。
2. 从货架图片到可训练数据集:标注策略与格式转换
2.1 货架场景的数据集该长什么样
货架商品识别和通用目标检测最大的区别在于类别体系的设计。通用检测数据集里"人""车""狗"是互斥的,但货架上同一层可能摆着几十个外观高度相似的 SKU,比如不同口味的同一品牌饮料、不同容量的同款洗发水。如果直接把每个 SKU 当成一个类别,类别数轻松上百,而且长尾分布极其严重——畅销品样本几千张,滞销品可能只有十几张。
我一般的做法是分两层:第一层做"商品/非商品"和"货架层"的粗检测,用于定位货架结构和判断缺货;第二层在粗检测裁剪出的区域内做细粒度 SKU 分类或检测。这样第一层的数据量需求小、泛化好,第二层可以针对重点品类单独优化。如果业务要求端到端出 SKU 位置,那就必须保证每个类别的实例数不低于 200,低于这个数的类别要么合并、要么用数据增强补。
采集时要注意几个硬性条件:摄像头视角固定(货架场景通常用固定枪机而非手持)、覆盖早中晚三种光照、包含"满架/半空/缺货"三种状态。缺货样本尤其重要,因为库存自动化管理的核心价值就是发现缺货,如果训练集里全是满架图,模型会把空位误判成背景。
2.2 标注规范与常见错误
标注用 LabelImg 或 CVAT 都行,关键是框的边界要统一。货架商品密集,标注员很容易出现两种偏差:一是框贴太紧,把商品边缘裁掉几个像素;二是框太松,把相邻商品的边缘框进来。这两种偏差在训练时都会让模型学到错误的边界特征。
统一规范建议:框的边界贴着商品可见轮廓的外沿,遮挡情况下只标可见部分,不要脑补被遮挡的区域。对于被遮挡超过 70% 的商品,直接标为"忽略区域"(在 YOLO 格式里可以用一个特殊类别或者干脆不标),否则模型会学到大量噪声。
2.3 把标注转成 YOLOv11 能吃的格式
YOLOv11 沿用 YOLO 系列的标注格式:每张图对应一个同名 txt,每行是类别索引 中心x 中心y 宽 高,坐标全部归一化到 0-1。如果你用的是 COCO 格式或者 VOC 格式,需要转换。下面是一个 VOC XML 转 YOLO txt 的脚本:
import xml.etree.ElementTree as ET import os # 类别名到索引的映射,必须和 data.yaml 里的 names 顺序一致 CLASS_MAP = {"bottle": 0, "can": 1, "box": 2, "bag": 3} def voc_to_yolo(xml_path, img_w, img_h, out_txt_path): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in CLASS_MAP: continue # 跳过未定义类别,避免索引错位 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) # 归一化并转成中心点+宽高 cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 边界裁剪,防止标注越界导致训练报错 cx = min(max(cx, 0.0), 1.0) cy = min(max(cy, 0.0), 1.0) w = min(max(w, 0.0), 1.0) h = min(max(h, 0.0), 1.0) lines.append(f"{CLASS_MAP[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_txt_path, "w") as f: f.write("\n".join(lines))这段脚本的关键点有三个:CLASS_MAP的顺序必须和训练时data.yaml里的names完全一致,否则类别会错乱;归一化后的坐标要做 0-1 裁剪,因为标注员偶尔会把框拖出图片边界,越界坐标会让 YOLOv11 在计算损失时产生 NaN;保留 6 位小数是为了避免密集小目标在量化时丢失精度。
转换完成后,目录结构按 Ultralytics 的约定组织:
dataset/ images/ train/ val/ labels/ train/ val/ data.yamldata.yaml内容:
path: /abs/path/to/dataset train: images/train val: images/val names: 0: bottle 1: can 2: box 3: bag提示:
path建议写绝对路径,相对路径在不同工作目录下启动训练时容易找不到数据,这是新手最常踩的坑之一。
3. YOLOv11 训练货架检测模型:参数怎么设、什么时候停
3.1 模型选型:n/s/m/l/x 在货架场景怎么选
YOLOv11 提供 n、s、m、l、x 五个尺度。货架场景的特点是目标密集、单类实例多、但类别语义相对简单(都是商品),所以不需要特别大的模型容量。我的经验是:如果部署在边缘设备(如门店的 Jetson 或 RK3588),选 yolov11n 或 yolov11s;如果部署在服务器端做批量盘点,选 yolov11m 性价比最高。l 和 x 在货架场景的收益递减明显,因为货架目标的类间差异主要靠纹理和颜色,而不是复杂语义,大模型容易过拟合到训练集的光照条件。
一个反直觉的结论:在货架商品识别里,yolov11s 加上充分的数据增强,往往比 yolov11l 在真实门店的泛化更好。原因是小模型对纹理噪声的鲁棒性反而更强,大模型会把训练集里某个货架的特定反光模式记下来。
3.2 训练命令与关键参数
Ultralytics 的训练入口很简洁,但参数含义要清楚:
yolo detect train \ model=yolov11s.pt \ data=/abs/path/to/data.yaml \ epochs=200 \ imgsz=1280 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ warmup_epochs=3 \ mosaic=1.0 \ close_mosaic=20 \ scale=0.5 \ degrees=0.0 \ fliplr=0.5 \ hsv_h=0.015 \ hsv_s=0.7 \ hsv_v=0.4 \ device=0 \ project=runs/shelf \ name=v11s_1280逐个说关键参数。imgsz=1280是货架场景的核心参数,因为货架图片里单个商品可能只占 30×30 像素,用默认的 640 会让小目标在特征图上只剩几个像素,直接消失。1280 的代价是显存和推理时间翻倍,但货架检测的精度提升非常明显。如果显存不够,可以用imgsz=1024折中。
mosaic=1.0是 YOLO 系列的招牌增强,把四张图拼成一张,能显著提升小目标和密集场景的表现。但close_mosaic=20必须在最后 20 个 epoch 关掉,否则拼接边界会让模型学到不存在的"图片边缘"特征,推理时出现莫名其妙的误检。
degrees=0.0是因为货架摄像头是固定的,商品不会旋转,开启旋转增强反而会让模型对"倒着的商品"产生错误认知。fliplr=0.5可以保留,因为左右翻转不改变商品的语义。
hsv_s=0.7和hsv_v=0.4调得比默认值高,是为了应对门店不同时段的光照变化和不同门店的色温差异。这是货架场景和通用检测的重要区别——光照鲁棒性直接决定模型能不能跨店部署。
3.3 训练过程怎么判断好坏
训练日志里要盯的不是loss,而是metrics/mAP50-95和val/box_loss的走势。货架场景常见的一种情况是:训练 loss 一直降,但验证 mAP 在某个 epoch 后停滞甚至下降,这说明模型开始记忆训练集的特定货架布局。这时候应该看验证集里"跨货架"的那部分样本(如果做了划分),而不是整体 mAP。
另一个要看的指标是每个类别的 AP。货架场景的长尾问题会让某些 SKU 的 AP 极低,但整体 mAP 被头部类别拉高,掩盖了问题。训练完一定要导出confusion_matrix.png和results.csv,逐个类别看。
如果验证 mAP 在 150 epoch 左右还在缓慢上升,可以继续训到 300;如果 100 epoch 就平了,加 epoch 没用,应该回去检查数据标注质量或者增加数据多样性。
4. 小目标与遮挡:货架检测绕不开的两个硬骨头
4.1 小目标优化的三个实操手段
货架商品识别里,小目标是最常见的翻车点。一个标准货架层高 40cm,摄像头距离 3 米,一个 5cm 宽的饮料罐在 1080p 画面里大概只有 40 像素宽,缩放到 640 输入后只剩 20 像素。YOLOv11 的 P3 特征图 stride 是 8,20 像素的目标在 P3 上只有 2-3 个格子,特征极其稀疏。
第一个手段是提高输入分辨率,前面说的imgsz=1280就是最直接的办法。第二个手段是调整 anchor 或者用 YOLOv11 自带的anchors自适应机制,让先验框更匹配小目标尺寸。第三个手段是切片推理(SAHI 思路),把大图切成小块分别检测再合并,这对超高分辨率货架图特别有效。
切片推理的核心逻辑:
from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction # 加载 YOLOv11 权重,注意 model_path 指向训练好的 best.pt detection_model = AutoDetectionModel.from_pretrained( model_type="yolov11", model_path="runs/shelf/v11s_1280/weights/best.pt", confidence_threshold=0.25, device="cuda:0" ) result = get_sliced_prediction( "shelf_01.jpg", detection_model, slice_height=640, # 切片高度,建议和训练 imgsz 接近 slice_width=640, overlap_height_ratio=0.2, # 切片重叠比例,防止目标被切断 overlap_width_ratio=0.2 ) result.export_visuals(export_dir="vis/")slice_height和slice_width设成和训练分辨率接近,能让每个切片里的目标尺度和训练时一致。overlap设 0.2 是为了处理跨切片边界的商品,重叠太小会漏检,太大则推理时间线性增长。SAHI 的代价是推理时间变成原来的 4-9 倍,所以只适合服务器端批量盘点,不适合实时视频流。
4.2 遮挡处理的思路
货架遮挡分两种:商品之间的相互遮挡,以及货架结构(层板、立柱)对商品的遮挡。前者靠数据增强里的mosaic和随机遮挡(cutout)来模拟,后者需要在标注时把货架结构也标出来,让模型学会区分"被挡住的商品"和"货架本身"。
一个实用技巧是在训练时加入copy-paste增强:把标注好的商品实例随机粘贴到其他货架图上,人为制造遮挡组合。Ultralytics 原生不支持这个,需要自己写 dataloader 或者在标注阶段就生成合成图。这个手段对提升遮挡场景的召回率效果明显,但要注意粘贴的商品光照要和背景一致,否则模型会学到"粘贴痕迹"这个伪特征。
4.3 密集场景的 NMS 调参
货架商品挨得近,默认的 NMS IoU 阈值 0.7 会导致相邻商品被误抑制。把iou调到 0.5-0.6 能保留更多相邻框,但代价是同一商品可能出多个框。实际调的时候要看具体货架的密集程度:饮料罐排列紧密的用 0.5,大件商品用 0.65。
推理时的命令:
yolo detect predict \ model=runs/shelf/v11s_1280/weights/best.pt \ source=shelf_test/ \ imgsz=1280 \ conf=0.25 \ iou=0.55 \ save=True \ save_txt=True \ save_conf=Truesave_txt=True和save_conf=True是接库存管理的关键,前者输出每个框的坐标和类别,后者带上置信度,后续做库存统计时可以用置信度过滤低质量检测。conf=0.25是货架场景的经验值,低于这个值的框大多是反光或价签引起的误检。
5. 从检测结果到库存数字:避坑与排查清单
5.1 检测框数量不等于库存数量
这是最容易翻车的地方。模型在一张货架图上检测出 47 个框,不代表库存就是 47。原因有三:同一商品可能被检测出多个框(NMS 没抑制干净)、被遮挡的商品可能漏检、货架深处的商品可能因为透视变形被误判。
正确的做法是建立"货架-层-位"的空间映射。先用货架结构检测确定每一层的位置,再把商品框分配到对应的层和位,最后按位统计。这样即使某个商品漏检,也能通过"位空缺"判断缺货,而不是简单地数框。
5.2 常见问题排查
现象:模型在训练集上 mAP 很高,换一个门店就崩。原因:训练集的光照、货架材质、摄像头角度太单一,模型过拟合到了特定环境。 解决:采集至少 3 个不同门店、不同时段的数据,训练时加大hsv增强,验证集必须包含未见过的门店。
现象:小目标召回率低,大目标正常。原因:输入分辨率不够,或者 P3 特征层的感受野和小目标尺度不匹配。 解决:提高imgsz到 1280 或 1536,检查data.yaml里小目标类别的实例数是否足够,必要时用 SAHI 切片推理。
现象:推理结果里同一商品出现多个重叠框。原因:NMS 的iou阈值太高,或者模型对某个类别的置信度普遍偏低导致抑制不彻底。 解决:把iou降到 0.5-0.55,检查训练时是否close_mosaic没生效导致模型学到了拼接边界特征。
现象:训练到一半 loss 变成 NaN。原因:标注里有越界坐标,或者学习率太高导致梯度爆炸。 解决:用前面的转换脚本做坐标裁剪,把lr0从 0.01 降到 0.005,加warmup_epochs=5。
现象:推理速度远低于预期。原因:imgsz太大、模型尺度选太大、或者没开半精度。 解决:推理时加half=True开启 FP16,边缘设备考虑导出 TensorRT 或 ONNX,yolo export model=best.pt format=engine half=True。
6. 把检测接到库存系统:一个可验证的落地技巧
模型跑通只是第一步,真正决定这套方案值不值得投入的,是检测结果能不能稳定地变成库存数字,并且这个数字能被业务验证。我一般会做一个"双通道校验":模型检测出的库存数,和 POS 系统的销售数据做对账。如果某个 SKU 模型显示缺货但 POS 显示还有库存,或者反过来,就说明检测在这一天出了问题,需要人工复核。
具体做法是在推理结果落库时,除了商品类别和数量,还记录三个字段:检测时间戳、货架 ID、每个检测框的置信度均值。然后用一个简单的 SQL 做日对账:
-- 找出模型库存和 POS 库存差异超过阈值的 SKU SELECT d.sku_id, d.shelf_id, d.detect_count, p.pos_count, ABS(d.detect_count - p.pos_count) AS diff FROM daily_detect_summary d JOIN daily_pos_summary p ON d.sku_id = p.sku_id AND d.shelf_id = p.shelf_id WHERE ABS(d.detect_count - p.pos_count) > 3 ORDER BY diff DESC;diff > 3这个阈值要根据货架容量调,小货架用 2,大货架用 5。对账差异大的 SKU 自动进入人工复核队列,复核结果反过来标注成训练数据,形成闭环。这个闭环是整套系统能不能长期跑下去的关键——没有它,模型会随着门店陈列变化慢慢退化,而你不知道。
还有一个技巧是给每个检测框加一个"稳定性分数":连续 N 帧(如果是视频流)或者连续 N 次盘点(如果是定时抓拍)都检测到同一位置有商品,才认为这个位置确实有货。单次检测的偶然误检会被这个机制过滤掉。实现上就是在数据库里对同一shelf_id + position做计数,超过阈值才更新库存状态。
# 简化的稳定性判断逻辑 STABLE_THRESHOLD = 3 # 连续 3 次检测到才算稳定 def update_inventory(shelf_id, position, detected, history): key = f"{shelf_id}_{position}" if key not in history: history[key] = [] history[key].append(1 if detected else 0) # 只保留最近 N 次记录 history[key] = history[key][-STABLE_THRESHOLD:] if len(history[key]) == STABLE_THRESHOLD and sum(history[key]) >= STABLE_THRESHOLD - 1: return "in_stock" elif len(history[key]) == STABLE_THRESHOLD and sum(history[key]) == 0: return "out_of_stock" return "unknown" # 状态未稳定,不更新库存这个逻辑看起来简单,但它把"单次检测的玄学"变成了"多次观测的统计",是让库存自动化管理真正可用的关键一步。我踩过的最大坑就是早期直接用单帧检测结果更新库存,结果顾客走过货架时手部遮挡导致大量误报缺货,门店店长直接不信任这套系统了。后来加了稳定性判断,误报率降了一个数量级。
做这套方案,我的习惯是先把检测精度做到能接受的下限(比如 mAP50 到 0.85),再花同样甚至更多的时间在结果后处理和业务对账上。模型本身只是整个库存自动化管理链条里的一环,后面那套"检测-映射-对账-反馈"的闭环才是真正决定项目成败的地方。希望帮到你。
本文还有配套的精品资源,点击获取