news 2026/9/28 16:41:52

可口可乐与百事可乐标志检测:2220张VOC+YOLO数据集实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
可口可乐与百事可乐标志检测:2220张VOC+YOLO数据集实战指南

简介:这份可口可乐与百事可乐标志检测数据集面向目标检测初学者与算法实践者,用于训练和验证品牌Logo识别模型,可应用于零售货架分析、广告监测等场景。数据集共2223张jpg图片,每张均配有对应的VOC格式xml与YOLO格式txt标注,标注类别为CocaCola和Pepsi两类,框数分别为2268和2398,总框数4666,采用labelImg工具按矩形框规则标注,标注准确合理。压缩包内共约2000个文件,以xml标注文件为主,另含一份说明文档,整体约120.84MB,目录结构清晰,便于直接接入YOLO或VOC训练流程。目前已有347人学习下载。读者可借此快速搭建双类别检测实验,省去自行采集与标注成本,并对照两种格式理解数据转换与训练配置,适合作为课程设计、毕业设计或算法入门的实战素材。

1. 可口可乐与百事可乐标志检测:2220 张双标签数据集怎么用起来

你手里如果有一批货架、冷柜、自动售货机的照片,想自动数出可口可乐和百事可乐的标志各出现了几次,第一道坎从来不是模型选型,而是数据。网上能直接下到的「可口可乐与百事可乐标志检测数据集2220张2个标签VOC+YOLO格式」这类资源,价值就在于它把最费人力的标注环节替你做了:2220 张图、2 个类别、同时给了 VOC 和 YOLO 两套标注格式。VOC 是 Pascal VOC 那套 XML,YOLO 是每张图一个 txt、每行类别 中心x 中心y 宽 高的归一化坐标。两套格式并存意味着你既能喂给 YOLO 系列,也能转去别的框架。这篇不聊虚的,就讲这个数据集从解压到跑出第一条训练曲线,中间每一步命令、每个参数、每个容易翻车的地方。适合刚拿到数据集想快速验证的人,也适合已经跑过 YOLO、想搞清楚 VOC 与 YOLO 互转边界的老手。

2. 先看清数据集结构:VOC 与 YOLO 两套标注到底差在哪

拿到压缩包别急着解压完就开训,先花十分钟把目录结构和标注格式摸清楚。这一步做扎实,后面 80% 的格式报错都能提前避开。很多人一上来就yolo train,结果报Label class xxx is not in the class list,回头再查,发现是类别名对不上或者坐标没归一化,白白浪费一轮调试时间。

2.1 解压后应该看到的目录长什么样

这类双格式数据集,常见做法是压缩包里同时放VOC2007风格目录和一份已经转好的 YOLO 目录,或者只给 VOC、让你自己转。先解压再tree一下,确认三件事:图片在哪、XML 在哪、有没有现成的 txt。

# 解压到独立目录,避免污染当前工作区 mkdir -p ~/datasets/coke_pepsi && cd ~/datasets/coke_pepsi unzip ~/Downloads/coke_pepsi_2220.zip -d . # 看两层目录结构,确认图片和标注的分布 find . -maxdepth 3 -type d | sort # 统计图片数量,确认是不是 2220 张 find . -type f \( -iname "*.jpg" -o -iname "*.png" -o -iname "*.jpeg" \) | wc -l # 统计 XML 数量 find . -type f -iname "*.xml" | wc -l # 统计 YOLO txt 数量 find . -type f -iname "*.txt" | wc -l

逻辑说明:find -maxdepth 3只往下看三层,避免目录太深刷屏;图片统计把 jpg/png/jpeg 都算上,因为不同来源的图格式可能混着。参数上没什么可调的,重点是看三个数字对不对得上——图片数应该接近 2220,XML 数应该等于图片数(每图一个标注文件),txt 数如果存在也应该等于图片数。如果 txt 数明显少于图片数,说明 YOLO 格式只转了一部分,你得自己补转。

提示:如果解压出来图片散在多个子目录里,先别合并,记下每个子目录的路径,后面写转换脚本时按目录遍历,比事后挪文件安全。

2.2 VOC XML 里到底存了什么

VOC 格式的每个 XML 描述一张图,核心是size和若干object。size给出宽高和通道数,object里name是类别名,bndbox是左上角和右下角的绝对像素坐标。用一段 Python 快速抽查一个文件,比肉眼翻 XML 靠谱。

import xml.etree.ElementTree as ET import glob # 抽查第一个 XML,打印关键字段 xml_files = glob.glob("**/*.xml", recursive=True) print("XML 总数:", len(xml_files)) tree = ET.parse(xml_files[0]) root = tree.getroot() # 图片尺寸 size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) print(f"图片尺寸: {w} x {h}") # 遍历所有目标框 for obj in root.findall("object"): name = obj.find("name").text box = obj.find("bndbox") xmin = int(box.find("xmin").text) ymin = int(box.find("ymin").text) xmax = int(box.find("xmax").text) ymax = int(box.find("ymax").text) print(f"类别={name} 框=({xmin},{ymin},{xmax},{ymax})")

逻辑说明:ET.parse解析 XML,findall("object")拿到所有目标。这里要重点看两件事:一是name的取值,正常应该只有coca_cola和pepsi两种(或者类似命名),如果出现第三种拼写,说明标注有脏数据;二是bndbox的坐标有没有超出size范围,超出就是标注错误,训练时会报越界。参数上recursive=True是为了兼容 XML 放在子目录的情况。

2.3 YOLO txt 的五个数字分别是什么

YOLO 格式每行五个值:class_id center_x center_y width height,后四个都是相对图片宽高的归一化值,范围 0 到 1。class_id从 0 开始,对应一个classes.txt或data.yaml里的类别顺序。抽查一个 txt:

import glob txt_files = glob.glob("**/*.txt", recursive=True) # 过滤掉 classes.txt 这类非标注文件 label_files = [f for f in txt_files if "classes" not in f.lower()] print("标注 txt 总数:", len(label_files)) with open(label_files[0]) as f: for line in f: parts = line.strip().split() if len(parts) != 5: print("异常行:", line) continue cid, cx, cy, bw, bh = parts print(f"类别id={cid} 中心=({cx},{cy}) 宽高=({bw},{bh})")

逻辑说明:先过滤掉classes.txt,否则会把类别清单当成标注文件统计。每行必须正好 5 个字段,多一个少一个都是格式错误。归一化值如果出现大于 1 或小于 0,说明转换时没除以图片宽高,这种数据直接训会让 loss 爆炸。参数上没什么可调,重点是校验字段数和数值范围。

注意:VOC 的坐标是绝对像素、左上右下;YOLO 是归一化、中心点加宽高。这两套坐标系的换算是最容易出错的地方,下一章专门讲转换。

3. 把 VOC 转成 YOLO:转换脚本与四个边界坑

如果你的数据集只给了 VOC,或者你想统一用 YOLO 格式训练,就得自己转。转换本身不难,难的是边界情况:坐标越界、类别名映射、图片和 XML 对不上、空标注文件。这一章给一份能直接用的转换脚本,再把四个坑逐个说清楚。

3.1 一份可直接跑的 VOC 转 YOLO 脚本

import os import glob import xml.etree.ElementTree as ET from PIL import Image # 类别映射:按你的实际类别名改,顺序决定 class_id CLASS_MAP = {"coca_cola": 0, "pepsi": 1} def convert_one(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() # 用 XML 里的 size,而不是重新读图,避免图片损坏导致中断 size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) if w <= 0 or h <= 0: return 0 # 尺寸异常,跳过 lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in CLASS_MAP: continue # 未知类别跳过,避免污染训练 cid = CLASS_MAP[name] box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) # 裁剪到图片范围内,防止越界 xmin = max(0, min(xmin, w)) xmax = max(0, min(xmax, w)) ymin = max(0, min(ymin, h)) ymax = max(0, min(ymax, h)) if xmax <= xmin or ymax <= ymin: continue # 裁剪后无效框,跳过 # 转成归一化的中心点+宽高 cx = (xmin + xmax) / 2.0 / w cy = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{cid} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") # 写 txt,文件名和图片同名 base = os.path.splitext(os.path.basename(xml_path))[0] out_path = os.path.join(out_dir, base + ".txt") with open(out_path, "w") as f: f.write("\n".join(lines)) return len(lines) if __name__ == "__main__": xml_files = glob.glob("**/*.xml", recursive=True) out_dir = "labels_yolo" os.makedirs(out_dir, exist_ok=True) total_boxes = 0 empty_count = 0 for xp in xml_files: n = convert_one(xp, out_dir) total_boxes += n if n == 0: empty_count += 1 print(f"转换完成: {len(xml_files)} 个 XML, {total_boxes} 个框, {empty_count} 个空标注")

逻辑说明:CLASS_MAP决定类别顺序,必须和后面data.yaml里的names完全一致,否则训练时类别全错。坐标裁剪那几行是防越界的关键,很多公开数据集都有几个框超出图片边界,不裁的话 YOLO 训练会警告甚至报错。{:.6f}保留六位小数,够用且不会让文件太大。最后统计空标注数量,空标注文件是合法的(表示这张图没有目标),但如果空标注占比过高,说明数据集质量有问题。

参数说明:CLASS_MAP按你的实际类别名改;out_dir是输出目录,建议和图片目录分开,方便后面组织数据集结构。

3.2 坑一:类别名大小写和空格不一致

现象:转换后训练报Label class xxx is not in the class list,或者明明只有两个类别,data.yaml里却要写三个。

原因:XML 里同一个类别可能写成Coca_Cola、coca cola、coca-cola多种形式,CLASS_MAP只认一种,其余全被跳过,导致某些图变成空标注。

解决:转换前先统计所有name取值。跑一段统计脚本,把出现过的类别名和次数打出来,再决定映射规则。

import glob import xml.etree.ElementTree as ET from collections import Counter counter = Counter() for xp in glob.glob("**/*.xml", recursive=True): root = ET.parse(xp).getroot() for obj in root.findall("object"): counter[obj.find("name").text.strip()] += 1 for name, cnt in counter.most_common(): print(f"{name}: {cnt}")

看到结果后,把大小写、空格、连字符统一,再写进CLASS_MAP。

3.3 坑二:图片和 XML 文件名对不上

现象:转换脚本跑完,txt 数量对,但训练时提示找不到某张图的标注,或者某张图的标注是空的。

原因:图片叫IMG_001.jpg,XML 叫img_001.xml,大小写或前缀不一致;或者图片在images/下,XML 在annotations/下,脚本按 XML 生成 txt,但 txt 和图片不在同一目录,YOLO 找不到。

解决:转换后做一次配对检查,确保每张图都有同名 txt。

import os import glob img_files = glob.glob("**/*.jpg", recursive=True) + glob.glob("**/*.png", recursive=True) label_dir = "labels_yolo" missing = [] for img in img_files: base = os.path.splitext(os.path.basename(img))[0] if not os.path.exists(os.path.join(label_dir, base + ".txt")): missing.append(img) print("缺标注的图片数:", len(missing)) for m in missing[:10]: print(m)

3.4 坑三:坐标越界和零宽高框

现象:训练日志里出现corrupt image/label警告,或者 loss 一开始就是 nan。

原因:XML 里xmax小于xmin,或者框完全在图片外,转换后归一化值超出 0 到 1。

解决:转换脚本里已经做了裁剪和有效性判断(xmax <= xmin就跳过)。如果你用的是别人的转换脚本,务必自己加这两步。转换后再跑一次范围校验:

import glob bad = 0 for tf in glob.glob("labels_yolo/*.txt"): with open(tf) as f: for line in f: parts = line.strip().split() if len(parts) != 5: bad += 1 continue vals = [float(v) for v in parts[1:]] if any(v < 0 or v > 1 for v in vals): bad += 1 print("异常行数:", bad)

3.5 坑四:空标注文件被误删

现象:训练时某些图完全没有目标,但你又不想删图,结果发现这些图的 txt 是空的,被某些脚本当成无效文件删掉了。

原因:空 txt 是合法的,表示这张图没有目标,YOLO 会把它当负样本用。但有些清洗脚本会把空文件删掉,导致图片和标注数量对不上。

解决:保留空 txt,只删图片和标注都缺失的样本。检查时用「图片存在但 txt 不存在」作为删除依据,而不是「txt 为空」。

4. 组织数据集并跑通第一次 YOLO 训练

格式转好了,接下来是组织目录、写data.yaml、启动训练。这一章按 YOLOv8 的常见流程走,命令和参数都能直接抄。如果你用 YOLOv5 或其他版本,目录结构类似,参数名略有差异,按官方文档对应改即可。

4.1 划分训练集验证集并摆好目录

YOLO 要求图片和标注分目录,且训练集和验证集各有一套。常见做法是 8:2 划分。

import os import glob import random import shutil random.seed(42) # 固定随机种子,保证可复现 img_files = glob.glob("**/*.jpg", recursive=True) + glob.glob("**/*.png", recursive=True) random.shuffle(img_files) split = int(len(img_files) * 0.8) train_imgs = img_files[:split] val_imgs = img_files[split:] for phase, files in [("train", train_imgs), ("val", val_imgs)]: os.makedirs(f"dataset/images/{phase}", exist_ok=True) os.makedirs(f"dataset/labels/{phase}", exist_ok=True) for img in files: base = os.path.splitext(os.path.basename(img))[0] shutil.copy(img, f"dataset/images/{phase}/{os.path.basename(img)}") label = f"labels_yolo/{base}.txt" if os.path.exists(label): shutil.copy(label, f"dataset/labels/{phase}/{base}.txt") else: # 没有标注就写空文件,保证一一对应 open(f"dataset/labels/{phase}/{base}.txt", "w").close() print(f"训练集 {len(train_imgs)} 张, 验证集 {len(val_imgs)} 张")

逻辑说明:random.seed(42)保证每次划分结果一致,方便复现。图片和标注分别复制到images和labels下,文件名保持一致。没有标注的图片写一个空 txt,避免 YOLO 报找不到标注。

4.2 写 data.yaml:两个类别和路径怎么写

path: /home/user/datasets/coke_pepsi/dataset train: images/train val: images/val names: 0: coca_cola 1: pepsi

参数说明:path是数据集根目录,train和val是相对path的图片目录。names的键必须从 0 开始连续,顺序和转换时的CLASS_MAP完全一致。如果顺序反了,模型会把可口可乐认成百事可乐,而且 loss 看起来还挺正常,这种错误最隐蔽。

注意:names里不要写中文,不要有多余空格,冒号后面空一格。YAML 对缩进敏感,用空格不用 Tab。

4.3 启动训练:关键参数怎么设

yolo detect train \ data=dataset/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/coke_pepsi \ name=exp1

参数说明:model=yolov8n.pt是最小的预训练模型,适合先跑通流程,显存不够就换yolov8n,显存充足可以上yolov8s或更大。imgsz=640是常见输入尺寸,如果你的图片分辨率远大于 640,标志又很小,可以提到 960 或 1280,但显存占用会明显上升。batch=16按显存调,报 OOM 就减半。lr0=0.01是初始学习率,小数据集可以降到 0.001 避免震荡。patience=20表示 20 轮没提升就早停,省时间。

4.4 看训练日志:哪些指标值得盯

训练启动后,重点看三列:box_loss、cls_loss、mAP50。box_loss下降说明框的位置在收敛,cls_loss下降说明类别判断在变好。mAP50是验证集上的主指标,前几轮为 0 很正常,一般 10 轮后开始上升。如果box_loss一直不降,先查标注格式;如果cls_loss降不下去,查类别映射。

# 训练结束后看结果目录 ls runs/coke_pepsi/exp1/ # 重点关注 weights/best.pt 和 results.csv

results.csv里每轮一行,可以用 pandas 画曲线,也可以直接看最后几行的mAP50。

5. 避坑与排查:训练不收敛、mAP 上不去怎么查

这一章集中说几个实际跑这个数据集时最容易遇到的问题。每条按「现象 → 原因 → 解决」写,方便你对号入座。

5.1 现象:loss 从第一轮就是 nan

原因:标注里有归一化值超出 0 到 1,或者宽高为 0。YOLO 在计算损失时对无效框没有兜底,直接产生 nan。

解决:回到第 3 章的校验脚本,把所有 txt 扫一遍,找出异常行。重点查bw或bh为 0 的行,以及坐标大于 1 的行。修正后重新训练。

5.2 现象:mAP50 一直在 0.1 以下

原因:类别映射反了,或者data.yaml的names顺序和 txt 里的class_id对不上。模型学的是「0 是百事」,你告诉它「0 是可口可乐」,指标自然上不去。

解决:抽一张图,用标注可视化工具把框画出来,确认类别和框的位置对得上。再检查CLASS_MAP和data.yaml的names是否一致。

5.3 现象:训练到一半报 CUDA out of memory

原因:batch或imgsz太大,或者没有及时释放缓存。

解决:先把batch减半,再把imgsz从 640 降到 512。如果还不行,换更小的模型(yolov8n)。另外可以在训练命令里加cache=False,避免把图片全部缓存到显存。

5.4 现象:验证集指标波动很大

原因:验证集太小,或者划分时没有打乱。2220 张按 8:2 分,验证集约 444 张,如果某一类在验证集里特别少,指标就会跳。

解决:划分时用分层抽样,保证两个类别在训练集和验证集里的比例接近。简单做法是先按类别分组再各自划分,或者多跑几次不同随机种子取平均。

5.5 现象:模型把背景认成标志

原因:负样本太少,或者标注时把一些模糊区域也标成了目标。

解决:检查空标注图片的数量,如果几乎没有空标注,模型没见过「没有标志」的图,就容易误检。可以适当加入一些纯背景图,或者在数据增强里加随机裁剪,让模型见到更多背景。

6. 进阶:用这个数据集验证模型改进是否真的有效

数据集跑通只是起点。2220 张、2 个类别的规模,正好适合做小规模消融实验——你想试新的损失函数、新的注意力模块、新的数据增强策略,都可以在这个数据集上快速验证。但小数据集有个陷阱:指标波动大,改进带来的提升可能被随机性淹没。这一章讲怎么把实验做得可信。

6.1 固定随机种子和划分,保证对比公平

任何改进对比,前提是训练集验证集划分完全一致。把第 4 章的划分脚本固定seed,并且把划分结果存成文件,后续所有实验都读同一份划分。

import json # 保存划分结果 split_info = { "train": [os.path.basename(p) for p in train_imgs], "val": [os.path.basename(p) for p in val_imgs] } with open("split.json", "w") as f: json.dump(split_info, f, indent=2)

后续实验直接读split.json,不再重新随机划分。这样 A 模型和 B 模型的验证集完全相同,指标才有可比性。

6.2 用多次运行取平均,而不是只看一次

小数据集上单次训练的 mAP 波动可能有 2 到 3 个点。判断一个改进是否有效,至少跑 3 次不同种子,看均值和方差。如果改进带来的提升小于方差,那这个改进在这个数据集上不可信。

实验种子mAP50备注
baseline420.812原始 YOLOv8n
baseline430.798原始 YOLOv8n
baseline440.805原始 YOLOv8n
改进 A420.821加注意力模块
改进 A430.815加注意力模块
改进 A440.819加注意力模块

看均值:baseline 约 0.805,改进 A 约 0.818,提升约 1.3 个点,且三次都高于 baseline 的最高值,这种改进才值得继续。如果改进 A 有一次掉到 0.79,那就得怀疑是随机性。

6.3 一个具体技巧:用混淆矩阵定位类别混淆

YOLO 训练完会输出混淆矩阵。这个数据集只有两个类别,混淆矩阵就是 2x2,非常直观。如果可口可乐和百事可乐互相误判的比例高,说明两个类别的视觉特征区分度不够,或者标注时有些模糊样本标错了。这时候与其调模型,不如回去清洗标注。

# 训练后混淆矩阵一般在结果目录下 ls runs/coke_pepsi/exp1/ # 找 confusion_matrix.png 或 confusion_matrix_normalized.png

我自己的习惯是:每次跑完新实验,先看混淆矩阵,再看 mAP。混淆矩阵能告诉你错误发生在哪两个类别之间,比一个孤零零的 mAP 数字有用得多。如果两个类别几乎不混,但 mAP 还是低,那问题多半在框的定位上,可以去看验证集的预测可视化。

这个数据集规模不大,但正好够你把「格式转换 → 目录组织 → 训练 → 排查 → 消融」整条链路走一遍。走完之后你再拿到别的目标检测数据集,流程是一样的,只是类别数和标注格式可能不同。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 16:40:32

Codex 插件安装配置与排错实战:从 CLI 到编辑器完整指南

1. 装完不等于会用&#xff1a;Codex 插件落地的真实门槛很多人第一次接触 Codex&#xff0c;心态都差不多&#xff1a;官网下载、装好插件、登录账号&#xff0c;然后打开编辑器就等着它自动帮我把代码写完。结果呢&#xff1f;要么插件面板一片空白&#xff0c;要么敲了半天没…

作者头像 李华
网站建设 2026/9/28 16:40:21

Allegro Skill自定义菜单加载实战:从零部署可交付工具

1. 项目概述&#xff1a;为什么Allegro Skill二次开发是PCB工程师的“第二把扳手”在Cadence Allegro PCB设计流程里&#xff0c;菜单栏上那些灰掉的按钮、重复十遍的手动操作、每次改版都要重画的铜皮区域——它们不是软件缺陷&#xff0c;而是你还没拿到那把真正的“定制化扳…

作者头像 李华
网站建设 2026/9/28 16:39:01

Halcon集成YOLO目标检测:工业视觉实战路线与避坑指南

在工业视觉圈子里&#xff0c;Halcon 和 YOLO 的关系一直有点微妙。前者是商业机器视觉的老牌劲旅&#xff0c;算子稳定、标定精准、亚像素测量是看家本领&#xff1b;后者是深度学习目标检测的当红方案&#xff0c;迭代快、生态活跃、社区资源丰富。很多做产线检测的朋友都遇到…

作者头像 李华
网站建设 2026/9/28 16:38:24

Ollama本地模型跑AI编程:显存配置、Modelfile调优与四类任务实测

1. 为什么我会折腾本地模型跑 AI 编程这件事去年下半年开始&#xff0c;我在几个 C# 和 Python 项目里密集用 AI 编程助手。云端方案确实省心&#xff0c;但有几个场景让我越来越难受&#xff1a;公司内网项目代码不能外传、出差路上网络不稳定、按 token 计费月底账单看着肉疼…

作者头像 李华
网站建设 2026/9/28 16:37:50

PULSE神经图像编解码:单线程CPU实现1080p实时解码

1. 这个编解码器到底在解决什么问题图像编解码这件事&#xff0c;过去十几年基本被传统方案统治着。JPEG、WebP、AVIF、HEIC&#xff0c;这些名字你可能天天见&#xff0c;它们的共同点是&#xff1a;压缩率靠手工设计的变换、量化、熵编码一步步堆出来&#xff0c;想再往前挪一…

作者头像 李华
网站建设 2026/9/28 16:37:37

大模型推理优化实战:从PyTorch到300ms低延迟的系统性调优方法论

1. 项目概述&#xff1a;Model-Optimizer 不是“一键加速器”&#xff0c;而是一套面向生产级大模型推理的系统性调优方法论你搜“Model-Optimizer”&#xff0c;十有八九会撞上一堆 TensorRT、vLLM、NVIDIA 驱动报错、CUDA 版本冲突、显卡识别失败的帖子——这恰恰说明&#x…

作者头像 李华