简介:面向图像处理初学者与研究者的图像去噪算法演示集合,融合 Python 与 PyQt5,既涵盖高斯滤波、中值滤波、快速傅里叶变换去噪、双边滤波、非局部均值去噪,也加入了 DnCNN 等深度学习模型思路,可应用于课程实验、算法对比和桌面小工具开发。压缩包共 6 个文件,其中 5 个 py 脚本分别对应不同滤波算法及程序入口,另有 1 个 ui 文件定义可视化界面,整体仅 6KB,轻量精简,便于直接阅读、修改和运行。资源已有 6212 人学习,用户可在图形界面中切换去噪方式、调节滤波器大小与平滑强度,实时观察处理效果并保存图像,从而直观理解空间域与频率域去噪的区别。对于希望系统掌握图像去噪原理并快速上手 PyQt5 应用开发的读者,这套代码提供了可复用的参考实现,能够帮助将算法理论快速落地为实际工具,也为后续扩展更多算法留出了清晰接口。
1. 图像去噪不是把图变干净:先认清噪声类型再谈算法
拿一张昏暗楼道里的监控画面说:传感器一升温,暗部全是彩色颗粒,这就是我们要处理的噪声。图像去噪的目标很直接——把噪声抹掉,但别把墙砖缝隙、车牌边缘、人物轮廓一起抹掉。很多初学者第一次跑均值滤波就发现“干净是干净了,但糊成一团”,这种翻车通常不是代码问题,而是没分清噪声类型和滤波器的适用范围。这篇文章把空间域滤波、小波阈值去噪、深度学习去噪三条路线写到能直接复现的程度,附上参数怎么调、指标怎么算、工程里最容易踩的坑。适合正在做图像处理作业、OpenCV 图像处理项目,或者想往 ISP 图像处理方向走的人。想直接抄代码的可以跳到第2章,但我建议耐住性子看完第5章,那几条坑我交过学费。
2. 空间域滤波:均值和高斯为什么抹边缘,中值和双边怎么救回来
空间域滤波是去噪的入门路线,也是 OpenCV 图像处理里最常用的工具箱。它直接对像素邻域做运算,速度快、解释性强,适合用来快速验证噪声水平,也适合做复杂算法的预处理步骤。需要注意:这类算法通常只对高斯噪声和椒盐噪声这两类常见噪声有稳定效果,遇到复杂真实噪声,比如暗光下的彩色噪点,只能打个底。
2.1 线性的均值与高斯滤波:为什么边缘也被抹平
先看一段基于 OpenCV 的最小实现:
import cv2 img = cv2.imread("noisy_demo.jpg", cv2.IMREAD_GRAYSCALE) # 均值滤波:窗口内像素求平均 mean_img = cv2.blur(img, (5, 5)) # 高斯滤波:窗口内像素按高斯权重加权求和 gauss_img = cv2.GaussianBlur(img, (5, 5), sigmaX=1.5)这段代码里 cv2.blur 的第二个参数是核尺寸,也叫卷积窗口,必须是正奇数。窗口越大,平滑越强,但边缘宽度也会被拉大。cv2.GaussianBlur 多了一个 sigmaX 参数,它控制高斯核的权重分布:sigmaX 建议取 1.0~1.6,如果窗口是 5x5、sigma 又给得很大,核权重会退化接近均值滤波,边缘丢失更严重。
很多教程会额外展示一个手动实现均值滤波的写法,用 np.ones((3,3), np.float32)/9 丢给 cv2.filter2D。这在原理上与 cv2.blur 完全等价,区别只是 filter2D 允许你自己设计核。理解这一步很重要:均值核每个位置权重相等,高斯核则是中心权重最大、四周按二维高斯函数递减。卷积核里没有考虑像素的局部关系,只是把邻域信息垒在一起,所以线性滤波几乎不可能“区分噪声和边缘”。
再补充一个容易忽略的点:高斯核的 sigma 与核尺寸需要匹配。OpenCV 里如果 sigmaX 传 0,会按公式 sigma = 0.3 * ((ksize - 1) * 0.5 - 1) + 0.8 自动推导,这个经验公式在 3x3 到 11x11 之间都适用。手动设置时,核尺寸不要超过 6 * sigma + 1,否则核外层的权重已经接近零,空转浪费算力。为什么工程上宁可用高斯也不用均值?因为均值核在频域对应一个矩形窗的 sinc 函数,阻带旁瓣很大,平滑时容易在强边缘附近留下类似振铃的光晕;高斯核没有这种旁瓣,平滑更温和。所以均值滤波现在基本只用来做超快预处理或算法教学。
如果观察到“图片干净但文字、栏杆发虚”,说明窗口太大或者 sigma 太大。这时要优先把窗口降回 3x3,而不是去加后续锐化,锐化只会把噪声再放大。均值和高斯在数学上都是线性操作,对边缘和噪声一视同仁地“平均”,所以在去噪和保细节之间天然没有平衡点,这是性质决定的,不是代码能补的。实际项目中,我一般只把线性滤波用在两个地方:一是去掉平坦区域中较轻的读出噪声,二是作为后续边缘检测或分割的预处理。真正的去噪主力,还得看下面这两个非线性方案。
2.2 中值和双边滤波:让边缘活下来的非线性方案
中值滤波的思路很朴素:把窗口内所有像素排序,取中间那个值作为中心像素输出。它对椒盐噪声,也就是突兀的黑白点,几乎是“一键清除”,因为它直接丢弃了离群点,不需要对离群值做加权。OpenCV 里一行即可:
median_img = cv2.medianBlur(img, 5) # 第二个参数为窗口直径,必须为大于1的奇数自己实现一个 3x3 中值也不难,本质是把窗口内 9 个像素排序后取第 5 个值,但注意这种写法在图像边缘会丢掉行列,实际使用要补边界值,用 BORDER_REPLICATE 效果不错。OpenCV 内部用的是历史直方图优化的快速中值,CPU 上已经很快,不必自己重写。中值滤波的代价是:窗口里的细小纹理都被当成“噪声”处理了,头发丝、栅栏这类高频结构,窗口稍大就消失,所以中值窗口一般不超过 5x5,超过后画质下滑明显。
双边滤波则换了个思路。它对像素做加权平均,权重同时考虑两个距离:空间距离,离中心越近权重越大;灰度距离,灰度差越大权重越小。这个灰度权重把边缘两边的像素“隔离”开来,因此能在去噪的同时保住边缘。实现如下:
# d 是窗口直径,sigmaColor 控制灰度相似度,sigmaSpace 控制空间相似度 bilat_img = cv2.bilateralFilter(img, d=9, sigmaColor=75, sigmaSpace=75)这三个参数第一次上手时很容易懵,尤其是 sigmaColor 和 sigmaSpace 怎么配合。我的习惯是:sigmaColor 取噪声幅度的 2 到 3 倍,sigmaSpace 取 40~80,d 先给 9。如果结果有破碎感,说明 sigmaColor 太大或 d 太小;如果边缘在抖,说明 sigmaSpace 太大。调参顺序固定为:先定 d,再调 sigmaSpace 到边缘稳定,最后慢慢缩小 sigmaColor 直到噪声刚好消失。这个过程没有公式可以直接给答案,非常依赖观察,这也是很多人说双边滤波参数是玄学的原因。
双边滤波还有一个视觉缺陷叫梯度反转:在强边缘附近,灰度距离权重可能把很小的噪声簇保留下来,形成类似拼接的块状感。遇到这种情况,常规做法不是继续调大 sigmaColor,而是降低输出图像分辨率后再滤波,或者把彩色图先转 Lab 空间,只对 L 通道做双边,减少色度方向的噪声干扰。实时视频里,窗口和 sigma 都偏大时双边滤波很容易成为性能瓶颈,尤其 CPU 上跑 1080p。常见做法是降采样到 720p 滤波后再放大,或者用 Bilateral Grid 这类快速近似把计算量降几个量级。在智能车图像处理这类对毫秒级延迟敏感的场景里,通常还会限制 sigmaSpace 不超过 40,并保证滤波前后帧间噪声方差变化不超过 0.5。
空间域滤波的整体选型可以按下表来:
| 算法 | 适用噪声 | 边缘保留 | 耗时 | 适用场景 |
|---|---|---|---|---|
| 均值滤波 | 均匀高斯噪声 | 差 | 极低 | 预处理、亮度平稳区域 |
| 高斯滤波 | 高斯噪声 | 差 | 极低 | 预处理、减少细节后做阈值分割 |
| 中值滤波 | 椒盐/脉冲 | 中等 | 低 | 监控夜间闪烁点、打印扫描去点 |
| 双边滤波 | 低强度高斯 | 好 | 中等 | 人脸美化、轻度去噪 |
耗时这列只在 CPU 小图上对比。真要跑在 FPGA 或 ISP 流水线里,中值和双边都能被改造成行缓冲结构的定点实现,但参数必须固定死,不能在运行期动态改窗口。这一章适合做第一版,对实时性要求高的场景,中值加自动阈值往往比后两章的算法更早能上板。
3. 小波阈值去噪:在高频系数里抖掉噪声
3.1 为什么小波能区分噪声和边缘:系数稀疏性与硬软阈值
在空间域里,噪声和边缘都是“变化剧烈的像素”,很难分开。小波变换把图像分解成不同尺度的子带:低频近似保留整体亮度,高频细节携带水平边缘、垂直边缘和对角纹理。真实边缘的能量集中在少数几个大系数上,而噪声能量均匀分布在所有高频小系数上。用一个阈值把“小”系数压掉,再把系数重建回图像,就完成了去噪。这就是小波阈值去噪的基本逻辑。
阈值选多少是全部工程细节所在。最通用的固定阈值取:
λ = σ × sqrt(2 × ln(N))
N 是像素总数。σ 不是主观指定,而是用第一层高频对角子带的中值绝对偏差来估计:
σ̂ = median(|cD|) / 0.6745
0.6745 是标准正态分布下 MAD 与标准差的比例常数。这里容易犯的错是直接对整幅图像算 std,得到的是一个被边缘污染的估计值。用对角子带是因为它在水平和垂直方向都不敏感,更接近纯噪声背景。
阈值函数有硬阈值和软阈值两种。硬阈值把小于阈值的系数置零、大于阈值的保留原值,结果干净但会产生振铃伪影;软阈值让所有系数向零收缩一个阈值,看起来更柔和,但会把边缘能量也削掉一部分。实践中绝大多数项目用软阈值,只有当图像本身是二值图或边缘极锐利时才用硬阈值。与傅里叶变换对比更能看出小波的位置:傅里叶变换把噪声和细纹理混在同一个频带里,简单低通会连纹理一起去掉;小波在多个尺度上做局部化分析,至少能在尺度这一维上把二者分开,但同一尺度内部仍然依赖阈值。这套方法对噪声标准差在 15~60 范围的图像效果最明显;噪声太弱时阈值会把细节全砍掉,噪声太强时系数已经完全淹没,阈值法也无能为力,只能交给深层模型。
3.2 基于 PyWavelets 的完整实现与 MATLAB 参数对照
下面这段代码可以直接跑通一张灰度图的去噪流程:
import numpy as np import pywt def wavelet_denoise(img, sigma=None, level=4, wav='db4', mode='soft'): # 分解:低频 + 多尺度高频 coeffs = pywt.wavedec2(img, wav, level=level, mode='periodization') # 用小波估计噪声标准差,取最后一层对角高频 if sigma is None: cD = coeffs[-1][2] sigma = np.median(np.abs(cD)) / 0.6745 # 固定阈值公式 thr = sigma * np.sqrt(2 * np.log(img.size)) # 第一个元素是低频,后续才是高频分量 new_coeffs = [coeffs[0]] for detail in coeffs[1:]: cH, cV, cD = detail # 阈值处理:默认软阈值 cH = pywt.threshold(cH, thr, mode=mode) cV = pywt.threshold(cV, thr, mode=mode) cD = pywt.threshold(cD, thr, mode=mode) new_coeffs.append((cH, cV, cD)) # 重构,与分解 mode 需一致 return pywt.waverec2(new_coeffs, wav, mode='periodization')调用例子:
denoised = wavelet_denoise(gray_img, level=4, wav='sym8', mode='soft')参数说明:
- level:分解层数,4 层对 512x512 图是常用起点;层数太多,低频近似被反复抽稀,会丢失大尺度纹理结构。判断 level 是否过大有个土办法:把最后一层低频近似图放大回原尺寸,如果它还能看到明显的闪烁点,说明噪声没有完全被分到高频,需要增加一层分解;反过来,如果低频图出现成块的“油墨感”,说明分解过度,细节已经被抽进高频并被阈值杀掉了。
- wav:小波基,工程里最先试 db4 或 sym8。db4 短小,局部分辨率好;sym8 更平滑,振铃更少。bior 系列也可以,但需要成对指定。
- mode:阈值处理方式,soft 默认;若图像是文档扫描件,换成 hard 能保留更锐利的笔画边缘。
- 分解时的延拓模式:上面代码用了 periodization。MATLAB 的 wdencmp 默认用对称延拓,直接统一成 periodization 反而能避免两边结果不一致。
如果用 MATLAB 做对照,wdencmp('gbl', X, 'db4', 4, thr, 's', 1) 里的 thr 就是上面算出来的标量阈值;'s' 对应软阈值,'h' 对应硬阈值。注意 MATLAB 与 PyWavelets 对边界延拓的约定不同,同参数下系数会有一圈像素的差异,算 PSNR 之前记得先裁掉 20 个像素。阈值倍率也是一个值得单独调的参数,刚开始不要直接改阈值函数,而是给固定阈值乘一个 0.8~1.2 的系数观察边缘完整度。倍率在 0.8 以下去噪几乎无效,在 1.2 以上边缘损失速度明显加快。
小波去噪最典型的问题有两个:一是振铃,在强边缘周围看到波纹,对应阈值过高或小波基太长;二是纹理丢失,对应 level 过高或软阈值收缩过猛。第一次上手时,先固定 level=4、wav='sym8',单独调阈值倍率,比一次性把四个参数全动有效得多。
4. 深度学习去噪:用 PyTorch 复现一个最小 DnCNN
深度学习去噪不是“把网络换大一点”就能无限变强的事。它的前提非常具体:训练数据里的噪声分布要接近真实场景。理解了这个前提,再去看模型结构、损失函数和训练细节,每一步都有明确目的。这节用一个轻量残差网络走通从合成噪声训练到推理的完整流程,整段代码在 CPU 上也能跑出可见效果。
4.1 理解残差学习:模型预测噪声而不是干净图
DnCNN 的思路是让卷积网络直接输出“噪声图”,而不是干净图。输入是含噪图 x,标签是纯噪声 n = y - x,网络学的是 f(x) ≈ n,最终干净图 = x - f(x)。这里的关键是:残差目标更接近白噪声响应,分布简单、尺度稳定,网络在同样参数下收敛更快,对深度设置的敏感度也更低。
即使不用 BatchNorm,一个 8 层左右的纯卷积残差网络已经能在合成高斯噪声上去掉一大半噪声。下面这个实现刻意去掉了 BatchNorm,避免在推理时踩到 batch 统计量不一致的坑。
import torch.nn as nn class TinyDnCNN(nn.Module): def __init__(self, depth=8, n_feats=64): super().__init__() self.in_conv = nn.Conv2d(3, n_feats, 3, padding=1) self.mid = nn.ModuleList([ nn.Conv2d(n_feats, n_feats, 3, padding=1) for _ in range(depth - 2) ]) self.out_conv = nn.Conv2d(n_feats, 3, 3, padding=1) self.relu = nn.ReLU(inplace=True) def forward(self, x): x = self.relu(self.in_conv(x)) for layer in self.mid: x = self.relu(layer(x)) return self.out_conv(x)输入输出刻意保持 3 通道,因为真实现实场景里彩色噪声的通道间相关性是客观存在的,逐通道独立去噪会留下色斑。模型并不神秘,它就是一组 3x3 卷积加 ReLU,预测的是噪声那张图。选择 MSE 作为主损失,是因为它与 PSNR 对齐度高、训练稳定;如果想让人眼看起来更锐利,可以在训练后期加入 SSIM 项做微调。
4.2 合成噪声训练:从照片切 patch 加噪声
真实带噪声的高质量成对数据集很难拿到,常见做法是找一批干净自然图像,在线切 patch 并加上高斯噪声,用有监督方式训练。这个合成数据和真实数据的 gap,就是深度学习去噪产品的第一个翻车点。
import torch import numpy as np import cv2 from torch.utils.data import Dataset class NoisyPatchDataset(Dataset): def __init__(self, image_paths, patch_size=64, sigma=25): self.paths = image_paths self.patch_size = patch_size self.sigma = sigma def __getitem__(self, idx): img = cv2.imread(self.paths[idx]) # 随机裁剪 h, w, _ = img.shape ph, pw = self.patch_size, self.patch_size y = np.random.randint(0, h - ph) x = np.random.randint(0, w - pw) patch = img[y:y+ph, x:x+pw].astype(np.float32) / 255.0 # 加高斯噪声,sigma 单位与像素一致 noise = np.random.randn(*patch.shape) * (self.sigma / 255.0) noisy = np.clip(patch + noise, 0, 1) return torch.from_numpy(noisy.transpose(2, 0, 1)), \ torch.from_numpy(patch.transpose(2, 0, 1))这里 sigma 的单位是 0~255 的灰度值。sigma=25 是中等噪声,肉眼能明显看到颗粒但图像内容仍然清晰;sigma=50 以上接近重度噪声,需要加深网络或加大 patch。patch 切到 64x64 是因为它能在内容丰富度和批量大小之间取平衡,切太小会学不到长纹理结构,切太大会拖慢训练。
训练循环本身不复杂,前向、算 MSE、反向:
model = TinyDnCNN(depth=8, n_feats=64) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) loss_fn = torch.nn.MSELoss() for epoch in range(20): for noisy, clean in dataloader: optimizer.zero_grad() pred_noise = model(noisy) # 训练目标:噪声图 = 干净图 - 含噪图 loss = loss_fn(pred_noise, clean - noisy) loss.backward() optimizer.step()初始学习率用 1e-3,每 10 个 epoch 乘以 0.5。如果显存足够,把 batch size 提到 64 比提高学习率更有利于训练稳定性;没有 GPU 的机器上,patch 可以降到 48x48,保证 20 分钟内能出一个能看的结果。推理时用输入减去预测噪声即可:
def denoise_with_model(model, noisy_bgr): tensor = torch.from_numpy(noisy_bgr.transpose(2, 0, 1)).unsqueeze(0).float() / 255.0 with torch.no_grad(): pred_noise = model(tensor) clean = tensor - pred_noise clean = clean.squeeze(0).permute(1, 2, 0).cpu().numpy() return np.clip(clean * 255, 0, 255).astype(np.uint8)这段代码跑出来的模型对 sigma=25 的合成高斯噪声,在常见测试图集上 PSNR 大约能到 31~33 dB,比双边滤波的 28~29 dB 有明显提升。但注意它解决不了真实暗光噪声里的彩色偏,因为训练数据只有高斯白噪声。要处理真实噪声,做法是把训练噪声换成 raw 域泊松噪声模板,或者用带真实噪声的配对数据做微调。这是深度学习去噪从 demo 走向产品的最关键一步,也是 ISP 图像处理链路里最不值得省时间的一步。
5. 去噪项目最容易踩的 5 个坑:现象、原因与解决
这一章集中记录我在实际项目里遇到过的典型问题。每一条都按现象、原因、解决的顺序写,方便你对照自己的图快速定位。
5.1 指标与边界:PSNR 高不代表肉眼舒服,换库后结果对不上
第一个坑,是指标崇拜。现象:降噪后 PSNR 提升了 1.5 dB,但把两张图放大到 100% 看,纹理像面团,边缘出现白边。原因:PSNR 只是逐像素误差的平均程度,它对“平滑感”几乎没有惩罚。一张把所有高频都抹平的图,PSNR 常常很高,因为它在平坦区减少了大量噪声误差;但人眼恰恰在意边缘周围有没有振铃、纹理区有没有被过度平滑。解决:用 PSNR 加 SSIM 双指标,并在每个测试集里独立出一类“强纹理图”。SSIM 如果掉到 0.9 以下,基本不能放行。肉眼在 100% 缩放下重点看三处:边缘 5 像素范围内的明暗过渡、大面积平滑区的颗粒感、横幅文字的锯齿。这三处没问题,再谈指标。
第二个坑是跨库边界不一致。现象:同一个算法、同一个阈值,OpenCV 和 MATLAB 的去噪结果 PSNR 对不上,差异集中在图像四周。原因:两边边界延拓方式完全不同,MATLAB 的 imfilter 默认用零填充,OpenCV 的多数滤波函数默认用 BORDER_REFLECT_101,小波分解对边缘的扩展方式也不一致。解决:评估时统一把图像四周裁掉 20 个像素,只对内部有效区域算指标。如果是对比不同库,最省事的做法是先用 cv2.copyMakeBorder 把边界统一填充成复制模式,处理完再裁回。凡是跨语言、跨库对比实验结果,这一步不能省。
5.2 彩色与结构:RGB 通道滤波和形态学为什么越干越脏
第三个坑,是在 RGB 三个通道上无脑滤波。现象:对一张夜景 RGB 图三个通道分别做高斯滤波,输出图像的边缘出现彩色光晕,去噪后色相偏移。原因:RGB 三个通道的噪声方差受传感器 Bayer 排列和增益影响,通道间相关很强。逐通道滤波没有利用这个相关性,还把高亮度通道的强噪声扩散到了其他通道。解决:先转 YCrCb 色彩空间,对亮度通道 Y 做强去噪,对 Cr/Cb 只做轻微去噪,比如 3x3 高斯,去噪完再转回 RGB。如果噪声是明显的彩色颗粒,要优先确认 Bayer 排列方式,并使用 ISP 馈送的 demosaic 后数据,而不是直接在 RGB 上堆滤波器。
第四个坑,是形态学操作被当成去噪万金油。现象:用 cv2.morphologyEx 的 MORPH_OPEN 处理后的图像,黑点少了,但文字变细、细小结构消失,高斯噪声反而被放大。原因:开运算本质是删除小于结构元的前景区域,适合处理孤立点和断续线这类形态噪声,对随机幅度分布的高斯噪声没有建模能力;膨胀又会让暗部噪声聚集成块。解决:形态学只用在二值图或掩膜上,处理空洞和断线,不要拿来应对灰度噪声。如果目标只是去掉椒盐噪声,中值滤波不仅效果更好,还不需要设计结构元。这条在工业检测里很常见,很多人图省事直接拿开运算当去噪,最后都会回来补中值。
5.3 传感器噪声:raw 域暗部那套高斯参数为什么失效
第五个坑,是真实传感器噪声不服从高斯分布。现象:同一套高斯去噪参数,在白天的图上效果很好,换到夜间暗部就出现大量青蓝色噪点,去不掉或越去越脏。原因:真实传感器噪声由光子散粒噪声和读出噪声组成,暗部光子数极低,噪声几乎服从泊松分布,又在高增益下被放大;高斯模型这时严重失真。解决:先用均匀亮度灰阶图做噪声标定,统计每个亮度区间的标准差,得到噪声水平随亮度的曲线,再决定去噪强度。工程上常见的让步做法是对暗部不做强去噪,而是用亮度自适应阈值:在 YCrCb 空间里把暗部像素的降噪强度衰减到亮部的一半,同时配合多帧时域平均。这也是大多数 ISP 图像处理链路里暗部降噪的标准做法。
提示:如果手里有 raw 原图,先把黑电平扣除并完成白平衡校正,再进亮度域去噪;跳过这一步直接处理 RGB,参数再好也救不回彩色噪点。
若拿到的是 raw 和对应 RGB 图,还可以用 Anscombe 变换把泊松噪声转成近似高斯,再做高斯去噪,最后逆变换,效果比直接在 RGB 上做更稳。这套流程看着多,但每步都是必要的。越早把“我的噪声是什么分布”这个问题搞清楚,后面调参越省力。
6. 上生产前先估噪声:MAD 估计器与残差验证
6.1 用 MAD 估计器给噪声“量个血压”,再决定去哪条路线
前面提到,阈值法和深度模型对噪声强度都很敏感。问题是很多项目拿到图片就开始调滤波器参数,调参用的图一旦换场景就失效。正确的顺序是:先估计当前画面的噪声水平,再根据水平选算法和参数。这个估计用最简单的 MAD 就能做:
import numpy as np import pywt def estimate_noise_std(gray_img): # 做一层小波分解,取对角高频子带 cD _, (cH, cV, cD) = pywt.dwt2(gray_img, 'db4') return np.median(np.abs(cD)) / 0.6745这个函数在任意一张灰度图上都能毫秒级返回一个标准差数值。按估计值选型,我一般这样分档:
- sigma < 5:肉眼几乎不可见,不需要去噪,最多做一次 3x3 高斯;
- 5 < sigma < 20:双边滤波或小波软阈值足够,优先双边,参数直观且计算快;
- sigma > 20:小波阈值开始不划算,直接上轻量深度模型;
- 如果场景是监控或工业相机且帧率要求很高,再考虑 FPGA 上的中值或小波硬件实现,但前提是 sigma 已经用上面的方式标定过,不要再拿默认参数去猜。
这个分档不能覆盖所有真实噪声,但给了一个可复现的起点。估计出的 sigma 只能指导算法选择,不能直接当作滤波参数:双边滤波的 sigmaColor 通常是它的 2~3 倍,小波阈值则是 sigma × sqrt(2 lnN) 再乘一个 0.8~1.2 的比例系数。
6.2 一张残差图,撕开“去噪”和“磨皮”的区别
去噪效果除了用指标判断,我习惯在调试时多生成一张残差图:残差 = 原图 - 去噪图。正确的结果里,残差应该是一张没有明显结构的随机颗粒图;如果残差里还能看到楼房的轮廓、窗帘的花纹,说明这些本不该被去掉的结构被滤波器吃了。反过来,残差灰度范围几乎为零,说明当前参数根本没碰到噪声,纯粹在空转。
residual = np.abs(orig.astype(np.float32) - denoised.astype(np.float32)) cv2.imwrite("residual.png", np.clip(residual * 8, 0, 255).astype(np.uint8))乘以 8 是为了把微弱残留放大到肉眼可见的程度。把残差图和原图叠在一起来回翻,能发现很多指标发现不了的问题,比如边缘过冲、色偏残留、局部块状伪影。我这几年的习惯是:任何去噪算法改动,先出残差图,再谈质量好坏。这样反而省掉大量来回沟通改参数的时间。
从估噪到选型,从指标到残差,这套流程是去噪项目能快速落地并且后期不靠玄学维护的关键。希望这些经验能帮到你,下次拿到一张“看着脏”的图,不用再从一个 3x3 高斯开始试起。
本文还有配套的精品资源,点击获取