3. ISP 图像信号处理:从 RAW 到人眼可见的魔法
各位同学,欢迎来到第三章。前两章我们聊了相机系统的整体架构和传感器是怎么输出 RAW 数据的。今天,我们进入一个真正「化腐朽为神奇」的环节——ISP 图像信号处理。
说白了,传感器吐出来的 RAW 数据,就像一块未经雕琢的璞玉。它带着各种噪声、偏色、暗角,甚至每个像素只有一种颜色。你想想看,这样的数据直接显示出来,画面能看吗?肯定不能。ISP 就是那个把 RAW 数据变成我们眼中漂亮 JPEG 的幕后英雄。
我个人习惯把 ISP 流程想象成一条流水线。今天我们就沿着这条流水线,逐个工位走一遍。每个工位解决一个问题,最终输出一张让人满意的图像。
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 中最棘手的问题之一。噪声来源很多:光子散粒噪声、读出噪声、热噪声……你想想看,弱光环境下,噪声几乎把信号淹没了。
去噪算法有很多种,从简单的均值滤波、高斯滤波,到复杂的双边滤波、非局部均值去噪。我个人在项目中用得最多的是双边滤波。为什么?因为它能在去噪的同时保留边缘细节。
双边滤波的核心思想很简单:
- 空间域权重: 距离中心像素越近,权重越大。
- 值域权重: 像素值差异越小,权重越大。
这样一来,平坦区域的噪声被平滑掉,而边缘区域因为像素值差异大,权重小,细节得以保留。
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;
}
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 空间。
小结
好了,这一章我们走完了 ISP 的核心流水线。从黑电平校正到 Gamma 校正,每一步都在解决一个具体的问题。你想想看,如果没有这些处理,RAW 数据直接显示出来会是什么样子?偏色、噪声、马赛克、暗部死黑……
在实际项目中,ISP 的调优是一个反复迭代的过程。我曾经为了一个低光场景的去噪参数,连续调了一周。每次改一个参数,拍一组照片,对比效果。嗯,这就是嵌入式视觉工程师的日常。