产线布匹质检这个活儿,干过的人都知道有多磨人。人工目检岗往往坐不了两小时,注意力就开始往下掉,漏掉一个疵点,后面裁剪、染色、包装全得跟着买单。传统图像处理在这块折腾了很多年,但面对忽明忽暗的光照、低对比度的污渍,以及和布面纹理混在一起的异常,阈值分割和纹理滤波基本处于"能看不能打"的状态。这篇笔记记录的是我把 Cascade Mask R-CNN 和 RegNetX 主干组合起来,做织物疵点检测与分类的一次完整实验。核心目标很直接:用实例分割拿到每个疵点的像素级轮廓,同时给出类别,为后续自动裁切、疵点评级提供可量化的依据。如果你正在"普通检测框还是实例分割"之间犹豫,或者在工业小样本场景下调不明白两阶段网络,这篇笔记应该能帮你少走几天弯路。
1. 织物疵点检测为什么必须走"分割"路线,而不是普通检测框
1.1 疵点不是"方框"能表达清楚的目标
布面疵点的种类多得超出很多人想象:破洞、油污、水渍、飞花、结头、褶皱、色斑、纬斜、织造纹理异常,每一种的形态都不一样。破洞是边界陡峭的孔洞,油污是带着晕染边缘的片状,飞花是细长的丝絮,褶皱是条状的明暗变化。这些目标最大的特点就是形状极不规则,几乎没有一个是能被矩形框优雅包裹的。
普通检测框在这类目标上有三个绕不开的麻烦。第一,框里塞了大量正常布面,后续无论做面积统计还是裁切判断,都会被无关像素干扰;一个长条形的油污,框的面积可能是真实瑕疵面积的好几倍。第二,相邻疵点一旦靠得近,两个框重叠严重,NMS 之后往往只保留其中一个,漏检就这么产生了。第三,低对比度疵点的框位置本身就不稳定,回归网络来回抖动,训练时损失下不去。
工业场景真正需要的是"这个疵点覆盖了哪块布、形状是什么、大概多大面积"。这些信息只有像素级掩码能给。所以从一开始我就没打算只做检测框。
1.2 实例分割在产线落地中的实际价值
我最初只是想试试实例分割在布面疵点上能跑到什么程度,但真正跑了几个样本之后发现,它的价值远不止"多输出一个掩码"。
首先是裁切决策。疵点轮廓出来后,可以精确计算掩码外接矩形、最小外接旋转框、主轴方向、面积、周长这些形态描述子。比如破洞严重程度可以用掩码面积和孔洞边缘长度来判断,油污可以用掩码在主方向上的延伸长度结合布匹运行方向推测污染范围。这些指标直接喂给下游工控系统,比让人对着截图手动判定靠谱得多。
其次是分级自动化。工厂里最常用的疵点分级不是看"有没有缺陷",而是看"缺陷多严重"。同一个类别的疵点,面积小、形状规则的可能只算轻微,穿透性破损、长条油污可能就是 A 级报废。实例分割天然提供面积和形态信息,分级逻辑可以直接落在掩码计算上,不用再另起一套传统图像处理流程。
还有一点容易被忽略:带掩码的叠加可视化,对人工复核极其友好。产线上保留少量人工抽检岗位,他们看的是叠加了半透明掩码的布面图像,疵点位置、类别、置信度一眼可见,比看密密麻麻的检测框效率高得多。
1.3 为什么基线选 Cascade Mask R-CNN 而不是单阶段网络
关于选型,我先说结论:这个实验是离线质检场景,优先保证准确率,所以选了两阶段的 Cascade Mask R-CNN。如果换成实时在线检测,结论可能要反过来。
两阶段网络先通过 RPN 生成候选区域,再做精细回归和分割,对低对比度、小尺寸目标更友好。布面疵点里小目标占比很高,单阶段网络为了速度牺牲了 proposal 阶段,在这类目标上召回率通常明显偏低。Cascade 又是在 Mask R-CNN 基础上加了多级 IoU 阈值回归,每一级都在提纯候选框质量,对模糊边界的疵点尤其有效。
代价是速度和显存都更紧张。我们的产线场景不是每秒钟都要出结果,离线抽样检验完全可以接受单张图一百毫秒级别的推理。如果你做的是在线全检,我建议另选轻量级单阶段分割方案,后面我也会放一组对比数据供参考。
2. Cascade + RegNetX 的组合逻辑:选型背后的三个权衡
2.1 Cascade 多级回归到底在解决什么问题
很多初学者对 Cascade 的理解停留在"它比 Mask R-CNN 多训练了几个头",这个认识不太准确。
传统 Faster R-CNN / Mask R-CNN 在训练时把与真实框 IoU 大于 0.5 的候选都视为正样本,检测器只在一个 IoU 阈值下学习,输出的框质量参差不齐。Cascade 的思路是把回归器串联成多级:第一级在 0.5 阈值下训练,输出一批质量稍好的框;第二级用 0.6 阈值在上一级的输出上继续训练;第三级用 0.7 阈值再精修。每一级都在前一级的基础上变挑剔,最终输出的框质量就高很多。
布面疵点特别吃这一套,原因有两个。一是很多疵点边界模糊,初始 proposal 质量本身不高,单级检测器会把高分给了那些"差不多对上"的框,而级联结构会逐步把这种近似框纠正掉。二是疵点类别间形态差异极大,破洞和褶皱在特征空间里离得很远,多级回归相当于给不同类型的目标各留了一次校准机会。
Mask 分支在每一级都会跟着输出,最终取最后一级的结果。我在实验中对比过,去掉 Cascade 只保留 Mask R-CNN 结构,AP50 大约掉 2-3 个点,而计算量增加并不多,这笔账是划算的。
2.2 RegNetX 与 ResNet 的真实差距
选择主干网络时,我一开始也想过直接沿用 ResNet-50,但后来查了一下 RegNet 系列在检测任务上的迁移表现,果断换了过去。
RegNet 和 ResNet 最大的区别在于网络结构不是人工堆出来的,而是从一个参数化的设计空间里搜索出来的。它不再沿袭"每个 stage 宽度固定、依次翻倍"的套路,而是对每个 stage 的宽度、深度、分组卷积组数做整体量化。RegNetX 是不带 SE 模块的版本,省掉了注意力模块的额外开销,推理速度比 RegNetY 更快。
在同等算力下,RegNetX 的分类精度本身就比 ResNet 高一点,而且这个优势能迁移到检测和分割任务。我在这批布面样本上做了个同 schedule 对比,RegNetX-4GF 的 mask AP50 比 ResNet-50 高出约 2 个点,参数量反而少了一半,单图推理时间还略短。可以说在检测框架下,RegNetX 是一个性价比明显更高的主干选择。
| 主干 | 参数量(约) | Mask AP50 | 单图推理时间 |
|---|---|---|---|
| ResNet-50 | 44M | 0.83 | 112ms |
| RegNetX-4GF | 22M | 0.86 | 98ms |
提示:以上数值来自我们自己的样本和单张 GPU 实测,只能做相对比较,不同布种、不同分辨率下会有波动。
2.3 计算量预算与主干选择的经验判断
RegNetX 按算力分档,常见的有 1.6GF、4GF、8GF、12GF。数值代表的是每张图前向计算量,越大精度上限越高,但显存和速度代价也越大。
我最终选了 4GF 档位,理由是产线缺陷图分辨率高,输入尺寸至少 1333×800,4GF 在中等性能 GPU 上单卡能跑 batch=2,双卡凑 batch=4,足够训练。如果显存吃紧,退到 1.6GF 档位也能用,但小目标召回率会掉一截。8GF 以上在训练阶段收益不明显,验证阶段花了更多时间,不推荐在起步阶段用。
还有一条经验:疵点检测的收益大头在数据质量和标注准确性上,主干网络带来的提升其实是边际性的。别为了追高分盲目换大主干,先把标注规则理顺、把光照环境固定下来,提升会更明显。
3. 数据准备和标注:实验结果好坏的风向标
3.1 疵点数据集的构建:数量、类别与成像条件
很多项目死在第一步:数据集太小、类别分布太偏。布面疵点不是标准公开任务里的目标,网上能找到的开源数据规模有限,而且成像条件和你实际产线差很远。最靠谱的方式还是自己搭采集环境,积累自己的数据库。
成像条件要固定的关键点包括:光源种类和角度、相机分辨率和物距、每毫米对应多少像素。油污这类疵点对打光角度极敏感,低角度条形光可以放大油污和凹凸纹理的对比度,环形光则更适合均匀布面。我们的经验是光源固定比图像算法里的很多 trick 都重要,同样的模型换一套光源,AP 可能掉 5 个点以上。
样本数量方面,常见类别最好每类 300 个实例起步,稀疏类别至少也要凑到 150 个。破洞在正常生产中本来就少,这类样本我建议收集实物后人工制作模拟样本补齐,低对比度小污渍也可以通过裁剪拼接扩充。还有一个实操细节:要覆盖产线上 80% 以上的布种,纯色布和带纹路的布要分开记录,模型在纹理布上的泛化能力通常远差于纯色布。
3.2 标注规范与难点
标注这块我用的是多边形标注,也就是逐点勾出疵点轮廓。检测框在这里会带来不可恢复的误差,所以必须一步到位。
第一个难点是边缘怎么画。油污的晕染区到底算不算疵点?破洞周边的起球要不要包含?我和标注员对齐的规则是:以人眼可辨识为准,晕染区扩大到肉眼可见的最外边界;破洞严格按孔洞轮廓,不包含周边轻微起球;飞花类目标只标主体丝絮,不标散落的单根纤维。规则定清楚之后,两名标注员的一致性才能过 0.9。
第二个难点是极小疵点。高分辨率图像下,一个 5mm 的针孔可能只有 20×20 像素,人眼标注非常疲劳,而且容易漏标。我们统一做了一个规则:小于 12×12 像素的目标可以不标,但要在项目文档里明确写出来,否则模型会把这些极小目标当成噪声学掉,推理时疯狂误检。
第三个难点是类别边界。飞花和起球经常混在一起,色斑和正常染色误差也难区分。我的建议是类别宁可粗一点,标注一致性远比类别粒度重要。初始版本先把油污、水渍统一成"污渍类",把飞花、毛絮统一成"纤维类",跑通流程后再细分。
3.3 数据增强策略与类别不平衡处理
布面图像的特点是无方向性,旋转增强可以放心用。我用了随机水平/垂直翻转、±30° 随机旋转、亮度对比度扰动、轻微弹性变换模拟布面起伏。色相扰动要克制,布的颜色是真实质检信号,乱调会把模型带偏。
类别不平衡是这个任务的大坑。实测下来,稀有类别如果占比低于 5%,模型基本学不出来——破洞类样本少,但破洞又是生产中最不能漏的缺陷。我处理的方式有三层:一是按类别做 oversampling,让每个 epoch 里稀有类别出现的次数不低于常见类别的 60%;二是对破洞、飞花这类稀有目标做 copy-paste 增强,把它们的掩码粘贴到正常布图的随机位置,模拟多目标场景;三是在 loss 权重上给稀有类别加一点系数,但不能加太多,否则会牺牲常见类别的精度。
还有一个容易犯的错误:划分训练集和验证集时,不能按文件名随机切,要按布种、按批次切。同一卷布的相邻区域纹理高度相似,随机会导致验证集虚高,模型真正换产线布种时就崩了。
4. 训练配置、损失曲线与收敛行为观察
4.1 关键超参数配置与理由
训练配置我一开始用的是公开检测工具箱里的默认模板,跑通之后做了几轮调整。下面这套参数是我在这批数据上比较稳的组合,可以参考:
model = dict( type="CascadeMaskRCNN", backbone=dict(type="RegNetX4GF", pretrained=True), neck=dict(type="FPN", out_channels=256), rpn_head=dict( anchor_scales=[4, 8, 16, 32], anchor_ratios=[0.5, 1.0, 2.0], nms_thr=0.7, ), roi_head=dict( mask_size=28, nms_thr=0.5, max_per_image=100, ), ) train = dict( input_size=(1333, 800), batch_size=4, # 单卡2 * 2卡 base_lr=0.005, # 按batch=8基准折算 lr_schedule=[8, 11], # 12e基础版 warmup_iters=500, momentum=0.9, weight_decay=0.0001, )学习率要跟着 batch size 走。很多参考模板的 0.01 是按 8 张卡、batch=16 折算的,我实际只有 batch=4,直接套 0.01 会导致损失震荡,降到 0.005 才稳定下来。warmup 的 500 iter 也很关键,主干加载了通用图分类预训练权重,一开始学习率太大容易把主干好的特征打坏。
4.2 训练过程的损失曲线解读
我把训练日志里每一部分的 loss 都拉出来看了。RPN 的 loss 前 2000 轮下降最快,这是因为预训练主干已经能提取出布面的纹理特征,RPN 只需要适应新域的 anchor 分布。mask loss 下降明显更慢,属于正常现象——像素级掩码的学习难度比框回归高不少。
我遇到的第一个异常是 mask loss 在训练后期出现周期性抖动。排查后确认不是学习率问题,而是油污类标注样本里混入了几张边界画得极不负责任的图,标注噪声导致梯度方向不稳定。把那几张图重新标注后,抖动立刻消失。这让我对"标注质量决定上限"这句话有了切身体会。
第二个异常是验证 AP 在 epoch 8 之后开始下降,训练 loss 还在继续降,典型的过拟合信号。我们的样本量不算大,出现这种情况不意外。处理方式是加增强强度、提前截断训练,并且把 weight decay 从 5e-5 提到 1e-4。最终模型选择在 epoch 10 保存,而不是跑满 12 个 epoch。
4.3 评估指标选择:mAP、mask AP 与实际误检率
调优阶段我用标准的 AP50 和 mask AP 来横向比较不同配置,但到了评估阶段,我额外看了两个工业指标:每百米布告警次数,和缺陷级召回率。这两个指标直接决定产线能不能用。
标准评测里的 0.5:0.95 平均精度对工业场景偏严格,布面疵点边界本身就模糊,过分追求 IoU 高精度没有实际意义。我用 AP50 作为主指标,AP75 作为辅助参考。而真正上线前,我在验证集上按疵点中心点判定"检出/漏检",按类别分别统计召回率,同时统计误报次数。表格里那些"AP 很高"的模型,在误检率上可能完全不合格,二者必须分开看。
5. 实验结果与典型失败案例复盘
5.1 各类疵点的检测/分割表现
训练完成后,我在留出的验证集上按类别统计了结果,下面这张表比较有代表性:
| 类别 | AP50 | Mask AP | 召回率 |
|---|---|---|---|
| 破洞 | 0.92 | 0.88 | 0.95 |
| 污渍 | 0.86 | 0.80 | 0.90 |
| 油污 | 0.84 | 0.79 | 0.88 |
| 飞花 | 0.74 | 0.69 | 0.80 |
| 褶皱 | 0.56 | 0.44 | 0.62 |
| 色斑 | 0.68 | 0.61 | 0.72 |
高对比度、边界清楚的破洞类几乎是无悬念的,模型很快就能学好。污渍和油污虽然形状随机,但纹理特征和正常布面差异大,表现也不错。最差的是褶皱和色斑,这两类共同的难点是"和正常布面的差异太微妙",褶皱本质是光影变化而不是异物,色斑可能只比周边深了一点点。
5.2 误检和漏检的典型场景
我复盘了大约 600 张错图,把所有错误按场景归类,发现三类问题最突出。
第一类是误检织造纹理。带人字纹、水波纹的布种里,模型经常把纹理转折处当成疵点。这类误报在产线上非常致命,因为误报意味着正常的布会被裁掉,直接损失材料。处理思路是收集大量纹理布的负样本,单独加进训练集里让它学习"这是正常纹理",同时把这类布种的检测阈值适当调高。
第二类是低对比度漏检。深色布上的深色油污、浅色布上的细褶皱,几乎都难过模型这一关。人的视觉系统都困难,算法也吃力。我试过 CLAHE 对比度增强做预处理,有一定效果,但也会引入新的纹理伪影。后来还是靠补充带打光变化的样本,以及把这类目标的阈值放宽来缓解。
第三类是相邻疵点合并。两个污渍如果离得够近、掩码有重叠,后处理 NMS 会删掉置信度略低的那一个,最终只输出一个掩码。解决方式是降低 NMS 阈值到 0.4,同时把 max_per_image 从默认的 100 提到 200。密集疵点场景下,这个参数非常容易被忽略。
5.3 与单阶段模型的直观对比
为了验证选型方向,我还跑了一个轻量级单阶段分割模型做对照。同样用 RegNetX-4GF 做主干,输入尺寸一致,结果如下:
| 模型 | Mask AP50 | 单图推理时间 | 小目标召回 |
|---|---|---|---|
| Cascade Mask R-CNN + RegNetX | 0.86 | 98ms | 高 |
| 单阶段分割基线 | 0.78 | 45ms | 中 |
单阶段快了接近一倍,AP50 低了 8 个点。如果产线有实时全检需求,且疵点类别相对单一,单阶段完全够用。但我们的目标是离线抽检加后续自动裁切,对掩码边界质量要求高,这两阶段模型的优势就出来了。选型永远没有银弹,先想清楚你的场景是要速度还是精度,再决定架构,比盲目追新模型重要得多。
6. 推理部署与踩坑记录
6.1 从训练模型到批量推理的流程
训练好的模型要落到产线,不是简单加载权重跑一张图就完事。布面图像实际尺寸很大——相机幅面动辄 2000 像素宽,长度方向连续扫码,不可能整张塞进网络去跑。我用的是滑窗切图方案:把大图切成 1024×1024 或 1333×800 的 patch,相邻 patch 之间保留 128 像素的 overlap,检测完再把掩码映射回全局坐标。
overlap 这一步很关键,否则缺陷正好卡在 patch 边界上就会被切成两半,变成两个半截掩码,下游裁切根本没法判断。跨 patch 合并时,我先按全局坐标累加掩码,再用 IoU 判断重叠区域是否属于同一目标,属于同一目标的把掩码做一个并集。这里注意阈值不要设太高,0.5 左右比较稳,设太高会把同一个长条油污拆成两段。
6.2 推理速度与阈值调整
推理阶段我按类别分别扫了一遍置信度阈值,而不是所有类别共用一个 0.5。不同类别的误报代价完全不同:破洞漏检是重大质量事故,阈值定到 0.25 也要保证召回;油污误报则是裁掉正常布,损失材料成本,阈值定到 0.55 甚至 0.6 更划算。选择哪个阈值,本质上是在"漏检风险"和"材料浪费"之间做平衡,这个平衡只有结合产线成本才能定,标准指标帮不了你。
我做了一个 0.2 到 0.7 的阈值扫描实验,每个阈值下统计验证集上的召回率和误报数,画成曲线找拐点。实测下来,油污类别把阈值从 0.3 提到 0.5,误报减少了一半,召回只降了 3%,这个交易非常值。
6.3 踩过的坑:环境、显存和后处理
最后写几个我亲手踩过、希望能帮你绕开的坑。
第一个是显存不足。RegNetX-4GF 搭配 1333×800 输入,单卡 batch=2 时显存基本吃满。我试过把输入短边降到 640,显存是省了,但小目标漏检率肉眼可见地上涨。工业场景别轻易降分辨率,优先减 batch、清显存,实在不行换 1.6GF 主干。
第二个是训练正常但推理输出全空。那段时间我一度怀疑模型坏了,后来排查发现是推理脚本里的归一化参数和训练时不一致,像素值范围差了 255 倍。排查链路可以按三步走:先确认图像的通道顺序(RGB 还是 BGR)和归一化;再确认类别索引和标签是否对应;最后用训练时的同一张图在 eval 模式下跑一遍,比对输出和训练日志里的图。这三步能解决我遇到的九成推理异常。
第三个是从其他项目复制配置时忘了改类别数。我把一个 5 类的配置文件拿过来改成 6 类,只改了标注文件,没有同步配置里的分类头输出维度,导致推理时出现类别索引越界。这类低级错误隐蔽性很强,每次改配置都要做一次全量检查,特别是 num_classes、max_per_image 这类参数。
后处理里还有一个容易被忽略的参数:max_per_image。默认值通常是 100,遇到一卷布上密集出现几十个飞花结头时,前面的检测把名额占满,后面的缺陷就全部被截断。我把这个值提到 300 之后,密集场景的召回率改善很大,推理耗时的增加可以忽略。
这个实验做到最后,我的体会是:布面疵点检测真正难的往往不是模型结构,而是数据一致性、标注规则和工业场景里那些"看上去不相关"的细节。如果你正在复现类似的方案,别急着在模型上反复横跳,先把数据规范和成像环境钉死,再把阈值策略按类别调开,整体结果会好很多。