ARTICLE DETAIL

资讯详情

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

435张毛巾缺陷图训练yolov8:标签格式转换与避坑指南

435张毛巾缺陷图训练yolov8:标签格式转换与避坑指南 简介用于毛巾缺陷检测的标注数据集定位在计算机视觉与工业质检场景特别适合本科毕业设计、课程实训以及刚接触目标检测的初学者。资源提供435张毛巾样本图并配套VOC格式的XML标签与YOLO格式的TXT标签可直接用于目标检测模型的训练与评估。资源包为ZIP压缩包共包含1307个文件包括435个JPG图片、436个XML标签文件和436个TXT标签文件整体约11.72MB文件结构简洁便于离线浏览或接入常见检测训练框架样本使用统一编号命名有利于后续做数据增强和训练集、验证集划分。当前已有420人学习下载该数据集在同类需求中具备一定的参考与复用价值。对进行纺织品瑕疵检测或目标检测流程演示的读者来说这是一套带标签的轻量级起步数据也便于后续按需扩展样本规模。1. 毛巾缺陷检测数据集435张图能跑通什么跑不通什么毛巾产线上的缺陷检测如果只用传统 opencv 阈值分割素色毛巾还能应付一旦碰到毛圈纹理、印花、褶皱灰度梯度被打乱误检率高到不敢上线。这个毛巾缺陷检测数据集-435张图片及对应xml标签和yolo格式标签解决的核心问题就是把“疵点长什么样、在什么位置”固化成可训练样本。435张图听着不大却足够跑通一次完整的 yolov8 训练和验证闭环也能验证数据增强在小样本工业数据上的真实效果。适合做工业视觉落地的工程师、刚上手 yolov8 训练自定义数据集的人以及纺织品缺陷检测方向的毕设项目。需要先说清楚它解决的是“框出缺陷位置和类别”不负责产线部署里的光源、采集频率、节拍问题那些是拿到模型之后单独要做的工程活。2. 标签格式拆解xml和yolo的坐标体系与目录组织2.1 VOC xml标签到底存了什么从object节点到bounding box这套数据集里最常见的xml标签沿用的是Pascal VOC风格。一个xml文件描述一张图size节点记录图片宽高object节点每出现一次就是一个缺陷目标里面嵌套name写类别名bndbox写左上角和右下角的绝对像素坐标。以毛巾上的油渍为例典型结构长这样annotation filenametowel_011.jpg/filename size width640/width height480/height depth3/depth /size object nameoil_stain/name bndbox xmin160/xmin ymin80/ymin xmax256/xmax ymax192/ymax /bndbox /object /annotationxmin, ymin是框左上角xmax, ymax是右下角坐标单位是像素。这种格式人类可读性极好用文本编辑器打开就能看到每个缺陷的类别和位置也很容易写脚本统计分析。但深度学习训练框架不会直接吃这种嵌套结构读取和解析成本高所以数据集的作者通常会把xml同时转一份yolo格式方便直接用ultralytics的训练管道。这套格式里有几个隐含约定要心里有数filename不一定和xml文件名一致有的数据集靠path定位原图size里的宽高必须是原图实际像素如果转yolo时拿到的尺寸和图片真实尺寸对不上归一化坐标会整体偏移训练出来的框全部错位。拿到数据集的第一件事不是开训而是先校验这两项。2.2 yolo标签为什么是五个数字归一化坐标不是玄学yolo格式的标签文件是txt每一行代表一个目标一行只有五个数字类别id、中心点x、中心点y、框宽w、框高h。前四后两个是归一化到0到1之间的小数。比如上面那个油渍框转成yolo格式大概是这样0 0.3250 0.2833 0.1500 0.2333类别0对应的是类别列表里的第一个类0.3250是框中心点的x坐标除以图片宽度0.2833是中心点y坐标除以图片高度后面依次是框宽占图片宽的比例、框高占图片高的比例。归一化的好处很直接模型不关心输入图片是640乘480还是1280乘960同一个目标在两种分辨率下计算出来的标签数值完全一致训练时也就不用为不同分辨率单独处理标签。对比一下两种格式的差异格式一行表达内容坐标基准优点缺点VOC xmlobject节点嵌套绝对像素左上右下可读性强、便于人工核对解析慢、文件冗余yolo txtclass 四个float归一化中心点宽高训练加载快、缩放不变肉眼无法直接判断坐标是否合理实际训yolo时只读txtxml只承担“给人看”和“转格式源数据”两个角色。如果你拿到的数据集两种标签都有训练时直接用txt目录就行如果只有xml就得自己做一次转换后面第3章会给出完整脚本。2.3 拿到数据集先做一遍目录体检别急着训练435张图的数据集虽然小但常见坑一个不少某些图有xml却没有对应txt某些图片文件损坏打不开某些类别只在极少数图里出现。我拿到任何缺陷数据集的第一件事都是先跑一个体检脚本统计文件数量和类别分布。import os import glob from collections import Counter import xml.etree.ElementTree as ET img_dir images xml_dir xmls imgs glob.glob(os.path.join(img_dir, *)) xmls glob.glob(os.path.join(xml_dir, *.xml)) img_names {os.path.splitext(os.path.basename(p))[0] for p in imgs} xml_names {os.path.splitext(os.path.basename(p))[0] for p in xmls} print(图片数量:, len(img_names)) print(xml数量:, len(xml_names)) print(有图无标签:, len(img_names - xml_names)) print(有标签无图:, len(xml_names - img_names)) cls_counter Counter() for x in xmls: tree ET.parse(x) root tree.getroot() for obj in root.findall(object): cls_counter[obj.find(name).text] 1 print(类别分布:, dict(cls_counter))这段脚本做了三件事核对图片和xml是否一一对应统计每个类别的目标框数量打印类别名。有图无标签和有标签无图这两个数字只要不为0就得先补齐或剔除否则转换yolo格式时会出现空标签文件训练时模型会莫名其妙学到一堆背景噪声。类别分布这一步尤其关键如果某个缺陷类只有十几二十个框你心里要清楚后面训练时必须对这个类做额外处理否则模型大概率直接忽略它。3. 把xml转成yolo格式一份能直接跑的转换脚本与三个参数陷阱3.1 转换脚本解析、归一化、落盘一步到位既然xml和yolo标签都有理论上不需要自己转换。但实际项目中经常出现xml和txt版本对不上或者你想把数据集扩充一批新标注数据就必须自己转。这里给一份我常用的转换脚本直接抄下来改路径就能跑。import os import glob import xml.etree.ElementTree as ET # 类别列表顺序一旦确定就不要改后续data.yaml里的names必须和这里一致 CLASS_NAMES [broken_warp, oil_stain, hole] def clip(value, max_value): return max(0.0, min(value, max_value)) def convert_one(xml_path, save_dir): tree ET.parse(xml_path) root tree.getroot() img_w float(root.find(size/width).text) img_h float(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASS_NAMES: print(f跳过未知类别: {name} in {xml_path}) continue cls_id CLASS_NAMES.index(name) bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 防止标注框超出图片边界超出部分裁掉 xmin clip(xmin, img_w) xmax clip(xmax, img_w) ymin clip(ymin, img_h) ymax clip(ymax, img_h) # 裁完之后框宽高必须为正否则丢弃该框 if xmax xmin or ymax ymin: print(f丢弃无效框: {xml_path}, {name}) continue x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) if lines: img_id os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(save_dir, f{img_id}.txt), w) as f: f.write(\n.join(lines) \n) def convert_all(xml_dir, save_dir): os.makedirs(save_dir, exist_okTrue) for xml_path in glob.glob(os.path.join(xml_dir, *.xml)): convert_one(xml_path, save_dir) print(f转换完成结果保存至: {save_dir}) if __name__ __main__: convert_all(xmls, labels)核心逻辑分成四段第一段从size读图片宽高这是归一化的基准第二段遍历object拿到每个缺陷的类别和坐标第三段做越界裁剪防止标注时手滑把框画出图片外第四段把左上右下坐标换算成中心点加宽高再除以图片宽高得到0到1之间的归一化值。f{cls_id} ...这行用格式化字符串控制小数点后保留6位6位小数在640分辨率下对应的误差不到1像素足够用。CLASS_NAMES是整个转换里最容易被忽略的参数。它的顺序直接决定txt里每行第一个数字是几如果后续训练data.yaml里的names顺序和这里不一致模型就会把所有类别都学串位推理时把油渍识别成破洞。正确的做法是先跑第2章的体检脚本拿到真实类别名按出现频率从高到低排列排完就锁死不要再改。3.2 三个易错参数类别映射、框裁剪、路径分隔符第一个坑是类别映射。有的数据集xml里用的类别名是中文比如“油渍”“破洞”转换脚本里就必须用同样的中文作为CLASS_NAMES的键而不是自己意译成英文。yolo训练带宽字符标签的能力在ultralytics里没问题但后续部署到C或TensorRT时要手动处理编码建议一开始就统一成英文小写下划线风格。第二个坑是框裁剪。标注人员经常把框拖到图片边缘外尤其缺陷正好在毛巾边角时xmax可能比图片宽度还大。如果转换时不裁剪归一化后的w会大于1训练时anchor匹配会把这部分梯度算得乱七八糟表现就是loss能正常下降但mAP始终是0。脚本里的clip函数对这种框做了截断还要顺手把宽高为负的框整个丢掉。第三个坑是路径分隔符。Windows下os.path.join会生成反斜杠路径但data.yaml和训练脚本里的路径最好统一用正斜杠相对路径。我见过不少人在Windows上转换完直接拷到Linux服务器训练txt路径写的是D:\datasets\towel\images这种绝对路径服务器上一跑必崩。建议把转换脚本和训练数据放在同一个目录data.yaml里全部写相对路径跨机器迁移时整个文件夹一起拷走。4. 用yolov8训练自己的毛巾缺陷模型数据划分与超参配置4.1 数据划分435张图按类别分层切分避免稀有类全进验证集很多人拿到数据集直接随机切分435张图随机分到train和val看起来没问题但毛巾缺陷数据集往往类别极不平衡。假设“破洞”这个类只出现在5张图里随机切分时有概率这5张图全部落进验证集训练集里一个破洞样本都没有模型训练时完全没见过这个类验证时却要评价它的检测能力mAP直接被拉垮。正确的做法是按类别分层划分。先把所有图片按照“这张图包含哪些类别”分桶同一个组合的图片放进同一个桶然后每个桶内部按比例随机切分。这样能保证稀有类在训练集和验证集里都有分布。import os import random from collections import defaultdict import xml.etree.ElementTree as ET random.seed(42) xml_dir xmls train_ratio 0.8 bucket defaultdict(list) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue xml_path os.path.join(xml_dir, xml_file) root ET.parse(xml_path).getroot() cls_set tuple(sorted({obj.find(name).text for obj in root.findall(object)})) img_id os.path.splitext(xml_file)[0] bucket[cls_set].append(img_id) train_ids, val_ids [], [] for cls_key, img_ids in bucket.items(): random.shuffle(img_ids) split int(len(img_ids) * train_ratio) if split 0: split 1 if len(img_ids) - split 0: split len(img_ids) - 1 train_ids.extend(img_ids[:split]) val_ids.extend(img_ids[split:]) print(训练集:, len(train_ids), 验证集:, len(val_ids))这段脚本里的分桶键是tuple(sorted(...))也就是说“只有破洞”的图放进一个桶“只有油渍”的放进另一个桶“既有破洞又有油渍”的再单独一个桶。每个桶里独立做80比20的切分而不是整批图一起随机切这样每个类别组合在训练和验证里都保留了一定比例。切分完还要注意一个问题毛巾缺陷数据大多是连续拍摄同一个批次、同一个光源下拍的图片背景高度相似。如果某个桶里的图恰好都是连续拍的模型很容易把背景特征当成缺陷特征来学验证时出现虚高的mAP。切分前最好先看下文件名编号把相邻编号打散再分桶。这个逻辑可以在脚本里加一步按文件名序号排序后隔几个取一个但前提是文件名能反映拍摄顺序。4.2 训练命令与超参小数据集的保守配置切分之后需要写data.yaml。这里有个细节数据集的目录里有images和labels两个目录但images下可能已经分好了train和val子目录labels也要对应地放成train和val两个子目录yolo训练时只认这种结构。path: ./towel_dataset train: images/train val: images/val names: 0: broken_warp 1: oil_stain 2: holenames列表的索引顺序就是第3章转换脚本里CLASS_NAMES的顺序两边必须一模一样。path写数据集根目录的相对路径train和val写相对于path的子目录路径。很多报错都出在这一步labels目录没有拆成train和val或者img和label文件名后缀不匹配训练时alarm提示找不到标签。训练命令我一般这么写yolo detect train \ datatowel.yaml \ modelyolov8n.pt \ epochs150 \ imgsz640 \ batch16 \ patience30 \ project./runs \ nametowel_defect \ lr00.001第一次跑会自动下载yolov8n.pt预训练模型权重如果环境不方便联网下载就手动把预训练权重文件放到当前工作目录命令里的model参数保持不变ultralytics会优先从本地找同名权重。这个预训练权重对422张小数据集意义很大它能提供已经在COCO上学好的底层纹理特征毛巾毛圈这类重复纹理在小样本情况下很难从零学起来。关键的参数配置如下参数取值说明modelyolov8n.ptnano版参数量小小数据集不容易过拟合epochs150配合patience早停实际100轮左右会收敛imgsz640缺陷目标小降到320会把细节磨掉batch16显存不够就8但不要低于4lr00.001比默认值0.01更保守patience30连续30轮mAP不涨就停省时间小数据集最忌讳大模型和大学习率。yolov8n是nano版本参数量约3.2M拿它训430多张图是够用的。有人一上来就换yolov8x训练两三百轮直接过拟合验证集loss一路飙升。如果训练到后面发现mAP50卡在0.5上不去可以试着把mosaic增强关掉再训最后20轮毛巾缺陷的边界往往依赖精确纹理信息过强的马赛克增强会把缺陷边缘切碎。5. 缺陷检测数据集避坑5条血泪经验让训练不翻车5.1 训练中loss突然变NaNBN崩溃不是玄学现象训练到第30轮左右loss从0.5左右突然跳变到NaN或者数值直接变成负的重启训练后在前20轮看似正常又到同一个位置翻车。原因batch size开太小。小数据集的batch经常被设成4甚至2batch normalization层的均值和方差统计量在小batch下抖动剧烈叠加mosaic增强带来的输入分布突变BN的滑动平均直接崩掉。毛巾表面纹理复杂缺陷图又少输入分布本来就比普通数据集更散更容易触发这个问题。解决把batch提到16显存不足就降imgsz到512。同时把lr0降到0.001。如果用的是yolov8还可以在训练到后期把mosaic关闭ultralytics里可以设置mosaic为0比如最后20轮把增强强度降下来。BN崩溃一旦发生最省事的办法是找到最后一次正常保存的权重从那个checkpoint恢复训练而不是从头再来。5.2 混淆矩阵行和不为1先分清归一化和bug现象训练结束打开confusion_matrix.png发现每一行的数值加总起来不等于1甚至有的行只有0.8左右第一反应是后处理代码写错了。原因yolo输出的混淆矩阵是归一化后的比值每一行代表“真实类别为A的样本被预测到了各个类别上的比例”。除了对角线之外还有一部分样本被预测为背景被算到了background那一列行和当然是1。但有人用的是自己写的评估脚本对predict结果重新统计了绝对数量这时行和反映的是“该类别样本总数乘以比例”不等于1就正常了。解决官方训练日志里的混淆矩阵直接用就行不要拿它算准确率。真正要排查的是PR曲线和F1曲线如果某一类的AP值明显偏低看混淆矩阵对应行是不是被预测成了别的类而不是纠结行和。毛巾缺陷里油渍和色差视觉特征非常接近这两个类互相误检在混淆矩阵上表现就是对角线数值低、邻近格数值高这时候应该合并类别或增加样本而不是改评估代码。5.3 标签坐标越界loss能降但mAP为0现象训练一轮下来loss降得挺正常从2.5一路降到0.3但验证集mAP50从头到尾都是0PR曲线完全贴边。原因xml转换yolo时没有做框裁剪。标注时框拖出图片边界xmax大于图片宽度转换后归一化坐标出现大于1的数。yolo训练时的anchor匹配和边框回归都基于“坐标在0到1之间”这个假设越界标签直接导致正样本匹配不上loss虽然能通过背景分类优化降下去但模型根本没学到任何正样本回归的能力。解决回到第3章转换脚本把clip函数加上重新转换一遍标签。排查时写个10行脚本扫一遍所有txt看有没有小于0或大于1的数字import glob for path in glob.glob(labels/train/*.txt): for line in open(path): nums list(map(float, line.strip().split())) if min(nums[1:]) 0 or max(nums[1:]) 1: print(path, 越界:, line.strip())这个检查脚本可以一直留着以后自己标注新数据时每次转换完都跑一遍。5.4 正常毛圈被识别成破洞细密纹理导致误检现象验证集里模型频繁把正常毛巾的毛圈阴影预测成破洞置信度还不低0.7以上。看起来模型学到的不是“破洞”的整体结构而是“局部灰度突变”这个低级特征。原因毛巾表面本来就是细密纹理毛圈高低差在光照下形成大量类缺陷边缘。小目标检测模型对局部patch很敏感训练样本里破洞的局部边缘和正常毛圈的阴影边缘高度相似模型选了一条偷懒的路只要看到灰度梯度大就报破洞。解决往训练集里加入正常毛巾图不给它们任何标签也就是空标签txt。yolo的空标签文件就是一个0字节txt模型看到这些图时只会把它们当作负样本学习。正常图不用多30到50张就够关键是覆盖不同的光照角度和毛圈方向。另一个有效做法是在训练时把iou置信度阈值提高推理时把conf阈值从0.25调到0.4过滤掉大量低置信度误检。5.5 某一缺陷类别recall特别低类别不平衡现象总体mAP看着还行可能到0.6但偏偏“油污”这一类recall只有0.210个真实油污框只框出来2个。打开训练日志油污的框数量在全部目标框里只占5%。原因数据集中各类缺陷数量差异太大。断经这类常见缺陷可能有两三千个框油污只有几十个框模型在训练时绝大多数梯度更新都来自大类小类的特征在小batch里很少有机会被充分优化。yolo默认的损失函数对所有类别一视同仁数量少的类别天然吃亏。解决最直接的是给稀有类加权。ultralytics支持在data.yaml里配置每一个类别的损失权重比例油污的实例数量少就在对应的权重系数上放大。另一个做法是对稀有类样本做离线过采样把含油污的原图复制几份每份做不同的翻转、旋转、亮度扰动。注意不能直接原地复制同一张图那样模型会把这些重复样本背下来而是要做增强后另存为新文件。6. 验证模型效果的三个硬指标与一个检查习惯6.1 从mAP50、PR曲线和F1-置信度判断模型能不能用训练完成后不要只盯着训练日志里的最后一行“mAP50: 0.623”就收工三个指标要结合起来看。mAP50达到0.6以上说明整体检测能力可用PR曲线的右下角如果明显往里弯说明高置信度区域误检比例高毛巾纹理场景下经常是这种情况。更实用的是F1-confidence曲线挑一个F1最高点对应的置信度阈值作为推理参数常见做法是取0.3到0.4之间。如果F1曲线很平且峰值不高说明模型泛化能力一般应该回到数据层面补样本或者重新切分而不是继续调阈值。6.2 用opencv把预测画到原图上做最终确认指标归指标最终信不信得过还得把预测结果画到原图上肉眼过一遍。我用opencv配合训练好的模型做单张验证这段代码可以直接跑import cv2 from ultralytics import YOLO model YOLO(runs/towel_defect/weights/best.pt) img cv2.imread(val_sample.jpg) results model.predict(img, conf0.3) for r in results: for box in r.boxes: x1, y1, x2, y2 map(int, box.xyxy[0].tolist()) cls int(box.cls[0]) score float(box.conf[0]) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) label f{model.names[cls]} {score:.2f} cv2.putText(img, label, (x1, max(0, y1 - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imwrite(result.jpg, img)这段逻辑很直白box.xyxy拿到检测框的像素坐标box.cls取类别idbox.conf取置信度opencv把框和文字画上去。conf0.3是推理阈值和验证阶段F1曲线挑出来的阈值保持一致。画完图重点看两类图被漏检的缺陷图、被误检的正常纹理图这两类比任何指标都更能说明模型能不能上线。我现在拿到任何缺陷检测数据集的习惯很简单先跑体检脚本看类别分布再做一次标签越界扫描然后才允许自己开训练。毛巾、无纺布、印刷品这些纹理复杂的场景翻车点高度相似这套检查流程能拦住八成以上的训练事故。希望帮到你。本文还有配套的精品资源点击获取
返回列表