ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

YOLOv8瓷砖缺陷检测实战:从数据集标注到训练避坑全指南

YOLOv8瓷砖缺陷检测实战:从数据集标注到训练避坑全指南 简介面向工业质检与微小缺陷检测场景这份YOLO瓷砖裂缝识别数据集覆盖“裂缝”和“正常”两类目标使用LabelImg标注共包含约1700张图像及对应txt标签。资源包共2000个文件其中1765个为txt标注文件234张为图像文件还提供1个Python可视化脚本压缩包整体约91.75MB数据已按目录划分附带的classes.txt可直接对应YOLO训练格式且部分样本经过翻转、添加噪声等增强处理有助于提升模型鲁棒性。除训练数据外这个Python可视化脚本无需修改即可运行随机传入一张图片就能自动绘制边界框并保存到当前目录非常适合快速核对标注质量或预览检测效果。目前已有194人学习下载对于希望快速上手瓷砖缺陷检测或基于YOLOv5做改进的开发者这套资源能有效节省数据准备与标注检查的时间。1. 瓷砖裂缝检测为什么难小目标、纹理噪声和2类标签的坑产线质检拍回来的瓷砖图分辨率动不动到 4000×3000上面最细的裂缝只有几个像素宽你要做的任务是 2 类检测一类“裂缝”一类“崩边”。第一次跑 YOLOmAP50 卡在 0.3 不是网络结构的问题而是数据集本身没立住。这篇文章要拆的就是标题里这套方案的全部内容已经划分好的 YOLO 数据集怎么用、class 文件怎么定义、数据可视化脚本怎么帮你排查标注错误以及你自己收集图像后怎么把数据整理到能直接开跑 YOLOv8 训练的程度。适合谁看手上有瓷砖缺陷图、想低成本验证深度学习检测方案又不想在数据准备上反复翻车的工程师。2. 先和数据集面对面标注规范、class 文件与可视化验证2.1 两类标签怎么定裂缝和崩边的判定标准瓷砖表面的缺陷类型其实很多裂纹、崩边、针孔、色差、熔洞。但这个数据集只定了 2 类说明在真实质检场景里真正影响出厂判定、值得用模型去扛的就是那两个crack裂缝和 edge_break崩边。剩下的缺陷要么靠前道工序拦截要么出现频率太低硬塞进来反而把类别不平衡搞得没法收拾。我一般会在动手标注之前先写死一份标注规范不然多人协作时同一个缺陷今天叫 crack、明天叫 break模型学到的特征就乱了。对瓷砖裂缝我的判定标准这么定crack线状或树枝状延伸的裂纹肉眼可辨长度不低于 5mm瓷砖表面的网状细纹正常纹理不算。edge_break瓷砖边缘的崩缺露出胚体最短边不小于 2mm角落的轻微掉角如果面积够大也算。写清楚阈值之后标注人员面对模糊样本时至少有一个可量化的判断依据。标注工具用 LabelImg 或 X-AnyLabeling 都可以导出格式选 YOLO工具会自动生成和每张图像同名的 txt 文件。这一步出来的东西直接决定了后续所有工作有没有意义。2.2 class 文件里写什么names 顺序就是推理输出的顺序YOLOv8 模型的类别定义写在 data.yaml 的 names 字段里也可以单独维护一个 class 文件比如 classes.txt 或 obj.names。内容是一行一个类名顺序不能乱下标从 0 开始crack edge_break这里 crack 对应 class_id0edge_break 对应 class_id1。训练完成导出模型后推理端每个 bbox 输出的第一个类别始终是 crack第二个是 edge_break。这个文件训练中途不能换顺序更不能调。踩过的坑是这样的从网上下载的数据集里class 文件里往往不止这两个类有人为了“精简”直接把列表里的其他类删掉结果删除后某个类别下标变了训练时的 class_id 和推理时对不上了模型框出来的目标是错的你还浑然不觉。所以拿到任何数据集的第一步先打开 class 文件确认 names 顺序再做可视化验证别急着训练。2.3 一张图配一个 txtYOLO 归一化坐标换算公式YOLO 格式的标签文件不是 XML 也不是 JSON每一行是一条检测目标五个字段class_id x_center y_center width height四个坐标值全部归一化到 01单位是相对图像宽高的比例。举个例子一张 1920×1080 的瓷砖图裂缝框左上角在像素坐标 (480, 270)右下角在 (960, 540)对应标签行是0 0.375 0.375 0.25 0.25换算过程x_center 等于左右 x 坐标的平均值除以图宽即 (480960)/2/19200.375y_center 同理(960-480)/19200.25 是框宽占比(540-270)/10800.25 是框高占比。这个公式是所有转换脚本里最容易写错的地方。手工标注一般没问题但如果你从 VOC 的 XML 或 COCO 的 JSON 转 YOLO公式里坐标减错、忘记归一化训练出来的模型框就会整体偏移。我自己在转完格式后绝不直接开训练一定先用可视化脚本抽查 50 张图确认框的位置、大小和类别都对得上。2.4 数据可视化脚本先跑一遍标签错位一眼就看出来标题里说的数据可视化脚本核心功能就两件事把标签框画回原图、按类别统计样本数量。画框用 OpenCV读取 txt 的时候要注意编码Windows 环境下有些标注工具保存的是 GBK 而不是 UTF-8直接按 utf-8 读会抛 UnicodeDecodeError。一个最小可用的可视化脚本可以这样写import cv2 from pathlib import Path def draw_labels(img_path, label_path, class_names): img cv2.imread(str(img_path)) h, w img.shape[:2] with open(label_path, r, encodingutf-8) as f: lines f.readlines() for line in lines: parts line.strip().split() if len(parts) ! 5: print(fskip bad line in {label_path}: {line}) continue cls_id int(parts[0]) if cls_id len(class_names): print(fclass id {cls_id} out of range in {label_path}) continue x_c, y_c, bw, bh map(float, parts[1:]) x1 int((x_c - bw / 2) * w) y1 int((y_c - bh / 2) * h) x2 int((x_c bw / 2) * w) y2 int((y_c bh / 2) * h) color (0, 0, 255) if cls_id 0 else (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, class_names[cls_id], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) return img class_names [crack, edge_break] img_path Path(dataset/train/images/001.jpg) label_path Path(dataset/train/labels/001.txt) out draw_labels(img_path, label_path, class_names) cv2.imwrite(check_001.jpg, out) print(done, check check_001.jpg)这段逻辑很直白读图、读 txt、把归一化坐标换算成像素坐标、按类别画不同颜色的框。两个细节是关键。第一坐标换算必须是 (x_c - bw / 2) * w先减后乘反了的话框会整体偏出去半个框宽。第二对异常行要容忍遇到空行、多字段少字段、class_id 越界打印出来跳过而不是直接中断否则几百张图里混进一张坏标签整个检查流程就停在半路上。跑完可视化标签错位、漏标、类别写反都会暴露。确认没问题了再进入数据集划分阶段。3. 划分好的数据集怎么复现按批分组、脚本划分与 data.yaml3.1 切分比例按拍摄批次分组比按文件随机切可靠标题强调“划分好的数据集”说明划分策略本身是这套资源的核心价值之一。常见的 8:1:1 划分比例不是万能公式前提是你得先搞清楚图像是怎么来的。产线固定相机拍的瓷砖图同一片砖只要传送带停顿半秒前后两张看似不同、实则高度相似随机划分后验证集里可能混着大量训练集图像的“近亲”mAP 虚高到 0.9一上现场马上跌回 0.4。我处理瓷砖数据会用按“批次”分组的方式先把所有图像按文件名前缀或所在子目录分成 N 组比如同一批瓷砖、同一天采集的图像归为一组划分时以组为单位而不是以单张图为单位。这样验证集里的缺陷形态是模型没见过的评估结果才可信。3.2 划分脚本固定随机种子、按组搬运、保留标签下面这个脚本是我处理类似项目时的常用做法按文件名前缀分组再从组维度随机划分成 train、val、test 三份import random import shutil from pathlib import Path random.seed(42) data_root Path(tile_defect) images sorted(data_root.glob(images/*.jpg)) groups {} for img_path in images: # 假设文件名格式batch_20240105_001.jpg取前两段作为批次号 prefix _.join(img_path.stem.split(_)[:2]) groups.setdefault(prefix, []).append(img_path) group_keys list(groups.keys()) random.shuffle(group_keys) total len(group_keys) train_keys group_keys[: int(total * 0.8)] val_keys group_keys[int(total * 0.8) : int(total * 0.9)] test_keys group_keys[int(total * 0.9) :] def copy_files(file_list, img_out, lab_out): img_out.mkdir(parentsTrue, exist_okTrue) lab_out.mkdir(parentsTrue, exist_okTrue) for img_path in file_list: lab_path data_root / labels / (img_path.stem .txt) shutil.copy(img_path, img_out / img_path.name) if lab_path.exists(): shutil.copy(lab_path, lab_out / lab_path.name) else: print(fmissing label for {img_path}) copy_files([p for k in train_keys for p in groups[k]], Path(dataset/train/images), Path(dataset/train/labels)) copy_files([p for k in val_keys for p in groups[k]], Path(dataset/val/images), Path(dataset/val/labels)) copy_files([p for k in test_keys for p in groups[k]], Path(dataset/test/images), Path(dataset/test/labels)) print(ftrain groups: {len(train_keys)}, val groups: {len(val_keys)}, test groups: {len(test_keys)})固定随机种子为 42 是有意的划分结果可复现。下次补了几百张新图再跑一遍验证集不会完全变掉你还能拿新旧模型在同一批验证图上对比。如果文件名前缀不好用更稳妥的做法是让不同批次的原始图分别放在 images/source_a、images/source_b 子目录里直接把二级目录名当作分组键这样拆分逻辑和采集来源一一对应我推荐你工程化时用这个方案。3.3 data.yaml 配置train/val 指向 images 目录的含义划分完成后YOLOv8 训练需要的 data.yaml 长这样path: ./dataset train: train/images val: val/images test: test/images names: 0: crack 1: edge_break这里有个关键点train 和 val 指向的是 images 目录而不是 labels 目录。YOLO 的训练逻辑是读取 train/images 下的图像列表再把路径里的 images 替换成 labels 去查找同名 txt如果你写成了直接指向 labels训练会报数据集找不到或者训练集图像数为 0。这个坑在往 server 上部署项目时尤其常见因为本地目录和服务器目录结构不一致。path字段建议写相对路径配合项目根目录使用。用绝对路径虽然一次能跑通但只要项目换个机器、换个盘符就要改一处相对路径就不存在这个问题。另外如果 test 目录暂时没有独立数据test 字段可以删掉验证只需要 val。4. 用 YOLOv8 训练自己的瓷砖裂缝模型最小命令、关键参数和损失函数4.1 最小训练命令从 yolov8s.pt 做迁移学习数据准备好了接下来是用 yolov8 训练自己的数据集。我用的是 Ultralytics YOLOv8模型起点选 yolov8ssmall而不是更轻的 n原因后面讲。先装环境再跑最小训练命令pip install ultralytics yolo detect train \ modelyolov8s.pt \ datadataset/data.yaml \ epochs100 \ imgsz640 \ batch16 \ patience20 \ projectruns/tile_train \ nameexp1首行 pip install 装的是 ultralytics 这个包它自带 yolo 命令行入口。训练时 modelyolov8s.pt 会自动下载预训练权重到当前目录然后用你的瓷砖数据做迁移学习。epochs100 是我在瓷砖缺陷数据上的经验起点这类任务 100 轮基本收敛再往后 mAP 提升很有限只会徒增耗时。如果你的卡是 24G 显存但跑不满 batch16优先怀疑数据读取瓶颈而不是盲目降低 batch。4.2 必调参数imgsz、batch、epochs、patience 怎么取舍训练自己的数据集时这几个参数是每次都要过的关卡imgsz默认 640。但如果原始图是 4000 像素的产线图我建议先用 imgsz1280 试跑 20 轮对比一下小裂缝的召回率。显存不够时优先降 batch而不是降 imgsz因为裂缝这种小目标对分辨率非常敏感。batch16 在单卡 24G 上比较稳。batch 太小BatchNorm 层的统计量抖动大裂缝这种纹理微弱的样本会让 loss 曲线像锯齿一样来回跳。epochs和 patience 配合使用。patience20 的意思是连续 20 轮验证集 mAP 不提升就提前停训这样 epochs 设 300 也不怕过拟合。workersWindows 上建议设 4不要设 8 或更高Windows 默认用 spawn 方式启动子进程worker 数太高容易直接卡死。这四个参数里imgsz 和 batch 对结果的影响最直接。我见过有人为了把 batch 从 16 提到 32把 imgsz 从 1280 降到 640结果裂缝检出率掉了十几个点这就是典型的本末倒置。4.3 训练过程监控loss 曲线和 mAP50 哪个更该信训练结束后runs/tile_train/exp1/ 目录下会生成一堆结果文件。我按重要性排序results.png包含 train/val 的 box_loss、cls_loss、dfl_loss、mAP50、mAP50-95 曲线、confusion_matrix.png、labels.jpg。看 results.png 时最需要警惕的是 val/box_loss 跌到某个点后开始回升这个转折点就是过拟合开始的信号。此时 mAP50 可能还在涨别高兴太早继续训下去验证集的 mAP 很快就会掉头。瓷砖产线质检任务我一般要求模型 mAP50 至少到 0.85 才敢上现场测试。mAP50-95 这个指标反而不用太苛求因为裂缝框特别细长在高 IoU 阈值下天然吃亏mAP50-95 低不代表模型不能用。另外confusion_matrix 里如果 crack 那行有大量被预测成背景的样本说明裂缝漏检集中在某些特定形态上优先回去看漏检图像长什么样而不是继续调参。4.4 YOLO 损失函数与裂缝细长框的矛盾这里补一个选型层面的判断很多人容易忽略。YOLOv8 的损失由三块组成分类损失BCE、边界框回归损失CIoU、以及 DFL 分布损失。其中边界框回归对宽高比非常敏感瓷砖裂缝的标签框通常极其细长宽高比 1:10 甚至 1:20 都很常见。CIoU 对长宽比的一致性是带惩罚的但裂缝框本身就是极端长宽比模型训练时学习到的先验框和它匹配不上box_loss 就会比普通目标检测数据高一个量级。我在实际项目中遇到的现象是mAP50 卡在 0.6 左右查 loss 发现 box_loss 始终降不到公开数据集那种水平。这时候别急着堆改进公式先回去看标注标注框是不是把裂缝框成了接近正方形的外接框框内塞了大量背景。如果标注确实紧贴裂纹那细长框难收敛就是天然的能够做的是把训练分辨率提到 1280或者在长宽比极端时考虑 NWD 这类对小目标更友好的损失变体——也就是网上常说的 nwd 改进 yolo 那条路线。但对一个 2 类的瓷砖检测任务先把标注做紧、把贴图分辨率提上来比一上来就换损失函数性价比高得多。5. 避坑指南瓷砖裂缝数据集的常见问题与排查5.1 现象mAP 指标正常现场却漏检细裂缝原因原始图像分辨率太高YOLO 在 640×640 下缩放后细裂缝只有一两个像素宽特征在卷积下采样过程中直接丢了。解决把大图做滑窗切块4000×3000 的图切成 4×4 共 16 个 1000×1000 的 patch再重新标注和训练推理阶段也用同样切法把 block 结果的框坐标换算回原图坐标系。这个方案是我在高分辨率瓷砖缺陷检测里最常用、最稳的一条路径。5.2 现象训练 loss 很低推理却几乎没输出原因数据里大量瓷砖图完全没有缺陷负样本占比过高模型学到的策略是“输出空结果”也能把 loss 压得很低。瓷砖表面纹理复杂大片干净区域让模型误以为没有目标就是最优解。解决把完全没有缺陷的图从训练集里剔除只保留一小部分比如 10%作为难负样本参与训练如果缺陷本来就稀少考虑用采样策略让每个 batch 里都保证至少出现部分正样本而不是纯靠文件列表顺序去碰运气。5.3 现象同一块瓷砖换个光照角度推理结果完全相反原因数据集里光照多样性不够。瓷砖表面是镜面材质反光区裂缝和阴影区裂缝看起来是两个物种模型只见过其中一种。解决训练时除了 YOLO 默认的 HSV 增强额外加 CLAHE 均衡和随机亮度偏移模拟不同光照下的成像差异。更踏实的是在采集阶段就覆盖三种光照条件正面光、侧光、暗场三种各拍一遍进数据集。这种“笨办法”对镜面材质的稳定性贡献比任何网络结构改进都大。5.4 现象class 文件顺序改过之后重训模型推理结果全乱原因names 列表的下标被调整过比如把 crack 从第 0 位挪到了第 1 位但标注文件里的 class_id 没有同步改训练和推理的类别映射发生错位。解决训练前写一个小脚本遍历 labels 目录统计出现的 class_id 集合确认最大 ID 不超过 names 长度减一同时把 names 打印出来和标注文件里的 ID 一一核对。这个检查我每次都做成本几秒钟省掉的是推理结果莫名错乱的排查时间。5.5 现象可视化脚本读标签报 UnicodeDecodeError原因Windows 下部分标注工具保存的 txt 是 ANSI/GBK 编码不是 UTF-8。解决读取时做编码回退。代码里这样写try: lines open(label_path, encodingutf-8).readlines() except UnicodeDecodeError: lines open(label_path, encodinggbk).readlines()这个小补丁看起来粗暴但在国内产线环境下非常实用标注工具来源不统一时能少翻一次车。5.6 现象训练集和验证集来自同一批瓷砖模型一换相机就废原因划分时按单张图随机切分同一片瓷砖的不同拍摄帧同时进了训练集和验证集模型本质上是在“背题”。解决严格按采集批次或编号分组划分见第 3 章的脚本思路。更严格的做法是把验证集从项目建立第一天就单独锁定任何一次迭代都不允许验证集图像回流到训练集给它留出一份“清白”的评估集。6. 验证阶段的可视化脚本跑通预测后必查的 3 件事6.1 在验证集上批量 predict 并输出带框图片训练完拿到 best.pt第一步是在验证集上做批量预测并保存可视化结果from ultralytics import YOLO model YOLO(runs/tile_train/exp1/weights/best.pt) results model.predict( sourcedataset/val/images, saveTrue, conf0.35, line_width2, projectruns/val_vis, nametile_check, )conf0.35 是我在瓷砖产线场景下习惯的起点。裂缝检测里置信度阈值设太低纹理干扰会被当成目标设太高细裂缝漏检直接拉高。0.35 这个值先用着后面根据 F1 曲线再调别迷信默认的 0.25。6.2 按类别输出 PR 曲线别被平均 mAP 骗了mAP 是平均过的它会把 crack 的短板藏到 edge_break 后面。训练输出目录里已经有 PR_curve.png但你还要自己看一眼每个类别的曲线端点。如果 crack 的 recall 端断崖下跌说明细裂缝大量漏检优先补长裂缝样本、提高 imgsz如果 edge_break 的 precision 端上不去说明误检多优先增加负样本或收紧标注规范。这一步不用写代码直接打开曲线文件就能判断。6.3 把 badcase 挑出来人工翻一遍这是我最坚持的一个步骤从验证集预测结果里把置信度低于 0.5 的真实正例漏检和置信度高于 0.5 的误检框各挑 20 张单独存一个目录翻一遍。漏检的 20 张如果全是细裂缝就是分辨率或标注的问题误检的 20 张如果全是瓷砖纹理边界就是标签定义需要补充干扰样本。这个习惯帮我排掉的雷比任何自动化评估指标都多因为指标只能告诉你“不行”只有翻图才能告诉你“为什么不行”。数据可视化脚本的价值不只是训练前检查验证阶段同样要靠它把模型行为翻译成人能看懂的画面。每次训练完我雷打不动地跑一遍预测可视化、翻一遍 badcase这套流程走完才会安心把模型交给产线同事去试跑希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进