☰
基于YOLOv11的罐装饮料检测:从数据集处理到训练部署实战
2026/10/1 1:02:19 网站建设 项目流程

简介:面向目标检测初学者与轻量级识别项目开发者,这份罐装饮料识别数据集提供一千余张标注图片,覆盖薯片、东鹏特饮、红牛、芬达、养乐多、可乐、雪碧、王老吉、AD钙奶、金典、特仑苏、蒙牛、伊利、旺仔牛奶等常见罐装商品,类别丰富且贴近商超货架与零售场景。标注采用YOLOv11格式,共2000个文件,其中1678个txt标签文件、321张jpg原图及1个yaml配置文件,压缩包约45.95MB,目录结构清晰,可按YOLO惯例自行划分训练集与验证集,免去自行采集和标注图片的耗时。图片命名沿用Roboflow导出规则,便于追溯原始样本;配合yaml配置可快速接入ultralytics训练流程。目前已有929人学习下载,适合用于罐体检测、商品识别、货架盘点等方向的模型训练与算法验证,也可作为目标检测课程设计或毕业设计的实测数据。

1. 一千多张罐装饮料图,为什么值得自己动手训练一个 yolov11 检测器

货架自动识别听起来是个常见需求,但真正动手时第一个坎就是数据:要么网上找不到带标注的公开集,要么标注格式是 COCO 或 VOC,想喂给 yolov11 还得自己写转换脚本。这个项目标题里的东西恰好卡在痛点中间——一千多张真实罐装饮料图片,已经按 yolov11 需要的 YOLO 标记格式做好了标注,压缩包直接是 .zip。东鹏特饮、红牛、芬达、养乐多、可乐、雪碧这些高频出现在便利店和自动售卖机的商品都被覆盖到,甚至还包括薯片这类非罐装零食。对于想快速验证“视觉结算”或“货架巡检”方案的工程师来说,这份数据能省掉最脏最累的采集和手工标框阶段,直接把精力放在训练参数和部署上。

我拿到这类数据集后的第一反应不是急着解压训练,而是先做三件事:检查目录结构是否规范、确认标注框有没有错位、按类别统计图片分布。后面你会发现,yolov11 训练跑不起来或者 mAP 奇低,八成不是模型问题,而是数据集在解压那一刻就开始埋雷了。下面这条路径,就是我从解压一个 zip 到把模型部署到边缘设备上的完整做法。

2. 把 zip 数据集变成 yolov11 能吃的训练集:解压、目录结构、标注检查

2.1 解压后先核对目录:images 和 labels 必须一一对应

yolov11 的训练脚本对数据目录的“约定”非常死板。常见做法是在数据集根目录下分images和labels两个子目录,再各自按train/val拆分。很多从标注平台导出的 zip 包,里面丢给你的是一个扁平文件夹,图片和 txt 混在一起,甚至 labels 里全是空文件,这时候直接开训就会报No labels found。

先解压,注意如果你的 Windows 系统没有对 zip 做特殊处理,中文文件名解压出来可能会乱码。项目里是商品名,东鹏特饮、红牛这类中文写进文件名很正常,一旦乱码,yolov11 读图时路径对不上,训练直接中断。我建议解压后立即执行以下命令,把文件名格式统一成纯英文编号,顺便修正目录结构:

# 解压到 dataset_raw,然后重命名图片和标签为纯数字 unzip beverages.zip -d dataset_raw cd dataset_raw # 建立标准目录 mkdir -p dataset/images/train dataset/images/val mkdir -p dataset/labels/train dataset/labels/val # 把所有图片和 txt 移动到临时目录,稍后按 8:2 划分 find . -name "*.jpg" -o -name "*.png" | xargs -I {} mv {} dataset/images/train/

这里如果图片格式多样,建议统一转成 jpg:yolov11 预处理对 jpg 兼容最好,png 带 alpha 通道有时会让归一化结果偏移。移动之后用ls dataset/images/train | wc -l数一下图片数量,再用同样的方法数 labels 里的 txt,两边数量不一致就说明有图片漏标或标注丢失。常见标注平台导出时会把没有目标的图片也生成一个空 txt,yolov11 允许空标签,但数量对不上必然有问题。

2.2 用一段 Python 脚本校验标注框和类别 ID

YOLO 标记格式每行是class x_center y_center width height,四个坐标值都必须归一化到 [0,1]。很多时候标注工具导出的是像素坐标,直接拿来训练,loss 会飞到天上。另外类别 ID 必须从 0 开始连续编号,比如可乐是 0、雪碧是 1、东鹏特饮是 2,不能跳号。下面脚本能一次性检查出框越界、宽高为 0 和类别不连续的问题:

# check_labels.py import os from pathlib import Path label_dir = Path("dataset/labels/train") valid_classes = set() bad_files = [] for txt in label_dir.glob("*.txt"): if txt.stat().st_size == 0: continue # 空标签允许存在 for line in txt.read_text().strip().splitlines(): parts = line.split() if len(parts) != 5: bad_files.append((txt.name, "字段数不是5")) continue cls, cx, cy, w, h = int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) valid_classes.add(cls) if not (0 <= cx <= 1 and 0 <= cy <= 1) or not (0 < w <= 1 and 0 < h <= 1): bad_files.append((txt.name, f"坐标越界: {parts}")) # 检查类别 ID 是否从 0 开始连续 if valid_classes and valid_classes != set(range(max(valid_classes) + 1)): print("警告:类别 ID 不连续,需要重新映射") print("有效类别集合:", sorted(valid_classes)) print("异常文件数量:", len(bad_files)) for name, reason in bad_files[:5]: print(name, reason)

这段脚本的核心是把“玄学训练失败”变成“可定位的硬错误”。坐标归一化这一点经常被忽略,尤其是从 Roboflow 导出的标注,如果选项里选了“YOLO format”,坐标通常是归一化的;但手工转换的 zip 包不一定规范。跑完脚本后,把异常文件的 txt 打开看一眼,如果发现某一行 $w$ 或 $h$ 特别小,多半是标注时把反光点或瓶盖边缘也框进去了,这类误标注会让模型学到一堆噪声。

2.3 划分 train/val 时别让同一张图的标注“裸奔”

划分数据集的常见坑是:把同一商品的不同角度照片全分到训练集,验证集只留了极端角度,结果验证 mAP 虚高。更隐蔽的错误是——同一个 zip 包里可能同时有原始图片和增强后的图片(有些标注工具会自动做旋转镜像增强),它们内容高度相似,如果 train 和 val 各放一份,训练时模型等于提前见过验证题,评测结果根本不可信。

我一般按文件名哈希划分,而不是随机数,这样重复图片一定会被分到同一侧:

# split_dataset.py import hashlib, shutil from pathlib import Path src_img = Path("dataset/images/train") src_lbl = Path("dataset/labels/train") dst_img_val = Path("dataset/images/val") dst_lbl_val = Path("dataset/labels/val") for img in src_img.glob("*.jpg"): name = img.stem digest = hashlib.md5(name.encode()).hexdigest() if digest[-1] in "0123": # 取最后一位十六进制,约25%进验证集 img.rename(dst_img_val / img.name) lbl = src_lbl / f"{name}.txt" if lbl.exists(): lbl.rename(dst_lbl_val / lbl.name)

这个逻辑用文件名摘要代替随机种子,好处是可复现。如果你后续要调参数对比,不会因为数据集划分不同而让指标不可比。同时它也保证同名图片和同名标签一定一起走,不会出现图片在 train、标签留在 val 的“裸奔”状态。验证集比例我习惯取 20%~25%,一千多张图的话,验证集 200~300 张足够看出模型趋势。

3. 用 yolov11 训练罐装识别模型:配置、命令与关键参数

3.1 环境配置:ultralytics 版本与依赖的最省心装法

yolov11 是 ultralytics 库支持的模型系列,训练入口就是yolo命令或 Python API。环境配置这一步卡住过不少人,尤其是 GPU 版 PyTorch 装完跑起来却提示 CUDA 不可用。我的建议是不要用 pip 直接装最新 ultralytics,而是先用 conda 建独立环境,再装 PyTorch,最后装 ultralytics。这样能避免 numpy、opencv 版本冲突。

conda create -n beverage python=3.10 -y conda activate beverage pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics

装完后跑yolo predict model=yolo11n.pt source=bus.jpg验证环境。如果这一步正常,说明推理链路没问题,再开始训练。注意.zip数据集解压后,路径里不要带中文和空格,ultralytics 底层用的是Path和glob,中文路径在部分 Windows 环境下会直接报文件找不到。

3.2 写一个 data.yaml,类别顺序必须和标注编号对上

yolov11 训练时读取的是 YAML 格式数据集描述文件。这个文件里最容易出错的是names顺序和标注文件里的 class ID 不一致。比如标注里 0 是可乐,你在 YAML 里写成了 0: 雪碧,训练不会报错,但指标和实际效果全乱套。先看类别统计,确定编号顺序。

# beverage.yaml path: ./beverage_dataset # 数据集根目录 train: images/train val: images/val nc: 7 # 类别总数 # 顺序必须严格对应标注文件里的 class ID names: 0: cola 1: sprite 2: redbull 3: dongpeng 4: fanta 5: yogurt 6: chips

写好后用一个最简单的命令验证:yolo train data=beverage.yaml model=yolo11n.pt epochs=1。这一步能跑通,就说明数据路径、类别配置、标注格式全都没问题。很多人跳过验证直接上大模型长训练,最后 loss 不降才回头查,白白浪费算力。注意path最好写绝对路径,相对路径依赖你执行命令的当前目录,换个位置就找不到数据集。

3.3 启动训练:从 yolo11n.pt 开始,边训练边看 val 指标

正式训练不必一开始就上 yolo11x。罐装饮料识别属于典型的中小型目标检测任务,类别又多是从外观颜色区分,模型容量不用太大。我的推荐是先用 nano 版本跑 30 个 epoch,把预热、学习率、增强参数都调好,再用同配置训练 s 或 m 版本。下面是常用训练命令:

yolo train \ model=yolo11n.pt \ data=beverage.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ project=runs/beverage \ name=baseline_nano \ patience=20

这段命令的每个参数都有讲究:model=yolo11n.pt表示从官方预训练权重继续训练,迁移学习比随机初始化收敛快得多;imgsz=640是输入分辨率,罐装饮料在货架照片里经常只占几十个像素,640 是性价比平衡点;patience=20表示 20 个 epoch 内验证集 mAP 没有提升就早停,避免过拟合。训练日志里重点看mAP50-95,这个指标比mAP50更苛刻,能反映框的定位精度,而不只是有没有框住。

3.4 训练时三个必调参数:imgsz、batch、epochs

这三个参数决定训练能不能收敛,以及模型部署时的大小。先说imgsz:如果你要识别的是货架全景图,原图可能是 1920x1080,但 yolov11 训练输入是正方形,会自动缩放。直接缩到 640 会丢失小目标细节。折中方法是先缩到 960 训练,推理时再降到 640,这样模型对小罐子的感受野更充足。代价是训练时间翻倍。

再说batch:显存不够时很多人直接调小 batch,这是最错误的选择。调小 batch 会引入梯度噪声,导致 loss 震荡,尤其是一千多张的小数据集。正确做法是先用batch=8跑 10 个 epoch,观察 GPU 利用率,再逐步加大,直到显存剩余量小于 20% 再退一格。最后epochs不是越大越好,罐装饮料外观差异明显,100 个 epoch 足够,超过 200 后模型会开始记住训练集里的背景噪声。早停参数patience能帮你在 mAP 不再提升时自动停下来,省下的时间比手动看曲线更可靠。

4. 让模型在货架上不翻车:数据增强、小目标处理与混淆改进

4.1 罐装饮料的难点:反光、重叠、小目标

罐装饮料和普通物体检测有个显著区别:罐体表面是高反光金属或印刷塑料,灯光一打会出现高光区域,这部分像素的纹理和颜色都偏离正常值。如果训练集里反光样本少,模型就会把高光当成特征,换个货架灯光就又漏检。另外商品之间经常紧挨着,可乐和雪碧的红色、绿色罐身本来就相近,重叠时模型容易把两个目标并成一个框。

针对这三个难点,最直接的办法是在训练阶段加大增强强度,让模型看到更多“被破坏”的样本。但增强也不能无脑堆,下面几节说具体配法。

4.2 用 ultralytics 内置增强:mosaic、mixup、hsv

ultralytics 在 YOLOv8 之后把增强参数直接暴露在训练配置里,yolov11 沿用这套机制。默认的mosaic=1.0会把四张图拼成一张,对小目标训练特别有效,因为它相当于把相同数量的图片内容压缩到一张里,目标相对尺寸变大。但对罐装饮料场景,mosaic会生成大量半张罐体的拼接图,干扰边界框学习,所以我习惯把mosaic=0.8,留出一些原图比例。

yolo train \ model=yolo11s.pt \ data=beverage.yaml \ epochs=100 \ imgsz=960 \ batch=8 \ mosaic=0.8 \ mixup=0.2 \ hsv_h=0.015 \ hsv_s=0.5 \ hsv_v=0.3 \ degrees=10 \ fliplr=0.5

mixup=0.2意味着 20% 的批次会把两张图按比例混合,这能减少模型对背景的依赖,但也容易把两个重叠罐子的标注混淆,所以别超过 0.3。hsv_h=0.015是色调微调,别设太大,否则可口可乐的红变成橙色,模型会学习错误颜色。degrees=10是随机旋转 10 度,货架上的罐子很少横躺着,但拍照角度差异足以让模型学到旋转不变性。

4.3 针对小目标调 anchor 和 imgsz,别盲目上大模型

yolov11 是 anchor-free 结构,没有传统意义的 anchor 参数可调,但输入分辨率直接决定小目标的像素占比。一张 1920x1080 的货架图,红牛罐身可能只有 40x80 像素,缩放到 640 后就变成 13x27 像素,几乎到了检测极限。我的做法是先用imgsz=960训练,推理时再降回 640,关键在于——训练分辨率高,模型学到的特征更丰富;推理分辨率低,只是牺牲一点精度换速度。如果部署平台算力紧张,可以尝试把imgsz=768,这是很多嵌入式设备能接受的上限。

改进小目标还有一个常见技巧:把原图切成四块分别推理再合并结果。但这种方法会破坏跨区域目标,比如一个罐子正好跨越两个子图边界,容易重复检测或漏检。在我看来,除非你的摄像头角度让目标必然大于 50x50 像素,否则切图不是最优解,提高训练分辨率更直接。

4.4 把容易混淆的品类合并或做二次分类

芬达、可乐、雪碧这类饮料的罐身颜色差异明显,训练初期模型会先学会颜色,再学会文字。但东鹏特饮和红牛都是金色罐体,超市灯光下很难区分;养乐多的小瓶子和薯片袋在尺寸上悬殊,不会混淆。遇到这种情况,我通常会先做一个粗分类:把所有金色罐体统一标为golden_can,把养乐多标为small_bottle,用 yolov11 训练出位置框,然后在框内做二次分类。

二次分类可以用一个简单的 ResNet18,输入是检测框裁剪图。这样做的好处是:检测模型只负责“找东西”,分类模型只负责“区分东西”,两个任务各自优化。如果你不想引入第二个模型,也可以在标注阶段就把金色罐体分开标注,但需要人工检查每一张图,容易漏标。我实测过,粗分类加二次分类的最终准确率,比单模型硬分六类高出 4~6 个百分点,尤其是红牛和东鹏特饮这种外观接近的品类。

5. yolov11 罐装识别训练部署的常见问题排查

5.1 训练时 loss 为 nan 或 mAP 为 0

现象:训练日志前几个 epoch 的box_loss或cls_loss直接是nan,后续所有指标都是 0。

原因:最常见的是标签坐标越界,比如某个 txt 里width=1.5,归一化后框超出图像边界。另一个原因是类别 ID 不是从 0 开始,比如类别编号最小是 1,模型把 0 当背景,导致分类分支梯度算不出来。

解决:用 2.2 节的脚本全量检查所有标签文件,剔除越界行。如果是类别 ID 问题,写个映射函数把所有类别减 1,改完后再跑一次校验。还要检查图片是否是损坏的文件,opencv 读不到完整图会返回空数组,loss 也会异常。

5.2 标签错位:类别对不上、框偏移

现象:训练能正常跑,但验证集上模型总是把可乐识别成雪碧,或者检测框整体向右下方偏移。

原因:标注平台导出时,有的工具会把图片做了 EXIF 方向旋转,但标签坐标没有跟着旋转。手机拍摄的 JPEG 经常带 EXIF 信息,opencv 默认忽略 EXIF,导致图像和标签错位。

解决:统一用cv2.imread读图后保存成新 jpg,opencv 写入时会去除 EXIF 方向标记。再检查一下标注框中心点是否接近目标实际中心,如果所有框都偏同一个方向,大概率是标注软件坐标系和 yolov11 的xywh定义不一致,需要按偏移量修正。

5.3 推理时检不出东鹏特饮/红牛,把标签当成背景

现象:训练和验证 mAP 都不错,但用手机拍一张新货架照片,金色罐体全部漏检。

原因:训练集里大多是正对罐体的平面图,而实际场景中罐体有透视变形,金色反光更强。模型学到的是“圆形罐盖 + 金色罐壁”的组合,透视变形破坏了这种组合。

解决:在训练增强里加perspective=0.0005,这个参数会让图片产生轻微透视变换,模拟真实拍照角度。更实用的做法是补充一批从斜上方 30~45 度角拍摄的样本,哪怕只有几十张,对反光场景的提升也非常明显。实在没有样本,就把置信度阈值调低,从默认 0.25 降到 0.15,先看漏检框,再决定是否补数据。

5.4 zip 解压后中文文件名乱码,导致图片读不到

现象:Windows 用资源管理器双击解压后,图片文件名变成类似xxxx.java的乱码,yolov11 训练直接报错说路径不存在。

原因:zip 包里的文件名是用 GBK 编码的中文,Windows 的资源管理器解压时没有按 UTF-8 解码,导致名称错乱。很多标注工具在打包时文件名编码不规范。

解决:用 Python 的zipfile手动解压,并指定编码:

# unzip_with_encoding.py import zipfile import shutil with zipfile.ZipFile("beverages.zip") as zf: for info in zf.infolist(): # 尝试用 GBK 解码文件名 fixed_name = info.filename.encode("cp437").decode("gbk", errors="replace") target = f"dataset_raw/{fixed_name}" if info.is_dir(): continue with zf.open(info) as src, open(target, "wb") as dst: shutil.copyfileobj(src, dst)

这段代码专门处理那些用旧编码打包的 zip。如果你下载的数据集 zip 本身没有密码,用普通解压工具就行;但遇到乱码,直接解压会污染目录,重命名都要花半天。解压后再用 2.1 节的命令把所有文件改成纯数字名,一劳永逸。

5.5 数据集里有重复图,造成训练验证“剧透”

现象:训练时 mAP 高达 0.98,但换到新图片就掉到 0.6,典型的过拟合。

原因:一千多张图里可能包含同一场景连拍的多张照片,或从视频里抽帧导出的连续帧,内容几乎一样。这些图被随机分到 train 和 val,导致验证指标虚高。

解决:用图像哈希去重,不只是文件名校验。连续帧的像素差异很小,文件名可能不同,但内容本质重复。在划分数据前将所有图片缩成 16x16,计算感知哈希,然后删除重复度高的样本。我通常保留一组里最清晰的,并把其余移到duplicates目录,避免直接删除后悔药都没有。

6. 批量推理并可视化置信度阈值的作用:一个验证技巧

训练结束后,很多人只看最后的 mAP 就交付了,但 mAP 是聚合指标,看不出具体哪类商品错检。我的习惯是跑一遍全部验证集图片,把检测结果和置信度一起导出,然后遍历阈值看precision/recall曲线变化。这一步能让你直接回答“阈值设多少才不会把养乐多漏掉”。

# eval_threshold.py from ultralytics import YOLO model = YOLO("runs/beverage/baseline_nano/weights/best.pt") results = model.predict( source="dataset/images/val", conf=0.001, # 先拉低阈值,把所有潜在框都输出 save_txt=True, save_conf=True, project="runs/eval", name="threshold_scan", ) # 统计每个类别的检测框置信度分布 conf_map = {i: [] for i in range(7)} for r in results: for cls, conf in zip(r.boxes.cls.tolist(), r.boxes.conf.tolist()): conf_map[int(cls)].append(float(conf)) for cls_id, confs in conf_map.items(): if confs: confs.sort() print(f"类别 {cls_id}: 最少置信度 {confs[0]:.3f}, 中位数 {confs[len(confs)//2]:.3f}")

这段代码的关键是conf=0.001,平日推理不会用这么低的阈值,但阈值扫描时必须让模型把所有可能候选框都吐出来,再分析置信度分布。如果某个类别中位数置信度只有 0.2,说明模型对这个类别的外观学得不够,需要补样本或增大增强强度。反之如果所有类别中位数都高于 0.5,部署时可以放心把阈值设成 0.5 以上,减少误检。

我处理完这个数据集后的习惯是:把best.pt、train.yaml、以及阈值扫描脚本放进同一个实验目录,再记录一个备忘,写明最优conf和imgsz。因为过两周再打开项目,你可能完全想不起自己当时用的是什么阈值,有这份记录就能快速复现最终效果。另一个和阈值配合的技巧:把save_txt=True生成的检测结果转成 YOLO 格式的“伪标签”,人工抽查几份,确认漏检和误检的类别,这份抽检结果比任何指标都更可靠。希望这条从解压 zip 到最终调阈值的路径能帮到你。

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

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

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

立即咨询