
简介面向目标检测与图像识别初学者及算法工程师这套罐装饮料识别数据集汇集了一千多张实拍图片覆盖薯片、东鹏特饮、红牛、芬达、养乐多、可乐、雪碧、王老吉、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.idcategory_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(imgIdsimg_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(imgIdsimg_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_rootcheck): 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(imgIdsimg_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_okTrue) 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分组每类抽2030张保证红牛和东鹏这种易混淆类目也被覆盖到。眼检时重点盯三个位置罐体边缘是否留白过大饮料瓶之间遮挡时框是否把两个目标圈在一起以及标注名和实物是否一致。框稍微偏几像素问题不大但框住两个目标会让训练产生“这个框里同时有红牛和东鹏”的错误监督信号。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, encodingutf-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(parentsTrue, exist_okTrue) 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), encodingutf-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(parentsTrue, exist_okTrue) lab_dir.mkdir(parentsTrue, exist_okTrue) 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.yamlpath: /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: wanglaojinames的顺序必须和转换脚本里的类别索引完全一致这是训练框架唯一的类别依据。训练命令我一般这样起yolo detect train \ datadata.yaml \ modelyolov8s.pt \ epochs120 \ imgsz640 \ batch16 \ patience30 \ seed0modelyolov8s.pt意思是加载COCO预训练权重然后在灌装饮料数据上微调。预训练权重从通用目标检测迁移过来的效果通常比从随机参数开始训练省事得多尤其对1000张这种规模。imgsz640是平衡速度和精度的常见选择。如果养乐多这类小目标占比低不要为了显存把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. 进阶验证技巧用真实货架视频抽帧做一份八类的混淆矩阵很多团队训练完只看验证集mAPmAP高就认为可以交付。我一般会多做一步用手机横屏拍摄一段真实货架或饮料柜的视频按每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, conf0.35, iou0.5, verboseFalse) annotated res[0].plot() cv2.imwrite(feval_frames/{frame_id:05d}.jpg, annotated) frame_id 1 cap.release()抽帧脚本只解决“模型看到了什么”的问题。要做混淆矩阵还得给这几十帧人工补标注量不大30帧足够。然后把人工框和模型输出框做IoU匹配IoU大于0.5算命中同一目标最后统计每类实际标签对应到哪个预测类别形成8×8矩阵。如果红牛和东鹏的交叉项明显偏高回看第5章的防混淆方法如果某个类在矩阵某一列完全没输出说明这个新规格或新角度在训练集里没见过该补数据。这里有个很现实的判断验证集mAP是模型在已知数据分布上的考试分数视频抽帧才是真实场景的摸底。罐装饮料识别的难点从来不是网络结构选得多大而是数据分布有没有覆盖到实际摆放、光照和反光。我自己曾有一段时间过于迷信验证集指标直到把一段10分钟的饮料柜视频丢进模型才发现金罐类目在冷柜灯光下被环境光分成两半这个教训后来一直提醒我不管标注包多完整都要留时间做真实场景抽帧验证。希望帮到你。本文还有配套的精品资源点击获取