3、HDR存储格式:OpenEXR格式详解、Radiance HDR格式、HDR元数据、色彩空间与色域

说到HDR图像,很多人第一反应是「拍出来的照片更亮」。其实没那么简单。真正决定HDR图像质量的,除了拍摄和合成算法,还有一个关键环节——存储格式。选错了格式,你辛辛苦苦合成的HDR数据,可能直接废掉一半。

我个人习惯把HDR存储格式比作「容器」。容器够大、结构合理,高动态范围的信息才能完整保留。今天我们就来拆解两个最主流的容器:OpenEXR和Radiance HDR,顺便聊聊元数据和色彩空间那些坑。

核心观点:HDR存储格式不仅仅是文件后缀名,它决定了你的亮度范围、色彩精度、元数据完整性,以及后续流程的兼容性。选对格式,事半功倍。

3.1 OpenEXR格式详解

OpenEXR是工业界的事实标准。ILM(工业光魔)在1999年开发,2003年开源。你看到的《阿凡达》《复仇者联盟》这些大片,中间帧几乎全是EXR。

为什么它这么强?说白了,它解决了三个核心问题:

  • 高动态范围:支持16位浮点(半精度)、32位浮点(单精度),甚至32位整数。亮度范围远超普通8位/16位整数格式。
  • 无损/有损压缩:PIZ、ZIP、RLE、DWAA/DWAB等多种压缩方式。我一般在最终交付时用PIZ无损压缩,中间调色用DWAA(有损但速度快)。
  • 多层存储:一个文件可以存多个通道(R、G、B、A、Z深度、法线、运动向量……)。这在后期合成里太方便了。

来看一个典型的OpenEXR文件结构:

OpenEXR File Structure:
┌─────────────────────────────┐
│         Magic Number        │  (4字节: 0x76, 0x2f, 0x31, 0x01)
├─────────────────────────────┤
│         Version Field       │  (4字节: 版本号 + 标志位)
├─────────────────────────────┤
│         Header              │
│  ┌───────────────────────┐  │
│  │  Display Window       │  │  (显示区域)
│  │  Data Window          │  │  (数据区域)
│  │  Channels             │  │  (通道定义: 名称, 类型, 采样)
│  │  Compression          │  │  (压缩方式)
│  │  LineOrder            │  │  (扫描线顺序)
│  │  ScreenWindowCenter   │  │  (屏幕窗口中心)
│  │  ScreenWindowWidth    │  │  (屏幕窗口宽度)
│  │  PixelAspectRatio     │  │  (像素宽高比)
│  │  ... (自定义属性)     │  │  (元数据: 白平衡, 镜头信息等)
│  └───────────────────────┘  │
├─────────────────────────────┤
│         Pixel Data          │
│  ┌───────────────────────┐  │
│  │  Scan Line 0          │  │
│  │  Scan Line 1          │  │
│  │  ...                  │  │
│  │  Scan Line N          │  │
│  └───────────────────────┘  │
├─────────────────────────────┤
│         Offset Table        │  (用于随机访问扫描线)
└─────────────────────────────┘

嗯,这里要注意:Header里的Display Window和Data Window是两个容易搞混的概念。Display Window定义了你「想看到」的区域,Data Window定义了你「实际存储」的区域。如果做裁剪或拼接,Data Window可能比Display Window大或小。我曾经在项目里遇到过一个EXR文件,显示正常但读像素数据时总报错,查了半天发现是Data Window设置错了。

我的建议:在Python里用OpenEXR库读写时,一定要检查header中的dataWindow和displayWindow。用exr.header()['dataWindow']和exr.header()['displayWindow']分别获取,确保它们一致,否则后续处理容易出bug。

3.2 Radiance HDR格式

Radiance HDR(.hdr或.pic)是另一个老牌格式,诞生于上世纪80年代,由Greg Ward开发。它比OpenEXR更轻量,但功能也相对简单。

它的核心编码方式是RGBE:每个像素用4个8位通道存储——R、G、B各8位,再加一个共享的指数(Exponent)通道。实际亮度值 = (R, G, B) × 2^(E - 128 - 8)。

你想想看,这种编码方式的好处是什么?

  • 存储效率高:每个像素只占4字节,比OpenEXR的16位浮点(6字节/像素)更省空间。
  • 动态范围够用:理论上支持10^76的亮度范围,实际应用中完全够用。
  • 解析简单:文件结构就是文本头 + 二进制像素数据,解析起来非常快。

但缺点也很明显:

  • 精度有限:RGBE只有8位尾数,在暗部区域容易出现条带(banding)。
  • 不支持多层:一个文件只能存一层RGBE,没法像EXR那样存Z深度、法线等辅助通道。
  • 色彩空间固定:Radiance HDR默认使用线性sRGB(其实更接近Rec.709原色),不能灵活指定色彩空间。

我记得有一次做环境光照采集,用单反拍了多张包围曝光照片,合成后存成.hdr格式。结果在渲染器里一用,发现暗部有明显的色阶断层。后来换成OpenEXR 16位浮点存储,问题就解决了。所以我的经验是:如果只是做简单的光照探针或背景图,.hdr够用;但如果要做精细的后期合成或调色,还是用EXR更稳妥。

3.3 HDR元数据

元数据是什么?就是「关于数据的数据」。在HDR图像里,元数据决定了你怎么正确解读像素值。

常见的HDR元数据包括:

元数据项 说明 典型值/范围
色彩空间(Color Space) 定义RGB原色和白点 sRGB, Rec.709, Rec.2020, ACEScg, DCI-P3
传递函数(Transfer Function) 定义编码值与亮度的映射关系 Linear, sRGB, PQ (ST 2084), HLG
亮度范围(Luminance Range) 最小/最大亮度值 0.0 ~ 65504.0 (FP16)
白平衡(White Balance) 色温或RGB乘数 D65 (6500K), D55 (5500K)
曝光信息(Exposure) 拍摄时的曝光设置 光圈、快门、ISO
自定义属性(Custom Attributes) 用户自定义的键值对 如:cameraMake, lensModel, artist

这里有个坑:很多软件在读取HDR图像时,会忽略或错误解析元数据。比如,你用一个线性色彩空间的EXR文件,但软件默认把它当成sRGB来显示,结果就是颜色完全不对。我曾经在项目交付时遇到过这个问题——渲染器读EXR时把线性数据当sRGB处理,导致整个镜头颜色偏灰。后来我们在EXR header里强制写入了chromaticities和whiteLuminance属性,才解决了问题。

避坑指南:在OpenEXR中写入自定义元数据时,建议使用标准属性名(如chromaticities、adoptedNeutral、whiteLuminance),而不是随便起名。非标准属性名可能被下游软件忽略。我曾经见过有人把白平衡信息写在myWhiteBalance里,结果Nuke和Photoshop都读不到。

3.4 色彩空间与色域

色彩空间这个话题,说大不大,说小不小。但在HDR流程里,它是个绕不开的坎。

简单来说:色彩空间 = 色域(Gamut)+ 传递函数(Transfer Function)+ 白点(White Point)。

常见的HDR色彩空间有:

  • ACEScg:AMPAS(美国电影艺术与科学学院)制定的标准,色域覆盖整个可见光谱的绝大部分。我个人的主力工作空间。
  • Rec.2020:ITU为4K/8K超高清电视制定的标准,色域比sRGB大很多。
  • DCI-P3:数字影院标准,色域介于sRGB和Rec.2020之间。
  • sRGB / Rec.709:老标准,色域小,但兼容性最好。

为什么HDR要关注色域?因为高动态范围 + 宽色域 = 更真实的视觉体验。你想想看,如果亮度范围上去了,但颜色还是sRGB那点可怜的范围,很多鲜艳的颜色(比如霓虹灯、夕阳)根本显示不出来。

来看一张我常用的色彩空间对比图:

常见色彩空间色域对比(CIE 1931 xy 色度图) sRGB DCI-P3 Rec.2020 ACEScg D65 注:ACEScg 色域最广,覆盖几乎所有可见光;sRGB 最小,仅覆盖约 35% 的 CIE 1931 色度图。

从图上可以清楚看到:ACEScg > Rec.2020 > DCI-P3 > sRGB。色域越大,能表示的颜色越丰富。

但这里有个实际问题:你的显示设备能覆盖多少色域? 我见过很多团队在ACEScg空间里做渲染,但最终输出到sRGB显示器上,颜色直接「缩水」。所以我的建议是:在制作流程中保持宽色域(ACEScg或Rec.2020),在最终输出时根据目标设备做色域映射(Gamut Mapping)。

实用技巧:在OpenEXR中存储ACEScg数据时,建议在header中写入chromaticities属性,值为ACEScg的原色坐标(R: 0.713, 0.293; G: 0.165, 0.830; B: 0.128, 0.044; W: 0.32168, 0.33767)。这样下游软件就能正确识别色彩空间,避免颜色偏差。

3.5 格式选择与最佳实践

说了这么多,到底该用哪个格式?我整理了一个对比表:

特性 OpenEXR Radiance HDR
位深 16位浮点 / 32位浮点 / 32位整数 32位(RGBE 8+8+8+8)
动态范围 极高(FP16: 10^4.5, FP32: 10^76) 高(10^76)
精度 高(浮点,无条带) 中(8位尾数,暗部有条带风险)
多层支持 是(任意通道数) 否(仅RGBE)
压缩 PIZ, ZIP, RLE, DWAA/DWAB RLE(仅亮度编码)
元数据 丰富(标准+自定义) 有限(仅文本头)
色彩空间 灵活(可指定任意色彩空间) 固定(线性sRGB/Rec.709)
文件大小 较大(但压缩后可控) 较小(4字节/像素)
兼容性 极好(VFX/动画/游戏行业标准) 好(老软件/光照探针常用)

我的个人建议是:

  • VFX/动画/后期合成:用OpenEXR,16位浮点,PIZ压缩,ACEScg色彩空间。
  • 环境光照/IBL探针:用Radiance HDR,简单快速,兼容性好。
  • 实时渲染/游戏:考虑压缩后的EXR或专用格式(如BC6H)。
  • 存档/交付:用OpenEXR,32位浮点,无损压缩,保留所有元数据。

一句话总结:OpenEXR是「瑞士军刀」,功能全面但稍重;Radiance HDR是「小刀」,轻便但功能有限。根据场景选,别盲目。

好了,HDR存储格式这部分就聊到这儿。记住:格式只是工具,真正重要的是你如何管理好里面的数据——亮度范围、色彩精度、元数据完整性,一个都不能少。