传统 ISP Pipeline 详解:从 RAW 图到 RGB 的完整流程
大家好,我是你们的老朋友。今天咱们来聊聊 ISP 里最经典、也是最基础的一条流水线——从 Sensor 出来的 RAW 图,怎么一步步变成我们肉眼看着舒服的 RGB 图。
说实话,我刚入行那会儿,觉得 ISP 就是个黑盒子。RAW 图进去,好看的图片出来,中间发生了什么?完全不知道。后来踩了不少坑,才把这条 Pipeline 里的每个环节摸清楚。
你想想看,Sensor 采集到的原始数据,其实就是一堆电压值。它跟人眼看到的场景,差了十万八千里。所以,我们需要一套流程来“校正”它。这套流程,就是传统 ISP Pipeline。
Pipeline 全景图:我们从哪里开始?
先给你一张总图,看看这六个核心步骤是怎么串起来的。
嗯,这张图你得记在心里。后面每一步,我都会结合项目里的实际案例来讲。
第一步:黑电平校正(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;
}
第二步:去噪(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。
第六步: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,暗部可能只有几个灰度级,看起来就是一片死黑。
总结一下
这六个步骤,环环相扣。前面任何一步出了问题,后面都很难补救。我见过很多新手,一上来就调色彩校正矩阵,结果发现颜色怎么都调不准。最后排查下来,是白平衡没做好。
所以,我的建议是:先把每一步的原理吃透,然后在项目里一步步验证。不要跳过任何一步。