☰
可口可乐与百事可乐标志检测:VOC转YOLO与训练避坑指南
2026/10/10 12:49:13 网站建设 项目流程

简介:面向目标检测入门与实战的可口可乐与百事可乐标志检测数据集,包含2223张jpg图像及对应标注,覆盖CocaCola与Pepsi两个类别,总标注框数达4666个。数据集同时提供Pascal VOC格式xml文件与YOLO格式txt文件,便于直接接入主流检测框架完成模型训练、验证与迁移学习,适合学习数据标注规范、练习检测算法或搭建饮料品牌识别应用。压缩包整体约2000个文件,以xml标注文件、txt类别说明等为主,体积120.84MB,目录结构清晰,标注工具采用labelImg画矩形框,标注规则简单明确。已有347人学习下载,可快速获得一份格式规整、标注完整的小规模标志检测数据,免去自行爬取图片与手动标注的繁琐过程。

1. 2220张双品牌标志检测数据集:先拆解标注再训练,才是真正的落地姿势

从网盘里拖回来的目标检测数据集,往往包装得比标题还漂亮,比如这份「可口可乐与百事可乐标志检测数据集,2220张、2个标签、VOC+YOLO格式.zip」。光看名字,它就是为品牌LOGO识别量身定做的:两张瓶装可乐图、一张PET瓶图,外加几十张特意拍了货架大场景的图,看起来直接丢进YOLO就能出模型。但这类数据集真正的成本不在那2220张图像,而在标注目录的噪音、类别分布和train/val划分。做零售视觉、自动售货机识别、商超品牌露出统计,或者把视觉结果喂给多模态AI分析系统的同学,大概率会撞上这类双品牌标志检测任务。下面这篇笔记不按说明书走,把从解压、体检标注、转格式、训练到验证这一路的参数和坑完整过一遍;新手能跟着步骤复现,熟手也能在避坑章节里翻到自己的教训。

2. VOC与YOLO双格式的真实含义:读懂标注文件再谈训练

2.1 双格式只是一份坐标的两种写法:VOC的xml和YOLO的txt差在哪

「VOC+YOLO格式」听起来像双保险,其实它描述的只是同一批框的两种编码方式。VOC格式来自PASCAL VOC竞赛,每个图像对应一个xml文件,里面用嵌套的object节点记录目标类别和边界框。打开任意一个标注文件,长这样:

<annotation> <folder>JPEGImages</folder> <filename>IMG_20240701_0012.jpg</filename> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <object> <name>coca_cola</name> <bndbox> <xmin>312</xmin> <ymin>240</ymin> <xmax>518</xmax> <ymax>536</ymax> </bndbox> </object> </annotation>

而YOLO格式就不一样了,图像的同名txt文件里,每一行代表一个目标,依次是类别id、归一化后的中心点x、中心点y、目标宽度w、目标高度h,五个数字全部用空格隔开。比如上面这个coca_cola框,在YOLO里就写成:

0 0.324219 0.538889 0.160938 0.411111

注意到没有,VOC的坐标是绝对像素值,YOLO的坐标是除以图像宽高后的0到1浮点数;VOC的类别是字符串,YOLO的类别是数字索引。训练框架只认其中一种,并不存在一个训练器同时吃两种格式的捷径。所以你拿到这份zip后的第一步,是先搞明白它给的到底是已经分好的YOLO目录,还是只有VOC的xml加jpg——后者就需要自己动手转。两类格式的核心差异,可以归纳成一张表:

维度VOC(xml)YOLO(txt)
文件组织Annotations下xml + JPEGImages下jpgimages下jpg + labels下txt
坐标方式xmin/ymin/xmax/ymax绝对像素x_center/y_center/width/height归一化
类别表示object/name字符串每行首数字,从0开始索引
一张图多目标多个object节点多个行
典型训练器Faster R-CNN、SSD、mmdetectionYOLO系列

我一般会先打开三五个xml看一眼,确认name字段的拼写。像coca_cola和pepsi_cola这种下划线命名就还好,最怕同一份数据集里一半叫coke、一半叫coca_cola,那后面的类别统计会直接失真。

2.2 用Python对xml做标签体检:类别分布、框数和边框质量全看一遍

在跑任何训练脚本之前,花十分钟对标注目录做一次体检,能省下后面大量调参时间。下面的脚本扫描Annotations目录下所有xml,统计每个类别的目标总数、每张图的平均框数、以及目标框占整张图面积的比重,这份输出就是后续清洗的决策依据:

import glob import os import xml.etree.ElementTree as ET from collections import Counter ann_dir = "Annotations" # VOC格式标注目录 cls_counter = Counter() # 每个类别的目标总数 box_counter = [] # 每张图像的框数 size_ratios = [] # 框面积占图像面积比例 unknown_names = set() # 不在预期类别内的name xml_list = glob.glob(os.path.join(ann_dir, "*.xml")) print("xml 文件数:", len(xml_list)) for xml_path in xml_list: tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) boxes = root.findall("object") box_counter.append(len(boxes)) for obj in boxes: name = obj.find("name").text cls_counter[name] += 1 bb = obj.find("bndbox") x1 = int(bb.find("xmin").text); y1 = int(bb.find("ymin").text) x2 = int(bb.find("xmax").text); y2 = int(bb.find("ymax").text) w = max(x2 - x1, 1); h = max(y2 - y1, 1) size_ratios.append((w * h) / (img_w * img_h)) print("类别统计:", dict(cls_counter)) print("平均每图框数: %.2f" % (sum(box_counter) / len(box_counter))) print("框平均面积占比: %.4f" % (sum(size_ratios) / len(size_ratios))) print("未预期类别:", unknown_names if unknown_names else "无")

这段代码本身不复杂,重点在读输出。类别统计能立刻暴露两类样本是否均衡:如果coca_cola有1400个框而pepsi_cola只有800个,后续训练就得给样本少的类加权重,或者针对性地补增强。平均每图框数如果接近1,说明大部分场景是单目标特写;如果能在0.1以下,说明存在大量货架大场景图,小目标检测的难度就在这。框平均面积占比更关键,0.2以上表示目标在画面里占大头,模型容易学成“识别红色块”;0.01左右的框占比则对锚点和imgsz都提出更高要求。

体检时还要顺手确认xml和jpg文件是否一一对应。常见问题是jpg命名带空格、png混在里面、或者xml里filename与实际文件名不一致,这类脏数据不清理,后续转格式时文件拷贝会静默跳过一部分图,等你训练时才发现数据量悄悄缩水。

2.3 双品牌检测的难点:可口可乐与百事可乐的类间差异比想象中小

很多第一次做标志检测的人容易把任务想简单,觉得可口可乐是红底白字、百事是蓝底红字,两类颜色差异明显。但真实拍摄场景里,难点集中在三处。第一是包装形态,同是可口可乐,铝罐、PET瓶、玻璃瓶、纸箱外包装上的印刷logo形状和反光完全不同;第二是光照,便利店冷柜的白色背光会让蓝罐百事变成灰蓝色,超市货架上的红色罐身在高光下直接过曝,检测框还在但颜色特征已经被摧毁;第三是小目标,大场景货架图里一瓶饮料可能只有40×80像素,logo区域就更小,这对数据增强策略和推理输入分辨率都是考验。

这也是这类品牌标志数据集和矿泉水瓶检测这类单类任务的关键区别。单类任务只要有框就行,双品牌要求模型同时具备“检测到”和“分得清”两个能力。体现在训练上,除了常见的位置损失,类别损失需要更谨慎地配比,标签平滑可以适当开一点,避免模型对罐体颜色产生过度自信的映射。接下来的章节就按这个思路,把VOC转YOLO、训练配置和验证流程完整走一遍。

3. VOC转YOLO格式并跑通YOLOv8训练:一套脚本加三个关键参数

3.1 先按ImageSets划分数据,再写转换脚本

拆开这类数据集zip,常见的目录结构是VOCdevkit下包含JPEGImages、Annotations、ImageSets/Main三个文件夹,其中ImageSets/Main里的train.txt和val.txt存放着划分好的图像索引。也有不少散装版本不提供划分文件,那就得先按9比1随机划分出train和val,再写转换。转换脚本的核心逻辑是:读取xml里的bndbox坐标,换算成归一化的YOLO格式写进txt,同时把对应图像拷贝到images目录下。

import glob, os import xml.etree.ElementTree as ET import shutil ann_dir = "Annotations" img_dir = "JPEGImages" sets_file = "ImageSets/Main/train.txt" # 分别对 train.txt 和 val.txt 执行 out_img = "images/train" out_lbl = "labels/train" os.makedirs(out_img, exist_ok=True) os.makedirs(out_lbl, exist_ok=True) cls_index = {"coca_cola": 0, "pepsi_cola": 1} # 类别顺序必须与yaml的names一致 with open(sets_file) as f: image_ids = [line.strip() for line in f if line.strip()] for idx in image_ids: xml_path = os.path.join(ann_dir, idx + ".xml") if not os.path.exists(xml_path): continue # 避免标注缺失导致整个脚本中断 tree = ET.parse(xml_path) root = tree.getroot() width = int(root.find("size/width").text) height = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in cls_index: continue # 打印未知类别,而不是直接抛异常 bb = obj.find("bndbox") x1 = float(bb.find("xmin").text); y1 = float(bb.find("ymin").text) x2 = float(bb.find("xmax").text); y2 = float(bb.find("ymax").text) cx = ((x1 + x2) / 2) / width cy = ((y1 + y2) / 2) / height bw = (x2 - x1) / width bh = (y2 - y1) / height if cx < 0 or cy < 0 or bw <= 0 or bh <= 0: print(f"跳过非法坐标: {idx}, {name}") # 脏标注留痕 continue lines.append(f"{cls_index[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") if lines: with open(os.path.join(out_lbl, idx + ".txt"), "w") as f: f.write("\n".join(lines)) shutil.copy(os.path.join(img_dir, idx + ".jpg"), os.path.join(out_img, idx + ".jpg"))

这个脚本注意三个细节。cls_index字典决定最终txt里类别id的顺序,如果后面data.yaml里names顺序是["coca_cola", "pepsi_cola"],那么这里class id 0必须是coca_cola;顺序错位是新手最容易翻车的地方。第二个细节是非法坐标检查,xml里偶尔会有xmax小于xmin、或者目标整个超出图像边界的坏框,这种目标喂给YOLO会直接产生NaN梯度,必须过滤。第三个细节是如果ImageSets/Main里没有现成划分文件,可以先用glob把jpg文件名全部扫出来,按9比1做随机分层抽样再写train.txt和val.txt。

3.2 编写dataset.yaml并启动训练:显存不够时的参数组合

转完目录后,数据集根目录下需要有data.yaml。以这份双品牌数据集为例:

# pepsi_coke.yaml path: ./datasets/pepsi_coke # 数据集根目录 train: images/train # 训练图片目录 val: images/val # 验证图片目录 names: 0: coca_cola 1: pepsi_cola

然后一条命令就能启动YOLOv8训练:

yolo detect train data=pepsi_coke.yaml model=yolov8s.pt \ epochs=100 imgsz=640 batch=16 lr0=0.01 \ mosaic=1.0 close_mosaic=10 patience=15

参数说明里,imgsz=640是速度和精度的平衡点;如果硬件是8GB显存,batch=16配合yolov8s是最稳的组合,24GB显存可以拉到batch=32甚至换yolov8m。mosaic=1.0表示开启马赛克增强,四张图拼一张训练,对双品牌这类场景差异数据很有帮助,但要注意close_mosaic=10这个参数——最后10个epoch一定要关掉mosaic,否则模型在增强后的分布上微调,验证时面对真实分布会掉点。lr0=0.01是YOLO系列的默认初始学习率,迁移预训练权重时不需要手动改小,patience=15会在15个epoch验证集mAP没有提升时自动早停。

关于预训练权重的选择,常见做法是直接用yolov8s.pt而不是yolov8n.pt。虽然n模型更轻,但对这个任务来说,品牌logo的纹理细节恰恰是轻量级模型最容易丢的部分;在边缘设备上部署时再考虑剪枝或转INT8压缩。另外,既然标题里写的是2220张图,epoch数没必要硬冲300,100个epoch基本够收敛;数据量越小越要关注过拟合,而不是追求更低的训练loss。

3.3 验证集划分的隐藏坑:按视觉场景分层,别让同一场景同时出现在train和val

真正让val mAP虚高的,往往不是训练参数,而是数据划分方式。这份数据集的图片很可能来自多天、多个门店、多台设备的拍摄,如果随机划分,同一个拍摄场景的连续画面会同时出现在训练集和验证集里。比如某便利店冷柜一共拍了30张,随机划分后25张去了train、5张进了val,模型等于在考试时见过原题,val mAP当然好看,一上真实场景就现原形。

解决办法是按场景ID做分层抽样。一般这类数据的文件名会带拍摄批次信息,比如IMG_20240701_0012.jpg里的日期段就是天然的场景标识:

from collections import defaultdict import random, os val_ratio = 0.2 scene_dict = defaultdict(list) for img in os.listdir("JPEGImages"): scene_key = img.split("_")[0] # 以文件名首段作为场景ID scene_dict[scene_key].append(img) train, val = [], [] for scene_imgs in scene_dict.values(): random.shuffle(scene_imgs) n_val = max(1, int(len(scene_imgs) * val_ratio)) val += scene_imgs[:n_val] # 该场景一部分进val train += scene_imgs[n_val:] # 剩余进train

这段逻辑保证了同一个场景不会被拆到两个集合里,模型学的是“见过这种场景类型”而不是“背下这张场景的答案”。如果你发现文件名里没有可用于分组的字段,退一步的做法是按文件夹路径分组再抽,比如图片本身就在cold_drawer、shelf、table_flat这些子目录里,每个目录按比例分别抽取就行。这个动作看起来不起眼,但恰恰是决定模型能不能落到真实货架上的关键一步。

4. 标志检测数据集避坑指南:5条踩坑记录,条条都是血泪经验

4.1 类别样本失衡:百事样本只有可口可乐的一半,score阈值怎么调都漏检

现象:训练完成后,验证集上coca_cola的recall有0.95,pepsi_cola的recall只有0.7,而且把置信度阈值从0.25降到0.1,百事的漏检率也降不下来。

原因:这份数据集的原始拍摄场景以红色包装的商店货架为主,百事蓝罐的出现频率天然偏低。检测模型在训练时见过大量红色特写,会把特征重心偏向可口可乐;百事样本太少,边界框回归和分类分支都学不充分。

解决:先看类别统计确认失衡比例,然后两条路并行。一条是数据增强层面,对样本少的类提高mosaic参与率,或者对百事类图片额外做hsv扰动和水平翻转;另一条是训练层面,在类别损失上给少样本类加权,YOLOv8训练配置里可以单独设置每个类别的权重列表。最土但有效的办法是把百事样本所在的图像做两次不同参数的复制增强后再参与训练,相当于给这个类加杠杆。

4.2 标注框太松:检测框外溢半个瓶身,vMAP虚高而误检成灾

现象:模型推理结果每个框都比真实瓶身大一圈,货架大场景下把红色价签、促销贴纸一并圈进检测框里,品牌曝光的统计数字直接失真。

原因:人工标注VOC时为了“稳妥”,习惯把边界框拉大一点,确保把瓶盖和瓶底都包住。这类外溢框在数据集中占比高的话,YOLO的回归分支会学到“框宁可大不可小”的错误倾向,mAP计算时由于IoU只要求0.5,这种虚大框依然能达标。

解决:对标注框做一次质检,计算每个gt框相对目标实际轮廓的紧凑度。常见做法是写一个后处理脚本,把每个框横向纵向各收缩10%,检查收缩后的IoU是否还大于0.7;如果大量框收缩后IoU骤降,说明标注本身就松。清洗时把所有外溢大于20%的框过滤掉,宁可少要数据,也不能让回归分支学到错误模式。

4.3 验证集泄漏:val mAP 0.95,部署到便利店冷柜视频流就翻车

现象:训练日志里val mAP一路冲到0.95,模型文件在本地测试图片上表现也不错,但接到真实的视频流接口后,几乎每几帧就丢一个目标,抖动还特别严重。

原因:数据集生成时把同一段视频按帧抽成了图片,随机划分后同一秒内的画面同时出现在train和val里,验证集已经失去了评估意义。这种情况在冷柜检测、流水线拍摄的场景里反复出现。

解决:拿到数据先问一句“这些图是不是同一段视频抽出来的”。如果是,按视频ID做group split,上面3.3小节的场景分层脚本直接能复用。检查手段很简单,把训练集和验证集里文件名前缀相同的图片数量统计出来,如果大量重合,这就是泄漏的铁证。

4.4 预训练模型names没改成两个类:检测框是准的,类别名称全是乱的

现象:训练结束后模型框得很准,但输出的类别标签完全错位,比如把百事标成可口可乐、把可口可乐标成百事,或者直接输出COCO预训练里的person类。

原因:data.yaml里names顺序和训练脚本转换时cls_index的顺序不一致。转换脚本里有几处硬编码,如果数据集的labels目录已经生成好了,txt里第一列是0的样本到底代表哪个类,完全由yaml的names顺序决定。任何一端的顺序对不上,都会造成全量错位。

解决:训练前做一次交叉验证,把labels目录里任意一个txt的第一行数字打出来,再打开对应的xml看name字段,确认0对应coca_cola还是pepsi_cola。我一般会在第一次训练前写一条命令把这两个信息同时打印出来对照,五秒钟的事,省得训练几十个epoch才发现整个模型白练。

4.5 小数据硬扛300个epoch:模型开始记住罐体图案,而不是识别logo

现象:训练loss持续下降,val mAP在120个epoch后小幅上升,到了250个epoch反而开始回落,检测框开始拿着罐体上的装饰条纹当分类特征。

原因:2220张图对两个类来说属于小样本,训练epoch拉满后,模型可学习的参数远多于有效样本量,SGD开始把训练集里的特定纹理、特定反光当作泛化特征记忆下来,也就是过拟合。

解决:直接把patience=15和epochs=100作为起点。如果val的mAP在60个epoch后就停滞不动,优先检查数据增强是否过强,比如mosaic的拼图尺度太大导致目标被切碎;其次检查模型是否偏大,yolov8s不行就试yolov8n,缩小容量比盲目加数据更符合小样本的规律。过拟合的另一个缓解手段是标签平滑,给置信度输出加一点缓冲,模型不会对训练集中的高频特征过于自满。

5. 验证与进阶:用0.25置信度跑真实货架,模型才算验收

5.1 十分钟真实场景验证:三张图就够

mAP只是第一个评分项。我的习惯是训练结束后,准备三张典型的真实场景图:一张桌面俯拍(平视单瓶特写)、一张便利店冷柜暗光图、一张超市货架大场景。然后分别用两个置信度档位去跑推理:

yolo detect predict model=runs/detect/train/weights/best.pt \ source=test_imgs/ shelf_night.jpg \ conf=0.25 iou=0.45 device=0

先跑0.25档,检查有没有漏检;再跑0.6档,观察误检是否减少。两张结果一叠,你就能看出这个模型的检测框到底是在“找logo”还是在“碰运气”。冷柜图是最佳试金石:白色背光环境下百事蓝罐很容易被吞进背景里,如果0.25档能稳定框出来,说明数据增强里对亮度扰动做得够。

5.2 把置信度阈值当作模型的一部分去调,而不是部署后才拍脑袋

调阈值不是等部署时才开始,应该和训练过程一起验证。对双品牌任务,漏一个大货架上的百事瓶子和多框一个红色促销牌,业务代价完全不同。我一般会看每类单独在0.25、0.35、0.5三个阈值下的recall曲线,然后按场景选阈值:货架大场景用0.3,冷柜特写用0.5,流动镜头用0.25。最后再配合NMS的iou参数,把互相重叠的框收敛干净。之前我踩过最深的坑,是拿0.5阈值部署到自动售卖机上,暗光场景里百事全漏,后来改成场景自适应阈值才解决。

数据源的整理、标注的清洗、训练参数的收敛,每一条都是在给最后的部署减少黑匣子。希望这篇踩坑整理能帮你在品牌标志检测这类任务上少花几周时间绕路。祝落地顺利。

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

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

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

立即咨询