☰
arc_conv_r55环形缺陷检测:极坐标展开与角度循环卷积实践
2026/10/24 4:16:53 网站建设 项目流程

简介:arc_conv_r55 是一套面向压缩包逆向与格式转换场景的 Arc 格式处理工具集,适合需要解包、打包、转换及清理 arc 类压缩文件的开发者与数据管理人员使用。压缩包共收录 415 个文件,约 2.38MB,内容以 asm 汇编源码、c 源文件和 h 头文件为主体,并配有 bat 构建脚本、inc 包含文件以及 exe、lib、dll 等可执行与库文件,从源码到运行组件相对完整。已有 4534 人学习或下载,说明其在相关格式处理需求中具备一定参考价值。目录中可见 arc_conv、arc_pack、arc_mod、text_conv 等模块,分别对应转换、打包、格式扩展与文本处理,配合 build.bat、clean.bat 可完成构建与维护;bin、include 等目录则便于查看程序结构和二次开发。整体适合用来研究 Arc 格式的解析流程、命令行工具组织方式及多模块协作思路。

1. arc_conv_r55 是什么:为什么环形缺陷检测要换一种卷积核

某个环形件产线上,划痕大多沿着圆周方向拉开,细长、低对比度,位置一旦绕到侧面就常常漏检。普通 3×3 卷积在方形的感受野里怎么学都抓不住这种绕圈的纹理,模型只能靠位置记忆硬背下来,因此换一个角度、换一张图,效果就忽高忽低。arc_conv_r55 要解决的就是这件事:把卷积的感受野从方块换成一条中心半径为 55 像素的圆弧带,让网络天生就沿着角度方向观察。

它不是某个框架里现成的算子,而是一套组合做法:先把环带按极坐标展开成矩形,再让卷积在角度方向上首尾相接。r55 里的 55 就是环带的中心半径,单位是像素;这个数字通常由零件尺寸和相机分辨率反推出来,并非随便取定。它适合做环形零件表面检测、镜片边缘、圆盘外圈这类场景的人,也适合手上已经有展开图、却不知道角度方向该怎么卷积的人。

2. 从圆环到矩形:把 r55 环带展开成可卷积的特征图

2.1 半径 55 的环带要采多少点:角度和半径的采样密度怎么算

极坐标展开本质上是重采样,两个方向的采样密度必须分开计算。角度方向的物理分辨率由弧长决定:环带中心半径是 55 像素,绕一圈的弧长是 2π×55 ≈ 345.6 像素;如果把一圈分成 N_theta 格,每格的弧长就是 345.6 / N_theta 像素。想让每个采样格大约对应 0.5 个像素的弧长,N_theta 就取 690 左右;工程上更习惯取 720,因为它能被 8、9、12 整除,后面加 stride 做下采样时不会出现相位漂移。

径向方向的采样比较简单,径向宽度 w 除以 n_r 就是径向分辨率 dR。ar_conv_r55 常见配置是 w=10、n_r=10,也就是 dR=1 像素,和图像本身的分辨率对齐。径向方向没有必要做到 0.5 像素,因为环带只有 10 像素宽,再细分就是在插值噪声里找信息。

真正需要盯住的是内外圈的弧长差。内径 50 像素时,一圈弧长是 314;外径 60 像素时,一圈弧长是 377,两者相差 20%。这意味着展开图同一列上,靠内的像素和靠外的像素代表的物理长度不一样,横向存在约 20% 的拉伸梯度。经验边界是 w/r ≤ 0.2,超过这个比例就应该把环带拆成两条甚至三条,让每条环带内部的拉伸比控制在 5% 以内。很多项目效果不稳定,原因并不是模型不够强,而是从这一步开始,环带就分得不对。

提示:N_theta 最好同时能被 kernel_size 和 stride 整除,比如核宽为 9、stride 为 3 时,就取能同时被 9 和 3 整除的角度数。这能让下采样后的角度相位保持稳定,少一类莫名其妙的特征抖动。

2.2 手写 remap 做环带展开:一段能直接跑的代码

OpenCV 自带极坐标变换,但它的宽高约定和直觉相反,而且只能从圆心开始向外展开,想只取半径 50 到 60 这一段,还得自己裁掉多余的行。更稳妥的做法是自己生成 remap 查找表,半径区间、角度起点、采样密度都握在自己手里。

import cv2 import numpy as np def build_ring_map(cx, cy, r=55.0, w=10.0, n_theta=720, n_r=10): """生成 r55 环带展开的 remap 查找表。 返回 map_x / map_y,形状均为 (n_theta, n_r),dtype=float32。 行索引 -> 角度,列索引 -> 半径。 """ # 角度取每个采样格的中心,避免第 0 行正好压在 0 度接缝上 theta = (np.arange(n_theta, dtype=np.float32) + 0.5) * (2.0 * np.pi / n_theta) # 半径从内径 r-w/2 到外径 r+w/2,同样取格中心 radii = (r - w / 2.0) + (np.arange(n_r, dtype=np.float32) + 0.5) * (w / n_r) T, R = np.meshgrid(theta, radii, indexing='ij') # 形状 (n_theta, n_r) map_x = (cx + R * np.cos(T)).astype(np.float32) map_y = (cy + R * np.sin(T)).astype(np.float32) return map_x, map_y def unwrap_ring(img, cx, cy, r=55.0, w=10.0, n_theta=720, n_r=10): map_x, map_y = build_ring_map(cx, cy, r, w, n_theta, n_r) return cv2.remap(img, map_x, map_y, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101)

代码里meshgrid用了indexing='ij',这样张量的第 0 维就是角度、第 1 维是半径,和后面卷积层的维度顺序保持一致。角度从 0 开始逆时针增长,+0.5的作用是把采样点挪到格子中心,避免第一行正好落在 0 度这条实际上首尾相接的接缝上。半径采样点同样取格中心,保证内圈边界不会采到环带外面去。

cv2.remap的插值选了双线性,这是因为环带展开后的采样点和原图像素本来就不会严格对齐,用最近邻会让展开图里出现阶梯状的锯齿,划痕这类细长目标会直接被切碎。边界模式选了BORDER_REFLECT_101,目的是减少零件贴近图像边缘时的黑边;环带内部的点基本都落在图像里,这个参数影响有限,但不能不设。

参数常用取值说明
r55环带中心半径,由零件尺寸和相机标定反推
w10环带宽度,建议 w/r ≤ 0.2
n_theta720角度采样数,决定弧长分辨率
n_r10径向采样数,通常等于 w
interpolationINTER_LINEAR细长缺陷必须用双线性,不能用最近邻
borderModeREFLECT_101影响图像边缘区域的采样

2.3 展开图的坐标系对齐:行是角度还是半径

维度顺序这件事必须在动手写第一行模型代码前定死。约定成(B, C, n_theta, n_r),也就是高度方向是角度、宽度方向是半径。这样定有两个原因:角度方向是循环的,需要在 H 维做 circular padding;PyTorch 的F.pad对 H 和 W 的处理刚好能从参数顺序上看出来,写错一次就会出现一整条横向的假边缘。

顺带说一句,OpenCV 的warpPolar输出是(n_r, n_theta),也就是宽度对应半径、高度对应角度,和这里的约定正好把两个方向对调了。用自带函数做验证时看到一个转置的图,不要怀疑自己算错了,这是它的既定约定。我一般会在展开函数返回前加一句assert out.shape == (n_theta, n_r),把这类维度错误挡在数据管道里,避免它一路传到训练阶段再暴露出来。

3. 角度方向怎么卷积:r55 卷积层的实现与循环填充

3.1 角度首尾相接,zero padding 会凭空造出一条假边缘

把展开图直接喂给普通nn.Conv2d,默认的 zero padding 会在角度方向的两端各补一圈 0。问题是展开图的第 0 行和第 719 行在物理上是同一条线,补 0 等于在这条线上硬生生插进一道梯度断层。网络很快就能学会利用这个断层:凡是在 0 度附近出现响应,权重就往那个方向偏,最后表现成模型对某个固定角度的缺陷特别敏感、对其他角度相对迟钝。

这类问题在验证集上不一定看得出来,因为验证集的角度分布往往和训练集一致,属于典型的玄学掉点。判断方法也简单:把所有样本整体旋转 180 度再做一次推理,如果指标明显下滑,基本就是 padding 模式选错了,或者展开图本身的接缝没对齐。

正确的做法是只在角度方向做循环填充,在半径方向做复制填充。半径方向的两端是环带的内外边界,那里没有循环关系,用 0 填充会引入一圈暗边,用复制填充更接近真实情况。

3.2 写一层 ArcConv55:两次 pad 加一次卷积

F.pad的 circular 模式不能同时对 H 和 W 生效,所以得拆成两次调用:先补角度方向(H),再补半径方向(W)。顺序不能反,因为第二次 pad 的参数里要把第一次已经补过的维度写成 0。

import torch import torch.nn as nn import torch.nn.functional as F class ArcConv55(nn.Module): """角度方向循环、径向方向复制的二维卷积。 输入/输出形状: (B, C, n_theta, n_r),stride=1 时尺寸不变。 """ def __init__(self, in_ch, out_ch, k_theta=9, k_r=3, stride=1, groups=1): super().__init__() assert k_theta % 2 == 1 and k_r % 2 == 1, "只支持奇数核,保证尺寸不变" self.pt = k_theta // 2 self.pr = k_r // 2 self.conv = nn.Conv2d(in_ch, out_ch, (k_theta, k_r), stride=stride, padding=0, groups=groups, bias=False) self.bn = nn.BatchNorm2d(out_ch) def forward(self, x): # x: (B, C, H=n_theta, W=n_r) # F.pad 参数顺序是 (左, 右, 上, 下),分别对应 (W, W, H, H) if self.pt > 0: assert self.pt < x.shape[2], "circular pad 长度不能超过该维度" x = F.pad(x, (0, 0, self.pt, self.pt), mode='circular') # 角度方向 if self.pr > 0: x = F.pad(x, (self.pr, self.pr, 0, 0), mode='replicate') # 半径方向 return self.bn(self.conv(x))

这段代码里有几个容易忽略的地方。assert self.pt < x.shape[2]是必须存在的:PyTorch 的 circular padding 在 4D 输入下要求填充长度不能超过对应维度,一旦网络加深、角度方向被下采样到只剩几行,这里就会直接报错,提前拦住比跑到一半崩掉要好。bias=False配合BatchNorm是常规做法,卷积层里的偏置在 BN 面前没有意义,还会额外多出参数。

半径方向的 pad 用replicate而不是circular,是因为内外圈没有循环关系;如果用 circular,相当于把环带内边界和外边界连接起来,模型会学到一条完全不存在的螺旋结构。

参数常用取值说明
k_theta9 ~ 41角度方向核宽,直接决定弧长感受野
k_r3径向核宽,环带本身只有 10 行,不宜再大
stride1 或 3取 3 时要求 n_theta 能被 3 整除
groups1 或 in_ch控制参数量,大核时建议分组

3.3 核宽不是拍脑袋定的:k_theta 对应多长的物理弧长

这一节是最容易翻车的地方。很多人从普通 CNN 迁移过来,顺手写个k_theta=3,觉得 3×3 是标配。但前面的采样密度已经算过了:N_theta=720 时每格的弧长是 345.6/720 ≈ 0.48 像素,k_theta=3 只用到了 3×0.48 ≈ 1.44 像素的物理长度。一条宽度 2 像素、长度 80 像素的划痕,在角度方向上只能被切到一小段,跟没看一样。

正确的算法是先想清楚这个缺陷在物理上有多长,再反推核宽。想覆盖 20 像素的弧长,k_theta 就取 20/0.48 ≈ 42,取奇数就是 41。想覆盖 5 像素,取 11。这就解释了为什么 arc_conv_r55 的角度核宽经常是几十而不是几,它对应的是物理尺度,不是网络深度上的习惯。

大核带来的参数增长要提前算。in_ch=64, out_ch=64, k_theta=41, k_r=3时参数量是 41×3×64×64 ≈ 50 万,单层就顶掉一个小网络。控制手段有三条:一是分组卷积,groups=16能把参数压到 1/16;二是先做一次 1×1 降维再上大核;三是用两层 k_theta=9 的堆叠代替一层 k_theta=41,感受野差不多,参数少一个数量级,代价是中间少了非线性之外的信息。

注意:感受野覆盖的物理长度不要超过环带本身的周长。k_theta 大于 N_theta 时循环填补会绕回来,等效于整个环带都被卷进一个核里,这时候角度方向的局部性就没了。

4. 从合成数据到上线验证:怎么确认 r55 卷积真的学到了环向纹理

4.1 造一个可控的环形缺陷数据集:三类样本缺一不可

真实产线上收集带标注的环形缺陷很慢,先拿合成数据把管道跑通是更务实的路径。合成时至少要有三类样本,每类验证不同的东西:沿角度方向拉长的细划痕,用来验证卷积的角度感受野;横跨径向的缺口,用来验证径向那几行有没有被正确处理;各向同性的低对比度污渍,用来当对照组,如果模型对污渍也表现出强烈的角度偏好,说明它学到的是方向先验而不是缺陷本身。

import numpy as np def make_ring_sample(n_theta=720, n_r=10, seed=0): """合成一张环带展开图与掩码,返回 (img, mask),形状均为 (n_theta, n_r)。""" rng = np.random.default_rng(seed) img = np.full((n_theta, n_r), 0.5, dtype=np.float32) img += rng.normal(0, 0.02, img.shape).astype(np.float32) # 底噪 mask = np.zeros_like(img) kind = rng.integers(0, 3) a0 = rng.integers(0, n_theta - 60) # 角度起点 r0 = rng.integers(2, n_r - 2) # 径向起点 if kind == 0: # 角度方向细划痕 img[a0:a0 + 50, r0:r0 + 2] -= 0.25 mask[a0:a0 + 50, r0:r0 + 2] = 1 elif kind == 1: # 径向缺口 img[a0:a0 + 5, :] -= 0.30 mask[a0:a0 + 5, :] = 1 else: # 低对比度污渍,用作方向偏好的对照组 img[a0:a0 + 12, r0:r0 + 4] -= 0.08 mask[a0:a0 + 12, r0:r0 + 4] = 1 return np.clip(img, 0, 1), mask

生成时角度起点在[0, n_theta-60)上均匀取,保证缺陷不会总落在某个固定区域。这一点很重要:如果合成数据里所有缺陷都从 0 度附近开始,模型完全可能只学会看那个区域,然后在对真实数据做旋转增广时原形毕露。掩码和图像同步生成,省掉了后续再对齐一次的麻烦。

4.2 旋转一致性测试:输入转 15 度,特征应该跟着转

环形卷积最大的卖点是角度等变性,但这个性质很容易在实现细节里被破坏,所以要有专门的测试来卡它。做法是:把同一张展开图沿角度方向滚动 k 格,网络输出也应该对应滚动 k 格;把它滚回去之后和原始输出比较,差异应该接近 0。

import torch import torch.nn.functional as F def rotation_consistency(model, x, k): """x: (1, C, n_theta, n_r);k: 沿角度方向滚动的格数,返回相对误差。""" model.eval() with torch.no_grad(): y0 = model(x) x_rot = torch.roll(x, shifts=k, dims=2) # 第 2 维就是 n_theta y1 = model(x_rot) y1_back = torch.roll(y1, shifts=-k, dims=2) # 滚回去再比 err = F.mse_loss(y0, y1_back) scale = y0.var() + 1e-8 # 用方差做归一化 return (err / scale).item()

判据可以按相对误差分档:小于 1e-3 说明等变性基本成立,剩下的差异来自 BN 和插值的非严格平移不变;1e-3 到 1e-1 之间说明实现里有个小毛病,通常是某一处 padding 模式不一致;大于 1e-1 基本就是实打实写错了,优先查F.pad的参数顺序和torch.roll的维度。这个测试跑一次只要几十毫秒,我习惯把它挂在训练脚本的启动自检里,每次改完模型结构先跑一遍,比看 loss 曲线快得多。

4.3 和普通 3×3 的对照实验怎么摆

想说服自己这套改动确实有用,得把对照实验做干净。固定随机种子、固定数据划分、固定训练轮数,只把第一层卷积换掉:一边是普通 3×3,一边是 ArcConv55。比较指标不要只看总体召回率,环形检测更该看的是「同一缺陷放到 12 个不同角度位置时,检测分数的标准差」。标准差大,说明模型还是靠位置记忆在工作;标准差压下来,才是真的学到了角度无关的纹理特征。

另一个值得看的量是收敛速度。r55 卷积因为角度方向感受野直接对上了缺陷形态,通常在前几个 epoch 就能把召回拉起来;普通卷积往往要在训练中后期才开始拟合,而且一旦学习率调大就容易震荡。这两条曲线放在一张图上,比任何消融表格都直观。

5. 避坑与排查:r55 环带卷积最容易翻车的五个地方

5.1 展开图在 0 度位置有一条固定的亮带或暗带

现象:把展开图连成视频看,总有一条竖线固定不动;模型对这个位置的响应明显强于其他角度。

原因:角度方向的插值边界和卷积的 padding 模式不一致。cv2.remap默认用BORDER_CONSTANT,而卷积层用 circular;一个在接缝处补了 0,一个把两头接了起来,网络看到的就是两种不同的边界条件。

解决:展开和卷积统一约定。展开时把角度起点偏移半格(代码里的+0.5就是干这个的),卷积严格用 circular padding,两边对齐之后这条固定的带自然消失。如果还有残留,检查 n_theta 是否能被 kernel_size 整除。

5.2 内圈清晰、外圈模糊,同一个缺陷在环带内外表现差别很大

现象:靠近内径的缺陷召回明显高于靠近外径的,或者反过来。

原因:环带太宽,内外圈弧长差太大。w=10、r=55 时差 20%,展开图横向存在拉伸梯度,同一个卷积核在内外圈对应的是不同物理尺度。

解决:把 w/r 控制在 0.2 以内;超了就拆成两条环带,各自独立展开、独立卷积,最后在特征层拼接。拆分比硬扛有效得多,代价只是多一次 remap。

5.3 产线一震就掉分,圆心稍微偏一点指标就大幅波动

现象:离线验证很好,上线后指标忽高忽低,和生产节拍对得上。

原因:圆心标定依赖外接圆拟合,单帧估计的抖动在几个像素量级;半径 55 的环带,圆心偏 3 像素就意味着展开图整体错位,缺陷可能被切到环带外面。

解决:圆心做时序平滑,用滑动窗口的中位数而不是单帧结果;同时把环带稍微放宽 2 到 3 像素作为容差,代价是引入一点背景,但换来了稳定性。这一步属于血泪经验,容差留不留,直接决定上线后要不要天天调参。

5.4 角度方向的特征粘连在一起,细划痕被抹平

现象:可视化中间特征图,角度方向上相邻几格的值几乎一样,缺陷的边界看不出来。

原因:角度采样不足导致混叠。N_theta 取得太小,每格对应的弧长超过 1 像素,高频的细划痕在展开阶段就丢了信息,后面再怎么卷积也找不回来。

解决:按 s ≈ 0.5 像素反推 N_theta,宁可比需要的多一点;如果显存吃紧,先展开再降采样,不要一开始就低采样。这条没有后悔药,信息丢了就是丢了。

5.5 训练 loss 正常,推理掉点,排查半天找不到原因

现象:训练集和验证集指标都正常,换一批数据或者换一台设备就掉好几个点。

原因:训练和推理的预处理不一致。训练时用了BORDER_REFLECT_101做展开,推理时用了默认的BORDER_CONSTANT;或者训练时圆心是人工标注的精确值,推理时是算法估计的。

解决:把展开函数的参数写进配置文件,训练和推理共用同一份代码路径,不接受两边各写一份。这类问题属于黑匣子,靠肉眼排查极难,只能靠工程约束从源头上消掉。

6. 进阶:多尺度环带组与一次前向的推理加速

单条 r55 环带只能覆盖一个半径区间,但真实零件的缺陷可能出现在任意半径上。我一般会同时展开三条环带,比如 r=35、55、75,宽度都取 10,然后把它们沿 batch 维拼起来送进同一个 ArcConv55 共享权重。这样一次前向就能覆盖三个半径区间,参数量不变,显存只按环带数线性增长。三条环带的输出再按角度对齐做拼接或者加权融合,融合权重让网络自己学。

环带中心半径宽度覆盖区间典型用途
内环35831 ~ 39内孔边缘、倒角
中环551050 ~ 60主工作面
外环751070 ~ 80外缘、密封面

推理加速方面,如果部署环境对时延敏感,可以把大核卷积换成 unfold 加矩阵乘。做法是用F.unfold按角度方向滑窗取出所有 patch,此时补零已经在 unfold 之前用 circular pad 做完了,再和展平的核做一次matmul,最后 reshape 回来。实测在 k_theta=41 这种大核上,能比原生卷积快一截,尤其是在 batch 较小、GPU 利用率不高的时候。

还有一个容易被忽略的技巧:把展开本身也搬到 GPU 上。remap 查找表只和圆心、半径有关,只要圆心稳定,表就可以预先算好并缓存在显存里,每帧只做一次采样,省掉了 CPU 和 GPU 之间的来回拷贝。这一条在产线节拍紧的时候很关键,把整个 pipeline 压进 5 毫秒以内基本靠它。

我自己踩得最深的一次,是花了两天怀疑模型结构,最后发现是展开函数里把indexing从'ij'改成了'xy',角度和半径整反了,网络其实一直在沿半径方向卷积。从那以后我养成了一个习惯:任何涉及坐标变换的代码,先在合成图上打一个已知角度的标记,跑一遍看输出对不对,再往下写模型。看似多花十分钟,省下的是两天。

希望帮到你。

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

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

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

立即咨询