毛巾缺陷检测数据集实战:VOC与YOLO格式转换及YOLOv8训练
2026/9/23 14:43:23 网站建设 项目流程

简介:毛巾缺陷检测数据集提供一批毛巾表面缺陷图片样本及配套标注,定位清晰,适合毕业设计、课题实训以及目标检测入门研究者快速上手,解决模型训练缺少现成标注数据的问题。整包共1307个文件,含436个txt标签、436个xml标签和435张jpg样本图,压缩包大小约11.72MB;txt对应YOLO格式,xml对应VOC格式,图片为原始缺陷样本,目录简洁便于按检测框架要求直接组织训练集与验证集。图片素材围绕毛巾表面缺陷场景采集,可直接用于模型训练、测试与效果评估;同时提供两种主流标注格式,省去自行转换标签的环节,也能减少预处理阶段的踩坑成本。已有420人学习下载。如需进一步扩充样本规模,可联系作者获取数据增强脚本;遇到标注细节、训练参数等问题也可留言交流,适合需要快速搭建缺陷检测基线并迭代优化的学习者。

1. 拿到毛巾缺陷检测数据集:先搞清楚手里有什么再说训练

做织物缺陷检测的人都知道,毛巾这类纹理密集、结构重复的材质,比一般工业零件表面更难做目标检测。纹理背景本身就容易产生大量误检,算法对缺陷的定位精度和置信度阈值都要反复调。这份毛巾缺陷检测数据集一共435张真实拍摄的毛巾缺陷图片,同时给了VOC格式的XML标签和YOLO格式的TXT标签,正好省去自己写标注工具和转换脚本的时间。435张图不算多,但对毕业设计、算法验证、跑通一套训练推理流程来说,量级是够用的,配合数据增强能撑住大部分实验场景。适合三类人:做毕设想快速拿到有标签数据集的在校生、要验证YOLO系列模型在织物缺陷上表现的研究者、以及刚接触目标检测想熟悉VOC与YOLO标签格式的入门开发者。下一章先把数据集的目录结构和两种标签的对应关系理清楚,这是后续所有操作的地基。

2. 数据集结构拆解:目录、命名规则与两种标签格式的对应关系

2.1 拿到压缩包后先做什么:核对目录结构与标签完整性

解压之后不要急着开训。第一步永远是核对数据完整性,这个习惯能避免后面至少一个小时的排错。一个标准的VOC风格数据集通常包含三个核心目录:JPEGImages放原始图片、Annotations放XML标签、ImageSets/Main放训练集和验证集的划分文件。这份毛巾缺陷检测数据集里,图片文件命名类似que(414).jpg,对应的XML标签是que(414).xml,YOLO标签是que(414).txt,三者文件名前缀完全一致,只是后缀不同。这种命名方式很直观,但也埋了一个坑——文件名里带了中文括号和英文括号的差异,后面单独讲。

建议先用一段脚本把三个目录的文件名对齐检查一遍,确认没有图片没标签、或者标签没图片的情况。因为后续无论转格式还是训练,默认前提都是三份文件一一对应。

import os from pathlib import Path img_dir = Path("JPEGImages") xml_dir = Path("Annotations") yolo_dir = Path("labels") imgs = {p.stem for p in img_dir.glob("*.jpg")} xmls = {p.stem for p in xml_dir.glob("*.xml")} yolos = {p.stem for p in yolo_dir.glob("*.txt")} # 检查三者的差集 print("有图无XML:", len(imgs - xmls)) print("有XML无图:", len(xmls - imgs)) print("有图无YOLO:", len(imgs - yolos)) print("有YOLO无图:", len(yolos - imgs)) # 打印具体缺失文件名 for name in (imgs - xmls) | (xmls - imgs) | (imgs - yolos) | (yolos - imgs): print("异常文件前缀:", name)

这段脚本的核心就是利用Python的set集合做差集运算,三个目录的stem(去后缀后的文件名)两两比较,任何不对称都会被抓出来。缺失的文件名会逐条打印,方便对照已有数据确认到底缺了什么,以及影响哪些图片参与训练。注意glob的匹配模式:如果你的图片扩展名混了.png或.bmp,要把*.jpg换成*.jpg或*.png这样的组合匹配,否则会把非JPG图片全漏掉,造成误报。

2.2 XML标签内部结构:看懂VOC格式的bounding box描述

VOC格式的XML标签是目标检测领域最经典的标注格式之一,它的设计目标是让一个标注文件既能表达图片基本信息,也能完整描述每一个目标的位置和类别。打开任何一个XML文件,里面大致是这个结构:annotation根节点下有folder、filename、path,然后是source与size节点,size节点里记录image的width、height、depth。后面跟着一到多个object节点,每个object对应一个缺陷目标,包含name标签名、pose姿态、truncated是否被截断、difficult是否难例,以及核心的bndbox节点——四个子节点xmin、ymin、xmax、ymax,单位是像素。

读取解析XML用Python标准库的xml.etree.ElementTree就可以。检查标签内容和边界合理性时可以这样做:

import xml.etree.ElementTree as ET def parse_voc_xml(xml_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) objects = [] for obj in root.iter("object"): name = obj.find("name").text bbox = obj.find("bndbox") xmin = int(float(bbox.find("xmin").text)) ymin = int(float(bbox.find("ymin").text)) xmax = int(float(bbox.find("xmax").text)) ymax = int(float(bbox.find("ymax").text)) # 边界合理性检查 assert xmin < xmax, f"xmin >= xmax in {xml_path}" assert ymin < ymax, f"ymin >= ymax in {xml_path}" assert xmax <= img_w and ymax <= img_h, f"bbox out of image in {xml_path}" objects.append({"name": name, "bbox": [xmin, ymin, xmax, ymax]}) return img_w, img_h, objects # 解析单个文件验证 w, h, objs = parse_voc_xml("Annotations/que(414).xml") print(f"图片尺寸: {w}x{h}, 缺陷数量: {len(objs)}") for obj in objs: print(obj["name"], obj["bbox"])

这里写了三层校验:坐标是否正向、是否超图片边界、以及XML里写入的size是否和真实图片尺寸一致。assert条件一旦不满足会直接抛异常,把问题文件暴露出来。注意bbox坐标可能被标注软件写成浮点字符串,所以用int(float(...))做双重转换更稳。这段脚本建议在拿到数据集后第一时间跑,别等训练报错再回头查。

2.3 YOLO标签的数学含义:归一化坐标与实际像素坐标之间怎么换算

YOLO格式的TXT标签每一行对应一个目标,五个数值依次是class_id、x_center、y_center、width、height。和VOC的关键区别在于:VOC存的是左上角和右下角的绝对像素坐标;YOLO存的是归一化后的中心点坐标和宽高,取值范围都在0到1之间。归一化的数学定义是:x_center等于xmin加上xmax的和除以2,再除以图片宽度;width等于xmax减去xmin的差,再除以图片宽度。y方向同理,分母换成图片高度。

这种归一化设计的好处是模型在训练时不需要关心输入图片的实际分辨率,任何尺寸的图片统一缩放到模型输入尺寸后,框的比例关系不变。反过来说,看TXT文件里的数字时,必须知道原图宽高才能反推出像素坐标,光看数字本身没有意义。某一行写着0 0.5123 0.4389 0.1832 0.1465,意思是类别ID为0的缺陷,中心点位于图片约51%宽度、44%高度处。

写一个双向转换工具类很有必要,后续做数据增强、样本分析、可视化验证时都会反复用到:

def voc2yolo(xml_path, output_txt_path): w, h, objs = parse_voc_xml(xml_path) lines = [] class_map = {"defect": 0} # 根据数据集实际类名修改 for obj in objs: xmin, ymin, xmax, ymax = obj["bbox"] x_center = (xmin + xmax) / 2.0 / w y_center = (ymin + ymax) / 2.0 / h box_w = (xmax - xmin) / w box_h = (ymax - ymin) / h # 归一化后数值越界检查 assert 0 <= x_center <= 1, f"x_center out of range: {x_center}" assert 0 <= y_center <= 1, f"y_center out of range: {y_center}" assert 0 <= box_w <= 1 and 0 <= box_h <= 1, f"box size out of range" cls_id = class_map.get(obj["name"], -1) assert cls_id >= 0, f"unknown class: {obj['name']}" lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") with open(output_txt_path, "w") as f: f.write("\n".join(lines))

换算逻辑本身不难,难的是边界情况。常见问题有三个:一是某些缺陷紧贴图片边缘,归一化的中心点或宽高恰好算出0或1,浮点精度导致越界;二是XML里xmax和ymax是像素索引,归一化时分母用width而不是width-1,差一个像素在缩放后几乎不可见,但严谨起见可以统一;三是类别名不一致,XML里写的是defect,代码里映射表写的是stain,那所有标签的类别ID都会判错。跑转换前先统计一遍XML里全部name值,确认类别集合再写映射表。

3. VOC与YOLO格式互转:坐标换算原则与批量转换脚本实现

3.1 为什么转换脚本是刚需:训练框架各自认一种格式

目标检测生态里格式割裂是个长期现象。YOLO系列官方训练代码要求TXT标签放在与JPEGImages同名labels目录下,类别从0开始编号;而很多公开标注工具、预训练数据、开源标注平台默认导出VOC格式XML。如果你拿到的是VOC格式,而训练框架是YOLOv5、YOLOv8这些主流仓库,就必须先转YOLO。反过来,如果你想用MMDetection的Faster R-CNN跑对比实验,官方推荐的中间格式又是COCO或VOC——这时你可能需要把TXT反推回XML。这份数据集两种格式都给了,省了转换的麻烦,但学会怎么转仍然重要,因为你后续做数据增强时,如果增强库只支持VOC格式或只支持YOLO格式,就要在两种格式之间来回倒腾。

3.2 批量转换全套代码:文件夹级处理带异常隔离

单文件转换会了之后,批量处理是实际使用时的常态。写批量脚本时要注意两点:转换过程不中断、异常文件单独记录。一张有问题的图片不应该毁掉整个转换任务,也不应该静默通过污染输出结果。

import os import xml.etree.ElementTree as ET from pathlib import Path def batch_voc2yolo(xml_dir, output_dir, class_list): xml_dir = Path(xml_dir) output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) class_map = {name: idx for idx, name in enumerate(class_list)} error_log = [] success_count = 0 for xml_path in sorted(xml_dir.glob("*.xml")): try: tree = ET.parse(xml_path) root = tree.getroot() # 获取图片尺寸 size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) lines = [] for obj in root.iter("object"): cls_name = obj.find("name").text if cls_name not in class_map: raise ValueError(f"未知类别: {cls_name} in {xml_path.name}") bbox = obj.find("bndbox") xmin = int(float(bbox.find("xmin").text)) ymin = int(float(bbox.find("ymin").text)) xmax = int(float(bbox.find("xmax").text)) ymax = int(float(bbox.find("ymax").text)) 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 value = class_map[cls_name] lines.append(f"{value} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") output_txt = output_dir / (xml_path.stem + ".txt") with open(output_txt, "w") as f: f.write("\n".join(lines)) success_count += 1 except Exception as e: error_log.append((xml_path.name, str(e))) print(f"转换成功: {success_count}, 失败: {len(error_log)}") for name, err in error_log: print(f" {name}: {err}") return error_log # 使用示例 class_list = ["defect"] # 替换为你的实际类别列表 batch_voc2yolo("Annotations", "labels", class_list)

逐行逻辑是:遍历XML目录、解析每个文件、读取size和所有目标框、归一化计算、写入TXT。值得注意的细节有三个。第一是sorted()对文件名排序,保证输出顺序可预期,排查问题时能按顺序核对文件。第二是class_list从外部传入,而不是硬编码在函数内部,这样数据集类别有变化时不用改函数体。第三是try-except包裹整个文件处理过程,异常被捕获后记录到error_log列表,转换任务继续跑,最后统一打印失败清单。这种设计比遇到一个坏文件整个程序崩溃要好得多——尤其当你半夜跑批量转换时,崩溃后不可能在边上盯着重启。

3.3 反转换场景:YOLO标注转回VOC用于可视化与第三方工具

有时需要把YOLO标签转回VOC。典型场景是:你要用LabelImg打开标注文件人工检查框是否画准,LabelImg只认VOC;或者要导入某个只支持VOC格式的分析工具做统计。反转换需要读取原图的宽高信息,把归一化坐标还原成像素坐标。由于TXT文件里没有图片尺寸信息,必须从对应图片文件读取。

from PIL import Image from pathlib import Path def yolo2voc(txt_dir, img_dir, output_xml_dir, class_list): txt_dir = Path(txt_dir) img_dir = Path(img_dir) output_xml_dir = Path(output_xml_dir) output_xml_dir.mkdir(parents=True, exist_ok=True) for txt_path in sorted(txt_dir.glob("*.txt")): img_path = img_dir / (txt_path.stem + ".jpg") if not img_path.exists(): print(f"图片不存在: {img_path}") continue with Image.open(img_path) as img: img_w, img_h = img.size root = ET.Element("annotation") folder = ET.SubElement(root, "folder") folder.text = img_dir.name filename = ET.SubElement(root, "filename") filename.text = img_path.name size = ET.SubElement(root, "size") width = ET.SubElement(size, "width") width.text = str(img_w) height = ET.SubElement(size, "height") height.text = str(img_h) depth = ET.SubElement(size, "depth") depth.text = "3" with open(txt_path, "r") as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls_id, x_center, y_center, box_w, box_h = ( int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) ) # 还原像素坐标,注意边界截断 xmin = int((x_center - box_w / 2) * img_w) ymin = int((y_center - box_h / 2) * img_h) xmax = int((x_center + box_w / 2) * img_w) ymax = int((y_center + box_h / 2) * img_h) xmin = max(0, min(xmin, img_w - 1)) ymin = max(0, min(ymin, img_h - 1)) xmax = max(0, min(xmax, img_w - 1)) ymax = max(0, min(ymax, img_h - 1)) obj = ET.SubElement(root, "object") name = ET.SubElement(obj, "name") name.text = class_list[cls_id] bndbox = ET.SubElement(obj, "bndbox") for tag, val in [("xmin", xmin), ("ymin", ymin), ("xmax", xmax), ("ymax", ymax)]: node = ET.SubElement(bndbox, tag) node.text = str(val) tree = ET.ElementTree(root) output_path = output_xml_dir / (txt_path.stem + ".xml") tree.write(output_path, encoding="utf-8", xml_declaration=True)

这里关键的处理是边界截断那四行。归一化坐标反算像素值时,由于浮点精度和标注时的人为误差,算出的xmin可能是负数或xmax超过图片宽度,直接用会导致后续读取图片区域时崩溃。截断到0到img_w-1的范围内,保证XML里的坐标永远合法。另外用PIL的Image.open读取图片尺寸,注意不要在这里调用img.load()或做任何像素访问,只取size属性后立刻关闭——大批量读取时这个开销差异很明显。

3.4 转换后验证:用可视化脚本确认标签框位置正确

转换完不能直接开训,先随机抽几张图片画框验证。这一步能发现大多数转换错误:坐标偏移、比例失调、框和缺陷位置对不上。可视化脚本的本质就是拿OpenCV或PIL把坐标画回图片上,肉眼直接对比。

import cv2 from pathlib import Path def visualize_yolo_boxes(img_path, txt_path, class_names, output_path): img = cv2.imread(str(img_path)) h, w = img.shape[:2] with open(txt_path, "r") as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls_id, x_center, y_center, box_w, box_h = map(float, parts) cls_id = int(cls_id) xmin = int((x_center - box_w / 2) * w) ymin = int((y_center - box_h / 2) * h) xmax = int((x_center + box_w / 2) * w) ymax = int((y_center + box_h / 2) * h) color = (0, 255, 0) # 绿色框 cv2.rectangle(img, (xmin, ymin), (xmax, ymax), color, 2) label = class_names[cls_id] cv2.putText(img, label, (xmin, max(0, ymin - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imwrite(str(output_path), img) print(f"已保存可视化结果: {output_path}") # 抽样验证 for idx in [0, 100, 200, 300, 400]: img_name = f"que({idx}).jpg" txt_name = f"que({idx}).txt" visualize_yolo_boxes( f"JPEGImages/{img_name}", f"labels/{txt_name}", ["defect"], f"vis/{img_name}" )

抽样逻辑是每隔100张挑一张,覆盖数据集中不同位置的图片,防止只验证到同一批相似样本。画框用的颜色在绿底图像上可能看不清,可以改成红色或蓝色——OpenCV默认BGR,cv2.rectangle传的(0, 0, 255)才是纯红,用(0, 255, 0)是纯绿,实际打印时看背景颜色调整即可。

4. YOLOv8训练实操:数据配置、类别映射与关键参数设定

4.1 数据集目录重排:按YOLO仓库要求的格式组织文件夹

YOLOv8官方仓库对数据目录结构有明确约定,不按这个结构来,训练脚本会直接报路径错误。标准的组织方式是images目录放图片,labels目录放TXT标签,然后在images下再分train和val两个子目录。业界常见做法是把原图按8:2或9:1切分到train与val,标签保持同名同步切分。对435张数据的小样本集,建议用8:2,即348张训练、87张验证——验证集太少则置信度评估波动大,太多则训练样本更紧张。

写一个切分脚本处理:

import random from pathlib import Path import shutil random.seed(42) img_dir = Path("JPEGImages") label_dir = Path("labels") train_img = Path("dataset/images/train") val_img = Path("dataset/images/val") train_label = Path("dataset/labels/train") val_label = Path("dataset/labels/val") for d in [train_img, val_img, train_label, val_label]: d.mkdir(parents=True, exist_ok=True) all_imgs = sorted(img_dir.glob("*.jpg")) random.shuffle(all_imgs) val_count = int(len(all_imgs) * 0.2) val_imgs = all_imgs[:val_count] train_imgs = all_imgs[val_count:] for img in train_imgs: shutil.copy(img, train_img / img.name) label_file = label_dir / (img.stem + ".txt") if label_file.exists(): shutil.copy(label_file, train_label / label_file.name) for img in val_imgs: shutil.copy(img, val_img / img.name) label_file = label_dir / (img.stem + ".txt") if label_file.exists(): shutil.copy(label_file, val_label / label_file.name) print(f"训练集: {len(train_imgs)} 张, 验证集: {len(val_imgs)} 张")

注意random.seed(42)这个细节。不设随机种子的话,每次运行切分结果都不一样,你今天跑实验A用这87张验证,明天跑实验B换了一批验证图,两次结果就没有可比性了。固定seed是实验可复现的第一步。另外shutil.copy比shutil.move安全,原始数据保留一份,切分出错了可以随时重新来,相当于给操作留了后悔药。

4.2 data.yaml配置:路径写绝对路径还是相对路径

YOLOv8的data.yaml是训练入口的配置文件,核心内容就三块:训练集路径、验证集路径、类别列表。这个文件写错最常见的报错是data file not found,或者训练时类别数对不上标签里的索引值。

# dataset/data.yaml path: /home/user/workspace/towel_defect # 数据集根目录的绝对路径 train: images/train val: images/val nc: 1 names: 0: defect

path字段建议写绝对路径,尤其是在服务器上跑任务时。相对路径的解析依赖当前工作目录,你在不同的目录下执行yolo命令可能解析到完全不同的地方,很难排查。train和val字段是相对path的路径,不要写绝对路径也不要加前导斜杠。nc是类别总数,names是类别名列表,顺序必须和TXT标签里的class_id索引严格一致。如果这份数据集里有多个缺陷类别,按一定优先级排序后放入names即可——注意names字典的value是从0开始递增的,不能跳号。

4.3 训练命令与参数选择:小数据集下的合理设置

435张图属于典型的小样本目标检测,训练参数不能照搬COCO那种大规模数据集的配置。模型选择上,优先用yolov8n或yolov8s这种轻量网络,参数规模小、过拟合风险低、训练速度快。如果直接上yolov8x,参数量大而数据量只有几百张,模型很容易把训练集特征背下来,验证集指标反而差。

cd /home/user/workspace/towel_defect yolo detect train \ data=dataset/data.yaml \ model=yolov8n.pt \ epochs=120 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ momentum=0.937 \ weight_decay=0.0005 \ patience=30 \ workers=4 \ device=0 \ project=run_towel \ name=exp_yolov8n \ seed=42

关键参数逐个说。epochs设120,小数据集收敛快,但配合patience=30做早停——连续30轮验证集指标没有提升就自动终止,不会空跑到120轮浪费时间。imgsz=640是速度和精度的平衡点,毛巾纹理细密,分辨率太小会丢失细小的缺陷特征;但435张图硬上1280分辨率,显存压力大且容易过拟合。batch=16在大部分消费级显卡上都能跑;如果你的显存只有8G,降到8更稳。device=0表示用第一块GPU,只有CPU的环境改成cpu。seed=42配合前面切分的seed,保证每组实验结果可复现。

4.4 训练日志里看什么:loss曲线坑位与验证指标解读

训练启动后不要傻等。需要实时盯着两条信息:训练日志里每个epoch的box_loss和cls_loss数值,以及验证阶段的precision和mAP。正常的训练过程是loss曲线平稳下降,前10个epoch下降明显,之后逐渐平缓;mAP50逐步上升到稳定区间。

常见的异常情况是loss掉得很快但mAP一直在低位徘徊,这说明模型是空转状态,可能原因包括标签转换出了问题、框坐标全是错的、或者类别ID不匹配。另一种是loss下不去、一直维持在高位,多半是数据质量问题——有些训练图片没有对应的标签文件,YOLO训练时忽略这些图片,但它们在迭代中占用了batch的配额,稀释了有效样本的梯度信号。

# 实时查看训练日志 tail -f run_towel/exp_yolov8n/train.log # 训练完成后查看指标汇总 cat run_towel/exp_yolov8n/results.csv

results.csv里记录了每一轮的精确率、召回率、mAP50、mAP50-95等所有指标,是最全的训练汇总文件。数值看不懂没关系,抓三个核心就好:mAP50在0.5以上算基本可用;recall明显低于precision说明漏检偏多,需要检查标注框是不是太严或缺陷太小;mAP50-95和mAP50差距过大说明框的定位精度不够,缺陷边缘可能标注得不准。

5. 避坑与常见问题:毛巾缺陷检测数据集使用中的五个典型踩坑记录

5.1 文件名带括号导致shell脚本批量处理时报错

现象是写好的循环命令批量处理图片,某些文件处理到一半报错找不到文件或路径不存在,单独跑那个文件又正常。

原因是文件名里的括号在shell中被解释为特殊字符。que(414).jpg中的括号会被bash当作子shell语法的一部分,不做引号处理直接拼接路径,就会变成que(414).jpg这样的命令替换结构,文件自然找不到。

解决方法是所有引用文件名的地方都加双引号,或者用Python的Path对象处理而不是字符串拼接。写bash脚本时循环变量必须写成"$file"而不是$file;写Python脚本时用pathlib.Path代替os.path.join。顺带说一句gas_mask这类不带括号的命名虽然丑但真的不会有这种坑,命名规范本身就是避免问题的手段。

5.2 XML里类别名与TXT里的class_id对不上

现象是训练时loss正常下降,但可视化验证时发现框的位置对,标签名错误——比如把某个实际缺陷标成了另一个类别。

原因是VOC格式的XML里存的是类别名字符串如defect,而YOLO的TXT里存的是数字ID。如果手工改过映射表,或者XML里的name值前后不一致,英文大小写不同也算不匹配,转换后就会错位。

解决方法是转换前先用脚本统计XML里全部name的去重集合,确认所有值都能映射到class_list,映射表写完后打印一遍对应关系,人工核对后再批量转换。别嫌这一步麻烦,我遇到过一次XML里同时存在defect和Defect两种写法,结果同一类缺陷拆成了两个类别,模型这两类都学不好,排查了好久才抓住原因。

5.3 小目标缺陷漏检严重,mAP指标虚高但实际效果差

现象是毛巾上的细小瑕疵如断纱、勾丝,在验证集的mAP上显示还行,但实际推理时漏检率很高,打印可视化框时发现很多小缺陷直接没框出来。

原因是这类数据集里大部分标注框面积占整张图片的比例很小。YOLO模型默认在三个尺度的特征图上做检测,小目标对应的是深层特征图,而深层特征图的分辨率低、语义信息强而空间信息弱,小缺陷很容易在这种特征图上被忽略。

解决方法是把imgsz从640提升到960,输入分辨率变大后小目标的像素占比增加,检测率会明显改善;也可以尝试用mosaic-9增强或复制粘贴增强,通过把小缺陷复制多份并随机粘贴到不同位置来提高采样频率。数据量小的话还可以考虑先用YOLOv8-seg模型训练分割,再从分割mask提取包围框,对小目标通常比纯检测效果好一些。

5.4 训练时提示标签类别数超过nc设定

现象是训练启动后报错,提示"class number out of range"或类似信息,训练进程直接退出。

原因是TXT标签里某个class_id大于等于data.yaml里nc的值。比如nc设了1,但某个TXT文件里有数字1,YOLO要求类别ID必须小于nc,index为1说明至少有两个类别。

解决方法是先统计所有TXT文件中的类别ID最大值,再核对XML里的类别数量是否一致。快速方法是用一行grep加sort命令提取所有TXT中的第一个字段看最大值;或者用脚本遍历所有标签文件收集类别集合。确认类别总数后把nc改成实际类别数量,同时检查names列表是否覆盖全部类别ID,常常是这边漏了。

5.5 数据增强导致标签失配,增强图与标签错位

现象是使用Albumentations或imgaug做数据增强后,部分增强图的框和缺陷实际位置对不上,有的框偏了,有的框尺寸错了。

原因是增强库的bbox位置处理有严格的格式要求。Albumentations默认要求bbox是归一化坐标,有的版本还要求格式为xyxy、xyyy需要显式指定。如果你的数据从YOLO格式直接喂给库但没声明bbox_format="yolo",库会把坐标当VOC格式解释,增强后坐标就全乱了。

解决方法是调用增强时必须显式声明bbox_params=Albumentations.BboxParams(format="yolo", label_fields=["class_labels"]),label_fields指明类别的字段名,库才能同步处理框和标签。增强后还建议自动跑一遍前文的可视化脚本,抽几张图人工核对框和缺陷位置是否仍然对齐。从那次之后我每次做增强都拿可视化图抽样看一遍再进训练。

6. 数据增强脚本与模型部署验证:把435张图的利用率拉满

小数据集最怕过拟合。435张原始图对深度学习模型来说确实不够,但通过数据增强可以把有效样本量放大几倍到十几倍。常见的增强手法分为像素级和空间级两类。像素级包括亮度对比度调整、高斯噪声、模糊、色彩抖动,这类操作不改变框的位置和尺寸;空间级包括水平翻转、垂直翻转、随机旋转、随机裁剪缩放,这类操作会改变框的坐标,需要增强库同步计算。

推荐直接用Albumentations库,它对目标检测任务提供了完整的bbox同步处理,不用自己写坐标变换逻辑。核心代码框架如下:

import albumentations as A import cv2 from pathlib import Path transform = A.Compose([ A.RandomBrightnessContrast(brightness_limit=0.3, contrast_limit=0.3, p=0.8), A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=20, val_shift_limit=20, p=0.5), A.GaussNoise(var_limit=(10.0, 50.0), p=0.5), A.HorizontalFlip(p=0.5), A.Rotate(limit=15, border_mode=cv2.BORDER_CONSTANT, p=0.5), A.RandomScale(scale_limit=0.2, p=0.5), A.RandomCrop(width=512, height=512, p=0.3), ], bbox_params=A.BboxParams(format="yolo", label_fields=["class_labels"]))

注意RandomCrop要小心用。裁剪后如果缺陷框超出裁剪区域,库会自动丢弃超出部分或把框截断。但如果设定的宽高比原图小太多,可能裁剪后只剩一两个缺陷甚至没有缺陷,这种样本对训练帮助有限。建议限制它的概率在0.3以下,起到增加位置多样性的作用但不至于打乱训练分布。Rotate的border_mode设为BORDER_CONSTANT,旋转产生的空白区域用黑色填充而不是镜像填充,避免引入不存在的纹理信息干扰模型。

增强脚本建议把每张原图扩充4到6倍,生成1800到2600张图,配合增强后的标签一起训练。每次增强时随机种子依然固定,保证同一份数据多次处理结果一致,实验可复现。

训练完成后的验证阶段我认为是最不能省的一步。不要只看mAP数字就收工,跑一段真实推理看看模型在实际光线下、不同角度下能否稳定检出缺陷。写推理脚本时留意NMS阈值的调度,毛巾纹理复杂时容易产生大量误检框,需要把conf阈值从默认的0.25调高到0.4左右,或者把IoU阈值从0.45调到0.5,用更严格的标准过滤重复框。真实部署时这类模型的推理速度通常不是瓶颈,检测框的稳定性才是现场判断好不好用的核心标准。

话说回来这套流程我用到现在,每次拿到新的小样本数据集都强制走一遍完整过程:核对标签、转格式、可视化验证、数据增强、训练、推理抽检。看起来步骤多,每一步都简单,最关键的就是链条上的每一个环节都不能跳过可视化验证这一步。从那次标签箭头搞错导致白训两天之后,我形成了习惯——每到一个阶段就随机抽图画框看一眼再继续下一步。你拿到这份毛巾缺陷检测数据集后,也建议从第2章的核对脚本开始走一遍全流程,把基础打扎实再谈调参和优化。希望帮到你。

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

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

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

立即咨询