3. ISP 图像信号处理:从 RAW 到人眼可见的魔法

各位同学,欢迎来到第三章。前两章我们聊了相机系统的整体架构和传感器是怎么输出 RAW 数据的。今天,我们进入一个真正「化腐朽为神奇」的环节——ISP 图像信号处理。

说白了,传感器吐出来的 RAW 数据,就像一块未经雕琢的璞玉。它带着各种噪声、偏色、暗角,甚至每个像素只有一种颜色。你想想看,这样的数据直接显示出来,画面能看吗?肯定不能。ISP 就是那个把 RAW 数据变成我们眼中漂亮 JPEG 的幕后英雄。

我个人习惯把 ISP 流程想象成一条流水线。今天我们就沿着这条流水线,逐个工位走一遍。每个工位解决一个问题,最终输出一张让人满意的图像。

核心观点: ISP 不是单一算法,而是一套精心编排的流水线。每个模块的输入输出都有严格的顺序和依赖关系。
ISP 图像信号处理流水线 ISP 核心流水线(从 RAW 到 RGB) RAW 输入 黑电平校正 去噪 自动白平衡 Demosaic Gamma 校正 RGB 输出 说明: 1. 黑电平校正:减去传感器暗电流偏移,恢复真实黑位。 2. 去噪:在保留边缘细节的前提下,去除光子噪声和读出噪声。 3. 自动白平衡:估计场景光源色温,调整 R/G/B 增益使白色物体还原为白色。 4. Demosaic:从 Bayer 模式插值出每个像素的 R/G/B 三通道值。 5. Gamma 校正:对亮度进行非线性映射,适配显示设备的响应特性。

3.1 黑电平校正:找回真正的黑色

先说说黑电平校正。传感器在完全没有光照的情况下,依然会输出一个非零的数值。这个数值就是黑电平,也叫暗电流偏移。为什么会有这个?因为传感器电路本身的热噪声、读出噪声,都会贡献一个底噪。

如果不做校正,你拍出来的黑色物体就不是纯黑,而是发灰的。校正方法很简单:从每个像素值中减去一个固定的黑电平值。

// 黑电平校正伪代码
uint16_t black_level = 64;  // 假设传感器黑电平为 64
for (int i = 0; i < width * height; i++) {
    raw_data[i] = (raw_data[i] > black_level) ? (raw_data[i] - black_level) : 0;
}
小技巧: 黑电平值不是一成不变的。温度升高,暗电流会增大。我建议在传感器模组中集成一个「暗像素行」,实时统计黑电平值,动态校正。

3.2 去噪:在细节和干净之间找平衡

去噪是 ISP 中最棘手的问题之一。噪声来源很多:光子散粒噪声、读出噪声、热噪声……你想想看,弱光环境下,噪声几乎把信号淹没了。

去噪算法有很多种,从简单的均值滤波、高斯滤波,到复杂的双边滤波、非局部均值去噪。我个人在项目中用得最多的是双边滤波。为什么?因为它能在去噪的同时保留边缘细节。

双边滤波的核心思想很简单:

  • 空间域权重: 距离中心像素越近,权重越大。
  • 值域权重: 像素值差异越小,权重越大。

这样一来,平坦区域的噪声被平滑掉,而边缘区域因为像素值差异大,权重小,细节得以保留。

避坑指南: 我曾经在一个项目中,去噪参数调得太猛,结果拍出来的照片像油画一样,细节全没了。后来我学乖了,去噪强度一定要根据 ISO 动态调整。ISO 越高,去噪越强,但也要保留一定的纹理。

3.3 自动白平衡:让白色回归白色

人眼有色彩恒常性,不管在日光灯下还是烛光下,我们看白纸都是白的。但传感器没有这个能力。它忠实地记录场景的光谱分布。如果光源偏黄,拍出来的照片就偏黄;光源偏蓝,照片就偏蓝。

自动白平衡(AWB)的任务就是估计场景光源的色温,然后调整 R、G、B 通道的增益,让白色物体还原为白色。

最经典的算法是灰度世界法。它假设场景中所有颜色的平均值是灰色(R=G=B)。基于这个假设,计算 R 和 B 通道的增益:

// 灰度世界法 AWB 伪代码
float avg_R = mean(red_channel);
float avg_G = mean(green_channel);
float avg_B = mean(blue_channel);

float gain_R = avg_G / avg_R;
float gain_B = avg_G / avg_B;

// 应用增益
for (int i = 0; i < width * height; i++) {
    red_channel[i]   = clamp(red_channel[i]   * gain_R, 0, 255);
    blue_channel[i]  = clamp(blue_channel[i]  * gain_B, 0, 255);
}

当然,灰度世界法在单一颜色场景(比如一片蓝天)下会失效。更鲁棒的做法是结合白点检测法,先找出场景中可能是白色的区域,再基于这些区域计算增益。

AWB 算法 原理 优点 缺点
灰度世界法 假设场景平均为灰色 实现简单,计算快 单一颜色场景失效
白点检测法 检测场景中接近白色的像素 鲁棒性较好 需要阈值调参
色温估计法 基于色温曲线估计光源 精度高,适应性强 需要标定色温曲线

3.4 色彩插值(Demosaic):从马赛克到全彩

还记得我们上一章讲的 Bayer 模式吗?每个像素只采集一种颜色(R、G 或 B)。Demosaic 就是根据相邻像素的颜色信息,插值出每个像素缺失的两种颜色。

最简单的插值方法是双线性插值。比如,要插值一个红色像素的绿色分量,就取它上下左右四个绿色像素的平均值。

但双线性插值有个问题——容易产生伪彩色和锯齿。我在项目中更常用的是自适应插值算法。它会根据边缘方向选择插值方向,避免跨过边缘插值。

// 自适应 Demosaic 伪代码(简化版)
// 假设当前像素是 R 像素,需要插值 G 分量
// 计算水平和垂直方向的梯度
float grad_h = abs(G_left - G_right);
float grad_v = abs(G_up - G_down);

if (grad_h < grad_v) {
    // 水平方向梯度小,沿水平方向插值
    G_interp = (G_left + G_right) / 2;
} else if (grad_v < grad_h) {
    // 垂直方向梯度小,沿垂直方向插值
    G_interp = (G_up + G_down) / 2;
} else {
    // 梯度相近,取四个方向平均
    G_interp = (G_left + G_right + G_up + G_down) / 4;
}
关键点: Demosaic 的质量直接影响最终图像的清晰度和色彩准确度。一个好的 Demosaic 算法,能有效减少伪彩色和摩尔纹。

3.5 Gamma 校正:适配人眼的非线性感知

最后一步是 Gamma 校正。人眼对暗部细节的敏感度远高于亮部。换句话说,人眼对亮度的感知是非线性的。而传感器和显示设备是线性的。

如果不做 Gamma 校正,直接显示线性数据,暗部会显得太暗,亮部会显得太亮。Gamma 校正通过一个幂函数,将线性亮度映射到非线性空间,让图像看起来更自然。

标准 Gamma 曲线公式:

// Gamma 校正伪代码
float gamma = 1.0 / 2.2;  // 标准 Gamma 值
for (int i = 0; i < width * height; i++) {
    // 先归一化到 [0, 1]
    float normalized = pixel[i] / 255.0;
    // 应用 Gamma 校正
    float corrected = pow(normalized, gamma);
    // 映射回 [0, 255]
    pixel[i] = (uint8_t)(corrected * 255.0);
}

这里有个细节要注意:Gamma 校正通常是在 RGB 空间做的,而且每个通道独立进行。有些系统还会在 Gamma 校正之前做色彩校正矩阵(CCM),把传感器 RGB 空间转换到标准 RGB 空间。

经验之谈: 不同的显示设备有不同的 Gamma 值。sRGB 标准是 2.2,但苹果的 Display P3 是 2.2 的变体。我建议在 ISP 中保留可配置的 Gamma 曲线,方便适配不同屏幕。

小结

好了,这一章我们走完了 ISP 的核心流水线。从黑电平校正到 Gamma 校正,每一步都在解决一个具体的问题。你想想看,如果没有这些处理,RAW 数据直接显示出来会是什么样子?偏色、噪声、马赛克、暗部死黑……

在实际项目中,ISP 的调优是一个反复迭代的过程。我曾经为了一个低光场景的去噪参数,连续调了一周。每次改一个参数,拍一组照片,对比效果。嗯,这就是嵌入式视觉工程师的日常。