☰
布匹缺陷检测数据集实战:VOC与YOLO标签转换及YOLO训练指南
2026/10/5 9:34:48 网站建设 项目流程

简介:面向工业缺陷检测与智能质检场景的布匹缺陷检测数据集,共包含800张布匹图像,标注“损坏”与“污渍”两类缺陷,同步提供VOC格式xml标签和YOLO格式txt标签,标注精确,主流目标检测算法可直接调用训练。压缩包共2401个文件,包括800张jpg原图、800个xml标注文件及801个txt标签文件,整体大小约988MB,文件命名对应清晰,便于按图像快速定位标签。该数据集适用于布匹破损识别、污渍检测、产线智能质检等实际项目,也可作为工业视觉算法研究、模型验证的标准化数据基础。目前已有984人学习下载,数据质量可靠,能够有效支撑深度学习模型的训练与评估,减少自行采集和标注数据的时间成本。

1. 布匹缺陷检测数据集:800 张损坏与污渍图,先看标签再谈训练

布匹缺陷检测数据集这五个字,放到实际产线上就是一条能省下不少质检人力成本的流程起点。这份资源一共 800 张布匹图像,缺陷类别只有两种:损坏和污渍,难得的是同时附了 VOC(xml)和 YOLO(txt)两套标签,这意味着你不用再为格式转换花一个下午。真正值得花时间的,反而是拿到手之后先把标签格式检查清楚,而不是直接扔进训练脚本里跑一夜——数据量不大,但格式干不干净,直接决定你后面是睡个好觉还是爬起来调 bug。

做工业缺陷检测、智能质检相关项目,或者正在做目标检测方向的毕业设计,都可能用得上这份数据。布面纹理重复度高、背景干扰强,和自然图像里的语义目标差异很大,属于典型的「小样本 + 强纹理」问题。下面我按自己拆数据的习惯来走一遍:先看格式,再规整目录,然后训练、踩坑、自检。

2. 先别急着开训:看懂 VOC 与 YOLO 两种标签格式,才算拿到钥匙

2.1 损坏与污渍:质检现场两类缺陷为什么必须分开标

布匹质检里,缺陷通常按「对售价的影响」来区分。污渍是布面异物,油污、色渍、水印都算,浅色污渍可能后道水洗能去掉,深色污渍只能降级处理;损坏则直接破坏坯布结构,抽丝、破洞、断经都是这一类。算法要做的不是笼统判断「这卷布有毛病」,而是说清楚毛病是哪一类、位置在哪、面积多大,后续才能决定是修补、降级还是直接判定报废。

所以这份数据集把「损坏」「污渍」分开标注,模型天然就是一个二分类检测任务。这个设计对你训练阶段的影响在于:你得确认这两个类别在训练集和验证集里都有足够样本,而不是某一类占了绝大多数。数据标注质量怎么样,不能光看描述,我习惯先跑一遍统计脚本。写一个简单的 Python 脚本,把 xml 里所有 object 的 name 去重并计数:

import glob import xml.etree.ElementTree as ET xml_files = glob.glob("path/to/Annotations/*.xml") cls_count = {} for f in xml_files: root = ET.parse(f).getroot() for obj in root.iter("object"): name = obj.findtext("name") cls_count[name] = cls_count.get(name, 0) + 1 print(cls_count)

逻辑说明:遍历所有 VOC 标注文件,取出每个 object 节点里的 name 字段,按类别名累加。运行后你一眼就能看到两个类别的名字到底是什么——可能是损坏、污渍这样的中文,也可能是 damage、stain 之类的英文。这个信息直接决定了你后面写 data.yaml 时的类别顺序。对应的,YOLO 侧也可以快速统计 class_id 分布,命令是:

awk '{print $1}' path/to/labels/*.txt | sort | uniq -c

awk 逐行读取所有 txt 标签,取第一列 class_id 做排序和去重计数。这样 xml 和 txt 两侧的类别统计就都摸清了。

2.2 同一目标两套坐标:XML 绝对框与 TXT 归一化框的换算

VOC 的 xml 和 YOLO 的 txt 本质上是同一批框的两种表达方式。xml 用绝对像素值描述左上角和右下角,人眼可读,但训练脚本读取时要额外解析;txt 用类别编号加归一化中心坐标描述,数据紧凑,YOLO 系列框架可以直接读。一张图里如果只有一个缺陷,那么对应一个 object 节点和一行的 txt;多个缺陷就是多个 object 和多行 txt,顺序不一定一致,所以在做格式转换或互检时,不能按行号硬对齐,要按几何位置去匹配。

对比项VOC(xml)YOLO(txt)
坐标形式xmin、ymin、xmax、ymax,绝对像素x_center、y_center、width、height,归一化
类别表示name 字符串,如损坏class_id 整数,从 0 开始
一图多目标多个 object 节点每个目标一行
存储方式一个 .xml 文件每张图一个同名 .txt 文件

说到这里,我一般会给刚接触这套格式的同事看一个具体换算例子。假设图片宽度 W=1000,高度 H=600,某个损坏框的绝对坐标是 xmin=120、ymin=80、xmax=340、ymax=260,那么归一化公式是:

xc = (xmin + xmax) / 2 / W = (120 + 340) / 2 / 1000 = 0.230 yc = (ymin + ymax) / 2 / H = (80 + 260) / 2 / 600 ≈ 0.283 w = (xmax - xmin) / W = (340 - 120) / 1000 = 0.220 h = (ymax - ymin) / H = (260 - 80) / 600 ≈ 0.300

于是 xml 里那一段 bndbox 节点,在 yolo txt 里对应的一行就是0 0.230 0.283 0.220 0.300。从这里也能看出一个关键点:YOLO 坐标是归一化的,不依赖图片原始分辨率。你训练时把图 resize 到 640x640,txt 里的值不需要跟着变,模型内部会按比例处理。

2.3 多目标多行标注:用脚本把类别清单和坐标边界摸一遍

容易踩的地方在这里:一张布匹图像里可能同时出现多个污渍,甚至损坏和污渍共存。VOC 侧就是多个<object>,YOLO 侧就是多行 txt。另一个容易搞混的是坐标原点,VOC 和 YOLO 都是左上角为原点、向右向下为正,和 OpenCV 图像坐标一致,但和某些标注工具输出的人工坐标体系不一定一致,所以拿到资源后不要凭感觉目测。

我拆这份数据时习惯写一个快速抽查脚本,随机挑两三张图,把 xml 和同名 txt 的内容都打印出来,人工核对一遍。核心逻辑是用 xml 里的绝对坐标反算 YOLO 格式,再和 txt 里的已有值对比,看误差是否在合理范围内:

import cv2 import numpy as np import xml.etree.ElementTree as ET def xml_to_yolo_box(obj, img_w, img_h): name = obj.findtext("name") bbox = obj.find("bndbox") xmin = float(bbox.findtext("xmin")) ymin = float(bbox.findtext("ymin")) xmax = float(bbox.findtext("xmax")) ymax = float(bbox.findtext("ymax")) xc = (xmin + xmax) / 2 / img_w yc = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h return [name, xc, yc, w, h] # 用 cv2 读图拿真实宽高,而不是只看 xml 里的 size 节点 img = cv2.imread("path/to/JPEGImages/buque_118_1.jpg") h_img, w_img = img.shape[:2] root = ET.parse("path/to/Annotations/buque_118_1.xml").getroot() for obj in root.iter("object"): print(xml_to_yolo_box(obj, w_img, h_img))

逻辑说明:xml_to_yolo_box把一个 object 节点转换成 YOLO 风格的五元组。读图时建议直接用 cv2 或 PIL 拿实际宽高,不要只信 xml 里 size 节点写的值——文件拷贝过程中图片被重新压缩或截断的情况我见过不止一次,xml 的 size 和真实图对不上,最后训练时目标全偏。打印出来的结果如果和你手里的 txt 内容一致,基本可以认为两套标签出自同一标注。如果不一致,优先以能直观核对的 XML 为基准。

3. 把 800 张图变成可训练的 YOLO 工程:目录规整与划分脚本

3.1 目录和 data.yaml:训练脚本认的是配对关系

YOLO 系列训练时,脚本只认一个目录约定:images 下的一张图,在 labels 下必须有一个同名但后缀为 .txt 的标签文件。目录层级怎么摆随你,但配对关系必须成立。我这几年习惯用这样的布局:

dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml

VOC 那套 xml 标注建议不要动,原样留在单独的目录里当源数据;训练时直接用 YOLO txt。这样万一后面对标签有疑问,还能回到 xml 查原始标注,相当于给自己留了后悔药。data.yaml 是这个布局的核心配置,内容长这样:

path: /absolute/path/to/dataset train: images/train val: images/val names: 0: damage 1: stain

参数说明:path建议写绝对路径,省得训练时找不到相对位置;train和val是相对 path 的目录;names的顺序必须和 txt 里的 class_id 严格对应。如果你的资源里两类缺陷叫其他名字,把damage和stain换成你实际统计出来的类别名即可。

3.2 划分 train/val:随机种子、类别平衡与数据去重

800 张图怎么划分是个看着简单、其实容易埋雷的问题。我一般固定随机种子先按 8:2 切一遍,把图片和对应 txt 成对复制到新目录,不改动原始文件。脚本如下:

import os import random import shutil src_img = "path/to/JPEGImages" src_txt = "path/to/yolo_labels" dst = "path/to/dataset" train_ratio = 0.8 random.seed(42) # 固定随机种子,保证可复现 imgs = [f for f in os.listdir(src_img) if f.lower().endswith((".jpg", ".png", ".jpeg"))] random.shuffle(imgs) split = int(len(imgs) * train_ratio) splits = {"train": imgs[:split], "val": imgs[split:]} for subset, flist in splits.items(): os.makedirs(f"{dst}/images/{subset}", exist_ok=True) os.makedirs(f"{dst}/labels/{subset}", exist_ok=True) for fname in flist: base = os.path.splitext(fname)[0] shutil.copy2(os.path.join(src_img, fname), f"{dst}/images/{subset}/{fname}") shutil.copy2(os.path.join(src_txt, base + ".txt"), f"{dst}/labels/{subset}/{base}.txt")

逻辑说明:先列出所有图片,shuffle 打乱顺序,按比例切成 train 和 val 两份,再把图片和标签复制过去。shutil.copy2会保留文件时间戳等信息,后续排查时能多一个参考维度。固定random.seed(42)是很重要的一个动作,不然每次跑出来的 train/val 划分都不同,你调出来的参数在下一轮划分里可能完全不适用。

划分完别急着训练,先做类别分布统计。用一个小脚本确认 train 和 val 里两个类别都有样本:

import glob from collections import Counter cnt = Counter() for subset in ("train", "val"): for txt in glob.glob(f"path/to/dataset/labels/{subset}/*.txt"): for line in open(txt): parts = line.strip().split() if len(parts) == 5: cnt[(subset, int(parts[0]))] += 1 print(cnt)

逻辑说明:读取 train 和 val 下所有 txt,把每行的 class_id 计到对应子集下。如果 val 里某个类别一个样本都没有,模型在验证阶段就会直接把这个类别的 AP 算成 0,你看到的 mAP 会是残缺的。碰到这种情况,改一下随机种子重新划分,或者手动从 train 里挪几张该类的图放到 val。

3.3 VOC 与 YOLO 互检:两套标签能不能对得上

资源同时给了两套标签,不代表一定能对齐。文件分发过程中 xml 和 txt 来自不同版本的情况并不少见。我一般会写一个互检脚本,把同一张图的 xml 转成 YOLO 格式,再和 txt 对比,超过阈值就报警:

import glob import os import cv2 import xml.etree.ElementTree as ET def parse_yolo_txt(txt_path): boxes = [] for line in open(txt_path): parts = line.strip().split() if len(parts) != 5: continue cid, xc, yc, w, h = parts boxes.append((float(xc), float(yc), float(w), float(h))) return boxes def boxes_to_yolo_set(boxes, img_w, img_h): yolo_set = set() for name, xmin, ymin, xmax, ymax in boxes: xc = round((xmin + xmax) / 2 / img_w, 6) yc = round((ymin + ymax) / 2 / img_h, 6) w = round((xmax - xmin) / img_w, 6) h = round((ymax - ymin) / img_h, 6) yolo_set.add((xc, yc, w, h)) return yolo_set for xml_path in glob.glob("path/to/Annotations/*.xml"): base = os.path.splitext(os.path.basename(xml_path))[0] txt_path = f"path/to/yolo_labels/{base}.txt" if not os.path.exists(txt_path): print(f"[missing] {base}.txt") continue img = cv2.imread(f"path/to/JPEGImages/{base}.jpg") h_img, w_img = img.shape[:2] root = ET.parse(xml_path).getroot() xml_boxes = [] for obj in root.iter("object"): bbox = obj.find("bndbox") xml_boxes.append(( obj.findtext("name"), float(bbox.findtext("xmin")), float(bbox.findtext("ymin")), float(bbox.findtext("xmax")), float(bbox.findtext("ymax")) )) xml_yolo = boxes_to_yolo_set(xml_boxes, w_img, h_img) txt_yolo = set(parse_yolo_txt(txt_path)) if xml_yolo != txt_yolo: print(f"[diff] {base}: xml={len(xml_yolo)} txt={len(txt_yolo)}")

逻辑说明:把 xml 里的每个框转成归一化 YOLO 元组,和 txt 解析出的结果做集合比较。误差阈值这里直接用了精确到 6 位的舍入,实际使用时可以换成 1e-3 的对位比较,避免舍入差异导致误报。这个脚本跑一遍,哪张图缺 txt、哪张图两套标签对不上,一清二楚。输出里[diff]的就是需要重点人工复核的样本。

这里说下我的选择:真出现不一致,我会以 xml 为基准重新生成 txt,因为xmin, ymin, xmax, ymax这种绝对坐标可以在可视化工具里直接核对,而归一化的 txt 肉眼不容易判断对错。生成方法就是上面boxes_to_yolo_set里的换算逻辑,把它写回 txt 文件即可,注意类别名要映射成对应的 class_id。

4. 常见问题排查:从报错到 mAP 虚高的五个坑

4.1 报错「Label class 3 exceeds nc=2」:类别编号错位

现象:训练刚跑起来就中断,终端报Label class 3 exceeds nc=2 in data.yaml。可能还伴随All labels are empty之类的提示。

原因:txt 里的 class_id 超出了 data.yaml 里 names 的数量。这份数据只有两个类别,class_id 只允许是 0 或 1。出现 3 说明要么混入了其他数据集的标签,要么原来的类别顺序和你 data.yaml 写的不一致,要么某些 txt 文件里混进了非法字符。

解决:先统计所有 txt 里出现过的 class_id,把分布打印出来。命令:awk '{print $1}' path/to/labels/*.txt | sort -n | uniq -c。如果看到大于 1 的编号,把那几个 txt 单独挑出来,回到 3.3 的互检脚本看对应 xml 里是什么类别,再决定是改 txt 还是改 data.yaml。另外注意 txt 文件末尾的多余空行,解析逻辑不严谨时会把空行当成非法样本。

4.2 loss 不降、mAP 为 0:先查归一化坐标越界

现象:训练能跑,但 loss 曲线降得很慢甚至不降,验证集 mAP 一直趴在 0 附近,画出的预测框位置明显不合理。

原因:一个常见来源是归一化坐标越界。比如某个 txt 里写了0 -0.02 0.5 0.1 0.1,中心点 x 为负数,框一部分跑到图外;或者w和h超过 1,直接把整个图捂住了。训练脚本一般不会因为这些值直接报错,但损失函数里梯度方向会被带偏,玄学一点的症状就是「看起来在训,实则没在学」。

解决:写个一次性脚本扫描所有 txt,检查每个值是否在合理区间:

import glob bad_files = [] for txt in glob.glob("path/to/labels/*.txt"): for line_num, line in enumerate(open(txt), 1): parts = line.strip().split() if len(parts) != 5: print(f"[format] {txt}:{line_num} -> {line.strip()}") bad_files.append(txt) continue cid, xc, yc, w, h = map(float, parts) if cid not in (0, 1) or not (0 < xc < 1 and 0 < yc < 1) or not (0 < w <= 1 and 0 < h <= 1): print(f"[range] {txt}:{line_num} -> {line.strip()}") bad_files.append(txt) print("bad files:", set(bad_files))

逻辑说明:逐行解析 txt,检查类别编号是否合法、中心点是否在图内、宽高是否为正且不超过 1。跑完后把有问题的文件单独处理:手动修正,或者从这份资源配套的 xml 重新生成对应 txt。越界框裁剪成图内框看起来简单,实际会改变中心坐标和宽高,我一般不建议直接 clamp,宁可回退到 xml 重新算。

4.3 训练 mAP 高、验证拉胯:标签和图片配对不上

现象:训练集上的 mAP 能到 0.95 以上,一到验证集就掉到 0.3 甚至更低,预测框位置整体偏移,看起来是「见过但认不出」。

原因:训练时 YOLO 按文件名配对图片和标签。如果划分目录时图片和 txt 没有成对复制,或者源目录里本就有部分图片没有对应标签,就会出现模型在训练时看到的全是「干净图」,验证时却要面对没见过的带缺陷图。还有一种是 txt 和图片来自不同版本,框的位置整体错位。

解决:划分完以后跑一遍配对检查,确保 train 和 val 下每个图片名都有同名 txt,反过来也成立。检查脚本很简单:

import os for subset in ("train", "val"): img_dir = f"path/to/dataset/images/{subset}" lab_dir = f"path/to/dataset/labels/{subset}" imgs = {os.path.splitext(f)[0] for f in os.listdir(img_dir)} labs = {os.path.splitext(f)[0] for f in os.listdir(lab_dir)} print(subset, "missing labels:", sorted(imgs - labs)) print(subset, "missing imgs:", sorted(labs - imgs))

逻辑说明:把图片和标签各自按文件名基名取集合,做差集就能定位到缺失项。这个检查应当放在划分脚本之后、训练脚本之前,养成习惯后能省掉大量调试时间。补配对的方式是回到原始 VOC 标注目录,看那张图是否有 xml,如果有就参照 3.3 的逻辑补生成 txt;如果确实没有标注,就直接把它从训练集里排除。

4.4 mAP 高得离谱:train/val 划分里的数据泄露

现象:验证集 mAP 高得不像话,超过 0.99,换成一张没参与训练的真实产线图立马翻车,漏检严重。

原因:数据泄露,最常见的是 train 和 val 划分时按图片文件简单随机打乱,但同一块布匹的多个局部图被同时分进了两个集合。比如文件名里像buque_118_1.jpg和buque_118_2.jpg这样的编号,如果它们是同一匹布上相邻的采样区域,视觉内容高度相似,模型在训练时其实已经「见过」了验证集的纹理和缺陷形态。布面纹理本身重复度高,这种情况比自然图像数据集更容易发生。

解决:划分时看图名前缀,按场景分组而不是按单张图分组。保守做法是把同一前缀的图全部归入同一个集合,再跑训练。另外,划分后可以用文件基名做一次交集检查:

def file_base(fname): # 按项目文件名的习惯,取第一个数字分段作为场景 id return "_".join(fname.split("_")[:2]) train_bases = {file_base(f) for f in train_files} val_bases = {file_base(f) for f in val_files} print("overlap bases:", train_bases & val_bases)

逻辑说明:file_base从文件名里提取场景前缀,然后看 train 和 val 的场景前缀是否有交集。有交集就说明存在泄露风险,需要重新划分。这个函数里的切分规则要按你实际的文件命名来调整,不是所有数据集都能用这段代码直接跑。如果你只是想快速验证,也可以先跑一次训练看 val mAP,高到异常就回去检查划分。

4.5 某类 AP 常年为零:类别不平衡下的「只认得污渍」

现象:整体 mAP 看着还可以,但拆开看每个类别的 AP,污渍很高、损坏接近 0,模型几乎把一切可疑区域都判成污渍。

原因:两个类别在 800 张图里的框数差距过大,损坏类样本太少,模型学到的最优策略就是忽略损坏类,把所有检测都押在样本多的类别上。这在二类缺陷检测里很常见,因为真实产线上污渍发生频率就是远高于破损。

解决:先用统计脚本确认两个类别的框数占比,然后分几步处理。第一步,训练时打开数据增强,YOLOv8 默认会启用 mosaic、翻转等增强,对样本少的类别有正面作用;第二步,如果损坏框数实在少,可以针对该类做复制粘贴式的简单增强,或者对包含损坏的整图做小幅旋转、亮度扰动后重新生成标注;第三步,如果依然不平衡,训练时适当提高该类别的损失权重。不要一上来就堆增强,先确认是数量问题还是标注质量问题,后者优先回到 3.3 排查。

5. 用 YOLO 训一版可上线的检测器:命令参数与指标自检

5.1 训练命令与参数速查:从预训练权重起步

800 张图属于典型的小样本工业数据集,我不建议随机初始化从头训,直接用预训练权重迁移是更稳的起点。以 YOLOv8 为例,目录规整好后,训练命令就是一行:

yolo detect train \ data=/path/to/dataset/data.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=640 \ batch=16 \ patience=30

参数说明:model=yolov8n.pt用轻量级预训练权重起步,显存占用小、迭代快,先跑通流程;imgsz=640是速度和精度的折中,如果缺陷目标在图上占比很小、比如只有几个像素的抽丝,可以试 1024,但显存压力和训练时间都会上来;batch=16按显卡显存调整,8G 显存跑不动就降到 8;patience=30表示验证指标连续 30 轮不提升就早停,省时间。训练时的损失函数由定位损失、分类损失和置信度损失三部分组成,如果你盯着 loss 曲线发现下降速度不对,先别急着动学习率,回去看一眼是不是标签问题,这是我吃了好几次亏换来的习惯。

5.2 验证集自检:别只盯着一个 mAP 数字

训练结束后别急着部署,先在验证集上做一次全量推理:

yolo detect predict \ model=runs/detect/train/weights/best.pt \ source=/path/to/dataset/images/val \ save_conf=True \ save_txt=True

跑完后把预测图片和原始标注并排看一遍,重点不是 mAP 数值,而是错检和漏检的形态。布面质检最怕的是「脏布判成好布」,漏检比误检代价高得多。打开保存的 txt 标签和原标注做数量对比,如果某些图上标注框有 5 个,模型只吐出 2 个,那不管 mAP 多高都不能上线。

再看分类别指标,YOLOv8 训练输出里的 results.csv 会有每个类别的精确率和召回率。损坏类召回率低就往增强和样本补充方向走,污渍类精确率低就检查是不是把纹理阴影误判成了污渍。部署前的最后一步,我会把 best.pt 导出成 ONNX 或 TensorRT 格式,在产线推理机上用同一批验证图再过一遍,确认端到端的结果和训练时一致。这套流程走完,模型才算真正「能碰产线」。

做完这一整套,我的一个血泪教训是:数据集的「双格式」描述不可靠,可靠的是你自己跑过一遍的核对脚本。从那以后,我每次拿到新数据集,不管介绍里写得多干净,都会先花半小时把类别、坐标边界和 train/val 划分跑一遍,再允许自己碰训练命令。这套流程虽笨,但帮我挡掉了至少三次通宵调参的夜晚。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询