毛巾缺陷检测:435张图训练YOLO模型的完整流程与避坑指南
简介一套面向织物质检场景的毛巾缺陷检测数据集特别适合高校学生用于毕业设计、课程实践或缺陷检测算法验证。数据涵盖435张实拍毛巾图像每张图片均配套VOC格式XML标注与YOLO格式TXT标注可同时满足传统检测框架与深度学习模型如YOLOv5、YOLOv8等的导入需求省去自行转换标注格式的麻烦。全包共1307个文件除435张jpg原图外还包含436个xml标签和436个txt标签整体压缩包约11.72MB结构精简下载后即可解压划分训练集与验证集。目前已有420人学习使用尤其适合正在筹备质检类毕设项目的同学参考。值得说明的是数据支持进一步做数据增强以扩充样本多样性配合标注文件可快速搭建完整的缺陷检测训练流程帮助用户聚焦算法设计与实验对比也可直接用于模型训练与精度评估。1. 毛巾缺陷检测数据集435张图能训练出可用的YOLO模型吗毛巾缺陷检测数据集名字已经把三件事说完了检测目标是毛巾表面的缺陷素材只有435张图片标注格式同时给了xml标签和yolo格式标签。第一次拿到这个量级我也犯嘀咕435张能训出什么真跑下来发现只要标签质量靠谱、迁移学习参数不翻车这个规模足够把一个能抓破洞、污渍、毛边异物的小模型送到验证台上甚至直接进产线当预筛选。做这类浅色纺织品缺陷检测最大的麻烦不是算法而是数据形态太乱。毛巾表面纹理密、反光不均匀一个污渍在不同光照下可能变成两个完全不同的特征。这套数据集的价值在于它把图片、xml、yolo格式标签三件套配齐了省掉了最耗时的转标工作。接下来重点不是再去找更多数据而是把435张图的标签吃透、转换干净、训练过程盯住让一个小模型稳定跑起来。适合谁看准备做工业视觉缺陷检测、刚入YOLO训练、以及手里只有几百张标注图想快速出结果的工程师。2. 拆解xml标签与yolo格式标签同一套框两种坐标表达拿到数据集的第一步不是急着训练而是先把两种标签的格式对应关系看清楚。xml标签通常按Pascal VOC那一套组织yolo格式标签则是一行一个目标、全部归一化。同一张毛巾图两套标签描述的是同一批缺陷框只是存放结构和坐标单位不同。2.1 xml标签长什么样Pascal VOC结构拆解用文本方式打开一个xml标签结构非常直观。每张图片对应一个annotation根节点里面记录文件名、图片宽高以及一个或多个object。每个object就是模型要学的一个缺陷实例。annotation foldertowel_defect/folder filenameIMG_0082.jpg/filename size width640/width height480/height depth3/depth /size object namehole/name bndbox xmin120/xmin ymin88/ymin xmax176/xmax ymax150/ymax /bndbox /object /annotation这里bndbox四个值分别代表缺陷框的左、上、右、下像素坐标单位是像素。特别注意filename字段有些标注工具会写入全路径有些会写入中文文件名转换前要统一取basename避免后续同名匹配失败。说到怎么打开和编辑xml最容易上手的方案是用VS Code装XML插件或者用Notepad的XML Tools插件做格式化校验。我一直不推荐用Excel直接改xml很容易把文件编码改成带BOM的UTF-8轻则解析告警重则整个标签读不出来。如果只是看内容浏览器直接拖进去也能看。2.2 把xml转成yolo格式标签Python转换脚本与四组关键参数这一节是整个数据准备里最重要的一段。yolo格式要求每行写类别id 中心点x 中心点y 框宽 框高并且中心点、宽高都必须除以图片宽高归一化到0到1之间。我用Python标准库xml.etree.ElementTree就能完成全量转换不需要额外依赖。import xml.etree.ElementTree as ET from pathlib import Path # 类别顺序一旦定下来就不能再改后面训练集的data.yaml要跟它完全一致 CLASSES [hole, stain, broken_yarn, fluff] def voc_to_yolo(xml_root: Path, img_root: Path, out_root: Path) - int: out_root.mkdir(parentsTrue, exist_okTrue) converted 0 for xml_file in sorted(xml_root.glob(*.xml)): tree ET.parse(str(xml_file)) root tree.getroot() filename root.findtext(filename) if not filename: print(f[skip] {xml_file.name} 缺 filename) continue img_file img_root / Path(filename).name if not img_file.exists(): print(f[skip] 找不到原图: {img_file}) continue img_w int(root.findtext(size/width)) img_h int(root.findtext(size/height)) lines [] for obj in root.findall(object): name obj.findtext(name) if name not in CLASSES: print(f[warn] 未知类别 {name} 在 {xml_file.name}) continue box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) # 转归一化坐标 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 越界修正手标时偶尔会把框拖到图像外 x_min_c max(0.0, x_center - w / 2) x_max_c min(1.0, x_center w / 2) y_min_c max(0.0, y_center - h / 2) y_max_c min(1.0, y_center h / 2) if (x_max_c - x_min_c) 1e-3 or (y_max_c - y_min_c) 1e-3: print(f[skip] 宽或高为0: {xml_file.stem} - {name}) continue x_center (x_min_c x_max_c) / 2 y_center (y_min_c y_max_c) / 2 w x_max_c - x_min_c h y_max_c - y_min_c cls_id CLASSES.index(name) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) if lines: out_txt out_root / (xml_file.stem .txt) out_txt.write_text(\n.join(lines), encodingutf-8) converted 1 return converted if __name__ __main__: n voc_to_yolo( xml_rootPath(annotations_xml), img_rootPath(images), out_rootPath(labels_yolo), ) print(f转换完成: {n} 个xml标签)这段脚本有几个参数需要盯住。第一CLASSES列表的顺序就是yolo标签里数字编号的依据hole是0stain是1后面训练配置里的names必须一字不差按这个顺序写顺序一旦错整个模型学的东西就全乱了。第二img_w和img_h来自xml里的size如果xml里的尺寸和实际图片不一致框的位置就会整体偏移建议转换后用程序统计对比一次。第三越界修正不是可选项435张图里总有几张手标时框跑出图像边界不修的话训练过程会一直刷坐标越界警告严重时YOLO会直接把这条标签丢弃。第四输出txt的文件名必须和xml文件名一致yolo靠文件名自动关联图片和标签文件名错一个字符这张图等于白标。2.3 转换后自检图片、xml、txt三者必须一一对应转换脚本跑完不算完还要做一次全量自检。435张图数量不大但正因为少任何一张图标签丢失都会被模型感知到。自检分三步先数文件个数再对文件名最后抽看坐标分布。find images -name *.jpg | wc -l find labels_yolo -name *.txt | wc -l如果两个数字对不上就用下面的Python脚本找出差异项from pathlib import Path imgs {p.stem for p in Path(images).glob(*.jpg)} txts {p.stem for p in Path(labels_yolo).glob(*.txt)} print(缺标签:, imgs - txts) print(缺图片:, txts - imgs)这一步能救回很多翻车现场。之前在另一个纺织品项目里我就遇到过一批图片因为文件名里的空格被工具替换成了下划线导致xml对不上原图转换时直接跳过最后少了几十张。如果发现某张图没有对应txt也先别急着删图回头看是不是xml里存在空object。只有图片、xml、txt三者完全对应才值得进入下一步训练。3. 用435张毛巾图跑通YOLO训练从目录结构到损失曲线数据格式准备好了接下来就是把这套毛巾缺陷检测数据集喂进YOLO。基于Ultralytics的训练流程整个准备工作的核心全在目录结构和data.yaml两个地方。只要这两个地方不出错训练命令本身就非常短。3.1 目录结构与data.yaml训练脚本的参数全在数据准备里YOLO读取训练数据不靠额外注册只认目录约定。常见做法是建立如下的目录结构towel_dataset/ ├── images/ │ ├── train/ 约350张jpg │ └── val/ 约85张jpg ├── labels/ │ ├── train/ 对应train的txt │ └── val/ 对应val的txt └── data.yaml关键点在于图片放在images/train标签必须放在labels/train两者靠同名关联。比如images/train/IMG_0082.jpg对应的标签路径是labels/train/IMG_0082.txt中间不能有别的映射文件。data.yaml内容如下path: /home/user/towel_dataset train: images/train val: images/val nc: 4 names: 0: hole 1: stain 2: broken_yarn 3: fluff这里最容易忽略的是path字段。建议直接写绝对路径不要写相对路径。用相对路径时你在不同目录下执行训练命令解析出来的数据位置会不一样这个问题排查起来非常隐蔽。还有一个细节训练集和验证集划分时别用简单的随机切分见第4章第4条避坑。3.2 最小训练命令与预训练模型选择目录和yaml都备好训练就一行命令的事。数据量小我用的是yolov8n作为底模先跑通再考虑换大模型。yolo detect train \ datatowel_dataset/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ workers4 \ seed42 \ patience20这个命令里每个参数都有明确作用。modelyolov8n.pt第一次运行会自动下载预训练权重如果你在离线环境需要提前把权重文件放到当前目录让YOLO直接复用本地权重。epochs100对435张图来说偏大但配合patience20早停实际跑到五六十轮就会自动停下来。batch16在8G显存的卡上跑640分辨率基本够用如果显存紧张可以降到8但下一章会讲batch太小会带来新的坑。seed42固定随机种子让每次训练可复现。workers4是数据加载线程数Windows上建议改成0或2避免多进程加载卡死。先用yolov8n而不是yolov8l原因很直接数据量小的时候大模型更容易过拟合训练时间还长。把n型号跑到稳定再用同一份数据集验证它和s、m型号的差距这种对比才有意义。3.3 看损失函数判断模型状态box_loss、cls_loss、dfl_loss训练一跑起来终端会循环打印一组损失值box_loss、cls_loss、dfl_loss。很多人只盯着box_loss降没降其实三个损失要一起看。box_loss衡量预测框和真实框的重合误差YOLOv8里走的是CIoU那套逻辑框的位置准不准主要看它。cls_loss是分类损失负责判断这个框里到底是破洞还是污渍。dfl_loss是分布焦点损失负责让框的边界更贴目标边缘对毛巾这种缺陷边界模糊的物体尤其重要。在小数据集上我最关注两类异常。第一类box_loss一直降cls_loss降不动还反复震荡这说明类别分布不均衡某个类别的缺陷框太少模型根本没学会。解决办法不是调损失权重而是回头数一下每个类别的框数量把数量少的缺陷单独做增强。第二类train的损失一路下降val的损失却开始抬头说明开始过拟合了。毛巾纹理细节多435张图撑不起模型把特征背下来一旦出现这个苗头靠patience20提前截断或者减少epochs重跑。4. 毛巾缺陷检测避坑指南5个让模型翻车的细节435张图的数据集没有容错空间一个标签错一个参数乱最后模型都会直接表现在推理结果上。这一章列几个真实遇过的坑按现象、原因、解决三部分写。4.1 xml解析报错标签框丢失一半多半是编码和结构问题现象写到转换脚本里ET.parse直接抛异常或者能解析但findall(object)只能拿到一部分框。原因xml文件被改过编码常见的是开头多了个BOM个别文件里还混入全角空格。另外有些标注工具会把object嵌在objects之类的父节点里路径没写对就漏掉。解决先用文本编辑器批量把文件转成UTF-8无BOM格式。解析时不要死写root.findall(object)先打印一层结构确认object到底在哪一层再写对应的XPath路径。转换前把全部类别名收集打印一遍能顺便发现拼写不一致。4.2 转换后txt是空文件或行数对不上类别名不一致现象自检时发现txt文件存在但内容为空或者xml里有5个目标txt里只有3行。原因最常见的坑是类别名大小写不一致。xml一个文件写Stain另一个文件写stain脚本里if name not in CLASSES直接把后者过滤掉了。其它情况是某个框的坐标字段缺失解析出来是None转float时抛异常。解决转换脚本里不要遇到未知类别就静默跳过改成打印警告并汇总。全部转换完后对比xml里每个object的数量和txt行数数量对不上就单独排查。把类别统一成小写砍掉首尾空格这类问题基本能消灭。4.3 训练刷越界警告一张图被整条丢弃现象训练日志里反复出现WARNING labels with size zero或是w、h大于1的提示。点开对应图片看缺陷框确实在图上。原因手标时把框拖出了图像边界归一化后宽高算出来是负数或0。YOLO内部对非法标签的处理方式是直接丢弃这张图的标注等于白做了。解决转换脚本里做越界修正把坐标clamp到0到1之间同时过滤掉宽高小于1e-3的框。这一步要在训练前做别指望训练过程自己去处理。当时我提过修复后警告消失验证集的mAP明显更稳定。4.4 验证mAP虚高换新毛巾直接漏检训练验证切分污染现象训练结束验证集mAP能达到0.9以上信心满满拿去测新拍的毛巾结果破洞一个都没检出。原因切分训练集和验证集时用了随机划分但同一块毛巾可能被拍了好几张不同角度和光照的图随机切分会让同一条毛巾同时出现在训练集和验证集里。模型等于见过考题验证分数自然虚高。解决按毛巾个体分组切分。如果图片文件名能区分样品编号就按编号前缀划分如果分不出来宁可分成训练集400张、验证集35张也要确保先聚类再切分。这个坑在435张的小数据集上杀伤力极大因为同一样品重复出现比例高虚高现象比大数据集严重得多。4.5 训练到一半loss变成NaNbatch太小触发BN崩溃现象前面几十轮正常突然某轮的box_loss变成nan然后整个训练崩掉。换小batch重跑还是崩基本无解。原因小batch下BatchNorm层的均值方差估计不稳定加上初始学习率偏大数值一下就溢出。这就是常说的训练中BN崩溃在低显存环境尤其常见。解决第一batch尽量提到16以上实在上不去就换yolov8n这种小模型模型参数量小BN统计量更容易稳。第二训练命令加freeze10前10轮冻结骨干层只训练检测头让BN逐步适应毛巾数据。第三把lr0调低到0.001给模型一个平稳的起点。如果还崩关掉混合精度加ampFalse这个办法救过我好几次。5. 用混淆矩阵验收毛巾模型阈值怎么定、漏检怎么找训练完不等于能上线先用验证集认真看一遍模型行为。跑一次验证命令输出目录里会有混淆矩阵、PR曲线和一张张推理结果图。yolo detect val \ modelruns/detect/train/weights/best.pt \ datatowel_dataset/data.yaml \ conf0.25打开confusion_matrix.png重点看对角线以外的格子。毛巾缺陷里最容易混的是hole和stain破碎的边缘在低分辨率下看起来就是深色污渍。如果这两类互相误判严重优先检查标注框是不是把破洞边缘扩太大把背景也包进去了。另一个看点是background那一行如果大量预测框落在背景上说明模型学到的特征太宽泛常见原因是验证集里有大量无缺陷毛巾图。阈值怎么定要看业务代价。毛巾破洞漏检意味着次品流出宁可误报也不能放过我会把conf降到0.15再跑一轮看漏检率降多少。而污渍这类误报容忍度低误报多了产线停线检查浪费工时对污渍单独把置信度提到0.4再输出。Ultralytics默认不支持逐类阈值我在后处理脚本里根据类别id做二次过滤逻辑很简单破洞类置信度大于0.15就保留污渍类大于0.4才保留。验证完成后把每一类的mAP50-95单独列出来对比。如果broken_yarn明显低于其它类不用纠结调参直接去看这个类别的标注框数量。毛巾数据集中像断纱这类缺陷本来就少见可能只有几十个框模型学不够。我的习惯是先把这类样本用旋转、平移、光照变换各扩一倍再重新划分训练比调任何损失权重都有效。做小数据集缺陷检测我一直记住一句话435张图的模型不是训出来的是审出来的。每一张验证图都要亲自过一遍看看模型到底在什么光照、什么纹理背景下犯错然后把这些犯错样本挑出来补进训练集。这套流程坚持下去比盲目加数据量更扎实。希望帮到你。本文还有配套的精品资源点击获取