2. 多帧HDR原理:短帧、中帧、长帧的曝光策略,以及如何对齐与合成
多帧HDR,说白了就是让传感器连续拍三张照片——一张欠曝、一张正常、一张过曝,然后把它们合成一张高动态范围的图像。听起来简单,但这里面的门道可不少。我刚开始做HDR时,以为就是简单地把三张图叠在一起,结果出来的图像要么鬼影重重,要么亮度断层,惨不忍睹。
这一章,我就把多帧HDR的核心逻辑拆开来讲。包括曝光策略怎么定、帧间怎么对齐、合成算法怎么选。这些都是我在项目里踩过坑、填过坑之后沉淀下来的经验。
2.1 短帧、中帧、长帧的曝光策略
先说说三帧的曝光时间怎么分配。这直接决定了HDR的动态范围能拉到多宽。
| 帧类型 | 曝光时间比例 | 目标场景亮度 | 典型应用场景 |
|---|---|---|---|
| 短帧(Short) | 1x(基准) | 高亮区域(天空、灯光) | 逆光、强光源场景 |
| 中帧(Middle) | 4x ~ 8x | 中间调(人脸、建筑) | 室内外过渡场景 |
| 长帧(Long) | 16x ~ 32x | 暗部区域(阴影、夜景) | 低照度、大光比场景 |
我个人习惯把短帧作为基准帧。为什么?因为短帧的曝光时间最短,运动模糊最小,鬼影也最少。中帧和长帧的曝光时间,都是基于短帧来推算的。
举个例子。如果短帧曝光时间是1ms,中帧我通常会设到4ms到8ms,长帧设到16ms到32ms。这样三帧的亮度跨度大概在4到5档EV之间,覆盖大部分真实场景的动态范围。
2.2 帧间对齐:消除鬼影的关键
三帧图像不是同时拍的,中间有时间差。如果场景里有运动物体——比如行人、车辆、树叶晃动——那三帧里同一个物体的位置就不一样。直接合成,就会出现鬼影。
对齐的目的,就是把中帧和长帧的图像内容,通过运动补偿,对齐到短帧的坐标系上。
2.2.1 全局运动对齐
全局运动,说白了就是相机本身的抖动或平移。这种情况用仿射变换或透视变换就能搞定。
// 全局对齐示例:基于特征点匹配
// 1. 提取短帧和长帧的ORB特征点
// 2. 使用FLANN匹配器找到对应点对
// 3. 通过RANSAC剔除误匹配,计算单应性矩阵H
// 4. 将长帧图像通过H矩阵变换到短帧坐标系
cv::Mat H = cv::findHomography(keypoints_long, keypoints_short, cv::RANSAC, 3.0);
cv::warpPerspective(long_frame, long_aligned, H, short_frame.size());
我在项目中遇到过一个问题:全局对齐对旋转和缩放很敏感。如果相机在拍摄过程中发生了明显的旋转,单应性矩阵的估计会不稳定。后来我加了一个陀螺仪数据辅助,用IMU的角速度来预补偿旋转,对齐精度提升了不少。
2.2.2 局部运动对齐
全局对齐只能解决相机运动。场景里的独立运动物体,比如行人、车辆,必须用局部对齐来处理。
常用的方法是光流法或块匹配法。把图像分成小块,对每个块单独计算运动矢量。
// 局部对齐示例:基于块匹配的运动估计
// 1. 将图像划分为16x16的块
// 2. 对每个块,在参考帧的搜索窗口内寻找最佳匹配
// 3. 使用SAD(差之和)作为匹配代价
// 4. 对运动矢量进行中值滤波,去除异常值
for (int y = 0; y < height; y += 16) {
for (int x = 0; x < width; x += 16) {
cv::Point2f mv = blockMatch(short_frame, long_frame, x, y, 16, 32);
motion_field[y/16][x/16] = mv;
}
}
2.3 合成算法:把三帧变成一帧
对齐之后,三帧图像在空间上已经对齐了。接下来就是合成——把短帧的高亮细节、中帧的中间调、长帧的暗部细节融合到一起。
2.3.1 权重融合法
这是最常用的方法。对每个像素,根据它的亮度值,给三帧分配不同的权重。
// 权重融合示例
// 短帧权重:高亮区域权重高
// 长帧权重:暗部区域权重高
// 中帧权重:中间调区域权重高
float w_short = exp(-pow(pixel_short - 0.8, 2) / (2 * sigma_s * sigma_s));
float w_mid = exp(-pow(pixel_mid - 0.5, 2) / (2 * sigma_m * sigma_m));
float w_long = exp(-pow(pixel_long - 0.2, 2) / (2 * sigma_l * sigma_l));
// 归一化权重
float sum = w_short + w_mid + w_long;
float result = (w_short * pixel_short + w_mid * pixel_mid + w_long * pixel_long) / sum;
权重融合的好处是平滑、自然。但有个坑:如果三帧的亮度差异太大,权重过渡区会出现亮度断层。我踩过这个坑,后来加了一个多尺度融合策略——先在拉普拉斯金字塔的不同层级上分别融合,再重建回原图。这样过渡就自然多了。
2.3.2 基于运动检测的融合
如果场景里有运动物体,权重融合会把运动物体的鬼影也融合进去。这时候需要引入运动检测。
基本思路是:先计算短帧和长帧之间的差异图,差异大的区域说明有运动。在这些区域,只使用短帧的数据,放弃长帧和中帧。
// 运动检测融合示例
// 1. 计算短帧和长帧的差
// 2. 对差异图做阈值分割,生成运动掩膜
// 3. 在运动区域,只使用短帧数据
// 4. 在静止区域,使用权重融合
cv::Mat diff = cv::abs(short_frame - long_aligned);
cv::Mat motion_mask = diff > threshold;
cv::Mat result;
if (motion_mask.at<uchar>(y, x) > 0) {
result.at<Vec3b>(y, x) = short_frame.at<Vec3b>(y, x);
} else {
result.at<Vec3b>(y, x) = weightedFusion(short_frame, mid_frame, long_aligned, y, x);
}
2.4 多帧HDR的完整流程
把上面的知识点串起来,多帧HDR的完整流程是这样的:
- 曝光控制: 根据场景亮度,确定短帧、中帧、长帧的曝光时间。
- 图像采集: 连续拍摄三帧图像,确保帧间间隔尽可能短。
- 全局对齐: 基于特征点匹配,补偿相机运动。
- 局部对齐: 基于块匹配或光流,补偿物体运动。
- 运动检测: 生成运动掩膜,标记运动区域。
- 权重融合: 在静止区域使用多尺度权重融合,在运动区域使用短帧数据。
- 色调映射: 将融合后的高动态范围图像映射到8bit显示空间。
嗯,这里要注意一点:色调映射虽然不在HDR融合的核心流程里,但它直接决定了最终图像看起来舒不舒服。我后面会专门用一章来讲色调映射的实战技巧。
2.5 实战中的避坑指南
最后,分享几个我在项目中踩过的坑,希望能帮你少走弯路。
我曾经在一个项目中,三帧的帧间间隔设到了30ms。结果拍快速移动的物体时,三帧里的物体位置差异太大,对齐算法根本救不回来。后来我把帧间间隔压缩到5ms以内,鬼影问题大幅改善。
有一次为了追求极致动态范围,我把长帧的曝光时间设到了短帧的64倍。结果长帧的暗部噪声被放大到无法接受的程度,合成后的图像噪点满天飞。后来我控制在16倍以内,信噪比才回到可接受范围。
运动检测生成的掩膜,边缘往往是锯齿状的。直接用它来切换数据源,合成后的图像在运动物体边缘会出现明显的接缝。我的解决方案是对掩膜做高斯模糊,让过渡区域平滑渐变。
多帧HDR的核心,说白了就是三件事:曝光策略定好、帧间对齐做准、合成算法选对。这三件事做好了,HDR的效果就稳了八成。剩下的两成,靠的是色调映射和3A算法的配合——这部分,我们后面慢慢聊。