布匹瑕疵检测实战:从数据泄漏到模型调优的完整方案

发布时间:2026/9/11 23:20:56
布匹瑕疵检测实战:从数据泄漏到模型调优的完整方案 简介一套面向工业视觉与智能制造领域的布匹瑕疵检测方案源自广东工业智造大赛复赛适合竞赛选手、算法工程师及计算机相关专业学生用于缺陷检测算法学习与实战。压缩包共543个文件主体为311个Python源码和170个pyc编译文件并附带C/CUDA扩展算子涵盖DCN、ROIAlign、NMS等以提升推理效率另有md说明文档、sh部署脚本、Dockerfile及JSON配置便于快速还原运行环境。整体仅11.48MB目录围绕数据、模型、训练与推理模块组织已有168人学习。代码经完整测试且运行成功可直接复现赛题结果也可作为毕业设计或课程项目二次开发配套说明文档与原始数据支持远程答疑方便在真实布匹样本上开展检测实验与参数调优。1. 布匹瑕疵检测复赛题和你平时练手的目标检测不是一回事复赛数据拿到手最先让人栽跟头的往往不是模型而是数据本身。赛题给的布匹瑕疵样本里一类瑕疵可能只有几十个标注实例另一类却占了大半张图同一个疵点在不同光照、不同布色下拍出来特征差异比类别差异还大。这种长尾分布加上工业现场的噪声决定了你不能照搬COCO那套训练流程直接跑。标题里同时出现“python源码”“文档说明”“数据”三样东西意味着这题的重心不只是刷精度还需要你把数据组织、模型选型、训练策略和结果验收串成一条可复现的流水线——源码是骨架文档是暗号数据才是真正决定排名的东西。想在这种比赛里拿到稳的中上成绩关键不是找某个“神网络”而是搞清楚瑕疵检测的评估口径和数据泄漏的边界。这篇文章就按“数据怎么准备、模型怎么选、参数怎么调、坑怎么避”的顺序把这套方案完整落一遍。2. 布匹瑕疵数据怎么组织、怎么标注、怎么切分先把“数据泄漏”这个坑填平2.1 复赛给到的布匹瑕疵数据先按这个目录结构归好类不管从网上下到的源码包是zip解压还是git clone先不要急着读模型文件第一步永远是整理数据目录。布匹瑕疵检测的训练集通常包含两类东西原始布匹图像和对应的像素级标注图mask。有的比赛给的是单通道png像素值0表示背景、1、2、3…表示不同瑕疵类别有的给的是RGB伪彩色标注需要做颜色到类别ID的映射。常见做法是先落成一个标准结构data/ train/ images/ 20230101_lot42_piece17_r01.png 20230101_lot42_piece17_r02.png masks/ 20230101_lot42_piece17_r01.png 20230101_lot42_piece17_r02.png val/ images/ masks/ test/ images/把图像和mask文件名对齐是后续所有脚本能跑通的前提。很多开源的瑕疵检测代码里默认按_mask或_label后缀去匹配mask文件如果你的文件名对不上第一个报错就会出现在DataLoader里。2.1.1 用Python脚本做一次完整性校验我一般会先写一个五分钟的检查脚本把缺失、尺寸不一致、类别数异常的样本全部滤出来from PIL import Image import os, numpy as np img_dir data/train/images mask_dir data/train/masks bad [] for name in sorted(os.listdir(img_dir)): img_path os.path.join(img_dir, name) mask_path os.path.join(mask_dir, name.replace(.jpg, .png)) if not os.path.exists(mask_path): bad.append((name, mask missing)) continue im Image.open(img_path) ms Image.open(mask_path) if im.size ! ms.size: bad.append((name, fsize mismatch {im.size} vs {ms.size})) mask_arr np.array(ms) if len(np.unique(mask_arr)) 8: # 假设类别上限是7背景 bad.append((name, funexpected classes: {np.unique(mask_arr)})) print(fchecked {len(os.listdir(img_dir))} images, {len(bad)} problems) for item in bad[:20]: print(item)逻辑说明这段脚本把图像路径、mask路径、尺寸和类别数一次性对齐。name.replace(.jpg, .png)是常见的命名替换如果你的数据全是png就换成直接拼接或replace(_image, _label)。检查出问题不要直接删样本先看是不是切分脚本路径写错了——多数情况下是源码包里的路径变量没改而不是数据本身损坏。2.2 切分数据集的正确姿势按布匹ID分不要按图片随机分这是布匹瑕疵检测和数据竞赛里最容易被忽略、又最影响复现结果的一步。一张布匹在生产线上会被裁成很多段连续拍摄同一个布匹ID下的多张图像在纹理、底色、光照上是强相关的。如果按图片粒度随机切分同一匹布的图像会同时出现在训练集和验证集里验证分数会明显虚高等你把模型提交上去跑测试集成绩立刻缩水。这种问题在竞赛里叫数据泄漏在数据库语境里有时也叫“样本泄露”。布匹瑕疵检测里正确的做法是按布匹ID做group切分。from sklearn.model_selection import GroupShuffleSplit import glob image_paths sorted(glob.glob(data/train/images/*.png)) # 文件名形如 20230101_lot42_piece17_r01.png 第3段是布匹ID def get_piece_id(path): return path.split(/)[-1].split(_)[2] piece_ids [get_piece_id(p) for p in image_paths] gss GroupShuffleSplit(n_splits1, test_size0.15, random_state42) train_idx, val_idx next(gss.split(image_paths, groupspiece_ids)) print(ftrain images: {len(train_idx)}, val images: {len(val_idx)}) print(fval pieces: {len(set([piece_ids[i] for i in val_idx]))})参数说明GroupShuffleSplit里的groups参数传的是每个样本所属的组ID这里就是布匹ID。test_size0.15表示拿15%的布匹ID做验证而不是15%的图片。random_state42固定随机种子保证每次切分结果一致这对后面调参时对比实验非常关键。如果源码包的文档说明里写了“验证集按图随机切分”建议自己改成按布匹ID重切否则后期换模型时会被虚高分数误导。观察一下输出里验证集的布匹数验证集有不少于3个完整布匹会比较稳如果只有一个布匹说明切分运气不好建议调整random_state或增加test_size。2.3 布匹瑕疵数据增强哪些可靠哪些反而把纹理变成了伪瑕疵布匹瑕疵检测的数据增强和通用语义分割不太一样。工业图像一个显著特点是拍摄环境相对固定但不同布种之间的纹理差异极大同时瑕疵是局部小目标增强操作不能破坏瑕疵和背景的边界关系。增强操作建议原因水平翻转 / 垂直翻转常用布匹图像没有方向性翻转不改变瑕疵语义随机裁剪缩放常用模拟瑕疵在不同视野尺度下的成像增强尺度鲁棒性亮度/对比度扰动常用工业现场光源衰减、反光导致亮度波动小角度旋转±15°谨慎大角度旋转会破坏布匹经纬纹理方向干扰模型学习纹理背景弹性形变低频使用形变过度会把正常纹理扭曲成类似褶皱的伪瑕疵随机擦除不推荐布匹背景本来就有大量纹理擦除后模型容易把擦除区域学成背景空洞具体到代码如果源码里用的是albumentations我通常这样配置import albumentations as A train_transform A.Compose([ A.RandomCrop(512, 512), A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomBrightnessContrast(brightness_limit0.15, contrast_limit0.15, p0.5), A.Rotate(limit10, border_mode0, p0.3), ]) # mask和image必须走同一套transform def apply_transform(image, mask): aug train_transform(imageimage, maskmask) return aug[image], aug[mask]RandomCrop的尺寸要和模型输入匹配。Rotate设了border_mode0旋转产生的空白区填黑值不会引入额外的纹理噪声如果你不填这个参数默认填充方式可能复制边缘像素在瑕疵分割任务里会造成伪边缘。RandomBrightnessContrast的幅度不要超过0.2否则会把浅色瑕疵直接淹没在背景里。还有一个和增强配套的细节训练时mask和image必须用同一套随机参数做变换也就是上面Compose里定义的统一调用方式。如果分两次调用不同的transform图像和标注会完全错位训练过程不会报错但loss曲线会一直在高位抖动。这一章把所有数据问题解决掉之后才能进入模型选型阶段。很多时候复现成绩不理想不是模型代码的问题而是数据切分和增强设置从一开始就歪了。3. 布匹瑕疵检测模型选型语义分割还是目标检测榜单指标到底在比什么3.1 像素级分割与框级检测的取舍先看瑕疵形态再定技术路线布匹瑕疵的形态大致分两类一类边界清晰的异物比如破洞、油污、线头一类是区域性的纹理异常比如褶皱、色差、起球。边界清晰的用目标检测也能框得住但纹理类瑕疵没有明显的矩形边界框级标注会把大量正常区域圈进正样本里模型学到的是“这块区域整体不对”而不是“哪些像素不对”。复赛级别的布匹瑕疵检测源码里清一色走语义分割路线的原因就在这里——输出和标注都是像素级mask对不规则瑕疵的表达能力更强。目标检测适合上游定位、快速筛选可疑区域语义分割适合需要知道瑕疵面积和形状的质检场景。如果源码里同时包含检测头和分割头优先用分割结果作为主输出。从落地角度给一个选型表需求推荐方案场景只判断有没有瑕疵 定位到框YOLOv8 或 Faster R-CNN在线粗筛、低成本快速检测需要瑕疵面积、形状、精细边界U-Net 系 / DeepLabV3 / SegFormer复赛“像素级标注”的标准做法同时要检测和分割Mask R-CNN 或 Mask2Former分类细、要求召回高但算力充足复赛给了像素级mask标注那主模型就用分割模型没必要自己发明混合结构。常见的做法是U-Net加一个预训练encoder骨干或者直接上DeepLabV3。基于标题里的源码方向我下面的示例用segmentation_models_pytorch这个库搭建因为它封装了多种encoder换骨干只需要改一行参数。3.2 布匹瑕疵检测评估指标mIoU、F1-score 和像素准确率的坑复赛榜上有三个常见评估口径mIoU平均交并比、F1-score、像素准确率pixel accuracy。像素准确率是最容易骗人的指标背景像素占了绝大多数模型只要把所有像素预测成背景准确率也可能超过90%。所以评判分割模型的主指标优先看mIoU它是逐类别计算交并比再取平均对类别不均衡更敏感。mIoU的计算方式在代码里非常直白import numpy as np def compute_miou(pred_mask, true_mask, num_classes): ious [] for cls in range(num_classes): pred (pred_mask cls) true (true_mask cls) intersection np.logical_and(pred, true).sum() union np.logical_or(pred, true).sum() if union 0: continue # 当前类在真值和预测中都没出现跳过 ious.append(intersection / union) return np.mean(ious)参数说明num_classes要包含背景类通常是类别数1。union 0的情况表示这一类别在整个样本里既没有真值也没有预测在工业瑕疵检测里经常出现某个类别在单张图里完全不存在的情况直接跳过比返回0更合理否则会把mIoU拉低和榜单口径也对不上。如果你的模型有ignore_index之类的参数注意和这个跳过逻辑保持一致。F1-score在瑕疵检测里通常按像素计算也就是把分割结果当成像素级的二分类或者多分类输出来算precision和recall。实际提交时如果榜单用mask IoU排名训练时就以mIoU为主要监控指标如果榜单把mask读成某种评测协议那么以官方文档说明为准。源码里如果写了自定义的评估脚本优先用源码里的那个因为复赛的官方评价口径往往和公开数据集的默认指标有细微差别。3.3 布匹瑕疵检测训练参数一个能稳定跑到中上成绩的基线配置我不建议一上来就换大模型。先跑一个能在验证集上正常收敛的基线再逐步迭代。以一个输入512×512的U-Net为例子用segmentation_models_pytorch初始化import segmentation_models_pytorch as smp import torch model smp.Unet( encoder_nametimm-efficientnet-b4, # 换backbone只要改这里 encoder_weightsimagenet, in_channels3, classes8, # 和数据集真实类别数保持一致 activationsoftmax2d, ) criterion smp.losses.DiceLoss(modemulticlass) optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max60, eta_min1e-6)参数说明encoder_weightsimagenet使用ImageNet预训练权重虽然工业布匹纹理和自然图像差距不小但预训练权重提供的底层边缘、颜色特征仍然是有效的从头训练的收敛速度和最终精度都明显更差。AdamW配合weight_decay1e-4在分割任务里比较稳SGD需要更长训练轮数才能发挥出来。CosineAnnealingLR的T_max60表示60个epoch内把学习率从3e-4余弦降到1e-6这样的学习率曲线在瑕疵检测里比固定学习率更好收敛。classes参数要改成你数据里瑕疵类别数1背景否则最后的输出层分类数量对不上。参数建议值调整方向输入尺寸512×512显存够就上640小瑕疵效果提升明显batch size8~16根据显存调太小会引入噪声初始学习率3e-4 (AdamW)不收敛或Loss震荡就降到1e-4Dice系数权重0.7~1.0小目标占比大类保持高权重混合精度FP16开启可显著提速注意loss scale设置训练轮数60~100早停点在验证集mIoU不再上升时为什么Dice Loss在这里比单纯的交叉熵好用布匹瑕疵里的局部缺陷像素占比可能只有0.1%甚至更低交叉熵的梯度会被背景像素主导Dice Loss直接优化区域重叠程度天然对前景类有侧重。训练中途如果发现验证分数波动很大可以检查是不是batch size太小导致Dice Loss不稳定适当加大batch或降低学习率。4. 布匹瑕疵检测训练踩坑实录Loss不下降、类别不均衡、小瑕疵漏检4.1 布匹瑕疵模型Loss不收敛先查这四个位置训练布匹瑕疵模型时最常遇到的现象是Loss一直不降或停在某个高位震荡。先从数据端排查再动模型参数。第一个容易出问题的是类别映射。RGB标注图和单通道mask的类别ID可能不一致比如源码期待类别从0开始你的标注里背景是255那就等于多了一个类别Loss会被干扰。第二个是多分类时用了sigmoid激活而没改成softmax输出通道之间不互斥模型无法正确学习类别归属。第三个是学习率过大尤其是用预训练encoder时微调阶段初始学习率超过1e-3很容易出现Loss爆升。第四个是DataLoader里mask和image没有同步归一化mask被当成图像做标准化之后类别值变成了小数模型读出的是连续值而不是离散标签。查这四处基本能覆盖90%的“不收敛”。当Loss数值正常下降但验证集mIoU纹丝不动时问题多半在数据切分或评估脚本上。我之前遇到过一次val损失在下降、mIoU却接近0的情况最后发现是验证集里混入了几个没有mask的空样本标注全黑mIoU被强制拉低。遇到这种情况先把验证集里任何标注为空或全背景的样本打出来确认是否真的是“无瑕疵布匹”的负样本——这类负样本在复赛数据里存在但不能占比过高。4.2 布匹瑕疵样本不均衡用加权损失和OHEM控制易分类样本的梯度瑕疵类别之间的样本量差距往往在几十倍以上比如“破洞”有500个标注区域“起球”可能只有20个区域。直接用Dice Loss或交叉熵模型会把精力全放在出现频率高的类别上低频瑕疵几乎学不到。我一般用类别频率加权的交叉熵混合Dice Loss来缓解import torch.nn.functional as F class_weights torch.tensor([0.5, 1.2, 2.0, 3.5, 1.0, 4.0, 2.5, 3.0]).cuda() bce_loss F.cross_entropy(logits, mask, weightclass_weights, ignore_index255) dice_loss smp.losses.DiceLoss(modemulticlass)(logits, mask) total_loss 0.5 * bce_loss 0.5 * dice_loss参数说明class_weights按每个类别出现像素频率的倒数做归一化数字越大代表该类样本越稀缺。你不需要精确统计只要把低频瑕疵的权重调到高频类的2~4倍即可过高的权重会让模型对噪声敏感。ignore_index255把标注里的无效区域排除在loss计算之外避免边界标注不准的地方干扰训练。这个混合策略比单独用Dice Loss稳定因为交叉熵提供了像素级梯度Dice Loss提供了区域级约束。如果混合Loss仍然压不住梯度噪声可以考虑在训练后期加入OHEM在线困难样本挖掘只选取loss值排在前30%的像素回传梯度。不过在语义分割任务里OHEM对超参数更敏感建议先用加权CEDice跑通主线遇到低频瑕疵不收敛再加OHEM不要在一开始就把复杂度拉满。4.3 小瑕疵漏检从标注协同和重叠滑窗推理两个方向修布匹瑕疵里的小目标问题是另一个重灾区。某些瑕疵占整张图不到1%的像素被连续下采样之后直接消失。根本原因是模型输入的尺寸受限比如Encoder下采样32倍512×512的输入到了最深层特征图就只剩16×16了小块破洞的信息在这个尺度下已经丢失。第一步修复方法是把输入尺寸从512提升到640甚至768但显存有限时成本太高。更常见的做法是在推理阶段用重叠滑窗把小瑕疵放大到模型更敏感的尺度。def sliding_window_infer(model, image, window512, stride256): h, w image.shape[:2] pred np.zeros((h, w), dtypenp.int64) count np.zeros((h, w), dtypenp.float32) for y in range(0, h, stride): for x in range(0, w, stride): y2 min(y window, h) x2 min(x window, w) crop image[y:y2, x:x2] crop torch.tensor(crop).permute(2,0,1).unsqueeze(0).float().cuda() with torch.no_grad(): out model(crop)[out] if isinstance(model(crop), dict) else model(crop) pred_crop out.argmax(dim1)[0].cpu().numpy() pred[y:y2, x:x2] pred_crop count[y:y2, x:x2] 1 return pred参数说明window是模型输入尺寸stride控制相邻窗口的重叠程度。stride256表示每次滑动半个窗口重叠区域的预测结果会被后写入的窗口覆盖不会做加权平均如果想更平滑可以把pred改成累加每个类别的概率再取argmax。滑窗推理的代价是推理时间成倍上升通常只在验证集或测试集上对漏检严重的小瑕疵启用。在数据端还有一个容易忽略的办法检查标注框边缘是否贴合瑕疵像素。很多提供出来的源码数据里标注是半自动生成的低频瑕疵的标注质量往往不如高频类。对漏检的低频类别可以单独统计这类瑕疵的边缘像素中预测为背景的比例如果比例高说明标注边界不齐导致模型没学会精确轮廓。此时不要急着换模型先用形态学腐蚀膨胀修正mask边缘再重新训练往往比换backbone更有效。5. 复赛源码和文档说明的正确用法伪标签迭代和提交前验证5.1 用预训练模型给无标注数据生成伪标签补低频瑕疵样本复赛的“数据”目录里往往会带一部分无标注图像这些图像的价值在于扩充低频瑕疵样本。但要注意伪标签的生成方式先用训练好的模型推理出像素级mask再对mask做置信度过滤只有置信度高于0.9的区域才进入训练集。低置信度区域的噪声会让模型记住错误特征得不偿失。生成伪标签时不要让模型自己确认自己建议用两个不同结构的模型比如U-Net和DeepLabV3分别推理只保留两个模型预测一致的像素做标签。5.2 文档说明里藏着推理路径按官方要求对齐输出格式源码包里的文档说明除了介绍数据还会写明测试集推理结果的上传格式。常见的两种格式一是输出RGB伪彩色图每种颜色对应一个瑕疵类二是输出单通道灰度图像素值就是类别ID。如果文档里要求RGB格式而你直接提交了单通道灰度图评分脚本会把图像加载成三通道再读像素值导致所有类别识别失败。写一个输出格式校验函数在提交前跑一遍def check_submission_format(pred_path): img Image.open(pred_path) arr np.array(img) print(fshape: {arr.shape}, dtype: {arr.dtype}, unique: {np.unique(arr)}) if arr.ndim 3: print(RGB mode, verify colors match label map) elif arr.ndim 2: print(single channel, pixel values are class ids)参数说明arr.shape如果返回(H, W, 3)说明是RGB输出(H, W)说明是灰度输出。unique输出真实的类别值列表如果出现负值或大于最大类别数的数说明预测mask越界需要检查softmax解码逻辑。5.3 一套可以照抄的提交前验收流程临近提交按下面的顺序过一遍能避免大部分低级失误用训练好的模型跑完整个验证集按榜单同样口径算mIoU确认与训练日志的val分数偏差在2%以内。偏差过大的话检查验证集切分方式是否与代码一致。随机挑10张验证图把原图、真值mask、预测mask拼在一起人工看一眼重点看低频瑕疵类是否被预测出来。指标一致性和可视化结果要同时通过。检查测试集推理输出尺寸是否和原图一致。布匹图像长宽不是固定值很多模型会把输入resize成固定尺寸推理时要恢复回原始尺寸再保存。确认提交文件的命名和目录层级和文档说明完全一致zip包内不能有多余文件。整套流程跑完复现的分数才算真正属于你的。调参过程中每改动一个变量就重新记录一次验证分数对比实验会直接告诉你哪个策略在你的数据上有效、哪个只是拖慢训练。本文还有配套的精品资源点击获取