☰
罐装饮料识别实战:COCO标注转YOLO训练与避坑指南
2026/10/7 20:56:42 网站建设 项目流程

简介:面向目标检测与图像识别初学者及算法工程师,这套罐装饮料识别数据集汇集了一千多张实拍图片,覆盖薯片、东鹏特饮、红牛、芬达、养乐多、可乐、雪碧、王老吉、AD钙奶、金典、特仑苏、蒙牛、伊利、旺仔牛奶等十余种常见商品,标注采用通用的COCO格式,可直接接入主流检测框架进行训练与验证。压缩包共含1681个文件,其中1676张JPG图片组成训练用图像库,3个JSON文件保存目标框和类别标注,另有2个TXT用于类别映射或数据划分,整体体积仅45.57MB,轻量易用。目前已有824人学习下载,非常适合商品识别、货架陈列分析、零售结算等场景的算法验证与课程设计。借助这份数据,读者能够跳过繁琐的采集和标注环节,快速完成数据划分、格式转换、模型调参与评估;同时多品牌罐装饮品的多角度、多光照样本,也有助于提升模型在真实货架环境下的泛化能力,是一份实用且易上手的目标检测训练资源。

1. 罐装饮料识别离不开数据:1000多张COCO格式图片能解决什么

第一次做罐装饮料识别的工程师,往往不是被模型难倒,而是被标注格式和类别不一致拖住。这套约一千多张图片的COCO格式标记包,覆盖薯片、东鹏特饮、红牛、芬达、养乐多、可乐、雪碧、王老吉八类目标,基本就是智能货柜、无人售货机和视觉结算台上最常见的SKU组合。拿到数据后直接开训是常见误区,我一般会先花半小时把COCO标注的images、annotations、categories三张表理清楚,确认类别索引、坐标原点和样本分布,因为后面所有训练和部署动作都建立在标注结构之上。

对刚接触目标检测的人,这份数据的好处是格式标准,能直接用pycocotools读取,也能转成YOLO系列训练的txt标注;对已经跑过检测流程的老手,它更像个易混淆罐体的试验场——红牛和东鹏特饮都是金罐,芬达和可乐又都是深色罐,类间差异远比想象中小。这篇文章从JSON结构讲起,到可视化验证、格式转换、训练参数,最后落到避坑和真实场景验证,按这条路径走完,你就能把这个数据集变成一套可上货架的检测方案。

2. COCO标注文件怎么读:三张表对齐与bbox坐标换算

2.1 一张标准标注JSON里到底有哪些字段

COCO格式的核心不是某个文件夹结构,而是instances_train.json这种标注文件。第一次接触时不要急着看图片,先把JSON里的三类数组对齐:images记录每张图的id和宽高,annotations记录每个目标的框和类别,categories记录类别字典。三者靠id关联,不是靠文件名关联。

{ "images": [ { "id": 1, "file_name": "IMG_0001.jpg", "width": 1920, "height": 1080, "license": 0 } ], "annotations": [ { "id": 1001, "image_id": 1, "category_id": 3, "bbox": [702.3, 415.8, 188.4, 246.2], "area": 46390.4, "iscrowd": 0, "segmentation": [] } ], "categories": [ {"id": 1, "name": "chips", "supercategory": "snack"}, {"id": 2, "name": "dongpeng", "supercategory": "drink"}, {"id": 3, "name": "redbull", "supercategory": "drink"}, {"id": 4, "name": "fanta", "supercategory": "drink"}, {"id": 5, "name": "yangleduo", "supercategory": "drink"}, {"id": 6, "name": "cola", "supercategory": "drink"}, {"id": 7, "name": "sprite", "supercategory": "drink"}, {"id": 8, "name": "wanglaoji", "supercategory": "drink"} ] }

这份JSON里,annotation的image_id指着images.id,category_id指着categories.id。很多人改标注时只改名字不改id,或者复制粘贴时把image_id写错,结果某张图凭空多出几个框。加载后先做一次对齐抽查,能省出一整天的排错时间。

bbox字段是[x, y, width, height],x和y是框左上角坐标,宽高是像素值,不是归一化值。COCO里允许浮点坐标,手工标注工具通常输出整数,但不要因此假设所有数据都是整数。area表示目标面积,一般等于bbox面积;如果segmentation是多边形,area应等于多边形面积。对罐装饮料检测来说,segmentation为空数组不影响训练,因为YOLO系只吃bbox。但要注意pycocotools在评估mAP时会按area把目标分成small、medium、large三档,area填错会让这个分层统计完全失真。

我一般会额外做一步:写个小脚本统计bbox宽高与图宽高的比值,看是否存在负值、零宽高或超出边界的框。这类脏标注不会让训练报错,但会悄悄拉低精度,越到后期越难定位。

2.2 类别id和类别名:为什么“雪碧”不建议直接写中文

categories里的name,官方COCO数据集常用英文小写,比如bottle、cup。对于这份中文场景的数据,常见做法是掐头去尾,把中文映射成ASCII短名:chips对应薯片,dongpeng对应东鹏特饮,redbull对应红牛,sprite对应雪碧。原因很现实:pycocotools和Python都能处理中文字符串,但一旦走到C++部署、ONNX导出或嵌入式端NCNN/TensorRT,中英文混排的names很容易变成乱码,或者索引错位。

很多团队在训练时用中文名没问题,转ONNX时发现输出层的80类名字全是乱码,再回头改数据集,等于重新训一遍。避免这种翻车的方式,是在进入训练前就固定一份label_map.json:

{ "1": "chips", "2": "dongpeng", "3": "redbull", "4": "fanta", "5": "yangleduo", "6": "cola", "7": "sprite", "8": "wanglaoji" }

这份映射文件后续会对应data.yaml里的names顺序,也会对应模型输出的类别索引。需要记住的是,COCO的category_id从1开始,YOLO的类别索引从0开始,转换时减1是常见操作;但如果初始JSON里category_id是从0开始,再减1就会把所有类别错位。所以进入转换前,先打印一次类别列表,眼见为实。

另一个容易踩的点是同类不同包装。东鹏特饮既有金罐,也有PET塑料瓶,外包装颜色相近但形态差异很大;养乐多也有小红瓶和大瓶装。如果标注时全都归为dongpeng,模型会试图用一个特征描述两种形态,误检概率上升。我的建议是先确认数据包里同类是否混装,如果混装严重,要么把同类的不同规格拆成子类,要么在训练集中刻意均衡两种形态的样本量。

3. 用pycocotools验证罐装饮料标注质量:三个脚本看清家底

3.1 最小加载脚本,先确认能读再谈训练

pycocotools是COCO标注最常用的读取库,它不只是个解析器,还内置了按image_id、category_id索引的查询接口。相比之下,直接用json.load去读再手工建映射,要处理很多边界情况,比如某张图没有标注、某个image_id被重复定义。

pip install pycocotools opencv-python matplotlib

装好后写个最小验证脚本:

from pycocotools.coco import COCO ann_file = "annotations/instances_train.json" coco = COCO(ann_file) # 打印类别表,确认category_id和name的对应关系 cat_ids = coco.getCatIds() cats = coco.loadCats(cat_ids) print("categories:", [(c["id"], c["name"]) for c in cats]) # 图片总量 img_ids = coco.getImgIds() print("images:", len(img_ids)) # 标注总量,注意getAnnIds要传入完整的image_id列表 ann_ids = coco.getAnnIds(imgIds=img_ids) anns = coco.loadAnns(ann_ids) print("annotations:", len(anns))

getAnnIds的imgIds参数是一个列表。如果传空列表,它返回空;如果只传一张图的id列表,就只返回那张图的标注。很多人在初学时把imgIds当成前缀id或过滤条件,传了单个数字进去,结果统计数量少了一大截。正确写法是按上面代码传入全部img_ids,或传None表示全量。

加载正常后,再抽查前几张图,看每张图是否都有标注:

for img_id in img_ids[:10]: img = coco.loadImgs([img_id])[0] ann_ids = coco.getAnnIds(imgIds=img_id) anns = coco.loadAnns(ann_ids) print(img["file_name"], "anns:", len(anns))

这一步能快速暴露两类问题:某些图文件名存在但图片文件缺失,或某些图标注为空。对检测任务来说,一些空图可以保留,它们能充当负样本抑制误检,但空图占比过高说明标注遗漏,要回到原数据确认。

3.2 可视化眼检,把bbox画回原图

数字统计看不出标注质量。罐装饮料的标注,最常遇到的问题是框不贴边、框住了相邻商品、类别标错。肉眼扫一遍比任何自动化指标都有效。

import cv2 import random from pathlib import Path from pycocotools.coco import COCO coco = COCO("annotations/instances_train.json") cat_id2name = {c["id"]: c["name"] for c in coco.loadCats(coco.getCatIds())} def draw_bboxes(coco, img_id, img_root, out_root="check"): info = coco.loadImgs([img_id])[0] img = cv2.imread(str(Path(img_root) / info["file_name"])) if img is None: print("missing file:", info["file_name"]) return for ann in coco.loadAnns(coco.getAnnIds(imgIds=img_id)): x, y, w, h = [int(round(v)) for v in ann["bbox"]] name = cat_id2name[ann["category_id"]] color = (0, 255, 0) cv2.rectangle(img, (x, y), (x + w, y + h), color, 2) cv2.putText(img, name, (x, max(0, y - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) out = Path(out_root) out.mkdir(exist_ok=True) cv2.imwrite(str(out / f"{img_id:06d}.jpg"), img) sample_ids = random.sample(img_ids, min(200, len(img_ids))) for img_id in sample_ids: draw_bboxes(coco, img_id, "images")

这段脚本把每张抽样图的bbox和类名画出来后写到check目录。不要纯随机抽,因为每类样本量不均,可能200张图里一半是可乐。我一般会按category分组,每类抽20~30张,保证红牛和东鹏这种易混淆类目也被覆盖到。

眼检时重点盯三个位置:罐体边缘是否留白过大,饮料瓶之间遮挡时框是否把两个目标圈在一起,以及标注名和实物是否一致。框稍微偏几像素问题不大,但框住两个目标会让训练产生“这个框里同时有红牛和东鹏”的错误监督信号。

3.3 按类统计实例数,判断训练值不值得投

可视化之后,做一次实例级统计。罐装饮料识别的难点不在总量,而在每个类别分到多少张图。如果某个类只有几十个实例,训练时会被大类别稀释,最后变成“多数类学了个大概,少数类被无视”。

from collections import Counter from pycocotools.coco import COCO coco = COCO("annotations/instances_train.json") anns = coco.loadAnns(coco.getAnnIds()) cat_id2name = {c["id"]: c["name"] for c in coco.loadCats(coco.getCatIds())} counter = Counter() for ann in anns: counter[cat_id2name[ann["category_id"]]] += 1 total = sum(counter.values()) print("total instances:", total) for name, cnt in counter.most_common(): print(f"{name:12s} {cnt:5d} {cnt/total:.1%}")

统计结果出来后,整理成一张表帮助决策:

指标作用注意点
图片总数决定训练epoch和batch的规模1000张左右不建议epoch超过200
标注实例总数反映模型能看到多少样本每类低于200时误检风险明显升高
单图目标数判断是否有密集场景单图超过20个框要查是否重复标注
bbox面积占比判断小目标比例养乐多这类小罐占比低时,需调高imgsz

这些统计不复杂,但能提前决定后续策略。比如发现可乐的样本量是养乐多的十倍,我会先做两件事:一是确认验证集里的类别分布不要差太远,二是考虑给养乐多做过采样,或者接受它精度偏低的事实。明知数据不平衡还硬训,效果只会更差。

4. 从COCO标记转成YOLO训练格式:txt生成与数据划分

4.1 COCO bbox转YOLO txt的坐标换算

YOLO训练不认COCO的JSON,它读取每张图对应的同名txt文件,每行是一个目标,格式为“类别索引 中心点x 中心点y 宽 高”。四个坐标都归一化到0~1。转换脚本是整个流程里最容易写错的地方,因为坐标原点和中心点换算差一步,检测框就会偏。

import json from pathlib import Path def coco_to_yolo(json_file, img_root, out_root): with open(json_file, "r", encoding="utf-8") as f: data = json.load(f) images = {img["id"]: img for img in data["images"]} ann_by_img = {} for ann in data["annotations"]: ann_by_img.setdefault(ann["image_id"], []).append(ann) out_path = Path(out_root) out_path.mkdir(parents=True, exist_ok=True) for img_id, img in images.items(): w, h = img["width"], img["height"] lines = [] for ann in ann_by_img.get(img_id, []): x, y, bw, bh = ann["bbox"] # COCO的框是左上角+宽高,YOLO要的是中心点+宽高,全部除以图宽高 cx = (x + bw / 2) / w cy = (y + bh / 2) / h nw = bw / w nh = bh / h # COCO类别id从1开始,YOLO索引从0开始 cls_id = ann["category_id"] - 1 lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}") stem = Path(img["file_name"]).stem (out_path / f"{stem}.txt").write_text("\n".join(lines), encoding="utf-8")

这段代码里,最关键的三个点是中心点换算、归一化除宽高、类别id减一。COCO的bbox是左上角坐标,YOLO要求中心点坐标,所以x要加一半的宽;图宽高在images数组里,不要从文件名去读图片算尺寸,那样慢且容易因图片损坏而中断。类别id减一的前提是初始JSON的category_id从1开始,如果数据源从0开始,这个减一会让所有标签错位。

转换完成后,抽查三个txt文件,人工核对第一行的归一化坐标乘回图宽高后是否与JSON里的bbox一致。这是最直接的验证方式,几分钟就能堵住低级错误。

4.2 训练集和验证集划分:按图划分,不是按标注行划分

目标检测的数据划分,最容易犯的错是把同一张图上不同目标的标注行分到训练和验证两个集合。这样模型在训练时已经见过这张图的像素,验证时再拿同一个图的其他框测试,mAP虚高,换到真实货架场景立刻现原形。正确做法是先对图像文件随机打散,再整体划分。

import random import shutil from pathlib import Path random.seed(42) image_files = sorted(Path("images").glob("*.jpg")) random.shuffle(image_files) val_ratio = 0.2 val_n = int(len(image_files) * val_ratio) val_files = image_files[:val_n] train_files = image_files[val_n:] for split, files in [("train", train_files), ("val", val_files)]: img_dir = Path("dataset/images") / split lab_dir = Path("dataset/labels") / split img_dir.mkdir(parents=True, exist_ok=True) lab_dir.mkdir(parents=True, exist_ok=True) for img_file in files: shutil.copy(img_file, img_dir / img_file.name) txt_file = Path("yolo_labels") / (img_file.stem + ".txt") if txt_file.exists(): shutil.copy(txt_file, lab_dir / txt_file.name)

这段脚本按8:2划分,固定随机种子让结果可复现。划分后应该check一下每个类别在train和val中的实例分布。有时候某个类是少数类,随机打散后可能全部跑进训练集,验证集一个样本都没有,模型在这个类上的精度无法评估。我一般会做分层抽样:先按类别把包含该类目标的图像分组,确保每类至少留出几张到验证集,再填充剩余比例。

还要注意文件名不能有重复,不同目录下的同名图会被覆盖。如果jpg和txt的stem对不上,训练时会报“image without labels”或“label not found”。这类错误在Ultralytics框架里通常不会中断进程,而是在日志里打一长串警告,容易被忽略。

4.3 小数据量的训练参数:yaml、imgsz和预训练权重怎么定

一千多张图片对检测模型来说属于小数据集,但八类罐装饮料不是极其稀疏的目标,yolov8s起步比较合适,不建议一上来就上yolov8x。模型大了容易过拟合,训练时间翻倍,最终精度不见得更好。先建一份data.yaml:

path: /data/canned_drink train: images/train val: images/val nc: 8 names: 0: chips 1: dongpeng 2: redbull 3: fanta 4: yangleduo 5: cola 6: sprite 7: wanglaoji

names的顺序必须和转换脚本里的类别索引完全一致,这是训练框架唯一的类别依据。训练命令我一般这样起:

yolo detect train \ data=data.yaml \ model=yolov8s.pt \ epochs=120 \ imgsz=640 \ batch=16 \ patience=30 \ seed=0

model=yolov8s.pt意思是加载COCO预训练权重,然后在灌装饮料数据上微调。预训练权重从通用目标检测迁移过来的效果,通常比从随机参数开始训练省事得多,尤其对1000张这种规模。imgsz=640是平衡速度和精度的常见选择。如果养乐多这类小目标占比低,不要为了显存把imgsz降到416,否则小目标直接被压缩到十几个像素,检测器无从下手。batch大小要看显存,16不够就降到8,但一般不要低于4,否则BN层的统计量会抖。

数据增强方面,Ultralytics默认会开mosaic和HSV扰动。对罐装饮料来说,HSV扰动很有用,因为不同灯光下可乐罐的红、王老吉的红存在色差;mosaic对小数据集能增加上下文多样性,但mosaic拼接出的方形图会让饮料罐比例变形,如果发现训练后期损失降不下去,可以关掉mosaic再看。

5. 罐装饮料识别落地避坑:5个高发问题与排查路径

5.1 红牛和东鹏特饮互相误检,金罐是硬骨头

现象:整体mAP看着能接受,但红牛和东鹏特饮在验证集上互相认错,有时红牛框里贴着东鹏标签。原因:两者都是金罐,罐体大小和轮廓几乎一致,模型只能靠罐身中段的品牌印刷区分;如果训练图中罐体较远,品牌字只占几十个像素,这种细节根本喂不进模型。解决:我在实际项目里的做法是分两步。第一步先保证罐体框准确,第二步在罐体中段裁出标签区域,单独训练一个分类头或加一个辅助分类损失。如果没有精力做辅助头,就尽量补充近景细节图,让模型有机会看到罐身上的文字纹理。顺便提一句,如果图片里东鹏特饮有瓶装和罐装两种规格,建议在标注阶段就拆成两个子类,否则模型会把瓶身特征和金罐特征平均在一起。

5.2 冷藏柜反光和水珠把logo糊掉

现象:常温环境下训练出来的模型,放进饮料冷藏柜后漏检率明显上升,罐体边缘高光部分经常被当成背景。原因:罐装饮料表面是金属或亮面塑料,冷柜里的冷凝水会在罐体上形成水珠和反光带,把原本的红蓝底色推成高光白,模型在训练时没见过这种极端亮度分布。解决:我一般会在训练前检查一下数据包里是否包含冰箱场景的图,如果没有,数据增广里把亮度扰动范围调大,或者用retinex增强生成一些亮度较极端的副本。不过增强只能缓解,治本还是要去真实冷柜拍50到100张图补进训练集。反光问题很容易被mAP掩盖,因为光照正常的验证集上模型表现得很好,所以验证集里一定要放反光样本。

5.3 养乐多这种小目标漏检,问题常在输入尺寸

现象:大罐装的可乐、红牛都能检测到,养乐多经常飘框,框位置忽左忽右。原因:养乐多瓶子小,在一张1920×1080的货架图里可能只占60×120像素,如果训练时imgsz设为416,这张图被压缩到416×234,养乐多只剩十几个像素宽,特征完全消失。解决:先统计该类bbox面积占整图的比例,如果低于1%,就该把imgsz提高到640或以上。如果显存不够,优先牺牲batch而不是imgsz。另外不要只盯着输入的全局尺寸,要看模型下采样步长。YOLOv8有多次下采样,小目标经过几轮stride后可能只剩下几个特征点,必要时可以单独对小目标区域做切图训练。

5.4 标注框不贴边,2至3像素偏差引发推理框抖动

现象:同一罐体在连续视频帧里检测框边缘不稳定,有时包住罐体,有时又窄了一圈。原因:手工标注时,很多人习惯沿着罐体的最大外接矩形画框,但罐体有弧度和倒角,标注框会在上下留出几像素空白。模型会把这种留白当成罐体特征的一部分,推理时框线随之波动。解决:我一般会在转换前加一个框修正步骤,对明显过度外扩的框做向内收缩。比如上下各收3像素、左右各收2像素,具体幅度先画几张图看效果再定。收缩时要注意,某些框标注时已经紧贴罐体,再收缩会把边缘裁掉。最稳妥的做法是把修正参数写进配置,同一套参数作用于训练集和验证集,避免两边框规则不一致。

5.5 类别名中英文混排,部署侧换框架就乱套

现象:Python端训练的模型一切正常,导出ONNX后用C++推理,输出的类别对不上,有时类别名全是乱码。原因:训练时data.yaml里用了中文类别名或中英文混排,Python环境能正确解码UTF-8,但C++端的加载逻辑不一定按UTF-8解析,索引错位就会变成“检测到红牛,显示的却是可乐”。解决:在转YOLO标注的阶段就把类别名统一成ASCII英文,并让data.yaml、label_map.json、部署端names文件三份内容保持一致。导出ONNX时不要套用COCO的80类标准names,只保留自己这8类的映射。记住,模型输出的永远是整数索引,类别名只是人类读的注释,注释不统一是这类问题反复出现的根源。

6. 进阶验证技巧:用真实货架视频抽帧,做一份八类的混淆矩阵

很多团队训练完只看验证集mAP,mAP高就认为可以交付。我一般会多做一步:用手机横屏拍摄一段真实货架或饮料柜的视频,按每10帧抽1帧,把抽出的帧交给模型推理,再和人工标注真值做IoU匹配,统计出八类之间的混淆矩阵。这一步暴露的问题,比任何指标都直观。

import cv2 from ultralytics import YOLO model = YOLO("runs/detect/canned/weights/best.pt") cap = cv2.VideoCapture("shelf_video.mp4") frame_id = 0 while cap.isOpened(): ok, frame = cap.read() if not ok: break if frame_id % 10 == 0: res = model(frame, conf=0.35, iou=0.5, verbose=False) annotated = res[0].plot() cv2.imwrite(f"eval_frames/{frame_id:05d}.jpg", annotated) frame_id += 1 cap.release()

抽帧脚本只解决“模型看到了什么”的问题。要做混淆矩阵,还得给这几十帧人工补标注,量不大,30帧足够。然后把人工框和模型输出框做IoU匹配,IoU大于0.5算命中同一目标,最后统计每类实际标签对应到哪个预测类别,形成8×8矩阵。如果红牛和东鹏的交叉项明显偏高,回看第5章的防混淆方法;如果某个类在矩阵某一列完全没输出,说明这个新规格或新角度在训练集里没见过,该补数据。

这里有个很现实的判断:验证集mAP是模型在已知数据分布上的考试分数,视频抽帧才是真实场景的摸底。罐装饮料识别的难点从来不是网络结构选得多大,而是数据分布有没有覆盖到实际摆放、光照和反光。我自己曾有一段时间过于迷信验证集指标,直到把一段10分钟的饮料柜视频丢进模型,才发现金罐类目在冷柜灯光下被环境光分成两半,这个教训后来一直提醒我:不管标注包多完整,都要留时间做真实场景抽帧验证。希望帮到你。

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

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

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

立即咨询