3885 字
19 分钟
黑电平为什么不是零?从 Optical Black 到逐通道 BLC

把镜头盖盖上,在完全没有光进入传感器时拍一张 RAW,像素值通常不会是 0。在一台 12-bit 相机里,漆黑区域可能稳定地落在 512 附近;不同颜色位置可能又分别是 520504508532

这并不表示传感器真的看见了灰光。为了让模拟电路能够表达零点附近的正负波动,读出链路会有意保留一段偏置;暗电流、放大器和 ADC 也会贡献额外 offset。黑电平校正(Black Level Correction,BLC)要做的,就是先找出这条“电子学的零线”,再把它从 RAW 数字中减掉。

一句话公式很简单:

signal=rawblacksignal=raw-black

但黑位估低了,暗部会发灰;估高了,阴影细节会被吞掉;把减法放在白平衡之后,还会把偏置按通道放大。更隐蔽的是,在无符号整数里直接相减,负数甚至可能绕回一个巨大的正数。

本文从固定黑位出发,依次讨论逐通道、逐行和 Optical Black 估计。范围集中在 RAW/Bayer 管线,不展开暗电流随长曝光建模、黑帧去噪和传感器工厂标定。

为什么“没有光”仍然不是零#

一个简化的 RAW 读数可以写成:

raw=signal+offset+noiseraw = signal + offset + noise

其中 signal 才是希望保留的光生电荷响应,offset 是黑电平,noise 是读出噪声、暗电流散粒噪声等随机波动。offset 常见来源包括:

  • 模拟前端为了避免零点附近信号被截断而加入的偏置;
  • ADC 自身的零点误差与列放大器偏差;
  • 暗电流在曝光期间积累的电子;
  • 模拟增益、温度、曝光时间和读出模式变化;
  • 不同颜色、行、列或读出通道之间的固定模式差异。

偏置并不全是坏事。假如电子噪声围绕物理零点上下波动,而数字编码不能表达负数,那么直接把物理零映射到 DN 0,会把所有负向波动截掉,留下一个不对称分布。把零信号抬到 512,ADC 就能完整记录零点两侧的随机变化,后续软件再减掉 512。

因此,RAW 的数字零与“没有光”是两个概念。BLC 不是给图片加一个审美上的黑场,而是在恢复线性信号坐标系的原点。

RAW 直方图:零光照不在零点

灰色柱是固定合成 RAW 数据。移动估计黑位,观察减法后暗场中心和被裁成 0 的样本比例。

暗部直方图12-bit DN
RAW 暗部直方图和估计黑电平4301040 DN
corrected = (raw − 512) / (4095 512)
真实黑位512 DN
暗场残差0 DN
负值样本9.9%

估计偏低会抬起黑位;估计偏高会吞掉靠近黑色的有效信号。裁剪能隐藏负数,却不能修复估计错误。

组件中固定数据的真实黑位是 512 DN。把估计滑到 500,暗场平均会残留约 12 DN,黑色被抬起;滑到 530,则一部分微弱但真实的信号也变成负数。最终 clip(0) 只会把它们压成同一个黑色,无法还原差异。

从 DN 到归一化线性值#

如果 RAW 的黑电平是 BB、白电平是 WW,把有效信号归一化到近似 [0,1][0,1] 的常见公式是:

linear=rawBWBlinear = \frac{raw-B}{W-B}

分母不是简单的 WW,而是从黑位到饱和白位之间真正可用的码值范围。以 12-bit 数据为例,若 B=512B=512W=4095W=4095,有效范围只有 35833583 DN。

一个读数 2304 的归一化值为:

230451240955120.500\frac{2304-512}{4095-512}\approx0.500

直接除以 4095 会得到约 0.563,相当于既没有正确去掉底座,也错误解释了曝光比例。这种误差会继续传给白平衡、CCM、降噪阈值和 Tone Mapping。

白电平也不一定等于数据类型最大值#

16-bit 容器中的 RAW 可能只有 12 或 14 个有效位;传感器的线性饱和点也可能低于理论最大码值。应读取相机元数据或标定参数,而不是根据 uint16 武断地使用 65535。

固定、逐通道和逐行黑电平#

最简单的 BLC 对所有位置减同一个常量:

y(i,j)=raw(i,j)By(i,j)=raw(i,j)-B

当传感器各读出路径足够一致时,它速度快、元数据少,也便于硬件实现。但 Bayer 阵列的 R、G₁、G₂、B 位置可能拥有不同偏置。此时更合理的模型是:

y(i,j)=raw(i,j)Bc(i,j)y(i,j)=raw(i,j)-B_{c(i,j)}

c(i,j)c(i,j) 表示该坐标的 CFA 通道。RGGB 的一个 2×22\times2 单元可写为:

R G₁
G₂ B

两个绿色位置虽然使用相同颜色滤光片,却可能经由不同的行列读出路径,因此标定中有时会分别提供 G₁ 与 G₂ 黑位。若把四个黑位都强行平均,亮部的十几个 DN 几乎看不出来,接近黑色时却会直接表现为色偏或棋盘纹。

一个黑位,还是逐通道黑位?

同一模拟链路里的 R、G₁、G₂、B 仍可能有不同偏置。统一减法会把差异留在最暗的颜色里。

校正参数
本应中性的暗部
校正后通道值raw − black
R
26 DNBL 520
G₁
10 DNBL 504
G₂
14 DNBL 508
B
38 DNBL 532
通道残差跨度28 DN
校正策略512 × 4

逐通道参数清除了固定偏置差,但并不替代白平衡:BLC 定义“零信号”,WB 校正零点以上的通道比例。

组件中的中性暗部给四个 CFA 位置添加了相同的真实光信号。选择统一黑位后,残余偏置改变了 R、G、B 比例;切换逐通道校正,四路重新对齐。请注意:这仍然不是白平衡。BLC 负责让“零光照等于零”,WB 负责调整零点以上的通道比例。

逐行与逐列模型#

某些传感器的偏置会沿行或列缓慢漂移,或者每个读出放大器有自己的固定 offset。此时可以进一步写成:

y(i,j)=raw(i,j)Bc(i,j)RiCjy(i,j)=raw(i,j)-B_{c(i,j)}-R_i-C_j

RiR_i 是第 ii 行修正,CjC_j 是第 jj 列修正。实际实现不一定同时使用三项:可能只按行估计,也可能按成组的列放大器建立查找表。模型越细,越能追踪固定模式;但每组可用样本越少,估计噪声也越容易被写回整行或整列。

Optical Black:藏在图像边缘的参考零点#

很多传感器在有效成像区周围保留一圈被金属遮住、不接收入射光的像素,常称 Optical Black、optically black pixels 或 masked pixels。它们和有效像素经历相似的曝光时间、温度与读出链路,却理论上没有场景光信号,因此可用于在线观察黑位。

最朴素的估计是取均值:

B^mean=1Nk=1Nxk\hat B_{mean}=\frac{1}{N}\sum_{k=1}^{N}x_k

如果噪声近似对称、样本没有异常值,均值能高效地利用所有观测。但 Optical Black 区也可能有热像素、坏点、瞬时干扰或靠近边界的光泄漏。一个极高样本就能明显拖动均值。

另一种选择是中位数:排序后取中间位置。它对少量极端值不敏感,却没有利用“偏离中心多远”的信息。在干净的高斯噪声下,中位数通常比均值有更高方差。

Optical Black:均值还是中位数?

遮光像素能观测电子偏置,但热像素和读出异常也会混进样本。比较两个估计量面对异常点时的表现。

样本
估计方法
均值511.8
中位数511.5
选中误差-0.2 DN

中位数对少量极端值更稳健;均值在噪声对称且异常点已经清理时,能更充分利用所有样本。工程里常先做坏点剔除,再对行、列或通道分组统计。

遮光像素读数26 samples
Optical Black 样本、均值与中位数Optical Black 区域 →

更实用的流程通常介于两者之间:先按通道、行或读出组划分 Optical Black 样本;再用中位数和 MAD、sigma clipping 或明确坏点表排除异常值;最后对剩余样本求均值,并在空间或时间上做适度平滑。

Optical Black 也不是真实图像区的万能真值,两者仍可能有布局差异或边缘光泄漏。裁切 RAW 时还要区分完整尺寸与 Active Area;不能把有效图像里最暗的像素当成遮光参考,因为场景未必存在纯黑区域。

为什么 BLC 必须在白平衡之前#

设 RAW 读数为 xx,黑位为 BB,白平衡增益为 gg。正确顺序是:

y=(xB)gy=(x-B)g

如果先乘增益,再减去原来的黑位:

ywrong=xgBy_{wrong}=xg-B

两者的差为:

ywrongy=B(g1)y_{wrong}-y=B(g-1)

这个残留和场景信号无关。蓝通道若需要 2.0× WB,512 DN 的偏置就会额外留下 512 DN;红、绿、蓝增益不同,电子学偏置会被染成颜色。即使之后再做 CCM,也只是把错误混到其他通道,不会让零点自动恢复。

正确的教学主线是:

RAW → BLC → 坏点/LSC/降噪等 RAW 处理 → Bayer WB → Debayer → CCM

不同 ISP 会调整坏点、LSC、降噪和 WB 的具体相对位置,但 BLC 通常必须足够靠前,因为许多后续算法都假定输入已经围绕物理零点。

BLC、WB 与数值精度:顺序对了也可能翻车

比较先减黑位与先乘 WB,并把运算切到无符号整数。RAW 低于黑位时,uint16 原地减法会绕回一个巨大正数。

运算类型
处理顺序
RAW 505− BL 512× WB 1.80-12.6
数学期望:(505512) × 1.80 = -12.6本次中间减法:-7
当前输出-12.6
相对数学期望+0.0 DN

稳健做法是先转成 float 或更宽的有符号类型,再减黑位、按策略保留或处理负值,最后应用 WB。不要靠最终 clip 掩盖中间溢出。

负值要不要立刻 clamp#

减掉黑位后出现小负值是正常的。暗场有随机噪声,既然均值被移到零附近,样本自然应该分布在零点两侧。立即执行:

linear = np.maximum(raw - black, 0)

会把负半边全部堆到零上,改变暗部均值和噪声分布。是否接受这种偏差,取决于后续管线:

  • 若马上进入只接受非负值的硬件模块,clamp 可能是明确且必要的接口约束;
  • 若接下来要做降噪、统计标定或暗部分析,保留有符号浮点通常更自然;
  • 若最终要显示,负光当然不能直接输出,但可以在更晚阶段统一处理;
  • 若为了“画面不报错”而在每一步都 clip,真正的黑位错误会被隐藏。

无符号整数下溢:最安静也最危险的错误#

假设 uint16 RAW 是 505,黑位是 512。数学结果应为 -7,但如果库执行无符号原地减法,可能得到:

505 - 512 → 65529

后面即使只乘 1.2,这个像素也会立刻冲向最大值。最终 clip 会把本应接近黑色的噪声点变成亮点。

稳健策略是先转换数据类型:

raw_f = raw.astype(np.float32)
signal = raw_f - black_level

若硬件必须使用定点数,也要明确位宽、有无符号、舍入与饱和规则。使用更宽的有符号中间寄存器,再在定义好的节点做饱和,不能依赖语言或 SIMD 指令的默认回绕行为。

Python / NumPy:从单一黑位到 RGGB 四通道#

先实现最基础的标量版本:

import numpy as np
def subtract_black(raw, black_level, white_level, clamp_negative=False):
if white_level <= black_level:
raise ValueError("white_level 必须大于 black_level")
raw = np.asarray(raw, dtype=np.float32)
linear = (raw - float(black_level)) / (white_level - black_level)
if clamp_negative:
linear = np.maximum(linear, 0.0)
return linear

函数不在上界执行 clip(1)。高于 white level 的值可以留给调用方决定;真正的传感器饱和像素通常仍需单独生成 mask,供高光重建或后续算法使用。

RGGB 四通道参数可按坐标奇偶应用:

def subtract_black_rggb(raw, levels, white_level):
"""levels 顺序为 R、G1、G2、B。"""
raw = np.asarray(raw, dtype=np.float32)
if raw.ndim != 2:
raise ValueError("Bayer RAW 必须是二维数组")
r, g1, g2, b = map(float, levels)
black_map = np.empty_like(raw, dtype=np.float32)
black_map[0::2, 0::2] = r
black_map[0::2, 1::2] = g1
black_map[1::2, 0::2] = g2
black_map[1::2, 1::2] = b
denominator = white_level - black_map
if np.any(denominator <= 0):
raise ValueError("每个通道的 white level 都必须高于 black level")
return (raw - black_map) / denominator

注意裁剪一行或一列会改变有效区域左上角的 Bayer Pattern。原图是 RGGB,去掉第一行后就不再能按原坐标直接套 RGGB mask。最安全的做法是同时携带 CFA pattern 和 crop offset,而不是在函数里硬编码。

用 Optical Black 估计每行偏置#

假设左侧 ob_columns 列确实是遮光区,可以做一个教学版逐行估计:

def estimate_row_black(raw, ob_columns=16):
raw = np.asarray(raw, dtype=np.float32)
optical_black = raw[:, :ob_columns]
center = np.median(optical_black, axis=1, keepdims=True)
deviation = np.abs(optical_black - center)
mad = np.median(deviation, axis=1, keepdims=True)
threshold = np.maximum(6.0 * mad, 1.0)
valid = deviation <= threshold
counts = np.maximum(valid.sum(axis=1), 1)
row_black = (optical_black * valid).sum(axis=1) / counts
return row_black.astype(np.float32)

这是说明统计流程的简化代码。真实数据要先确认 OB 区域坐标、通道排列、光泄漏边缘和坏点规则。若按 Bayer 通道分别估计,还需让每一行只汇总相同 CFA 类型的位置。

BLC 与其他 RAW 算法的关系#

坏点校正#

坏点检测往往比较中心像素和同色邻居。若行列偏置尚未去掉,固定模式可能被误判成坏点;反过来,Optical Black 估计也应先排除 OB 区热像素,避免一个异常值污染整行修正。

镜头阴影校正#

LSC 会在画面边缘乘较大增益。如果 BLC 偏低,残留 offset 也被空间增益放大,四角可能发灰或偏色。因此通常先定义正确零点,再做乘性校正。

降噪#

Shot noise 与信号强度相关,读出噪声则在黑位附近仍存在。噪声模型若直接读取未减黑的 DN,会把电子学偏置误当成光信号。另一方面,是否在降噪前 clamp 负值也会影响噪声分布,需要让模型与数据约定一致。

白平衡与 CCM#

WB 是逐通道乘法,CCM 是通道混合。两者都依赖先获得正确的线性零点。BLC 的加性 offset 不能简单并入一个只含乘法的 3×33\times3 CCM;若用齐次坐标可以数学合并仿射变换,但 Bayer 域位置、动态参数和调试需求通常让各阶段保持独立更清晰。

常见故障怎样从画面反推#

暗部整体发灰#

可能是黑位估低、元数据未应用,或归一化时误把分母写成 white level。先在遮光区或镜头盖暗帧上检查均值,不要先调 Tone Curve 掩盖。

暗部带固定颜色#

可能把四通道黑位合成了一个值,也可能 Bayer Pattern 或 crop offset 错位。对 R、G₁、G₂、B 分别统计残差,通常比看最终 sRGB 更容易定位。

横向或纵向条纹#

检查逐行、逐列读出 offset、OB 统计样本数和时间漂移。如果校正后条纹反而更强,可能是过细的估计把随机噪声复制到了整行。

黑色区域冒出亮点#

重点检查 uint16 下溢、坏点和 OB 异常值。先打印中间结果的数据类型与 min/max;最终图片被 clip 后,回绕和普通高光看起来都可能只是白点。

换 ISO 或长曝光后失效#

说明黑位参数没有和模拟增益、转换增益、温度或曝光模式绑定。固定常数只在它的标定条件内可靠。

一份可执行的调试清单#

不要只凭“一张照片看起来黑得够不够”判断。至少做这些测试:

  1. 用镜头盖暗帧或有效 Optical Black,检查校正后各通道均值是否接近零。
  2. 同时保留校正前后的直方图,确认分布是平移,而不是大面积被提前截断。
  3. 分别统计 R、G₁、G₂、B,以及每行、每列或每个读出组的残差。
  4. 给函数输入恰好等于、略低于、略高于 black level 的合成值。
  5. 记录每一步的数据类型、最小值与最大值,专门测试无符号下溢和乘法溢出。
  6. 用单位 WB 验证 BLC 输出不受影响,再用不同通道增益检查是否严格执行 BLC → WB。
  7. 验证 (raw-black)/(white-black),尤其不要把容器最大值误认为 white level。
  8. 测试四种 Bayer Pattern,以及裁掉奇数行列后的 pattern 偏移。
  9. 给 OB 样本注入热像素,确认异常值策略不会把整行黑位拖走。
  10. 比较多个 ISO、曝光时间、温度与传感器模式,确认参数的适用范围。

回到原点#

黑电平不是画面风格中的“黑色有多黑”,而是传感器线性坐标系的原点。它可能是一个常数,也可能按 R、G₁、G₂、B、行、列和读出模式变化;Optical Black 为在线估计提供了参考,但仍需要异常值处理与标定验证。

真正稳健的 BLC 可以浓缩成四件事:确认参数对应当前模式;先转成安全的数据类型;先减黑位,再做 WB 与其他乘性处理;把 clamp 当成明确的管线决策,而不是遮错工具。

一旦原点正确,后面的白平衡、Debayer、CCM、降噪和 Tone Mapping 才是在处理光信号,而不是把电子学偏置一路放大到最终照片里。

黑电平为什么不是零?从 Optical Black 到逐通道 BLC
https://cloudsir.top/posts/how-black-level-correction-works/
作者
CloudSir
发布于
2026-08-24
许可协议
CC BY-NC-SA 4.0
高光是怎么装进屏幕的?从曝光压缩到 Filmic Tone Mapping
画面四角为什么更暗?从增益网格理解 Lens Shading Correction