2. 噪声模型与参数估计
任务目标
本案例针对RAW域传感器噪声,建立高斯-泊松混合噪声模型,并基于不同ISO下的暗场(暗电流)与均匀光照拍摄数据,估计模型参数。最终输出每个ISO对应的信号依赖噪声系数与固定噪声方差,为后续降噪算法提供参数依据。
噪声模型定义
传感器输出信号\( y \)与真实光电子数\( x \)的关系建模为:
\[ y = x + n_{shot} + n_{read} \]
其中\( n_{shot} \)为散粒噪声(泊松过程近似为信号依赖高斯),\( n_{read} \)为读噪声(固定方差高斯)。合并后总噪声方差为:
\[ \sigma^2(y) = a \cdot y + b \]
参数\( a \)为散粒噪声系数(与ISO增益相关),\( b \)为固定噪声方差。本案例通过多帧静态场景估计\( a \)与\( b \)。
实现代码
import numpy as np
import cv2
import glob
import json
def estimate_noise_params(image_paths, iso_label):
"""
输入: 同一ISO下同一静态场景的多帧RAW图像(16bit, 单通道)
输出: (a, b) 噪声模型参数
"""
frames = []
for p in image_paths:
img = cv2.imread(p, cv2.IMREAD_UNCHANGED)
if img is None:
raise ValueError(f"无法读取图像: {p}")
frames.append(img.astype(np.float64))
frames = np.stack(frames, axis=0)
mean_img = np.mean(frames, axis=0)
# 分块估计: 将图像分为32x32块, 每块内像素近似均匀
h, w = mean_img.shape
bh, bw = 32, 32
a_est, b_est = [], []
for i in range(0, h - bh, bh):
for j in range(0, w - bw, bw):
block_mean = mean_img[i:i+bh, j:j+bw]
block_var = np.var(frames[:, i:i+bh, j:j+bw], axis=0)
# 只使用均值在有效范围内的块(避免过暗或过曝)
valid = (block_mean > 100) & (block_mean < 60000)
if valid.sum() < 100:
continue
x = block_mean[valid].ravel()
y = block_var[valid].ravel()
# 线性拟合: var = a * mean + b
A = np.vstack([x, np.ones_like(x)]).T
coef, _, _, _ = np.linalg.lstsq(A, y, rcond=None)
a_est.append(coef[0])
b_est.append(coef[1])
if len(a_est) == 0:
raise RuntimeError("没有足够的有效块用于参数估计")
# 取中位数作为稳健估计
a = float(np.median(a_est))
b = float(np.median(b_est))
return {'iso': iso_label, 'a': a, 'b': b}
def batch_estimate(config_file):
"""
批量处理多个ISO的噪声参数
config_file: JSON文件, 格式:
{
"ISO100": {"path": "./data/iso100/", "frames": 8},
"ISO400": {"path": "./data/iso400/", "frames": 8}
}
"""
with open(config_file, 'r') as f:
config = json.load(f)
results = []
for iso, info in config.items():
pattern = f"{info['path']}*.tiff"
paths = sorted(glob.glob(pattern))[:info['frames']]
if len(paths) < 3:
print(f"警告: ISO{iso} 有效帧数不足({len(paths)}), 跳过")
continue
params = estimate_noise_params(paths, iso)
results.append(params)
print(f"ISO{iso}: a={params['a']:.4f}, b={params['b']:.2f}")
return results
if __name__ == "__main__":
# 使用示例: 修改config.json指向实际数据路径
results = batch_estimate("config.json")
with open("noise_params.json", "w") as f:
json.dump(results, f, indent=2)
print("参数已保存至 noise_params.json")
数据采集要求
- 暗场数据(可选,用于单独估计b):盖上镜头盖,拍摄8帧以上。
- 均匀光照数据:拍摄均匀白墙或使用积分球光源,确保画面无纹理、无渐变,至少8帧。
- RAW格式:使用dcraw或libraw转换为16bit单通道TIFF,或直接读取DNG的RAW通道。
- 静态场景:相机固定在三脚架上,关闭自动对焦,避免任何运动模糊。
运行方式
# 1. 准备数据目录结构
# data/iso100/0001.tiff, 0002.tiff, ...
# data/iso400/0001.tiff, ...
# 2. 创建config.json
# {
# "100": {"path": "data/iso100/", "frames": 8},
# "400": {"path": "data/iso400/", "frames": 8},
# "800": {"path": "data/iso800/", "frames": 8}
# }
# 3. 运行估计
python estimate_noise.py
验证方法
通过条件:
- 程序正常输出每个ISO的a、b值,无报错。
- 参数合理性检查:a值应随ISO升高而近似线性增大(ISO翻倍,a约翻倍);b值应基本恒定(与ISO无关)。
- 交叉验证:用估计的(a,b)对另一组同ISO图像计算预测噪声方差,与实际方差对比,相对误差应小于20%。
# 验证脚本片段
def verify_params(img_paths, a, b):
frames = [cv2.imread(p, cv2.IMREAD_UNCHANGED).astype(np.float64) for p in img_paths]
frames = np.stack(frames)
mean_img = np.mean(frames, axis=0)
var_img = np.var(frames, axis=0)
pred_var = a * mean_img + b
# 在有效像素范围内计算相对误差
mask = mean_img > 200
rel_err = np.abs(var_img[mask] - pred_var[mask]) / var_img[mask]
return np.median(rel_err)
常见问题
- 块内不均匀:若画面有轻微渐变,可减小块尺寸至16×16,或使用局部均值归一化。
- 异常块:个别块可能因坏点或微振动导致方差异常,中位数估计可有效抵抗。
- 黑电平偏移:若RAW未减去黑电平,需先减去黑电平值(通常为512或1024),否则b值会偏大。