鬼影的数学建模:从光流到亮度映射

做HDR合成这么多年,我踩过最大的坑就是鬼影。你以为对齐了就没事了?其实鬼影的本质,是运动物体在不同曝光帧里位置和亮度都不一致。要干掉它,得先把它数学化。

这一章,我们就来建立鬼影的数学模型。说白了,就是搞清楚三件事:物体怎么动、运动差异怎么量化、亮度怎么变。

基于光流场的运动估计模型

光流,听起来高大上,其实核心假设就一个:同一个像素点,在相邻帧之间亮度不变。数学上写成:

I(x, y, t) = I(x + dx, y + dy, t + dt)

意思是,t时刻在(x,y)位置的像素,到了t+dt时刻,移动到了(x+dx, y+dy)位置,亮度保持不变。把这个等式做泰勒展开,忽略高阶项,就得到光流约束方程:

Ix * u + Iy * v + It = 0

其中u和v就是我们要估计的水平方向和垂直方向运动矢量。

关键点:光流估计的是亚像素精度的运动,不是整像素。这对HDR合成很重要——曝光帧之间的运动往往不是整数个像素。

我个人习惯用Farnebäck多项式展开法做稠密光流。它比Lucas-Kanade法更鲁棒,尤其在纹理稀疏区域。我在项目中遇到过好几次,用LK法算出来的光流在天空区域全是噪声,换成Farnebäck就好多了。

像素级 vs 块级运动差异分析

光流算出来之后,怎么判断哪里是鬼影?这里有个核心概念:运动差异。

我把它分成两个层次:

层次 定义 优点 缺点
像素级 每个像素独立计算运动矢量 精度高,能捕捉细微运动 对噪声敏感,计算量大
块级 将图像分成N×N块,每块估计一个运动矢量 速度快,抗噪性强 运动边界模糊,块效应明显

你想想看,如果只用像素级,遇到纹理平坦区域,光流根本算不准。如果只用块级,运动物体的边缘就会糊成一团。怎么办?

我的做法是:先做块级粗估计,再做像素级精调。具体来说:

  1. 将图像分成16×16的块,用SAD(差和)做块匹配,得到初始运动矢量
  2. 在初始矢量附近做像素级光流精化,搜索半径设为±2像素
  3. 对精化后的光流做中值滤波,去除离群点

避坑指南:我曾经在块匹配时直接用全搜索,结果处理4K图像慢到怀疑人生。后来改成三步搜索法,速度提升了5倍,精度损失不到1%。

曝光时间差异导致的亮度非线性映射

这是新手最容易忽略的问题。你以为两帧之间只有运动?不对,还有亮度变化。

HDR的多帧曝光时间不同,比如1/30秒和1/1000秒。即使同一个静止物体,在不同曝光帧里的亮度值也不是简单的线性关系。因为相机的响应曲线(CRF)是非线性的。

数学上,亮度映射可以写成:

I_short = f( k * I_long )

其中k是曝光时间比,f是相机的响应函数。这个f通常不是线性的,尤其在暗部和亮部区域。

所以,在做光流估计之前,必须先做亮度归一化。我的标准流程是:

  • 用Debevec方法标定相机响应曲线
  • 将所有帧映射到同一辐射域
  • 再做光流估计

注意:如果不做亮度归一化,光流估计会把亮度变化误判为运动。我在一个夜景HDR项目里就吃过这个亏——路灯区域的鬼影怎么都去不掉,后来发现是亮度非线性导致的伪运动。

完整模型框架

把上面三部分整合起来,鬼影的数学建模流程就是:

鬼影数学建模流程 多曝光帧输入 步骤1:亮度归一化(CRF标定) 步骤2:块级粗估计(16×16块匹配) 步骤3:像素级精调(Farnebäck光流) 步骤4:运动差异检测 → 鬼影掩码 鬼影区域掩码输出 核心假设 亮度不变假设 运动连续性假设 CRF可标定假设

嗯,这个框架图基本概括了我们的建模思路。从输入到输出,每一步都有明确的数学依据。

实际项目中的经验参数

最后分享一组我在实际项目中调出来的参数,供你参考:

参数 推荐值 说明
块匹配尺寸 16×16 太大则运动边界模糊,太小则噪声大
搜索半径 ±8像素 覆盖大多数手持拍摄抖动
光流精化半径 ±2像素 亚像素精度调整
中值滤波核 5×5 去除光流离群点
运动差异阈值 3.0(像素) 超过此值判定为鬼影区域

核心总结:鬼影的数学建模,本质是运动估计 + 亮度归一化的组合。光流提供运动信息,CRF标定提供亮度映射,两者缺一不可。