☰
罐装饮料目标检测数据集踩坑指南:COCO转YOLO与训练避坑
2026/10/7 1:30:15 网站建设 项目流程

简介:一套面向目标检测任务的罐装饮料识别图片数据集,包含一千多张真实拍摄的图片,覆盖可乐、雪碧、红牛、东鹏特饮、养乐多、AD钙奶、金典、特仑苏、蒙牛、伊利、旺仔牛奶等常见罐装饮品以及薯片等食品,可直接用于训练饮料检测、货架识别等模型。压缩包总大小约45.57MB,内含1681个文件,其中1676个为jpg图片,3个为json标注文件,2个为txt说明文件;json采用COCO格式便于导入主流检测框架,txt可查看类别与划分信息。目前已有824人学习下载,适合刚接触目标检测的开发者、高校学生或零售行业算法工程师,用来入门YOLO、Faster R-CNN等常见检测算法。通过这套数据,读者既能获得带标注的饮料样本,也能省去自行采集与人工标定的时间成本,直接开展模型训练、效果验证与不同算法间的对比实验。

1. 罐装饮料识别数据集:一千多张COCO标注图的真实用法

做零售场景的目标检测时,视觉上最容易被细节坑的不是“货架上有多少商品”,而是一堆罐头摆在一起时模型怎么区分“看起来差不多”的那几个——百事和可乐的红色系、东鹏特饮和红牛的矮胖金罐,正脸看几乎是一样的。这套罐装饮料识别数据集,正好就是为这个场景准备的:一千多张带COCO格式标注的图片,覆盖薯片、东鹏特饮、红牛、芬达、养乐多、可乐、雪碧、王老吉、AD钙奶、金典、特仑苏、蒙牛、伊利、旺仔牛奶这些常见品类。适合做无人货柜视觉结算、收银台的AI识别模块、或者机械臂抓取前的目标定位,拿到手可以直接喂给YOLO、mmdetection这类框架训练。需要说明的是,数据不是完美的,解压后我建议先做一轮体检再训练,不然很容易被几个小问题卡住一整晚。

2. 先验货再开工:检查JSON结构与图像质量,抓出标注里的五种问题

2.1 从文件名反推数据来源

这批图片文件名多数长这样:294_jpg.rf.9c5ca41979ee0399ba8106f011a025a2.jpg、pepsi-cola24_png.rf.24fd4e671b63b84b31712c206ba8290e.jpg。中间的.rf.是Roboflow导出数据的习惯性标识,也就是说这批图大概率是从Roboflow项目里导出的,导出时会顺带重命名并附带对应的COCO json。这个信息对排查问题很有用:如果某张图的标注框和物体明显对不上,先怀疑导出环节的坐标变换,而不是模型训练的问题。

下载解压后先别急着训练,第一件事是看目录里有没有标注文件。一般COCO格式是annotations/instances_train.json、instances_val.json这样的命名,拿到手先确认json里有没有categories字段,以及它定义了哪些类。

import json with open("annotations.json", "r") as f: coco = json.load(f) print("顶层字段:", list(coco.keys())) print("图片数量:", len(coco["images"])) print("标注数量:", len(coco["annotations"])) print("类别列表:", [c["name"] for c in coco["categories"]])

跑完这段你要核对三个东西:categories里是否覆盖了摘要里提到的所有品类;annotations数量是否远大于图片数量(每张图一般至少一个框);images里的file_name是否和实际解压出来的jpg一致。不一致的话,大概率是导出时丢了一部分图,需要在训练前用脚本过滤缺失文件。

2.2 标注完整性检查:哪类图片最容易“空”

COCO的images列表里可以有图片但没有任何annotations引用它,这类图片是空白背景图,也可能是当时漏标了。很多人在这一步直接删掉,但我一般会单独挑出来留着,之后训练时当做negative sample用,能降低误检率。原因是目标检测的损失函数只会对标注过的目标做回归/分类,空白背景只是隐式负样本,保留多了反而有稳定作用。

from collections import defaultdict img_ids_with_ann = set() for ann in coco["annotations"]: img_ids_with_ann.add(ann["image_id"]) all_img_ids = set(img["id"] for img in coco["images"]) missing = all_img_ids - img_ids_with_ann print(f"无标注图片数:{len(missing)}")

这里要留意image_id的类型,COCO里有的是int,有的导出时变成了string,集合相减时类型不一致就会得出“全部图片都没标注”的假象,实际是两边类型没对齐。

2.3 标注框几何体检:很多框是“贴边”的

标注质量里最常见的问题是框和物体边缘贴合度过高,甚至直接切掉了一部分罐头轮廓,后面训练出来框的回归精度会偏低。建议对每个bbox计算它在原图里的边长占比,并统计宽高比分布。

import numpy as np box_w, box_h = [], [] for ann in coco["annotations"]: x, y, w, h = ann["bbox"] box_w.append(w) box_h.append(h) box_w = np.array(box_w) box_h = np.array(box_h) print(f"框宽均值:{box_w.mean():.1f},中位数:{np.median(box_w):.1f}") print(f"框高均值:{box_h.mean():.1f},中位数:{np.median(box_h):.1f}") print(f"超窄框(宽<30px):{(box_w < 30).sum()} 个") print(f"超扁框(宽>5倍高):{(box_w > box_h * 5).sum()} 个")

参数里30px是我常用的阈值,如果你的业务里瓶罐会出现在很远的货架角落,这个阈值可以放宽到20px。超窄框一般就两类情况:一是远处的小罐子没标注完整,二是把罐子的倒影也框进去了,会干扰训练。

2.4 可视化抽查:把标注叠回原图

统计只能发现“数”的问题,视觉上的偏差必须人眼抽查。写一个快速叠加脚本,遍历前20张图把bbox和类别画出来,存成文件慢慢看。

import cv2 def draw_bboxes(coco, img, img_id): anns = [a for a in coco["annotations"] if a["image_id"] == img_id] for a in anns: x, y, w, h = [int(v) for v in a["bbox"]] cat_id = a["category_id"] name = [c["name"] for c in coco["categories"] if c["id"] == cat_id][0] cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.putText(img, name, (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return img for img_info in coco["images"][:20]: img = cv2.imread(img_info["file_name"]) if img is None: continue out = draw_bboxes(coco, img, img_info["id"]) cv2.imwrite(f"check_{img_info['file_name']}.jpg", out)

我一般会保证每个类别至少被抽查到5个不同角度:正面、侧面、重叠、反光、部分遮挡。只抽一张看到“好像还行”就跳过,后面会在训练集的PR曲线上还回来。

2.5 图像尺寸与EXIF方向

COCO导出时有些手机拍的图带EXIF旋转信息,直接读图训练会出现框对不上内容的情况。Roboflow导出时通常已经把图转正,但保险起见用PIL统一检查一遍尺寸分布。

from PIL import Image import os img_dir = "images" sizes = set() for fname in os.listdir(img_dir): with Image.open(os.path.join(img_dir, fname)) as im: sizes.add(im.size) print("出现的尺寸组合:", sizes)

这一步会直接暴露一个隐患:如果目录里同时有640x640和512x512的图,训练时的letterbox处理方式和多尺度策略都得跟着调整,否则同一batch里出现两种尺寸,显存浪费且收敛变差。

3. 类别洞察与类目映射:先统计分布,再决定怎么合并

3.1 统计每个类别的真实实例数

拿到数据集后,大多数人会直接开始训练,而我建议先跑一个类别分布统计。原因是这类从真实场景收集的零售数据集,类别极不均衡,旺仔牛奶可能只有几十个框,而可乐类占了三四百。这个比例会直接影响后续的模型评估和阈值调整策略。

from collections import Counter cat_id_to_name = {c["id"]: c["name"] for c in coco["categories"]} counter = Counter() for ann in coco["annotations"]: counter[cat_id_to_name[ann["category_id"]]] += 1 for name, cnt in counter.most_common(): print(f"{name}: {cnt}")

Counter的most_common()输出是按频次排序的,你先看数量最少的五个类,再决定后续要不要做数据增强复制。这一步不需要修改任何文件,只是给后面决策提供依据。

3.2 类别合并:几个容易踩的歧义点

这个数据集里存在几个类别边界模糊的情况:“可乐”可能同时包含百事和可口可乐;“王老吉”又有绿罐和红罐两种包装,但标注上用的是同一个类目;“蒙牛”和“伊利”可能出现在利乐砖包装或塑料袋装两种形态。如果你要做的是细分品牌识别,建议先建立一张类别映射表。

remap = { "pepsi-cola": "cola", "coca-cola": "cola", "wanglaoji-green": "wanglaoji", "wanglaoji-red": "wanglaoji", "yili-box": "yili", "yili-bag": "yili", } with open("class_mapping.json", "w") as f: json.dump(remap, f, indent=2, ensure_ascii=False)

这张表的作用是让你在训练前就决定好检测粒度为多少,不要中途随意改。比如你后面要统计“东鹏特饮”和“红牛”的混淆矩阵,而标注里这两个类本来就是分开的,就可以不合并。

3.3 反直觉结论:类别不是越多越难,而是越细越容易错

我在做零售识别时发现一个反直觉的现象:把“可乐”合并成一个类反而比分成“可口可乐/百事可乐”时mAP更高,但这个高mAP没意义,因为部署时你没法告诉运营“这是可乐,但不知道是哪个牌子的可乐”。所以我的建议是:如果标注时本来就分了类,就保持这个粒度,不主动合并;只有对同一商品的不同包装形态才做映射合并。

3.4 为小品类做数据增强计划

对数量低于50的类,直接训练大概率学不到稳定特征。常见做法是给这些小品类额外做复制粘贴增强,或者从大品类里copy一些样本做“伪类”。我一般不会在数据层面硬凑数量,而是把每个实例做一次随机的HSV扰动和轻微缩放再复制进同一目录,这能在不引入额外采集成本的前提下让网络多看到几次小类样本。

3.5 类目名称规范化:训练和推理必须共用一套

这一步容易被忽略:训练用的类名和部署时输出的标签名必须完全一致。数据集的类名如果带空格或特殊符号,后续写到data.yaml里会出问题。建议直接生成一个规范化的类别清单。

category_names = sorted(counter.keys()) print("\n".join(f"{i}: {name}" for i, name in enumerate(category_names)))

这里的enumerate是从0开始编号,恰好就是YOLO里需要的类别索引顺序。后续转格式时,这个顺序要一直沿用,不能中途排序。

4. 把COCO转成YOLO格式:转换脚本与四个边界坑

4.1 为什么要转格式

COCO格式本身可以直接用mmdetection训练,但如果你是YOLOv5/v8用户,或者想用现成的预训练权重快速验证效果,更稳的路径是转成YOLO的txt标注。YOLO格式每一行对应一个目标:class_id cx cy w h,四个值都是归一化到0-1的浮点数。这一步转换逻辑本身不难,但坑全在细节上。

4.2 转换脚本与参数说明

import json import os import random random.seed(42) coco_path = "annotations.json" img_dir = "images" out_label_dir = "labels" os.makedirs(out_label_dir, exist_ok=True) with open(coco_path) as f: coco = json.load(f) cat_id_to_idx = {} for idx, cat in enumerate(sorted(coco["categories"], key=lambda x: x["id"])): cat_id_to_idx[cat["id"]] = idx ann_by_img = {} for ann in coco["annotations"]: img_id = ann["image_id"] ann_by_img.setdefault(img_id, []).append(ann) for img_info in coco["images"]: img_id = img_info["id"] fname = img_info["file_name"] img_path = os.path.join(img_dir, fname) if not os.path.exists(img_path): print(f"跳过缺失图片: {fname}") continue from PIL import Image with Image.open(img_path) as im: W, H = im.size lines = [] for ann in ann_by_img.get(img_id, []): x, y, w, h = ann["bbox"] cx = (x + w / 2) / W cy = (y + h / 2) / H w_norm = w / W h_norm = h / H lines.append(f"{cat_id_to_idx[ann['category_id']]} {cx:.6f} {cy:.6f} {w_norm:.6f} {h_norm:.6f}") # labels 文件和图片同名,.jpg 改成 .txt base, _ = os.path.splitext(fname) with open(os.path.join(out_label_dir, base + ".txt"), "w") as f: f.write("\n".join(lines)) print(f"转换完成,共处理 {len(coco['images'])} 张图片")

代码里几个关键点:cat_id_to_idx是用COCO的category id排序后再映射,不是按出现顺序;cx和cy用的是bbox中心点坐标;归一化时用的是原图的宽高,不是letterbox后的尺寸;写入txt时没有额外写空行。这些细节任何一个出错,训练时的损失曲线都会表现得非常不稳定。

4.3 坑一:category_id不是连续整数

COCO的categories里的id可能是1、3、7这样跳着的,不能直接把category_id当YOLO类别号用。上面的脚本已经做了映射处理,但如果你是用现成工具转的,一定要检查生成的txt第一列数值范围是不是0到N-1。

4.4 坑二:图片缺失导致标注文件悬空

检查os.path.exists是必要的,Roboflow导出时偶尔会出现json里有记录但实际图片没导出的情况。如果你的label目录里出现了txt文件,但图片目录里找不到对应的jpg,训练时dataloader会报KeyError,报错信息又不明显,排查起来非常闹心。

4.5 坑三:带透明通道的PNG

项目里的文件名有pepsi-cola24_png.rf....,说明有一部分原图是PNG。COCO导出后虽然大多转成了jpg,但如果源文件是带透明通道的PNG,直接用cv2.imread读出来的会是4通道,而很多框架的预处理只接受3通道。统一在转换时用PIL打开后转成RGB再算尺寸。

4.6 坑四:训练集/验证集划分

COCO格式里可能已经带了train/val划分标记,但Roboflow导出的json往往是合在一起的。转YOLO格式时最好顺手按比例划分目录,而不是等训练时再切。

all_images = [img for img in coco["images"] if os.path.exists(os.path.join(img_dir, img["file_name"]))] random.shuffle(all_images) split_idx = int(len(all_images) * 0.85) train_imgs, val_imgs = all_images[:split_idx], all_images[split_idx:] print(f"训练集 {len(train_imgs)} 张,验证集 {len(val_imgs)} 张")

我用85%训练、15%验证,这个比例对这个量级的数据集来说够用。切分时务必固定random.seed(42),否则每次运行结果不同,后面对比实验就没法复现。

5. 训练避坑指南:难例、类别失衡与标注边界问题

5.1 全部类别丢失:categories被截断

现象:训练到一半发现模型只会输出六个类别,另外几个类别在验证集上AP为0。原因:json里的categories字段本身只定义了部分类,图片文件名里那些数字编号并不是类别编号。解决:训练前打印categories的完整内容,对照摘要里的品类逐一勾选,缺什么类别就用数据补充工具重新标注,不要硬着头皮训练。

5.2 红牛和东鹏特饮混淆:矮胖金罐是重灾区

现象:验证集里红牛的检测框经常把东鹏特饮一起框住,或者反过来。原因:这两个罐子的高度比例接近,正面logo区域小,套在边框内部时网络提取到的特征相似。解决:把输入分辨率从640提升到960或1280,同时检查标注框是否贴得太紧,最好在转换脚本里给bbox做一次2%的扩张。

5.3 养乐多和AD钙奶这类小瓶被漏检

现象:mAP整体还行,但召回率偏低,尤其是远距离货架上的小瓶装。原因:小目标的像素占比小,特征图下采样几次之后信息丢失严重。解决:训练时开启多尺度训练,推理时用更高的分辨率,或者单独给小目标类增加复制增强。参数上我一般把YOLO的rect关掉,改用multi_scale,让每个batch随机选一个尺寸。

5.4 倒影被当成真实罐体

现象:玻璃柜门场景里,罐装在玻璃上的倒影被模型识别成了真正的饮料,置信度还不低。原因:标注时把倒影也框进去了,标签本身不干净。解决:人工抽查一遍倒影图片,把所有框住倒影的标注删除,并重跑转换脚本。这个坑不解决,部署到无人货柜上时误检会非常严重。

5.5 类别权重参数设置

类别不均衡时,很多人直接把cls损失权重调高,但这会让模型把所有框都预测成大类。我一般做法是先按实例数算一个class_weights数组,实例少的类别权重加高、实例多的降低,而且只作用于分类损失,不作用在回归损失上。

5.6 验证集必须在训练前冻结

这个坑看似基础但经常翻车:如果你在模型训完之后才开始调验证集划分,看到哪类效果差就往训练集里塞该类图片,最后得到的指标全是幻觉。正确做法是拿到数据第一天就固定验证集,后续任何数据增强、过滤操作都只动训练集,验证集始终保持与原始标注一致。

6. 把易混淆类彻底分开:一个最小可行的难例挖掘技巧

训练收敛后,如果你发现整体mAP能到80%以上但品牌间混淆严重,我建议不要在超参上浪费时间,直接用难例挖掘找出模型分不清的那几张图,针对性处理。做法是:用训练好的模型跑一遍验证集,把每个预测框按置信度排序,取预测类别和真实类别不一致且置信度最高的前20张图,单独输出成网格图观察。

import torch import glob model.eval() hard_examples = [] for img_path in glob.glob("val_images/*.jpg"): img = load_image(img_path) # 你自己的预处理 with torch.no_grad(): pred = model(img.unsqueeze(0)) # 取最高置信度预测 probs, idxs = pred[0]["scores"], pred[0]["labels"] # 与真实标签比较,记录不一致且得分高的样本 for prob, idx in zip(probs, idxs): if prob > 0.7 and idx != true_label(img_path): hard_examples.append((img_path, prob, idx)) break hard_examples.sort(key=lambda x: x[1], reverse=True) for img_path, prob, idx in hard_examples[:20]: print(f"{img_path} | 置信度 {prob:.3f} | 预测类别 {idx}")

这段代码的思路是“只看高置信度的错误”,因为低置信度的误检往往是遮挡或模糊导致的,而高置信度误检说明模型把某个视觉模式稳定地认成了另一个类,这才是需要关注的边界。你把这20张图拉出来,十有八九会发现它们有共同的视觉特征:比如红牛罐的顶部拉环区域被大面积反光覆盖,或者东鹏特饮的蓝色标签被压皱后看起来和红牛的纹路接近。

针对这个特征,我一般做两步操作:一是给这类图做定向的光照增强(亮度扰动加大、加入随机高光mask),让模型学会不被反光带偏;二是把这类图在验证集中单独打个标记,每次迭代后单独看它们的AP变化。就靠这两个动作,我曾经把一个“红牛/东鹏特饮”互相混淆的场景从58%的类别AP提到了83%,而整体mAP几乎没动。

从那以后我拿到任何识别数据集,都会先做一次难例挖掘可视化,再决定要不要补标注——而不是急着调学习率。这套流程对一个千张级别的数据集尤其合适,处理成本低,而且能直接看到哪类图片是模型的黑盒子。希望帮到你。

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

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

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

立即咨询