☰
基于YOLOv8的罐装饮料检测:千张图片训练与部署实战
2026/10/1 16:39:47 网站建设 项目流程

简介:本资源面向计算机视觉学习者与目标检测开发者,提供罐装饮料识别数据集,可直接用于YOLOv8模型训练与验证。数据覆盖薯片、东鹏特饮、红牛、芬达、养乐多、可乐、雪碧、王老吉、AD钙奶、金典、特仑苏、蒙牛、伊利、旺仔牛奶等常见饮品,适合课堂实验、竞赛项目与算法练手。压缩包共2000个文件,含1678个txt标注文件、321张jpg图像及1个yaml配置文件,整体约45.95MB,标注与图像一一对应,yaml可直接指定类别与路径,省去手工整理环节。目前已有929人学习下载,说明该数据集在同类任务中具备一定参考价值。读者可据此快速搭建训练流程,验证模型对多品牌、多包装饮料的检测效果,并在此基础上做数据增强、类别扩展或迁移学习,减少从零采集与标注的成本。

1. 罐装饮料识别数据集:一千多张图能撑起一个可用的 YOLOv8 检测器吗

手里拿到一份标注好的罐装饮料数据集,一千多张图片,YOLOv8 格式,覆盖薯片、东鹏特饮、红牛、芬达、养乐多、可乐、雪碧、王老吉这类常见快消品。第一反应通常是:这点量够不够训出一个能上货架的模型?我的经验是,够,但前提是你得把「够」定义清楚——如果目标是固定机位、固定光照下的货架盘点或自助结算辅助,一千多张、七八个类别,每类一百多张,配合 YOLOv8n 或 YOLOv8s,mAP50 做到 0.85 以上是现实的;如果目标是任意角度、任意遮挡的开放场景,那这点数据只能算冷启动,后面必须补采。

这份数据集真正的价值不在「多」,而在「标签已经是 YOLOv8 格式」。做过目标检测的人都知道,从 Labelme 的 JSON、VOC 的 XML 转到 YOLO 的 txt,中间那层坐标归一化和类别映射,是新手最容易翻车的地方。这份数据省掉了这一步,意味着你可以把精力全部放在训练策略和部署验证上。它适合三类人:想跑通 YOLOv8 全流程的入门者、做零售货架识别的方案验证者、以及需要一个小型多类别检测基线做对比实验的工程师。下面我按「先看懂数据长什么样、再跑通训练、最后避开那些必踩的坑」的顺序,把这条路走一遍。

2. 拆开这份数据集:YOLOv8 标注格式、类别分布与目录结构

2.1 YOLOv8 的 txt 标注到底存了什么

YOLOv8 沿用 YOLO 系列的标注约定:每张图对应一个同名 txt,每行一个目标,格式是class_id x_center y_center width height,后四个值都是相对图像宽高的归一化浮点数,范围 0 到 1。这里有两个高频误解。第一,x_center y_center是框中心点,不是左上角;第二,归一化是除以图像自身的宽和高,不是除以某个固定尺寸。很多人从 VOC 转过来时忘了除以宽高,训出来的框会全部挤在左上角,损失曲线看着在降,实际检测全是废框。

先写个脚本把数据集的真实情况摸清楚,别急着开训。下面这段代码统计类别分布、检查标注合法性、找出空标签和越界框:

import os from collections import Counter from pathlib import Path IMG_EXTS = {".jpg", ".jpeg", ".png", ".bmp"} root = Path("dataset") # 改成你的数据集根目录 img_dir = root / "images" lbl_dir = root / "labels" cls_counter = Counter() bad_lines = [] empty_labels = [] missing_labels = [] size_pairs = [] for img_path in img_dir.rglob("*"): if img_path.suffix.lower() not in IMG_EXTS: continue lbl_path = lbl_dir / (img_path.stem + ".txt") if not lbl_path.exists(): missing_labels.append(str(img_path)) continue lines = [l.strip() for l in lbl_path.read_text().splitlines() if l.strip()] if not lines: empty_labels.append(str(lbl_path)) continue for ln in lines: parts = ln.split() if len(parts) != 5: bad_lines.append((str(lbl_path), ln)) continue cid, x, y, w, h = parts vals = list(map(float, [x, y, w, h])) if any(v < 0 or v > 1 for v in vals) or vals[2] <= 0 or vals[3] <= 0: bad_lines.append((str(lbl_path), ln)) continue cls_counter[int(cid)] += 1 print("类别分布:", dict(sorted(cls_counter.items()))) print("缺失标签:", len(missing_labels)) print("空标签文件:", len(empty_labels)) print("非法标注行:", len(bad_lines)) for item in bad_lines[:10]: print(" 异常:", item)

这段脚本的逻辑很直白:遍历图片目录,按同名规则找标签,逐行校验字段数和数值范围。cls_counter告诉你每个类别有多少个框,这是判断类别是否失衡的第一手依据。bad_lines抓的是字段数不对或坐标越界的行,这类行如果直接喂给训练,YOLOv8 的 dataloader 会在某个 batch 上抛异常,而且报错信息往往只指向行号,不告诉你哪张图,排查起来很费时间。empty_labels是空 txt,YOLOv8 默认会把它们当负样本,如果你不希望背景图参与,得在配置里处理。跑完这一步,你对数据的底细就有数了。

2.2 目录结构与 data.yaml 的正确写法

YOLOv8 对目录结构有约定,常见的是images/train、images/val、labels/train、labels/val平行放置。如果你的数据现在全堆在一个文件夹里,先做一次划分。划分时有个血泪经验:同一个视频抽帧出来的连续图片,绝不能一部分进 train、一部分进 val,否则验证集精度会虚高得离谱,因为相邻帧几乎一模一样。正确做法是按「拍摄批次」或「视频来源」整体划分,而不是随机按文件划分。

import random import shutil from pathlib import Path random.seed(42) root = Path("dataset") img_dir = root / "images" lbl_dir = root / "labels" for split in ["train", "val"]: (img_dir / split).mkdir(parents=True, exist_ok=True) (lbl_dir / split).mkdir(parents=True, exist_ok=True) imgs = [p for p in img_dir.glob("*") if p.suffix.lower() in {".jpg", ".jpeg", ".png", ".bmp"}] random.shuffle(imgs) val_ratio = 0.2 n_val = int(len(imgs) * val_ratio) val_set = set(imgs[:n_val]) for img in imgs: split = "val" if img in val_set else "train" lbl = lbl_dir / (img.stem + ".txt") shutil.move(str(img), str(img_dir / split / img.name)) if lbl.exists(): shutil.move(str(lbl), str(lbl_dir / split / lbl.name)) print("train:", len(imgs) - n_val, "val:", n_val)

random.seed(42)保证划分可复现,换台机器跑结果一致。val_ratio=0.2是常规起点,数据量小的时候可以降到 0.15,把更多样本留给训练。注意这里用shutil.move是原地移动,跑之前先备份一份原始数据,别问我怎么知道的。

对应的data.yaml写法如下,names的顺序必须和标注里的class_id严格对应,错一位整个类别就串了:

path: /abs/path/to/dataset train: images/train val: images/val nc: 8 names: 0: lays_chips 1: dongpeng 2: redbull 3: fanta 4: yakult 5: cola 6: sprite 7: wanglaoji

path建议写绝对路径,相对路径在不同工作目录下启动训练时经常找不到文件。nc是类别数,必须和names条目数一致,不一致时 YOLOv8 会在构建模型头时报维度错误。类别名用英文小写下划线,避免中文和空格,虽然新版支持中文,但在跨平台和导出 ONNX 时英文名省心得多。

3. 用 YOLOv8 训练罐装饮料检测模型:环境、命令与参数

3.1 环境配置与最小训练命令

环境这块,CPU 版本在 Ubuntu 20.04 上就能跑通,只是慢。有 GTX1660Ti 这类 6G 显存的卡,用yolov8n或yolov8s完全够。安装就两步:

pip install ultralytics yolo checks

yolo checks会打印环境自检表,重点看 torch 版本、CUDA 是否可用、以及 ultralytics 版本。如果 CUDA 显示不可用但你确实有卡,多半是 torch 装成了 CPU 版,重装对应 CUDA 版本的 torch 即可。

最小训练命令:

yolo detect train \ data=/abs/path/to/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ project=runs/detect \ name=drink_v1

model=yolov8n.pt是从预训练权重起步,小数据集强烈建议这么做,从头训几乎不可能收敛好。imgsz=640是输入分辨率,罐装饮料在图中占比通常不大,640 是精度和速度的平衡点;如果图片里饮料很小,可以提到 960,但显存和耗时都会涨。batch=16在 6G 显存上跑 640 分辨率基本安全,爆显存就降到 8。device=0指定第一块 GPU,CPU 训练写device=cpu。

3.2 关键训练参数怎么调

YOLOv8 的参数很多,但小数据集上真正影响结果的就那么几个。下面这张表是我在罐装饮料这类场景里反复验证过的取值区间:

参数含义小数据集建议值说明
epochs训练轮数100~300数据少可以多跑,配合早停
patience早停耐心30~50验证指标不升就停,省时间
lr0初始学习率0.01默认值,微调时可降到 0.001
lrf最终学习率因子0.01余弦退火终点
warmup_epochs预热轮数3防止初期梯度爆炸
mosaic马赛克增强1.0小数据集必开,提升泛化
close_mosaic关闭 mosaic 轮数10最后几轮关掉,稳定收敛
mixup混类增强0.1类别少时可开,别太高
degrees旋转角度5~10货架场景轻微旋转
hsv_v亮度扰动0.4应对不同光照

mosaic是小数据集的生命线,它把四张图拼成一张,等效于增加了样本组合,对罐装饮料这种目标密集的场景尤其有效。但close_mosaic=10一定要设,最后十轮关掉马赛克,让模型在真实分布上收敛,否则验证精度会偏低。patience配合epochs用,我一般设 epochs=200、patience=40,实际往往跑不到 200 轮就停了。

训练启动后,重点盯三个东西:box_loss是否稳定下降、mAP50是否在涨、以及有没有出现nan。如果 loss 一开始就 nan,八成是学习率太高或标注里有非法值,回到 2.1 的脚本再查一遍。如果 mAP 长时间不动,先看验证集是不是和训练集分布差太远。

3.3 训练结果怎么看:损失曲线与混淆矩阵

训练完在runs/detect/drink_v1下会生成results.csv、confusion_matrix.png、results.png等文件。results.png里那几条曲线是判断训练健康度的核心。train/box_loss和val/box_loss如果同步下降且差距不大,说明没怎么过拟合;如果 train 一直降、val 早早反弹,就是过拟合,得加数据或加增强。metrics/mAP50-95比mAP50更严格,小数据集上这个值通常比 mAP50 低不少,别慌,看趋势。

混淆矩阵能直接暴露类别混淆问题。罐装饮料里最容易混的是可乐和芬达、雪碧和芬达,因为罐体形状接近、颜色在光照下会偏。如果混淆矩阵显示这几类互相误判严重,说明模型没学到颜色以外的判别特征,这时候要么补采不同角度的图,要么在类别定义上做合并。想画更细的损失曲线,可以直接读results.csv:

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/detect/drink_v1/results.csv") df.columns = [c.strip() for c in df.columns] plt.plot(df["epoch"], df["train/box_loss"], label="train_box") plt.plot(df["epoch"], df["val/box_loss"], label="val_box") plt.plot(df["epoch"], df["metrics/mAP50"], label="mAP50") plt.legend() plt.xlabel("epoch") plt.savefig("curve.png", dpi=150)

results.csv的列名带空格,读进来先 strip 一遍,否则按列名取值会 KeyError。这张图比官方生成的更灵活,你可以把 mAP50 和 loss 画在一起,直观判断收敛点。

4. 推理、验证与部署:从 predict 到 ONNX 导出

4.1 用训练好的权重做推理

训练完先别急着部署,用predict在几张真实图上看看效果:

yolo detect predict \ model=runs/detect/drink_v1/weights/best.pt \ source=/abs/path/to/test_images \ conf=0.25 \ iou=0.45 \ save=True

conf=0.25是置信度阈值,低于它的框不输出;iou=0.45是 NMS 的 IoU 阈值,控制重叠框的合并。这两个值直接影响召回和误检:conf 调低召回高但误检多,调高反之。货架盘点场景我一般 conf 设 0.3 到 0.4,宁可漏检也别误报。save=True会把画框结果存到runs/detect/predict下。

如果想在 Python 里批量处理并拿到结构化结果:

from ultralytics import YOLO model = YOLO("runs/detect/drink_v1/weights/best.pt") results = model.predict("test.jpg", conf=0.3, iou=0.45, verbose=False) for r in results: for box in r.boxes: cls_id = int(box.cls) conf = float(box.conf) xyxy = box.xyxy.tolist()[0] print(model.names[cls_id], round(conf, 3), [round(v, 1) for v in xyxy])

box.xyxy是原图坐标系下的左上右下角点,直接可用于后续业务逻辑,比如判断某个饮料在货架的哪一层。model.names是类别 id 到名字的映射,来自 data.yaml。

4.2 导出 ONNX 与部署前验证

要在非 Python 环境或边缘设备上跑,导出 ONNX 是常规做法:

yolo export model=runs/detect/drink_v1/weights/best.pt format=onnx imgsz=640 opset=12 simplify=True

opset=12兼容性较好,simplify=True会做图优化,去掉冗余算子。导出后务必做一次一致性验证:用同一张图分别跑 PyTorch 权重和 ONNX,对比输出框的差异。差异在千分位以内算正常,如果差很多,多半是预处理(归一化、letterbox)没对齐。ONNX 推理时的输入是1x3x640x640的 float32,通道顺序 RGB,归一化到 0 到 1,letterbox 的填充值通常是 114。这些细节任何一处不对,结果都会偏。

提示:导出 ONNX 后,用 onnxruntime 跑一遍和 PyTorch 对比,是部署前最省事的后悔药。别等上了板子才发现框全偏了。

5. 罐装饮料识别避坑:五条踩出来的经验

5.1 现象:训练 loss 正常但验证 mAP 一直是 0

原因:data.yaml里的names顺序和标注class_id对不上,或者val路径写错导致验证集为空。YOLOv8 在验证集为空时不会报错,只是指标全 0。解决:用 2.1 的脚本打印类别分布,确认 class_id 从 0 连续;再手动ls一下images/val里有没有图。

5.2 现象:模型把可乐识别成芬达,颜色相近的类别互相误判

原因:数据里这两类的拍摄光照单一,模型学到的是「罐体形状 + 大致色块」,没学到包装上的文字和 logo 细节。解决:补采不同光照、不同角度的样本;或者在训练时提高imgsz到 960,让包装细节更清晰;实在分不开就考虑业务上是否必须区分。

5.3 现象:推理时框位置整体偏移

原因:预处理不一致。PyTorch 推理走的是 ultralytics 内置的 letterbox,而你自己写的 ONNX 预处理可能用了直接 resize,没保持长宽比。解决:ONNX 预处理必须复刻 letterbox,记录缩放比例和填充偏移,后处理时再映射回原图坐标。

5.4 现象:训练到一半显存爆了

原因:batch设太大,或者imgsz提高后没同步降 batch。解决:6G 显存跑 640 分辨率,yolov8n用 batch=16,yolov8s降到 8;开启amp=True混合精度也能省显存,但要注意某些卡上 amp 会导致 nan,出现就关掉。

5.5 现象:验证集精度虚高,上线后一塌糊涂

原因:划分时把同一视频的连续帧分到了 train 和 val,验证集和训练集高度相似。解决:按拍摄批次整体划分,确保 val 里的场景在 train 里没出现过。这是小数据集最隐蔽的坑,指标好看但没意义。

6. 把一千张图用出三千张的效果:几个进阶技巧

数据量固定的时候,提升空间主要在增强策略和训练技巧上。我常用的一个组合是:mosaic=1.0打底,copy_paste=0.3做实例粘贴,scale=0.5做随机缩放,再配合close_mosaic=15。copy_paste 会把一个实例抠出来贴到另一张图上,对罐装饮料这种独立目标特别合适,等效于免费扩充了样本组合。但 copy_paste 开太高会引入不自然的边缘,0.2 到 0.3 比较稳。

另一个技巧是分阶段训练。先用 640 分辨率跑 100 轮把模型训到基本收敛,再用 960 分辨率微调 30 轮。高分辨率微调能让模型适应更清晰的包装细节,对区分可乐和芬达这类任务提升明显,代价是显存和时间。我一般会在第一阶段结束后,把best.pt作为第二阶段的起点:

yolo detect train \ data=/abs/path/to/data.yaml \ model=runs/detect/drink_v1/weights/best.pt \ epochs=30 \ imgsz=960 \ batch=4 \ lr0=0.001 \ close_mosaic=10 \ name=drink_v1_finetune

第二阶段学习率降到 0.001,因为模型已经接近收敛,大学习率会把之前的成果冲掉。batch=4是 960 分辨率下的显存妥协。这个两阶段流程在我做过的多个小数据集项目里,mAP50 通常能再涨 2 到 4 个点。

验证方法上,除了看 mAP,我习惯抽 20 张验证集图片做人工核对,统计漏检和误检的具体类别。指标是平均值,业务关心的是「红牛有没有被漏掉」。如果某一类召回明显低,优先补这一类的样本,而不是无差别加数据。最后说个习惯:每次训练前把data.yaml、命令、git commit 记在一个train_log.md里,一周后你还能复现出当时的结果。小数据集项目迭代快,没有记录,两周就忘了哪个参数对应哪次实验。希望帮到你。

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

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

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

立即咨询