传统 ISP Pipeline 详解:从 RAW 图到 RGB 的完整流程

大家好,我是你们的老朋友。今天咱们来聊聊 ISP 里最经典、也是最基础的一条流水线——从 Sensor 出来的 RAW 图,怎么一步步变成我们肉眼看着舒服的 RGB 图。

说实话,我刚入行那会儿,觉得 ISP 就是个黑盒子。RAW 图进去,好看的图片出来,中间发生了什么?完全不知道。后来踩了不少坑,才把这条 Pipeline 里的每个环节摸清楚。

你想想看,Sensor 采集到的原始数据,其实就是一堆电压值。它跟人眼看到的场景,差了十万八千里。所以,我们需要一套流程来“校正”它。这套流程,就是传统 ISP Pipeline。

核心观点: 传统 ISP Pipeline 的本质,是一个“确定性”的校正过程。每一步都有明确的物理意义和数学模型。理解它,是理解 AI ISP 的基础。

Pipeline 全景图:我们从哪里开始?

先给你一张总图,看看这六个核心步骤是怎么串起来的。

传统 ISP Pipeline 流程图 传统 ISP Pipeline 核心流程 RAW 输入 黑电平校正 去噪 去马赛克 白平衡 色彩校正 Gamma 校正 RGB 输出 说明:去马赛克之后,数据从 Bayer 格式变为 RGB 格式。白平衡、色彩校正、Gamma 校正都是在 RGB 域进行的。

嗯,这张图你得记在心里。后面每一步,我都会结合项目里的实际案例来讲。

第一步:黑电平校正(Black Level Correction)

Sensor 在完全无光的情况下,像素点也会有电压输出。这个输出值,就是黑电平。说白了,它是个“底噪”。

如果不校正,你拍出来的黑色物体,会发灰。暗部细节也会被淹没。

校正公式很简单:

// 黑电平校正
// 假设 sensor 的黑电平是 64(12-bit RAW 数据范围 0-4095)
uint16_t black_level = 64;
for (int i = 0; i < width * height; i++) {
    raw_data[i] = (raw_data[i] > black_level) ? (raw_data[i] - black_level) : 0;
}
我的经验: 不同 Sensor 的黑电平可能不一样,甚至同一个 Sensor 在不同增益下黑电平也会漂移。我曾经遇到过一个项目,暗光下黑电平从 64 漂到了 72,结果整张图偏紫。排查了两天才找到原因。所以,黑电平一定要动态校准。

第二步:去噪(Denoise)

Sensor 的噪声来源很多:热噪声、读出噪声、光子散粒噪声…… 尤其是暗光下,噪声会非常明显。

传统 ISP 里,去噪主要分两步:

  • 空域去噪: 比如双边滤波、非局部均值滤波。我习惯在 RAW 域做一次轻度的空域去噪,因为 RAW 域的噪声模型相对简单。
  • 时域去噪: 利用多帧图像的信息。比如三帧降噪,前后两帧做运动补偿,然后加权平均。

这里有个坑:去噪太强,细节就没了。去噪太弱,图像又没法看。怎么平衡?

我个人的习惯是:先做一次边缘检测,只在平坦区域做强去噪,边缘区域保留细节。这个策略,在大多数场景下都管用。

第三步:去马赛克(Demosaic)

Sensor 的每个像素只能感知一种颜色(R、G、B)。常见的 Bayer 模式是 RGGB。去马赛克,就是通过插值,把每个像素缺失的另外两种颜色算出来。

最简单的插值方法是双线性插值。但效果嘛…… 边缘会有明显的锯齿和伪色。

稍微好一点的是方向性插值。比如,先判断当前像素是水平边缘还是垂直边缘,然后沿着边缘方向插值。

// 伪代码:方向性插值
// 假设当前像素是 G 通道,需要插值 R 和 B
// 1. 计算水平和垂直方向的梯度
float grad_h = abs(G(x-1,y) - G(x+1,y));
float grad_v = abs(G(x,y-1) - G(x,y+1));

// 2. 选择梯度小的方向进行插值
if (grad_h < grad_v) {
    // 水平方向插值
    R(x,y) = (R(x-1,y) + R(x+1,y)) / 2;
} else {
    // 垂直方向插值
    R(x,y) = (R(x,y-1) + R(x,y+1)) / 2;
}
注意: 去马赛克的质量,直接决定了后续所有色彩处理的精度。如果这一步产生了伪色,后面的白平衡和色彩校正再怎么调,也救不回来。我曾经在一个项目中,因为去马赛克算法没选好,导致红色文字边缘出现了绿色锯齿,被客户投诉了三次。

第四步:白平衡(White Balance)

人眼有“色彩恒常性”。不管在日光下还是白炽灯下,我们看一张白纸,都觉得它是白的。但 Sensor 没有这个能力。它只会忠实地记录光谱。

所以,我们需要通过白平衡,把不同色温下的白色物体,校正回标准的白色。

常用的方法是“灰度世界”假设:认为一张图片里,所有颜色的平均值是灰色(R=G=B)。

计算增益:

// 灰度世界白平衡
float avg_r = mean(R_channel);
float avg_g = mean(G_channel);
float avg_b = mean(B_channel);

float gain_r = avg_g / avg_r;
float gain_b = avg_g / avg_b;

// 应用增益
R_out = R_in * gain_r;
B_out = B_in * gain_b;

当然,还有更高级的方法,比如“完美反射”假设、色温估计法。我个人在项目中用得最多的是色温估计法,因为它对复杂光源的适应性更好。

第五步:色彩校正(Color Correction)

Sensor 的光谱响应和人眼不一样。所以,即使白平衡做对了,颜色还是偏的。比如,红色可能偏橙,蓝色可能偏紫。

色彩校正,就是用一个 3x3 的矩阵,把 Sensor 的色彩空间映射到标准色彩空间(比如 sRGB)。

步骤 输入 操作 输出
1 R_sensor, G_sensor, B_sensor 乘以 3x3 色彩校正矩阵 (CCM) R_linear, G_linear, B_linear
2 R_linear, G_linear, B_linear 钳位到 [0, 1] 范围 R_clamped, G_clamped, B_clamped

这个矩阵怎么来的?通常是用色卡(比如 X-Rite ColorChecker)标定出来的。你拍一张色卡图,然后解一个最小二乘问题,就能算出最优的 CCM。

关键点: CCM 是跟光源相关的。日光下的 CCM,在荧光灯下可能就不准了。所以,有些 ISP 会准备多组 CCM,根据色温自动切换。

第六步:Gamma 校正(Gamma Correction)

人眼对暗部的变化更敏感,对亮部的变化不敏感。但 Sensor 是线性的。所以,我们需要做 Gamma 校正,把线性数据映射到人眼的感知空间。

标准 Gamma 曲线:

// Gamma 校正 (Gamma = 2.2)
float gamma = 1.0 / 2.2;
for (int i = 0; i < width * height; i++) {
    // 先归一化到 [0, 1]
    float normalized = rgb_data[i] / 255.0f;
    // 应用 Gamma
    float corrected = pow(normalized, gamma);
    // 反归一化
    rgb_data[i] = (uint8_t)(corrected * 255.0f);
}

Gamma 校正还有一个好处:它能在 8-bit 的量化精度下,保留更多的暗部细节。如果不做 Gamma,暗部可能只有几个灰度级,看起来就是一片死黑。

我的建议: 不要直接用标准的 Gamma 2.2。不同显示设备的 Gamma 值不一样。我一般会在最后输出前,根据目标显示设备做一次 Gamma 映射。比如,手机屏幕的 Gamma 通常在 2.2 左右,但专业显示器可能是 2.4。

总结一下

这六个步骤,环环相扣。前面任何一步出了问题,后面都很难补救。我见过很多新手,一上来就调色彩校正矩阵,结果发现颜色怎么都调不准。最后排查下来,是白平衡没做好。

所以,我的建议是:先把每一步的原理吃透,然后在项目里一步步验证。不要跳过任何一步。