ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

小波变换结合梯度下降去除脉冲噪声:从优化视角突破中值滤波瓶颈

小波变换结合梯度下降去除脉冲噪声:从优化视角突破中值滤波瓶颈 处理脉冲噪声也就是大家常说的椒盐噪声我以前的第一反应永远是中值滤波。直到有一次拿到一张老照片扫描件脉冲密度高到用 5x5 窗口都救不回来边缘细节却糊成一团。后来我换了个思路不再把去噪当滤波问题而是把它当成一个优化问题用梯度下降去迭代求解一个带小波稀疏约束的目标函数。这篇文章就完整记录我这次把小波变换和梯度下降结合处理脉冲噪声的思路、推导和 Python 实现适合对图像处理有一定基础、但想跳出 OpenCV 滤镜套路的读者参考。先说结论这个方法在脉冲密度偏高时PSNR 和主观视觉都明显优于中值滤波和传统小波软阈值。代码不长核心迭代也就二十行左右但里面的数学和工程细节比功能实现更有意思。下面我从头梳理整个项目。1. 脉冲噪声为什么难缠先搞清楚敌人长什么样1.1 脉冲噪声的生成机制与分布特征脉冲噪声的经典模型很简单每个像素以概率 p 被破坏被破坏的像素以相同概率变成最小值或最大值。对灰度图来说就是随机撒上黑点和白点。这种噪声在信号处理里通常被建模为一个稀疏的离群项 e干净图像 x 加上它得到观测图像 yy x e其中向量 e 只有大约 p 比例的分量非零而且非零分量的值非常大基本固定在信号幅值的两个极端。这和高斯噪声有本质区别高斯噪声是“每个像素都被轻微污染”脉冲噪声是“少数像素被重度污染”。这个区别直接决定了算法设计的优先级。对高斯噪声模型关心的是整体能量最小化L2 范数那套理论非常顺手。对脉冲噪声模型必须对极端离群值不敏感否则少数几个坏点就会主导整个优化过程。1.2 经典方法在脉冲噪声面前的失效原因中值滤波的思路很聪明在一个窗口里取中值如果某个像素是孤立极值排序后它会跑到序列端点中值不受影响。密度低时比如 10% 以下3x3 窗口基本能清理干净而且边缘比均值滤波保留得好很多。但密度一旦上来比如 30%~50%问题就来了。窗口里同时出现多个黑点和白点时排序序列两端都是坏点中值可能落在坏点上也可能夹在两个坏点之间取到一个中间值结果是图像上残留大量斑点。加大窗口到 5x5、7x7能压住噪声但边缘被明显磨圆细线细节直接消失。均值滤波就更不合适了一个 255 的白点落在 3x3 窗口里均值直接被拉高 28 个灰度级整块区域发灰。本质上均值和中值这类局部滤波方法是在用一个固定形状的窗口做假设一旦噪声密度突破了窗口内“坏点是少数派”的前提方法就失效了。1.3 小波变换为什么能当手术刀多尺度视角我后来把注意力转到小波变换上是因为小波对“稀疏离群点”天然敏感。小波变换把图像分解成多个尺度的近似系数和细节系数干净自然图像的能量高度集中在少数大幅值系数上近似系数承载主体结构细节系数只在边缘和纹理处有较大值大部分系数接近零。脉冲噪声虽然看起来是稀疏的像素点但它不是自然图像结构。它被小波分解后散布在细节子带里表现为孤立的、幅度很大的尖峰系数。换句话说干净图像在小波域是稀疏的脉冲噪声在小波域也是稀疏的但两者的稀疏模式不同前者集中在边缘位置后者随机分布且没有结构性。这给了我们一个操作空间如果能在小波域把噪声对应的系数压下去同时保住图像结构对应的系数去噪目标就完成了。但怎么做最大就需要把它写成目标函数用优化方法去迭代求解。2. 把去噪翻译成优化目标函数怎么从零推出来2.1 从噪声模型到最大后验估计优化方法的第一步是写目标函数。从贝叶斯角度给定观测 y我们要找最可能的干净图像 x。根据最大后验估计这等价于最大化 p(x|y)也就是最大化 p(y|x)p(x)。对数展开后最大化后验等价于最小化两项之和一项是负对数似然衡量重建的 x 和观测 y 的吻合程度叫保真项另一项是负对数先验衡量 x 是否符合自然图像规律叫正则项。脉冲噪声 e 的分布可以用拉普拉斯分布近似它的对数似然对应 L1 范数也就是绝对值之和。干净图像在小波域上的系数先验也可以用拉普拉斯分布建模对应小波系数的 L1 范数。两者合起来目标函数就是minimize ||y - x||₁ λ ||Wx||₁这里 W 是小波变换算子x 是待求图像y 是观测图像λ 是平衡两项权重的超参数。2.2 保真项选 L1 还是 L2一个容易被忽略的致命细节很多人第一次尝试把去噪写成优化问题时会顺手用 L2 保真项 ||y - x||₂²因为平方误差处处可导处理起来方便。但在脉冲噪声场景下这是一个致命错误。L2 的梯度正比于残差 y - x。如果某个像素被脉冲噪声污染成 0 或 255残差的绝对值会非常大梯度也就非常大。一次迭代后这个坏点会以巨大的力量把周围本来干净的像素一起拉偏形成“彗星尾巴”一样的拖尾效应。脉冲密度越高这种拖尾越严重最终结果甚至比中值滤波还差。L1 就没有这个问题它的梯度只有正负 1。不管残差是 10 还是 250对参数的修正量都是固定幅度坏点不会因为自身强度大而获得更大的影响力。用收入分布来类比L2 拟合就像用平均值描述一群人一个亿万富翁能把平均值拉到天上L1 拟合更像用中位数极端值再多也撼动不了它。脉冲噪声处理必须用 L1 保真这是整个方法成立的第一块基石。2.3 稀疏正则项小波系数里的“少数派”再来看 ||Wx||₁。我前面说过自然图像的小波系数是稀疏的也就是少数大系数集中了大部分能量其余系数接近零。L1 范数和 L2 范数对稀疏性的态度完全不同。L2 范数对小幅值系数不痛不痒因为平方后更小了优化器没有动力把系数真正压到零。L1 范数的梯度是常数即使系数已经是 0.01它仍然给一个固定大小的推力把它推向零点。所以 L1 正则化是“强行逼系数归零”这就实现了去噪空间里的稀疏约束。把这两项合在一起优化器需要在两端之间找平衡保真项逼着 x 接近 y不让去噪把原图内容丢掉正则项逼着小波系数变小变稀疏把噪声对应的细小散列系数清掉。λ 就是这两股力量的拉锯点。3. 梯度下降在带 L1 的目标函数上怎么落地3.1 次梯度L1 范数在零点处的导数是什么目标函数里都是 L1 范数L1 范数在零点不可导。严格来说它的次梯度是一个区间 [-1, 1]传统牛顿法、拟牛顿法在这里直接失灵。但梯度下降家族使用次梯度方向仍然可以工作只是收敛速度会从指数级退化到亚线性对去噪这种工程任务完全够用。实现中最简单的做法是用 np.sign 函数作为 L1 的次梯度。sign(0) 取 0实际上是从 [-1, 1] 这个区间里选中点既不上推也不下压让优化器根据其他项来决定零点的去留。这种做法工程上合理数值上也稳定。3.2 小波算子的前向与反向“传播”现在要回答一个关键问题正则项 ||Wx||₁ 对 x 的梯度怎么算W 是小波变换是线性算子所以可以把它和神经网络里的线形层类比。前向传播就是做小波分解得到系数反向传播时梯度的流向是 W 的伴随算子 Wᵀ。对正交小波加上周期延拓模式WᵀW I反向传播就等价于用小波系数重构回图像域。这给了我们极大方便不需要自己手写伴随算子直接用小波重构函数就能把梯度从系数域映射回图像域。整条梯度计算链路是图像 x - 小波分解 - 对每个系数取符号 - 小波重构 - 得到正则项梯度方向。这个链条每一步都有成熟函数库支持完全不需要从零写滤波器组。3.3 从次梯度到近端梯度软阈值让迭代更稳次梯度下降能跑但有一个毛病它对 L1 范数的偏移反应不灵敏迭代后期会围绕最优点出现抖动收敛解也不够稀疏。更优雅的做法是近端梯度下降把 L1 正则项单独拎出来用软阈值算子闭式求解。软阈值算子长这样soft(r, t) sign(r) * max(|r| - t, 0)它的作用很直观把幅度小于阈值 t 的系数直接清零大于阈值的系数向零收缩 t。把小波系数整体做一次软阈值就等价于对当前系数做了一次正则项的近端步比单纯用 sign 当梯度更稳收敛也更快。不过要注意目标函数里还有 L1 保真项两个 L1 项并存的严格近端算子不像单个正则项那么干净。我实测下来的折中是主循环用次梯度下降推进最后一步做一次完整的系数域软阈值收尾效果综合下来最好既能避开数值不稳定又能获得稀疏解。4. 手写 Python 实现把公式变成能跑的去噪管线4.1 环境准备与数据构造我用到的库有 numpy、pywt、scipy、scikit-image 和 matplotlib。pywt 是 PyWaveletsPython 生态最成熟的小波工具箱scipy.ndimage.median_filter 负责基线的中值滤波实现scikit-image 提供测试图和评估指标。测试阶段我用 skimage.data.camera() 灰度图归一化到 [0,1] 浮点范围。这个范围的选取很重要它直接影响学习率的量级如果你在 [0,255] 范围跑所有超参数都得等比调整。我在 [0,1] 范围处理学习率 0.1 附近就能稳定工作。脉冲噪声注入函数实现如下import numpy as np from scipy.ndimage import median_filter import pywt def add_pulse_noise(img, density0.3): noisy img.copy() mask np.random.rand(*img.shape) density salt np.random.rand(*img.shape) 0.5 noisy[mask salt] 1.0 noisy[mask ~salt] 0.0 return noisymask 决定哪些像素被污染再随机分配黑白值。密度 0.3 表示三成像素被破坏属于中高污染场景足够拉开方法差距。4.2 小波域梯度的代码实现正则项梯度是核心。用 periodization 模式做小波分解这个模式保证正交性和完美重构重构操作才能正确扮演伴随算子。如果换成默认的对称延拓模式重构不是严格伴随梯度方向会有偏差迭代容易在边界处出现问题。def grad_regularizer(x, waveletdb4, level3): coeffs pywt.wavedec2(x, wavelet, levellevel, modeperiodization) grad_coeffs [np.sign(c) for c in coeffs] return pywt.waverec2(grad_coeffs, wavelet, modeperiodization)这里有一个实现细节值得注意coeffs 列表的第一个元素是近似系数后面每个元素是一个三元组包含水平、垂直、对角三个细节子带。np.sign 函数直接作用在任何形状的数组上所以这个处理方式不需要区分层级和方向统一取符号即可。4.3 完整训练循环与损失观察有了保真项梯度 sign(x-y) 和正则项梯度 grad_regularizer(x)完整迭代就非常简洁def denoise_pulse_image(y, lambda_0.1, eta0.1, iters200, waveletdb4, level3): x median_filter(y, size3).astype(np.float64) for i in range(iters): grad np.sign(x - y) lambda_ * grad_regularizer(x, wavelet, level) x x - eta * grad if i % 50 0: coeffs pywt.wavedec2(x, wavelet, levellevel, modeperiodization) loss np.mean(np.abs(x - y)) lambda_ * np.sum( np.abs(coeffs[0]) ) / y.size print(fiter {i:4d}, loss {loss:.6f}) return x注意一个关键选择x 的初值不是 y而是中值滤波后的结果。这个初值选择背后有工程考量。中值滤波虽然在高密度下不完美但它已经清掉了大部分极端脉冲让小波域里剩余的噪声系数更接近“小幅值、可被 L1 压掉”的状态。从更好的起点出发次梯度迭代的收敛速度明显加快通常 200 次迭代就够用。如果用 y 直接当初值500 次迭代还可能残斑明显。4.4 完整代码一次跑通把训练循环、噪声注入、评估串联起来完整的可运行脚本如下import numpy as np import pywt from scipy.ndimage import median_filter from skimage import data def psnr(img1, img2): mse np.mean((img1 - img2) ** 2) return 10 * np.log10(1.0 / (mse 1e-12)) def add_pulse_noise(img, density0.3): noisy img.copy() mask np.random.rand(*img.shape) density salt np.random.rand(*img.shape) 0.5 noisy[mask salt] 1.0 noisy[mask ~salt] 0.0 return noisy def grad_regularizer(x, waveletdb4, level3): coeffs pywt.wavedec2(x, wavelet, levellevel, modeperiodization) grad_coeffs [np.sign(c) for c in coeffs] return pywt.waverec2(grad_coeffs, wavelet, modeperiodization) def denoise_pulse_image(y, lambda_0.1, eta0.1, iters200, waveletdb4, level3): x median_filter(y, size3).astype(np.float64) for i in range(iters): grad np.sign(x - y) lambda_ * grad_regularizer(x, wavelet, level) x x - eta * grad return x img data.camera().astype(np.float64) / 255.0 noisy add_pulse_noise(img, density0.3) result denoise_pulse_image(noisy, lambda_0.1, eta0.1, iters300) print(PSNR of noisy:, psnr(img, noisy)) print(PSNR of result:, psnr(img, result))这段代码在普通笔记本上跑一张 512x512 灰度图300 次迭代大约需要几十秒性能完全可接受。如果想要更快可以减少迭代次数到 150 次左右PSNR 损失不到 0.5dB。5. 实测对比与调参哪些参数对结果影响最大5.1 与中值滤波、传统小波阈值的 PSNR 对比我在 camera 图上做了对比实验脉冲密度固定在 0.3评估三个方法3x3 中值滤波、传统小波软阈值去噪、本文的次梯度下降方法。结果如下表方法PSNR (dB)视觉表现原图脉冲噪声10.8黑白点密集无法直接看3x3 中值滤波26.9大部分噪声清除边缘略糊5x5 中值滤波24.3边缘明显发虚残留团状斑点小波软阈值23.7平滑过度白色脉冲留下光斑小波梯度下降29.4噪点干净边缘细节保留最好这里我必须说明数值会因图像不同而波动不同随机种子产生的噪声分布也会带来 0.5dB 左右的差异但方法之间的相对排序在同类测试图上基本稳定。为什么传统小波软阈值表现不佳因为它本质上是高斯噪声假设下的方案只做系数收缩不区分噪声来源。面对脉冲噪声这种极端离群值硬收缩会把亮点周围也一起模糊形成光晕。本文方法用 L1 保真项显式建模脉冲离群值梯度下降过程能自动区分哪些偏差来自真噪声、哪些来自图像结构效果自然更优。5.2 学习率的量级判断、发散症状与对策学习率是最先要调的参数。在 [0,1] 浮点图像范围总梯度的量级大约是 1保真项符号函数加上 λ正则项梯度所以 eta 取 0.05~0.2 是合理区间。我实测 0.1 最稳。eta 太大会看到明显的发散症状PSNR 曲线在初始值附近剧烈振荡图像上出现棋盘格状的新噪声。这是因为每次更新步长太长在分段线性目标函数的折线上来回弹跳。处理方法不是强行降低 eta而是加衰减。我常用一个简单策略每 50 次迭代 eta 乘以 0.8让后期步长自然缩小既能快速下降又能精细收敛。eta 太小则表现为 loss 下降缓慢、200 次迭代后还有明显残斑。一种稳妥的做法是以 10 为量级依次试探0.01、0.1、1看 loss 曲线是平滑下降还是振荡上升就能快速锁定合适量级。5.3 lambda 的物理意义与选择区间lambda 控制保真和正则的平衡。lambda 太大正则项权重过高优化器更愿意把图像抹平以换取小波系数更稀疏结果是细节丢失图像偏软。lambda 太小正则项压不住噪声系数迭代结束后仍残留细小斑点。对 [0,1] 图像、符号函数次梯度做保真项的场景lambda 在 0.05~0.3 区间表现稳定。我通常从 0.1 起步观察结果如果残留白点说明 lambda 偏小增大到 0.2如果画面发糊、细节丢失说明 lambda 偏大减小到 0.05。这个参数对噪声密度的敏感度其实不大。密度从 0.2 升到 0.5最优 lambda 只从 0.1 漂移到 0.15 左右可调整空间非常友好。真正影响 lambda 选择的是图像内容纹理细节丰富的图像需要更小的 lambda 来保护高频结构平滑图像则可以适当加大。5.4 小波基、分解层数与边界模式的影响小波基的选择没有想象中那么敏感。db4 和 sym8 在大部分测试图上效果接近sym8 略优但差距不到 0.3dB。bior 系列表现也不差但有轻微的视觉伪影。我的建议是直接用 db4速度快、对称性中等、边界伪影可控。分解层数 level 取 3 或 4。层数太少低频近似不够彻底噪声混在近似系数里压不掉层数太多深层系数维度太小正则项作用范围被稀释而且计算量增加。3 层是在 512x512 图像上的甜点值。边界模式这里必须用 periodization。原因我在第 4.2 节说过它保证正交性和完美重构让 waverec2 能精确扮演小波变换伴随算子的角色。用默认对称模式时梯度方向在边界处不准确迭代效果肉眼可见地下降具体表现是边界一圈明显比中心脏。6. 我在实测中踩过的坑和一点个人体会6.1 次梯度抖动问题与收尾技巧次梯度下降和标准梯度下降的收敛行为差异很大。标准梯度下降最后会稳定在一个很小的邻域内次梯度下降即使到了最优点附近也可能因为符号函数的跳变而来回小幅振荡。最典型的现象是 loss 曲线在最后几十次迭代不下降反而有锯齿状抖动。这不是 bug是 L1 正则项的固有特性。完整的近端梯度下降能消除这个抖动但实现更复杂。我采用的折中方案是主循环用次梯度迭代结束后对结果再做一次小波软阈值收尾阈值取当前小波系数的某个分位数比如第 70 百分位。这一步能把次梯度迭代残留的小幅值系数批量清理掉实测能再提升 0.2~0.5dB而且不增加太多计算量。6.2 彩色图与高密度噪声的处理细节彩色图像不能直接把这个算法套在 RGB 三通道上那样会破坏通道间相关性出现彩色边缘伪影。两个可行方案一是把 RGB 转成 YUV只在亮度通道 Y 上做去噪色度通道 U、V 原本没有脉冲污染但如果你已经拿到的是被污染图可以简单做一次中值滤波这样处理速度高、视觉效果好。二是三通道独立处理后再合并但需要把 lambda 对每个通道分别微调实际效果不如第一种。脉冲密度超过 60% 时中值滤波初始化就不再可靠因为窗口里的中值本身就可能是坏点。这种情况我建议先用大窗口比如 7x7 的中值滤波把图像粗略救回来再进入迭代否则迭代要从极高噪声水平开始很容易收敛到局部结构而非真实图像。6.3 给想继续深挖的读者的建议做完这个项目后我最大的体会是把去噪问题形式化为目标函数收益远不止一个方法本身。一旦写成可微目标函数就能用统一的优化框架去控制保真和稀疏的平衡还能根据实际噪声情况灵活替换保真项、正则项。更进阶的一个扩展方向是深度展开网络。如果把这里的迭代结构固定下来把 lambda、学习率、软阈值变成可学习的参数用一批带噪声的样本去训练就得到 ISTA-Net 这类可学习优化器。它的收敛速度比固定参数的迭代优化快一个数量级。这也是我从这个项目往外延伸时最感兴趣的方向——小波变换提供结构和可解释性梯度下降提供迭代框架两者结合后往上叠加学习能力是图像复原领域相当值得深耕的路径。
返回列表