4、镜头阴影校正(LSC):边缘变暗的“罪魁祸首”

你有没有拍过这样的照片——画面中心挺亮,但四个角明显暗下去,像戴了个暗角滤镜?

这就是镜头阴影,也叫“边缘亮度衰减”。

我刚开始做ISP调试时,第一次看到这个现象还以为是传感器坏了。后来才知道,这其实是物理规律在捣乱。说白了,镜头本身就是一个光学系统,光线从中心到边缘走过的路径不一样,能量自然就衰减了。

4.1 镜头阴影现象:为什么边缘会变暗?

镜头阴影的成因主要有两个:

  • 光程差:边缘光线比中心光线走的路更长,能量衰减更严重。
  • 余弦四次方定律:边缘光线入射角大,传感器接收到的光强与cos⁴θ成正比。角度越大,亮度越低。

你想想看,一个广角镜头,边缘入射角可能到30°甚至40°,cos⁴(40°) ≈ 0.35,也就是说边缘只有中心35%的亮度。这差距,肉眼一看就明显。

关键指标:镜头阴影通常用“中心与边缘亮度比”来衡量。一般消费级镜头,边缘亮度是中心的40%~60%;专业级镜头能做到70%以上。

我在项目中遇到过一款手机镜头,边缘亮度只有中心的30%。嗯,这种镜头如果不做校正,拍出来的照片根本没法看。

4.2 LSC原理:增益矩阵

镜头阴影校正(LSC)的原理其实很简单——给每个像素乘一个增益系数。

中心区域亮度高,增益小一点;边缘区域亮度低,增益大一点。最终让整幅画面亮度均匀。

这个增益系数怎么来?

通常的做法是:在均匀光照下拍一张“平场”图像,然后计算每个像素相对于中心亮度的比值,生成一个增益矩阵。

公式很简单:

校正后像素值 = 原始像素值 × 增益系数

但这里有个坑——增益不能无限大。边缘太暗的话,增益一放大,噪声也跟着放大。我曾经调试过一款低端传感器,边缘增益到了3.0,结果暗角是没了,但边缘全是噪点,比暗角还难看。

注意:增益矩阵通常按通道分别计算。因为不同颜色通道的衰减程度不一样,尤其是Bayer阵列中的R、G、B通道,需要各自独立的增益矩阵。

4.3 径向与多项式校正模型

实际工程中,我们不会给每个像素都存一个增益值——那样太占内存了。通常用数学模型来拟合增益曲线。

4.3.1 径向模型

径向模型假设镜头阴影是中心对称的。增益只与像素到图像中心的距离有关。

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

其中r是归一化半径(0~1),a₁、a₂、a₃是拟合系数。

这个模型简单高效,适合大多数镜头。但有个前提——镜头必须是完美的中心对称。我遇到过一些廉价镜头,光轴偏了,径向模型怎么调都调不好。

4.3.2 多项式模型

多项式模型更灵活,它用二维多项式来拟合增益曲面:

gain(x, y) = c₀ + c₁·x + c₂·y + c₃·x² + c₄·xy + c₅·y² + ...

这个模型能处理非对称的阴影,比如镜头安装偏了、传感器倾斜等情况。代价是计算量更大,需要更多系数。

模型 优点 缺点 适用场景
径向模型 参数少、计算快 只能处理对称阴影 高端镜头、中心对称好
多项式模型 能处理非对称阴影 参数多、计算量大 廉价镜头、光轴偏移

我个人习惯是:先用径向模型试试,如果残差太大再换多项式。毕竟少几个系数,校准流程也简单些。

4.4 代码实现:从平场到校正

下面我写一个完整的LSC实现流程。假设我们已经拍好了平场图像。

import numpy as np
import cv2

def generate_gain_map(flat_field, target_value=None):
    """
    从平场图像生成增益矩阵
    flat_field: 均匀光照下拍摄的RAW图像
    target_value: 目标亮度,默认取中心区域均值
    """
    if target_value is None:
        h, w = flat_field.shape
        center_region = flat_field[h//4:3*h//4, w//4:3*w//4]
        target_value = np.mean(center_region)
    
    # 防止除零
    flat_field = np.maximum(flat_field, 1)
    gain_map = target_value / flat_field
    
    # 限制最大增益,防止噪声放大
    gain_map = np.clip(gain_map, 0.5, 3.0)
    
    return gain_map

def apply_lsc(raw_image, gain_map):
    """
    应用镜头阴影校正
    """
    # 确保数据类型一致
    raw_image = raw_image.astype(np.float32)
    gain_map = gain_map.astype(np.float32)
    
    # 逐像素相乘
    corrected = raw_image * gain_map
    
    # 裁剪到有效范围
    corrected = np.clip(corrected, 0, 65535).astype(np.uint16)
    
    return corrected

# 使用示例
if __name__ == "__main__":
    # 加载平场图像和原始RAW图
    flat = cv2.imread('flat_field.tiff', cv2.IMREAD_UNCHANGED)
    raw = cv2.imread('raw_image.tiff', cv2.IMREAD_UNCHANGED)
    
    # 生成增益矩阵
    gain = generate_gain_map(flat)
    
    # 应用校正
    result = apply_lsc(raw, gain)
    
    # 保存结果
    cv2.imwrite('corrected.tiff', result)

工程技巧:实际项目中,增益矩阵不需要每帧都算。通常在生产线上标定一次,把系数存到设备参数里。运行时直接查表或计算多项式即可。

4.5 LSC知识体系总览

下面这张图帮你理清LSC的完整流程:

镜头阴影校正知识体系 镜头阴影校正(LSC)知识体系 问题:边缘亮度衰减 成因:光程差 + 余弦四次方定律 核心原理:增益矩阵 径向模型 多项式模型 查表法 代码实现:平场采集 → 增益计算 → 逐像素校正

4.6 避坑指南

做LSC这些年,我踩过不少坑。挑几个典型的说说:

  • 平场图像要拍准:我曾经用一张有灰尘的平场图做标定,结果校正后照片上全是灰尘的影子。后来我学乖了,每次拍平场前先清洁镜头和传感器。
  • 增益不能太大:前面说过,增益超过3.0基本就废了。我一般限制在2.5以内,剩下的靠镜头设计去解决。
  • 注意温度变化:镜头阴影会随温度漂移。我在户外项目中发现,夏天和冬天用同一组系数,效果差很多。后来加了温度补偿,才稳定下来。
  • Bayer通道分开处理:千万别偷懒用同一个增益矩阵处理所有通道。R通道衰减最严重,B通道次之,G通道相对好一些。分开处理才能保证白平衡正确。

一句话总结:镜头阴影校正就是给边缘“补光”,但补多少、怎么补,需要根据镜头特性和场景灵活调整。没有万能参数,只有不断调试出来的最佳方案。