简介:面向YOLO系列算法目标检测实战的罐头和瓶子数据集,包含1531张带标签图像,已划分训练集、验证集和测试集,并附有data.yaml配置文件,可直接适配yolov5、yolov7、yolov8、yolov9、yolov10及yolo11等主流版本,无需额外整理即可训练和验证。压缩包共2000个文件,其中1073个XML文件与927个TXT文件分别对应VOC格式和YOLO格式标注,方便在两种主流标注体系间切换和对照学习,整体大小68.59MB。目前已有54人学习浏览,适合目标检测入门者快速体验完整的数据准备与模型验证流程。数据集不仅提供标准标注,还包含YOLO格式的类别索引、中心坐标及宽高比例说明,且标签已按训练、验证、测试分类存放;配合VOC格式可深入对比差异,data.yaml使用户能直接启动训练,两种标签文件分目录管理,结构清晰,是练习模型部署与参数调优的实用素材。
1. 罐头和瓶子数据集:1531张带标签图像到底够不够训练一个能用的YOLO模型
做工业质检、商超货架识别、回收分拣这类项目时,最头疼的不是模型结构,而是手上没有带标签的数据。YOLO算法本身很成熟,部署教程也遍地都是,但真正卡住进度的地方是数据集的质量和数量。这份「罐头和瓶子数据集」一共1531张图像,带YOLO格式的txt标签,目标覆盖罐头、瓶子,还有一个容易被忽略的子类——鲜奶。对这个规模的数据集,很多人第一反应是“太少了,肯定训练不出好东西”,实际用下来恰恰相反:单类目标、场景相对固定、标签干净的前提下,1500张图足够把模型跑到可用的水平,关键看你有没有把数据整理到位。
这篇文章就围绕这套数据集,从解压zip开始,到标签格式校验、目录整理、训练配置,再到踩坑记录和验证技巧,按一条能落地的路径讲清楚。适合正在学YOLO训练、做毕设,或者刚接手一个检测项目但不知道从哪里下手的初学者,也适合想快速验证一个数据集能不能用的熟手。我不会讲太多原理层面的东西,重点是你拿到zip之后每一步该做什么、命令怎么写、参数怎么调、翻车了怎么排查。
2. 拆开zip看数据:YOLO标签格式、类别分布与鲜奶子类的识别难点
2.1 YOLO标注文件怎么读:一行五个数字代表什么
用labelimg之类的工具打标完成后,每张图像会对应一个同名的txt文件,这是YOLO系列的通用标注格式。拿这份数据集里一张标注了罐头和瓶子的图举例,它的txt文件内容长这样:
0 0.482031 0.517881 0.246875 0.537037 1 0.769531 0.480556 0.153125 0.535185 2 0.295703 0.542824 0.161719 0.481481每一行代表一个目标框,由5个数字组成。第一个数字是类别id,从0开始数,这张图里出现了3个类别的目标;后面四个数字分别是归一化后的中心点x坐标、中心点y坐标、框宽度、框高度。所谓归一化,就是把像素值除以图像的宽和高,所有值都落在0到1之间。
这就有个很容易忽略的细节:坐标看的是像素相对比例,不看绝对像素。假设原图宽度是1024,某个框中心x方向在512像素处,那么txt里记录的就是0.5。写代码解析标签时,一定要记得把归一化坐标乘回原图宽高,才能画出真正的框。反过来,如果你要手动改标签,也必须把像素坐标除以图像宽高,否则训练时YOLO会把框的位置完全理解错。这个转换关系是后面所有数据清洗工作的基础,建议先花几分钟把这份数据集随机抽几张,跑一遍上面的解析逻辑,确认数据和图像能对上再继续。
2.2 1531张图里到底有多少个目标:统计类别分布和边界情况
拿到数据集之后,我一般不会急着训练,先写一个统计脚本,搞清楚每个类别的目标数量、图像尺寸分布、有没有空标签文件。这一步能提前暴露很多问题,比如某个类别样本极少、标签文件损坏、图像尺寸不统一等。下面这个脚本可以直接跑:
import os from collections import Counter from PIL import Image img_dir = "images" # 图像目录 label_dir = "labels" # 标签目录 class_counter = Counter() # 统计每个类别的目标数量 img_sizes = Counter() # 统计图像尺寸 empty_labels = [] # 记录空标签 label_count = 0 # 标签文件总数 for label_name in os.listdir(label_dir): if not label_name.endswith(".txt"): continue label_path = os.path.join(label_dir, label_name) img_name = label_name.replace(".txt", ".jpg") img_path = os.path.join(img_dir, img_name) if not os.path.exists(img_path): print(f"图像缺失: {img_name}") continue with open(label_path, "r", encoding="utf-8") as f: lines = [line.strip() for line in f.readlines() if line.strip()] if len(lines) == 0: empty_labels.append(label_name) continue for line in lines: parts = line.split() if len(parts) != 5: print(f"标签格式异常: {label_name} -> {line}") continue class_id = int(parts[0]) class_counter[class_id] += 1 # 记录每张图的尺寸 img = Image.open(img_path) img_sizes[img.size] += 1 print("类别统计:", dict(class_counter)) print("图像尺寸分布:", dict(img_sizes)) print("空标签数量:", len(empty_labels))这个脚本做的事情很简单:逐个读取标签文件,检查图像是否存在,统计每个类别的框数量、图像的尺寸分布,同时找出空标签和格式异常的行。类别统计非常关键,它能直观告诉你数据集的平衡程度。比如发现罐头类有2000多个框,但鲜奶类只有300个框,那你训练时就要给鲜奶类提高损失权重,或者做针对性的数据增强,否则模型天然会偏向多数的罐头类,鲜奶的召回率会低得可怜。
图像尺寸分布同样值得注意。如果1531张的尺寸五花八门,训练时统一resize到640x640会产生不同程度的形变拉伸,这会影响小目标的检测精度。看到这种情况,可以按尺寸聚类一下,确定一个合适的目标尺寸,或者训练前先做letterbox预处理。
2.3 鲜奶和瓶子容易混:标注边界模糊带来的训练陷阱
这套数据集的标题把「鲜奶」和「瓶子」并列,说明标注方认为它们是不同类。但实际看一眼标注你会发现,很多鲜奶产品本身就有瓶身或者盒身结构。鲜牛奶装在玻璃瓶里、塑料瓶里、纸盒里,从外形上和「瓶子」高度相似。如果你的标注原则没定清楚,模型就会学到一团浆糊的特征。
常见做法是把标注原则定为:只要目标物是鲜奶产品本身,不管容器是瓶还是盒,都标记为鲜奶类;单独的、不属于鲜奶的空瓶才标记为瓶子类。这样每个类别内部的视觉一致性才够强。标注边界如果不够清晰,训练出来的模型多半会在鲜奶和瓶子之间反复横跳,推理时同一个目标这次判瓶子、下次判鲜奶。
你可以在拿到数据集后随机抽100张图,把txt标签和图像画在一起,人工做一遍一致性抽检。如果发现标注原则不统一,最稳妥的做法是重新分组:把所有鲜奶类目标挑出来,单独用一个类别id重新打标,瓶子只保留非鲜奶的瓶类目标。这个前置的清洗工作约等于人工复核一遍标签,1500多张图大概需要一个下午,但带来的收益非常直观——mAP能提升好几个点。
3. 把zip整理成可训练的YOLO目录:解压、格式校验与划分训练集验证集
3.1 YOLO官方约定的目录结构:images和labels必须对应
解压完zip之后,里面的目录结构大概率不是YOLO直接能读的格式。常见的情况是:图片在某个文件夹里,标签在另一个文件夹里,还可能嵌套多层子目录。YOLOv5和YOLOv8默认的数据集目录结构是这样的:
dataset/ ├── images/ │ ├── train/ │ │ ├── img001.jpg │ │ └── ... │ └── val/ │ ├── img101.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── img001.txt │ │ └── ... │ └── val/ │ ├── img101.txt │ └── ... └── data.yamlimages和labels各自下面再分train和val两个子目录,一一对应。文件名必须完全一致,只是后缀不同,训练脚本按文件名去找对应的标签。很多人会在这里翻车:图片是.jpg、标签却是.JPG或者.png,又或者文件名带了空格、中文,这些都会导致训练时图像被读取但标签对应不上,自动跳过,最后loss异常或者目标数量统计为0。
这一步没什么好取巧的,老老实实写一个脚本,把文件名统一,后缀统一为小写,图片复制到images/train,标签复制到labels/train。如果文件名里有中文或者特殊字符,建议直接用数字序号重命名。文件名里的中文在Windows下没有问题,但放到Linux服务器上跑训练时容易遇到编码问题,为了省事,一步到位全改掉。
3.2 划分训练集和验证集:随机种子和按目录划分的区别
1531张图不需要划分测试集,留出训练集和验证集就够了,划分比例我一般用9:1或者8:2。这里的关键是以图像为单位随机划分,不是以文件夹为单位。如果原本的目录结构是一个文件夹放50张图,你按文件夹划分,训练集和验证集的特征分布就会失衡。
下面的脚本按比例划分,并把结果输出到YOLO需要的目录结构:
import os import random import shutil random.seed(42) # 固定随机种子,保证结果可复现 img_dir = "原始图片目录" label_dir = "原始标签目录" output_dir = "dataset" train_ratio = 0.9 all_images = [f for f in os.listdir(img_dir) if f.lower().endswith((".jpg", ".jpeg", ".png"))] random.shuffle(all_images) train_count = int(len(all_images) * train_ratio) train_images = all_images[:train_count] val_images = all_images[train_count:] def copy_files(images, split): img_out = os.path.join(output_dir, "images", split) label_out = os.path.join(output_dir, "labels", split) os.makedirs(img_out, exist_ok=True) os.makedirs(label_out, exist_ok=True) for img_name in images: stem = os.path.splitext(img_name)[0] # 找到同名标签文件,不区分后缀大小写 label_name = None for suffix in (".txt", ".TXT"): cand = stem + suffix if os.path.exists(os.path.join(label_dir, cand)): label_name = cand break if label_name is None: print(f"警告: {img_name} 没有对应标签,跳过") continue shutil.copy(os.path.join(img_dir, img_name), os.path.join(img_out, img_name)) shutil.copy(os.path.join(label_dir, label_name), os.path.join(label_out, label_name)) copy_files(train_images, "train") copy_files(val_images, "val") print(f"训练集: {len(train_images)} 张") print(f"验证集: {len(val_images)} 张")这段脚本的重点是固定了random.seed(42)。深度学习训练里有太多随机因素了,不固定种子,两次训练的结果可能差很多,会把调参变成玄学。固定随机种子至少保证数据划分是可复现的,你改了一个超参数后重新训练,对比结果时才分得清是改参数的效果还是数据切分的偶然。另外注意脚本里对标签后缀做了大小写兼容处理,这看起来是件小事,实际运行中遇到的概率一点都不低。
3.3 针对罐头、瓶子和鲜奶的数据增强:反光、遮挡与样本不均衡
罐头和瓶子这类产品最典型的视觉干扰是反光。金属罐的表面对光线的反射会造成高光区域,这部分区域的纹理完全丢失;玻璃瓶身则会透出背景,边缘和背景融为一体。如果直接拿原始数据训练,模型很容易把高光当成目标的一部分,推理时换了光照就漏检。
所以划分好目录之后,我建议做一轮针对性的离线增强。常见的做法是复制原始样本,做三类变换:亮度随机调整(模拟光照变化)、高斯噪声(模拟摄像头传感器噪声)、随机裁剪缩放(模拟不同拍摄距离)。需要说明的是,做增强时要连标签一起变换,不能只增强图片,否则标签和图像内容对不上。
翻转增强要慎重。罐头和瓶子不是完全对称的,特别是带标签文字的瓶身,左右翻转之后文字是反的。如果训练时用了水平翻转参数,模型会同时见过正反两向的文本特征,推理时对真实场景的文字方向就失去了判断依据。像这类带文字信息的目标,我一般把翻转增强关掉,或者翻完以后用OCR先检测一下文字是否正常,再做取舍。
对于鲜奶类别数量偏少的情况,可以在增强时提高这个类别的采样概率。典型的做法是:统计每个类别在训练集中的框数量,给少的类别每次迭代多复制几份参与训练。YOLOv8本身支持按类别设置采样权重,你也可以用离线的方式,对含鲜奶目标的图片做3到5次增强,扩充到训练集里。这个操作对鲜奶的召回率提升非常明显。
4. 用YOLOv8训练罐头和瓶子识别模型:环境准备、数据配置与参数调整
4.1 data.yaml怎么写:路径、类别名和类别id的顺序不能错
进入训练之前,先把数据配置文件写对。YOLOv8读的是data.yaml,内容非常简单,但写错一个地方训练就跑了半天发现loss不降。完整配置如下:
# data.yaml path: /absolute/path/to/dataset # 数据集根目录,建议写绝对路径 train: images/train # 训练集图片目录,相对于path val: images/val # 验证集图片目录,相对于path names: 0: can # 罐头 1: bottle # 瓶子 2: milk # 鲜奶这里有两个最容易出错的点。第一个是path字段:如果填相对路径,YOLOv8会根据当前工作目录去拼接,不同环境工作目录不一致就直接报错找不到图片。我通常直接写绝对路径,一了百了。第二个是names字段的顺序:类别id必须和txt标签里的第一个数字完全对应。这份数据集里,标签文件第一行第一个数字如果是0,那names里的第0项就必须是can,不能把类别名词的顺序打乱了。如果标签里的id和names不匹配,训练不会报错,但验证时看到的mAP会莫名其妙地低。
还有一个细节:names列表在YOLOv8里是从0开始编号的,如果你写names的时候漏了中间某一项,比如只写了1和2没写0,或者不小心把字典写成了从1开始,前面所有标签为0的目标全部会被忽略。常见做法是训练前写一个小脚本,把训练集所有标签里出现过的类别id打印出来和names对一遍,确认完全一致再开工。
4.2 训练命令与关键参数:imgsz、epochs、batch的合理范围
YOLOv8在Anaconda环境下的安装和训练一条命令就能跑起来。假设你的数据集已经整理成了上一章的结构,训练命令长这样:
yolo detect train \ data=/absolute/path/to/data.yaml \ model=yolov8n.pt \ epochs=120 \ imgsz=640 \ batch=16 \ project=runs/detect \ name=can_bottle_milk \ workers=4 \ seed=42用yolov8n.pt而不是yolov8s.pt或更大的模型,是有考虑的。这份数据集本身只有1531张图,属于中小规模。模型越大,过拟合风险越高。预训练权重yolov8n.pt是COCO数据集上训出来的,虽然COCO里没有「鲜奶」这类细分类,但底层的边缘、纹理、形状特征已经学到了,迁移过来只需要在顶层做微调,能让小数据集的收敛速度大幅加快。
参数方面几个重点说一下。imgsz=640是性价比最高的输入尺寸,太大显存不够,太小罐头上的小字标签会糊掉;如果你的图像分辨率普遍很高,比如都是3000x3000的工业相机图,可以尝试imgsz=960,但显存占用会翻倍。batch=16是8G显存环境下的常规值,实际要看显存动态调整,显存不够时优先降到8,不要直接换小模型回避问题。epochs=120在小数据集上略偏多,一般60到80轮就已经收敛,多出来的轮数主要用来观察验证集的mAP是不是过了峰值开始回落,方便你判断什么时候应该提前停。
训练过程中日志输出的loss数值会持续下降,刚开始下降很快,后面变平缓,这是正常的。验证集的mAP每一轮都会更新,等到验证集的mAP连续20轮不再上升,说明已经收敛,可以按Ctrl+C手动停掉,不必等epochs跑完。
4.3 中断恢复与显存不足:训练到一半失败的应对方案
训练到一半最常见的中断原因是显存不够。报错信息一般是CUDA out of memory。这个问题的解决思路是先降batch,降到8还不行就降到4,再不行就换更小的模型。还有一个容易忽略的点是workers数值,workers是数据读取的进程数,配置过大会占大量内存,不过这块报错通常和CUDA无关,是系统内存满了。
如果训练中断了,yolo命令提供了一个断点续训的参数。代码里写resume=True就行:
yolo detect train resume=True这条命令会读取runs/detect/你的项目名/weights/last.pt,从上次中断的轮次继续训练,不需要重新跑前面几十轮。使用resume前要确认中断前后代码版本是一致的,如果中途换过YOLOv8的版本,权重文件可能不兼容,报错的话建议直接基于最后的权重重新训练:
yolo detect train \ data=... \ model=runs/detect/你的项目名/weights/last.pt \ epochs=120 \ imgsz=640 \ batch=16基于last.pt继续训练和resume的差别是:前者会从第一轮重新开始记录训练历史,但权重是继承下来的;后者会完整恢复训练曲线。如果只是断了一次,用resume,省事;如果折腾过环境或者改过配置,干脆从头训。
5. 常见问题与避坑:标签错位、鲜奶漏检、结果随机性排查
5.1 某个类别标签全部为空,训练时mAP始终为0
现象:训练日志正常输出,loss也下降,但训练结束时某个类别(比如鲜奶)的mAP一直是0,其他类别正常。
原因:标签文件里类别id和data.yaml中的names列表顺序对不上。例如标签里milk类是2,但data.yaml的names只写了两个类别,milk被视为超出范围的目标,训练时被直接忽略。
解决:用2.2节的统计脚本重新统计所有标签里的类别id。关注打印结果中的class_counter字典,对照data.yaml里的names,确保两边编号完全对齐。如果标签里的id有空缺,比如只有0和2,没有1,说明数据整理时丢掉了一部分类别,建议检查解压后的原始标签目录,找出所有txt文件逐行过一遍,找出缺失的类别是从哪个环节丢的。
5.2 鲜奶包装反光导致漏检,增强方向调错
现象:验证集上罐头和瓶子的检出效果不错,鲜奶类别的召回率偏低,尤其是有反光的鲜奶盒或者半透明包装的鲜奶瓶。
原因:增强参数配置里加了大量的随机旋转和透视变换,本来是想提升鲁棒性,但这类瓶罐目标的识别高度依赖边缘和结构特点。过度旋转后目标外观变化过大,模型学到的是各种奇怪角度的共性特征,反而削弱了对鲜奶盒、鲜奶瓶这种特定长宽比目标的表征。
解决:把旋转角度限制在±10度以内,去掉透视变换,增加亮度对比度扰动。训练参数里对应的是YOLOv8的增广参数,hsv_h、hsv_s、hsv_v这些,可以适当调高亮度和饱和度的随机范围,去模拟不同光源下反光程度的变化。如果还是漏检,对含鲜奶目标的图像单独做离线增强,每种变换做3份,把鲜奶在训练集中的占比提起来。
5.3 zip解压后中文路径导致读取失败
现象:全部按流程走完了,启动训练时提示找不到图片文件,或者验证集图片读取数量为0。
原因:Zip压缩包内可能嵌套了带中文的目录名。解压到Windows系统时,中文路径在PyTorch的DataLoader下偶发无法正常读取,尤其是文件路径里同时包含中文字符和空格时,报错信息还不太直观。
解决:把数据集放在一个纯英文路径下,所有目录名都用英文小写,图片和标签的文件名只用数字和字母组合。文件名较长的建议改短。这一步虽然看起来是小事,但能省掉无数排查时间。路径问题不会在你跑通一次训练后就消失,后续推理、导出模型时还会反复踩到,最好一开始就彻底规避。
5.4 两次训练结果差异很大,无从判断哪个配置好
现象:用同一份数据集、同样的参数,分两次训练,mAP差了2到3个点,怀疑是代码有bug,或者模型写死了随机行为。
原因:YOLO训练涉及大量随机性:数据加载顺序、增强参数采样、权重初始化、dropout等。我一般会固定数据划分的随机种子,通常设为42。同时,训练时加seed=42参数,把训练过程也固定下来。如果你需要横向对比不同的参数组合,记得每次都固定同一个seed,这样每次实验之间唯一的变量就是你要调的那个参数。
解决:每次实验在命令行里显式加上seed=42,或者写一个shell脚本把所有训练命令统一管理,日志按日期命名留存。最后看结果时对比的不是单个mAP,而是多次运行的平均值和方差。如果方差大于1个点,说明数据集本身波动就大,单次实验的最好成绩没有参考意义。
5.5 数据集中存在标签边界超过1.0的情况
现象:训练日志没有异常,但用验证集评估时,个别的检测框位置整体偏移,特别是位于图像边缘的目标。画出框来一看,框跑到了图像外面。
原因:打标工具在标边缘目标时,框的四边可能超出了图像边界,保存的归一化坐标就会出现小于0或者大于1的情况。YOLO训练时会自动忽略越界的框,但你的数据统计和图框可视化脚本没有做越界判断,导致你看到一堆诡异的框在图像外面。
解决:写脚本对全部标签做一次越界检查,把小于0和大于1的值强制截断到0到1之间。越界量不太大的情况下,截断基本不影响标注质量,比如框的右边界是1.05,截断成1.0只是让框贴到图像边缘,目标本身没有损失。越界超过0.3的说明打标时误差很大,建议人工复核一下对应图像,确认标签是否可保留。
def clamp_label(line, img_w, img_h): parts = line.split() cls = int(parts[0]) cx, cy, w, h = map(float, parts[1:]) cx = max(0, min(1, cx)) cy = max(0, min(1, cy)) w = max(0, min(1, w)) h = max(0, min(1, h)) return f"{cls} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}"这段代码按行读取标签,对越界值做截断。注意这里截断的是中心点、宽和高,不是直接对左上角和右下角做裁剪,因为YOLO格式存的本来就是中心点和宽高,写的时候不要混淆概念。
6. 验证模型效果:从mAP指标到单张推理的边界测试
训练结束后,模型结果在runs/detect/can_bottle_milk/weights/best.pt。这个best.pt是验证集mAP最高的权重,也是你后续做推理和部署要用的文件。很多人习惯用last.pt,就是最后一轮保存的权重,实际上遇到验证集mAP先升后降的情况,最后一轮的模型往往是过拟合的,效果反而不如中间某轮的best.pt。所以为保险起见,推理时只用best.pt,不要用last.pt。
验证模型效果不能只看总mAP,要分开看每个类别的precision和recall。YOLOv8在验证完会输出每个类别的详细指标,你需要关注的是鲜奶类的召回率。这个指标决定生产环境中鲜奶目标被漏检的比例,比mAP更实际。如果召回率不理想,参考5.2节的方向增强,或者把置信度阈值调低一档,从0.25调整到0.15,用误检换漏检。
单张推理的指令如下:
yolo detect predict \ model=runs/detect/can_bottle_milk/weights/best.pt \ source=test_images/milk_bottle.jpg \ conf=0.25 \ save=True跑通推理之后,我建议做一轮更细的边界测试:挑几张含反光、模糊、遮挡、多个目标堆叠的图片,逐一观察模型的检测结果。这一步能发现量化指标看不出的问题,比如类别置信度在0.3左右徘徊的目标、两个目标靠太近被合并成一个框、小目标完全没被检出。这些现象汇总起来,就是你下一步数据补充的方向。
如果检测框位置比实际目标偏大或者偏小很多,可以看训练时imgsz和原始图像分辨率的差异。原始图像如果是720p,你强行缩到320训练,框的精度肯定会打折。这个只能通过调高imgsz重新训练解决,没有别的捷径。而如果框的位置准确但类别判断总是犹豫,说明类别间的特征区分度不够,方向还是在标签清洗和数据增强上,再训练多少轮都白搭。
我自己的习惯是验证完模型后,把误检和漏检的图片单独建一个文件夹,按错误类型重命名分类保存。下次再迭代数据集时,这些图片就是最有价值的样本,直接告诉你要补什么类型的数据。这套工作流用的顺了,下次换任何数据集都能快速跑通训练,少走很多弯路,希望帮到你。
本文还有配套的精品资源,点击获取