首页
产品服务
模型广场
Token工厂
算力市场算力商情行业资讯
注册

YOLOv11 在零售领域实战:利用公开的商品检测数据集 (如 SKU110K 的子集),训练一个 YOLOv11 模型,用于识别货架上的各种商品

发布日期:2026-04-11 来源:CSDN软件开发网作者:CSDN软件开发网浏览:2

一、零售智能化的敲门砖:为什么选择YOLOv11做商品检测

1.1 零售领域的痛点与技术选型的必然

  咱们做技术的,最怕的就是老板一句话:“咱们能不能搞个无人超市?”或者“能不能自动盘点一下库存?”这时候,你要是还在用传统图像处理那一套,比如OpenCV的边缘检测、模板匹配,那大概率是要熬夜到头秃的。传统的零售商品检测,难点太多了。货架上的商品五花八门,包装袋稍微有点褶皱,或者光线暗一点,传统的算法就歇菜了。更别提商品被遮挡、堆叠这些情况了。

  这时候,深度学习目标检测就派上用场了。而在目标检测领域,YOLO(You Only Look Once)系列那可是如雷贯耳。为什么选YOLOv11?很简单,它“快、准、狠”。对于零售场景来说,实时性要求高,超市里的监控视频流得实时处理,你总不能让顾客拿了个商品,系统反应三秒钟才识别出来吧?YOLOv11继承了家族的优良传统,速度极快,同时精度还在不断提升。对于我们程序员来说,工程落地是王道,YOLOv11的部署生态非常成熟,不管是TensorRT加速还是OpenVINO推理,都有现成的工具链,这就省了我们不少事儿。

1.2 揭开SKU110K数据集的面纱

  巧妇难为无米之炊,搞模型训练,数据集是核心。咱们这次用的SKU110K数据集,在零售检测圈子里那是相当有名气。它不是那种干干净净、只有几个商品的数据集,它是真的“硬核”。

  SKU110K主要拍摄的是超市货架的密集场景。咱们得先搞清楚这个数据集的特点,才能对症下药。

特性维度 具体描述 对我们训练的影响
密集程度 极高。图片中全是密密麻麻的商品,一张图甚至有上百个目标。 检测头的设计要跟上,小目标检测能力要强,NMS(非极大值抑制)阈值得调,不然框全被滤掉了。
标注格式 通常提供的是XML文件(VOC格式)或者TXT文件,包含边界框坐标。 需要编写脚本将其转换为YOLO格式,这是个细致活,稍有不慎坐标就对不上。
场景多样性 包含不同的货架角度、光照条件、拍摄距离。 增强了模型的泛化能力,但也要求我们在数据增强时不能太激进,要模拟真实场景。
类别定义 实际上,SKU110K主要关注的是“商品”这一大类的检测,也就是on-shelf detection,细分类别不是重点。 我们训练时可以将其视为单类检测任务,降低分类难度,专注于定位精度。

  咱们这次的任务,就是利用这个数据集的一个子集,把YOLOv11模型训练出来,让它能在这个密集的“货架丛林”里,精准地把每一个商品框出来。

1.3 从零搭建你的实验环境

  在开始撸代码之前,咱们得先把“厨房”布置好。环境配置这事儿,虽然枯燥,但要是没弄好,后面全是坑。

  首先,你的机器得有一张NVIDIA的显卡,显存最好是8G以上,毕竟SKU110K的图片分辨率不小,而且目标数量多,显存小了容易爆OOM(Out of Memory)。

  操作系统推荐Ubuntu 20.04或者22.04,当然Windows 10/11也能跑,但Linux在训练稳定性上还是略胜一筹。

  接下来是环境创建,咱们用Conda来管理,这可是Python程序员的福音。

# 1. 创建一个新的虚拟环境,Python版本推荐3.10,比较稳健
conda create -n yolo_retail python=3.10 -y

# 2. 激活环境
conda activate yolo_retail

# 3. 安装PyTorch,这是重中之重
# 注意,这里要去PyTorch官网找对应的命令,一定要看准CUDA版本
# 比如你的显卡驱动支持CUDA 12.1,那就安装对应版本
# 这里我演示一个通用的安装命令,具体根据你的硬件情况调整
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121

# 4. 安装YOLOv11所在的ultralytics库
# 这个库封装得非常好,一行命令就能搞定
pip install ultralytics

# 5. 验证安装是否成功
python -c "from ultralytics import YOLO; print('YOLOv11 环境配置成功!')"

  这个过程看似简单,但有几个雷区:

  1. CUDA版本匹配:很多人的显卡驱动很旧,却想装最新的PyTorch,结果报错。一定要确保你的驱动版本支持你所选的CUDA版本。
  2. 依赖冲突:不要在这个环境里乱装其他无关的包,特别是会影响系统库的包,保持环境纯净。

  装好了环境,咱们就可以开始下一步了:把那堆乱七八糟的数据,变成模型能“吃”进去的营养餐。

二、数据炼金术:SKU110K数据集的深度清洗与格式转换

2.1 原始数据的真相与挑战

  拿到SKU110K数据集的第一刻,千万别急着扔进去训练。你得先解压看看里面是什么德行。通常,下载下来的数据集包含三个文件夹:images(图片)、annotations(标注文件)、lists(划分列表)。

  SKU110K的标注文件通常是XML格式的,这是Pascal VOC标准。里面记录了每个商品的坐标 $(x_{min}, y_{min}, x_{max}, y_{max})$。但是!YOLOv11模型,它“吃”的是TXT格式,也就是每张图片对应一个TXT文件,里面每一行代表一个目标,格式是: $class\_id \quad x_{center} \quad y_{center} \quad width \quad height$ 注意,这里的坐标都是归一化之后的相对坐标(0到1之间)。

  这就给我们提出了第一个挑战:格式大迁徙。我们需要把XML里的绝对坐标,转换成YOLO需要的归一化中心点坐标。

  咱们来看看这个转换的数学原理,虽然简单,但容不得半点马虎: 假设图片宽度为 $W$,高度为 $H$,VOC格式标注的框坐标为 $(x_{min}, y_{min}, x_{max}, y_{max})$。

  1. 计算框的宽高: $w_{box} = x_{max} - x_{min}$ $h_{box} = y_{max} - y_{min}$
  2. 计算框的中心点: $x_{center} = x_{min} + \frac{w_{box}}{2}$ $y_{center} = y_{min} + \frac{h_{box}}{2}$
  3. 归一化处理(这一步最关键): $x_{norm} = \frac{x_{center}}{W}$ $y_{norm} = \frac{y_{center}}{H}$ $w_{norm} = \frac{w_{box}}{W}$ $h_{norm} = \frac{h_{box}}{H}$

  这就是数据预处理的核心逻辑。如果不做归一化,模型训练出来的框会飞到九霄云外去。

2.2 编写工业级的数据转换脚本

  手动改几个文件还行,SKU110K动辄几千张图,必须写脚本自动化处理。下面这个脚本,我会写得尽量详细,加上各种容错机制,确保大家可以直接拿去用。

import os
import glob
import xml.etree.ElementTree as ET
from PIL import Image
from tqdm import tqdm  # 这是一个进度条库,没装的pip install tqdm

# 定义输入输出路径
# 假设你的数据集解压在 './SKU110K' 文件夹下
SOURCE_IMAGES_DIR = './SKU110K/images'
SOURCE_ANNOTATIONS_DIR = './SKU110K/annotations'
OUTPUT_LABELS_DIR = './SKU110K/labels'  # 转换后的YOLO格式标签存放位置

# 确保输出目录存在
os.makedirs(OUTPUT_LABELS_DIR, exist_ok=True)

# 定义类别映射
# SKU110K其实主要检测“商品”,我们可以把它定义为类别0
# 如果数据集有细分类别,这里需要扩展字典,但对于基础商品检测,我们就统一为 'goods'
CLASS_DICT = {'goods': 0}

def convert_box(size, box):
    """
    核心转换函数:将VOC格式的坐标转换为YOLO格式的归一化坐标
    :param size: 图片尺寸
    :param box: voc格式的坐标
    :return: yolo格式的坐标
    """
    dw = 1. / size[0]  # 宽度的倒数,用于归一化
    dh = 1. / size[1]  # 高度的倒数
    # 计算中心点和宽高
    x_center = (box[0] + box[2]) / 2.0
    y_center = (box[1] + box[3]) / 2.0
    width = box[2] - box[0]
    height = box[3] - box[1]
    # 归一化
    x = x_center * dw
    y = y_center * dh
    w = width * dw
    h = height * dh
    return (x, y, w, h)

def convert_annotation(image_id):
    """
    解析单个XML文件并生成对应的TXT标签文件
    """
    # 构建XML文件路径,这里假设文件名和图片ID对应
    # SKU110K的文件命名可能比较特殊,比如 'train_0.jpg' 对应 'train_0.xml'
    xml_file = os.path.join(SOURCE_ANNOTATIONS_DIR, f"{image_id}.xml")
    
    # 如果对应的xml文件不存在,就跳过(处理测试集或无标注图片)
    if not os.path.exists(xml_file):
        # 有些数据集可能xml文件名不带后缀或者有细微差别,这里可以加容错逻辑
        # 比如尝试查找类似 'train_0_anno.xml' 之类的,SKU110K通常是直接对应的
        return

    # 解析XML
    try:
        tree = ET.parse(xml_file)
        root = tree.getroot()
    except ET.ParseError as e:
        print(f"Warning: 解析文件 {xml_file} 出错: {e}")
        return

    # 获取图片尺寸,这一步至关重要!
    size = root.find('size')
    if size is None:
        # 有些数据集的XML里没有size,那就得用PIL打开图片看尺寸
        img_path = glob.glob(os.path.join(SOURCE_IMAGES_DIR, f"{image_id}.*"))[0]
        im = Image.open(img_path)
        w, h = im.size
    else:
        w = int(size.find('width').text)
        h = int(size.find('height').text)

    # 打开输出的txt文件准备写入
    out_file_path = os.path.join(OUTPUT_LABELS_DIR, f"{image_id}.txt")
    with open(out_file_path, 'w') as out_file:
        # 遍历所有目标对象
        for obj in root.iter('object'):
            # 获取类别名称,SKU110K里通常叫 'object' 或者具体的商品名
            # 这里做兼容处理
            cls_name = obj.find('name').text
            if cls_name not in CLASS_DICT:
                # 如果遇到了未定义的类别,可以动态添加或者跳过
                # 这里为了演示,我们将所有未知类别都归为 'goods' 类
                cls_name = 'goods'
            
            cls_id = CLASS_DICT[cls_name]
            
            # 获取边界框坐标
            xmlbox = obj.find('bndbox')
            # VOC格式: xmin, ymin, xmax, ymax
            b = (float(xmlbox.find('xmin').text), 
                 float(xmlbox.find('ymin').text), 
                 float(xmlbox.find('xmax').text), 
                 float(xmlbox.find('ymax').text))
            
            # 调用转换函数
            bb = convert_box((w, h), b)
            
            # 写入文件,保留6位小数
            out_file.write(f"{cls_id} {bb[0]:.6f} {bb[1]:.6f} {bb[2]:.6f} {bb[3]:.6f}\n")

# 主处理循环
def main():
    print("开始数据格式转换...")
    # 获取所有图片文件名(不带后缀)
    # 这里要注意,SKU110K可能包含train, test, val子文件夹
    # 我们需要递归遍历或者分别处理
    image_files = []
    # 假设我们处理的是训练集,图片都在 train 子目录下
    # 实际操作时请根据解压后的目录结构调整 glob 模式
    search_pattern = os.path.join(SOURCE_IMAGES_DIR, '**', '*.jpg')
    image_files.extend(glob.glob(search_pattern, recursive=True))
    
    print(f"共找到 {len(image_files)} 张图片,开始处理对应的标注文件...")
    
    for img_path in tqdm(image_files):
        # 提取文件名(不含后缀)
        image_id = os.path.splitext(os.path.basename(img_path))[0]
        convert_annotation(image_id)
        
    print(f"转换完成!标签文件已保存至: {OUTPUT_LABELS_DIR}")

if __name__ == '__main__':
    main()

  这段代码虽然长,但逻辑非常清晰:找文件 -> 解析XML -> 算坐标 -> 写TXT。其中加了几个关键的容错:

  1. XML解析错误的捕获。
  2. XML中没有尺寸信息时的PIL读图备用方案。
  3. 类别名称的兼容处理。

  跑完这个脚本,你的文件夹里就会多出来一堆TXT文件,这就是YOLOv11最喜欢吃的“食物”了。

2.3 数据集的“户籍管理”:YAML配置文件详解

  数据转换好了,还得告诉模型这些数据都在哪,有哪些类别。这时候就需要编写一个YAML配置文件。这个文件虽然短,但是连接数据和模型的桥梁。

  我们在项目根目录下创建一个 retail_data.yaml

# retail_data.yaml

# 数据集所在的根目录路径
# 这里建议使用绝对路径,避免因为运行路径不同导致找不到文件
path: /home/user/project/SKU110K  # 替换成你自己的实际路径

# 相对于path的子目录路径
train: images/train  # 训练集图片路径,实际是 path/images/train
val: images/val      # 验证集图片路径

# 类别数量
nc: 1

# 类别名称
# 这里我们只有一个类别 'goods'
names:
  0: goods

  这个文件里有几个坑要注意:

  1. 路径问题:80%的训练报错都是路径问题。path一定要写对,或者直接在训练代码里指定绝对路径。
  2. 数据划分:刚才我们的脚本只是转换了格式,还没划分训练集和验证集。YOLOv11支持自动划分,但为了稳妥,建议手动划分。

  下面是一个简单的脚本,帮你把图片和标签按照9:1的比例划分到 trainval 文件夹里:

import os
import shutil
import random
from tqdm import tqdm

# 原始数据路径
IMAGES_SRC = './SKU110K/images_all' # 假设所有图片都在这
LABELS_SRC = './SKU110K/labels'     # 上一步生成的标签

# 目标路径
BASE_DIR = './SKU110K'
TRAIN_IMG = os.path.join(BASE_DIR, 'images', 'train')
VAL_IMG = os.path.join(BASE_DIR, 'images', 'val')
TRAIN_LBL = os.path.join(BASE_DIR, 'labels', 'train')
VAL_LBL = os.path.join(BASE_DIR, 'labels', 'val')

# 创建目录
for p in [TRAIN_IMG, VAL_IMG, TRAIN_LBL, VAL_LBL]:
    os.makedirs(p, exist_ok=True)

# 获取所有图片并打乱
all_images = [f for f in os.listdir(IMAGES_SRC) if f.endswith('.jpg')]
random.shuffle(all_images)

# 划分比例
split_ratio = 0.9
split_idx = int(len(all_images) * split_ratio)
train_images = all_images[:split_idx]
val_images = all_images[split_idx:]

def move_files(file_list, img_dst, lbl_dst):
    for f in tqdm(file_list):
        # 移动图片
        src_img = os.path.join(IMAGES_SRC, f)
        dst_img = os.path.join(img_dst, f)
        shutil.move(src_img, dst_img) # 或者用 shutil.copy
        
        # 移动标签
        lbl_name = f.replace('.jpg', '.txt')
        src_lbl = os.path.join(LABELS_SRC, lbl_name)
        if os.path.exists(src_lbl):
            dst_lbl = os.path.join(lbl_dst, lbl_name)
            shutil.move(src_lbl, dst_lbl)

print("正在划分训练集...")
move_files(train_images, TRAIN_IMG, TRAIN_LBL)

print("正在划分验证集...")
move_files(val_images, VAL_IMG, VAL_LBL)

print("数据集划分完成!")

  这一步做完,你的目录结构就是标准的YOLO格式了:

SKU110K/
├── images/
│   ├── train/
│   └── val/
├── labels/
│   ├── train/
│   └── val/
└── retail_data.yaml

  至此,数据准备工作大功告成,咱们可以开始真正的模型训练了。

三、模型训练:从参数配置到实战演练

3.1 理解YOLOv11的训练参数玄学

  训练模型,某种程度上来说,是一门“炼丹”的艺术。参数调得好,效果翻倍;调不好,模型不收敛或者过拟合。YOLOv11虽然封装得很好,提供了很多默认参数,但针对零售货架这种密集场景,咱们还是得微调一下。

  我们主要关注以下几个核心参数:

参数名 默认值 通俗解释 零售场景调优建议
imgsz 640 输入图片的尺寸,模型会把图片缩放到这个大小。 SKU110K图片很大,且小目标多。建议设为 1280 甚至更高,宁可训练慢点,也要保证小商品不被漏检。
batch 16 一次塞给显卡多少张图。 取决于你的显存。如果是 imgsz=1280,显存不够的话,batch_size 可能得降到 48
epochs 100 把整个数据集学习多少遍。 密集场景难训练,建议 200 往上走,让模型彻底“看透”货架。
lr0 0.01 初始学习率,相当于步长。步子太大容易扯着蛋(发散),步子太小走得慢。 默认值一般可以,配合Cosine学习率衰减策略,效果不错。
mosaic True 数据增强神器,把四张图拼成一张。 重点! 对于货架检测,Mosaic非常有效,能模拟密集堆叠效果,千万别关。
overlap_mask True 处理重叠区域的掩码。 虽然我们做的是检测,但这个参数涉及到底层对重叠框的处理逻辑,保持默认即可。

  还有一个特别重要的参数是 degreesscale。货架上的商品摆放角度其实挺随机的,所以适当增加旋转增强(比如 degrees=45)是很有必要的。

3.2 编写训练启动脚本

  我们不推荐在命令行里直接敲一长串命令,那样不好维护,也不好复现。咱们写一个Python脚本来启动训练,这样逻辑更清晰。

from ultralytics import YOLO
import torch

def main():
    # 1. 检查显卡是否可用,这步不能少
    if not torch.cuda.is_available():
        print("警告:未检测到GPU,将使用CPU训练,速度会非常慢!")
        device = 'cpu'
    else:
        device = 0 # 指定使用第0号显卡
        print(f"检测到GPU: {torch.cuda.get_device_name(0)}")

    # 2. 加载模型
    # 这里我们有两种选择:
    # a) 从头开始训练:model = YOLO('yolo11n.yaml')
    # b) 加载预训练权重进行微调:model = YOLO('yolo11n.pt')
    # 强烈建议选择,因为COCO数据集已经学到了很多通用特征,微调收敛快,效果好。
    # 'n' 代表nano版本,速度快;如果是服务器端部署,可以用 's' (small) 或 'm' (medium)
    model = YOLO('yolo11n.pt') 

    # 3. 开始训练
    # 这里的参数就是我们刚才分析过的
    results = model.train(
        data='./retail_data.yaml',  # 指定刚才写的配置文件
        imgsz=1280,                 # 提高分辨率,为了看清小商品
        epochs=200,                 # 训练轮数
        batch=8,                    # batch size,根据显存调整
        name='yolo11_retail_exp',   # 实验名称,结果会保存在 runs/detect/yolo11_retail_exp
        device=device,              # 指定设备
        patience=50,                # 早停耐心,如果50轮没提升就停
        save=True,                  # 保存检查点
        save_period=10,             # 每10轮保存一次,防止断电白练
        project='runs/train',       # 项目保存路径
        
        # 下面是一些增强参数的微调,针对零售场景
        degrees=15.0,               # 随机旋转角度,货架可能有倾斜
        translate=0.1,              # 平移
        scale=0.5,                  # 缩放比例,模拟远近不同的商品
        shear=0.0,                  # 剪切变换
        perspective=0.0,            # 透视变换
        flipud=0.0,                 # 上下翻转概率,商品倒过来就离谱了,设为0
        fliplr=0.5,                 # 左右翻转概率,货架左右镜像还是合理的
        mosaic=1.0,                 # Mosaic概率,必须拉满
        mixup=0.1,                  # Mixup增强,混合图像,增加难度
    )

    # 4. 训练结束后,打印结果路径
    print(f"训练完成!最佳模型保存在: runs/train/yolo11_retail_exp/weights/best.pt")

if __name__ == '__main__':
    main()

代码功能深度剖析

  1. 模型加载YOLO('yolo11n.pt')。这一步看似简单,背后其实是在下载官方的权重文件。如果网不好,可能会卡住。建议提前下载好 .pt 文件放到本地。
  2. 早停机制patience=50。这是个很实用的功能。如果验证集的损失在50个epoch内一直没有下降,程序就会自动停止,防止过拟合,也帮你省钱。
  3. 数据增强组合拳
    • fliplr=0.5:商品左右翻转是合理的,毕竟货架左边看和右边看是对称的。
    • flipud=0.0:这个千万别开!你见过倒着放的洗发水瓶子吗?开了这个,模型会学傻的。
    • mosaic=1.0:这是YOLOv4以来的神技。把四张货架图拼在一起,能让模型在一个Batch里看到更多的商品上下文,对密集检测效果拔群。

  运行这个脚本,你会看到一个进度条开始滚动,显卡风扇开始狂转。这时候,你可以去喝杯咖啡,或者看看TensorBoard的曲线。

3.3 监控训练过程:读懂Loss曲线

  训练不是扔进去就不管了,咱们得盯着点。YOLOv11会自动生成 results.csv 和图表。

  我们主要看这几个指标:

  1. box_loss:边界框回归损失。这个值越小,说明框的位置越准。在密集场景下,这个loss下降可能会比较慢,因为稍微偏一点点就可能盖住旁边的商品。
  2. cls_loss:分类损失。因为我们只有一个类(goods),这个值应该降得很快,并且接近0。
  3. dfl_loss:Distribution Focal Loss,这是YOLO系列用来处理回归问题的核心损失函数,帮助模型更好地定位目标。

  如果发现训练集loss一直在降,验证集loss却开始上升,那就是过拟合了。这时候可以降低学习率,或者增加数据增强强度。

四、模型评估与优化:让结果更上一层楼

4.1 模型评估的核心指标:mAP与置信度阈值

  训练完了,那个 best.pt 文件就是咱们的“宝贝”。但在用它之前,得先考考试,看看它到底考了多少分。

  在目标检测里,最核心的指标是 mAP@0.5mAP@0.5:0.95

  • mAP@0.5:当预测框和真实框的重叠度大于0.5时,就算预测正确。这是一个比较宽松的标准,及格线。对于货架检测,这个值通常应该很高,比如90%以上。
  • mAP@0.5:0.95:这是一个严苛的标准。它要求IoU从0.5开始,每隔0.05计算一次AP,一直算到0.95,然后取平均。这考验的是模型能不能把框画得“严丝合缝”。对于零售来说,这个指标很重要,因为框画得准,才能准确判断商品是不是被拿走了。

  还有一个关键概念是 置信度阈值。模型预测出来的框,都会带一个概率值,表示“我有多确定这是个商品”。如果阈值设得太高,比如0.9,那很多模糊的商品就被过滤掉了(召回率低);设得太低,比如0.1,那货架缝隙都会被当成商品(精确率低)。

4.2 动手进行模型验证

  我们写个脚本,用验证集来测试模型,并绘制出那些漂亮的曲线图。

from ultralytics import YOLO

def evaluate_model():
    # 1. 加载训练好的最佳模型
    model = YOLO('./runs/train/yolo11_retail_exp/weights/best.pt')

    # 2. 在验证集上运行评估
    # verbose=True 可以打印详细信息
    metrics = model.val(
        data='./retail_data.yaml',
        imgsz=1280,
        conf=0.5,       # 置信度阈值设为0.5,这是个常用的基准
        iou=0.6,        # NMS的IoU阈值
        device=0,
        plots=True,     # 自动绘制PR曲线、混淆矩阵等
        save_json=True  # 保存结果为JSON,方便后续分析
    )

    # 3. 打印核心指标
    print("\n" + "="*30)
    print("模型评估结果汇总")
    print("="*30)
    print(f"mAP@0.5: {metrics.box.map50:.4f}")
    print(f"mAP@0.5:0.95: {metrics.box.map:.4f}")
    print(f"Precision (精确率): {metrics.box.mp:.4f}")
    print(f"Recall (召回率): {metrics.box.mr:.4f}")
    
    # 4. 混淆矩阵分析
    # 结果文件夹里会有一个 confusion_matrix.png
    # 我们要看的是:False Negative (漏检) 和 False Positive (误检)
    # 对于零售,漏检(把商品当背景)比误检(把背景当商品)通常更严重
    # 因为你不知道货少了,库存管理就乱了。

if __name__ == '__main__':
    evaluate_model()

  这段代码运行后,会生成 confusion_matrix.pngPR_curve.png

  • PR曲线:越往右上角凸越好。如果曲线像个大鼓包,说明模型强劲。
  • 混淆矩阵:对角线越亮越好。看看“background -> goods”这一格(误检)和“goods -> background”这一格(漏检)的数值。

4.3 针对密集场景的NMS优化

  YOLOv11默认使用的NMS(非极大值抑制)算法,是用来去除重复框的。但是在货架上,两个商品挨得特别近,NMS很容易误以为是一个目标,把其中一个框给删了。

  这就需要我们调整 IoU阈值 或者使用 Soft-NMS

  YOLOv11的代码库里其实已经集成了高级的NMS策略,我们可以通过参数来控制。如果发现很多紧挨着的商品被漏检了,我们可以手动改一下推理代码里的 iou 参数,默认是0.7,我们可以降到0.5或0.4,让NMS更“宽容”一些。

  或者,我们可以在导出模型时,使用更适合密集场景的设置。但最直接的方法,是在推理时进行微调。

def predict_with_custom_nms():
    model = YOLO('./runs/train/yolo11_retail_exp/weights/best.pt')
    
    # 运行预测
    results = model.predict(
        source='./SKU110K/images/val/test_image.jpg', # 单张图片测试
        imgsz=1280,
        conf=0.25,    # 置信度稍微设低点,宁可错杀一千,不可放过一个
        iou=0.4,      # 这里调低IoU阈值,保留更多重叠框
        max_det=300,  # 最大检测数量,零售场景可能目标很多,默认100可能不够,得开大
        device=0
    )

    # 结果可视化
    for result in results:
        # result.plot() 会把框画在图上并返回BGR格式的数组
        annotated_frame = result.plot()
        
        # 保存结果
        import cv2
        cv2.imwrite('./prediction_result.jpg', annotated_frame)
        print(f"检测到 {len(result.boxes)} 个商品,结果已保存。")
        
        # 打印具体的框坐标(用于后续的货架分析)
        boxes = result.boxes.xywhn.cpu().numpy() # 获取归一化后的中心点宽高
        print("部分商品坐标示例:", boxes[:5])

if __name__ == '__main__':
    predict_with_custom_nms()

代码分析

  • iou=0.4:这是个关键改动。默认值通常较高,容易删掉挨得近的框。调低后,只要两个框重叠度不是极其高,就都保留下来。
  • max_det=300:默认值通常是100或300。如果你拍的是整个货架通道,商品可能有几百个,这个参数必须得设大,否则后面的商品检测不出来。

五、实战应用:货架分析与库存管理的逻辑构建

5.1 从检测框到货架层级的映射

  仅仅把商品框出来,只是第一步。老板想要的是数据:哪个货架缺货了?哪种商品卖得好?

  这就需要我们对检测到的框进行二次分析。 我们就得把一堆乱七八糟的坐标,组织成有序的“货架层级”。

  这就涉及到一个几何变换问题。我们可以把货架看作一个网格。检测到的商品框的中心点 $(c_x, c_y)$,我们可以根据 $y$ 坐标(垂直位置)来对商品进行聚类。$y$ 坐标相近的商品,很可能在同一层货架上。

  我们可以用简单的 K-Means聚类 或者 分位数切分 来实现这个逻辑。

5.2 编写货架分析工具类

  这个工具类将读取模型的预测结果,然后输出每个货架层的商品密度。

import numpy as np
import cv2
from sklearn.cluster import KMeans
from ultralytics import YOLO

class ShelfAnalyzer:
    def __init__(self, model_path, num_shelf_layers=5):
        """
        初始化分析器
        :param model_path: 模型路径
        :param num_shelf_layers: 预估的货架层数,用于聚类
        """
        self.model = YOLO(model_path)
        self.num_layers = num_shelf_layers

    def analyze_image(self, image_path):
        # 1. 预测
        results = self.model.predict(source=image_path, conf=0.25, iou=0.4, verbose=False)
        if len(results) == 0:
            return None
        
        result = results[0]
        
        # 2. 获取所有框的中心点坐标
        # xyxy 格式是,我们取中心点
        boxes_xyxy = result.boxes.xyxy.cpu().numpy()
        if len(boxes_xyxy) == 0:
            return None
            
        centers = []
        for box in boxes_xyxy:
            x_center = (box[0] + box[2]) / 2
            y_center = (box[1] + box[3]) / 2
            centers.append([y_center, x_center]) # 注意:聚类主要看y,所以把y放前面
            
        centers = np.array(centers)
        
        # 3. 按Y坐标进行聚类,识别货架层
        # 这里我们假设有5层货架,用K-Means把它们分成5堆
        if len(centers) < self.num_layers:
            print("检测到的商品太少,无法分层")
            return None
            
        kmeans = KMeans(n_clusters=self.num_layers, random_state=0, n_init='auto').fit(centers[:, 0].reshape(-1, 1))
        labels = kmeans.labels_
        
        # 4. 统计每一层的商品数量
        layer_counts = {}
        layer_boxes = {}
        
        # 获取聚类中心并排序,确定哪是第一层,哪是最后一层
        cluster_centers = kmeans.cluster_centers_.flatten()
        # argsort返回的是排序后的索引,这就对应了层级的顺序
        sorted_layer_indices = np.argsort(cluster_centers)
        
        # 映射关系:聚类标签 -> 实际层级 (0, 1, 2...)
        label_to_level = {old_idx: new_idx for new_idx, old_idx in enumerate(sorted_layer_indices)}
        
        for i, label in enumerate(labels):
            level = label_to_level[label]
            if level not in layer_counts:
                layer_counts[level] = 0
                layer_boxes[level] = []
            layer_counts[level] += 1
            layer_boxes[level].append(boxes_xyxy[i])
            
        return layer_counts, layer_boxes

    def visualize_layers(self, image_path, layer_boxes):
        """
        在图上把不同层级的商品框画出来,不同层级不同颜色
        """
        img = cv2.imread(image_path)
        colors = [
            (255, 0, 0), (0, 255, 0), (0, 0, 255), 
            (255, 255, 0), (255, 0, 255)
        ] # 蓝、绿、红、青、洋红
        
        for level, boxes in layer_boxes.items():
            color = colors[level % len(colors)]
            for box in boxes:
                x1, y1, x2, y2 = map(int, box)
                cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)
                # 写上层级编号
                cv2.putText(img, f"L{level+1}", (x1, y1 - 5), 
                            cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
        
        return img

# 使用示例
if __name__ == '__main__':
    analyzer = ShelfAnalyzer('./runs/train/yolo11_retail_exp/weights/best.pt', num_shelf_layers=4)
    counts, boxes = analyzer.analyze_image('./SKU110K/images/val/test_image.jpg')
    
    print("货架层级分析结果:")
    for level in sorted(counts.keys()):
        print(f"第 {level+1} 层: 检测到 {counts[level]} 个商品")
        
    # 可视化保存
    vis_img = analyzer.visualize_layers('./SKU110K/images/val/test_image.jpg', boxes)
    cv2.imwrite('./shelf_analysis_visual.jpg', vis_img)

这段代码的精髓在于: 它把纯粹的视觉问题(检测框),转化为了结构化的数据问题(层级统计)。

  • K-Means 聚类算法根据 $y$ 轴坐标把商品分成了几堆。
  • np.argsort 确保了层级顺序是“从上到下”的,而不是随机的。
  • 最后输出的 layer_counts 就可以直接发给仓库管理员:“嘿,第二层货架只有3个商品了,赶紧补货!”

5.3 拓展应用:销售分析与客流热力图

  虽然我们的模型只检测“商品”,但结合一些简单的逻辑,我们就能做更多事。

  1. 缺货率计算: $\text{缺货率} = \frac{\text{理论陈列数量} - \text{实际检测数量}}{\text{理论陈列数量}}$ 我们可以设定一个阈值,比如缺货率超过 30%,系统自动报警。
  2. 陈列合规检查: 很多品牌商要求自己的商品必须放在“黄金陈列位”(比如视线平行的货架层)。我们可以检查检测到的商品框是否落在了规定的区域。

  例如,如果我们想检查某品牌是否在“黄金位置”,我们可以定义一个感兴趣区域(ROI),然后计算检测框与ROI的重叠率。

def check_compliance(image_path, roi_coords, model):
    """
    检查商品是否在指定的ROI区域内
    :param roi_coords: (x1, y1, x2, y2) 黄金陈列位的坐标
    """
    results = model.predict(image_path, conf=0.25, verbose=False)
    img = cv2.imread(image_path)
    
    # 画出ROI区域
    cv2.rectangle(img, (roi_coords[0], roi_coords[1]), 
                  (roi_coords[2], roi_coords[3]), (0, 255, 255), 3)
    
    # 这里的IoU计算逻辑稍微简化
    roi_area = (roi_coords[2] - roi_coords[0]) * (roi_coords[3] - roi_coords[1])
    
    compliance_score = 0
    
    for box in results[0].boxes.xyxy.cpu().numpy():
        # 计算检测框与ROI的重叠面积
        # ... (这里省略具体的IoU计算代码,逻辑是求两个矩形的交集)
        # 如果重叠面积很大,说明商品确实在黄金位置
        pass
        
    # 最终输出一个合规评分
    return img, compliance_score

六、模型部署与工程化落地

6.1 模型格式转换:从PyTorch到ONNX

  训练好的 .pt 文件是PyTorch格式的,在Python里用着爽,但要是想集成到App里,或者用C++写高性能后端,就得转成通用格式。ONNX (Open Neural Network Exchange) 就是模型界的“世界语”。

  YOLOv11提供了一个非常方便的导出接口,不需要我们再去写繁琐的转换脚本。

from ultralytics import YOLO

def export_to_onnx():
    # 加载模型
    model = YOLO('./runs/train/yolo11_retail_exp/weights/best.pt')
    
    # 导出为ONNX格式
    # opset=12 是版本号,通常越高支持的操作越多,但要考虑推理引擎的支持情况
    # simplify=True 会用onnx-simplifier工具优化模型图,去掉冗余算子
    success = model.export(
        format='onnx', 
        imgsz=1280, 
        opset=12, 
        simplify=True,
        device=0 # 在GPU上导出,速度更快
    )
    
    if success:
        print(f"模型已成功导出为 ONNX 格式:{success}")

if __name__ == '__main__':
    export_to_onnx()

  导出成功后,你会得到一个 best.onnx 文件。这个文件不仅体积更小,而且加载速度更快,是工业部署的首选。

6.2 TensorRT 加速:让模型飞起来

  如果你是在NVIDIA显卡上做服务端部署,那TensorRT是绕不开的神器。它可以把模型进一步优化,通过层融合、精度校准(FP16/INT8)等技术,让推理速度翻几倍。

  YOLOv11支持直接导出TensorRT引擎(.engine文件),但这需要你的环境里安装了TensorRT库。这个过程比较复杂,咱们简要提一下操作步骤:

# 需要先安装 tensorrt 库
# 导出脚本
def export_to_tensorrt():
    model = YOLO('./runs/train/yolo11_retail_exp/weights/best.pt')
    
    # 导出为TensorRT格式,自动执行FP16量化
    success = model.export(
        format='engine', 
        imgsz=1280, 
        half=True, # 开启FP16,速度提升巨大,精度损失极小
        device=0
    )
    print(f"TensorRT引擎导出成功: {success}")

  在实际工程中,使用TensorRT引擎进行推理,处理一帧货架图片的时间可以从几十毫秒缩短到几毫秒,这对于实时视频流分析至关重要。

6.3 编写一个简单的Flask API服务

  为了让前端或者其他系统能调用我们的模型,咱们得写一个简单的Web API。这里用Flask演示,它轻量级,非常适合做这种微服务。

from flask import Flask, request, jsonify
import cv2
import numpy as np
from ultralytics import YOLO
import base64

app = Flask(__name__)

# 全局加载模型,避免每次请求都加载,浪费时间
# 实际生产中,这里加载的是 ONNX 或者 TensorRT 模型以获得更高性能
MODEL = YOLO('./runs/train/yolo11_retail_exp/weights/best.pt')

@app.route('/detect', methods=['POST'])
def detect():
    """
    接收图片,返回检测结果
    """
    if 'image' not in request.files:
        return jsonify({'error': 'No image provided'}), 400
    
    # 读取图片
    file = request.files['image']
    img_array = np.frombuffer(file.read(), np.uint8)
    img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
    
    # 推理
    results = MODEL.predict(img, conf=0.25, iou=0.4, verbose=False)
    
    # 解析结果
    detections = []
    for box in results[0].boxes:
        x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().tolist()
        conf = box.conf[0].cpu().numpy().tolist()
        detections.append({
            'bbox': [x1, y1, x2, y2],
            'confidence': conf,
            'class': 'goods'
        })
        
    # 返回JSON数据
    return jsonify({
        'count': len(detections),
        'detections': detections
    })

if __name__ == '__main__':
    # 启动服务,debug=True仅用于开发环境
    app.run(host='0.0.0.0', port=5000, debug=False)

这个API怎么用呢? 前端或者摄像头抓拍程序,只需要发一个POST请求到 http://你的IP:5000/detect,带上图片文件,就能收到所有商品的坐标框。这对于开发库存管理系统来说,已经是万事俱备了。

七、总结与回顾

  这次实战之旅,咱们从零售场景的痛点出发,一步步完成了YOLOv11商品检测模型的落地。

  咱们做了这些事儿:

  1. 数据清洗:把SKU110K这个“硬骨头”啃下来,完成了VOC到YOLO格式的完美转换。
  2. 模型调优:针对密集货架场景,调整了输入分辨率、NMS阈值等关键参数,解决了漏检难题。
  3. 业务落地:不仅仅是画框,咱们还通过聚类算法实现了货架分层分析,把AI技术转化成了实实在在的业务指标(缺货率、层级饱和度)。
  4. 工程部署:从ONNX导出到Flask API搭建,打通了模型到应用的“最后一公里”。
本文转载自CSDN软件开发网, 作者:CSDN软件开发网, 原文标题:《 YOLOv11 在零售领域实战:利用公开的商品检测数据集 (如 SKU110K 的子集),训练一个 YOLOv11 模型,用于识别货架上的各种商品 》, 原文链接: https://blog.csdn.net/2501_92809516/article/details/159955053。 本平台仅做分享和推荐,不涉及任何商业用途。文章版权归原作者所有。如涉及作品内容、版权和其它问题,请与我们联系,我们将在第一时间删除内容!
本文相关推荐
暂无相关推荐
点击立即订阅