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

🎪 摸鱼匠:个人主页

🎒 个人专栏:《YOLOv11实战专栏

🥇 没有好的理念,只有脚踏实地!


文章目录

一、零售智能化的敲门砖:为什么选择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程序员的福音。

bash 复制代码
# 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 m i n , y m i n , x m a x , y m a x ) (x_{min}, y_{min}, x_{max}, y_{max}) (xmin,ymin,xmax,ymax)。但是!YOLOv11模型,它"吃"的是TXT格式,也就是每张图片对应一个TXT文件,里面每一行代表一个目标,格式是:
c l a s s _ i d x c e n t e r y c e n t e r w i d t h h e i g h t class\id \quad x{center} \quad y_{center} \quad width \quad height class_idxcenterycenterwidthheight

注意,这里的坐标都是归一化之后的相对坐标(0到1之间)。

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

咱们来看看这个转换的数学原理,虽然简单,但容不得半点马虎:

假设图片宽度为 W W W,高度为 H H H,VOC格式标注的框坐标为 ( x m i n , y m i n , x m a x , y m a x ) (x_{min}, y_{min}, x_{max}, y_{max}) (xmin,ymin,xmax,ymax)。

  1. 计算框的宽高
    w b o x = x m a x − x m i n w_{box} = x_{max} - x_{min} wbox=xmax−xmin
    h b o x = y m a x − y m i n h_{box} = y_{max} - y_{min} hbox=ymax−ymin

  2. 计算框的中心点
    x c e n t e r = x m i n + w b o x 2 x_{center} = x_{min} + \frac{w_{box}}{2} xcenter=xmin+2wbox
    y c e n t e r = y m i n + h b o x 2 y_{center} = y_{min} + \frac{h_{box}}{2} ycenter=ymin+2hbox

  3. 归一化处理 (这一步最关键):
    x n o r m = x c e n t e r W x_{norm} = \frac{x_{center}}{W} xnorm=Wxcenter
    y n o r m = y c e n t e r H y_{norm} = \frac{y_{center}}{H} ynorm=Hycenter
    w n o r m = w b o x W w_{norm} = \frac{w_{box}}{W} wnorm=Wwbox
    h n o r m = h b o x H h_{norm} = \frac{h_{box}}{H} hnorm=Hhbox

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

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

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

python 复制代码
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

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 文件夹里:

python 复制代码
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格式了:

text 复制代码
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脚本来启动训练,这样逻辑更清晰。

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 动手进行模型验证

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

python 复制代码
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更"宽容"一些。

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

python 复制代码
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 ) (c_x, c_y) (cx,cy),我们可以根据 y y y 坐标(垂直位置)来对商品进行聚类。 y y y 坐标相近的商品,很可能在同一层货架上。

我们可以用简单的 K-Means聚类 或者 分位数切分 来实现这个逻辑。
YOLOv11推理
按Y坐标聚类
计算层内框的数量
OCR识别/特征匹配
原始图像
获得商品检测框
数据分析模块
分层货架信息
货架层饱和度分析
具体SKU识别
生成补货预警
销售热力图统计

上图展示了从原始图像到业务逻辑的流转过程。下面我们来用代码实现这个"货架分层"的逻辑。

5.2 编写货架分析工具类

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

python 复制代码
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 y y 轴坐标把商品分成了几堆。
  • np.argsort 确保了层级顺序是"从上到下"的,而不是随机的。
  • 最后输出的 layer_counts 就可以直接发给仓库管理员:"嘿,第二层货架只有3个商品了,赶紧补货!"

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

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

  1. 缺货率计算
    缺货率 = 理论陈列数量 − 实际检测数量 理论陈列数量 \text{缺货率} = \frac{\text{理论陈列数量} - \text{实际检测数量}}{\text{理论陈列数量}} 缺货率=理论陈列数量理论陈列数量−实际检测数量

    我们可以设定一个阈值,比如缺货率超过 30%,系统自动报警。

  2. 陈列合规检查

    很多品牌商要求自己的商品必须放在"黄金陈列位"(比如视线平行的货架层)。我们可以检查检测到的商品框是否落在了规定的区域。

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

python 复制代码
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提供了一个非常方便的导出接口,不需要我们再去写繁琐的转换脚本。

python 复制代码
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库。这个过程比较复杂,咱们简要提一下操作步骤:

python 复制代码
# 需要先安装 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演示,它轻量级,非常适合做这种微服务。

python 复制代码
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搭建,打通了模型到应用的"最后一公里"。
相关推荐
imbackneverdie2 小时前
分享一些高级感科研绘图配色
图像处理·人工智能·ai·aigc·ai绘画·贴图·科研绘图
行者-全栈开发2 小时前
AI 驱动的智能行程规划系统:腾讯地图 Map Skills 实战
人工智能·路径规划·ai agent·多人协同·tool calling·mcp 协议·poi 检索
antzou2 小时前
语音识别 (ASR)
人工智能·语音识别·onnx·asr·paraformer
逸风尊者2 小时前
2026 主流 Claw 类产品技术报告
人工智能·后端·算法
两万五千个小时2 小时前
Claude Code 源码:工具 Plan 模式
人工智能·程序员·架构
NikoAI编程2 小时前
Anthropic 的一周两面:Managed Agents基建和Mythos模型
人工智能·agent·ai编程
linux_map2 小时前
大模型微调实战指南
人工智能·python·ai·策略模式
V搜xhliang02462 小时前
多期CT影像组学融合临床危险因素模型预测甲状腺乳头状癌中央区淋巴结转移的价值
人工智能·重构·机器人
RFID舜识物联网2 小时前
耐高温RFID技术如何解决汽车涂装车间管理难题?
大数据·人工智能·嵌入式硬件·物联网·安全·信息与通信