传统 ISP Pipeline 详解:从 RAW 到 YUV 的三大域处理
各位同学,今天我们来啃 ISP Pipeline 里最核心的一段——从 RAW 域到 YUV 域的完整处理链路。我做了这么多年图像处理,可以负责任地说:这部分是基本功,也是区分工程师水平的分水岭。
先看一张整体流程图,把今天要讲的三个域串起来。
RAW 域处理:传感器原始数据的第一次整形
传感器出来的 RAW 数据,说白了就是一堆电荷值。每个像素只记录一种颜色——R、G 或 B,按 Bayer 模式排列。这个阶段的数据最原始,也最脆弱。
黑电平校正
先说说黑电平。传感器在完全无光的情况下,输出并不是 0。为什么?因为电路本身有暗电流,还有 ADC 的偏移。我见过不少新手直接拿 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 域最关键的步骤。每个像素只有一种颜色,需要插值出另外两种。常用的方法有:
- 双线性插值——简单,但边缘模糊严重
- 边缘导向插值——先检测边缘方向,沿边缘插值
- 自适应插值——根据局部纹理复杂度选择插值策略
我推荐用自适应插值。虽然计算量大一点,但效果提升明显。下面是一个简化版的边缘导向插值思路:
// 边缘导向去马赛克(绿色通道示例)
// 假设当前像素是 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;
}
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;
}
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 算子——一阶微分,提取边缘方向
降噪
YUV 域的降噪和 RAW 域不同。这里噪声已经被 Gamma 校正改变了分布,不再是高斯噪声。我推荐用:
- Y 通道——用双边滤波,保边去噪
- UV 通道——用均值滤波或中值滤波,因为色度噪声容忍度高
压缩
最后一步是压缩。YUV 4:4:4 转 4:2:0,色度通道下采样。这是 JPEG/H.264 等编码标准的要求。
下采样方法:
| 方法 | 描述 | 质量 |
|---|---|---|
| 最近邻 | 直接取左上角像素 | 差,有锯齿 |
| 双线性 | 2x2 块取平均 | 中等 |
| Lanczos | 使用 sinc 函数插值 | 好,但计算量大 |
好了,传统 ISP Pipeline 的三大域处理就讲到这里。从 RAW 到 YUV,每一步都有它的道理。你想想看,如果黑电平没校正好,后面的白平衡、颜色校正全都会偏。这就是为什么我强调——基础要打牢。