ARTICLE DETAIL

资讯详情

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

YOLOv11零售货架商品识别:从训练调参到库存统计落地

YOLOv11零售货架商品识别:从训练调参到库存统计落地 简介内容面向零售行业算法工程师与目标检测学习者由35页PDF系统讲解YOLOv11在货架商品识别与库存动态管理中的完整落地流程。整体覆盖零售场景需求分析、YOLO系列算法演进与原理、商品识别系统搭建、库存监控与补货策略并给出实验结果评估既适合入门者建立技术框架也能为实际项目提供参考。资源包共1个文件为PDF文档压缩包大小1.89MB支持目录跳转、左侧大纲与章节快速定位文字、图表显示正常。文档重点展示单阶段检测算法如何快速精准识别多目标并结合库存预警与数据决策帮助商家降低成本、减少缺货积压。目前已有77人学习这份资料适合需要系统掌握目标检测在零售业应用的读者。1. YOLOv11在零售货架识别里先解决什么问题从逐帧画面到库存数字货架上两瓶饮料只差一行包装字摄像头挂在头顶却只当安防用补货员每天要花两小时巡好几遍货架——这是零售场景里最典型也最费人力的环节。YOLOv11做货架商品识别不是做一个能画出框的Demo而是要把连续视频帧变成“哪个货架位缺了哪种商品”的库存数字再推给补货流程去执行。这个方向真正难的不是类别多而是商品外观极其接近、货架排列密集、一瓶水在1080p画面里可能只有几十个像素以及单帧漏检如果直接当作缺货信号会把补货通知刷爆。这篇笔记适合门店数字化、无人货架、供应链库存团队里负责落地的工程师目标是让模型不仅认得货还能接得住库存管理这一层逻辑。2. 装好能跑通YOLOv11的环境Python、PyTorch与ultralytics的选型顺序2.1 先选模型尺寸再选机器货架场景为什么从s或m起步YOLOv11这一代的公开权重里按体积从小到大大致是n、s、m、l、x。网上经常讨论yolov11改进点和网络结构但在零售货架这件事上真正决定选型的不是结构细节而是你的摄像头数量、采集频率和推理机器。货架是静态场景不需要每秒处理几十帧两秒一帧完全够用所以推理速度不是首要约束精度才是。我一般建议从s或m开始做基线不要一上来就用l或x。原因有两点第一零售货架的数据集规模通常不大几百张到几千张图撑不起大模型模型大了反而容易过拟合第二部署端很可能是普通办公机或者Jetson这类边缘设备l和x的显存和延迟会直接卡住上线节奏。以下是我常用的选型参考型号精度层次显存占用推理速度货架场景建议n最低极小最快只适合快速验证链路不建议直接上线s中等2-4GB快小门店单机推理够用m较高4-8GB中等推荐起点精度与开销较平衡l高8-12GB慢离线批量抽帧分析可用x最高12GB以上最慢不推荐除非类别数量超过几十个2.2 创建Python环境并安装依赖先装torch再装ultralytics环境配置是yolov11进入工程实践的第一道坎大多数翻车都出在PyTorch和ultralytics版本不匹配上。常见做法是先创建独立的conda环境再装指定CUDA版本的PyTorch最后才装ultralytics。顺序反过来容易让pip自动拉一个CPU版的torch训练时慢得让人怀疑人生。# 创建 Python 3.10 的独立环境避免污染其他项目 conda create -n retail python3.10 -y conda activate retail # 安装 CUDA 12.1 对应的 PyTorch如果你的显卡驱动版本老改成 cu118 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 # 安装 ultralytics 包它会连带装上 opencv 等依赖 pip install ultralytics这段命令里有两个参数需要根据机器调整。一个是--index-url里的cu121它对应CUDA 12.1如果本地nvidia-smi显示的CUDA版本是11.8就换成cu118否则torch虽然能装上但调用GPU时会报错说CUDA驱动不兼容。另一个是Python版本ultralytics目前对3.10支持最稳不要图新鲜用3.12部分依赖的wheel还没跟上容易踩坑。装完以后先验证GPU是否真的被torch识别到这一步能省掉后面所有“训练时莫名其妙用CPU跑”的排查时间。python -c import torch; print(torch.__version__, torch.cuda.is_available())如果cuda.is_available()输出的是False别急着重装先检查是不是装成了CPU版。用pip list | grep torch看看包名后面有没有cpu后缀有的话就按上面的命令重新装GPU版。2.3 一条命令验证环境并保存推理结果环境跑通后先用官方预训练权重跑一张示例图确认检测、可视化、结果保存三件事都正常。这一步也是后面所有工作能否继续的前提。# 下载预训练权重并用示例图跑一次推理 yolo predict modelyolo11s.pt sourcehttps://ultralytics.com/images/bus.jpg saveTrue命令里的modelyolo11s.pt指定使用s尺寸的预训练权重saveTrue会把可视化结果图保存到runs/detect/predict/目录。如果网络下载不了权重可以先用一张本地图片做source权重文件单独下载后放到当前目录。跑通之后再看一下用Python接口做推理输出的方式因为后续做库存统计时你需要的是坐标和置信度数据不是一张画了框的图。from ultralytics import YOLO # 加载本地权重后续可以直接换成训练出来的 best.pt model YOLO(yolo11s.pt) # 对单张货架图做推理imgsz 设成 1280 是因为货架图通常像素密度高 results model.predict( sourceshelf_01.jpg, conf0.25, saveTrue, # 保存带框可视化图方便人工抽查 save_txtTrue, # 保存 txt 标签文件供库存统计读取 save_confTrue, # 在 txt 里追加置信度字段 imgsz1280 ) # 每个结果对象里的 boxes.data 是 N x 6 的数组x1, y1, x2, y2, conf, cls print(results[0].boxes.data)这段代码里最关键的参数是save_txt和save_conf。如果只开着saveTrue你得到的是图片后续统计库存还得重新解析图片开了save_txt之后每个检测框会按YOLO格式写进txt文件配合save_conf每一行末尾还带置信度直接就能做后处理。imgsz1280针对货架这种小目标场景很有用默认的640会丢失远处货架层上的商品信息。3. 货架商品数据集SKU定义、标注规范与转换脚本3.1 先定义清楚“一个类别”是什么SKU粒度决定模型上限做货架识别数据集时最常见的失败不是标注数量不够而是类别定义本身就有问题。货架商品有两种分类粒度品类可乐、果汁、矿泉水和SKU可口可乐330ml罐装、可口可乐500ml瓶装。YOLOv11的类别是模型输出的直接维度类别设得越细模型需要区分的特征就越精细对数据量和成像质量的要求也越高。我的一般做法是第一版先按“品类规格”定义类别比如cola_330ml和cola_500ml分开但把同一品牌的不同口味先合并。原因是货架识别要服务库存管理缺的是“某种规格的商品还剩多少”这个数字而不是“哪个口味”。如果两个SKU外观差异只在于包装上的一行小字摄像头角度稍微偏一点就分不出来强行分开只会让混淆矩阵里这两类互相污染。等第一版跑通确认误判集中在哪几对SKU上再决定要不要用OCR或细粒度分类去拆。类别不平衡也是个绕不开的问题。畅销品在训练集里的标注框可能有几千个冷门品只有一两百个。YOLOv11对类别不平衡的处理能力有限完全靠采样策略补不回来。我的底线是每个类别至少收集200个实例框低于这个数就考虑合并到上级品类。标注质量比数量重要框偏了5个像素在小目标上就是一次漏检。3.2 采集与标注同一货架要拍出“不同时间”的差异货架商品识别的训练数据不能只在白天灯光均匀的时候拍。实际部署后模型要面对早中晚不同色温的灯光、补货员站在货架前遮挡、商品被顾客拿乱后侧摆倒放、甚至货架灯管坏了一半的情况。只拍整齐的货架训练出来的模型在实拍场景里必然漏检。采集时尽量覆盖这几类变化同一货架从正面偏左、正面偏右两个角度各拍一组同一组商品分上午、下午、晚上三个时段拍把部分商品故意摆成侧放、倒放、外包装褶皱的状态。标注时用开源标注工具LabelImg这类就够导出VOC格式或COCO格式都行。标注框要贴住商品主体不要把背景框进来也不要只框商品的三分之一。对于远方高层货架上的小商品宁可放大图片再标也不要随手画一个糊框。3.3 把VOC标注转成YOLO格式并划分训练集ultralytics训练需要的是每个图片对应一个同名txt文件每行格式为“类别id 中心x 中心y 宽 高”坐标都是归一化到0到1之间的小数。大多数标注工具导出的是VOC或COCO格式转换这一步不能手搓用下面这个脚本批量处理。import os import random from pathlib import Path import xml.etree.ElementTree as ET # 类别列表一旦定下就不能轻易改顺序否则旧标签全错位 CLASSES [cola_330ml, cola_500ml, water_550ml, juice_orange] def voc2yolo(xml_path, out_dir): 把单张VOC XML标注转成YOLO txt tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASSES: continue box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) # 转成中心和宽高并归一化到 0~1避免不同分辨率图片混训出问题 cx ((x1 x2) / 2) / img_w cy ((y1 y2) / 2) / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{CLASSES.index(name)} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) Path(out_dir).mkdir(parentsTrue, exist_okTrue) Path(out_dir, xml_path.stem .txt).write_text(\n.join(lines)) xml_files list(Path(annotations).glob(*.xml)) random.seed(7) random.shuffle(xml_files) val_count max(1, int(len(xml_files) * 0.1)) # 先转全部再按文件名分训练/验证图片同步复制到对应目录 for i, xml_file in enumerate(xml_files): subset val if i val_count else train voc2yolo(xml_file, fdataset/labels/{subset}) print(fconverted {xml_file.name} - {subset})这段脚本里有一个容易被忽略的参数random.seed(7)。如果不去设置随机种子每次跑脚本生成的训练验证划分都不同同一个图片一会儿在训练集一会儿在验证集模型评估指标会失真。另外CLASSES列表的顺序一旦定下后续所有代码和训练配置都要沿用同一个顺序中途调整类别顺序等于让模型重新学一遍标注。转换完成后检查一下每个txt文件是否都对应存在图片文件以及类别id有没有落在CLASSES范围内。货架小目标多还建议统计一下标注框的像素面积分布把大量小于32×32像素的框存在的问题前置暴露出来否则训练时采样到这些小框损失函数里全是噪声。4. 训练与调参YOLOv11的小目标优化和评估口径4.1 写训练配置一份数据yaml和一条完整的训练命令训练前先准备好数据集描述文件ultralytics靠它找到图片和标签。这个yaml里的路径建议都用相对路径这样换机器跑训练时不用改配置。# shelf.yaml path: ./dataset train: images/train val: images/val # 类别顺序必须和转换脚本里的 CLASSES 完全一致 names: 0: cola_330ml 1: cola_500ml 2: water_550ml 3: juice_orange训练命令里需要调的参数比很多人想的多给一份实测可用的起点配置yolo detect train \ datashelf.yaml \ modelyolo11m.pt \ epochs100 \ imgsz1024 \ batch16 \ patience15 \ optimizerSGD \ lr00.01 \ weight_decay0.0005 \ mosaic1.0 \ close_mosaic10 \ seed42这条命令里有几个参数值得单独说明。imgsz1024是货架小目标场景的关键调整默认640会让高层货架上的商品缩成几个像素基本学不到特征但显存有限时不要盲目上调后面会讲替代方案。close_mosaic10表示最后10个epoch关闭mosaic增强因为mosaic拼出来的图虽然有助模型学鲁棒性但和真实货架排列差距太大最后阶段关掉能让模型回归真实分布。optimizerSGD配合weight_decay0.0005在中小规模数据集上比Adam的最终精度高一到两个点。4.2 小目标优化的两条路放大输入尺寸与切图训练yolov11小目标优化在零售场景里绕不开。imgsz调到1024后如果显存爆掉优先考虑切图训练而不是换小模型。切图就是把大分辨率的货架图切成带重叠的块每块独立做训练样本推理时再对同一张图切块预测然后合并结果。from PIL import Image from pathlib import Path def crop_train_patches(src_dir, out_dir, patch_size(640, 640), overlap0.3): 把大图切成带重叠的训练块重叠区域用于缓解边缘目标被切断的问题 files [f for f in Path(src_dir).iterdir() if f.suffix in (.jpg, .png)] Path(out_dir).mkdir(parentsTrue, exist_okTrue) for img_path in files: img Image.open(img_path) w, h img.size step_x int(patch_size[0] * (1 - overlap)) # 重叠30%时步长只有70% step_y int(patch_size[1] * (1 - overlap)) idx 0 for y in range(0, h - patch_size[1] 1, step_y): for x in range(0, w - patch_size[0] 1, step_x): crop img.crop((x, y, x patch_size[0], y patch_size[1])) crop.save(Path(out_dir, f{img_path.stem}_{idx}.jpg)) # 对应的标注框坐标要同步减去 (x, y) 偏移并裁掉超出patch范围的部分 idx 1这段切图脚本里overlap0.3是重点。如果不设重叠一个商品恰好落在切图边界上时会被切成两半训练集里就会出现大量残缺目标。30%的重叠能保证即使目标在边缘也至少有一张图中它保持完整。切图之后训练用的imgsz可以回到640或768因为目标在图片中的相对尺寸已经被放大了显存压力也小很多。代价是训练样本数量成倍增加训练时间变长。还有一个配合手段是开启自动混合精度训练ultralytics默认就在用但如果你自己改过配置确认ampTrue是开着的显存能省将近一半。4.3 验证与评估口径货架场景主要看mAP50和混淆矩阵训练结束后用验证集跑一次评估别只盯着训练日志里的loss曲线。loss降得再漂亮验证集mAP上不去都是白搭。评估代码和参数如下from ultralytics import YOLO # 加载训练过程里自动保存的最优权重 model YOLO(runs/detect/train/weights/best.pt) # 验证时 imgsz 要和训练保持一致conf 设低一点避免漏评估 metrics model.val(datashelf.yaml, imgsz1024, conf0.1) print(mAP50:, metrics.box.map50) print(mAP50-95:, metrics.box.map) print(Precision:, metrics.box.p) print(Recall:, metrics.box.r) # 每个类别的AP单独打印找出拖后腿的类 for i, ap in enumerate(metrics.box.ap): print(fclass {i}: {ap:.3f})零售货架这个场景我一般主要看mAP50因为库存统计关心的是“这一瓶水有没有被认出来”IoU卡在0.5已经足够。mAP50-95在密集排列场景下会偏低因为它对定位精度要求更高货架上商品彼此挨着框稍微偏一点就掉分但业务上并不需要那么精细的框。从metrics.box.p和metrics.box.r能看到每个类别的查准率和查全率如果某个类别的recall特别低说明模型漏检这个商品优先级高于处理误检。评估完成后生成混淆矩阵图重点看对角线之外哪些格子数字大。两个类别互相误判就要回到第3章说的类别合并策略而不是继续加训练数据硬扛。5. YOLOv11部署与库存统计的踩坑清单五个高频问题5.1 训练正常但实拍漏检频出训练集和实拍分布差太远现象验证集mAP50有0.85模型一接到门店实拍画面同一层货架上的商品漏掉三分之一。原因训练图大多是在整理干净的货架前拍的而实拍画面里存在补货员遮挡、商品倒放、灯光色温变化模型学到的其实是“整齐摆放的货架”这个整体特征而不是商品本身。解决从实拍视频里按场景抽帧把逆光、夜间、补货中途、商品被翻乱这四类画面各抽一部分按不低于训练集总数30%的比例混入。混入后重新训练mAP50可能会先掉两三个点但实拍召回率会明显上升。如果实拍画面和训练画面差距实在太大考虑在实拍机位重新采集一套数据做增量微调。5.2 两个SKU互相误判类别定义细过头了现象cola_330ml和cola_500ml的Recall都在0.8以上但混淆矩阵显示两者互相认错的框数量占各自总框数的四成库存统计时330ml和500ml的数量总是此消彼长。原因这两个SKU的罐身和包装设计几乎一样区别只是罐体高度和包装上的规格字样。在摄像头倾斜视角下高度差被压缩模型拿到的特征就是“同一个东西”。解决把这两个SKU合并成一个类别cola_can先把识别问题变成“这一格有没有可乐”这个二分类问题。规格区分留给两个方案一是加一个数字OCR模型识别罐底或包装上的规格文字二是利用货架格的固定位置先验同一个货架格正常情况下只会放一种规格用位置信息去推断规格。不要指望单纯加数据能把几乎一样的两个SKU彻底分开。5.3 训练到一半OOM中断不是换小模型是先降imgsz现象imgsz设成1280batch设成16训练到第5个epoch直接报CUDA out of memory进程被杀。原因1280的输入尺寸让特征图显存占用是640的四倍左右batch16直接吃爆。换n模型虽然能缓解但精度掉得厉害。解决优先把batch降到4配合梯度累积达到等效batch16的效果。具体是在训练命令里加batch4和nbs64ultralytics会根据nbs自动累积梯度。如果batch降到4还爆就把imgsz降到960并开启切图训练方案。记住查看显存占用别靠猜训练时另开一个终端跑nvidia-smi -l 1实时盯着。5.4 Jetson Nano上推理速度太慢导出TensorRT引擎要在目标设备上做现象Jetson Nano部署yolov11模型用best.pt直接推理一张图要两三秒并发几路摄像头完全跑不动。原因pt权重走的是PyTorch运行时在Jetson这种低功耗设备上既慢又费内存。换TensorRT引擎能快数倍但很多团队习惯在PC上导出引擎再拷贝到Jetson结果设备不识别或者精度莫名下降。# 导出engine格式注意 halfTrue 用FP16加速Jetson Nano不支持就去掉 # 必须在目标设备上执行导出TensorRT版本和GPU架构会写进引擎 yolo export modelbest.pt formatengine imgsz1024 halfTrue device0导出engine时注意device0要指向Jetson的GPU不要用CPU导出。halfTrue开启FP16速度提升明显但如果发现导出后检测结果出现大量空框就改成halfFalse再用FP32。另外Jetson上用的torch和tensorrt版本要和ultralytics要求的一致否则导出会直接报版本不匹配。推理时加载engine文件后续调用方式和pt一样但内存占用和延迟都好看得多。5.5 推理结果保存不全save_txt才是库存统计的数据源现象用yolo predict modelbest.pt sourcexxx/ saveTrue跑完后runs/detect/predict目录里只有带框的图片找不到任何坐标或标签文件后续统计库存拿不到数据。原因saveTrue只输出可视化图片YOLO的标签文件要靠save_txtTrue单独开。这两者是独立的开关不开save_txt就没有结构化的检测结果。解决保存完整结果的命令如下这样一张图能同时拿到可视化和标签数据。yolo predict \ modelbest.pt \ sourcestore_cam_01.mp4 \ saveTrue \ save_txtTrue \ save_confTrue \ conf0.25 \ imgsz1024save_confTrue让txt文件里每行多一个置信度数值库存统计阶段可以依赖它过滤低质量检测框。生成的标签文件在runs/detect/predict/labels/下按视频帧或图片名命名。做库存动态管理时直接读这些txt文件解析即可不需要再对图片调用模型一遍。6. 把识别结果变成库存动作置信度阈值、滑窗投票与补货触发6.1 从检测框到库存量先定ROI再计数拿到一帧的检测结果后别急着统计数量。货架上每个陈列格是一个固定位置把图像里每个格子的区域预先标成ROI然后判断检测框的中心点落在哪个ROI内。同一个ROI内同一类别的框数量就是这一格该商品的可见数量。ROI要画得比货格略小一圈避免相邻货格的商品框中心漂移过来造成串数。置信度阈值要单独设。库存统计场景下阈值太低会混入大量错框阈值太高又漏检。我一般先用验证集算出每个类别的置信度分布选择能让Precision和Recall交叉的那个值作为起点再在实拍画面里抽查200个框做人工确认。宁可阈值偏高让系统偶尔漏报也不要阈值过低导致同一格出现三四个重叠框数量虚高。6.2 防漏检误判的滑窗投票单帧检测结果不能直接用货架识别最大的坑是单帧漏检不可避免直接拿单帧结果判断“缺货”会把补货系统带偏。常见做法是维护一个滑动窗口在最近30帧里统计每个ROI的检测结果只有当某个ROI连续多帧没有检测到目标时才判定为缺货。from collections import deque # 每个SKU在对应ROI内的最近检测记录 window_len 30 # 滑窗长度约1分钟假设每2秒处理一帧 missing_ratio_th 0.8 # 缺失比例阈值超过80%才算缺货 history {sku_id: deque(maxlenwindow_len) for sku_id in sku_ids} def update_stock(frame_result): frame_result 是单帧检测结果格式为 [(sku_id, conf, x, y)] for sku_id in sku_ids: present any(d[0] sku_id for d in frame_result) history[sku_id].append(1 if present else 0) if len(history[sku_id]) window_len: missing_ratio 1.0 - sum(history[sku_id]) / window_len if missing_ratio missing_ratio_th: if stock_status[sku_id] ! out_of_stock: stock_status[sku_id] out_of_stock push_replenishment_event(sku_id)滑动窗口里两个参数要联动调。window_len拉长能过滤掉瞬间遮挡和单帧漏检但缺货响应会变慢missing_ratio_th调高能减少误报但真正缺货时触发也变慢。我实际用过比较顺手的组合是30帧窗口配0.8缺失率也就是连续24帧以上看不到目标才触发补货。画面里有顾客伸手拿货导致短暂遮挡的帧基本不会触发误报。6.3 动态更新模型的节奏用负样本回流做增量微调模型上线后不会一劳永逸。货架上的商品包装改版、新品上架、灯光老化都会让精度缓慢下降。常见做法是每天从实拍画面里抽一段样本和人工修正后的检测结果一起存下来每周做一次增量微调。微调时用原训练集的子集加上新采集的样本学习率设为初始训练的十分之一跑30个epoch左右就够。这里最要紧的教训是动态管理库存本质是在管理“模型不确定性”的释放节奏单帧检测永远有误差系统设计必须把这层误差在判断逻辑里接住。我最早一版直接把单帧结果当库存数上线当天补货通知被漏检刷爆。希望帮到你。本文还有配套的精品资源点击获取
返回列表