4. 镜头校正:光学暗角、畸变与色差的消除

镜头校正,说白了就是让镜头“说实话”。

再贵的镜头,物理上也不可能完美。光线穿过一堆玻璃片,总会留下各种“副作用”——边缘变暗、直线变弯、彩色条纹。我刚开始做计算摄影时,总觉得算法能搞定一切,结果第一次用廉价镜头拍测试图卡,校正后的图像边缘全是伪影。嗯,从那以后我再也不敢小看镜头校正这一步了。

4.1 光学暗角(Vignetting)的成因与校正

暗角就是照片四角比中心暗的现象。你想想看,为什么会出现这种情况?

4.1.1 成因分析

暗角主要有三种来源:

  • 自然暗角(Natural Vignetting):光线斜射入镜头时,到达传感器边缘的光通量自然比中心少。这是物理定律,躲不开。
  • 光学暗角(Optical Vignetting):镜头镜筒的机械结构遮挡了边缘光线。光圈收小一点会改善。
  • 像素暗角(Pixel Vignetting):传感器像素本身对斜射光的响应不如垂直光。微透镜阵列设计可以缓解。
我的经验: 实际项目中,自然暗角占大头。我用过一款手机镜头,全开光圈时边缘亮度只有中心的40%。校正后虽然能拉回来,但噪点也被放大了——这就是代价。

4.1.2 校正模型

最常用的暗角校正是基于径向衰减模型。说白了,就是用一个多项式来描述亮度从中心到边缘的下降曲线。

// 暗角校正增益计算
// r 为归一化半径(0~1),中心为0,角落为1
// params 为4阶多项式系数 [k0, k1, k2, k3, k4]

float correctVignette(float r, float* params) {
    float r2 = r * r;
    float r4 = r2 * r2;
    float gain = 1.0f 
               + params[0] * r2 
               + params[1] * r4 
               + params[2] * r2 * r4
               + params[3] * r4 * r4;
    return gain;  // 像素值乘以这个增益
}

校正流程分三步:

  1. 标定:拍一张均匀光照的灰卡,提取每个像素的衰减系数。
  2. 拟合:用径向多项式拟合衰减曲线,得到4~6个系数。
  3. 应用:对每张照片,按像素半径计算增益并乘回去。
注意: 校正增益不要超过3倍。我曾经为了追求四角亮度一致,把增益拉到5倍,结果角落的噪点惨不忍睹。暗角校正和降噪要配合着做。

4.2 镜头畸变(Distortion)的数学模型与校正

畸变就是直线变弯了。拍建筑时最明显——楼房的边缘会向外鼓或者向内凹。

4.2.1 畸变类型

类型 现象 常见场景
桶形畸变 直线向外弯曲 广角镜头、手机主摄
枕形畸变 直线向内弯曲 长焦镜头、变焦镜头远端
须形畸变 混合形态 变焦镜头中间焦段

4.2.2 数学模型

畸变校正的核心是布朗-康拉迪(Brown-Conrady)模型。这个模型把畸变分解为径向分量和切向分量。

// 畸变校正:从畸变坐标映射到理想坐标
// (xd, yd) 为畸变图像坐标(归一化)
// (xu, yu) 为校正后理想坐标
// k1, k2, k3 为径向畸变系数
// p1, p2 为切向畸变系数

void undistortPoint(float xd, float yd, 
                    float& xu, float& yu,
                    float k1, float k2, float k3,
                    float p1, float p2) {
    float r2 = xd*xd + yd*yd;
    float r4 = r2 * r2;
    float r6 = r4 * r2;
    
    // 径向畸变
    float radial = 1.0f + k1*r2 + k2*r4 + k3*r6;
    
    // 切向畸变
    float tx = 2*p1*xd*yd + p2*(r2 + 2*xd*xd);
    float ty = p1*(r2 + 2*yd*yd) + 2*p2*xd*yd;
    
    xu = xd * radial + tx;
    yu = yd * radial + ty;
}

关键点: 畸变校正是“反向映射”——我们遍历校正后的图像,反查它在原图中的位置。这样能避免出现空洞。

4.2.3 校正流程

我习惯用OpenCV的标定流程,但实际项目中会做两点优化:

  • 棋盘格标定:拍20~30张不同角度的棋盘格照片,提取角点。
  • 参数优化:用Levenberg-Marquardt算法最小化重投影误差。
  • 查表加速:把映射关系预计算成LUT,运行时直接查表。
避坑指南: 我曾经用15张棋盘格标定,结果畸变校正后边缘还是有残留弯曲。后来增加到30张,覆盖了画面各个区域,效果才稳定。标定照片数量不能省。

4.3 色差(Chromatic Aberration)的检测与消除

色差就是物体边缘出现红/蓝或紫/绿的条纹。说白了,是不同波长的光在镜头里折射率不同,导致它们聚焦不到同一个点上。

4.3.1 色差分类

类型 表现 成因
纵向色差 焦平面偏移,红蓝模糊程度不同 不同波长聚焦距离不同
横向色差 边缘出现彩色条纹,红蓝向两侧分离 不同波长放大率不同

4.3.2 检测方法

我常用的检测方法是梯度相关法:

  1. 将图像分解为R、G、B三个通道。
  2. 计算每个通道的梯度幅值。
  3. 在边缘区域,比较R和B通道的梯度方向差异。
  4. 如果差异超过阈值,标记为色差区域。
// 横向色差检测:计算R和B通道的偏移量
// 返回水平方向的像素偏移

float detectLateralCA(const float* r_channel, 
                      const float* b_channel,
                      int width, int height) {
    float total_offset = 0.0f;
    int count = 0;
    
    for (int y = 1; y < height-1; y++) {
        for (int x = 1; x < width-1; x++) {
            // 计算R通道水平梯度
            float gx_r = r_channel[y*width + x+1] 
                       - r_channel[y*width + x-1];
            // 计算B通道水平梯度
            float gx_b = b_channel[y*width + x+1] 
                       - b_channel[y*width + x-1];
            
            // 梯度幅值足够大才统计
            if (fabs(gx_r) > 0.1f && fabs(gx_b) > 0.1f) {
                // 偏移量 = 梯度方向差异
                float offset = (gx_r - gx_b) / 
                              (fabs(gx_r) + fabs(gx_b) + 1e-6f);
                total_offset += offset;
                count++;
            }
        }
    }
    return total_offset / count;
}

4.3.3 消除策略

色差消除分两步走:

  • 横向色差:对R和B通道做局部缩放。以G通道为参考,用多项式拟合R和B的缩放系数。
  • 纵向色差:对R和B通道做局部去模糊。用维纳滤波或RL反卷积,恢复清晰度。
注意: 色差校正不要过度。我曾经在一个高反差场景(白墙前的黑树枝)上做色差消除,结果把树枝边缘的紫色条纹去掉了,但同时也把树枝本身的纹理抹平了。校正强度要控制在肉眼可见但不过度的范围。

4.4 知识体系总览

下面这张图总结了镜头校正的完整流程和三个核心模块的关系:

镜头校正知识体系 镜头校正知识体系 RAW 图像输入 暗角校正 自然暗角 光学暗角 像素暗角 径向多项式校正 增益 ≤ 3x 畸变校正 桶形 / 枕形 / 须形 Brown-Conrady 模型 径向 + 切向分量 棋盘格标定 LUT 加速 色差消除 纵向色差 横向色差 梯度检测 通道缩放 / 去模糊 避免过度校正 校正后图像 ⚠ 注意事项 校正顺序:暗角 → 畸变 → 色差 先降噪再校正,避免放大噪声

4.5 校正顺序与工程实践

三个校正步骤的顺序不能乱。我踩过这个坑:

  • 先做暗角校正:因为暗角校正会改变像素亮度,影响后续梯度计算。
  • 再做畸变校正:畸变校正会重映射像素位置,色差校正依赖空间一致性。
  • 最后做色差消除:色差检测需要准确的边缘信息,前面的校正能提供更干净的边缘。

工程建议: 在移动端实现时,把三个校正合并成一个LUT。输入像素坐标,直接输出校正后的像素值和增益。这样能省掉三次插值,性能提升明显。我在一个项目中用这个方法,把单帧处理时间从15ms降到了5ms。

嗯,镜头校正这部分就讲到这里。记住一个原则:校正不是万能的,过度校正比不校正更糟糕。保留一点物理缺陷,往往比强行拉平所有指标更自然。