简介:这份罐装饮料识别数据集面向计算机视觉初学者、算法工程师与零售场景开发者,是一套可直接投入目标检测模型训练的标准数据集。包体共1681个文件,其中1676张真实场景jpg图片、3个json标注文件与2个txt说明文件,压缩包约45.57MB;图片覆盖薯片、东鹏特饮、红牛、芬达、养乐多、可乐、雪碧、王老吉、AD钙奶、金典、特仑苏、蒙牛、伊利、旺仔牛奶等多种常见罐装商品,标注采用COCO格式,方便接入YOLO、MMDetection等主流检测框架,也可用于教学实验或货架商品识别原型验证。对学习者而言,这套数据省去了自行采集、清洗与标注的时间,txt文件还能辅助完成类别映射和数据集划分,从预览看图像命名规范、场景分布较多样,适合迁移学习与小样本调优。已有823人学习下载,适合需要快速构建饮品识别模型的研究者和开发者。
1. 罐装饮料识别数据集:一千多张标注图能省掉你两三天标注时间
"罐装饮料识别"这个标题看着像普通图片包,实际是目标检测里最缺的东西:一份已经标注好的数据集。做过货架识别、无人售货柜、自动贩卖机视觉的人都会遇到同一个问题——模型好训,标注费人。一千多张图片手动画框,一个人得干两三天,还免不了画歪、漏画。这份数据集把东鹏特饮、红牛、芬达、养乐多、可乐、雪碧、王老吉,连常见薯片一起做了COCO格式的标记,拿到手可以直接转成YOLO训练格式,省掉的恰好是目标检测流程里最消耗人的一步。适合想快速验证饮料检测方案、又不想在标注上耗时间的开发者,也适合拿来做数据集处理练手。后面按我实际跑通的流程,从拆包检查、转YOLO格式、训练到避坑,一步步说清楚。
2. 先读懂COCO标记再动手:统计脚本与标注质量检查
拿到数据集第一件事不是解压就训,而是打开标记文件看结构。COCO格式的标记集中在一个JSON文件里,按images、annotations、categories三段组织。很多新手第一次打开会被层级绕晕,实际检测任务里只需要把三个字段搞清楚,再花十分钟检查标注一致性,能避免后面训练走弯路。
2.1 COCO标记的三段结构:image_id、bbox、category_id怎么关联
categories段定义"有哪些类别",每一行是一个类别的id和name。annotations段是核心,每一条对应一个目标框:bbox字段给出左上角x、左上角y、框宽w、框高h,单位是像素,没有归一化;category_id指到categories段,image_id指到images段。images段则给每张图片一个全局id和文件名。整个关系是:annotation->categories拿到类别名,annotation->images拿到图片路径。
这份数据集的标记文件打开后大致长这样:
{ "images": [ {"id": 1, "file_name": "dongpeng_001.jpg", "width": 640, "height": 480} ], "annotations": [ {"id": 10, "image_id": 1, "category_id": 2, "bbox": [120, 80, 180, 260], "area": 46800} ], "categories": [ {"id": 1, "name": "chips"}, {"id": 2, "name": "dongpeng"} ] }这里bbox是[x, y, width, height],不是中心点格式。很多人在这里踩坑:COCO的xywh是左上角加宽高,YOLO是中心点加宽高,转换时别直接套用。另外,目标检测只依赖bbox,segmentation和keypoints字段对训练没有影响,JSON里有没有都无所谓。我一般会顺手检查一下area字段是否等于w * h,如果不一致,说明标注工具在导出时可能对坐标做了缩放,后面必须警惕。
2.2 写统计脚本:每个类别到底标了多少框
在转格式之前,先统计类别分布。这一步能看出数据集有没有"偏科"——比如东鹏特饮标了500个框,养乐多只有30个框,那训练时养乐多大概率学不好。脚本很简单,核心就是遍历annotations,用Counter计数:
import json from collections import Counter with open('annotations.json', 'r', encoding='utf-8') as f: data = json.load(f) cat_names = {c['id']: c['name'] for c in data['categories']} cat_counter = Counter() img_counter = Counter() for ann in data['annotations']: cat_counter[cat_names[ann['category_id']]] += 1 img_counter[ann['image_id']] += 1 print('标注框总数:', len(data['annotations'])) print('图片总数:', len(data['images'])) print('类别分布:') for name, cnt in cat_counter.most_common(): print(f' {name}: {cnt}') print('平均每张图框数:', round(len(data['annotations']) / len(data['images']), 2))这个脚本的逻辑很简单:cat_names先建立category_id到类别名的映射,遍历data['annotations']时,用ann['category_id']查表得到类名,分别对类别和图片计数。most_common()按数量降序输出,方便一眼看出哪个类最多、哪个类最少。
对一千多张图片的数据集,我一般会这样看结果:类别框数如果能到两三百,训练基本够用;如果某个类别只有几十个框,后面要做针对性处理(见4.3节);平均每张图框数如果超过5,说明很多图片是密集摆放场景,如果只有1到2,说明多数图是单罐特写。两种分布对应的数据增强策略不一样,前者要防小目标漏检,后者要防过拟合。
2.3 可视化抽查:把bbox画回图片,别信标注文件的"完美"
统计脚本只能看数量,看不到质量。标注框画得松、画得偏、类别张冠李戴,这些统计数字都体现不出来。我的习惯是转换前先做一次可视化抽查,把标注框画回原图,随机看一两百个,确认框贴不贴罐体边缘。
import json import os from PIL import Image, ImageDraw ANNO_PATH = 'annotations.json' IMG_DIR = 'images' SAVE_DIR = 'check_bbox' os.makedirs(SAVE_DIR, exist_ok=True) with open(ANNO_PATH, 'r', encoding='utf-8') as f: data = json.load(f) id2info = {img['id']: img for img in data['images']} cat_id2name = {c['id']: c['name'] for c in data['categories']} # 抽查前 200 个标注框,按 annotation 的 id 顺序取 for ann in data['annotations'][:200]: img_info = id2info[ann['image_id']] img_path = os.path.join(IMG_DIR, img_info['file_name']) img = Image.open(img_path).convert('RGB') draw = ImageDraw.Draw(img) x, y, w, h = ann['bbox'] draw.rectangle([x, y, x + w, y + h], outline=(255, 0, 0), width=3) draw.text((x, y - 10), cat_id2name[ann['category_id']], fill=(255, 0, 0)) save_path = os.path.join(SAVE_DIR, f"ann_{ann['id']}.jpg") img.save(save_path) print('已导出到', SAVE_DIR, '共', len(data['annotations'][:200]), '张')运行后打开check_bbox目录,重点看两类问题:一是框是否明显比罐体大一圈或小一圈,二是类别名和实物是否对得上。罐装饮料里红牛、可乐、王老吉都是红罐,如果标注时把三者标混,训练出来的模型会在这三类上互相误检,后面想靠调参救回来基本是玄学。可视化抽查这一步花不了十分钟,但能帮你省下后面几轮无效训练的时间。数据集的图片规模和标注格式确认没问题后,再进入下一步转换。
3. 把COCO格式转成YOLO格式:转换脚本与三个关键参数
YOLO系列训练不认COCO的JSON,它要求每张图片对应一个同名txt文件,每一行是"类别号 中心点x 中心点y 宽 高",坐标全部归一化到0到1。转换这一步看着简单,但类别顺序、坐标归一化、越界钳制三个地方处理不好,训练时就会翻车。
3.1 为什么不能拿COCO的JSON直接训练
常见做法是把标注统一转成YOLO格式的txt,原因有两个。第一,COCO JSON把所有图片的标注堆在一个文件里,训练时每张图都要去大JSON里按image_id查一次,读取效率低;YOLO的txt按图片拆分,数据加载器直接读对应文件,快得多。第二,YOLO格式是目标检测领域的"通用货币",今天用YOLOv8,明天换YOLOv11,只要txt还在,数据不用重标。我平时用X-AnyLabeling或LabelImg标注完,导出也是先转成COCO或VOC,再统一转YOLO,中间步骤完全一致。
转换的核心逻辑就三步:按image_id把标注聚合到每张图,把bbox的左上角加宽高换算成中心点加宽高,再除以图片的真实宽高做归一化。下面这个脚本可以直接跑。
3.2 转换脚本主体:固定类别顺序是第一个关键点
import json import os from PIL import Image def main(): anno_json = 'annotations.json' img_dir = 'images' label_dir = 'labels' os.makedirs(label_dir, exist_ok=True) with open(anno_json, 'r', encoding='utf-8') as f: data = json.load(f) # 固定类别顺序:按 categories 里的 id 升序排列 cats = sorted(data['categories'], key=lambda c: c['id']) cat_id2cls = {c['id']: idx for idx, c in enumerate(cats)} print('类别顺序(写 yaml 时按这个顺序):') for idx, c in enumerate(cats): print(f' {idx}: {c["name"]}') img_id2info = {img['id']: img for img in data['images']} # 按 image_id 聚合标注 anns_by_img = {} for ann in data['annotations']: anns_by_img.setdefault(ann['image_id'], []).append(ann) for img_id, anns in anns_by_img.items(): img_info = img_id2info[img_id] file_name = img_info['file_name'] # 用 PIL 读真实宽高,不要直接用 json 里的 width/height img = Image.open(os.path.join(img_dir, file_name)) img_w, img_h = img.size txt_path = os.path.join(label_dir, os.path.splitext(file_name)[0] + '.txt') with open(txt_path, 'w', encoding='utf-8') as f: for ann in anns: x, y, w, h = ann['bbox'] cls_id = cat_id2cls[ann['category_id']] # 左上角 + 宽高 -> 中心点 + 宽高,再归一化 cx = (x + w / 2) / img_w cy = (y + h / 2) / img_h nw = w / img_w nh = h / img_h # 越界钳制:标注框偶尔会画出图片边缘一点 cx = max(0.0, min(1.0, cx)) cy = max(0.0, min(1.0, cy)) nw = max(0.0, min(1.0, nw)) nh = max(0.0, min(1.0, nh)) f.write(f'{cls_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n') print('转换完成,生成', len(anns_by_img), '个 txt 文件') if __name__ == '__main__': main()这段代码有两个细节值得说明。第一,cat_id2cls是拿categories按id排序后映射的,这样数据集原始的类别编号不管从0还是从1开始,转换后都按顺序归位,后面写yaml的names时直接照打印结果抄,不会错位。第二,归一化除的宽高是用PIL实际读出来的,不是JSON里写的值,这一点在后面避坑章节会展开,很多标注文件里的width和height实际和图片像素不一致。
还要注意bbox里可能出现宽高为0的空框,转换前最好加一层过滤:if w <= 0 or h <= 0: continue。空框一旦写进txt,训练时会产生NaN Loss。这个过滤我习惯加在cx = ...之前,遇到无效框直接跳过这一条标注。
3.3 划分train/val:按图片划分,随机种子要固定
转出来的txt都堆在labels目录,下一步按图片划分训练集和验证集。常见比例是8:2或9:1,一千多张图建议8:2,验证集能留出两百多张,评估结果更稳。划分时注意:图片和对应的txt必须一起移动,而且按图片划分,不能按标注框划分,否则同一张图的多个框会分散到两个集合里,造成数据泄漏。
import os import random import shutil random.seed(42) IMG_DIR = 'images' LBL_DIR = 'labels' TRAIN_IMG = 'images/train' VAL_IMG = 'images/val' TRAIN_LBL = 'labels/train' VAL_LBL = 'labels/val' for d in [TRAIN_IMG, VAL_IMG, TRAIN_LBL, VAL_LBL]: os.makedirs(d, exist_ok=True) all_imgs = [f for f in os.listdir(IMG_DIR) if f.lower().endswith(('.jpg', '.jpeg', '.png'))] random.shuffle(all_imgs) val_cnt = int(len(all_imgs) * 0.2) val_imgs = all_imgs[:val_cnt] train_imgs = all_imgs[val_cnt:] for img in train_imgs: shutil.move(os.path.join(IMG_DIR, img), os.path.join(TRAIN_IMG, img)) lbl = os.path.splitext(img)[0] + '.txt' if os.path.exists(os.path.join(LBL_DIR, lbl)): shutil.move(os.path.join(LBL_DIR, lbl), os.path.join(TRAIN_LBL, lbl)) for img in val_imgs: shutil.move(os.path.join(IMG_DIR, img), os.path.join(VAL_IMG, img)) lbl = os.path.splitext(img)[0] + '.txt' if os.path.exists(os.path.join(LBL_DIR, lbl)): shutil.move(os.path.join(LBL_DIR, lbl), os.path.join(VAL_LBL, lbl)) print('train 图片数:', len(train_imgs), 'val 图片数:', len(val_imgs))random.seed(42)这行别省。不固定种子,每次运行划分结果不同,后面调参时训练集换来换去,模型效果波动你分不清是参数引起的还是数据划分引起的,排查起来极其痛苦。划分完看一眼labels/train和labels/val里的txt数量是否和图片数量对应,不一致就说明有图片没标注,这种图要单独挑出来,别混进训练集。无标注图片在YOLOv8里会被当成背景样本,少量没问题,多了会拉低召回。
4. 用YOLOv8训练罐装饮料识别:数据yaml与类别不均衡处理
数据集格式转好、目录分好之后,训练本身的工程量不大,但参数设置有几个针对小数据集的讲究。用YOLOv8训练自己的数据集,很多人的第一反应是直接跑默认参数,结果一千多张图训练完mAP虚高,一到真实货架就露馅。这一章把数据yaml和关键参数一次说清。
4.1 组织目录结构并写beverage.yaml
YOLOv8用yaml文件描述数据集,路径、训练集、验证集、类别名都在里面。目录结构按Ultralytics的约定来,images和labels必须同级,且子目录名要一一对应。
beverage_data/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── beverage.yaml对应的yaml文件长这样:
# beverage.yaml path: /home/user/beverage_data # 改成自己的绝对路径 train: images/train val: images/val names: 0: chips 1: dongpeng 2: hongniu 3: fenda 4: yangleduo 5: cola 6: sprite 7: wanglaojinames后面的索引必须和3.2节转换脚本打印出来的类别顺序完全一致,一个位置都不能错。这里最容易出问题:如果你自己重新排了类别,忘了同步改yaml,模型训练时会把类别A的标注当成类别B来学,Loss看起来正常,实际学了个错乱映射。检查方法很简单,训练前打印一下data['train_labels']里每类的数量,和2.2节的统计对一下,数量对不上就是映射错位。
4.2 训练命令与三个关键参数
目录和数据yaml就绪后,训练命令很短:
yolo detect train \ data=beverage.yaml \ model=yolov8s.pt \ epochs=120 \ imgsz=640 \ batch=16 \ patience=20几个参数按这个思路调:
| 参数 | 建议值 | 说明 |
|---|---|---|
| model | yolov8s.pt | 一千多张图属于小数据量,用s起步,不要直接上x,容易过拟合 |
| imgsz | 640 | 罐装饮料是中尺寸目标,640够用,调大反而拖慢训练 |
| epochs | 120 | 小数据集训练收敛快,100到150足够 |
| batch | 16 | 显存不够降到8,不要为了凑batch牺牲图片尺寸 |
| patience | 20 | 连续20轮验证集不提升就停,防止后期过拟合 |
还有一个容易被忽略的隐藏参数是mosaic。YOLOv8默认开启马赛克增强,对密集摆放的罐装饮料很友好,但训练后期马赛克会把多个罐子拼在一起,导致小目标上下文混乱,我一般保持默认的1.0,只在最后一轮epoch里关闭,或者让模型微调时用mosaic=0.0。数据增强里hsv_s和hsv_v值得调大一点,罐装饮料的表面反光很严重,增强色彩饱和度能提升模型对光照变化的鲁棒性。
4.3 类别不均衡:先看分布再决定怎么调
小数据集几乎必然遇到类别不均衡。按2.2节的统计结果,如果最少的类别(通常是养乐多或某款小众饮品)框数不到最多类别的十分之一,训练时模型会倾向于牺牲小众类别来换取整体Loss下降。
处理方案按优先级排,第一个是复制粘贴增强,把稀有类别的目标实例随机复制到其他图片上,配合YOLOv8的copy_paste参数,对小数据集非常有效;第二个是过采样,训练时对包含稀有类别的图片提高采样权重,实现方式是在数据加载时给稀有类图片重复读几次;第三个是直接检查是否漏标——很多情况下"类别不均衡"其实是标注遗漏,统计显示某类只有40个框,打开图片看看可能发现场景里明明有大量该品类没画框,补标比调参靠谱得多。
我一般先做第三项检查,再决定要不要上增强。漏标导致的不均衡,再怎么调参都是治标不治本。
5. 罐装饮料检测避坑记录:从解压到上线的5个真实问题
这一章写的是我在类似数据集上踩过的坑。每条都是先现象、再原因、后解决,按处理顺序排下来,从解压到推理全覆盖。
5.1 中文路径引发的训练崩溃
现象:在Windows上解压数据集后直接训练,yolo detect train跑到数据加载阶段就报UnicodeDecodeError或FileNotFoundError,但图片明明就在那里。
原因:数据集压缩包里的文件名带中文或特殊字符(比如"东鹏特饮_001.jpg"),Ultralytics内部用glob遍历后传给OpenCV读取,Windows下的编码处理和Linux不一致,中文路径直接被卡掉。这不是模型问题,是路径编码问题。
解决:解压后第一时间统一改名。把所有图片和目录改成纯英文数字,比如beverage_data/images/0001.jpg。改名脚本用os.rename循环处理即可,重点是把images、labels、annotations.json都放在不含中文的路径下。这个坑在Windows尤其常见,Linux服务器上一般遇不到,但如果你在本地调试过再去服务器跑,两边路径不一致还会翻车。
5.2 坐标被JSON里的宽高带偏
现象:转换脚本跑完,训练无报错,Loss也正常下降,但可视化发现所有框都偏到图片右下角,偏移量还不固定。
原因:COCO JSON里images段的width和height与实际图片像素不一致。常见来源是手机拍摄的图片带EXIF旋转信息,标注工具读取时按旋转前分辨率写入JSON,PIL实际打开图片时会自动按EXIF旋转,宽高就对不上了。归一化时除以了JSON里的错误宽高,坐标自然偏移。
解决:转换脚本里统一用PIL实际读取图片大小,不要用json里的width和height,3.2节的脚本已经这么处理。如果不确定当前数据集有没有这个问题,跑一遍脚本后随机挑几个txt,反算回像素坐标和原图对照,五分钟能确认。
5.3 红牛、可乐、王老吉互相误检
现象:训练完成后验证集mAP整体不差,但单独看混淆矩阵,红牛、可乐、王老吉三类互相误检率很高,红罐类之间尤其严重。
原因:这三类都是红色易拉罐,罐形接近,颜色特征高度重合。数据集中如果拍摄环境单一(比如都是白炽灯下的货架),模型学到的颜色特征会过拟合到当前光照,换个环境红罐全混。这是视觉上"长得像"造成的模型混淆,不是代码Bug。
解决:训练时把HSV增强里的hsv_s和hsv_v适当调大,让模型不能只靠颜色区分;推理时conf阈值从默认0.25提高到0.35,过滤掉那些介于两类之间的低置信度框。如果条件允许,补拍不同光照、不同角度的红罐类图片,比任何调参都更治本。注意不要指望提高epochs能解决,数据本身的问题训练再久也学不会。
5.4 训练Loss低但验证mAP上不去
现象:训练集Loss收敛到0.05以下,验证集mAP50只有0.6,涨不动,也不掉。
原因:标注框画得太松,把罐体周围的背景也包进去了。模型学到了"罐子+一圈背景"的组合特征,验证集里标注框画得紧,背景特征对不上,mAP自然上不去。这是标注质量问题,数据集的标注是别人做的,松紧标准未必和你一致。
解决:用2.3节的可视化脚本重新抽查,把框画回原图看贴合度。如果大量框比罐体大一圈,需要对这部分图片重新标注,或者用脚本把框按比例内缩。内缩有风险,罐子边缘的拉环、高光会被切掉一部分,推荐内缩10%以内,然后重新转一遍YOLO格式再训练。数据集的标注大概率不会故意画松,通常是个别类别的标注标准不统一,重点查标注数量最多的那个类。
5.5 推理时重复框和检测框抖动
现象:视频流推理时一个罐子被两个框同时框住,或者同一个罐子的框忽大忽小,置信度都在0.3到0.5之间波动。
原因:NMS阈值设置不合理。罐装饮料表面反光强,边缘在图像上表现为多条高对比度线条,模型在罐体边缘附近产生多个候选框,iou阈值太低时这些框不会被合并,同一个目标就出现了重复框。
解决:推理参数调整为conf=0.35, iou=0.7,高置信度低阈值先过滤松框,再用IoU合并重叠框。如果视频场景还有抖动,加一个简单的帧间位置平滑,用上一帧的框中心点和当前帧做加权平均,比直接上跟踪器简单。记住推理参数和训练参数是两回事,别把iou=0.7写进训练配置里。
6. 验证模型效果的两个技巧:混淆矩阵与高宽比过滤
训练完别急着部署,先用YOLO自带的验证命令导出混淆矩阵,再用一个简单脚本过滤明显误检。这两个技巧花不了半小时,能让你对模型的实际能力有个准确判断。
6.1 用val模式导出混淆矩阵
训练完成后跑一次验证,Ultralytics会自动生成confusion_matrix.png:
yolo detect val data=beverage.yaml model=runs/detect/train/weights/best.pt打开混淆矩阵图,重点看对角线之外哪两个类互混最严重。如果是红牛和可乐互混,说明颜色特征主导了分类决策;如果是薯片被误检成某饮料,说明框的形状特征学习不到位。这一步能直接指导要不要补数据、要不要调增强参数,比盯着mAP数字猜有效得多。
6.2 批量推理脚本:固定置信度阈值,用高宽比过滤误检
对固定摆放场景(桌面上依次排开的罐装饮料),可以给推理加一道后处理过滤——用目标框的高宽比做筛选。罐装饮料的形态相对固定,红牛、可乐这类矮罐高宽比接近1,东鹏特饮、王老吉这类细罐在1.6到2.2之间,薯片桶更高一点。如果检测框的高宽比明显超出已知范围,大概率是背景误检。
from ultralytics import YOLO import cv2 model = YOLO('runs/detect/train/weights/best.pt') cap = cv2.VideoCapture('test.mp4') while True: ret, frame = cap.read() if not ret: break results = model.predict(frame, conf=0.35, iou=0.7, verbose=False) for r in results: boxes = r.boxes.xywh.cpu().numpy() clss = r.boxes.cls.cpu().numpy() for box, cls_id in zip(boxes, clss): w, h = box[2], box[3] ratio = h / w # 罐装饮料高宽比一般在 0.8 ~ 2.5 之间,超过 3.5 的先滤掉 if ratio > 3.5: continue # 正常画框逻辑,按需要保存或显示 x_center, y_center = box[0], box[1] x1 = int(x_center - w / 2) y1 = int(y_center - h / 2) x2 = int(x_center + w / 2) y2 = int(y_center + h / 2) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imshow('frame', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()高宽比过滤是最后一个后悔药,它不是通用方法,只能用在摆放姿势相对固定的场景。如果检测对象是随手扔在桌上的罐子,角度变化导致高宽比波动很大,这个阈值就不要卡死。我自己的习惯是先用一个宽松的阈值过滤ratio > 5.0的极端误检,再通过混淆矩阵去定位真正的混类问题,而不是依赖后处理硬扛。
第一版模型翻车通常就翻在标注质量上,我在这类数据集上吃过一次亏:统计脚本显示类别分布正常,没有可视化抽查就直接转格式训练,后来发现一部分标注框把两个相邻罐子框成了一个,模型整个学歪。从那次之后,我拿到任何标注数据集,第一件事永远是统计加可视化抽查,这一步省下的训练时间远比花掉的多。希望帮到你。
本文还有配套的精品资源,点击获取