简介用于瓷砖瑕疵检测的VOCYOLO标准标注数据集面向视觉检测算法工程师、深度学习开发者和制造质检相关项目团队适用于从算法入门到产线预研的不同阶段可适配YOLOv5、YOLOv8等常见版本满足不同精度与速度需求帮助快速搭建表面缺陷识别模型。压缩包共包含2000个XML标注文件整体仅4.35MB标注内容记录了每张图像中的瑕疵边框、类别等关键信息采用PASCAL VOC格式存储格式规范清晰可方便转换为YOLO等标注格式直接用于模型训练文件名中带有相机编号等信息便于样本管理与追溯。目前已有791人学习下载。利用这些标注开发者可以省去大量人工标注时间专注于模型训练与参数调节从而验证不同YOLO版本在瓷砖裂纹、划痕、污渍等缺陷上的检测效果同时适合作为课程设计、毕业设计或产线质检预研的数据基础有助于实现从数据整理、模型训练到效果评估的完整流程。对于缺少标注数据的研发团队而言该数据集能够明显降低前期准备成本聚焦核心检测算法加速项目落地。1. 瓷砖瑕疵检测数据集为什么说双重标注比多采集一千张图更值钱在工业视觉里瓷砖质检是可以让你直观感受到“数据格式比模型结构更影响结果”的领域。裂纹、针孔、崩边、色斑这类表面缺陷目标小、对比度低有些类别在低分辨率下几乎分不清直接拿通用目标检测模型练手最常见的体感是 loss 在降、mAP 卡着不动。很多人第一反应是继续加图但多数项目卡住的真正原因是标注格式、类别映射和训练管线没对齐。这套 VOC YOLO 双重标注的瓷砖瑕疵数据集把原始图像、XML 标注和 YOLO 的 txt 标注一次配齐省掉了格式转换的损耗适合做工业质检方案选型、快速跑通 YOLOv8 训练也适合想用真实场景数据练手目标检测的入门者。它的价值就在让你把时间花在调模型上而不是花在跟格式搏斗上。2. 数据集内容拆解VOC 的 XML 和 YOLO 的 txt 是怎么一一对应的2.1 解压后的目录结构解压 zip 后我习惯先不看说明文件直接铺开目录结构确认图像和标注是不是一一对应。这套数据集常规情况下是这样组织的tile_defect_dataset/ ├── images/ # 瓷砖原始图像jpg ├── Annotations/ # VOC 格式标注xml └── labels/ # YOLO 格式标注txt三个目录分别存放原始图像、VOC 格式 XML 和 YOLO 格式 txt文件名一一对应tile_0001.jpg对应Annotations/tile_0001.xml和labels/tile_0001.txt。实际项目里标注工具生成目录的命名经常不统一有的叫annotation有的叫Label这不是错误但会让训练脚本里的路径对不上。建议动手训练前先把目录名统一后面所有命令基于统一后的路径写。2.2 VOC XML 标注里要看懂的字段VOC 格式出自 PASCAL VOC 竞赛是目标检测里最常用的标注格式之一一个图像对应一个 XML 文件。打开任意一个 XML核心字段是这几个filename是图像文件名size记录图像宽高和通道数每个object下有name类别名和bndbox目标框像素坐标。annotation folderimages/folder filenametile_0001.jpg/filename size width1920/width height1080/height depth3/depth /size object namecrack/name difficult0/difficult bndbox xmin532/xmin ymin204/ymin xmax698/xmax ymax331/ymax /bndbox /object /annotationbndbox里xmin、ymin是左上角坐标xmax、ymax是右下角坐标单位是像素。瓷砖质检场景里有两个点需要特别留意。第一name是字符串它不会跟 YOLO 的类别索引自动对应这条映射关系必须由你维护后面所有环节都依赖它。第二difficult字段表示这个目标是不是模棱两可如果数据集里存在不少难例被标成 1训练时要么专门挑出来增强要么干脆过滤否则模型会被这些不确定样本带偏。2.3 从 XML 转换到 YOLO txt归一化坐标是关键YOLO 训练读取的 txt 文件每一行代表一个目标格式是类别索引 x_center y_center width height四个坐标值都归一化到 0 到 1。这种表示法的好处是图像任意缩放标注都不会失效训练时 YOLO 会统一把图像 resize 到输入尺寸归一化坐标可以直接用不需要二次换算。从 VOC 的 bndbox 转成 YOLO 坐标公式如下x_center (xmin xmax) / 2 / image_width y_center (ymin ymax) / 2 / image_height width (xmax - xmin) / image_width height (ymax - ymin) / image_height配套的转换脚本大概长这样import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_txt_path, img_w, img_h, class_names): xml_path: 输入的 VOC XML 文件 out_txt_path: 输出的 YOLO txt 文件 img_w / img_h: 图像真实宽高 class_names: 类别名列表列表索引就是 YOLO 类别 id 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: continue cls_id class_names.index(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) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h width (xmax - xmin) / img_w height (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines))这段脚本里最容易出错的三个点逐个说一下。第一class_names的顺序不是随便排的。YOLO 格式里的数字索引对应names列表的下标比如class_names [crack, pinhole, stain]那么pinhole的类别 id 就是 1。后面写 data.yaml 时如果顺序变了整个模型的类别含义就全乱了这是所有格式坑里最隐蔽的一个。第二公式里的两个分母必须用图像真实宽高不是标注时预览窗口的尺寸。如果 XML 里的size和实际图像不一致转换出的归一化坐标就会出现系统性偏移。常见做法是优先用PIL.Image.open读实际尺寸再传给转换函数而不是完全信任 XML 里写的数值。第三输出坐标保留了 6 位小数。对 1920×1080 的图像来说6 位小数对应亚像素精度训练读取完全够用写太多位数没有意义写太少小数点后三位以下会产生明显框偏移。2.4 为什么两套标注格式都要保留既然 YOLO 训练只需要 txt那 VOC 版本的 XML 留着干什么这个问题经常被问。实际项目里XML 是“人可读”的标注打开就能看到类别名、难易标记、目标框坐标也方便放进 CVAT、labelimg 里继续编辑txt 是“机器可读”的训练输入解析快、不依赖 XML 树结构但也正因为太精简一旦标注错误人眼根本看不出来。常见的生产流程是把 VOC XML 当作标注修改的基准每次改完标注重跑一次转换脚本生成 txt 交给训练训练中发现异常框再回到 XML 侧复核。这套数据集把两套标注同时给出来本身就省掉了维护中间文件的麻烦。3. 用 YOLOv8 训练前要做的事目录重排、数据划分与 yaml 配置3.1 按 YOLO 惯例重建数据目录YOLOv8 默认的数据加载逻辑是按images和labels两个顶层目录去找文件结构要求是dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/原来的images / Annotations / labels结构里没有 train 和 val 子目录需要自己重排。我一般会新建一个干净目录用脚本做复制而不是直接在原目录上移动保留一份原始数据方便回溯。cd tile_defect_dataset mkdir -p yolo_dataset/images/train yolo_dataset/images/val mkdir -p yolo_dataset/labels/train yolo_dataset/labels/val python - EOF import random, os img_files sorted(os.listdir(images)) random.seed(42) val_count int(len(img_files) * 0.15) val_files set(random.sample(img_files, val_count)) for img in img_files: stem img[:-4] if img in val_files: os.system(fcp images/{img} yolo_dataset/images/val/) os.system(fcp labels/{stem}.txt yolo_dataset/labels/val/) else: os.system(fcp images/{img} yolo_dataset/images/train/) os.system(fcp labels/{stem}.txt yolo_dataset/labels/train/) EOF这段脚本用固定随机种子把 15% 的图像划给验证集再把图像和对应 txt 同步复制到新目录。写完先检查两个 labels 子目录里文件数量是否和 images 对得上数量不齐说明有图像缺标注或者文件名后缀不一致不要急着往下走。3.2 数据划分的坑按批次分而不是随机打散上面随机划分有一个隐患如果数据集里同一块瓷砖拍了多张图像随机划分会把同源图像同时分进 train 和 val验证集里就会出现跟训练集高度相似的样本mAP 虚高。这个问题在瓷砖质检场景几乎必然存在因为采集通常是流水线上同一批次连续拍摄相邻几张图的光照、位置都很接近。我的建议是先看文件名。如果文件名带批次或编号信息就按批次分桶再把整个桶划给 train 或 val。比如文件名格式是T20240513_B02_0037.jpg可以取T20240513_B02作为桶的 key这样同批次的图像不会同时出现在训练集和验证集里。验证集不用太大工业检测数据集通常目标数不多15% 到 20% 足够估算 mAP留多了反而压缩训练样本量。提示val 集只承担评估职责不要为了追求 mAP 好看刻意挑选简单样本进去。宁可在 val 里放几个难例让模型在评估阶段就暴露问题。3.3 写 data.yamlnames 顺序必须和 txt 索引一致YOLOv8 靠一个 YAML 文件描述数据集路径和类别。文件本身的写法很简单但names列表顺序必须和 txt 标注里的整数索引完全对应。path: ./yolo_dataset train: images/train val: images/val names: 0: crack 1: pinhole 2: stain 3: edge_collapse写完后花 30 秒检查一件事names里的顺序是否跟转换脚本里class_names的顺序一致。如果转换脚本里 crack 是 0yaml 里 crack 也是 0万事大吉如果两边顺序不一致模型训练期间不会报任何错误但推理输出的类别名会全乱套。这个错误我犯过一次排查了整整一天最后发现只是 names 顺序写反了。3.4 跑第一个训练命令参数怎么选确认目录和 yaml 没问题后直接跑yolo detect train \ datatile_defect_dataset/yolo_dataset/tile.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ workers4 \ projectruns/tile_exp \ namev8s_defect第一次跑通主要关心四个参数modelyolov8s.pt第一次用 s 版本最稳。n 版本省显存但针对瓷砖这种小目标场景效果偏弱m 及以上版本在小数据集上容易过拟合后面再根据效果升级。imgsz640速度和精度的平衡点。如果瓷砖缺陷里有大量几十像素的小目标可以提到 960 甚至 1280代价是显存占用明显增加。判断标准是看缺陷标注框在原始图像里占多大比例。batch16显存不够时先降 batch 而不是降 imgsz。batch 低于 8 时 BatchNorm 统计不稳定loss 曲线会抖得厉害。epochs100工业缺陷数据集通常几百到几千张图100 轮基本够用。train loss 和 val loss 都进入平台期之后再继续训收益很小。第一次训练完成后先看两样东西results.png里 loss 曲线有没有持续下降以及confusion_matrix.png里类别之间是不是互相串。这两个文件在项目输出目录下比盯着终端输出直观得多。4. 避坑瓷砖缺陷训练里最容易翻车的五个细节4.1 现象训练 loss 正常下降但验证 mAP 一直很低训练曲线很好看loss 从 0.3 一路掉到 0.05验证 mAP 却始终在 0.5 以下。这种反差最容易让人怀疑是网络结构不行于是去换更大的模型结果没改善。原因一是数据划分泄漏或划分过度。同批次图像被拆进 train 和 val会让验证结果虚高反过来把某个批次的全部图像划进 val而该批次恰好包含一种罕见缺陷mAP 自然被拉低。原因二是 XML 转 txt 时某个类别名在class_names里不存在目标被静默跳过表现为该类别漏检但整体分数不低。解决方法是先看 val 目录里有没有包含全部类别再打开results.png看 val loss 曲线。如果 val loss 降了但 mAP 不涨重点检查标注文件里是否有大量空 txt也就是图像里一个目标都没标的情况。空 txt 占比超过 20% 时模型会把背景学得很死小目标全被吞掉。4.2 现象loss 降得很快但验证框位置整体偏移如果 mAP 数值不低但把验证图像画出来看预测框普遍往一个方向偏半个身位几乎可以断定是标注坐标系出了问题。原因是 VOC 转 YOLO 时除了错误的分母。比如 XML 的size字段写的是 1920×1080而图像实际分辨率是 1280×720或者标注时看的是缩放预览图造成归一化坐标与真实位置不一致。另一种情况是 YOLO 格式要求的中心点坐标被误写成了左上角坐标。解决方法是从 XML 重新核对bndbox和size是否匹配然后重跑转换脚本。不要手动去改 txt 里的数字YOLO 训练时图像缩放是等比进行的任何手动修改的绝对值都会引入新的偏移。后续项目里我会把转换脚本和画框检查脚本一起保留任何标注修正都走这两条路不做临时性手动修改。4.3 现象个别缺陷类别几乎检不出来裂纹检得很好针孔 mAP 基本为 0这是典型的类别不平衡问题。原因是针孔样本数量少而且目标很小在 640×640 缩放下只占几个像素特征在卷积下很容易被淹没。数据标注阶段也容易漏标小目标框稍微偏一点训练时就会当成背景处理。解决方法是先从数量上确认差距再对少数类做简单的过采样把含针孔的图像在数据列表里重复几次。YOLOv8 没有直接设置类别权重的命令行参数想调损失权重需要改训练脚本对大部分项目来说过采样更省事。同时把imgsz提到 960减少 mosaic 增强让小目标图像在增强过程里不被缩得太小。4.4 现象开启增强后小目标消失了我在一个项目里遇到过这种情况训练中打开增强后的中间结果标注框还在但缺陷区域被模糊成几个像素的黑点肉眼已经看不出来是什么。原因是数据增强对自然场景很友好对工业小目标并不友好。mosaic 把四张图拼接再随机缩放瓷砖缺陷本就不大缩放后可能直接变成 5 像素以下默认参数下的缩放、旋转组合也容易让细小缺陷特征在插值过程中被抹掉。解决方法是显式控制增强参数。我一般这样设置mosaic0.5、scale0.3、hsv_h0.01、hsv_s0.2不会把 mosaic 开到默认的 1.0。调整后对比 val mAP通常会有几个点的提升。如果发现某个增强选项让 loss 曲线出现异常抖动直接关掉再试。4.5 现象训练完 mAP 看着不错现场一测全乱离线评估时混淆矩阵很干净部署到产线相机上误检率飙升这是工业视觉项目里最常见也最难排查的问题。原因是现场光照与数据集分布不一致。瓷砖是高反光材质同一种色斑在不同照明角度下拍出来可能是完全不同的纹理模型学到的纹理特征一旦与现场图像分布偏移表现就会崩。数据集里包含的图像如果来自不同批次批次之间的亮度、色温差异也会造成同样的问题。解决方法是在训练时加入光照扰动把hsv_s、hsv_v适当调大部署前单独采集一小批现场图像做验证这个步骤不要省略。如果现场图像和数据集差异实在大最直接的办法是拿现场图像做增量标注补进训练集再微调一轮。5. 训练前先跑一遍标注自检脚本一个代码块挡掉 80% 的格式坑5.1 批量核对标注和图像拿到任何带标注的数据集我第一步不是急着训练而是用脚本把“图像能不能打开、标注坐标合不合法、文件名对不对得上”全过一遍。这套流程用下面这个脚本就够了import os import glob from PIL import Image def check_yolo_annotations(img_dir, label_dir): img_files glob.glob(os.path.join(img_dir, *.jpg)) for img_path in img_files: base os.path.splitext(os.path.basename(img_path))[0] txt_path os.path.join(label_dir, base .txt) if not os.path.exists(txt_path): print(f[missing] {txt_path}) continue try: with Image.open(img_path) as im: w, h im.size except Exception as e: print(f[bad image] {img_path}: {e}) continue with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(f[bad line] {txt_path}: {line.strip()}) continue cls, xc, yc, bw, bh parts if not cls.isdigit(): print(f[bad cls] {txt_path}: {line.strip()}) continue xc, yc, bw, bh map(float, (xc, yc, bw, bh)) if bw 0 or bh 0: print(f[bad box] {txt_path}: {line.strip()}) if not (0 xc 1 and 0 yc 1): print(f[out of range] {txt_path}: {line.strip()}) print(done) check_yolo_annotations(yolo_dataset/images/train, yolo_dataset/labels/train)这段脚本查三件事图像能不能正常打开、txt 行结构是否合法、归一化坐标是否越界。越界检查有两种典型输出坐标小于 0 或大于 1说明转换时除了错误的分母宽高为非正数说明 bndbox 的 xmin/xmax 填反了。跑完后随便挑几张有打印的图像在原图上画框对照一下比看任何文档都直观。5.2 拿 VOC 做复核兜底如果检查出来异常不要直接改 txt。回到对应的 XML用 labelimg 或 CVAT 打开确认原始框位置修正 XML 后重新跑转换脚本。这个习惯能避免“手动改了一个框结果越改越乱”的连锁反应。从那以后我每次拿到新数据集都强制自己先跑一遍这个检查再做任何训练参数调整希望帮到你。本文还有配套的精品资源点击获取