传统 ISP Pipeline 详解:从 RAW 到 YUV 的三大域处理

各位同学,今天我们来啃 ISP Pipeline 里最核心的一段——从 RAW 域到 YUV 域的完整处理链路。我做了这么多年图像处理,可以负责任地说:这部分是基本功,也是区分工程师水平的分水岭。

先看一张整体流程图,把今天要讲的三个域串起来。

传统 ISP Pipeline 三大域处理流程 RAW 域处理 黑电平校正 去噪 去马赛克 输入:Bayer RAW 输出:RGB RGB 域处理 白平衡 颜色校正 Gamma 校正 输入:RGB 输出:sRGB YUV 域处理 边缘增强 降噪 压缩 输入:sRGB 输出:YUV 4:2:0 每个域处理的数据格式不同,算法设计思路也完全不同。千万别混着用!

RAW 域处理:传感器原始数据的第一次整形

传感器出来的 RAW 数据,说白了就是一堆电荷值。每个像素只记录一种颜色——R、G 或 B,按 Bayer 模式排列。这个阶段的数据最原始,也最脆弱。

黑电平校正

先说说黑电平。传感器在完全无光的情况下,输出并不是 0。为什么?因为电路本身有暗电流,还有 ADC 的偏移。我见过不少新手直接拿 RAW 数据做处理,结果暗部全是偏绿的。

我的习惯做法:先采集全黑帧(盖上镜头盖),取平均得到黑电平值。然后每帧 RAW 数据都减去这个值。注意,不同增益下的黑电平可能不同,最好做增益补偿。

校正公式很简单:

// 黑电平校正示例
uint16_t black_level = 64;  // 10-bit RAW 典型值
for (int i = 0; i < frame_size; i++) {
    raw_data[i] = (raw_data[i] > black_level) ? 
                  (raw_data[i] - black_level) : 0;
}

去噪

RAW 域的去噪和后面 YUV 域的去噪完全不同。这里噪声主要是光子散粒噪声和读出噪声,而且和信号强度相关。我个人偏好用双边滤波或者非局部均值,但要注意保边。

踩坑提醒:我曾经在一个项目中,RAW 域去噪力度太大,结果去马赛克后出现了严重的伪彩色。后来我学乖了——RAW 域去噪只做轻度处理,重度的留给 YUV 域。

去马赛克

这是 RAW 域最关键的步骤。每个像素只有一种颜色,需要插值出另外两种。常用的方法有:

  • 双线性插值——简单,但边缘模糊严重
  • 边缘导向插值——先检测边缘方向,沿边缘插值
  • 自适应插值——根据局部纹理复杂度选择插值策略

我推荐用自适应插值。虽然计算量大一点,但效果提升明显。下面是一个简化版的边缘导向插值思路:

// 边缘导向去马赛克(绿色通道示例)
// 假设当前像素是 R 位置,需要插值 G
int grad_h = abs(G_left - G_right);
int grad_v = abs(G_up - G_down);
if (grad_h < grad_v) {
    G_out = (G_left + G_right) / 2;  // 水平边缘
} else if (grad_v < grad_h) {
    G_out = (G_up + G_down) / 2;     // 垂直边缘
} else {
    G_out = (G_left + G_right + G_up + G_down) / 4;
}

RGB 域处理:让颜色回归真实

去马赛克之后,我们有了完整的 RGB 图像。但这时候的颜色还不对——受光源影响,整体偏色。RGB 域就是做颜色还原的。

白平衡

白平衡的核心假设:场景中白色物体在 R、G、B 三个通道的响应应该相等。但实际不是,因为光源色温不同。

常用的方法:

方法 原理 适用场景
灰度世界法 假设场景平均色为灰色 色彩丰富的场景
完美反射法 取最亮像素作为白色参考 有高光区域的场景
色温估计法 根据色温曲线查表 已知光源类型
实战经验:我一般用灰度世界法做初调,再用色温估计法做精调。两个方法互补,效果比单一方法好很多。

颜色校正

白平衡之后,颜色还是不准。因为传感器的色彩响应和人眼不同。需要用颜色校正矩阵(CCM)把传感器 RGB 映射到标准 RGB。

CCM 通常是一个 3x3 矩阵:

// 颜色校正矩阵示例
float ccm[3][3] = {
    {1.8, -0.5, -0.3},
    {-0.2, 1.6, -0.4},
    {-0.1, -0.3, 1.4}
};

for (int i = 0; i < height * width; i++) {
    float r = rgb[i].r, g = rgb[i].g, b = rgb[i].b;
    rgb[i].r = ccm[0][0]*r + ccm[0][1]*g + ccm[0][2]*b;
    rgb[i].g = ccm[1][0]*r + ccm[1][1]*g + ccm[1][2]*b;
    rgb[i].b = ccm[2][0]*r + ccm[2][1]*g + ccm[2][2]*b;
}
注意:CCM 的系数不能太大,否则会放大噪声。我一般限制每个系数值不超过 2.0。另外,不同色温下 CCM 不同,最好做色温分段。

Gamma 校正

Gamma 校正解决的是显示设备非线性响应的问题。人眼对暗部变化敏感,对亮部变化不敏感。所以我们要对暗部做拉伸,亮部做压缩。

标准 Gamma 曲线:

// Gamma 校正(Gamma = 2.2)
float gamma = 1.0 / 2.2;
for (int i = 0; i < frame_size; i++) {
    rgb[i].r = pow(rgb[i].r / 255.0, gamma) * 255.0;
    rgb[i].g = pow(rgb[i].g / 255.0, gamma) * 255.0;
    rgb[i].b = pow(rgb[i].b / 255.0, gamma) * 255.0;
}
小技巧:实际工程中不用 pow 函数,太慢。用查找表(LUT)替代,256 个元素的表就够了。我一般提前算好,运行时直接查表。

YUV 域处理:为显示和压缩做准备

Gamma 校正之后,图像看起来正常了。但人眼对亮度更敏感,对颜色不敏感。所以我们要转到 YUV 域,把亮度和色度分开处理。

色彩空间转换

从 sRGB 到 YUV 的转换:

// sRGB 到 YUV 转换(BT.601 标准)
Y =  0.299 * R + 0.587 * G + 0.114 * B;
U = -0.147 * R - 0.289 * G + 0.436 * B + 128;
V =  0.615 * R - 0.515 * G - 0.100 * B + 128;

边缘增强

只在 Y 通道做边缘增强。为什么?因为人眼对亮度细节敏感,对色度细节不敏感。常用的方法:

  • Unsharp Mask——原图减去模糊图,得到边缘,再加回去
  • Laplacian 算子——二阶微分,增强边缘
  • Sobel 算子——一阶微分,提取边缘方向
我的经验:Unsharp Mask 最稳定。参数控制好,不容易出现振铃效应。我一般用 3x3 高斯核做模糊,增强系数 0.5~1.0。

降噪

YUV 域的降噪和 RAW 域不同。这里噪声已经被 Gamma 校正改变了分布,不再是高斯噪声。我推荐用:

  • Y 通道——用双边滤波,保边去噪
  • UV 通道——用均值滤波或中值滤波,因为色度噪声容忍度高
避坑:UV 通道千万别用太强的滤波,否则颜色会糊成一团。我曾经吃过这个亏,后来 UV 通道的滤波半径控制在 3x3 以内。

压缩

最后一步是压缩。YUV 4:4:4 转 4:2:0,色度通道下采样。这是 JPEG/H.264 等编码标准的要求。

下采样方法:

方法 描述 质量
最近邻 直接取左上角像素 差,有锯齿
双线性 2x2 块取平均 中等
Lanczos 使用 sinc 函数插值 好,但计算量大
建议:移动端用双线性就够了。PC 端可以用 Lanczos。我一般用双线性,速度快,效果也能接受。

好了,传统 ISP Pipeline 的三大域处理就讲到这里。从 RAW 到 YUV,每一步都有它的道理。你想想看,如果黑电平没校正好,后面的白平衡、颜色校正全都会偏。这就是为什么我强调——基础要打牢。