☰
毛巾缺陷检测数据集实战:XML转YOLO格式与YOLOv8训练指南
2026/10/5 7:16:39 网站建设 项目流程

简介:面向纺织质检与缺陷检测场景的毛巾缺陷数据集,包含435张原始缺陷图片,每张图片均配有VOC格式的xml标注和YOLO格式的txt标注,可直接用于YOLO系列、Faster R-CNN等常见目标检测模型训练,适合本科毕业设计、课程项目及工业视觉入门研究。图片样本覆盖毛巾表面典型瑕疵类型,目标框坐标与类别信息已完整标注,文件名重复编号便于按序查阅;若训练样本不足,可结合数据增强脚本扩充,以适应不同检测任务需要。压缩包共1307个文件,主要包含435个jpg图像、436个xml标签和436个txt标签,标签文件与图片一一对应,整体仅11.72MB,携带和复用成本低。目前已有421人学习下载,可直接用于模型训练验证,也可作为数据集格式转换与标注工具学习的参考素材,帮助使用者快速开展实验并聚焦算法调优。

1. 毛巾缺陷检测数据集:435张图能跑通什么、解决什么问题

纺织车间的质检工位,是目标检测落地时最容易看懂价值的场景:毛巾下线后靠人眼盯着传送带找破洞、污渍、毛圈缺失,一个班次几千条看下来,漏检和疲劳是必然的。这个“毛巾缺陷检测数据集-435张图片及对应xml标签和yolo格式标签”,就是把这类产线需求压缩成一个小而全的样本集——435张真实毛巾图,带两套缺陷标签,一版是Pascal VOC风格的XML,一版是YOLO训练直接可用的txt。它解决的不是“拿一个现成模型去部署”,而是让你用最小成本把整套链路跑通:标签格式怎么解析、XML怎么转YOLO、小样本训练怎么设参数、哪些坑会让模型在产线上翻车。适合刚入行工业视觉的算法工程师、想做织物缺陷检测课题的学生,以及准备评估YOLO方案是否值得投入的产线技术负责人。

2. 解析毛巾缺陷数据集的XML标签:Pascal VOC坐标怎么变成YOLO的一行数字

2.1 XML和YOLO标签的字段差异与对应关系

拿到这个数据集,最先要分清的是两套标签的表达方式。XML标签是Pascal VOC的标准写法,一个<annotation>根节点下面,有<size>记录图片宽高,然后是若干个<object>,每个<object>里有<name>表示缺陷类别,<bndbox>里给xmin / ymin / xmax / ymax四个绝对像素坐标。这种格式人类可读性极好,任何文本编辑器都能打开,VS Code、Notepad++ 看都行,缺点是文件体积大、解析速度慢。

YOLO格式标签则完全不同。每张图对应一个同名txt文件,每一行是五个数字:class_id x_center y_center width height。这里有几个关键差异:第一,类别不写字符串,只写整数编号,编号顺序完全由你的训练配置文件names列表决定;第二,坐标全部相对图片宽高做了归一化,取值范围在0到1之间;第三,给的是中心点坐标和框宽高,不是左上角和右下角。所以 XML 转 YOLO 的核心,就是把xmin ymin xmax ymax换算成x_center y_center width height,再除以图片宽高。这个换算本身不难,难的是处理越界、空标签、文件名匹配这些边角情况。

2.2 XML转YOLO的批量转换脚本:参数与边界处理

我一般会写一个Python脚本批量转换。用xml.etree.ElementTree解析,而不是正则表达式,因为XML的嵌套结构用DOM解析更稳。

# xml_to_yolo.py # 作用:把Pascal VOC格式的XML标签批量转成YOLO训练用的txt标签 import os import xml.etree.ElementTree as ET from glob import glob # 类别表:顺序就是YOLO的class_id,必须和训练yaml里的names一致 # 这里用stain/hole/miss_pile做占位,实际以XML里<name>字段为准 CLASSES = ["stain", "hole", "miss_pile"] def xml_to_txt(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() # 从<size>节点读图片原始宽高,这是归一化的分母 img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): cls_name = obj.find("name").text # 跳过没在类别表里定义的物体,避免脏数据混进来 if cls_name not in CLASSES: continue cls_id = CLASSES.index(cls_name) bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # 转YOLO格式:中心点 + 宽高,全部除以图片宽高做归一化 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 # 越界保护:某些标注框边缘会超出图片范围,直接截断到[0,1] x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) width = min(max(width, 0.0), 1.0) height = min(max(height, 0.0), 1.0) lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") # 输出文件与图片同名,扩展名换成.txt base = os.path.splitext(os.path.basename(xml_path))[0] txt_path = os.path.join(out_dir, base + ".txt") with open(txt_path, "w") as f: f.write("\n".join(lines)) if __name__ == "__main__": xml_dir = "Annotations" # 放XML的目录 out_dir = "labels" # 输出YOLO标签的目录 os.makedirs(out_dir, exist_ok=True) for xml_file in glob(os.path.join(xml_dir, "*.xml")): xml_to_txt(xml_file, out_dir) print(f"converted {len(glob(os.path.join(xml_dir, '*.xml')))} xml files")

几个参数值得单独说。保留6位小数不是玄学,YOLO训练时读取标签会解析浮点数,位数太少会让小目标的中心点产生量化误差,本来只有十几个像素宽的破洞可能因此偏出半个框。截断到 [0,1] 是因为标注软件导出的框偶尔会超出图片边界,这种框进训练会让模型学到一个永远不可能出现的越界位置。另外CLASSES的顺序一旦定下来就不要改,否则之前转好的txt全部作废。

2.3 把txt标签画回图片:最直观的格式校验手段

转换脚本跑完,先别急着开训练。这一步省掉,后面出问题你很难判断是标签错还是模型没收敛。我的习惯是写一个可视化脚本,把txt坐标还原成矩形画在原图上,肉眼检查人名和框的位置是否对得上。

# draw_boxes.py # 作用:读YOLO txt,把归一化坐标画回原图,确认标签是否和图片对齐 import cv2 import os from glob import glob CLASSES = ["stain", "hole", "miss_pile"] COLORS = [(0, 0, 255), (0, 255, 0), (255, 0, 0)] # BGR颜色,注意OpenCV顺序 img_dir = "images" # 图片目录 label_dir = "labels" # YOLO标签目录 save_dir = "check_visual" os.makedirs(save_dir, exist_ok=True) for img_path in glob(os.path.join(img_dir, "*.jpg")): img_name = os.path.splitext(os.path.basename(img_path))[0] label_path = os.path.join(label_dir, img_name + ".txt") img = cv2.imread(img_path) if img is None: print(f"[skip] cannot read {img_path}") continue h, w = img.shape[:2] if not os.path.exists(label_path): # 负样本:图片无缺陷,没有txt文件,直接复制一份原图 cv2.imwrite(os.path.join(save_dir, img_name + ".jpg"), img) continue with open(label_path) as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls_id, xc, yc, bw, bh = map(float, parts) # 中心点转左上角和右下角,注意cls_id要转成int才能索引颜色 x1 = int((xc - bw / 2.0) * w) y1 = int((yc - bh / 2.0) * h) x2 = int((xc + bw / 2.0) * w) y2 = int((yc + bh / 2.0) * h) color = COLORS[int(cls_id)] 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.6, color, 2) cv2.imwrite(os.path.join(save_dir, img_name + ".jpg"), img)

这个血泪经验值得记一下:看到框只画在图片角落、或者框的大小完全不符合缺陷实际占比,多半是size节点的宽高和真实图片尺寸不一致。有些标注软件的XML里宽高写的是裁剪前模板的尺寸,转出来的YOLO坐标就整体错位。画出框之后扫一遍check_visual目录,错误一眼就能看出来,比看数值日志直观得多。

3. 用YOLOv8训练毛巾缺陷数据集:yaml、参数与产物解读

3.1 目录编排与data.yaml:names顺序决定生死

格式校验完,下一步是把数据喂给训练框架。用Ultralytics YOLOv8是工业缺陷检测最常见的做法,因为它把数据集配置、训练、导出、推理封装得很完整,435张小样本也能稳定跑通。先按它的约定组织目录:

towel_dataset/ ├── images/ │ ├── train/ # 约340张 │ └── val/ # 约95张 └── labels/ ├── train/ └── val/

图片和标签的同名对应关系必须严格保持,文件名是这个体系的唯一索引。然后写数据集配置文件:

# towel_defect.yaml path: ./towel_dataset # 根目录,用相对路径避免换机器就崩 train: images/train val: images/val nc: 3 # 类别数 names: ["stain", "hole", "miss_pile"] # 顺序必须和txt第一列对应

这里最容易翻车的点是names顺序。YOLO的txt标签第一列写的是整数编号,训练框架读取时只认这个编号在names里的位置。如果转换脚本里CLASSES = ["stain", "hole", "miss_pile"],训练yaml里却写["hole", "stain", "miss_pile"],模型会把污渍当成破洞去学,mAP直接崩。每次新建训练任务,第一件事就是用脚本统计txt第一列分布,对照yaml里的names。

3.2 最小训练命令:每个参数为什么这样设

数据就位后,训练命令可以很简短。命令行和Python脚本两种方式等价,我一般用Python方式,因为方便在复现实验时统一管参数。

# 命令行方式 yolo detect train \ model=yolov8n.yaml \ data=towel_defect.yaml \ imgsz=640 \ epochs=100 \ batch=16 \ patience=20 \ device=0 \ seed=42 \ project=./runs/towel
# train_towel.py from ultralytics import YOLO # 用yolov8n的预训练权重做迁移学习起点 # 注意:model参数填yolov8n.pt会加载COCO预训练权重,比从头训练收敛快很多 model = YOLO("yolov8n.pt") results = model.train( data="towel_defect.yaml", epochs=100, imgsz=640, batch=16, patience=20, device=0, seed=42, project="./runs/towel", name="yolov8n_towel" )

参数这里逐个说。model="yolov8n.pt"用的是nano版本,参数量大约3M出头,毛巾缺陷这种简单纹理场景用nano足够,训练和推理都快,部署时也省显存。epochs=100是目标轮数,但配合patience=20后,如果验证集指标连续20轮不提升,训练会自动提前停止,这是防止小数据集过拟合的关键手段。batch=16在435张图上意味着每轮大约21个step,梯度更新足够频繁,同时显存压力不大。imgsz=640是均衡选择,但如果你的缺陷主要是小破洞,后面可以试试1280。device=0指定第一块GPU,seed=42固定随机种子,同样数据同样参数跑两次结果一致,这是做实验对比的底线。

3.3 训练产物解读:best.pt、混淆矩阵和PR曲线

训练结束后,runs/towel/yolov8n_towel/目录下有几个产物要会看。weights/best.pt是验证集mAP最高的权重,weights/last.pt是最后一轮的权重,部署时永远选best。目录里的confusion_matrix.png是归一化混淆矩阵,可以看到哪个缺陷类别被错认成了哪个,比如破洞被认成污渍,说明两者纹理特征在小分辨率下确实接近。results.csv里每行是每个epoch的loss、precision、recall和mAP,重点看val的mAP50和mAP50-95中间有没有明显回退。

工业缺陷检测场景下,mAP50比mAP50-95更贴近实际需求,因为产线质检只需要框够准能区分缺陷和正常区域,不需要像素级精度的框回归。我判断模型能不能上产线,先看mAP50过不过0.9,再看recall过不过0.95,最后才看precision——漏检比误检危害大得多,漏掉一个破洞就是直接流出不良品,而多报警一次顶多是让挡车工多看两眼。

4. 435张小数据集的五个典型坑:现象、原因与排查

4.1 标签与图片错位:loss正常但验证mAP等于0

现象:训练log里train loss正常下降,但val mAP全程为0,或者某个类别在混淆矩阵里整行归零。原因:文件名排序规则不一致。比如图片命名为towel_1.jpg到towel_10.jpg,字符串排序后towel_10排在towel_2前面,而某些划分脚本用了字典序,导致train和val里出现同一张图的不同标签版本,或者标签txt和图片根本没对应上。解决:写一个文件对齐检查,把训练集和验证集的图片名、标签名逐一比对。

# check_file_mismatch.py # 作用:找出有图无标签、有标签无图的文件 import os from glob import glob for split in ["train", "val"]: img_files = {os.path.splitext(os.path.basename(p))[0] for p in glob(f"images/{split}/*.jpg")} txt_files = {os.path.splitext(os.path.basename(p))[0] for p in glob(f"labels/{split}/*.txt")} print(f"[{split}] images without txt:", img_files - txt_files) print(f"[{split}] txt without images:", txt_files - img_files)

这个检查永远跑在训练之前。输出为空才继续,否则就回到第2章的可视化脚本,逐张看框。

4.2 类别编号错位:names顺序和txt第一列对不上

现象:训练能跑完,mAP也不低,但打开混淆矩阵发现预测结果整体偏移,比如实际是污渍的框被模型预测成毛圈缺失,而且每个类别都有同样的问题。原因:转换脚本的CLASSES列表顺序与训练yaml的names列表顺序不一致,模型用一个编号体系训练,推理时用另一个编号体系解释。解决:固定一个唯一类别表,转换脚本、data.yaml、推理脚本三处引用同一个文件,不要各自维护三份。可以直接让CLASSES = names从yaml读取:

import yaml with open("towel_defect.yaml") as f: cfg = yaml.safe_load(f) CLASSES = cfg["names"] # 转换脚本也读同一个yaml,根源上消除错位

4.3 小破洞漏检:recall在低IoU都上不去

现象:模型对污渍、毛圈缺失检出不错,但小破洞的recall一直低于0.5,放大PR曲线发现小目标类别曲线整体趴在左下角。原因:毛巾上的破洞可能在640分辨率下只占几十个像素,训练时下采样后特征几乎丢失,这是小目标检测的经典问题。解决分三步走:第一步,把imgsz提到1280,相当于让小目标在特征图上的像素占比翻倍;第二步,损失函数换成NWD(Normalized Wasserstein Distance),它对小目标的定位误差更宽容;第三步,如果还不行,解锁更大的模型如yolov8s或融合P2层特征,但435张数据上大模型更容易过拟合,要配更强的增强。

4.4 验证集mAP很高、产线实测翻车

现象:训练时mAP50有0.95,拷到产线试跑,换了一条不同颜色的毛巾,误检框突然爆炸。原因:435张数据很可能来自同一台机台、同一批次面料、同一固定光源,模型学到的其实是这个特定环境的纹理分布——背景纹理成了隐式特征。解决:训练时把HSV扰动拉大,模拟不同染缸色差和光照变化;更彻底的做法是补采集数据,至少覆盖不同毛圈密度、不同底色、不同光线角度。这条坑提醒一个原则:小数据集上验证集指标只能证明模型在这个数据分布内有效,不能证明泛化。

4.5 空标签与负样本处理不当

现象:数据集里有些毛巾没有任何缺陷,转换时直接跳过生成txt,训练时某个epoch报错,或者验证时所有正常毛巾都被误检成缺陷。原因:第一种情况,YOLO要求负样本图片有一个空txt文件,而不是完全缺失文件,缺失会导致数据加载时标签和图片对不上;第二种情况,正常毛巾没有参与训练,模型没见过正样本的“正常长什么样”,上线必然乱报。解决:转换脚本里保留空txt占位文件,确保每张图都有同名标签;负样本按20%-30%比例混进训练集,让模型学会输出“无框”。

5. 把毛巾缺陷的mAP再往上提:增强组合、冻结主干与交叉验证

5.1 毛巾纹理下的增强参数怎么调

小数据集最怕过拟合,也是数据增强最值得投入的地方。但毛巾有特殊的纹理周期性——毛圈排列、提花条纹都有方向性,无脑套默认增强会引入反作用。我用的一套参数组合如下:

参数推荐值作用注意
hsv_h0.015色调偏移值太大会改变毛巾真实色相
hsv_s0.5饱和度模拟染缸色差别超过0.7,否则色彩失真
hsv_v0.4亮度扰动模拟光照变化产线光源稳定可降到0.2
flipud0.5上下翻转毛巾无方向性纹理时可开
fliplr0.0左右翻转对条纹方向敏感的缺陷慎用
mosaic1.0四图拼接增强上下文小目标提升明显,但会引入拼接痕
degrees0.0旋转毛巾有卷边,旋转会产生假缺陷
translate0.1平移小量平移增加框位置多样性

加入训练参数就是一行的事:

model.train( data="towel_defect.yaml", epochs=100, imgsz=640, hsv_h=0.015, hsv_s=0.5, hsv_v=0.4, flipud=0.5, fliplr=0.0, mosaic=1.0, degrees=0.0, translate=0.1, seed=42, )

5.2 先冻结主干再解冻:小数据集迁移学习的标准节奏

435张图直接全量微调预训练模型,很容易把ImageNet学来的通用特征在头几十轮里冲掉。我的做法是分两阶段:先冻结backbone只训练head,等head适应了毛巾缺陷的分布,再解冻全部层做细调。freeze=10表示冻结前10层,正好覆盖YOLOv8n的backbone主体。

from ultralytics import YOLO # 阶段一:冻结主干,只训练检测头,防止灾难性遗忘 model = YOLO("yolov8n.pt") model.train( data="towel_defect.yaml", epochs=50, freeze=10, imgsz=640, batch=16, seed=42, project="./runs/towel", name="freeze_stage", ) # 阶段二:解冻全部层,用低学习率细调 model = YOLO("./runs/towel/freeze_stage/weights/best.pt") model.train( data="towel_defect.yaml", epochs=50, freeze=0, lr0=0.0005, # 解冻后降低初始学习率,避免破坏已有权重 imgsz=640, batch=16, seed=42, project="./runs/towel", name="finetune_stage", )

第一阶段epochs给50就够,head收敛很快;第二阶段lr0降到默认值的一半左右。如果第一阶段结束mAP已经不错,第二阶段的提升幅度可能不大,但通常能让框更稳。

5.3 用5折交叉验证代替固定划分:435张图的最稳划分法

固定按7:3划分训练验证集,在435张图上有个隐患:某个缺陷类别样本少,随机分一次很容易全落在训练集里,验证集指标虚高或失真。我的习惯是用5折交叉验证,每折80%训练、20%验证,跑完取均值。

# kfold_split.py # 作用:把图片清单按5折交叉验证切分,每个fold单独训练 import os from glob import glob from sklearn.model_selection import KFold img_files = sorted(glob("images/*.jpg")) kf = KFold(n_splits=5, shuffle=True, random_state=42) for fold, (train_idx, val_idx) in enumerate(kf.split(img_files)): os.makedirs(f"fold{fold}", exist_ok=True) with open(f"fold{fold}/train.txt", "w") as f: for i in train_idx: f.write(img_files[i] + "\n") with open(f"fold{fold}/val.txt", "w") as f: for i in val_idx: f.write(img_files[i] + "\n")

KFold的关键参数是shuffle=True配合random_state=42,让每折的类别分布尽量均匀且可复现。跑5次训练后取mAP均值和方差,比单次划分的结论可靠得多。如果某折mAP明显低于其他折,说明这一折的验证集里含有罕见的缺陷形态,这也是数据采集要补齐的线索。

6. 上线前的最后一道验证:可视化推理与错误分析

训练结束,best.pt拿到手,下一步不是直接上产线,而是先跑一轮可视化推理,看模型在没见过的图片上到底输出了什么。我会专门留一批完全不参与训练的图片当最终测试集,这批图从采集源头就要隔离开,避免任何形式的偷看。

# predict_and_inspect.py # 作用:用best.pt在测试图上推理,保存画框结果和txt输出,供错误分析 from ultralytics import YOLO model = YOLO("runs/towel/yolov8n_towel/weights/best.pt") results = model.predict( source="test_images", # 测试图目录,训练和验证都没见过 conf=0.25, # 置信度阈值,产线实测可调高到0.4 iou=0.5, # NMS的IoU阈值 save=True, # 保存画好框的图片 save_txt=True, # 同时保存txt标签,方便脚本统计 project="./runs/predict", name="test_eval", )

推理参数注意两个值。conf=0.25是调试阈值,能看到模型在低置信度下输出多少假阳性;产线上我会调高到0.4左右,牺牲一点recall换更少误报。save_txt=True不只是为了方便部署,它让后续错误分析可以直接读txt统计:哪些框落在了非缺陷区域——毛巾的缝边、水洗标、折叠阴影是常见假阳来源。把这些区域统计出来,要么补充这些负样本进训练集,要么在预处理阶段裁剪掉。

我的个人习惯是,每次换数据集做的第一件事永远是可视化,先画标签再画预测,这个习惯救过我很多次翻车。格式转换的错、编号错位的错、数据划分泄漏的错,全都能在可视化环节暴露出来,而不用等训练跑完才面对一个没有解释的黑匣子。希望帮到你。

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

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

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

立即咨询