简介:这是一份面向毕业设计及工业视觉缺陷检测入门的高质量图像数据集,包含435张毛巾表面缺陷原始图片,并同时提供VOC格式的xml标注与YOLO格式的txt标注,覆盖多种常见瑕疵类型,方便直接接入主流目标检测框架进行训练与验证。压缩包共1307个文件,其中jpg图像435张、xml标签436个、txt标签436个,整体大小仅11.72MB,轻量易传输,适合快速开展模型迭代与算法实验。标注文件与图片一一对应,目录结构清晰,既可用于从零训练检测模型,也适合在现有模型上进行微调或对比不同标注格式的效果。目前已有420人学习下载,资源保持完整更新,并支持在评论区留言获取数据增强脚本等配套工具,能够有效降低毕设或课题前期数据准备的门槛。对于需要毛巾缺陷检测基线数据集或目标检测标注范式的使用者来说,这份资源具备较强的实用价值。
1. 435张图的毛巾缺陷检测数据集:xml和yolo双份标签才是它的硬通货
拿到一个只有435张图的毛巾缺陷检测数据集,第一反应通常是“太少,没法训练”。但如果你在产线上跑过视觉项目就知道,真正的瓶颈往往不是图片数量,而是标签格式能不能直接喂进自己熟悉的训练管线。这个数据集的硬通货在于:每张图自带xml标签和yolo格式标签,同一份标注换了两套主流格式,意味着你不必自己写坐标转换,也不用纠结VOC转YOLO时的比例换算,解压之后可以直接开训。适合两类人:一是刚开始做布匹、毛巾类柔性材料质检,想快速评估目标检测路线值不值得投入的工程师;二是想完整走一遍“数据集整理-训练-验证”流程,但又不想在格式转换上耗时间的人。接下来按我实际做项目的顺序,说清楚怎么用、参数怎么设、坑在哪。
2. 看清双格式标签:目录结构、坐标体系和一致性校验
2.1 目录结构与xml标签的读取
这类数据集解压后,文件名通常按序号排,一张jpg配一个同名xml,yolo标签目录里是同名txt。我一般先不急着训练,第一件事是把一张图的xml标签读出来,确认标注框用的是绝对像素坐标、坐标原点是左上角,同时把图像宽高记下来——后面所有换算都以这两个值为基准。
import xml.etree.ElementTree as ET xml_path = "Annotations/0001.xml" tree = ET.parse(xml_path) root = tree.getroot() # 图像尺寸在xml的size节点里,yolo格式归一化时需要用到 width = int(root.find("size/width").text) height = int(root.find("size/height").text) for obj in root.iter("object"): name = obj.find("name").text 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) print(name, xmin, ymin, xmax, ymax, "img_size:", width, height)这段代码做的事情很基础,但值得一行行看:root.find("size/width")拿到的是整张图的像素宽度;iter("object")遍历图中所有标注目标,因为毛巾缺陷图里一张图上可能同时有三四处污渍或破洞;每个目标的bndbox节点里存的是矩形框的左上角和右下角坐标,单位是像素。很多人在这一步囫囵吞枣,等到后续转格式时才发现xmin/ymin被当成了中心点坐标,导致框全错位。
2.2 yolo标签的坐标约定与还原
yolo格式的txt文件每一行是“类别id 中心点x 中心点y 框宽 框高”,后四个值全部除以图像宽高做了归一化,取值范围0到1。这跟xml里的绝对像素坐标是两套语言,理解清楚才能在做校验时不犯迷糊。
# 读取一份yolo标签并还原为像素坐标 with open("labels/0001.txt", "r") as f: for line in f: parts = line.strip().split() cls_id = int(parts[0]) cx, cy, w, h = map(float, parts[1:]) # 归一化坐标还原成绝对像素坐标 xmin = int((cx - w / 2) * width) ymin = int((cy - h / 2) * height) xmax = int((cx + w / 2) * width) ymax = int((cy + h / 2) * height) print(cls_id, xmin, ymin, xmax, ymax)注意w和h是目标框的相对宽度和高度,不是实际像素宽高。还原时先减半再乘width/height,得到左上右下角点。这里的width、height继续沿用2.1节从xml里读出的原始图像尺寸,不能自己随便改,否则画的框会整体漂移。
2.3 双格式一致性校验:一份脚本盯住换格式的脏数据
数据集同时给两套标签,最大的好处是能互相验证。我每次拿到类似数据,先写一段脚本把xml坐标换算成yolo格式,再和数据集自带的txt逐行比对,误差超过1e-3就报警。不要信“发布者说两套标签一致”这种话,实际数据里经常出现xml改了一版但txt没同步的情况。
import xml.etree.ElementTree as ET def xml_to_yolo_line(xml_path, width, height): """把xml里的一个目标转成yolo格式的字符串""" tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter("object"): name = obj.find("name").text cls_id = class_names.index(name) # 需要提前定义class_names列表 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) cx = (xmin + xmax) / 2 / width cy = (ymin + ymax) / 2 / height w = (xmax - xmin) / width h = (ymax - ymin) / height lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") return lines # 用txt文件逐行比较,这里省略读txt的代码,逻辑是逐行split后比较浮点差值这个校验脚本只做一件事:证明两份标签描述的是同一组框。如果某张图在xml里有3个目标、txt里只有2行,多半是txt漏写;如果数值对不上,多半是某次标注修改后只存了一份。这种脏数据如果不筛出来,训练时模型会学到互相矛盾的监督信号,轻则mAP偏低,重则整个类别学不出来。花十分钟跑一遍脚本,比训练完再回头查数据高效得多。
提示:校验时比较浮点数用绝对误差,不要用字符串相等,因为xml转yolo时小数位数会截断。
3. 用yolo格式直接训练yolov8:目录、data.yaml与最小可复现命令
3.1 按项目约定重排目录
yolov8训练自己的数据集,目录结构有约定俗成的规矩:images里分train和val,labels里也分train和val,图片和标签文件名一一对应,放到同一个父目录下。这个步骤看起来机械,但80%的路径报错都出在这里——train下面有一张图对应的txt不在labels/train里,训练时会报“label not found”。
# 项目根目录结构示意 towel-dataset/ ├── images/ │ ├── train/ │ │ ├── 0001.jpg │ │ └── ... │ └── val/ │ ├── 0400.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── 0001.txt │ │ └── ... │ └── val/ │ ├── 0400.txt │ └── ... └── data.yaml如果你解压出来的数据是“图片一个目录、xml一个目录、yolo标签一个目录”的平铺结构,就先写个小脚本按9:1或8:2划分训练验证集,把图片和对应txt分别拷到对应子目录。划分时别用随机乱抽,要确认每张图都有txt;这个任务上435张图建议留40到50张做验证集,其他做训练集。
3.2 data.yaml与类别名防错
data.yaml是整个yolo训练的中枢配置,路径、类别数、类别名都写在这里。最容易踩的坑是names列表的顺序必须和txt里每行开头的数字严格对应,txt里的0就代表names里的第一个类别,你要是把列表顺序写反,训练不会报错,但预测出来的标签语义全是错的。
# data.yaml path: /home/user/towel-dataset train: images/train val: images/val nc: 3 names: 0: wrinkle 1: stain 2: holepath字段是数据集的绝对路径或相对工程根目录的路径,train和val相对于path写。nc必须和names的长度一致。类别名叫wrinkle、stain、hole只是举例,你要打开数据集里的txt看一眼哪几个数字出现得最频繁,再去xml里确认这些数字对应什么缺陷名,以实际标注为准。
3.3 yolov8训练命令与关键参数
整理好目录、写好data.yaml后,一条命令就能跑起来。
yolo detect train \ data=dataset.yaml \ model=yolov8s.pt \ epochs=200 \ imgsz=640 \ batch=16 \ patience=30 \ workers=4 \ seed=42model=yolov8s.pt是加载COCO预训练权重做迁移学习,435张图的数据集千万不要从随机初始化开始训练,收敛速度和最终精度都会差很多。imgsz=640是训练输入尺寸,毛巾原图如果是高分辨率工业相机拍的,一张图可能3000x2000,直接压缩到640会把细小的纱线缺陷糊没,这是毛巾检测里最微妙的问题——我一般先看标注框的最小尺寸,如果框宽高小于20像素,宁可把imgsz调到1280,代价是显存和训练时间翻倍。patience=30表示验证集mAP连续30轮不涨就早停,小数据集上这个值设小一点能省时间,因为很容易过拟合。
还建议加一个cache=True参数,第一次会把图片加载进内存缓存,435张图占用不大,但每轮迭代都能省下磁盘IO时间。
3.4 训练过程中看什么指标判断收敛
训练开始后不要干等,盯着终端输出的几个指标就够了。重点看mAP50和mAP50-95的曲线是否还在上升,同时看val_cls_loss是否和train_cls_loss保持同步下降。如果训练集loss一直降、验证集loss在某个epoch开始反弹,这就是过拟合信号,此时训练出来的模型对训练图背得滚瓜烂熟,换一张新毛巾就废。对于435张图这种体量,前50轮mAP50能到0.8以上不算稀奇,关键是验证集mAP50不要比训练集低太多,差值超过0.2就要考虑加数据增强或调小模型容量。
训练结束后,模型权重会存在runs/detect/train/weights/best.pt和last.pt。best.pt是验证集mAP最高的那一轮,用它做推理;last.pt只是断点续训用的,别拿去部署。
4. 用xml格式走迁移学习路线:Faster R-CNN在小样本上的可复现流程
4.1 为什么这类小样本场景仍值得跑一轮Two-stage检测器
yolo是省事的第一选择,但遇到像毛巾这种纹理复杂、目标尺度变化大的柔性材料,one-stage检测器有时候会吃亏——小污渍在浅层特征图里,语义信息和位置信息结合得不够准。Faster R-CNN这类two-stage检测器先由RPN提候选框、再对候选框做分类回归,对“小目标+背景复杂”的场景通常更稳。我在只有几百张图的项目上,习惯两条线都跑:yolo出结果做快速验证,Faster R-CNN出结果做对比,哪个在验证集上mAP高就用哪个。消费级显卡上435张图训练一轮Faster R-CNN也就几分钟,完全跑得起。
4.2 自定义Dataset读取xml标签
用torchvision自带的Faster R-CNN实现,只需要自己写一个Dataset类,把xml标签解析成boxes和labels的tensor返回。下面是核心代码骨架。
import os import cv2 import torch import xml.etree.ElementTree as ET from torch.utils.data import Dataset class TowelVOCDataset(Dataset): def __init__(self, img_dir, xml_dir, class_names, transforms=None): self.img_dir = img_dir self.xml_dir = xml_dir self.class_names = class_names self.transforms = transforms self.ids = [f.split('.')[0] for f in os.listdir(xml_dir) if f.endswith('.xml')] def __getitem__(self, idx): img_id = self.ids[idx] img_path = os.path.join(self.img_dir, img_id + '.jpg') image = cv2.imread(img_path) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) height, width = image.shape[:2] boxes = [] labels = [] tree = ET.parse(os.path.join(self.xml_dir, img_id + '.xml')) for obj in tree.iter('object'): name = obj.find('name').text labels.append(self.class_names.index(name)) # 注意:类别id从0开始 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) # 防止越界和无效框 if xmax <= xmin or ymax <= ymin or xmin < 0 or ymin < 0: continue boxes.append([xmin, ymin, xmax, ymax]) boxes = torch.as_tensor(boxes, dtype=torch.float32) labels = torch.as_tensor(labels, dtype=torch.int64) target = {"boxes": boxes, "labels": labels} if self.transforms is not None: image, target = self.transforms(image, target) return image, target__getitem__返回两个对象:图像tensor和target字典。target里boxes必须是[N, 4]的tensor,labels必须是[N]的int64 tensor。注意代码里做了边界防御——如果xml里某个框xmax小于等于xmin就跳过,这种数据在这个数据集里是否出现不知道,但加上这行没坏处。Faster R-CNN要求batch里每张图的框数量不同也能一起训练,所以DataLoader的collate_fn要单独写,把list原样返回,不能像分类任务那样stack成一个大tensor。
4.3 训练循环、学习率与backbone冻结策略
小样本上最容易翻车的是从零训练一个完整的Faster R-CNN,参数太多,几百张图根本喂不饱。正确做法是加载在COCO上预训练好的权重,把最后的分类头换成自己的类别数,先冻结backbone只训练检测头,后期再解冻全部微调。
import torchvision from torchvision.models.detection import FasterRCNN from torchvision.models.detection.rpn import AnchorGenerator # 使用预训练backbone backbone = torchvision.models.resnet50(weights="DEFAULT") # 只取backbone部分,输出channel是2048 model = FasterRCNN(backbone, num_classes=len(class_names)+1, min_size=800, max_size=1333) # 冻结backbone所有层 for param in model.backbone.parameters(): param.requires_grad = False optimizer = torch.optim.SGD( [p for p in model.parameters() if p.requires_grad], lr=0.005, momentum=0.9, weight_decay=0.0005 ) # 训练3轮检测头后,再解冻backbone for epoch in range(3): for images, targets in data_loader: images = [img.to(device) for img in images] targets = [{k: v.to(device) for k, v in t.items()} for t in targets] loss_dict = model(images, targets) losses = sum(loss for loss in loss_dict.values()) optimizer.zero_grad() losses.backward() optimizer.step() for param in model.backbone.parameters(): param.requires_grad = True optimizer = torch.optim.SGD(model.parameters(), lr=0.0005, momentum=0.9, weight_decay=0.0005)这个两段式训练有几个参数是调出来的:第一阶段检测头lr用0.005,因为随机初始化的分类头需要相对大的步长才能快速学起来;第二个阶段backbone已经有预训练基础,lr降到0.0005做微调,太大容易把预训练特征破坏掉。min_size=800、max_size=1333是torchvision默认的输入缩放策略,等比缩放而不是直接压成正方形,这对毛巾上的长条形褶皱检测很重要——直接resize成正方形会导致长宽比变形,褶皱特征被拉歪。
4.4 用mAP和失败样例双向验证
Faster R-CNN训练完,不能只看loss。torchvision没有内置的mAP计算器,但可以用torchvision.ops手动算,也可以用工程里常用的方式:先关掉梯度,用模型对验证集每张图做推理,收集预测框、得分、标签,再和xml里的真值做IoU匹配,统计mAP50。
model.eval() with torch.no_grad(): predictions = model([img.to(device) for img in images]) # predictions[0]包含boxes、scores、labels三个字段这里有个实用的检查方法:把预测结果画在图上,肉眼扫一遍。指标好看但画出来全是框框套框框,说明NMS阈值没调好;指标一般但画出来错误很一致(比如总把褶皱当破洞),说明类别定义本身有歧义,这时要回看数据集的xml里这些类别的分布。
提示:小数据集上单一指标不可信,务必结合可视化样本判断。mAP是统计指标,会掩盖局部的系统性错误。
5. 训练中常见的坑与排查:从坐标越界到“训练loss下降但map为0”
5.1 坐标越界与负宽度:xml转yolo时最常见的脏数据
现象:训练启动时报错,提示某些标签的框宽或框高为负数;或者训练能跑但loss出现nan。用脚本检查txt,发现有的行cx、cy超出0到1范围,有的w、h大于1。
原因:原始xml里标注框本身就画出了图像边界,比如xmin为0、xmax显示为2000但图片宽度只有1980;也可能是某个标注工具导出时把左上和右下坐标写反了,导致xmin大于xmax。xml转yolo脚本只做了简单除法,没做合法性校验,脏数据就一路流进了训练管线。
解决:在转换或校验阶段加过滤规则,对完全越界的框直接丢弃,对部分越界的框做截断。
# 修正部分越界框的示例代码 xmin = max(0, xmin) ymin = max(0, ymin) xmax = min(width, xmax) ymax = min(height, ymax) if xmax <= xmin or ymax <= ymin: continue # 截断后变成无效框,丢弃这部分数据量通常很小(一个35张图的数据集里可能就两三处),直接丢弃不会影响训练,但留着会污染loss。我在每次训练前都把这份“边界检查”脚本固定跑一遍,输出不合规的行号,属于强制步骤。
5.2 训练loss正常但mAP一直是0:类别索引与names对不上
现象:yolov8训练了150轮,训练loss稳步下降,但验证集mAP50一直为0,日志里每个类别的AP全是0。落盘推理时发现预测框全标成了同一个类别。
原因:txt文件里写的是0、1、2,但data.yaml里names顺序和数据集作者标注时的意向不一致。比如作者把0定义为污渍、1定义为破洞,你的yaml里0却写成了破洞、1写成了污渍。模型本身没训错,错的是“类别和数字的映射字典”,loss不会因此异常,但算mAP时每个预测框都和真值语义对不上。
解决:先统计txt里类别id的实际分布,再逐个对照xml里的name字段确认映射关系。
# 统计全量txt里每行第一个数字的出现次数 cat labels/train/*.txt | awk '{print $1}' | sort | uniq -c然后随机抽一个样本的txt,把坐标还原画到图片上,肉眼确认框的位置和缺陷类型是否对应得上。
import cv2 img = cv2.imread("images/train/0001.jpg") with open("labels/train/0001.txt") as f: for line in f: cls_id, cx, cy, w, h = line.split() cx, cy, w, h = float(cx), float(cy), float(w), float(h) x1 = int((cx - w/2) * img.shape[1]) y1 = int((cy - h/2) * img.shape[0]) x2 = int((cx + w/2) * img.shape[1]) y2 = int((cy + h/2) * img.shape[0]) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(cls_id), (x1, y1-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) cv2.imwrite("check_label.jpg", img)画出来看一遍,类别映射的错误会非常直观——框圈在油渍上但显示的类别id是破洞,那就知道谁错了。这个坑耗时最多的是排查过程,很容易在“代码写得没问题”的自我暗示里打转,可视化是打破黑匣子的最快办法。
5.3 训练集收敛但现场漏检:环境分布差异与伪标签修正
现象:验证集上mAP50在0.9以上,误差案例也少,但把模型放到产线新拍的照片上测,漏检一大半,尤其是浅色毛巾上的浅色污渍。
原因:数据集里的照片是在某一固定光照、固定背景、某几类毛巾花纹下拍摄的,模型学到的是“这种光照下的缺陷长这样”。产线环境背景更杂、毛巾花纹换了、光源角度不同,像素分布整体漂移,预训练特征在小样本条件下没有余量去泛化这种偏移。
解决:采集一小批现场新样本(几十张即可),用当前最优模型做推理,把置信度在0.9以上的预测框作为伪标签,人工抽检后加入训练集再微调。这个自训练流程是这类小样本质检项目最常用的补数据手段。
# 对新采集的无标签图片做批量推理,输出带置信度的标签 yolo detect predict model=runs/detect/train/weights/best.pt \ source=./new_samples \ save_txt=True \ conf=0.9 \ project=./pseudo_labelconf=0.9是关键参数,不要为了多捞几个框降到0.5。伪标签的噪声是累加的,低置信度框一旦混进训练集,二次训练会把噪声当真理学进去。宁可少捞,不可捞错。这个坑在数据增强调不动的情况下的确能救回来一截,但它不是银弹,根本解法还是持续积累真实标注数据。
5.4 显存爆掉:输入尺寸与batch形态错配
现象:训练启动后几秒就报CUDA out of memory,即使把batch降到4也照样爆。
原因:毛巾数据集里的原图如果是工业相机输出的高分辨率大图,yolo在训练时会把图resize到imgsz参数指定的大小,这个逻辑对显存的影响是固定的。但Faster R-CNN那类two-stage检测器不一样,它按输入图像的原始长宽比做缩放,min_size=800、max_size=1333意味着每张图可能被缩放到1333x1333甚至更大,再叠加batch,显存占用远超yolo同参数下的水平。
解决:先把数据集里的图缩放或裁剪到统一尺寸,再做训练。对毛巾这种纹理密集型材质,等比缩放比直接压成正方形好,因为长宽比变形会改变褶皱的形态特征。如果用的是yolo管线,直接把imgsz从640降到512通常能腾出三分之一显存;如果用的是Faster R-CNN,把min_size改成600、max_size改成1000,框的绝对精度会略微下降,但小样本场景里这个代价可接受。
这个问题没有通用解,只能按“显存占用 = 输入尺寸 × batch × 模型复杂度”这个关系自己掂量。换显卡是最省事的方案,但先把输入尺寸降下来往往就解决了。
6. 小样本毛巾质检的增强三板斧与我的验证习惯
最后一公里,说说我在这类小样本数据集上最常用的三个增强策略和一个固定习惯。
第一板斧是mosaic之外加“针对性光度扰动”。yolo自带的mosaic会把四张图拼在一起训练,对小目标检测效果好,但毛巾缺陷往往集中在局部,mosaic反而让缺陷变得更小更难学。我会额外开启亮度扰动和对比度扰动,模拟产线不同光源角度下的成像差异,幅度控制在±25%以内,太大容易把浅色污渍直接调成背景色。
第二板斧是“冻结骨干再解冻”的两段式微调。不管用yolo还是Faster R-CNN,小数据集上从头训练所有层都会过拟合。常见做法是先用预训练权重在冻结backbone的情况下训几轮,让检测头适应毛巾缺陷的任务分布,然后解冻全部层用更低的学习率微调。我一般把预训练权重文件提前放到工程目录缓存里,内网离线环境也能直接加载,不依赖在线下载。
第三板斧是伪标签自训练,第五章第三节已经详细说过,这里强调一个操作细节:伪标签加入训练集时,给这部分样本单独建一个目录,不要混进原始训练集里。这样如果二次训练mAP反而下降,可以快速把伪标签数据剔除,恢复到上一版权重,等于给自己留了一颗后悔药。
最后是我雷打不动的验证习惯:每次训练完,除了看mAP曲线,一定从验证集里挑10张图,把预测框画出来人工看一遍。指标会说谎,但画出来的框不会。有一次模型mAP50已经到了0.95,画出来才发现所有预测框都比真实框小一圈,因为标注里框得太紧、模型学到了紧缩的框分布,这种问题在指标上看不出来,但部署到产线上会让定位精度不达标。后来我把验证集的可视化脚本固定成训练流程的一部分,每轮训练完自动输出一版带预测框的图,肉眼扫一眼再决定要不要继续调参。
小样本数据集的每一次训练都是“数据质量优先于模型技巧”,先把双格式标签的一致性校验跑通,把可视化做扎实,再谈调参和换模型。希望帮到你。
本文还有配套的精品资源,点击获取