4、镜头阴影校正(Lens Shading Correction)

镜头阴影校正,圈里人常叫 LSC(Lens Shading Correction)。说白了,就是解决照片四周发暗、发绿的问题。你想想看,用手机拍一张纯色的白墙,是不是经常发现中间亮、四周暗?这就是镜头阴影,也叫 Vignetting。

我刚开始做 ISP 的时候,觉得这玩意儿不就是个简单的增益嘛,四周乘个系数就完事了。后来踩了坑才发现,这里面的门道比我想象的多得多。今天咱们就把成因、校正表生成、还有径向渐变算法,一次性讲透。

4.1 镜头阴影的成因

镜头阴影不是单一原因造成的。我习惯把它拆成两类:亮度衰减和色彩偏移。

4.1.1 亮度衰减(Luminance Shading)

这个好理解。光线从镜头中心进入传感器,和从边缘进入传感器,路径长度不一样。边缘的光线要走更远的路,而且入射角度也偏了。根据 cos⁴θ 定律,边缘的照度会急剧下降。

cos⁴θ 定律:传感器上某点的照度,正比于该点与光轴夹角的余弦值的四次方。

θ = 0° 时,照度 100%;θ = 30° 时,照度只剩约 56%。

除了物理光学原因,还有镜头遮挡。有些镜头模组的设计,镜筒本身就会挡住一部分边缘光线。尤其是小光圈的时候,这个效应更明显。

4.1.2 色彩偏移(Color Shading)

这个坑我印象特别深。有一次调一个广角模组,亮度校正做完后,四周还是发绿。查了半天才发现,是微透镜阵列(Microlens Array)的问题。

Bayer 传感器的每个像素上面都有一个微透镜,用来把光线汇聚到感光区域。但边缘像素的微透镜,接收到的光线角度偏大,导致不同颜色的像素响应不一致。通常红色通道衰减最快,蓝色次之,绿色相对好一些。结果就是:四周偏绿。

阴影类型 根本原因 表现特征 校正难度
亮度衰减 cos⁴θ 定律、镜头遮挡 四周均匀变暗 低(单通道增益即可)
色彩偏移 微透镜入射角差异 四周偏绿/偏紫 中(需分通道处理)

4.2 增益校正表生成

校正的核心思路很简单:给暗的地方补光,给亮的地方不动。但怎么补、补多少,这里就有讲究了。

4.2.1 标定流程

我个人习惯用 均匀光源 + 灰卡 来做标定。具体步骤是这样的:

  1. 拍摄均匀灰卡:用积分球或者均匀光源,拍一张 RAW 图。确保画面中间区域亮度在 50% 左右,别过曝也别欠曝。
  2. 提取 Bayer 通道:把 RAW 图拆成 R、Gr、Gb、B 四个通道。注意,Gr 和 Gb 在镜头阴影下表现不一样,别偷懒合并。
  3. 计算参考值:取图像中心区域(比如 10% 半径内)的像素均值,作为每个通道的参考亮度。
  4. 生成增益矩阵:对于每个像素位置 (x, y),增益 = 参考值 / 当前像素值。
  5. 平滑与压缩:原始增益矩阵有噪声,需要做平滑滤波。然后压缩成网格形式,方便硬件查表。

我的经验:标定时光源色温要跟实际场景匹配。用 D65 光源标定的校正表,拿到白炽灯下用,色彩偏移反而会更严重。

4.2.2 网格增益表

硬件不可能存一整张跟图像一样大的增益矩阵。通常的做法是:存一个稀疏网格,比如 16×16 或者 32×32 的网格点。每个网格点存一个增益值,中间的点靠插值算出来。

// 网格增益表结构示例
struct LSC_GainTable {
    uint16_t grid_width;      // 网格列数,如 16
    uint16_t grid_height;     // 网格行数,如 16
    uint16_t block_size_x;    // 每个网格覆盖的像素列数
    uint16_t block_size_y;    // 每个网格覆盖的像素行数
    uint16_t gains[16][16];   // 增益值,Q4.12 定点格式
};

这里有个细节:增益值的精度。我见过有人用 8bit 存增益,结果校正后出现带状条纹。建议至少用 12bit 定点数,Q4.12 格式比较常见,能表示 0 到 15.999 的增益,精度 1/4096。

4.3 径向渐变校正算法

网格增益表虽然通用,但计算量大。对于很多嵌入式平台,径向渐变模型更实用。它假设镜头阴影是中心对称的,只跟像素到图像中心的距离有关。

4.3.1 数学模型

径向模型用多项式拟合增益曲线:

G(r) = 1 + a₁·r² + a₂·r⁴ + a₃·r⁶

其中:
r = sqrt((x - cx)² + (y - cy)²) / R_max
cx, cy 是光轴中心坐标
R_max 是图像对角线的一半
a₁, a₂, a₃ 是拟合系数

为什么只用偶次项?因为镜头阴影是径向对称的,奇次项会引入不对称。我刚开始做的时候用了奇次项,结果校正后的图像一边亮一边暗,找了半天 bug。

4.3.2 分通道拟合

亮度衰减和色彩偏移要分开处理。我通常的做法是:

  • 亮度校正:四个通道共用一组径向系数,只校正整体亮度。
  • 色彩校正:R、B 通道各自独立拟合,G 通道作为参考基准。

这样做的好处是:亮度校正可以用低阶多项式(2 阶就够了),色彩校正需要高阶一些(4 阶或 6 阶)。

注意:径向模型假设光轴中心在图像正中心。但实际模组组装有公差,光轴中心可能偏移几个像素。标定时一定要先标定光轴中心,否则校正效果会打折扣。

4.3.3 硬件实现要点

在硬件里实现径向校正,我总结了几条经验:

  1. 查表代替计算:r² 到 r⁶ 的计算开销大,预计算一个增益查找表,用 r² 做索引。
  2. 定点化:系数用 Q2.14 格式,r² 用 Q0.16 格式,乘法结果截断时注意舍入。
  3. 边界处理:图像四角的增益可能超过 4 倍,要限制最大增益,防止噪声被过度放大。
// 径向校正查表实现(伪代码)
uint16_t lsc_lut[1024];  // 预计算的增益表,索引 0~1023 对应 r²=0~1.0

for (each pixel (x, y)) {
    float dx = x - cx;
    float dy = y - cy;
    float r2 = (dx*dx + dy*dy) / (R_max * R_max);
    int idx = (int)(r2 * 1023.0f + 0.5f);
    idx = clamp(idx, 0, 1023);
    uint16_t gain = lsc_lut[idx];
    pixel_out = pixel_in * gain >> 12;  // 定点乘法
}

4.4 知识结构总览

下面这张图,我把镜头阴影校正的完整链路画出来了。从成因分析到标定流程,再到两种校正算法的选择,一目了然。

镜头阴影校正(LSC)知识结构 成因分析 亮度衰减 色彩偏移 标定流程 均匀光源 增益矩阵 校正算法 网格法 径向法 径向渐变校正算法详解 数学模型 G(r)=1+a₁r²+a₂r⁴+a₃r⁶ 仅偶次项,保证对称 分通道拟合 亮度:共用系数 色彩:R/B 独立拟合 硬件实现 查表代替多项式计算 定点化 + 增益限幅 关键点:光轴中心标定 → 分通道处理 → 增益限幅防噪声放大 网格法灵活但存储大,径向法高效但依赖对称假设

4.5 避坑指南

最后分享几个我踩过的坑,希望能帮你少走弯路。

坑一:光轴中心不准

我曾经用图像几何中心代替光轴中心做径向校正,结果四周校正过度,出现亮环。后来用标定板重新标定光轴中心,问题才解决。标定方法:拍一张点阵图,找畸变最小的点作为光轴中心。

坑二:增益过大导致噪声爆炸

图像四角的增益有时候会到 4 倍以上。如果不限幅,暗区的噪声会被放大到肉眼可见。我一般把最大增益限制在 3.0 倍,超过的部分用相邻像素插值。

坑三:温度漂移

镜头阴影会随温度变化。传感器温度升高,微透镜的折射率会变,色彩偏移也跟着变。如果产品工作温度范围大,建议做多组温度标定表,根据温度传感器动态切换。

嗯,镜头阴影校正这块,说难不难,说简单也不简单。核心就是理解成因、选对模型、做好标定。你只要把径向模型吃透了,再去看网格法,会发现很多思路是相通的。