ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

瓷砖瑕疵检测实战:VOC与YOLO标注转换及YOLOv8训练避坑指南

瓷砖瑕疵检测实战:VOC与YOLO标注转换及YOLOv8训练避坑指南 简介一套面向瓷砖表面瑕疵检测的VOC标注数据集兼容YOLO训练所需的格式转换旨在解决人工目检效率低、漏检率高的问题可直接用于目标检测模型的训练与验证。压缩包内共2000个XML标注文件总大小仅4.35MB每个XML详细记录图像中瑕疵的边界框坐标与类别符合Pascal VOC规范可借助现有工具快速转换为YOLO需要的txt标签。数据集文件以时间戳与相机编号命名便于追溯产线来源适合需要真实工业样本的算法工程师、在校学生和视觉系统开发者。目前已有791人学习下载使用时可快速完成训练集/验证集划分、标注格式转换与模型调参免去人工采集和标注瓷砖瑕疵图像的繁琐工作显著降低时间成本助力构建高精度自动化质检系统提升制造环节质量控制效率。1. 拿到瓷砖瑕疵检测数据集包先分清VOC与YOLO两套标注做陶瓷厂质检自动化的人多半都下过这种压缩包标题写着“瓷砖瑕疵检测数据集VOCYOLO标注.zip”解压进去一堆图片配了两套标签。VOC 是 xmlYOLO 是 txt二者坐标语义完全不同。很多人上来就把两套文件混着用训练时报一大堆 missing label、异常 bbox 错误最后怪数据集不行。其实这个数据集本身没毛病只是你没摸清两套标注各自怎么和训练框架配合。这篇笔记就按我整理这类数据集的习惯来写先看懂目录再做格式转换和划分然后跑通 YOLOv8 训练最后把最常见的几个坑提前排掉。适合刚入行目标检测的工程师也适合准备把新购入的瑕疵数据接进现有质检流程的熟手。2. 数据集结构与坐标对照先确认数据是干净再动手训练2.1 解压后的目录速览三类文件决定训练能否启动VOC 格式那套一般长这样JPEGImages放原图Annotations放同名 xmlImageSets/Main里是 train.txt、val.txt 这类分本列表。xml 里记录的是绝对像素坐标也就是xmin, ymin, xmax, ymax左上点和右下点。YOLO 那套则是images目录加labels目录标签是纯文本每行五个数字类别 id、归一化中心点 x、归一化中心点 y、归一化宽、归一化高。我先会用一条命令把当前目录结构扫出来重点看 labels 下面有没有 train、val 子目录因为不同数据包的分法不一样find . -maxdepth 3 -type d | sort有些数据包把 labels 平铺在根目录下有些按 train/val 分好。区分这两类很简单平铺版本往往同时提供 train.txt、val.txt 两个文件清单分好的版本直接按目录切分不需要再读清单。如果两个都没有那就是要自己划分后面第三章我会写划分逻辑。VOC 侧的ImageSets/Main里如果能看到文件名列表建议解压后先数一遍 jpg 数量和 xml 数量。瓷砖这类工业数据集的常见问题是转存时丢了一部分 xml但列表还留在文件里。数量对不上后面训练就会发现一部分图片没有标签在 YOLO 训练日志里表现为训练轮数内 val 指标始终偏低。2.2 用脚本逐个标签读取VOC 与 YOLO 的对应关系我会先写一个很短的分析脚本把第一张图片对应的 xml 和 yolo txt 同时读出来。目标不是看文件长啥样而是确认类别名称、框坐标是否落在图片范围内。import xml.etree.ElementTree as ET from pathlib import Path voc_xml Path(VOCdevkit/Annotations/000001.xml) yolo_txt Path(labels/000001.txt) img_size (640, 480) # 换成 JPEGImages 里实际尺寸 root ET.parse(voc_xml).getroot() print(VOC objects:) 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}) print(YOLO lines:) for line in Path(yolo_txt).read_text().strip().splitlines(): cls_id, xc, yc, bw, bh map(float, line.split()) x1 int((xc - bw / 2) * img_size[0]) y1 int((yc - bh / 2) * img_size[1]) x2 int((xc bw / 2) * img_size[0]) y2 int((yc bh / 2) * img_size[1]) print(f class {cls_id}: pixel box {x1} {y1} {x2} {y2})这个脚本把两组标签还原成了同一个坐标系。如果看到 VOC 里是 crackYOLO txt 里对应类别 id 是 0而 yaml 里把 0 定义成了 pinhole那就是类别顺序错位做多了就会遇到这种数据。所以不是在电脑上看一眼就算是要逐字比对。2.3 把标注渲染到图上验证数据包里没有脏框代码能打印坐标但坐标在不在视觉上合理的位置只有渲染出来才看得出。瓷砖瑕疵数据集最常见的毛病是框画得比实物大一圈或者把水印、接地孔当成了瑕疵。我想写个快速渲染脚本把 YOLO 标注画回原图存成一张检查图翻几页就能感觉数据质量如何。import cv2 from pathlib import Path classes [crack, chip, pinhole, glaze_drop, color_defect] def draw_yolo_label(img_path, txt_path, out_path): img cv2.imread(str(img_path)) height, width img.shape[:2] for line in Path(txt_path).read_text().strip().splitlines(): cls_id, xc, yc, bw, bh map(float, line.split()) x1 int((xc - bw / 2) * width) y1 int((yc - bh / 2) * height) x2 int((xc bw / 2) * width) y2 int((yc bh / 2) * height) color (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, classes[int(cls_id)], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) cv2.imwrite(str(out_path), img) draw_yolo_label(Path(images/train/000001.jpg), Path(labels/train/000001.txt), Path(check_000001.jpg))跑完翻几页检查图目光放在三处框有没有明显贴住瑕疵边缘小裂纹是否漏标以及类别名是不是张冠李戴。我见过一个包里面把“崩角”标成了“色差”这类错位如果不纠正后面训练出来的模型就会把这两类一起预测错。3. 把VOC标注转成YOLO格式转换脚本与数据集划分3.1 转换脚本归一化坐标与类别映射数据集包给两套标注不代表你一定用 YOLO 这套。实际我建议用 VOC 的那套 xml 作为母版重新转一遍。原因很简单xml 可读性好出了坐标问题容易定位txt 一旦写错连原始信息都没了。经过转换脚本处理后的 txt格式和顺序都由自己控制。下面是我常用的 VOC 转 YOLO 脚本。它做的事情很简单读 xml → 算像素宽高 → 把 bndbox 改成归一化中心点格式 → 按类别映射表输出 txt。import xml.etree.ElementTree as ET from pathlib import Path VOC_XML_DIR Path(VOCdevkit/Annotations) VOC_IMG_DIR Path(VOCdevkit/JPEGImages) OUT_LABEL_DIR Path(labels) OUT_LABEL_DIR.mkdir(exist_okTrue) CLASS_MAP { crack: 0, chip: 1, pinhole: 2, glaze_drop: 3, color_defect: 4, } def convert_one_xml(xml_path: Path) - list: tree ET.parse(xml_path) root tree.getroot() size root.find(size) width int(size.find(width).text) height int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in CLASS_MAP: print(f[skip] unknown class {name} in {xml_path.name}) continue bndbox obj.find(bndbox) xmin int(float(bndbox.find(xmin).text)) ymin int(float(bndbox.find(ymin).text)) xmax int(float(bndbox.find(xmax).text)) ymax int(float(bndbox.find(ymax).text)) # 这里做一次裁剪防止标注框把图片边界撑爆 xmin, xmax max(0, xmin), min(width, xmax) ymin, ymax max(0, ymin), min(height, ymax) if xmax xmin or ymax ymin: print(f[warn] empty box in {xml_path.name}) continue xc ((xmin xmax) / 2) / width yc ((ymin ymax) / 2) / height bw (xmax - xmin) / width bh (ymax - ymin) / height lines.append( f{CLASS_MAP[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f} ) return lines for xml_path in sorted(VOC_XML_DIR.glob(*.xml)): lines convert_one_xml(xml_path) out_path OUT_LABEL_DIR / (xml_path.stem .txt) out_path.write_text(\n.join(lines) \n if lines else )逻辑上有个细节CLASS_MAP这个字典的顺序就是之后训练 yaml 里names的顺序两边必须完全一致否则类别错位。脚本里:.6f保留六位小数对 640 分辨率来说精度完全够别为了省空间压到 3 位否则坐标误差会被放大。参数说一句VOC_XML_DIR、VOC_IMG_DIR这些路径根据解压位置自己改。如果 xml 里没有size节点这个脚本会直接抛异常。遇到这种情况就把该 xml 拿出来修完再跑不要改脚本硬吞下去。3.2 文件夹配额与同名校验按批次划分而不是无限随机转完所有 xml 之后接下来做 train、val 划分。这里我强烈建议不要用train_test_split随机切因为瓷砖瑕疵数据往往是同一片砖、同一光照条件下连续拍摄的随机切很容易把几乎相同视角的图片同时放进训练集和验证集。训练时 mAP 很好看一到现场换一条产线就现原形。我一般先按文件名前缀或拍摄批次分组再把整个组划进同一侧。比如文件名里带batch01_前缀的样本全部进训练集batch05_的全部进验证集。下面是一段手动分组代码import random from pathlib import Path images sorted(Path(images/train_src).glob(*.jpg)) random.seed(42) random.shuffle(images) train_ratio 0.85 split_idx int(len(images) * train_ratio) train_files images[:split_idx] val_files images[split_idx:] with open(train.txt, w) as f: f.write(\n.join(p.as_posix() for p in train_files)) with open(val.txt, w) as f: f.write(\n.join(p.as_posix() for p in val_files)) # 校验每个被分出去的图片labels 目录下必须有同名 txt label_dir Path(labels) for split_name, file_list in [(train, train_files), (val, val_files)]: missing [] for img in file_list: label label_dir / (img.stem .txt) if not label.exists(): missing.append(img.stem) if missing: print(f[{split_name}] missing labels: {missing[:10]})这段代码核心是两点一是随机种子固定别人复现你的结果时不会因为 shuffle 顺序不同而偏离二是划分完成后立刻做了同名校验有图无标签的文件会被抓出来。出现 missing 的直接从文件列表里剔除或者回到原数据集找对应 xml 补转一次。3.3 YOLO 标签文件和图片的放置方式转出来的 labels 目录里是所有 txt 平铺的但 YOLO 训练框架依赖下面的目录结构images/train、images/val、labels/train、labels/val。我见过有人把图片放images/标签放labels/yaml 里 train 指到images/结果训练时框架完全找不到配对标签或者找到了但目录名不对。所以要养成一个习惯先把 images 移动好再为它们生成对应位置的 labels不让图片和标签错位。mkdir -p tiles_dataset/images/{train,val} mkdir -p tiles_dataset/labels/{train,val} while read -r line; do cp $line tiles_dataset/images/train/ cp labels/$(basename $line .jpg).txt tiles_dataset/labels/train/ done train.txt最后再把train.txt、val.txt删掉因为进入 YOLO 训练后不再需要这些清单yaml 里的目录路径已经决定了划分。整套流程跑完数据目录干净、配对关系明确训练阶段的问题就少了一大半。4. 用YOLOv8训练瓷砖瑕疵检测模型配置文件与启动命令4.1 训练 yaml 的写法路径、类别名与数量YOLO 训练自己对数据的描述文件是 yaml。这个文件每写错一个字符都会在训练启动后给出各种离谱的报错。最典型的坑是names数量与 nc 不匹配nc 写 5names 列表里只有 4 个框架初始化模型时直接崩。下面是我常用的一份模板可以直接替换路径使用。path: /data/tiles_dataset train: images/train val: images/val nc: 5 names: 0: crack 1: chip 2: pinhole 3: glaze_drop 4: color_defect注意path这一段建议写绝对路径。相对路径在某些框架版本里是相对启动目录解析的你在项目根目录按下训练命令和从weights/目录按下命令解析出来的位置完全不一样。要省事就统一用绝对路径train、val 则写成相对于 path 的目录名不要写../这样的相对跳跃。4.2 启动训练模型选择与关键参数数据集不大、瑕疵形态相对固定时我从 yolov8s 起步很少直接上 large 或 xlarge。瓷砖检测大多数时候是单类别或多类别小目标s 模型已经能把裂纹、针孔这类小目标抓好还能省显存。下面是启动命令yolo train datatiles.yaml modelyolov8s.pt imgsz640 batch16 epochs200 patience20如果走 Python 脚本方式等效写法是from ultralytics import YOLO model YOLO(yolov8s.pt) model.train( datatiles.yaml, imgsz640, batch16, epochs200, patience20, workers4, )参数说明几处patience20表示验证集 mAP 连续 20 轮不涨就早停。工业数据集小我一般设 15 到 30太小容易被单点波动误杀太大则白跑很多轮。imgsz640是默认基准如果瓷砖原图是 1200 甚至 3000 像素的小目标可以先按 640 跑通流程再对比 960 看看有没有收益。batch 大小受显存限制显存只有 12G 时 batch16 配 imgsz640 通常能跑再往上加就需要看显卡剩余空间。这里还有个和标签质量相关的参数叫cacheTrue它会在训练前把图片全部读入内存或磁盘缓存数据集几百张图时建议打开能明显缩短数据加载时间。数据量特别大时不要开会把内存吃掉。4.3 训练日志与中间产物怎么判断模型在正常学习训练启动后不要只盯着 loss 曲线。YOLO 会输出包含box_loss、cls_loss、dfl_loss、mAP50、mAP50-95的表格。对瓷砖瑕疵这种小目标检测mAP50比mAP50-95更贴近产线需求因为质检只需要判断有没有缺陷IoU 阈值放宽一些反而更符合现场。如果 mAP50 能到 0.95 以上而 mAP50-95 只有 0.6说明框的位置还不够准但检出能力已经可用。这时不必继续加训练轮数应该做难样本标注迭代后面第六章写。训练完成后runs/detect/train/下会出现best.pt和last.pt。排验证指标用best.pt不要图省事拿last.pt推理尤其早停触发时二者差距可能相当大。4.4 推理看效果置信度阈值怎么调训练完成后立刻跑一次验证集推理不要只信控制台指标。推理命令我一般这样写yolo predict modelruns/detect/train/weights/best.pt sourcereal_samples imgsz640 conf0.25 iou0.45conf是置信度阈值。工厂环境对漏检零容忍对误检有一定容忍度时我会把 conf 降到 0.15 再跑一遍看哪些地方被误报出来。如果误报多才把 conf 拉回 0.3。iou是 NMS 的 IoU 阈值保留 0.45 即可别为了压框数调到 0.7重叠的裂纹框反而会被留下来。5. 常见问题与避坑五条我踩过的瓷砖检测翻车现场5.1 图片名与标签名配对不齐训练一直有静默漏检现象训练过程中能正常启动tensorboard 里 loss 正常下降但每个 epoch 都有大量图片没有参与训练最后 mAP50 始终在 0.5 左右上不去。原因数据集里有部分图片被预先翻转、裁剪过文件名多了_aug后缀但对应 txt 没有一起复制。这类文件不会报错只会被训练框架忽略。解决在划分完数据集后立即可靠地做一次 stem 集合比对imgs {p.stem for p in Path(images/train).glob(*.jpg)} labels {p.stem for p in Path(labels/train).glob(*.txt)} print(有图无标签:, sorted(imgs - labels)[:20]) print(有标签无图:, sorted(labels - imgs)[:20])把这些缺失标签的图片挑出来回原 xml 目录补转或者直接从训练集中剔除。剔除是快速解法但如果缺失数量超过 5%还是补标签更划算。5.2 标注坐标越界训练直接 NaN现象训练刚开始没几轮box_loss 变成 nan日志文件里出现assert之类报错。重启几次都一样。原因yolo 的 txt 文件没有做坐标归一化写的还是 0 到 1000 的像素值。YOLO 对归一化坐标有隐式约束超出[0, 1]后训练很快崩掉。另一类情况是 xml 的 bndbox 值超出了图片宽高转换时没有裁剪。解决转换脚本里加边界检查和裁剪这是我每次必写的逻辑。上面 3.1 的脚本已经写了max(0, xmin)和min(width, xmax)这段一定不要删。如果数据集是你拿到的现成 yolo 标注直接用脚本扫描一遍import numpy as np from pathlib import Path for txt in Path(labels).glob(*.txt): arr np.loadtxt(txt) if arr.ndim 1: arr arr.reshape(1, -1) if arr[:, 1].max() 1.0 or arr[:, 1].min() 0.0: print(越界文件:, txt)5.3 CLASS_MAP 顺序不一致把裂纹学成了崩角现象验证时模型总把裂纹缺陷预测成别的类别但召回率其实不低。单独看每个类别的 mAP有的为零有的高得离谱。原因VOC 转 YOLO 时CLASS_MAP的字典写在脚本里和训练 yaml 的names顺序不一致。比如脚本里crack: 0, chip: 1yaml 里0: chip, 1: crack模型看到的标签就是错位的。解决定下来类别映射后把转换脚本里的CLASS_MAP和训练 yaml 的names放在同一个文件里维护。我习惯在工程根目录建classes.yaml转换脚本直接读它训练 yaml 也用同一份这样只维护一个真源。转换环节不匹配的问题就根除了。5.4 验证集泄漏mAP 高到假象现象训练 mAP50 达到 0.99实际到现场一试漏检严重。原因随机划分时同一批次拍摄的瓷砖图像里训练集和验证集出现相似甚至重复视角验证指标虚高。瓷砖纹理、光照固定模型把背景纹理记住也能得高分。解决按批次或场景划分数据划分前看文件名前缀把所有同前缀的图片放到同一组。不要偷懒用random_split。如果数据量少宁可用 K 折交叉验证也不要随机乱切。5.5 混淆矩阵总和总合不等于样本数别拿它核数据现象验证完导出混淆矩阵发现每行数值加起来和标注总目标数对不上总和也不是图片总数。原因YOLO 的混淆矩阵不是按图片算的而是按“预测框 vs 真实框”配对统计。一个真实框可能匹配多个预测框被漏检的框还会进到背景列所以矩阵中的计数和标注目标数没有简单的一一对应关系。解决不要拿混淆矩阵的总数去做数据比对。要看的话按行归一化看召回率按列归一化看精确率。真正要去核数据量回 train 日志里的instances字段那才是每类标注目标的真实数量。6. 进阶用法用LabelImg增量标注难样本并复查指标6.1 难样本怎么标LabelImg 与 VOC 格式的选择模型在难样本上漏检是常态。我会先跑一次全验证集推理把漏检图片筛出来再用 LabelImg 对上面补标。补标时建议在 VOC 模式下进行因为 xml 便于增量合并。标注完的 xml 放回原有 VOC 文件夹跑一遍前面 3.1 的转换脚本新样本就自动接进了训练集。LabelImg 里类别顺序的默认排序和 yaml 不一致时手动保证类别选择正确即可。标注时遇到有些常规瑕疵看不到、但测试中确实存在的情况把它当作待确认样本去产线复核真值后再决定是不是新类别。不要看到可疑点就加标签噪声标签比缺标签对模型的伤害更大。6.2 用 results.csv 判断有没有必要再标一版训练结束后runs/detect/train/results.csv里有每一轮的完整指标。我会写一个小脚本直接读这个 csv看最后一行的mAP50与 cls_loss和上一版本训练做对比。如果 mAP50 提高不明显但误检数量明显降低说明新增样本确实压住了模型在难样本上的错误。如果两者都没变化该换思路调数据分布而不是继续堆同一类样本。import pandas as pd df pd.read_csv(runs/detect/train/results.csv) last_row df.iloc[-1] print(last_row[epoch], last_row[mAP50], last_row[cls_loss])看完指标后我会把这次训练保存的 best.pt 复制到固定目录文件名带上数据集版本例如best_v2_batch01.pt。否则过一个月再回来看你根本不知道这个权重对应哪一版标注。这是我踩过几次翻车后养成的习惯数据版本和模型版本对不上是最难排查的一类玄学问题。这里给你参考希望帮到你。本文还有配套的精品资源点击获取
返回列表