☰
YOLOv11货架商品识别实战:从数据标注到动态库存管理
2026/10/5 15:41:49 网站建设 项目流程

简介:面向零售行业技术开发者与管理者,这份PDF围绕YOLOv11实现货架商品识别与库存动态管理展开,针对传统人工盘点效率低、易出错和库存数据滞后等痛点,提供完整的算法选型、系统搭建与优化思路。文档共35页,内容从零售业务需求分析切入,系统讲解YOLO系列算法演进及YOLOv11的核心原理(网络结构、损失函数、训练流程),并覆盖货架商品识别系统的数据采集与标注、模型训练与部署,库存动态管理系统的实时监控、预警机制与补货策略,最后给出实验结果分析和未来展望。整个资源以1个PDF文件形式提供,压缩包仅1.89MB,支持目录章节跳转和左侧大纲快速定位,文字、图表显示清晰,便于按章查阅。目前已有77人学习/下载,很适合正在探索目标检测在零售场景落地实践的中高级开发者。

1. 零售货架识别的真实画像:为什么库存管理卡在"视觉最后一公里"

在一家连锁便利店的总部,每天上午都会有几十个门店店长把货架照片发到群里,然后由运营人员一张张打开、比对、录入缺货商品。这套流程的代价不仅是人力,更在于数据是"事后"的——等发现某款饮料已经空了两天,补货单才被创建。货架商品识别要解决的正是这个"视觉最后一公里":让摄像头或手机拍的普通照片,直接变成结构化库存数据。标题里的 YOLOv11,是目前把这件事落地最顺的检测模型之一;它负责找出画面里的每个商品、每个空位,再把位置和类别信息喂给后端的动态库存逻辑,让"缺了什么、还剩多少、该补多少"从一个需要人看的问题变成一个可以自动计算的指标。这篇笔记适合两类人:一类是想在自己门店或小仓库里搭一套库存视觉方案的技术负责人,另一类是想把 YOLOv11 练出来并部署在零售场景的算法工程师。我会从数据、训练、小目标优化到库存去重,把这条路完整走一遍,并标出我踩过的坑。

2. 货架商品识别的数据与选型:先搞定标签,再谈YOLOv11

2.1 货架场景为什么让检测模型集体翻车:光照、遮挡和密集排列

在普通目标检测数据集里,一个物体通常占据画面的主要部分,背景干净、目标独立。货架却完全是另一个世界:商品紧密排列,相邻包装几乎贴在一起;瓶装饮料有反光,袋装零食有皱褶;层板遮挡下半部分,价签、促销牌又制造大量干扰。我第一次拿通用 COCO 预训练模型直接去跑超市饮料货架照片,结果非常震撼——它把一排同款可乐认成十几个不同物体,还把价签当成了商品。原因在于通用模型的学习分布和货架场景差异太大。

货架识别的几个硬约束:一是密集,同类商品挨得很近,NMS(非极大值抑制)稍微激进一点,就会把相邻的真实目标合并成一个;二是弱纹理,很多商品包装大面积纯色或高光,模型不容易提取稳定特征;三是类别不平衡,可口可乐和百事可乐长得像,但库存意义上它们是不同 SKU,需要模型区分到包装细节。所以在上手 YOLOv11 之前,第一优先级不是调参,而是把数据问题和标签问题想清楚。常见做法是自采数据,不要试图在网上找现成的商品数据集——不同国家、不同货架的陈列方式差异太大,公开数据集只能用来做预训练或验证流程。

2.2 数据采集与标注:货架图片怎么拍、标签怎么打

采集阶段要覆盖门店的多个角度、多个时间段、多种光环境。我会用一个可以伸缩的自拍杆,模拟不同身高视角,从正面和稍微俯视的角度各拍一遍;每张照片里保证货架层板大致水平,商品不出现严重透视变形。数量上,每个 SKU 至少要有 200 个实例,如果 SKU 总数是 50,那么标注框总量最好在 1 万到 2 万之间。注意这里的"实例数"是指所有图片中该商品出现的次数之和,不是图片张数。

标注工具我一般用 LabelImg 或 X-AnyLabeling。标注时有一个容易被忽略的细节:框要贴住商品可见部分,而不是商品投射到地面的完整矩形。比如一个被前面商品挡住一半的盒子,只标可见区域;如果完全看不见,就不标。这样训练出来的模型学的是"当前视角下能看到什么",而不是"想象背后有什么",能防止推理阶段产生幻觉。标注类别名建议直接使用 SKU 编码,例如sku_coca_cola_500ml,不要用"可乐1"这种显示名,因为后续库存管理逻辑会直接匹配这个编码。

标注完以后,还需要做一遍质量抽检。我会写一个脚本统计每个类别的标注框宽度、高度分布,如果发现某个类别的框宽度中位数只有十几个像素,这个类别大概率属于小目标,需要在后文的小目标优化阶段特别处理。以下是检查标注分布的一个简单脚本:

import os from collections import Counter # 遍历YOLO格式的txt标注文件 label_dir = "labels/train" size_stats = Counter() box_count = Counter() for fname in os.listdir(label_dir): with open(os.path.join(label_dir, fname)) as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls = int(parts[0]) w = float(parts[3]) # 归一化宽度 h = float(parts[4]) # 归一化高度 box_count[cls] += 1 size_stats[cls] += w # 累计宽度,粗略看小目标比例 for cls, cnt in box_count.items(): avg_w = size_stats[cls] / cnt print(f"Class {cls}: boxes={cnt}, avg_norm_width={avg_w:.4f}")

这段代码的作用是读取 YOLO 格式的标签,统计每个类别的标注框数量和平均归一化宽度。如果某个类别的平均归一化宽度小于 0.05,意味着在 1920x1080 的画面里宽度不到 96 像素,基本可以归入小目标范畴,后续要考虑切图或调整增强策略。参数说明:labels/train是你的训练集标注目录,YOLO 标签每行格式是class x_center y_center width height,其中位置和宽高都是相对图片尺寸的归一化值。

2.3 模型选型与权重文件:为什么从YOLOv11开始,以及怎么下载预训练权重

目标检测模型现在选择非常多,RT-DETR、YOLOv8、YOLOv11、YOLOv12 都在迭代。我在零售场景里优先选 YOLOv11 而不是更早的版本,原因有两个:一是 Ultralytics 对 YOLOv11 的工程化支持非常完善,从训练到 ONNX 导出再到 TensorRT 部署,命令行和 Python API 都有文档;二是 YOLOv11 在同样精度下推理速度比 v8 更快,对门店里几十路摄像头的场景来说,单路节省几个毫秒累积起来都是电费和时间。

但要注意,YOLOv11 不是一个"装上就懂货架"的模型。它自带的是 COCO 预训练权重,能认出 80 类日常物体,但里面没有"可口可乐细长罐"或"某品牌薯片"这种细粒度类别。所以你需要下载 COCO 预训练权重作为起点,在自己的货架数据上微调。下载方式有两种:一种是在 Ultralytics 代码里直接指定模型名,例如yolov11n.pt、yolov11s.pt、yolov11m.pt,第一次运行时会自动从 GitHub 下载;另一种是从官方发布页手动下载到本地。我建议用yolov11s.pt作为起点,n 太小对包装细节的拟合能力不足,m 和 l 在训练阶段吃显存,先跑通流程再用更大的模型。

权重文件下载后的存放位置也有讲究,最好固定在一个目录,比如weights/,并在训练脚本里用绝对路径引用。因为 Ultralytics 在训练时会自动把权重缓存到当前目录,如果你在不同目录下运行了多次,会残留多个.pt文件,后期排查版本时容易混乱。实际项目中,我习惯先把yolov11s.pt复制到项目weights/下,再开始写训练脚本。下面会展示完整的环境配置和训练流程。

3. 从环境配置到首个训练:Ultralytics版YOLOv11跑通货架模型

3.1 适合0基础的YOLOv11环境配置:Python、CUDA、ultralytics安装

网上搜"YOLOv11环境配置",会看到大量适合0基础纯小白的超详细教程,但很多是从安装 Anaconda 开始一步步截图,信息密度太低。我这里给一个更简洁、也更容易复现的步骤。前提是你有一台 Windows 或 Linux 电脑,建议显存 6GB 以上;没有 GPU 也能训练,但速度会慢到让你失去耐心。

首先是创建 Python 环境。我习惯用 conda,但 venv 也完全可以。YOLOv11 依赖 Python 3.8 以上,建议 3.10 或 3.11,因为很多第三方库对 3.12 的兼容性还在补。创建的 Python 环境要与显卡驱动匹配的 CUDA 版本匹配,但 Ultralytics 的 pip 包会自动安装一个可用的 torch 版本。最稳妥的做法是:先安装 CUDA 11.8 或 12.1 对应的 PyTorch,再安装 ultralytics。以下命令在 Ubuntu 20.04 上验证过:

# 创建环境并激活 conda create -n yolov11 python=3.10 -y conda activate yolov11 # 安装 PyTorch(根据你的 CUDA 版本选择命令,这里是 CUDA 11.8) pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118 # 安装 ultralytics pip install ultralytics

这段命令的核心是两步:先把 PyTorch 装对,再装 ultralytics。--index-url参数指定 PyTorch 官方预编译包,避免 pip 默认装到 CPU 版本。安装完成后,用python -c "import torch; print(torch.cuda.is_available())"验证 CUDA 是否可用,输出True说明 GPU 版本装好了。如果输出False,先检查显卡驱动nvidia-smi,再看 PyTorch 的 CUDA 版本是否匹配。这里最常见的翻车是:电脑装的是 CUDA 12.2,但 pip 装了 cu118 的包,其实这通常也能用,因为 PyTorch 会捆绑自己的 CUDA 运行时,只要驱动足够新就行。

3.2 把你的货架数据转成YOLO格式,并用命令行开始训练

标注工具直接输出 YOLO 格式还好,如果你从 LabelImg 导出了 VOC XML 或 COCO JSON,需要先转换。Ultralytics 支持读取 COCO 格式的数据集目录,但更简单的做法是转成 YOLO 的目录结构。标准结构如下:

dataset/ images/ train/ 20240101_shop001_01.jpg val/ 20240101_shop002_01.jpg labels/ train/ 20240101_shop001_01.txt val/ 20240101_shop002_01.txt data.yaml

data.yaml是数据集描述文件,内容很简单:

path: /path/to/dataset # 数据集根目录 train: images/train # 训练图片目录 val: images/val # 验证图片目录 nc: 20 # 类别数量(SKU数) names: 0: 'coca_cola_500ml' 1: 'pepsi_cola_500ml' # 按顺序列出所有SKU

nc必须和names里的数量一致,否则训练时类别索引会越界。转换过程中要注意类别 ID 的顺序:YOLO 标签里的数字类 ID 是依据data.yaml里names的顺序,而不是你的文件夹顺序。我见过一个项目,标注时用的 ID 和 names 表错位,导致模型训练时"认识"的类别和标签不一致,准确率却还很高——因为它把两种长得很像的商品当成了一类,这类错误最危险。

转换完成后,就可以用最简命令训练了:

yolo train data=dataset/data.yaml model=weights/yolov11s.pt epochs=100 imgsz=640 batch=16 lr0=0.01

yolo train是 Ultralytics 的命令行入口。data指定数据集配置,model指定起始权重,epochs=100是训练轮数,imgsz=640是输入图片尺寸,batch=16是批大小,lr0=0.01是初始学习率。第一次跑的时候,Ultralytics 会读取模型结构并加载预训练权重,同时打印网络结构摘要。如果你的显存是 8GB,batch=16 配 imgsz=640 可能刚好,但若训练中途报CUDA out of memory,就调到 batch=8 或 imgsz=512,不要硬扛。

3.3 训练参数怎么设:imgsz、batch、epochs与数据增强的取舍

训练参数不是越猛越好。很多人在一张 1080p 的货架照片上有 200 个小商品,直接 imgsz=640 训练,结果小目标被缩得太小,几乎看不见。常见做法是把 imgsz 提高到 960 或 1280,但显存占用按平方增长,batch 必须降低。我一般会先看标注框的平均大小:如果大多数框的宽高都在原图的 5% 以下,imgsz 至少要 960,否则后面再怎么调模型都没用。这里有个血泪经验:不要因为 GPU 显存大就无脑开 imgsz=1280,大图训练会让模型过度关注纹理,反而在真实环境中因为摄像头分辨率不足而翻车。

epochs 也不是越大越好。100 epoch 对货架这种中大规模数据集足够;如果数据量小,几百个 epoch 很容易过拟合,验证集 mAP 开始下降后就停。Ultralytics 默认开了早停(patience=50),不用关。学习率用默认的 0.01 配合余弦退火即可,如果 loss 震荡,降低到 0.005。重点要动的是数据增强参数。Ultralytics 默认开启 mosaic、fliplr 等,但货架图片有强方向性,水平翻转要谨慎——价签文字翻转后会变得不可读,模型可能学到错误线索。我在训练中会关闭垂直翻转,并降低透视增强权重,让模型专注于商品本身,而不是过期促销牌。

训练完成后,模型权重保存在runs/detect/train/weights/best.pt。注意是best.pt而不是last.pt,last.pt只是最后一个 epoch 的权重,通常不如 best 准。接下来我们进入推理和小目标优化阶段。

4. 让模型看清小目标:YOLOv11的小目标优化与推理结果保存

4.1 货架商品为什么是小目标重灾区:感受野与特征图的关系

YOLOv11 沿用了 FPAN 多尺度特征融合结构,输入图片缩放到imgsz后,经过若干次下采样,会有三个尺度的输出头,分别负责大、中、小目标。但很多时候,货架商品几十像素宽,而小目标的输出头对应的是 80x80 的特征图,每个网格的原始感受野可能已经覆盖了周围好几个商品,这让小目标区分度很低。我在实际测试中发现,依云矿泉水瓶如果画面里只有半个瓶身,模型很容易把它漏检,或者和旁边的苏打水混检。

解决思路有三个层面,按性价比排序:第一,提高输入分辨率,让目标占更多像素;第二,在推理时做切片(TTA),把大图切块分别检测;第三,从网络结构上增强小目标分支,比如给 YOLOv11 加一个更高分辨率的检测头,或者引入注意力模块。前两个是工程手段,立竿见影;最后一个是研究手段,网上常提到的"YOLOv11 小目标优化"里,HCANet 这类注意力网络就是尝试用注意力引导模型关注小而重要的区域。但在项目初期,我强烈建议先做前两件事,用便宜的手段换精度,再决定是否动模型结构。

4.2 三个立竿见影的小目标优化:切片推理、多尺度训练和注意力模块

切片推理是解决小目标最直接的方法。把一张 1920x1080 的货架图分割成 3x3 的九个 640x640 的小块,分别送入模型,然后把所有检测框映射回原图坐标。这样每个商品在原图中的尺寸不变,但检测时它占输入图像的比例变大了。代价是推理时间约为原来的 9 倍,所以只适合门店每日低频盘点,不适合实时流。Ultralytics 的predict方法不直接支持自动切片,但你可以用 Python 脚本实现。

多尺度训练也值得开,Ultralytics 默认会在训练时随机缩放 0.5~1.5 倍,但只限于同一个 batch 内的统一缩放。如果想针对小目标更激进,可以把scale设为 0.3~0.9,让模型见过更多被缩得很小的实例,增强小目标鲁棒性。不过要配合足够大的imgsz,否则小目标本来就只有几个像素,再缩小就真的消失了。

还有一个常见做法是修改yolov11s.yaml中的anchors。YOLOv11 默认锚框是根据 COCO 统计的先验,你用货架数据训练时,可以先用 k-means 重新聚类你的标注框,得到更贴合商品的锚框尺寸,替换配置文件中的值。不过我在实践中发现,YOLOv11 的 anchor-free 分支对这种修改并不敏感,改锚框收益不如提高分辨率来得高。所以我的建议是:预算精力集中在输入分辨率和切片推理,锚框调参可以作为最后的手段。

给出一个切片推理并保存结果的 Python 脚本,这是实际项目里经常要用到的工具:

import cv2 import torch from ultralytics import YOLO import numpy as np model = YOLO("runs/detect/train/weights/best.pt") def slice_inference(image_path, save_path, slice_size=640, overlap=0.2): img = cv2.imread(image_path) h, w = img.shape[:2] results_all = [] step = int(slice_size * (1 - overlap)) # 生成切片坐标,按步长滑动 for y in range(0, h, step): for x in range(0, w, step): x1 = min(x + slice_size, w) y1 = min(y + slice_size, h) # 保证切片尺寸不小于模型要求 crop = img[y:y1, x:x1] if crop.shape[0] < 64 or crop.shape[1] < 64: continue res = model.predict(crop, imgsz=640, conf=0.25, verbose=False)[0] for box in res.boxes: cx, cy, bw, bh = box.xywh[0].tolist() orig_x = x + cx - bw / 2 orig_y = y + cy - bh / 2 results_all.append([ int(box.cls[0]), float(box.conf[0]), orig_x, orig_y, orig_x + bw, orig_y + bh ]) # 在原图上绘制所有切片检测结果 for cls, conf, x1, y1, x2, y2 in results_all: cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) label = f"{model.names[cls]} {conf:.2f}" cv2.putText(img, label, (int(x1), int(y1)-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 1) cv2.imwrite(save_path, img) return results_all

这段代码的关键在坐标还原:每个切片在网络内部被缩放到了 640 大小,但输出框的xywh是相对于切片的归一化坐标,必须乘上切片实际尺寸,再加上切片左上角在原图中的偏移量。step是滑动步长,overlap=0.2让相邻切片有 20% 的重叠,避免商品刚好被切缝切成两半。重叠区域里的同一商品可能被重复检测,所以后面还需要去重,这个我们放到库存管理章节讲。脚本中model.predict的conf=0.25是置信度阈值,太低会出很多误检,太高会漏检小目标,一般 0.25~0.35 是个安全区间。

4.3 预测之后怎么保存结果:两种常用的保存方式与可视化

YOLOv11 保存推理结果很灵活。最简单的是用model.predict时指定save=True,Ultralytics 会把带框的图片保存到runs/detect/predict下。但这在批量处理门店图片时不够用,因为你要的不是可视化图片,而是结构化结果。第二种是直接读取检测结果并序列化,比如存成 JSON 或写入数据库。以下代码展示如何把一张图的检测结果保存为 JSON:

from ultralytics import YOLO import json model = YOLO("best.pt") results = model.predict("shop_01.jpg", imgsz=960, conf=0.3, verbose=False)[0] output = { "image_file": "shop_01.jpg", "detections": [] } for box in results.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) x1, y1, x2, y2 = [float(v) for v in box.xyxy[0].tolist()] output["detections"].append({ "sku": model.names[cls_id], "confidence": conf, "bbox": [x1, y1, x2, y2] }) with open("shop_01_det.json", "w", encoding="utf-8") as f: json.dump(output, f, ensure_ascii=False, indent=2)

这段代码的results.boxes.xyxy返回的是绝对像素坐标,直接可存。注意model.names是从训练配置继承的类别名,如果你做过类别映射,确保推理时加载的权重和names一致。另外,results.boxes是按置信度降序排列的,但不同 SKU 之间没有排序,后续去重和计数时注意不要假设顺序。

5. 库存动态管理落地的避坑与常见问题排查:从识别到计数

5.1 现象:模型在测试集上很好,一上货架就乱——光照和遮挡排查

现象:训练集 mAP 0.85,验证集 0.80,看起来不错,但拿到门店现场测试,模型频繁漏检深色包装商品,比如黑咖啡、深色酱油瓶;有时候把阴影里的饮料箱当成空位。

原因:货架场景的光照变化远大于你的离线数据集。门店的灯管色温、空调出风口阴影、商品包装的强反光,都会改变颜色分布;测试集如果都是晴天拍摄,模型会把"晴朗"当成隐性特征。深色商品在暗处对比度低,特征被背景吞没。

解决:在采集阶段刻意覆盖不同光照条件,特别是阴天、夜晚室内灯光、阳光直射货架的场景。如果无法重新采集,就用数据增强模拟:对训练集随机调整亮度、对比度、饱和度,让模型对光照不敏感。Ultralytics 的训练配置里有hsv_h、hsv_s、hsv_v这几个增强参数,默认值已经开启,但强度不够,我可以将hsv_v从默认 0.4 调到 0.6,hsv_s从 0.7 调到 0.8。另外,在推理侧使用灰度均衡或 CLAHE 预处理,能缓解局部阴影。我的一次真实经历是:把曝光不足的测试图强行提亮后,漏检率从 40% 降到 12%。

5.2 现象:类别总是认错——相似商品和标签噪声的处理

现象:同品牌的乌龙茶和绿茶包装几乎相同,只是标签颜色不同;模型经常把两款口味弄混,导致库存统计里某个 SKU 的数量虚高,另一个虚低。

原因:模型在低分辨率下只能看到颜色和形状,如果两种商品包装在同一货架的反射条件下颜色相近,就会混淆。还有一个隐蔽原因是标签错误:标注人员在框选时,把相邻的绿茶框给了乌龙茶,这等于喂给模型错误答案。数据噪声对细分类的伤害比想象中大,有时候 5% 的错标就能让准确率掉 10 个点。

解决:第一,对容易混淆的 SKU,采用"差量标注"策略——人工检查每张图中这些易混品类的标签,确保边界和类别都正确。第二,在训练时给这些少数类提高权重,Ultralytics 没有直接提供类别权重,但你可以用class_weight搭配loss_scale实现,或者简单地对易混类别的图片做更多复制。第三,提高 imgsz 到 1280 让模型看到足够大的标签区域。如果还是分不清,就该考虑引入 OCR 识别包装上的文字辅助分类,而不是只靠视觉特征。

5.3 现象:库存计数对不上——重复检测与漏检的解决

现象:一张货架图里明明有 12 瓶某饮料,算法数出来 14 个,因为相邻两瓶被识别成同一个,或者同一瓶在切片重叠区域被检测两次;另外一排深处的小瓶装被前面的大罐子挡住,模型看不到,漏掉 2 个。

原因:重复检测源于切片重叠和 NMS 参数过松;漏检源于严重遮挡和小目标,模型只能看到可见部分,如果一个商品被遮挡了 70%,它确实不应该被算作"有货",因为从视觉上看它已经不可售。

解决:重复检测的解法是用非极大值抑制和时序去重。在切片场景里,我写的slice_inference会返回多个重复框,可以先用全局 NMS 合并。Ultralytics 的model.predict内部已经做了全局 NMS,但对跨切片的重复框无效,因为它们是分开推理的。所以你需要自己实现一个 IoU 去重:把同一类别、IoU 大于 0.4 的框合并成一个,置信度取最高。漏检的问题要结合库存语义:如果商品被遮挡但货架层板反射或摆放规律暗示后面还有,应该通过"排面计数"补齐。比如按行检测,把同一行的最小、最大 x 坐标与标准商品宽度相除。这些规则要由后端库存系统来做,模型只负责输出可见框。我的经验是,不要试图让模型"数出绝对准确的数量",而是让它输出"当前可见排面数",然后结合每个 SKU 的单排最大容量推算缺货深度。

5.4 现象:训练慢且显存爆——硬件与参数的边界

现象:用 16GB 显存训练 imgsz=960,batch=16,一上去就CUDA out of memory;降 batch 后训练一个 epoch 需要 40 分钟,完全无法迭代。

原因:显存占用主要由输入特征图大小和 batch 大小决定。imgsz=960 时,特征图是 640 的 2.25 倍,显存也接近 2.25 倍,加上梯度、优化器状态,16GB 很可能不够。

解决:优先降 batch,因为 batch 对显存的影响是线性的,而 imgsz 是二次的。同时打开 Ultralytics 的amp=True混合精度训练,显存能省一半。如果显存还是不够,就把imgsz降回 640,并用切片推理处理小目标。另外,可以考虑冻结前 10 层做迁移学习,能减少显存和训练时间。Ultralytics 的freeze参数可以指定冻结层数,例如freeze=10会让前 10 层参数不更新。我通常在数据量小于 2000 张时冻结 10 层,等到 loss 不再下降再解冻微调。这里有个玄学:冻结过多时模型可能学不到货架特征,所以要在训练曲线上观察第一次下降后是否停住了,停住就解冻。

6. 让库存动态管理真正跑起来:时序去重与补货预警的落地技巧

6.1 用视频流和帧间匹配实现动态库存:一个轻量跟踪方案

静态图识别只能得到某一时刻的库存快照。要做"动态管理",还需要连续帧之间的去重和跟踪。常见做法是给检测到的每个目标分配一个 ID,然后用 IoU 与上一帧目标匹配。当摄像头固定时,货架上的商品位置基本不变,只是偶尔被顾客碰动;所以简单的位置匹配就足够。我会为每个跟踪目标记录一个"最后一次被看到的时间",一旦连续 N 帧没有匹配,就认为该商品被移动或售出了,库存量减一。这里要注意不要用过于复杂的跟踪器,货架商品不会高速运动,你用 ByteTrack 反而是杀鸡用牛刀。

class TrackState: def __init__(self): self.tracks = {} # track_id -> (bbox, last_seen_frame) def update_tracks(detections, frame_id, iou_thr=0.4): # detections是当前帧的检测框列表,每个框为 [sku, x1, y1, x2, y2] # 对每个框,找上一帧IoU最大的track进行匹配 for det in detections: best_tid = None best_iou = iou_thr for tid, (old_bbox, last_frame) in self.tracks.items(): iou = compute_iou(det_bbox(det), old_bbox) if iou > best_iou: best_iou = iou best_tid = tid if best_tid is not None: self.tracks[best_tid] = (det_bbox(det), frame_id) else: new_id = max(self.tracks.keys(), default=0) + 1 self.tracks[new_id] = (det_bbox(det), frame_id)

这个轻量跟踪不依赖深度学习,计算量可以忽略。compute_iou就是标准交并比计算。关键参数iou_thr设为 0.4,太高会导致同一目标轻微移动就产生新 ID,太低会让不同商品匹配到一起。如果你发现库存抖得厉害,检查这个阈值是不是太严。

6.2 从库存计数到补货预警:规则怎么写才不误报

最后一步是把识别和跟踪结果转成业务动作。我的做法是,每轮扫描结束,统计每个 SKU 的可见排面数,再与"安全库存阈值"比较。如果某 SKU 数量低于阈值且持续 10 分钟没有恢复,就生成补货任务。这个"持续 10 分钟"非常重要,因为顾客临时拿起又放回、补货员正在上架都会导致检测值瞬时波动。用时间窗口过滤,可以避免系统每天产生几十条假警报。

还有一个易踩的坑:不要把同一个商品在摄像头视野内停留的帧数直接累加为"库存数"。货架商品只要没有移动,它就会持续出现在每一帧里,正确的做法是记录"当前实时的唯一目标数",而不是累计检测次数。我见过一个团队把检测次数当成库存,结果一天下来同一瓶可乐被计数几千次。所以在跟踪方案里,库存量等于self.tracks中在最近 N 帧内活跃的 track 数量,而不是历史累计值。

总结一下我自己的教训:做货架库存管理,最大的风险不是模型精度,而是业务流程对"视觉不确定性"的容忍度。哪怕模型做到 99% 准确,那 1% 的错误在成千上万个 SKU 上也是不可接受的。所以一定要给后端留一个人工复核和规则纠正的入口,让模型和人类各司其职。希望这些实操细节能帮你把 YOLOv11 真正落到货架上;如果你从环境配置开始走一遍,至少能避开我踩过的那几个大坑。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询