对着一面照明均匀的白墙拍照,画面中心往往明亮,四角却逐渐变暗;把颜色饱和度拉高,还可能看到角落发红、发绿或发紫。这不一定是场景真的不均匀,而可能是镜头、滤光片、微透镜与传感器共同留下的空间响应。
Lens Shading Correction,简称 LSC,要做的就是为图像不同位置乘上不同增益:中心接近 1.0,越靠近响应较弱的角落,增益通常越大。它看起来像给照片套一层反向暗角,但 RAW ISP 里的真实实现更细:Bayer 的 R、Gr、Gb、B 四个平面可以各有一张二维增益网格。
本文从一个均匀场景出发,逐步回答四个问题:阴影为什么出现?怎样从标定图得到增益?稀疏网格怎样覆盖几百万个像素?校正以后,噪声和高光为什么也会改变?
一面均匀的墙,为什么拍出来不均匀
如果场景亮度是 ,镜头—传感器系统在位置 的相对响应是 ,暂时忽略噪声和黑电平,RAW 测量可以写成:
中心响应通常归一化为 ,角落可能只有 0.5。LSC 保存的增益场近似为其倒数:
于是角落 0.5 乘 2.0,理论上回到与中心一致。实际系统会留出增益上限、平滑网格并处理噪声,因此不会简单地对每个像素做一次完全自由的除法。
径向暗角合成场景
假设一面亮度完全均匀的墙。调节镜头阴影和校正强度,观察空间增益怎样把四角拉回中心。
这里的场景本来均匀,所有空间变化都来自镜头—传感器系统。真实镜头的阴影并不一定严格径向对称,因此量产 ISP 通常使用二维网格。
组件用径向函数制造了一张均匀灰场。真实镜头的阴影可能偏心、椭圆,甚至因为装配误差出现不对称;径向模型适合建立直觉,却不是量产标定必须遵守的形状。
阴影从哪里来
“四角暗”常被统称为渐晕,但它背后有几类机制。
光学渐晕与自然渐晕
离开光轴的光束会受到镜筒、光圈和镜片边缘限制,一部分光线无法到达像面。大光圈时这种机械或光学遮挡通常更明显。即使没有明显遮挡,离轴成像也会因投影面积、立体角等因素出现自然亮度下降,经典近似常提到 规律。
这个规律只能描述某类理想条件。具体镜头的结构、焦距、对焦距离、光圈和传感器尺寸都会改变实际分布,不能拿一条固定径向曲线替代标定。
CRA 与像素角度响应
CRA(Chief Ray Angle,主光线角)描述主光线相对像面法线的角度。画面边缘的光往往以更斜的角度进入传感器。像素上方还有彩色滤光片、金属布线和微透镜,斜入射光可能不能被完整导向光敏区域,响应因此下降。
不同波长的折射、滤光片透过率和微透镜效率又不完全相同,所以红、绿、蓝的衰减比例可能随位置变化。这就是颜色阴影:问题不只是角落“更暗”,还会改变 RGB 比例。
像素响应与装配误差
传感器自身存在像素响应不均匀,红外截止滤镜、保护玻璃和镜头相对传感器的偏心或倾斜也会引入低频空间差异。高频的单个坏点通常交给 DPC 或固定模式噪声校正,LSC 更擅长描述平滑、缓慢变化的二维场。
因此,LSC 不是一个只属于镜头的常数。它往往依赖具体的镜头—传感器组合,也可能随焦距、对焦距离、光圈、温度或光源光谱变化。
亮度阴影和颜色阴影是两件事
假设中心中性块测得:
R = 0.50, G = 0.50, B = 0.50角落若变成:
R = 0.30, G = 0.30, B = 0.30三个通道同比例下降,主要表现为亮度阴影。若角落是:
R = 0.34, G = 0.30, B = 0.25除了总体变暗,通道比例也变了,角落会带颜色。只用一张公共亮度增益图,可以把明暗大致拉平,却无法同时让中性区域恢复中性。
RAW Bayer 又不是每个位置都有完整 RGB。以 RGGB 为例:
R GrGb B工程实现通常分别为 R、Gr、Gb、B 保存四张低分辨率 gain mesh。两个绿色平面理论上响应相近,但空间位置、读出路径或微透镜布局不同,保留两张表更容易覆盖细微差异。
RGGB 四平面的颜色阴影
同一个位置的 R、Gr、Gb、B 衰减可以不同。分别标定四张增益图,才能同时修正亮度阴影和颜色阴影。
Gr 与 Gb 来自 Bayer 单元中两个不同位置。多数情况下它们很接近,但分别保存能够覆盖微透镜、读出路径或标定误差造成的不对称。
这四张图不是四张输出彩色图,而是四类 CFA 样本各自的空间系数。应用时,程序根据 Bayer pattern 和像素坐标,为当前位置选择正确平面的插值增益。
为什么用 gain mesh,而不是逐像素表
一张 4000×3000 的 RAW 有 1200 万个采样点。如果每个像素、每个通道都保存一个浮点增益,标定文件和硬件 SRAM 都会很大,而且逐像素测量噪声容易被直接拟合进去。
镜头阴影通常是低频变化,所以可以只保存例如 17×13、33×25 这样的稀疏网格节点,再在节点间插值。这样做有三点好处:
- 存储和带宽显著下降;
- 平滑插值自然抑制平场拍摄中的随机噪声;
- ISP 可以用固定流水线逐像素生成增益。
代价是网格太稀时无法描述局部变化,太密时又可能拟合灰尘、照明不均或噪声。网格尺寸是模型能力与稳健性之间的选择,不是越大越好。
二维双线性插值怎样工作
假设像素落在四个网格节点之间。左上、右上、左下、右下的增益分别为 ,像素在当前单元内的归一化位置为 ,其中 。
先沿水平方向插值:
再沿垂直方向插值:
合并后就是:
四个权重非负且相加为 1,所以结果位于四个节点增益的范围内。它对节点值是线性的,但二维曲面在每个小格内是双线性的。
增益网格与二维双线性插值
ISP 不为每个像素保存一个系数,而是在稀疏节点间插值。移动采样位置,观察四邻点共同决定结果。
每个像素只读取所在网格单元的四个节点。网格越密越能描述不规则阴影,但标定数据、存储量和对噪声的拟合风险也会增加。
真实实现还要明确网格覆盖的是有效成像区还是包含 optical black 的完整 RAW、节点位于像素中心还是边界、裁切和缩放后的坐标怎样映射。半个像素的坐标约定差异可能形成格状残差,不能只确认“公式写了双线性”就结束。
怎样从平场图标定一张 LSC 表
最直接的标定输入是一张平场图:相机对准亮度和光谱尽量均匀的积分球或漫射光源,在固定曝光、模拟增益、光圈和焦距下采集多帧 RAW。
1. 先做基础 RAW 预处理
标定图必须先减去正确的黑电平。若测量值为:
直接用 center / raw 求增益,会把黑电平也当成有效光信号,尤其在暗角处造成系统性错误。坏点、饱和点和明显污染也应剔除。多帧平均可以降低 shot noise 与 read noise。
2. 分离 Bayer 四平面
根据 CFA pattern 抽取 R、Gr、Gb、B。Pattern 写错、活动区起点错一行或错一列,都会让四套增益套到错误样本上。结果未必崩溃,却会产生周期性颜色网格。
3. 估计每个网格节点的响应
在每个节点周围选取窗口,用均值、中位数或稳健拟合估计局部响应 。窗口要足以压低噪声,又不能跨过明显的非均匀区域。若平场本身存在梯度,需要先校准光源,或用旋转相机、多位置拍摄等办法区分光源与相机响应。
4. 选择参考并归一化
常见做法以中心区域或全图最大可靠响应为参考:
这里的 表示 R、Gr、Gb、B 平面。以中心归一化会让中心增益接近 1.0;以其他统计量归一化也可以,但会改变整体亮度,必须与曝光和白平衡的增益预算一起设计。
5. 平滑、约束并验证
节点增益通常需要平滑,限制突变与最大值。应在独立平场上验证校正后的亮度均匀性、角落颜色和格状残差,而不是只看生成表所用的同一批数据。
一个简化的 NumPy 标定示意如下:
import numpy as np
def estimate_plane_gain(plane, rows=13, cols=17, max_gain=3.0): """从已经减黑电平的单个 Bayer 平面估计节点增益。""" h, w = plane.shape y_edges = np.linspace(0, h, rows + 1, dtype=int) x_edges = np.linspace(0, w, cols + 1, dtype=int) response = np.empty((rows, cols), dtype=np.float32)
for j in range(rows): for i in range(cols): patch = plane[y_edges[j]:y_edges[j + 1], x_edges[i]:x_edges[i + 1]] response[j, i] = np.median(patch)
center = response[rows // 2, cols // 2] gain = center / np.maximum(response, 1e-6) return np.clip(gain, 1.0, max_gain)
8 collapsed lines
raw_linear = raw.astype(np.float32) - black_levelr = raw_linear[0::2, 0::2]gr = raw_linear[0::2, 1::2]gb = raw_linear[1::2, 0::2]b = raw_linear[1::2, 1::2]
meshes = [estimate_plane_gain(p) for p in (r, gr, gb, b)]这段代码用于说明流程,不是完整标定程序。它默认 RGGB、平场完全均匀、中心可靠且没有饱和,也没有处理窗口几何、平滑、离群值和多个镜头状态。
在 Bayer 域应用四张增益网格
应用阶段要把低分辨率 mesh 插值到每个 Bayer 样本位置。教学代码可以先把单平面网格放大,再写回相应位置:
import numpy as np
def bilinear_sample(mesh, x, y): """x、y 是 0 到 1 的归一化坐标数组。""" rows, cols = mesh.shape gx = x * (cols - 1) gy = y * (rows - 1) x0 = np.floor(gx).astype(int) y0 = np.floor(gy).astype(int) x1 = np.minimum(x0 + 1, cols - 1) y1 = np.minimum(y0 + 1, rows - 1) tx = gx - x0 ty = gy - y0
top = mesh[y0, x0] * (1 - tx) + mesh[y0, x1] * tx bottom = mesh[y1, x0] * (1 - tx) + mesh[y1, x1] * tx return top * (1 - ty) + bottom * ty
20 collapsed lines
def apply_lsc_rggb(raw, meshes): """raw 已减黑电平;meshes 顺序为 R、Gr、Gb、B。""" output = raw.astype(np.float32).copy() locations = [ (slice(0, None, 2), slice(0, None, 2)), (slice(0, None, 2), slice(1, None, 2)), (slice(1, None, 2), slice(0, None, 2)), (slice(1, None, 2), slice(1, None, 2)), ]
for location, mesh in zip(locations, meshes): plane = output[location] yy, xx = np.meshgrid( np.linspace(0, 1, plane.shape[0]), np.linspace(0, 1, plane.shape[1]), indexing="ij", ) output[location] = plane * bilinear_sample(mesh, xx, yy)
return output生产实现通常边扫描边递增插值权重,不会真的创建与图像同尺寸的 xx、yy 数组。硬件还可能把增益量化成定点数,所以要测试系数量化、累加位宽和舍入误差是否产生条带或网格边界。
LSC 在 ISP 管线的什么位置
一条简化的 RAW 路径可以写成:
RAW 读取 ↓BLC 黑电平校正 ↓DPC / RAW 域降噪 ↓LSC 镜头阴影校正 ↓Bayer 域 WB ↓Debayer ↓CCM / Tone Mapping / Gamma具体产品会调整 DPC、降噪、LSC 和 WB 的次序,但几个原则很稳定。
BLC 通常在 LSC 前
LSC 应放大有效信号,不应放大电子偏置。正确关系是:
若先乘空间增益再减固定 black,角落会残留随增益变化的偏置,表现为黑位不均和颜色阴影。
LSC 通常在 Debayer 前
颜色阴影发生在不同 CFA 样本的采集阶段。在 Bayer 域分别应用四平面 mesh,可以在插值混合相邻通道前修正原始样本,也避免为完整 RGB 保存三张全分辨率增益图。
Debayer 后做图像域 vignette correction 并非绝对错误,它适合通用照片编辑或只有 RGB 图像的场景;但它很难准确还原 Bayer 四平面差异,也可能把已经扩散到邻域的颜色阴影一起处理。
LSC 与 WB 都是乘法,但维度不同
WB gain 随通道变化、通常不随位置变化;LSC gain 同时随通道和位置变化。在线性浮点且没有中间裁剪时,两次标量乘法可以交换:
但定点量化、增益上限、裁剪和不同模块位宽会破坏严格等价。工程上分开保存更容易针对镜头状态更新 LSC、针对光源更新 WB,也方便调试增益预算。
校正会放大四角噪声
设角落信号为 ,噪声为 。乘以 后:
信号均值得到恢复,噪声幅度却也乘了 。LSC 无法重新收集进入镜头时已经损失的光子;它只是改变数字比例。因此一张“亮度完全均匀”的校正图,角落信噪比仍可能比中心差。
Shot noise 与信号相关,read noise 又有不同模型,真实噪声传播不能只用一个固定标准差描述。但结论不变:角落增益越大,暗部噪声越显眼。这也是 LSC 常与空间相关降噪强度、最大增益限制一起设计的原因。
高光与余量同样要算清楚
如果角落测量值因为阴影只有 0.6,乘 2.0 会得到 1.2。若中间格式允许浮点或留有整数 headroom,后续曝光、tone mapping 仍能处理这个值;若 LSC 模块立即裁剪到 1.0,超过上限的差异就消失了。
四个 Bayer 平面的增益不同,还可能让某一通道最先越界,从而形成高光偏色。标定时把中心归一化为 1.0 并不代表任何真实场景都不会溢出,整个 ISP 必须共同规划 white level、WB、LSC 与数字增益的余量。
校正强度、噪声与高光余量
空间增益不会免费创造信号:它会一起抬高角落信号、噪声和高光。比较保留浮点与立即裁剪。
欠校正会保留暗角,过校正会制造亮角。高增益区域通常还需要更强的降噪或增益上限;裁剪应尽量延后。
“为了不裁剪,把整张图除以最大 LSC gain”可以换取数值空间,但会降低整体亮度或有效精度;它并不能恢复传感器已经饱和的通道。保留更宽中间位深、延后裁剪通常更稳妥。
什么时候一张 LSC 表不够
变焦镜头在不同焦距下光学结构明显变化;光圈改变遮挡;对焦会移动镜组;不同光谱让颜色阴影发生变化。因此相机可能保存多组标定表,并根据运行状态插值:
- 焦距节点之间插值;
- 对焦距离或光圈节点之间插值;
- 不同色温或光源类别之间插值;
- 模组个体表与典型 golden table 混合。
这又引入一层参数空间插值。切表必须平滑,否则视频中会看到角落亮度或颜色突然跳变。若设备只使用一张典型表,则要验证模组公差下的最坏情况,并避免过度校正。
常见错误怎样从画面反推
四角仍然偏暗:增益不足、坐标覆盖范围不对、使用了不匹配的焦距表,或平场标定把光源暗角误当成镜头特性。
四角反而变亮:增益过大、中心参考选择错误,或同一份校正被应用了两次。
出现彩色棋盘或 2×2 周期纹:Bayer pattern、有效区起点或 R/Gr/Gb/B 表映射错误。
出现网格边界:节点顺序、插值坐标或定点精度错误;也可能是增益表本身不连续。
亮度拉平但颜色仍偏:只使用了公共 luma mesh,没有分别校正四个 Bayer 平面,或标定光谱与实际光源相差太大。
角落彩噪明显:可能是预期的噪声放大,不一定是插值错误。检查原始角落 SNR、最大增益和空间降噪策略。
高光边缘变色:不同通道在 LSC 或后续 WB 后先后饱和,且中间阶段过早裁剪。
验证一套 LSC 的清单
实现完成后,不要只凭一张普通照片判断。至少检查:
- 单位表测试:所有节点设为
1.0,输出应与输入一致。 - 常数输入测试:给定解析 gain mesh,逐点比对 CPU 参考结果。
- 节点与边界测试:像素恰好落在节点、四条边和四个角时,索引不能越界。
- CFA 映射测试:用 R、Gr、Gb、B 四张明显不同的常数表确认位置对应。
- 平场均匀性:比较校正前后中心、边缘和角落的均值与色度残差。
- 独立数据验证:生成表和验证表不能完全来自同一帧,避免把噪声当成能力。
- 噪声检查:测量不同半径处的均值、方差与 SNR,确认角落代价符合预期。
- 高光检查:在 LSC 与 WB 的组合最大增益下验证位宽、white level 与裁剪位置。
- 状态切换检查:变焦、对焦、光圈或色温切表时,视频不能闪烁或跳色。
- 元数据检查:确认有效区、裁切、旋转、镜像和 binning 后的坐标仍匹配标定。
最后记住:LSC 修正响应,不恢复光子
LSC 的数学核心只是一个随位置变化的乘法:测得哪里响应弱,就在那里使用更大增益。gain mesh 与二维双线性插值让这张空间函数可以被紧凑保存,Bayer 四平面又让亮度阴影与颜色阴影能够同时处理。
但“校正后看起来均匀”不等于各处图像质量相同。角落失去的光子不会因为乘法回来,噪声会随增益上升,高光也需要更多数值余量。真正可靠的 LSC,既要把平场拉平,也要把 CFA 映射、标定条件、插值坐标、噪声和整条 ISP 的增益预算一起算清楚。