观前提示:抽象和中二的部分都是 DeepSeek R1 写的,它还有更多更离谱的。

  • 《ISP锻造入门:从零开始铸造你的「原神之眼」RAW处理器》
  • 《博士,你甚至不肯叫我一声ISP:三原色与勇者傻瓜套装》
  • 《像素工程师修炼Day1:如何让RAW的野生RGB臣服于人类の色彩暴政》

§0. 序章:勇者的觉醒——在像素荒原上捡到一本《ISP入门指南》

ISP(Image Signal Processor)负责将传感器输出的 RAW 图像转换为在屏幕上显示的图像。通常涉及各种颜色空间的转换,处理和映射。

这个系列将从最基础的 ISP 开始,逐渐加入模块来解决遇到的问题,提高图像质量。

接下来将实现一个最基本的两步 ISP,获得初始武器。

§1. 理想的 RAW 图像

理想的起点是范围在 0-1 之间,0 代表没有输入,1 代表传感器的饱和值的三通道图像。但相机吐出的 RAW 图像是有黑电平补偿、没有解拜尔阵列、经过相机厂商编码的私有格式。

好在已经有很多开源工具能够帮我们完成这些预处理,比如 dcraw、libraw 等。Rawpy 是一个 Python 包装的 LibRaw,使用下面的代码可以将 RAW 图像读取为一个比较理想的 numpy 数组:

def read_raw_image(path):
    with rawpy.imread(path) as raw:
        rgb = raw.postprocess(
            gamma=(1, 1),
            output_bps=16,
            use_auto_wb=False,
            use_camera_wb=False,
            user_wb=[1, 1, 1, 1],
            output_color=rawpy.ColorSpace.raw,
            no_auto_bright=True,
            half_size=True,
        )
    rgb = rgb / 65535.0
    return rgb

这里的 rgb 是一个三维的 numpy 数组,形状是 (H, W, 3)HW 分别是图像的高和宽。是经过预处理的理想 RAW 图像。

如果直接编码成图像,得到的就是“原图”。

§2. 从RAW RGB到XYZ

请看前传:颜色空间转换:RAW 与 XYZ

CCM(Color Correction Matrix)是一个 3x3 的矩阵,用于将 RAW RGB 转换为 XYZ。

ccm = np.array(
    [[1.297, 0.558, 0.0596], 
    [0.0793, 0.569, -0.1675], 
    [0.1033, -0.1577, 1.2465]]
)
cameraRGB_2D = cameraRGB.reshape(-1, 3)
XYZ_2D = np.dot(cameraRGB_2D, ccm)
XYZ = XYZ_2D.reshape(cameraRGB.shape)

此处两步 reshape 是为了进行矩阵乘法,如果之后还需要进行其他操作,可以暂时保留向量形态。

此时得到的 XYZ 是对拍摄环境中三刺激值的估计,于是,从相机各自不同的光谱响应转换到了一个统一的颜色空间。这里的操作并不考虑三刺激值的绝对值,如果要对整体亮度调整,在 XYZ 上进行操作是比较合理的,比如乘上一个系数来模拟曝光补偿。

§3. 从XYZ 到 sRGB

请看前传:颜色空间转换:XYZ 与 sRGB

M_XYZ2sRGB = np.array(
    [[3.2406, -1.5372, -0.4986],
     [-0.9689, 1.8758, 0.0415],
     [0.0557, -0.2040, 1.0570]]
)
sRGB_linear_2D = np.dot(XYZ_2D, M_XYZ2sRGB.T)
sRGB_linear = sRGB_linear_2D.reshape(cameraRGB.shape)
sRGB_linear_clipped = np.clip(sRGB_linear, 0, 1)
sRGB = np.where(
    sRGB_linear_clipped <= 0.0031308,
    12.92 * sRGB_linear_clipped,
    1.055 * np.power(sRGB_linear_clipped, 1 / 2.4) - 0.055,
)

这里的操作包括色彩空间转换和 OETF(光电传递函数),将 XYZ 转换为 sRGB 空间。sRGB 是一个 0-1 之间的三通道图像,可以直接显示在屏幕上。注意在应用 OETF 之前,需要将线性空间的 sRGB_linear 限制在 0-1 之间,这一步实际上是将超出色域的颜色直接裁切,保证色域内的颜色绝对再现,是一种最简单的色域映射方法。

§4. 初始武器锻造报告

至此,我们已经完成了一个最基础的 ISP,虽然简单,重要的是它每一步都有色彩科学的理论支撑

为了展示这个 ISP 有多脆弱,我们点亮这盏灯,就能遇到的第一个问题。

高光溢出:当超出传感器的饱和值,传感器将其记录为(1, 1, 1),这样的像素在经过初始版本的 ISP 处理后,变成(1, 0.8, 1),呈现粉色。一种简单粗暴的解决办法是在 cameraRGB 上检测是否有饱和像素,如果有,就直接显示白色。

之后,我们将补齐初始版本省略的模块,解决会遇到的各种问题,并逐渐提高图像质量。