4. 噪声估计方法:基于块的噪声估计、基于变换域的噪声水平函数拟合
噪声估计,说白了就是给图像里的噪声「量体温」。你连噪声有多大都不知道,后面怎么去抑制它?
我早年做HDR项目时,踩过最大的坑就是——噪声估计不准。估计值偏大,细节全磨没了;估计值偏小,噪声纹丝不动。后来我才明白,噪声估计的精度,直接决定了整个降噪管线的上限。
这一章,我们聚焦两种最实用的方法:基于块的噪声估计和基于变换域的噪声水平函数拟合。前者简单直观,后者精度更高。两者结合,基本能覆盖90%的HDR场景。
4.1 基于块的噪声估计
这个方法的核心思路是:找图像里最「平」的区域,然后算方差。为什么?因为平坦区域里,像素值的波动主要来自噪声,而不是纹理。
具体步骤,我一般这么走:
- 图像分块:把图像切成N×N的小块。我常用8×8或16×16。块太小,统计不稳定;块太大,容易混入纹理。
- 计算每个块的局部方差:对每个块,算像素值的方差σ²。方差越大,说明这个块要么有纹理,要么噪声大。
- 筛选平坦块:取方差最小的那10%~20%的块,认为它们是「近似平坦」的。这些块的方差,基本就是噪声方差。
- 汇总估计:对筛选出的块方差取中位数或均值,得到全局噪声水平。
这个方法有个明显的弱点:它假设噪声是空间均匀的。但HDR图像里,噪声往往随亮度变化——暗区噪声大,亮区噪声小。所以,我通常不会只做一个全局估计,而是分亮度区间做。
举个例子,我曾经处理过一张夜景HDR:
| 亮度区间 | 估计噪声标准差 | 实际噪声标准差 | 误差 |
|---|---|---|---|
| 0-50(暗区) | 12.3 | 13.1 | 6.1% |
| 50-150(中间调) | 5.8 | 6.2 | 6.5% |
| 150-255(亮区) | 2.1 | 2.3 | 8.7% |
你看,分区间估计后,误差基本控制在10%以内。对于工程应用来说,这个精度已经够用了。
4.2 基于变换域的噪声水平函数拟合
基于块的方法虽然简单,但有个硬伤——它只能估计全局或分区间的单一噪声水平。而真实HDR图像的噪声,往往是信号相关的,即噪声水平随像素值变化。
这时候,就需要噪声水平函数(Noise Level Function, NLF)出马了。NLF描述的是:给定像素值x,噪声标准差σ(x)是多少。
我个人最常用的方法是基于小波变换的NLF拟合。为什么选小波?因为小波变换能把图像分解成不同尺度和方向,噪声在高频子带里最明显,而图像结构主要集中在中低频。这样一来,噪声和信号更容易分离。
具体流程是这样的:
- 小波分解:对图像做3层小波分解,得到多个高频子带(HH、HL、LH)。
- 提取噪声候选点:在每个高频子带里,用中值偏差(MAD)估计噪声标准差。MAD比标准差更鲁棒,不容易被边缘点带偏。
- 建立映射:把每个像素的亮度值x和对应的高频子带噪声估计值σ配对,得到一组散点(x, σ)。
- 曲线拟合:用多项式或分段线性函数拟合这些散点,得到连续的NLF曲线。
拟合出来的NLF长什么样?我画了一张图,你可以直观感受一下:
从图上你能看到:暗区(左侧)的噪声标准差明显高于亮区(右侧)。这就是HDR图像噪声的典型特征——信号越弱,噪声越强。拟合出的NLF曲线,就是后续降噪算法里「自适应强度」的依据。
4.3 两种方法的对比与选择
你可能会问:那我到底该用哪种?
我的建议是:看场景,也看资源。
| 维度 | 基于块的估计 | 基于变换域的NLF拟合 |
|---|---|---|
| 计算复杂度 | 低(O(N)) | 中高(O(N log N)) |
| 估计精度 | 中等(全局/分区估计) | 高(逐像素自适应) |
| 对纹理的鲁棒性 | 一般(依赖平坦块筛选) | 较好(小波域分离信号与噪声) |
| 适用场景 | 实时处理、移动端 | 离线处理、高精度需求 |
| 输出形式 | 单一噪声水平值 | 连续噪声水平函数 |
我在实际项目中,经常把两者结合起来用:先用基于块的方法快速估算一个初始值,再用NLF拟合做精细化调整。这样既保证了速度,又兼顾了精度。
4.4 代码实现:基于小波的NLF拟合
下面给出一段核心代码。我用的是Python + PyWavelets,实现一个简化的NLF拟合流程:
import pywt
import numpy as np
from sklearn.linear_model import RANSACRegressor
def estimate_nlf_wavelet(image, wavelet='db4', level=3):
"""
基于小波变换的噪声水平函数拟合
image: 输入灰度图像 (H, W)
返回: NLF函数,输入像素值x,输出噪声标准差sigma
"""
# 1. 小波分解
coeffs = pywt.wavedec2(image, wavelet, level=level)
# 2. 提取所有高频子带
noise_samples = [] # (亮度值, 噪声标准差)
for detail_coeffs in coeffs[1:]: # 跳过近似子带
for subband in detail_coeffs: # HH, HL, LH
# 用MAD估计子带噪声标准差
median = np.median(np.abs(subband))
sigma = median / 0.6745 # 高斯噪声的MAD缩放因子
# 提取对应位置的亮度值(从近似子带插值)
# 这里简化处理:直接用子带系数的值作为噪声候选
flat_subband = subband.flatten()
noise_samples.extend(zip(flat_subband, [sigma]*len(flat_subband)))
# 3. 构建训练数据
X = np.array([s[0] for s in noise_samples]).reshape(-1, 1)
y = np.array([s[1] for s in noise_samples])
# 4. 用RANSAC拟合(鲁棒回归,排除异常点)
reg = RANSACRegressor().fit(X, y)
# 5. 返回NLF函数
def nlf(x):
return reg.predict(np.array(x).reshape(-1, 1))
return nlf
# 使用示例
# image = load_hdr_gray() # 你的HDR灰度图
# nlf_func = estimate_nlf_wavelet(image)
# sigma_at_100 = nlf_func(100) # 像素值100处的噪声标准差
1. 代码里用了RANSAC回归,它能自动剔除边缘和纹理造成的异常点,比普通最小二乘更鲁棒。
2. 实际应用中,建议把亮度值归一化到[0,1]区间,再送入拟合器,数值稳定性更好。
3. 如果图像是彩色的,我习惯先在YUV空间的Y通道上做NLF估计,因为人眼对亮度噪声最敏感。