翻拍屏幕这件事,只要做过图像质量相关的项目,基本都会撞上摩尔纹。手机对着显示器拍一张,回来一看满屏彩色水波纹,客户说"你这拍的什么玩意儿",你还得解释这不是相机坏了。摩尔纹去除这个方向这两年因为深度学习的介入,效果比早年的频域陷波滤波强了不止一个档次,但真正动手复现的时候会发现论文里轻描淡写的一句话,背后可能是一个月的调试。这篇笔记围绕深度学习去除摩尔纹这条主线,把我读论文、复现结构、造数据、调损失、踩坑排查的完整链路摊开讲清楚,适合已经有点图像恢复基础、准备上手做这个任务的人,也适合只想搞明白"为什么我拍的屏幕照片会花"的同行。
1. 摩尔纹不是普通噪声,先把它的物理成因摸透
1.1 从采样混叠说起:屏幕栅格和传感器栅格的拍频
很多人第一反应是把摩尔纹当成一种噪声去处理,加个去噪网络就完事,结果训练出来的模型要么把图磨平了,要么纹路还在。根本原因在于摩尔纹不是随机噪声,它是确定性的采样混叠产物。显示器的像素是一个规则的二维点阵,红绿蓝子像素按固定间距排列;相机的传感器同样是一个规则点阵,还叠了一层 Bayer 彩色滤镜阵列。当这两个周期性结构在空间上以某个角度、某个缩放比例重叠时,就产生了拍频。
用公式描述更直观:设屏幕像素栅格的空间频率为 $f_s$,传感器采样频率为 $f_c$,那么出现的摩尔纹频率约为 $|f_s - k \cdot f_c|$,其中 $k$ 是整数。这个式子解释了几件很关键的事:第一,摩尔纹的频率不是固定的,它随拍摄距离、镜头焦距、拍摄角度实时变化;第二,因为 $k$ 可以取多个值,所以一张图里往往同时存在好几组不同频率、不同方向的条纹;第三,当 $f_s$ 接近 $f_c$ 的整数倍时,摩尔纹会被"折叠"到低频区域,表现为大面积的彩色色带,这就是为什么有些照片上的纹路特别粗。
理解了这一点,后面所有的方案选型都有了依据。摩尔纹在频域上是稀疏的、成对出现的峰值,在空域上是周期性的、方向性明显的条带,而且它和图像本身的纹理(织物、砖墙、发丝、密集文字)在频段上高度重叠。这就注定了任何"一刀切"的滤波思路都会误伤。
1.2 传统方法为什么在这件事上集体失灵
我刚开始接触这个任务的时候,想着先用传统方法打个 baseline,结果挨个试下来没一个能打的。把几种常见思路列出来对比一下,能省掉不少弯路:
| 方法 | 核心思路 | 实际表现 | 失效原因 |
|---|---|---|---|
| 高斯/双边滤波 | 空域平滑抑制高频 | 纹路淡了一点,细节全糊 | 摩尔纹频段与纹理频段重叠,无法区分 |
| 频域陷波滤波 | FFT 后手动压制峰值 | 固定场景有效,换张图就废 | 峰值位置随拍摄参数漂移,且陷波引入振铃 |
| 中值滤波 | 抑制脉冲型异常 | 几乎无效 | 摩尔纹是连续条带,不是孤立异常点 |
| 多帧平均 | 连拍多张取平均 | 有效但不可用 | 需要拍摄端配合,单图场景直接淘汰 |
| 导向滤波 + 抠图 | 分层处理 | 轻微摩尔纹尚可,重度无效 | 依赖引导图质量,重条纹下分层本身就错了 |
陷波滤波这套思路我自己动手实现过,流程是:FFT → 找幅度谱上的局部极大值 → 生成陷波掩膜 → IFFT。在小范围的固定场景里,比如固定距离拍固定显示器,确实能把条纹压下去。但只要拍摄距离变一点点,峰值位置就整体偏移,掩膜就失效了。更麻烦的是陷波会在空间域产生振铃效应,图像边缘出现一圈一圈的水波纹,看起来比原来的摩尔纹还难受。
这背后的逻辑其实很朴素:传统方法本质上是在假设摩尔纹的频率是已知且稳定的,而这个假设在真实场景里根本不成立。深度学习的价值就在于,它不需要你显式告诉它频率在哪,而是从大量样本里自己学出"什么样的纹理组合是摩尔纹、什么样的纹理组合是真实细节"。这是一个数据驱动的判别问题,而不是一个固定的信号处理问题——这个认知转变,是我读这类论文时最大的收获。
2. 把网络结构图拆开:每个模块到底在解决什么问题
2.1 多尺度分支:为什么非要把图缩到二分之一、四分之一再处理
几乎所有效果比较好的摩尔纹去除网络都会用多尺度结构,论文里通常一句话带过"我们采用多尺度输入以捕获不同频率的摩尔纹",但这句轻飘飘的话背后有很实在的理由。前面说过,摩尔纹的频率跨度极大,粗条纹可能周期上百像素,细纹路可能只有三五个像素。单个卷积核的感受野是固定的,靠堆层数去覆盖上百像素的周期,参数量和计算量会爆炸。
多尺度输入解决的就是这个矛盾。把图像下采样到 1/2、1/4 甚至 1/8 分辨率后,原本周期很长的大尺度色带在小图上变成了相对高频的信号,普通卷积就能覆盖;同时下采样本身就是一种低通滤波,高频的细纹路在这种尺度下被滤掉了,网络可以专注处理低频色偏。反过来,原分辨率分支专门负责恢复被摩尔纹污染的高频纹理细节。
我在复现的时候做过一个对照实验,把三尺度分支砍掉只留单尺度,参数量反而差不多(因为把省下的容量加宽了通道),但重度摩尔纹的色带几乎完全去不掉。这验证了多尺度不是"锦上添花的 trick",而是针对摩尔纹频率跨度大的必要设计。实测下来,两尺度能解决 70% 的场景,三尺度是比较好的性价比平衡点,再往上加尺度收益递减明显,而推理耗时是线性增长的。
2.2 粗到细的融合:上采样之后的特征对齐是个隐形坑
多尺度结构难点不在分支本身,而在融合。粗尺度分支输出的特征图尺寸小,要跟细尺度特征结合,必须做上采样。这里有两个细节论文往往不写,但直接决定成败。
第一个是上采样方式。我一开始用转置卷积(ConvTranspose2d),训练出来图像上有明显的棋盘格伪影,尤其是在纯色天空、白墙这类平坦区域特别刺眼。换成最近邻插值 + 3×3 卷积之后就干净了。原因不复杂:转置卷积的卷积核在步长大于 1 时,不同输出位置接收到的输入点数不均匀,这种不均匀在平坦区域暴露得最彻底。摩尔纹去除的输出本来就要求平坦区域绝对干净,所以棋盘格问题会被放大。
第二个是融合时的特征对齐。粗尺度特征上采样之后,它的空间位置和细尺度特征并不是严格对应的,因为下采样过程中经过了多次池化或步长卷积,存在半像素级别的偏移。如果直接相加,融合出来的特征会有轻微的双影,最终表现为图像边缘变软。我的处理是给上采样后的特征接一个 3×3 卷积做位置微调,再和细尺度特征做逐元素相加,这一个小改动让验证集上的指标涨了大概 0.15 dB,更重要的是边缘锐度主观上明显改善。
融合模块的写法大概是这样,用残差的形式比较稳:
import torch import torch.nn as nn import torch.nn.functional as F class CoarseToFineFuse(nn.Module): def __init__(self, ch): super().__init__() # 上采样后做一次 3x3 卷积校正位置偏移 self.align = nn.Conv2d(ch, ch, 3, 1, 1) # 融合后再做通道压缩,避免特征冗余 self.fuse = nn.Sequential( nn.Conv2d(ch * 2, ch, 1, 1, 0), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(ch, ch, 3, 1, 1), ) def forward(self, coarse, fine): up = F.interpolate(coarse, size=fine.shape[-2:], mode='nearest') up = self.align(up) out = self.fuse(torch.cat([up, fine], dim=1)) return out + fine # 残差连接,保证细尺度信息不被覆盖这段代码没什么花哨的,但align那一层和最后的残差相加,是我踩了坑之后才补上的。没有残差的话,训练初期粗尺度分支的输出还很随机,会把细尺度已经学到的细节冲掉,收敛速度肉眼可见地慢。
2.3 把"周期性"显式建模:频域分支与通道注意力
摩尔纹最本质的特征是周期性,而标准卷积对周期性的建模能力其实很弱。于是不少工作会引入频域分支,把特征图做二维 FFT,在频域上做卷积或掩膜,再 IFFT 回来。这个思路的理论依据是:周期性信号在频域上表现为离散的亮点,用一个小卷积核就能捕捉到,而在空域上需要很大的感受野。
我实测下来,频域分支确实对规则性强的摩尔纹有效,尤其是在合成数据上,指标提升很明显。但它在真实数据上的收益就没那么稳了,我猜测是因为真实拍摄的摩尔纹受镜头畸变、透视变换影响,周期性被破坏了,能量在频域上被打散成一个一个小簇而不是尖锐的点。所以我的做法是保留频域分支但把它放在较深的层,让前面的空域卷积先把几何畸变的影响吸收掉,同时把频域分支的输出作为一个门控信号去调制主干特征,而不是直接替换主干。
如果你不想引入复杂的 FFT 运算,一个便宜的替代方案是在通道维度上做注意力,让网络自己学"哪些通道对应周期性强的响应"。我对比过两种做法,在轻量级模型(参数量 1M 以下)上,通道注意力的性价比更高;参数量放宽到 5M 以上时,频域分支才开始显出优势。
3. 数据集才是这类任务真正的胜负手
3.1 合成摩尔纹:把"拍屏幕"这件事完整仿真一遍
这个任务最大的难点根本不是网络,是数据。真实场景下你很难拿到严格配对的图像:同一块屏幕上显示的内容,你要么拍下来得到带摩尔纹的图,要么直接截图得到干净的图,但这两个东西在颜色、亮度、噪声、几何上全都不一样,不是像素级对齐的。
所以主流做法是合成。合成的核心思想是把"显示器显示 → 相机拍摄"这条链路拆成几个可仿真的步骤,每一步都要建模到位,任何一步偷懒都会让合成数据和真实数据之间出现巨大的域差异。完整链路大致是:
| 步骤 | 要模拟的物理过程 | 常见实现 | 偷懒的后果 |
|---|---|---|---|
| 1. 屏幕显示模拟 | 内容在屏幕上按像素栅格重采样 | 下采样到屏幕分辨率,模拟子像素排列 | 缺少第一级采样,摩尔纹根本不会出现 |
| 2. 光学投影 | 镜头透视、畸变、景深模糊 | 单应矩阵 + 轻微高斯模糊 | 摩尔纹方向过于单一,泛化差 |
| 3. 传感器采样 | 第二次采样,产生混叠 | 再下采样到传感器分辨率 | 混叠强度不对,条纹要么过重要么过轻 |
| 4. 色彩滤波 | Bayer 阵列采样 | 按 CFA 排列分离通道 | 缺少彩色摩尔纹,只有灰度条纹 |
| 5. 后处理链 | 噪声、压缩、色调映射 | 加高斯/泊松噪声 + JPEG 压缩 | 模型在真实图上一遇到压缩块就崩 |
第一步和第三步是灵魂所在。摩尔纹的成因是两次独立的采样,如果你只做一次下采样再放大,那是插值伪影,不是摩尔纹。我在第一版合成脚本里就犯了这个错,生成出来的"摩尔纹"看起来像规则网格,训练出来的模型在真实数据上完全失效,排查了快两周才发现是采样链路少了一级。
关键的一步是屏幕分辨率要显著高于传感器分辨率,这样第二次采样时高频信息才会折叠回来。伪代码大概是这样:
import numpy as np def simulate_screen_capture(clean_img, screen_scale=0.35, sensor_scale=0.6, tilt_deg=3.0): # 1. 内容 -> 屏幕像素栅格(第一次重采样,制造高频) h, w = clean_img.shape[:2] screen = resize(clean_img, (int(w * screen_scale), int(h * screen_scale)), mode='area') # 模拟子像素排列:轻微的水平方向的通道位移 screen = shift_channels(screen, offsets=[0.0, 0.33, 0.66]) # 2. 光学投影:轻微旋转 + 透视,制造方向性 M = build_homography(tilt_deg=tilt_deg, scale=sensor_scale) projected = warp_perspective(screen, M) # 3. 传感器采样(第二次重采样,混叠就在这里发生) # 注意:这里必须用 area/bilinear 这类会混叠的插值, # 不能用 antialias 模式,否则摩尔纹被抹掉了 captured = resize(projected, (w, h), mode='bilinear') # 4. 后处理:噪声 + 压缩 captured = add_sensor_noise(captured, sigma=2.0) captured = jpeg_compress(captured, quality=85) return captured注意第 3 步的注释,这是我调试时最想强调的一点。很多图像库的 resize 默认带抗锯齿,会主动抹掉高频,你仿真出来的图就特别"干净",摩尔纹很淡。必须显式关掉抗锯齿,或者用 bilinear 这种不抑制高频的模式,才能得到接近真实拍摄的混叠强度。
3.2 真实数据的配对难题:半监督和非配对的现实取舍
纯合成数据训出来的模型,放到真实拍摄的图上,通常会遇到几个典型问题:颜色偏冷或者偏暖、纹路去得不干净、画面整体发灰。这就是合成域和真实域之间的 gap。
想拿真实数据做微调,就得解决配对问题。实际操作里能走通的路大概三条:
一个是固定机位截图对齐。把手机架在三脚架上拍显示器,拍完之后从系统里直接截取同一屏内容,用单应矩阵把截图变换到拍摄视角。这条路能拿到几何上基本对齐的配对,但颜色和噪点对不上,而且变换本身会引入插值模糊。我试过,用来做微调可以,用来做严格评测不够。
第二个是非配对训练。引入对抗损失,让判别器去区分"网络输出的去摩尔纹图"和"真实的无摩尔纹图"的分布。这条路能显著改善颜色和整体的自然度,但训练极不稳定,而且有把内容改掉的风险——判别器只关心"像不像干净图",不关心"是不是同一张图"。
第三个是半监督 + 伪标签。先用合成数据训一个初始模型,拿它去跑真实数据生成伪标签,人工挑出质量好的那部分加入训练集,迭代几轮。这条路最土但最有效,我的经验是每轮挑 5%~10% 的高置信样本,两三迭代之后模型在真实数据上的主观效果就有明显提升。缺点是费人力,而且伪标签里的错误会被模型学进去,所以宁可少挑也不要凑数。
3.3 合成到真实的域差异,我踩过的具体坑
分享几个具体的、只在实际调试中才会暴露的问题:
- 合成图过于锐利。仿真用的干净图往往是高质量照片,本身锐度很高,而真实翻拍图受镜头和噪声影响偏软。这导致模型学会"输出特别锐的图",到真实数据上反而出现过度锐化的边缘光晕。解决办法是在合成链路里加一步轻微的高斯模糊,把锐度拉低到和真实数据接近的水平。
- 噪声模型不对。我一开始只加高斯噪声,结果模型在真实图上遇到 ISO 拉高后的彩噪就完全失效。后来改成"亮度通道加高斯 + 色度通道加低频噪声"的组合,泛化明显改善。
- 屏幕子像素排列没考虑。早期版本我在通道上做了简单的水平位移,但真实屏幕的子像素排列有各种变体,条纹的彩色分布很不一样。后来在合成时随机化子像素的位移方向和幅度,模型对彩色条带的适应性好了很多。
- 摩尔纹强度分布不均。真实拍摄里,屏幕中央和边缘的摩尔纹强度差异很大,因为透视和景深不同。合成时如果只用一个固定的变换矩阵,摩尔纹会均匀铺满整张图,模型学不到"局部强弱变化"这种模式。加上随机的局部强度调制之后,效果才有质的提升。
4. 损失函数与训练细节:论文一笔带过,实操全是坑
4.1 L1、感知损失、对抗损失怎么配比
损失函数这块,论文里通常就是一行公式加一句"我们使用了 L1 损失和感知损失的组合",但权重怎么定、为什么这么定,几乎没人写。我自己试出来的经验是这样:
| 损失项 | 作用 | 建议权重 | 调高后的副作用 | 调低后的副作用 |
|---|---|---|---|---|
| L1 | 保真,约束像素级误差 | 1.0(基准) | 输出变糊、纹理丢失 | 颜色偏移、结构不跟随输入 |
| 感知损失(深层特征) | 提升纹理自然度 | 0.02 ~ 0.05 | 出现"脑补"纹理,与原图不符 | 细节偏平,缺乏质感 |
| 对抗损失 | 提升锐度和真实感 | 0.005 ~ 0.01 | 训练震荡,局部出现虚假纹理 | 结果偏保守,偏软 |
| 频域幅度损失 | 压制残留周期性条纹 | 0.1 ~ 0.2 | 图像出现振铃 | 大尺度色带残留 |
| SSIM 损失 | 结构一致性 | 0.1 ~ 0.3 | 对比度被压缩 | 结构细节失真 |
为什么 L1 优于 L2,这点值得展开说。L2 对大误差惩罚重,模型为了降低极端误差,倾向于在所有可能值之间取平均,结果就是输出变模糊——这在去摩尔纹任务里是致命的,因为摩尔纹区域本身就是"该保留什么"存在不确定性的区域,L2 会直接给你一个中间值,糊成一片。L1 的梯度是常数,对不确定区域的惩罚更温和,模型更有动力去赌一个具体的纹理,视觉效果明显更好。
感知损失我建议只在中后段特征图上算,不要用太浅的层。浅层特征图主要反映颜色和边缘,用它算感知损失会让颜色漂移;深层特征图反映语义和纹理,更贴合"看起来自然"这个目标。至于对抗损失,我的态度是能不用就不用,除非你的应用场景对锐度有硬性要求,否则为了那一点点锐度去换训练的不稳定性,不太划算。
4.2 patch 采样、批组成与学习率策略
输入尺寸方面,256×256 的 patch 是性价比比较高的选择。太小的 patch(比如 128)会让大尺度的粗条纹无法在单个样本里完整呈现,网络学不到低频色带的全局结构;太大(512 以上)显存吃紧,而且多尺度分支的高分辨率支路会撑爆。
批组成有个容易被忽略的细节:不要完全随机采样。摩尔纹的严重程度在图像里分布极不均匀,纯随机采样会导致一个 batch 里全是容易样本,另一个 batch 全是困难样本,梯度波动很大。我的做法是按严重程度分层,每个 batch 里困难、中等、轻微样本大致按 3:4:3 的比例混合,训练曲线会平滑很多。
学习率用 Adam,初值 1e-4,配合 cosine 衰减到 1e-6。这里有个坑:多尺度结构里不同分支的收敛速度不一样,粗尺度分支通常收敛快,细尺度分支收敛慢。如果统一用一个大学习率,粗尺度分支早就过拟合了,细尺度还在爬坡。我的处理是给不同尺度的分支设置不同的学习率倍数,粗尺度用 0.5 倍,细尺度用 1.0 倍,验证集上的收敛一致性会好很多。
另一个细节是归一化层的选择。小 batch(4 或 8)下 BatchNorm 的统计量很不稳,我换成 InstanceNorm 之后训练稳定性和最终指标都有改善。如果你的显存充裕、batch 能开到 16 以上,BatchNorm 也可以用,但这个任务对显存的胃口不小,多数人还是在 8 以下,那就老老实实用 InstanceNorm 或 GroupNorm。
4.3 不收敛、过拟合、输出异常:完整排查链路
训练过程中会遇到各种问题,我整理了一份按"现象 → 可能原因 → 排查手段"的排查表,都是实际撞过的:
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| loss 正常下降但验证图整体发灰 | 数据配对没对齐,合成时插值核不一致 | 取一组配对图直接做差,看残差是否有结构 |
| 输出出现棋盘格 | 转置卷积上采样 | 换成 nearest + conv,立刻验证 |
| 平坦区域有残留网格 | 合成数据的子像素模拟不真实 | 检查合成链路的通道位移设置 |
| 纹理区域出现"水彩画"感 | L2 损失或感知损失权重过高 | 降感知损失权重,改 L1 |
| 颜色整体偏冷/偏暖 | 真实域与合成域颜色分布差异 | 统计两域的颜色直方图做对比 |
| 训练前期 loss 剧烈震荡 | batch 组成不均、学习率过大 | 分层采样 + 学习率 warmup |
| 某些方向的条纹永远去不掉 | 数据增强破坏了方向统计 | 检查是否用了 90 度旋转增强 |
最后一条值得单独说。我一开始为了扩增数据,用了水平翻转 + 90 度旋转 + 180 度旋转的全套增强。结果模型在某些角度拍摄的图上表现特别差,条纹残留明显。排查了很久才想明白:合成数据里摩尔纹的方向是由仿真时的透视矩阵决定的,如果我做 90 度旋转,就等于人为制造了一种"真实数据里几乎不会出现"的方向分布,模型的学到的先验被污染了。后来改成只保留水平翻转和小角度随机旋转(±10 度),问题消失。这类坑,光看论文永远看不到。
5. 指标好看不等于图好看:评估与后处理的实际经验
5.1 PSNR、SSIM 与主观感受之间的错位
这个任务上 PSNR 和 SSIM 的参考价值有限,必须清醒地认识到这一点。原因很简单:这两个指标都偏爱平滑输出。一个把所有纹理都抹平的模型,只要能保证大体的亮度和颜色接近,PSNR 往往不低;而一个把细节恢复得很好、但和 ground truth 的纹理有细微差别的模型,PSNR 反而可能更低。
我在一次对比实验里遇到过很典型的情况:模型 A 的 PSNR 比模型 B 高 0.8 dB,但主观上看 A 的输出整体发糊,织物纹理完全没了,B 的纹理保留得很完整。最后工程上选的是 B,因为用户看的是图,不是数字。
所以我建议评估体系里至少要加两项:一是 LPIPS 这类基于深度特征的感知指标,它和主观感受的相关性明显更好;二是在摩尔纹区域单独算指标,而不是全图平均。全图平均会把大片干净区域的分数稀释进来,掩盖掉模型在真正困难区域的表现。具体做法是用一个简单的检测器(比如基于局部频谱能量的阈值判断)生成摩尔纹区域的 mask,只在 mask 内统计。这个改动虽然麻烦,但能让你看清模型到底在哪儿强、在哪儿弱。
5.2 纹理残留与颜色偏移的收尾处理
即便网络训得不错,实际输出还是会有一些小毛病,需要后处理兜底。
最常见的是残留的低频色带。深度学习模型对高频条纹的压制很到位,但对周期上百像素的大尺度色带往往力不从心,因为它超出了感受野。我的处理是在网络输出之后接一个轻量的后处理:对输出做一次小波分解,在低频子带上做自适应的色度平滑,只动色度通道、不动亮度通道。这样既能把色带压掉,又不会伤到亮度细节。这一步实测能明显改善大面积屏幕拍摄的观感。
第二个是局部颜色漂移。尤其是在屏幕边缘、摩尔纹特别重的区域,去完纹之后颜色会偏。原因是这些区域的原始颜色信息被摩尔纹严重污染,模型只能靠上下文"猜"。缓解办法是在损失里加一项色度一致性约束:把输出和输入都转到 YCbCr 空间,在 Cb、Cr 通道上算一个加权 L1,权重按摩尔纹强度自适应。强度越大的区域权重越低,因为那里本来就不可信;强度小的区域权重高,保证干净区域的颜色绝对准确。
第三个是过度平滑。有些场景下模型会把细密的真实纹理(比如远处建筑的窗格)当成摩尔纹一起干掉。这个问题的根治方法是在训练数据里加入"看起来像摩尔纹但不是摩尔纹"的困难负样本,专门告诉模型这两者的区别。如果不想重新训,一个临时的补救是给输出叠一层从输入提取的高频细节(用引导滤波做保边分解),控制好混合系数,能把纹理找回来一部分。
6. 从论文到可用:推理优化和接入业务时的现实问题
6.1 多尺度结构的推理耗时与量化掉点
多尺度结构在训练时看不出代价,部署时才知道疼。三尺度意味着同一张图要做三次不同分辨率的编码,虽然小分辨率的计算量小,但整体 FLOPs 大概是无多尺度版本的 1.6 到 2 倍。在服务端跑无所谓,端上实时预览就吃不消了。
我的优化顺序是这样的:先做通道剪枝,把每个分支里贡献度低的通道去掉,通常能砍掉 30% 的参数量而指标只掉 0.1 dB 左右;再做结构上的取舍,把 1/8 分辨率的分支直接砍掉(这一支主要负责极低频色带,可以由后处理的小波平滑来替代),速度能提升将近 40%;最后才考虑量化。
量化这里有个坑:如果你的模型里有频域分支(FFT → 卷积 → IFFT),INT8 量化在这部分的掉点会非常明显。原因是 FFT 输出的动态范围很大,量化到 8 bit 之后相位信息损失严重。我的处理是保留频域分支为 FP16,其余部分做 INT8,混合精度推理,速度损失不大但精度保住了。如果对精度要求没那么苛刻,也可以直接砍掉频域分支换取全 INT8 的部署便利。
6.2 大分辨率图像的分块推理与接缝处理
实际业务里的图往往远大于训练时的 256×256,直接整图推理显存不够,必须分块。分块就有接缝问题,尤其是摩尔纹这种全局周期性结构,块与块之间处理强度不一致时,接缝会特别明显——因为摩尔纹本身就是条纹状的,接缝会和条纹叠加,形成很扎眼的断裂。
标准做法是 overlap-tile:块之间重叠 32 到 64 像素,重叠区域用余弦窗做加权融合。但我在实测中发现光这样还不够,因为每个块独立推理时,网络看到的上下文不同,输出的整体色调会有细微差异,融合之后表现为轻微的块状明暗不均。加一步后处理能解决:对融合后的整图做一次大半径的低频分量对齐,以输入图(带摩尔纹的原始图)的低频分量为参考,把输出的低频拉回去。这一步几乎不影响细节,但能把块与块之间的色调差异抹平。
还有一个细节是分块尺寸的选择。块太小,网络缺少全局上下文,粗条纹去不干净;块太大,显存吃紧。我的经验是推理块至少要到 384×384,重叠 48 像素,这个配置在多数场景下比较平衡。如果硬件允许,直接上 512 会更稳。
最后分享一个我在实际项目里踩得很深的坑:评测时用的图和部署时用的图,预处理链路必须完全一致。我曾经遇到过模型在离线评测里表现很好,上线之后效果打折的情况,查了两天才发现是业务侧的图像在进模型之前多做了一次不必要的缩放,把摩尔纹的频率分布改变了,模型的先验对不上了。这类问题没有任何技术含量,但杀伤力极大,接手新场景时第一件事就是把端到端的预处理链路和训练时逐行对齐,别嫌麻烦。