Skip to content

RAW显影流程:从传感器到屏幕 ​

前面的章节介绍了光学成像、曝光控制与传感器。这一节继续追问:传感器记录的数字,怎样变成屏幕上的颜色?

本文以 ARW 数字图像处理教程为线索。rawpy 负责解码文件,NumPy 用来展示像素运算的原理。代码片段按处理环节组织,重点是说明各个数组代表什么、为什么要进行下一步,而不是复刻相机厂商的完整显影算法。

本文配图来自实际 ARW 运算

所有场景图均由 DSC00178.ARW 实际处理生成。原始画幅为 7168×5120,有效区域为 7008×4672;白平衡、去马赛克和颜色变换在完整有效区域上计算,配图才缩小。

全套 13 张 WebP 合计约 748 KB,单张约 29~80 KB,最长边不超过 1440 像素。图片与本文位于同一目录,均带有 sRGB 配置;图片内用英文标注,中文说明放在图下。

中间数据不一定可以直接显示:图注会注明显示增益、开平方拉伸或 sRGB 编码。去马赛克小图是局部放大示意,其他场景图保留完整原始画幅或完整有效画幅。

先分清四种“RGB” ​

同样写成三个数字,并不意味着它们表示相同的颜色。

数据含义能否直接当普通图片显示
Bayer RAW每个位置只有一种滤光通道的传感器读数可以看采样强度,尚无完整彩色像素
相机 RGB去马赛克得到的三个相机响应通道不应直接当作标准 sRGB
线性 sRGB已转换到 sRGB 原色定义下、尚未编码的线性值直接交给普通图片显示接口通常显得偏暗
编码后的 sRGB经 sRGB 传递函数编码、用于保存与交换的数值可以按 sRGB 图片解释

白平衡、颜色空间转换、色调映射、显示编码是不同的操作。 它们虽然都能改变最终画面的观感,却不能互相替代。

本文的教学流程为:

实际显影器可能调整部分步骤的顺序,还会加入坏点处理、降噪、镜头校正、锐化和高光重建。

传感器记录的不是“颜色名称” ​

光谱经过滤光片,变成通道响应 ​

设物体到达相机的光谱为 L(λ),某个通道的综合光谱响应为 Sk(λ),则其信号可以近似写成:

Ck∝t∫L(λ)Sk(λ)dλ,

其中 t 表示曝光时间。对反射物体,在简化条件下可把入射光谱写为光源光谱与物体反射率的乘积:

L(λ)∝E(λ)ρ(λ).

因此,同一个物体在不同光源下会产生不同的通道读数。不同相机使用不同的响应函数,也不一定记录出相同的 RGB 数值。

R、G、B 滤光片的响应范围有重叠,它们不是只接收某一个波长的三个“理想开关”。单个像素的读数可以简化为:

p=B+gCk+n,

其中 B 是黑电平,g 包含增益与数字尺度,n 表示噪声。RAW 中的整数通常称为数字量值,单位记作 DN。

为什么需要 XYZ ​

人眼的颜色感知也依赖对光谱的积分。CIE XYZ 用标准观察者的配色函数表示三刺激值:

X=k∫L(λ)x―(λ)dλ,Y=k∫L(λ)y―(λ)dλ,Z=k∫L(λ)z―(λ)dλ.

相机响应与标准观察者的配色函数并不相同,所以相机 RGB 需要经过标定和变换,才能与显示颜色建立联系。一个 3×3 矩阵是实用近似,但不能保证对所有光源、所有物体光谱都完全准确。

ARW 解码与 Bayer 排列 ​

解码不等于显影 ​

ARW 是文件容器与编码格式。解码后得到的马赛克数组,并不是已经具有正常色彩和对比度的照片。

python
from pathlib import Path
import numpy as np
import rawpy

path = Path("data/DSC00178.ARW")

with rawpy.imread(str(path)) as raw:
    mosaic = raw.raw_image_visible.copy()
    channel_id = raw.raw_colors_visible.copy()
    pattern = raw.raw_pattern.copy()
    colors = raw.color_desc.decode("ascii", errors="replace")
    blacks = np.asarray(raw.black_level_per_channel, dtype=np.float32)
    white = float(raw.white_level)
    wb = np.asarray(raw.camera_whitebalance, dtype=np.float32)
    cam_xyz_coefficients = raw.rgb_xyz_matrix.copy()
    sizes = raw.sizes

这里保留 channel_id,让每个像素都能对应到其黑电平和白平衡通道。数组在关闭 RAW 对象后仍要使用,因此先复制;raw_image_visible 已经去掉一层边缘,但不应据此认定它等于最终输出的全部有效画幅。rawpy:RawPy 接口

RAW 完整画幅与仅用于观察的亮度拉伸

图 1|解码后的 RAW。 左图按白电平归一化后直接灰度显示;右图对同一数据开平方,仅便于看清结构。两侧仍保留外围边缘,并未做彩色显影。

一个像素位置通常只有一个通道 ​

教程中这张 Bayer 图像的索引与颜色关系为:

[0132]⟶[RG0G1B].

两个 G 是不同的采样位置,在元数据中可能有独立的校正参数。四个 CFA 索引并不意味着最终图片有四个显示颜色通道。

用字符 R/G/B 做可视化很直观;执行黑电平、白平衡等校正时,保留数值索引更可靠。

为什么原始图会显示成彩色,或者 CFA 图全红 ​

二维数组传给 imshow 时,默认颜色映射可能产生伪彩色。观察强度应显式指定:

python
plt.imshow(mosaic, cmap="gray", vmin=0, vmax=white)

另一个陷阱是直接显示:

python
mosaic[::4, ::4]

因为 CFA 周期是 2×2,步长 4 会一直采到同一种相位。它不是原图所有通道的均衡缩小,而可能只是红色采样点组成的图。

需要区分两种预览目的:

  • 观察 CFA 结构:显示一个小区域,明确画出每个滤光位置。奇数间隔虽能保留交替相位,但缩到屏幕上仍可能产生混叠。
  • 观察整张 RAW 的强度结构:先对区域求平均再缩小,避免固定抽取某一种 CFA 相位。

下面只生成预览,不改变处理用的全分辨率数据:

python
def block_mean_preview(a, factor=4):
    h, w = a.shape
    hh, ww = h // factor * factor, w // factor * factor
    blocks = a[:hh, :ww].astype(np.float32)
    return blocks.reshape(hh // factor, factor,
                          ww // factor, factor).mean(axis=(1, 3))

plt.imshow(block_mean_preview(mosaic), cmap="gray")

这种平均只是采样强度示意,并不是经过标定的亮度,也不等于去马赛克。

实际 CFA 排列以及偶数步长和奇数步长的采样差别

图 2|CFA 与抽样混叠。 左边是文件的实际 RGGB 相位;中间每隔 4 个像素只取到 R;右边每隔 5 个像素保留交替相位。这是滤光位置示意,不是场景本身的颜色。

黑电平、白电平与有效区域 ​

黑电平是数值基线 ​

对位于通道 k 的像素,先减去黑电平:

plinear=p−Bk.

原教程用 maximum(..., 0) 把负值截为零,便于学习与显示。更精细的统计处理可能先保留这些负值,因为噪声会让测量值落在基线两侧,过早截零会改变暗部统计。

一定先转浮点数,否则无符号整数相减可能发生下溢。

python
black_map = blacks[channel_id]
linear = mosaic.astype(np.float32) - black_map
linear = np.maximum(linear, 0.0)  # Teaching simplification

减去黑电平前后的完整画幅对比

图 3|黑电平校正。 两侧使用相同的开平方显示曲线。减去 512 DN 后,暗部基线下降,但边缘不会因此消失;左右分别使用校正前后的归一化分母。

白电平定义可用的数值尺度 ​

归一化应使用与通道一致的分母:

x=max(p−Bk,0)Wk−Bk.

如果文件只采用一个共同白电平 W,则:

python
denominator = np.maximum(white - black_map, 1.0)
raw01 = linear / denominator

原代码使用 white - black_tile.max()。当所有黑电平相同,例如都为 512 时,这个简化没有差别;若各通道黑电平不同,应改为逐位置分母。若有可靠的逐通道白电平,也应按相同索引匹配。

例如 p=1200、B=512、W=16383:

x=1200−51216383−512≈0.04335.

这表示相对于当前归一化尺度的线性信号,不是“屏幕亮度 4.3%”,也不直接等于物体反射率。

减黑电平不会移除黑边 ​

黑电平校正改变的是数值;裁切改变的是坐标和数组范围。

文件中的 RAW 边缘、可见区域、建议裁切区域不一定完全相同,边缘也不一定全部是光学黑像素。教程里的 crop_top_margin 等字段只能在明确坐标原点后使用,不能把同一组偏移无条件套到所有处理阶段。

裁切时,应让以下数组使用同一组切片:

python
# row_slice and col_slice must be expressed in the current array coordinates.
mosaic = mosaic[row_slice, col_slice]
linear = linear[row_slice, col_slice]
channel_id = channel_id[row_slice, col_slice]

如果还单独维护周期图案,新的相位为:

P′(i,j)=P((i+y0)mod2,(j+x0)mod2).

奇数偏移会改变 RGGB 等排列的相位。直接同步裁切逐像素 channel_id,可以减少相位错位和布尔掩码尺寸不一致的问题。

元数据有效区域边界和裁切后的完整有效画幅

图 4|保留有效成像区域。 左图用粉色框标出有效区;右图保留其完整范围。此次裁切原点为左侧 12、顶部 8,输出尺寸 7008×4672,没有为构图任意截取局部。

白平衡:先让中性物体保持中性 ​

白平衡是通道增益 ​

白平衡可以写成对角矩阵:

[RwGwBw]=[gR000gG000gB][RGB].

它调整的是通道之间的相对比例,并不是把整张照片变成灰色。

若这张文件的白平衡系数为 [2213,1024,1761,1024],以绿色为参考,可得到:

gR≈2.1611,gG=1,gB≈1.7197.

对当前 RGBG 通道定义,可以保留两个绿色索引的独立系数:

python
green_ids = [i for i, label in enumerate(colors) if label == "G"]
green_ref = np.mean(wb[green_ids])
gains_by_id = wb / green_ref
balanced = raw01 * gains_by_id[channel_id]

这段假定所需白平衡系数均有效且为正。换相机、换文件时,应先检查通道定义与元数据,不能永远写死 [R,G0,B,G1]。

相同显示条件下的相机白平衡前后对比

图 5|白平衡的作用。 左图未加白平衡,右图使用文件系数。为观察颜色,每个 2×2 CFA 单元先组合成一个 RGB 预览点,两侧统一使用 2 倍显示增益和 sRGB 编码。这只是降采样示意,尚未应用相机颜色矩阵。

除以最大值,为什么不是偏色的根源 ​

把所有增益都除以同一个正数,只会改变整体尺度:

gmax(g)=gGmax(g)ggG.

两组增益之间只差一个共同倍率,不会单独改变 R/G、B/G 的比值。在后续都保持线性、没有裁剪的前提下,它影响整体明暗,不会凭空导致偏绿。

真正需要排查的是通道顺序、重复白平衡、提前裁剪和颜色矩阵。

白平衡后为什么允许超过 1 ​

若线性红色值为 0.7,乘以 2.16 后约为 1.51。它仍是合法的中间计算结果;此时立即截为 1,会破坏高光的通道比例。

因此,教程在白平衡后保留大于 1 的值是合理的。最终怎样压缩或裁剪,应留到曝光、色调映射和输出阶段处理。

去马赛克:估计缺少的颜色 ​

去马赛克要把每个位置的单通道采样,重建为三个通道:

(H,W)⟶(H,W,3).

设 Mc 是颜色 c 的采样掩码,K 是局部插值核。教程中的核心操作是归一化卷积:

I^c=K∗(IMc)K∗Mc.

分子汇总邻域内同色采样值,分母统计实际参与的权重,避免把“缺失”当成数值零来平均。最后将原本已有的采样位置恢复为原始值。

python
numerator = conv2(balanced * mask, kernel)
denominator = conv2(mask.astype(np.float32), kernel)
plane = np.divide(
    numerator, denominator,
    out=np.zeros_like(numerator),
    where=denominator > 0,
)
plane[mask] = balanced[mask]

对规则 Bayer 排列,3×3 均匀邻域平均在内部像素上对应常见双线性插值的基本关系;边界结果还取决于填充方式。

例如,在红色像素位置估计绿色:

G^(i,j)=G(i−1,j)+G(i+1,j)+G(i,j−1)+G(i,j+1)4.

估计蓝色时则使用四个对角方向的蓝色采样。算法容易理解,但在细线、纹理和高反差边缘可能产生伪色、锯齿与拉链状边缘。

去马赛克后看起来有颜色,不代表已经处在正确的显示颜色空间。 此时仍然是相机 RGB。

同一32乘32采样细节去马赛克前后放大比较

图 6|缺失通道的重建。 左图把未采到的颜色通道置零,右图填入双线性估计。这里选取同一处 32×32 边缘细节,用相同显示倍率放大;实际去马赛克运算覆盖整张有效图像。

颜色矩阵:给 RGB 数值一个明确的意义 ​

标准 RGB 空间需要定义原色与白点 ​

RGB 数值只有配合原色、白点和传递函数,才能明确表示什么颜色。线性 sRGB 使用以下常见的 D65 RGB 到 XYZ 矩阵:

[XYZ]=[0.41245640.35757610.18043750.21267290.71515220.07217500.01933390.11919200.9503041]⏟Ms→XYZ[RsGsBs].

反方向使用其逆矩阵。矩阵系数与向量约定必须一起看。 若列向量形式为 y=Mx,而 NumPy 图片把 RGB 放在最后一维,则对应:

python
y = x @ M.T

这只是行列向量的换写,不能据此猜测任何一个库返回的矩阵是否已经转置、是否需要先求逆。

教程中的 rgb_xyz_matrix 到底怎样使用 ​

原教程第 8 节的文字描述是“直接把相机 RGB 乘矩阵得到 XYZ”,但实际代码采用了组合、逐行归一化、求伪逆的另一条路径,两者不能混为一谈。

rawpy 给出的矩阵形状为 4×3;其文档说明普通 RGB 相机最后一行为零。读取这个属性,也不等于证明系数全部直接存储在当前 ARW 中;LibRaw 还包含按相机型号匹配的系数表。rawpy 矩阵说明、LibRaw 颜色系数表

针对本教程的三通道情况,按 LibRaw cam_xyz_coeff 的基础构造过程理解:组合系数后归一化各相机通道,再反求输出变换,而不是把原始系数直接当成最终相机 RGB → XYZ 矩阵。LibRaw:cam_xyz_coeff 实现

令读出的前三行系数为 A,定义:

C=AMs→XYZ,di=∑jCij,C~ij=Cijdi.

在这个构造约定中,C~ 表示从输出线性 RGB 到已作相应尺度归一化的相机通道的关系。求其伪逆得到反向变换:

c≈C~s,s≈C~+c.

因此,对图像最后一维为 RGB 的 NumPy 数组:

python
A = cam_xyz_coefficients[:3, :].astype(np.float64)
M_srgb_to_xyz = np.array([
    [0.4124564, 0.3575761, 0.1804375],
    [0.2126729, 0.7151522, 0.0721750],
    [0.0193339, 0.1191920, 0.9503041],
])

C = A @ M_srgb_to_xyz
row_sum = C.sum(axis=1, keepdims=True)
if np.any(row_sum <= 1e-8):
    raise ValueError("Invalid camera matrix normalization")
C_normalized = C / row_sum

M_camera_to_srgb = np.linalg.pinv(C_normalized)
linear_srgb = rgb @ M_camera_to_srgb.T

这里的 rgb 是完成白平衡和去马赛克的相机 RGB。这个命名比原代码中的 srgb_to_camera = pinv(...) 更清楚:求逆之后,方向已经变成相机通道到输出通道。

本段只解释教程所采用的三通道基础近似,并不等价于完整 LibRaw 显影。白平衡尺度、相机标定、不同光源下的配置与其他校正仍可能造成差异。第四行不为零、通道模型不同或矩阵缺失时,不能直接照搬前三行处理。

相机RGB与转换到线性sRGB后的颜色对比

图 7|颜色变换。 两侧统一使用 2 倍显示增益和 sRGB 编码,只比较相机颜色变换前后。左图把相机通道暂当显示 RGB,右图已经完成目标颜色变换;中性区域接近,但彩色区域的饱和度与色相会改变。

白平衡与白点适应也不相同 ​

白平衡在相机通道上调增益;色度适应则是在明确源白点、目标白点和颜色空间后进行的转换。通道已经中性,并不足以证明某个未知矩阵已经满足目标白点条件。

同样,画面偏绿也不能直接推出“缺少 Bradford 适应”。应先核对矩阵方向、白平衡是否重复、通道索引与输入尺度,再决定是否需要白点适应。

为什么矩阵输出会有负数 ​

颜色变换后的负值可能来自噪声、标定误差或颜色超出目标 RGB 空间可表示的范围,不一定说明代码立即出错。

原教程在变换后直接截掉负数,是一种显示简化。做分析时可先保留浮点结果;进入教学用色调映射之前再显式处理负值,并知道这不等于完整的色域映射。

曝光和色调映射 ​

计算中的曝光倍率 ​

线性曝光调整为:

x′=2ΔEVx.

所以教程中的 exposure=4.0 相当于对已取得的数据增加两档线性倍率。它不等同于拍摄时真正多收集两档光子,也不能恢复已饱和的细节。

一倍和四倍线性曝光调整的显示比较

图 8|线性曝光倍率。 右图为左图的 4 倍线性信号,即 +2 EV。两侧都编码为 sRGB,但尚未色调映射;超出显示范围的高光会被直接裁白。

Reinhard 曲线压缩高亮 ​

教程使用:

T(x)=x1+x,x≥0.

当 x 变大时,输出逐渐接近 1。但它不是相机厂商的完整风格曲线,也不是显示编码。

对 RGB 三个通道分别应用这条非线性曲线,会改变颜色比例。 例如:

(2,1,0.5)⟶(23,12,13).

R/G 的比例从 2 变成 4/3。因此,不能说逐通道 Reinhard 完全不影响色彩。

一种教学上的替代做法,是先压缩线性 sRGB 的亮度,再对三个通道使用同一个倍率:

Y=0.2126729R+0.7151522G+0.0721750B,Ctone=CT(Y)max(Y,ε).

同一像素的 RGB 比例可在后续裁剪之前保留,但结果仍可能超出输出范围,也不代表完成了感知均匀的色域映射。

相同曝光下直接裁剪与Reinhard色调压缩的比较

图 9|色调映射。 曝光均为 4 倍;左边直接裁剪显示,右边先执行逐通道 Reinhard,再编码为 sRGB。高光得到压缩,但这条逐通道曲线也会影响颜色比例。

sRGB 编码:为什么线性图直接显示很暗 ​

普通图片查看流程通常按编码后的数值解释图像。如果把线性值 0.18 直接作为编码值显示,它不会表达预期的线性强度。

sRGB 的正向编码是分段函数:

S(L)={12.92L,L≤0.0031308,1.055L1/2.4−0.055,L>0.0031308.

它不是简单的 L1/2.2;sRGB 的标准白点为 D65。ICC:sRGB 编码定义

python
def linear_to_srgb(x):
    x = np.clip(x, 0.0, 1.0)
    return np.where(
        x <= 0.0031308,
        12.92 * x,
        1.055 * np.power(x, 1 / 2.4) - 0.055,
    )

例如:

S(0.18)≈0.461.

显示端按相应传递关系还原后,才对应目标线性强度。它不是通过“伽马”额外创造曝光。

如果把已经编码过的 sRGB 再编码一次,会产生另一种错误。反过来,需要在线性域进行运算时,可使用逆函数:

L(S)={S/12.92,S≤0.04045,(S+0.0551.055)2.4,S>0.04045.

相同线性数据直接显示和标准sRGB编码后的比较

图 10|显示编码。 左边把色调映射后的线性数值直接交给普通图像显示;右边先做标准 sRGB 编码。两侧曝光、白平衡与色调映射相同,仅输出编码不同。

一段完整的教学输出过程 ​

python
# linear_srgb: floating-point result of the camera color transform
x = np.maximum(linear_srgb, 0.0) * 4.0
tone = x / (1.0 + x)  # Per-channel teaching curve; may change color ratios
encoded_srgb = linear_to_srgb(tone)

原教程按 99.5 百分位缩放中间预览,是为了便于观察,并不是标准曝光标定。若每一步都分别自动缩放,跨阶段比较明暗时要格外注意:显示倍率已经变化了。

Reinhard曲线与标准sRGB传递函数

图 11|两种曲线负责不同的工作。 左边压缩高动态范围,右边规定线性强度怎样编码。右侧虚线是恒等映射,用于说明“线性值直接显示”与正确编码的差别。

保存图片:数值与颜色描述要配套 ​

8 位量化可以写成:

q=round(255clip(S,0,1)).
python
from PIL import Image

pixels = np.uint8(np.clip(encoded_srgb, 0, 1) * 255 + 0.5)
Image.fromarray(pixels).save("arw_preview.png")

但数组写成 PNG,并不自动意味着文件已经携带正确的 ICC 配置。生产输出应同时明确颜色空间标记;屏幕显示还取决于查看软件、系统颜色管理和显示器配置。

因此,比较两张照片时至少要确认:它们采用了相同的颜色解释、曝光与显示条件。

完整有效画幅的手写RAW处理最终结果

图 12|手写流程结果。 完整有效画幅经过白平衡、双线性去马赛克、颜色变换、4 倍曝光、Reinhard 和 sRGB 编码。这里使用压缩 WebP 展示;并不是把原始 ARW 改成了低分辨率。

怎样与 rawpy 参考结果比较 ​

原教程的参考处理很适合做观感对照,但并不是严格控制了所有变量的实验。

尤其要注意:output_color=sRGB 和输出传递曲线不是同一个参数。rawpy 文档给出的默认 gamma 为 (2.222, 4.5),标为 BT.709,而不是上面手写的标准 sRGB 分段编码。rawpy:处理参数

为了把“颜色空间”和“编码曲线”分开观察,可以请求线性参考输出,再手动应用相同的 sRGB 编码:

python
with rawpy.imread(str(path)) as raw:
    reference_u16 = raw.postprocess(
        use_camera_wb=True,
        output_color=rawpy.ColorSpace.sRGB,
        output_bps=16,
        gamma=(1, 1),
        no_auto_bright=True,
        user_flip=0,
    )

reference_linear = reference_u16.astype(np.float32) / 65535.0
reference_display = linear_to_srgb(reference_linear)

这仍然包含 LibRaw 的内部尺度、高光处理等步骤,且 16 位整数输出不能保留任意大的线性值;它不是无损的浮点相机原始数据。比较时应把双方的曝光和色调曲线进一步对齐。

还要避免两种常见误判:

  1. 重复裁切:postprocess 的输出尺寸、方向或裁切可能与原始马赛克不同。不要不加判断地再套一次 RAW 阶段的偏移。
  2. 把系统预览当作唯一真值:查看软件可能使用 RAW 内嵌 JPEG,也可能调用自己的显影引擎;相机风格曲线与色彩配置都会影响观感。

要尽量接近某个软件,必须先明确它实际显示的是哪一种结果,再对齐白平衡、曝光、曲线、颜色配置与输出编码。单靠一条“伽马校正”无法保证一致。

手写显影和LibRaw参考显影在相同展示条件下的比较

图 13|与 LibRaw 对照。 参考分支关闭自动亮度、请求线性 16 位 sRGB 输出,并保持未旋转方向;根据实际输出尺寸对齐有效区域。两侧统一应用 4 倍倍率、Reinhard 和标准 sRGB 编码。LibRaw 内部尺度仍可能不同,因此这是可解释的观感对照,不是逐像素完全等价的验证。

现象与排查顺序 ​

现象优先检查
RAW 强度图带蓝绿色是否用了默认伪彩色映射,而非 cmap="gray"
CFA 图全红或全绿是否用偶数步长抽样,只保留一种 CFA 相位
裁切后布尔索引报错马赛克、CFA 索引、黑电平图是否同步裁切
去马赛克后整体很暗是否仍是线性相机 RGB;显示范围是否合理
颜色矩阵后突然偏绿、偏红通道顺序、矩阵实际方向、求逆和转置、白平衡是否重复
白平衡后高光变色是否过早把大于 1 的通道裁掉
减黑电平后仍有黑边是否混淆数值基线与有效区域裁切
色调映射后颜色变淡是否逐通道使用非线性曲线,改变了通道比例
导出图与参考图不同曝光、自动亮度、色调曲线、传递函数和色彩管理是否一致

一个有用的调试习惯,是给中间数组明确命名:raw_dn、black_corrected、camera_rgb、linear_srgb、encoded_srgb。每一步都记录形状、数据类型、范围和通道定义,而不是只凭“看起来像照片”判断是否正确。

回顾 ​

从传感器到屏幕,至少要回答五个不同的问题:

  1. 采到了什么? CFA 位置决定每个读数来自哪个通道。
  2. 基线和尺度是什么? 黑电平、白电平与有效区域使数据具备正确起点。
  3. 光源和缺失采样怎样处理? 白平衡调整中性关系,去马赛克重建完整通道。
  4. 这些数值代表什么颜色? 相机标定和颜色变换建立与标准颜色空间的联系。
  5. 怎样输出到屏幕? 曝光、色调映射、sRGB 编码、量化和颜色管理共同决定最终呈现。

理解这五层关系后,偏色、过暗、黑边和预览差异就能被拆成具体问题,而不必反复凭感觉调整矩阵和增益。