简介:面向目标检测初学者与品牌视觉识别项目开发者,提供一套可口可乐和百事可乐双类别标志检测数据集。资源包含对应JPG图片及Pascal VOC格式XML、YOLO格式TXT两套标注文件,可直接用于YOLO、Faster R-CNN等主流检测模型的训练与评估。所有标注由labelImg以矩形框逐张完成,CocaCola框数2268、Pepsi框数2398,总框数4666,类别数量比较均衡,有利于减少模型训练中的样本不均衡问题;类别体系简单,只需区分可口可乐与百事可乐,适合作为二分类目标检测的练习数据。压缩包内文件总数2000个,以XML标注文件为主,并含TXT文件,整体大小约120.84MB;XML标注可配合VOC生态工具完成数据检查与划分,TXT标注则可直接被YOLO系列训练脚本读取,免去自行转换标注格式的环节。数据集只提供准确合理的标注结果,不捆绑任何预训练权重,使用者可按需划分训练集、验证集,自行构建检测流程。目前已有347人学习,适合正在准备品牌标志识别项目、需要规范双格式数据并快速进入模型训练阶段的学习者。
1. 目标检测数据集:2220张可口可乐与百事可乐标志,二分类边界在哪
准备做无人货柜、货架识别或广告投放监测的人,大概率会遇到同一个需求:在画面里找到“可口可乐”和“百事可乐”的标志。这个目标检测数据集给的正是这组素材,2220张图片,两个标签Coke与Pepsi,压缩包里同时带了VOC和YOLO两种标注格式,对新手很友好,对熟手来说可以直接喂给训练脚本。注意,小样本不等于简单任务。二分类做起来最怕的不是“分不清”,而是标注口径和验证集划分不干净,导致本地指标好看、部署后翻车。这组数据能不能用,取决于你怎么定义使用边界。
2. 拆解 VOC 与 YOLO 双格式:目录结构、标签映射和一键校验脚本
2.1 双格式的目录结构:VOC的XML和YOLO的txt各管什么
解压后先别急着训练,花10分钟把目录结构看清楚。常见的VOC组织方式是这样的:
- JPEGImages/:存放JPG原图,文件名一般是数字id或带语义的名字,例如000001.jpg。
- Annotations/:存放同名XML文件,每个XML记录这张图里所有目标的位置和类别。
- ImageSets/Main/:存放划分列表,常见的是train.txt、val.txt、trainval.txt,内容只有文件名,不带扩展名。
- labels/ 或 txt/:存放YOLO格式的标签文本,每行一个目标。
XML里真正有用的是object这个节点。name字段写类别名(通常叫Coke或Pepsi),bndbox字段写xmin、ymin、xmax、ymax,这四个值都是绝对像素坐标,左上角为原点,单位就是像素。图片宽度高度在size节点下,训练脚本会读取它做归一化。
YOLO格式的txt每行五个数:类别id、x_center、y_center、width、height,全部归一化到0到1之间。
0 0.512345 0.600123 0.123456 0.234567 1 0.213456 0.345678 0.987654 0.456789第一个数0和1对应类别表,x_center是框中心点在宽度方向的比例,width是框宽占图片宽度的比例。注意txt里不写图片宽高,因为训练时会从images目录读取原图尺寸反推。把这两套格式放在同一份压缩包里,本质是给训练框架开方便之门,但如果两份标注不是从同一次标注导出的,就会埋下隐患,后面会专门讲这个坑。
2.2 二分类标签的边界:检测目标是“标志”还是“商品”
“2个标签”指的是类别数。常见约定是这样的:
- Coke:可口可乐标志,包括瓶身贴纸、易拉罐面上的弧形logo。
- Pepsi:百事可乐标志,包括圆形商标、瓶盖上的小标志。
这里有个边界问题值得在动手前想清楚。如果把整个瓶身当成标注框,模型学到的是“可乐瓶的剪影”,而不是标志本身。货架上相似颜色、相近形状的竞品瓶很容易触发误检,因为形状特征比logo特征更容易被神经网络抓到。反过来,如果标注严格贴着标志画框,模型学到的才是“标志检测”。这个数据集标题写的是标志检测,那么正常预期是更贴近后者。拿到数据后,建议随机打开20张图看标注框是不是真的贴在logo上。框比logo大50%以上就要警惕,这会影响小目标识别和遮挡场景的表现。
另一个容易忽略的是场景多样性。2220张图如果全是在相对干净的背景上拍的,模型就只能记住“摆在正中间的标志”这个朴素模式。部署到货架、冷柜、卡车侧边广告这类复杂背景时,性能会明显下降。所以使用之前先按拍摄场景分桶统计,而不是只看总量。
2.3 校验两套标注是否一致:三分钟脚本排查
双格式数据集最值得做的一件事,是写脚本验证VOC和YOLO标注是否真的对得上。对不上的典型结果是训练时loss抖动,验证集mAP和手动抽查结果互相打架。我一般用下面这种方式做交叉验证。
import os import xml.etree.ElementTree as ET from PIL import Image def read_voc_boxes(xml_path, img_w, img_h): """读取VOC XML,返回归一化后的[xmin, ymin, xmax, ymax]列表""" tree = ET.parse(xml_path) root = tree.getroot() boxes = [] for obj in root.iter('object'): name = obj.find('name').text bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) / img_w ymin = float(bbox.find('ymin').text) / img_h xmax = float(bbox.find('xmax').text) / img_w ymax = float(bbox.find('ymax').text) / img_h boxes.append((name, xmin, ymin, xmax, ymax)) return boxes def read_yolo_boxes(txt_path): """读取YOLO txt,转成归一化后的[xmin, ymin, xmax, ymax]列表""" boxes = [] with open(txt_path, 'r', encoding='utf-8') as f: for line in f.readlines(): parts = line.strip().split() if len(parts) != 5: continue cls_id, xc, yc, w, h = parts xc = float(xc) yc = float(yc) w = float(w) h = float(h) xmin = xc - w / 2 ymin = yc - h / 2 xmax = xc + w / 2 ymax = yc + h / 2 boxes.append((int(cls_id), xmin, ymin, xmax, ymax)) return boxes def compute_iou(a, b): """计算两个归一化框的IoU""" x1 = max(a[1], b[1]) y1 = max(a[2], b[2]) x2 = min(a[3], b[3]) y2 = min(a[4], b[4]) inter = max(0.0, x2 - x1) * max(0.0, y2 - y1) area_a = max(0.0, a[3] - a[1]) * max(0.0, a[4] - a[2]) area_b = max(0.0, b[3] - b[1]) * max(0.0, b[4] - b[2]) union = area_a + area_b - inter return inter / union if union > 0 else 0.0这里两个读取函数都做了同一件事:把标注统一换算成归一化坐标,再用IoU判断两份标注里的框是否匹配。代码里有两个关键参数值得注意:一是归一化分母用的是PIL读出来的真实图片宽高,而不是XML里size节点写的值,因为XML的size在某些后期编辑场景下可能和实际图片不一致;二是IoU判匹配的阈值,一般取0.85以上算匹配。如果IoU低于0.5但类别相同,说明框的位置有明显偏移,多半是转换脚本写错了坐标换算。
# 批量比对:同一张图,两份标注应该基本一致 voc_xml_dir = 'Annotations' yolo_txt_dir = 'labels' img_dir = 'JPEGImages' matched = 0 unmatched = 0 for name in os.listdir(voc_xml_dir): base = os.path.splitext(name)[0] xml_path = os.path.join(voc_xml_dir, name) txt_path = os.path.join(yolo_txt_dir, base + '.txt') img_path = os.path.join(img_dir, base + '.jpg') if not os.path.exists(txt_path) or not os.path.exists(img_path): print(f'缺失: {base}') continue w, h = Image.open(img_path).size voc_boxes = read_voc_boxes(xml_path, w, h) yolo_boxes = read_yolo_boxes(txt_path) # 贪心匹配:每个YOLO框找IoU最大的VOC框 used = set() for yb in yolo_boxes: best_iou = 0.0 best_idx = -1 for i, vb in enumerate(voc_boxes): if i in used: continue iou = compute_iou(vb, yb) if iou > best_iou: best_iou = iou best_idx = i if best_iou >= 0.85: matched += 1 used.add(best_idx) else: unmatched += 1 print(f'不匹配: {base}, max iou={best_iou:.3f}') print(f'匹配框数: {matched}, 不匹配框数: {unmatched}')这个批处理脚本有几个设计细节。used集合记录已经被匹配的VOC框,避免一个VOC框同时和两个YOLO框匹配,这在同一张图里有两个紧密挨着的瓶罐时很常见。unmatched只统计YOLO侧找不到高IoU框的个数,反向的VOC侧冗余框也会体现在这里,但要想精确对应到XML里的具体框,可以在print处补充输出VOC框的索引,自查时会更快。
如果输出大量“不匹配”项,优先怀疑三类问题:一是两份标注里有空XML或空txt没有正常生成;二是类别名拼写不一致,比如一个文件里写Coke,另一个写coca_cola,读取时索引错位;三是转换脚本把中心点除以图片尺寸时用了0.9之类缩放系数,导致所有框整体偏移。先修数据,再谈训练。
3. 用 YOLO 训练这个二分类数据集:转换脚本、关键参数与评估口径
3.1 把自备图片转成训练格式:VOC转YOLO脚本与参数说明
训练这个数据集之前,最常见的准备工作是做数据格式统一。如果你的自备图片标注是LabelImg输出XML,那就把VOC格式转成YOLO格式。我每次都会先写一个一次性函数,转换成txt后立刻抽查三张图,用可视化脚本把框画回去看。
import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, class_names, out_txt_path, img_w, img_h): """把单张VOC标注转为YOLO txt""" tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_names: print(f'跳过未知类别: {name}') continue cls_id = class_names.index(name) bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) ymin = float(bbox.find('ymin').text) xmax = float(bbox.find('xmax').text) ymax = float(bbox.find('ymax').text) dw = 1.0 / img_w dh = 1.0 / img_h x_center = (xmin + xmax) / 2.0 * dw y_center = (ymin + ymax) / 2.0 * dh w = (xmax - xmin) * dw h = (ymax - ymin) * dh lines.append(f'{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}') with open(out_txt_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines))这个函数的两个关键参数是class_names的顺序和img_w/img_h。class_names的顺序决定了最终txt里的class id,训练时必须和数据集YAML里names列表保持完全一致,比如names: ['Coke', 'Pepsi'],那么txt里0就是Coke,1就是Pepsi。img_w和img_h建议用PIL读原图获得真实宽高,不要直接从XML的size节点读,因为部分标注工具在裁剪后没有更新XML里的size。
还有一种常见需求是把Yolo格式转回VOC格式用于某些平台展示。反过来写即可:txt里xc、yc、w、h乘以各自的长宽得到绝对像素xmin/xmax/ymax/ymin,然后组织成XML结构。转换后生成一份新的XML目录,尽量不要覆盖原始Annotations,因为原始VOC标注如果本身质量更高,后续做数据修正时还需要拿来做底稿。
3.2 训练参数怎么设:2220张图的小样本调参经验
训练这类小数据集,参数选择最忌讳直接套大模型训练参数。先用YOLOv8n这类轻量权重的常用做法起步,看baseline再加大。官方提供的默认参数是锚点基准,但不是小数据集的推荐方案。
| 参数 | 推荐值 | 理由 |
|---|---|---|
| imgsz | 640(显存受限用512) | 标志面积小,过小分辨率会丢失细节 |
| epochs | 150 | 小数据集训练快,跑太少达不到收敛 |
| batch | 16 | 受显存限制,8也能跑,越小波动越大 |
| patience | 40 | 验证集停滞40个epoch即停止 |
| mosaic | 0.5到1.0 | mosaic增强对小目标会切得更碎 |
| hsv_h | 0.015 | 轻微色相扰动,平衡红蓝瓶色 |
| hsv_s | 0.7 | 增强饱和度变化,模拟不同打光条件 |
| fliplr | 0.5 | 左右翻转,标志形态对称,安全 |
训练命令按当前主流yolo系列可以写成:
yolo train data=cola.yaml model=yolov8n.pt epochs=150 imgsz=640 batch=16 patience=40如果你的环境还在用yolov5风格,则用:
python train.py --data cola.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 150 --patience 40其中的cola.yaml是数据配置,路径指到训练根目录:
path: ./cola_dataset train: images/train val: images/val names: 0: Coke 1: Pepsinames的顺序必须和转换脚本里的class_names一致,这里出错极其隐蔽,训练出来的模型类别标签会整体错位。pre-trained权重是否可用取决于你的网络环境,如果本机没有预训练权重,可以先把batch调小到8、freeze前几层,从零训练小数据更容易过拟合,叠加early stopping会好一些,但mAP大概率不如用预训练权重起步。
数据增强参数不要一上来就开满。mosaic合成图会让小标志变形,在高分辨率下表现反而是负优化。先用官方默认,然后只开hsv_h、hsv_s、fliplr,mosaic调到0.5。真正常见的稳定配置是mosaic 0.5到0.8配合copy_paste 0.1这类额外增强,具体要看你的部署环境。
3.3 评估口径:mAP 高不代表能落地
训练完成后,先看验证集mAP@0.5和mAP@0.5:0.95。二分类问题里,只报“总体mAP”是在逃避问题。要分别看Coke和Pepsi的AP80到90段。如果Pepsi的AP比Coke低一大截,常见原因是Pepsi标志是圆形,在透视角度下会变成椭圆,标注框覆盖面积大而有效特征少。可口可乐的弧形logo外框长宽比稳定,模型更好学。
从训练结果目录里找到confusion_matrix.png和PR曲线保存页,重点关注两类之间的互检错误。如果发现“Coke预测成Pepsi”的格子明显偏高,说明两个品牌在某个光照条件下的特征区分度不足,常见解决办法不是加数据,而是先做颜色归一化预处理,或给检测结果后接一个小分类网络。
验证另一件事是画框可视化。YOLO训练完会在runs/val下生成val_batch*.jpg,里面是模型在验证集上的预测框。逐张看这些图和标注框的偏差,特别是小标志的图。如果模型在验证集上每个Pepsi小标志都漏检,那说明训练时的锚框或分辨率设置不合适,建议把imgsz提高到768或使用切片推理,而不是继续加训练轮数。
4. 品牌标志检测避坑指南:5 个容易翻车的问题与处理办法
4.1 VOC 与 YOLO 双格式标注对不上
现象:训练时loss和mAP曲线波动剧烈,验证集分数时而0.9时而0.4,手动抽查发现同一张图在两份标注里框的位置和数量不一样。
原因:标注工具输出VOC XML后,做了一次自动转换生成YOLO txt。但操作时图片被二次裁剪过,XML里记录的原始坐标没有跟着更新,导致txt里归一化坐标与原图尺寸错位。还有可能是两份标注来自不同批次,某一批只标了部分图片,另一批补标了另一部分,加载时又合并到一起。
解决:不要自己肉眼去判断用哪个格式。先跑前面那个校验脚本,把不匹配的文件名全部导出,单独检查。检查时打开原始图和两份标注叠加的显示,确认哪一份是错误来源。如果是从VOC转的,就以VOC为准重新转换;如果是工具直接导出,就以txt为准重新转voc。修完再开始训练。
注意:拿到压缩包后先跑校验,不要为了省时间直接开局。
4.2 标志太小,模型经常漏检
现象:检测结果里远处货架罐瓶上的标志被漏掉,precision很高,recall偏低,线上监控发现漏检的都是画面里面积占比小于3%的目标。
原因:YOLO默认下采样倍率高,小尺寸标志经过多层卷积后特征几乎消失。如果训练时imgsz使用416,一个在原始图里只有18×18像素的logo,在网络里只占不到两个像素。
解决:第一优先把imgsz提到640或768。显存不够就切成单目标推理,或者用切片推理把整张大图拆成512×512的小块分别预测,再合并结果。合并时注意相邻切片的overlap至少20%,否则会切坏标志。训练侧还可以给mosaic概率降低,因为mosaic会把多个小标志压缩成更小的目标,进一步加大学习难度。
4.3 反光、过曝、白平衡漂移让标志颜色不准
现象:白天阳光直射下的易拉罐高光区域被模型误判为“标志”,或者暗光环境里可口可乐标志的红色发黑,被划到Pepsi。
原因:易拉罐表面曲率大,高光容易覆盖logo,加上不同光源下白平衡差异明显,红色和蓝色灯色在规范化后可能接近。网络主要学颜色特征时,很容易被这种光照噪声带偏。
解决:训练侧把hsv_s开到0.7以上,hsv_v开到0.4以上,模拟曝光变化。部署侧尽量固定相机位置和曝光参数,避免自动曝光在背光时剧烈变化。条件允许的时候加偏振片,能直接滤掉高光干扰。如果已经训练完了才发现问题,在预处理环节加一次灰度化白平衡校正,效果类似给模型修正颜色偏差。
4.4 Coke 和 Pepsi 混淆:颜色学得太多,形状学得太少
现象:这是二分类场景最常见的翻车点。模型在近距离高分辨率图上表现得非常好,拉远到3米后,Coke和Pepsi互相误判明显增大。混淆矩阵里两家主要错在对方类别上。
原因:两个品牌标志颜色对比较强,红对蓝,这在数据集主体环境下容易被当作决定性特征。一旦光线改变,红蓝接近,特征就失效。形状上,可口可乐的弧形飘带更细,百事的圆形更实,网络在特征图下采样后很难保留这种精细的轮廓信息。
解决:给输入图像加一个亮度归一化或灰度化预处理,强迫模型学形状。也可以在检测框crop后接一个小的图像分类头,专门对两个品牌做二分类,这个分类头可以用预训练权重复用,效果比直接改检测模型更快。数据层面可以补充“两个品牌同框”的图片,让模型被迫学习区别特征而不是全部框成一个类。
4.5 数据泄漏:train 和 val 来自同一场景的连续帧
现象:本地验证集mAP@0.5高达0.98,线上测试却只有0.65,而且总是漏焦点。逐张检查发现验证集里很多图片和训练集几乎一样,只是差了10帧。
原因:视频抽帧数据集如果不按场景分组,随机划分会把同一视频里高度相似画面同时分到train和val。模型等于提前看过答案,自然表现出极高分数。
解决:划分前先按视频ID分组。如果数据集没有视频ID信息,用图像的感知哈希做去重。
import imagehash from PIL import Image def phash_similar(img_path1, img_path2, threshold=5): hash1 = imagehash.phash(Image.open(img_path1)) hash2 = imagehash.phash(Image.open(img_path2)) return hash1 - hash2 <= threshold用phash对全量图片两两计算汉明距离,距离小于5视为重复图。然后从验证集里剔除与训练集重复的图片,保证验证集真正独立。这个方法对JPEG压缩、缩放、亮度微调有很强的鲁棒性,但对同一物体不同角度拍摄不敏感,正好覆盖视频抽帧这种场景。
划分完成后应该做一次分组统计,确认train和val里两类的图片数量和标志实例数分布大致一致。如果train有92%的Coke图片,val却只有60%,那train出来的模型在正式环境里必然偏向Coke。
5. 把 2220 张可乐数据用出效果的三个进阶技巧
5.1 切片推理解决小标志漏检
如果imgsz已经调到640还是漏检远处小标志,不要急着加训练数据,先用切片推理验证上限。做法是把推理时的原图按固定尺寸切片,每块独立预测,再按坐标合并。常用切图尺寸是640×640,overlap 0.2。合并时要处理重复框,可以用NMS把同一目标在多个切片里的预测框合并。这样能直接提升小标志召回率,代价是推理时间增加,适合固定点位摄像头实时检测,不适合分布式大批量离线任务。
5.2 训练之前先定一条过关线
我习惯在第一次训练前先写下一份验收清单,写在项目笔记最上面:mAP@0.5不低于0.9,Coke和Pepsi分别的召回率不低于0.9,小标志子集mAP不低于0.85。没有达标就回到数据或参数上调整,而不是直接去调部署脚本。这个习惯帮我避免了很多“看起来模型很好,上线却翻车”的场面。
5.3 加批硬负样本当后悔药
模型训练完后,把测试集里所有误检结果存成crop,人工过一遍。标为“错误目标”的图片不需要太多,500张就能明显压掉常见的误检。把它们连同原始正确标注一起加入训练集,重训一轮。这类错误往往集中在特定背景纹理、特定角度、特定光照,反向学习后效果立竿见影。这个方法比盲目再增加正确标注的图片更划算,因为误检本身就是模型暴露给我们的明确弱点。
我第一次做品牌标志识别时,拿到类似的数据集,直接开了训练,看验证集mAP很高就觉得很稳。上线才发现验证集和训练集基本来自同一段连续视频,模型根本没学会泛化。后来养成了一个习惯:任何数据集先做视频ID或感知哈希去重,再谈训练。这组2220张的双格式数据值得认真对待,先把数据集校验干净,再谈模型调参,希望帮到你。
本文还有配套的精品资源,点击获取