观前提示:抽象和中二的部分都是 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)
,H
和 W
分别是图像的高和宽。是经过预处理的理想 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 上检测是否有饱和像素,如果有,就直接显示白色。
之后,我们将补齐初始版本省略的模块,解决会遇到的各种问题,并逐渐提高图像质量。