4、RAW域降噪基础:从Bayer到噪声特性

各位同学,欢迎来到RAW域降噪的第一课。说实话,很多做ISP的工程师在RAW域待的时间并不长,因为大部分算法都在YUV域或者RGB域处理。但我个人的经验是——降噪这件事,越早做效果越好。RAW域就是最早的战场。

这一章我们不讲复杂的滤波公式,先把RAW域的底子打牢。你只有搞懂了Bayer Pattern、黑电平校正、LSC这些前置模块对噪声的影响,才能真正理解为什么RAW域降噪这么特殊。

4.1 RAW图格式:Bayer Pattern

传感器输出的原始数据,说白了就是一个马赛克图像。每个像素只记录一种颜色——R、G或者B。这就是经典的Bayer Pattern。

最常见的格式是RGGB,也就是左上角是R,然后G、G、B交替排列。为什么G有两个?因为人眼对绿色最敏感,传感器就多采了一半的绿色信息。

Bayer类型 排列方式 常见场景
RGGB 第一行 R G R G...
第二行 G B G B...
最主流,高通、索尼传感器
BGGR 第一行 B G B G...
第二行 G R G R...
部分三星传感器
GRBG 第一行 G R G R...
第二行 B G B G...
少数OmniVision方案

我在项目里遇到过最坑的事——拿到一个RAW图,默认当成RGGB去解,结果颜色全乱套了。后来才意识到,那个传感器用的是BGGR。所以第一件事:确认Bayer顺序,别想当然。

小技巧: 拿到陌生RAW图,先看绿色通道的均值。如果G通道均值明显高于R和B,说明Bayer顺序大概率是对的。如果G通道反而偏低,那就要怀疑顺序了。

4.2 黑电平校正(BLC)对噪声的影响

黑电平校正,英文叫Black Level Correction。为什么要做这个?因为传感器在完全无光的情况下,输出的也不是0,而是一个偏置值。这个偏置就是黑电平。

BLC的公式很简单:

I_corrected = I_raw - black_level

但这里有个坑——黑电平本身也是有噪声的。你减去一个带噪声的偏置,相当于把噪声也带进来了。

我记得有一次调试夜景模式,发现暗部噪声特别大。排查了半天,最后发现是BLC的black_level估计不准,导致暗部被过度拉伸,噪声放大了好几倍。

避坑指南: 我曾经在量产项目中遇到过——BLC的black_level是固定值,但不同增益下黑电平其实会漂移。结果低增益没问题,高增益下暗部噪声爆炸。后来改成动态BLC,根据增益查表补偿,问题才解决。

BLC对噪声的影响主要体现在两点:

  • 偏置误差放大噪声:black_level偏大,暗部被抬升,噪声方差跟着变大
  • 截断效应:减去黑电平后,小于0的值被截成0,丢失了暗部细节

4.3 LSC对噪声的影响

Lens Shading Correction,镜头阴影校正。说白了就是镜头边缘进光量少,图像四周偏暗,需要把四周像素乘上一个增益系数来补偿。

LSC的公式:

I_corrected(x,y) = I_raw(x,y) * gain(x,y)

问题来了——增益是乘性的。边缘区域的增益可能达到2倍甚至3倍。噪声也跟着被放大了。

你想想看,中心区域增益是1.0,噪声不变。边缘区域增益是2.0,噪声方差直接变成4倍(因为噪声方差与增益平方成正比)。这就是为什么很多手机拍出来的照片,四周噪点明显比中心多。

区域 LSC增益 噪声放大倍数 实际影响
中心 1.0x 1x 噪声正常
中间环 1.5x 2.25x 噪声开始明显
边缘 2.0x 4x 噪声爆炸
关键认知: RAW域降噪必须考虑LSC增益的空间变化。你不能在全图用同一个降噪强度,否则边缘要么过平滑,要么欠平滑。我习惯的做法是——把LSC增益图作为降噪强度的调制因子,增益高的区域降噪强度也相应提高。

4.4 RAW域噪声特性分析

好了,前面铺垫了这么多,现在进入核心——RAW域的噪声到底长什么样?

简单说,RAW域的噪声是信号相关的。什么意思?就是亮的地方噪声大,暗的地方噪声小。这跟YUV域不一样,YUV域经过gamma校正后,噪声特性已经被扭曲了。

RAW域噪声模型通常用泊松-高斯混合模型来描述:

I_noisy = I_clean + n_shot(I_clean) + n_read

其中:
n_shot ~ Poisson(λ = I_clean)  → 散粒噪声,与信号相关
n_read ~ Gaussian(0, σ_read)   → 读出噪声,与信号无关

散粒噪声是光子计数的统计特性决定的。光子到达传感器是随机的,服从泊松分布。信号越强,光子数越多,噪声方差也越大。但注意——信噪比是随着信号增强而提高的,因为SNR = I / sqrt(I) = sqrt(I)。

读出噪声则是传感器电路引入的,跟信号大小没关系。在暗部,读出噪声占主导;在亮部,散粒噪声占主导。

工程经验: 我一般用两个参数来标定传感器的噪声模型——σ_read(读出噪声标准差)和K(散粒噪声系数)。K值通常在0.5~1.0之间,单位是e-/DN。有了这两个参数,你就可以在RAW域做精确的噪声估计了。

还有一个容易被忽略的点——Bayer通道的噪声特性不同。G通道的量子效率通常比R和B高,所以G通道的散粒噪声系数K会小一些。这意味着G通道的信噪比天生就比R/B好。降噪时,R/B通道需要更强的处理。

下面这张图总结了RAW域噪声的完整链路:

RAW域噪声链路与影响因素 传感器输出 散粒噪声 读出噪声 RAW图 BLC 偏置误差放大噪声 暗部截断效应 LSC 增益 放大 信号相关噪声 通道差异(G vs R/B) 空间非均匀性(LSC) 核心结论: RAW域噪声 = 散粒噪声(信号相关)+ 读出噪声(信号无关),受BLC偏置误差和LSC增益放大影响,且不同Bayer通道噪声水平不同。

4.5 噪声参数标定:工程落地第一步

理论说完了,来点实际的。你在项目里怎么拿到噪声参数?

我常用的方法是——拍灰卡。具体步骤:

  1. 用均匀光源照射灰卡,拍多张RAW图
  2. 取多帧的均值作为clean信号
  3. 计算每帧与均值的差值,得到噪声残差
  4. 对不同亮度区域统计噪声方差
  5. 拟合方差-信号曲线,得到σ_read和K
# 伪代码示例:噪声参数标定
def calibrate_noise(raw_images):
    # raw_images: list of RAW frames
    mean_img = np.mean(raw_images, axis=0)
    residual = raw_images - mean_img
    
    # 按信号强度分bin
    bins = [0, 256, 512, 1024, 2048, 4096]
    noise_var = []
    signal_level = []
    
    for i in range(len(bins)-1):
        mask = (mean_img >= bins[i]) & (mean_img < bins[i+1])
        noise_var.append(np.var(residual[mask]))
        signal_level.append((bins[i] + bins[i+1]) / 2)
    
    # 拟合: noise_var = K * signal + σ_read^2
    # 用最小二乘法
    K, sigma_read_sq = np.polyfit(signal_level, noise_var, 1)
    return K, np.sqrt(sigma_read_sq)
注意: 标定时一定要关掉所有ISP处理——BLC、LSC、去噪、gamma全部bypass。否则你标定出来的噪声参数是假的,后面降噪算法会出问题。我曾经吃过这个亏,标定完发现噪声模型跟实际对不上,排查了两天才发现是LSC没关。

好了,这一章的内容就是这些。RAW域的噪声特性,说白了就是三个关键词:信号相关、通道差异、空间非均匀。搞懂这三点,你后面设计降噪算法就有了方向。