4. HDR视频的色调映射基础
任务说明
本案例的目标是:给定一张HDR格式图像(或视频帧),通过全局色调映射算子将其动态范围压缩至SDR显示范围(0~255),并输出可正常显示的LDR图像。你需要理解全局映射与局部映射的本质区别,并实现两种经典全局算子:线性缩放与对数映射,同时观察不同算子对亮度层次的影响。
核心原理
HDR图像的像素值通常以浮点形式存储,亮度范围可达0~10^6 cd/m²,而SDR显示设备仅能呈现0~100 cd/m²左右的亮度。色调映射(Tone Mapping)的核心任务是将HDR亮度域压缩到SDR亮度域,同时尽量保留视觉对比度与细节。全局色调映射算子对图像中所有像素使用同一个映射函数,计算简单、实时性好,但可能在极端高动态范围场景下丢失局部细节。本案例聚焦两种基础全局算子:
- 线性缩放:直接乘以缩放系数,简单但会压缩整体对比度,暗部细节易丢失。
- 对数映射:利用人眼对亮度感知近似对数的特性,将高动态范围压缩到低动态范围,能更好地保留暗部层次。
代码实现
以下Python代码使用OpenCV读取HDR图像(格式为.hdr或.exr),实现两种全局色调映射算子,并输出对比结果。代码中包含了完整的图像读取、映射函数定义、结果保存与可视化流程。
import cv2
import numpy as np
def linear_scale(hdr_img, scale=0.18):
"""
线性缩放色调映射
:param hdr_img: HDR图像 (float32, 范围0~1或更大)
:param scale: 缩放系数,将最大亮度映射到1.0
:return: LDR图像 (uint8, 0~255)
"""
# 将HDR亮度归一化到0~1
max_val = np.max(hdr_img)
normalized = hdr_img / max_val
# 线性缩放
mapped = normalized * scale
# 裁剪到0~1并转为8bit
mapped = np.clip(mapped, 0, 1)
return (mapped * 255).astype(np.uint8)
def log_mapping(hdr_img, mu=5000):
"""
对数映射色调映射 (基于 Reinhard 对数算子)
:param hdr_img: HDR图像 (float32)
:param mu: 对数映射参数,控制压缩强度
:return: LDR图像 (uint8, 0~255)
"""
# 计算亮度 (使用灰度近似)
if len(hdr_img.shape) == 3:
luminance = cv2.cvtColor(hdr_img, cv2.COLOR_BGR2GRAY)
else:
luminance = hdr_img
# 对数映射公式: Ld = log(1 + mu * L) / log(1 + mu)
# 先归一化到0~1
max_lum = np.max(luminance)
if max_lum < 1e-6:
max_lum = 1.0
norm_lum = luminance / max_lum
mapped_lum = np.log(1 + mu * norm_lum) / np.log(1 + mu)
# 将映射后的亮度应用到RGB通道
if len(hdr_img.shape) == 3:
# 保持色相,调整亮度
hsv = cv2.cvtColor(hdr_img, cv2.COLOR_BGR2HSV)
hsv[:, :, 2] = mapped_lum # V通道替换为映射后亮度
result = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
else:
result = mapped_lum
# 裁剪并转为8bit
result = np.clip(result, 0, 1)
return (result * 255).astype(np.uint8)
# 主程序
if __name__ == "__main__":
# 读取HDR图像 (请替换为实际路径)
hdr_path = "input.hdr"
hdr_img = cv2.imread(hdr_path, cv2.IMREAD_UNCHANGED)
if hdr_img is None:
print("错误:无法读取HDR图像,请检查文件路径")
exit(1)
# 确保为float32类型
if hdr_img.dtype != np.float32:
hdr_img = hdr_img.astype(np.float32)
# 应用两种映射
ldr_linear = linear_scale(hdr_img, scale=0.18)
ldr_log = log_mapping(hdr_img, mu=5000)
# 保存结果
cv2.imwrite("output_linear.png", ldr_linear)
cv2.imwrite("output_log.png", ldr_log)
# 显示对比 (可选)
# cv2.imshow("Linear", ldr_linear)
# cv2.imshow("Log", ldr_log)
# cv2.waitKey(0)
# cv2.destroyAllWindows()
print("处理完成,结果已保存为 output_linear.png 和 output_log.png")
落地与验证
运行环境:需要安装Python 3.7+、OpenCV(pip install opencv-python opencv-contrib-python)以及NumPy。HDR测试图像可从OpenCV官方示例库或HDR图像数据库(如EMoR数据库)获取,也可使用以下命令生成一张简单的合成HDR图像用于测试:
# 生成合成HDR图像 (在Python中执行)
import cv2
import numpy as np
hdr = np.zeros((256, 256, 3), dtype=np.float32)
# 创建从暗到亮的渐变
for i in range(256):
hdr[i, :, :] = (i / 255.0) ** 2 * 1000 # 亮度范围0~1000
cv2.imwrite("input.hdr", hdr)
验证方法:运行主程序后,检查输出图像是否满足以下判定标准:
- 像素范围正确:输出图像为8位PNG,像素值在0~255之间,无溢出或全黑/全白。
- 亮度层次可见:在合成渐变图像中,线性缩放结果应呈现从黑到白的均匀渐变(但暗部层次可能被压缩);对数映射结果应能区分更多暗部细节,整体亮度分布更接近人眼感知。
- 对比观察:将两张输出图像并排查看,对数映射结果应比线性缩放结果保留更多暗部信息,但整体对比度可能略低。
判定标准:若两张输出图像均能正常显示且无异常色偏,且对数映射结果在暗部区域的灰度层次明显多于线性缩放结果,则本案例实现通过。
关键观测点
上图展示了本案例的实现流程与验证要点。通过对比两种算子的输出,可以直观理解全局色调映射中映射函数形状对亮度分布的影响——对数映射将更多编码空间分配给暗部区域,因此能保留更多暗部细节,而线性映射则保持均匀的亮度间隔。