鬼影的数学建模:从光流到亮度映射
做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块,每块估计一个运动矢量 | 速度快,抗噪性强 | 运动边界模糊,块效应明显 |
你想想看,如果只用像素级,遇到纹理平坦区域,光流根本算不准。如果只用块级,运动物体的边缘就会糊成一团。怎么办?
我的做法是:先做块级粗估计,再做像素级精调。具体来说:
- 将图像分成16×16的块,用SAD(差和)做块匹配,得到初始运动矢量
- 在初始矢量附近做像素级光流精化,搜索半径设为±2像素
- 对精化后的光流做中值滤波,去除离群点
避坑指南:我曾经在块匹配时直接用全搜索,结果处理4K图像慢到怀疑人生。后来改成三步搜索法,速度提升了5倍,精度损失不到1%。
曝光时间差异导致的亮度非线性映射
这是新手最容易忽略的问题。你以为两帧之间只有运动?不对,还有亮度变化。
HDR的多帧曝光时间不同,比如1/30秒和1/1000秒。即使同一个静止物体,在不同曝光帧里的亮度值也不是简单的线性关系。因为相机的响应曲线(CRF)是非线性的。
数学上,亮度映射可以写成:
I_short = f( k * I_long )
其中k是曝光时间比,f是相机的响应函数。这个f通常不是线性的,尤其在暗部和亮部区域。
所以,在做光流估计之前,必须先做亮度归一化。我的标准流程是:
- 用Debevec方法标定相机响应曲线
- 将所有帧映射到同一辐射域
- 再做光流估计
注意:如果不做亮度归一化,光流估计会把亮度变化误判为运动。我在一个夜景HDR项目里就吃过这个亏——路灯区域的鬼影怎么都去不掉,后来发现是亮度非线性导致的伪运动。
完整模型框架
把上面三部分整合起来,鬼影的数学建模流程就是:
嗯,这个框架图基本概括了我们的建模思路。从输入到输出,每一步都有明确的数学依据。
实际项目中的经验参数
最后分享一组我在实际项目中调出来的参数,供你参考:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 块匹配尺寸 | 16×16 | 太大则运动边界模糊,太小则噪声大 |
| 搜索半径 | ±8像素 | 覆盖大多数手持拍摄抖动 |
| 光流精化半径 | ±2像素 | 亚像素精度调整 |
| 中值滤波核 | 5×5 | 去除光流离群点 |
| 运动差异阈值 | 3.0(像素) | 超过此值判定为鬼影区域 |
核心总结:鬼影的数学建模,本质是运动估计 + 亮度归一化的组合。光流提供运动信息,CRF标定提供亮度映射,两者缺一不可。