3、HDR合成基础原理:多曝光融合原理、相机响应曲线(CRF)、辐射度图构建

好,咱们进入第三章。这一章是HDR合成的核心地带,说白了就是搞明白「多张照片怎么拼成一张高动态范围图」的底层逻辑。

我刚开始接触HDR时,以为就是把亮图暗图直接叠在一起。结果做出来的图要么发灰,要么有鬼影。后来才明白,这里面的门道深着呢。

3.1 多曝光融合:为什么不是简单叠加?

你想想看,一张照片里,暗部细节和亮部细节是互相矛盾的。曝光时间长了,暗部亮了,但亮部过曝成一片白。曝光时间短了,亮部有细节,暗部又死黑。

多曝光融合,就是取长补短。把不同曝光下「表现最好」的像素挑出来,合成一张图。

但问题来了——怎么定义「表现最好」?

我个人习惯用三个权重:

  • 对比度权重:边缘清晰的像素更可靠
  • 饱和度权重:颜色鲜艳的区域保留更多信息
  • 曝光度权重:中间调(不黑不白)的像素最稳定

我在项目中遇到过一种情况:用简单的平均融合,结果天空出现奇怪的条纹。后来改用加权融合,问题就解决了。

核心公式(简化版)

最终像素 = Σ(权重 × 像素值) / Σ(权重)

权重 = 对比度权重 × 饱和度权重 × 曝光度权重

嗯,这里要注意:权重不是随便给的。曝光度权重通常用高斯曲线,中间调给最高权重,过暗过亮都压低。

3.2 相机响应曲线(CRF):从像素值到真实亮度

相机拍出来的像素值(0-255),和真实世界的亮度(辐射度)不是线性关系。为什么?因为相机传感器和图像处理芯片会做各种映射。

CRF就是描述这个映射关系的曲线。有了它,我们才能把像素值「反推」回真实亮度。

我曾经踩过一个坑:直接用线性假设去反推,结果合成出来的图颜色完全不对。后来才意识到,不同相机的CRF差异很大。

怎么求解CRF?Debevec在1997年提出了一种方法,核心思路是:

  1. 拍摄多张不同曝光的照片
  2. 对同一个场景点,不同曝光下的像素值应该对应同一个辐射度
  3. 用最小二乘法拟合出CRF曲线

我的经验:拍摄时用三脚架固定相机,光圈固定,只改变快门速度。这样能保证场景的辐射度不变,只改变曝光量。

CRF曲线长什么样?一般来说是S形。暗部被压缩,亮部也被压缩,中间调相对线性。

3.3 辐射度图构建:把像素值还原成真实亮度

有了CRF,我们就可以构建辐射度图了。辐射度图就是一张浮点精度的图像,每个像素存储的是真实世界的亮度值(单位:W/m²/sr)。

构建步骤:

  1. 对每张输入图像,用CRF反函数把像素值映射成辐射度
  2. 对同一个场景点,取多张图像中「可靠」的辐射度值
  3. 加权平均得到最终的辐射度值

这里有个细节:不同曝光下,同一个场景点的信噪比不同。曝光不足的区域噪声大,曝光过度的区域饱和失真。所以加权时,要优先选择中间曝光的像素。

避坑指南:我曾经用3张曝光(-2EV, 0EV, +2EV)合成,结果发现+2EV的暗部噪声特别大。后来改成5张曝光(-4EV, -2EV, 0EV, +2EV, +4EV),噪声问题明显改善。

3.4 知识体系总览

下面这张图,是我自己总结的HDR合成基础原理框架。你看一眼,心里就有谱了。

HDR合成基础原理框架 多曝光图像序列 相机响应曲线(CRF) 辐射度图构建 HDR辐射度图 曝光时间:1/1000s ~ 30s 光圈固定,ISO固定 Debevec算法求解 最小二乘法拟合 S形曲线映射 加权融合 浮点精度存储 核心流程:多曝光图像 → CRF反推 → 辐射度加权融合 → HDR图 关键点:CRF是桥梁,权重是灵魂,浮点精度是基础

3.5 代码实战:用Python实现CRF求解

光说不练假把式。下面是我常用的CRF求解代码片段,你拿去就能跑。

import numpy as np
from scipy.optimize import least_squares

def solve_crf(images, exposures):
    """
    求解相机响应曲线
    images: 多张曝光图像,形状 (N, H, W)
    exposures: 对应的曝光时间列表
    """
    N, H, W = images.shape
    # 采样像素点
    sample_points = np.random.choice(H*W, 256, replace=False)
    
    def objective(params):
        g = params[:256]  # CRF曲线
        lnE = params[256:]  # 辐射度对数
        residuals = []
        for i in range(N):
            for j, idx in enumerate(sample_points):
                z = images[i].flat[idx]
                if z > 0 and z < 255:  # 忽略饱和像素
                    residuals.append(g[z] - lnE[j] - np.log(exposures[i]))
        return residuals
    
    # 初始值
    x0 = np.zeros(256 + len(sample_points))
    result = least_squares(objective, x0)
    return result.x[:256]  # 返回CRF曲线

使用建议:实际项目中,我会先做图像对齐,再采样。采样点要均匀分布在图像各个区域,避免集中在天空或阴影里。

3.6 辐射度图构建的完整流程

有了CRF,构建辐射度图就水到渠成了。流程如下:

步骤 操作 说明
1 读取多曝光图像 确保图像已对齐,格式为16位或32位浮点
2 应用CRF反函数 将像素值映射为辐射度值
3 计算权重 基于像素值位置(中间调权重高)
4 加权融合 对每个像素位置,取多张图的加权平均
5 输出HDR图 保存为.exr或.hdr格式

嗯,这里要注意:权重计算时,我习惯用三角权重函数。像素值在128附近给最高权重,0和255给0权重。这样能有效抑制噪声和饱和问题。

我曾经踩过的坑:有一次用8位图像做HDR合成,结果辐射度图出现明显的量化噪声。后来改用RAW格式(12位或14位),效果好了不止一个档次。

好了,这一章的内容就到这里。多曝光融合、CRF、辐射度图构建,这三者是HDR合成的基石。你把这些搞明白了,后面的色调映射、局部自适应处理,学起来就轻松多了。


专注资料整理