两张 JPG

现在的手机基本都支持了 Gainmap 照片,OPPO 一直提到的 ProXDR 照片便是指 Gainmap,以前是自有格式,后来使用的是谷歌的 UltraHDR。

如果你在二进制模式的文件中寻找 JPG 的起始符 FF D8 和结束符 FF D9,会发现这种图像文件中能找到不止一张 JPG 图片,一般可以在第一个结束符后面,紧接着找到下一个起始符,直接在字节流中把它们分开,保存成两个 JPG 文件,便可看到主图和增益图,同时,在增益图的 APPn 标记段中,可以以 XMP 形式找到解码增益图需要的一些元数据,比如 Min,Max,Gamma 和 Offset。

比较有意思的是,适马最新的相机 Sigma BF 的直出 JPG 也是这种格式,是第一台能够直出 HDR JPG 图片的相机,能在传统派相机行业里看到这种格式是非常惊喜的。

主图、增益图和 HDR 效果图

这是一张由 OPPO Find X6 Pro 拍摄的,符合 UltraHDR 标准的 JPG 照片,华为练秋湖的这栋楼当时阳光从一侧照亮,形成较大的明暗反差。其中左侧的是第一段 JPG,即 UltraHDR 中的主要图像,是一个普通的 SDR 图像,在不兼容 UltraHDR 的播放器,或显示器不兼容 HDR 时,看到的就是该图像,中间为第二段 JPG,即 Gainmap,由于并不是为直接显示而设计的,此处只是将其按照 RGB 渲染,当作一种“可视化”的手段。

{
  "Version": 1.0,
  "GainMapMin": 0,
  "GainMapMax": 2.16048,
  "Gamma": 1,
  "OffsetSDR": 0,
  "OffsetHDR": 0,
  "HDRCapacityMin": 0,
  "HDRCapacityMax": 2.16048,
  "BaseRenditionIsHDR": false,
  "OplusScale": 4.47065
}

这是从第二段 JPG 的 APP1 中获得的元数据,增益图中的像素值需要按照元数据计算出一个“倍率”,来获得右侧的 HDR 效果。其中 OplusScale 是 OPPO 自己的一个字段,不清楚具体用途是什么,其余都是 UltraHDR 的标准元素。

右侧是合成的 HDR 效果,为了方便展示,该图实际上是解码之后,再以 PQ 编码在一起的纯 HDR AVIF 图像,并不是 UltraHDR,在峰值亮度允许的情况下,显示效果是一致的,否则会有所不同,下图是 UltraHDR 的原始版本,文件体积较大,可能加载慢一点。

UltraHDR 格式的原图

记录差异

Gainmap 记录的是 SDR 和 HDR 之间的差异,理解这一点可以从很多角度入手。

现在有一个图片,在 HDR 设备上,希望画面上的某些区域的亮度能够超过原先 SDR 的白点,做到高亮的效果。在不改动 SDR 显示效果的情况下,用另一幅图像标记出需要提亮的区域和提亮的程度,然后把 SDR 和差异图编码在一起。

或者现有一个 HDR 图片,但由于这种图片格式本身的兼容性,以及依靠设备下变换到 SDR 的兼容性都不够好,可以先下变换出一个满意的 SDR 图片,把它编码成传统、成熟的格式,然后用另一幅图像记录下原图和 SDR 图片的区别,编码到一起。

总之,Gainmap 本身只记录差异,你可以把一个有画面内容的图片当作 Gainmap,封装到一个纯色的主图边上,创造一个只有解码成功的 HDR 设备才能看到画面的东西。

上面的两个例子都是主图为 SDR 的情况,实际上,主图也可以是 HDR 的,通过 Gainmap 记录如何下变换到 SDR,但绝大多数情况下,都是使用 SDR 主图,这样即使解码器不支持,也可以当作普通的 SDR 图片来显示,有最好的兼容性。

因此,Gainmap 只是一种编码手段,在有了 HDR 和 SDR 图像之后进行编码的时候使用,而不应当将 Gainmap 本身看作一种调节图像的方法。比如说,在 Gainmap 上做某种操作来让图片更亮,或者在没有 HDR 图像的情况下以某种算法生成一个 Gainmap 出来,使一个本身是 SDR 的图上变换到 HDR。

Gainmap 的计算

关于如何生成 Gainmap,我觉得读者更应参考具体平台的文档,尤其是 Gainmap 这样标准繁多的新事物,本文更多只介绍 Gainmap 的概念。

比较推荐的几个文档和网页:

  • 谷歌的 UltraHDR,目前该格式是最为广泛使用的一种 HDR 双层格式,v1.1 版本还兼容了 ISO 21496-1 国际标准。如果您还是安卓平台的开发者,更是不得不看了。
  • Adobe 的 Gain map,Adobe 也是该格式的大力推动者,它推出的一款 Demo APP 是浏览各种 HDR 图片最方便的方法,该页面中还有一份详细的 Spec 文档,里面有一些很有参考价值的笔记。

在生成 Gainmap 前,你需要准备在线性光空间的 SDR 和 HDR 图像,比如规定 SDR 图像的像素值范围是 0-1,HDR 与之尺度对齐,但允许负数或超过 1 的数。这是因为 SDR 和 HDR 需要拥有相同的基色(比如 P3),如果基色范围选的比较小(比如 sRGB),或者 HDR 图像的色域特别广,就会出现负值,无需担心,Gainmap 是能够记录负值的。超过 1 的部分则是 HDR 本身的特征。

$$ G = \log_2 \frac{\text{HDR}+k_{\text{hdr}}}{\text{SDR}+k_{\text{sdr}}} $$

其中,两个 k 是偏移量,用于将可能的负数偏移到正数,确保这个对数运算是有意义的。

然后,找到 G 的最大值和最小值,用于将 G 映射到 0-1 范围内。为了提高可靠性,可以选择排除一小部分最小和最大值之后再选,然后再裁切掉超出 0-1 的值。

这样,你就得到了一个 0-1 范围内的图像,可以对其施加 Gamma 来提高量化的效果,并选择一个合适的位深(通常是 8 bit)和编码方式将其编码。

同时,还要把偏移量、最大值、最小值、Gamma 等元数据一并按规范编码。

单通道与三通道

为了节约空间,Gainmap 可以是灰度的,比如大部分安卓手机正在采用的,OPPO X8 Ultra 上的“原彩 ProXDR”照片指的便是升级到了三通道。

灰度 Gainmap 和三通道的彩色 Gainmap 区别主要在于 SDR 和 HDR 之间的关联或自由度。

三通道的 Gainmap 几乎完全自由,HDR 和 SDR 可以没有联系,Gainmap 可以在两个任意的点之间建立联系。但单通道的 Gainmap 要求 SDR 和 HDR 图像中的 RGB 像素值具有相同的比例,或者说在空间坐标系中,与原点位于一条直线上。

在不约束 SDR 图像的情况下,两种 Gainmap 能达到的 HDR 色域容积是一样的,但三通道时,允许更自由的下变换方法,单通道的增益图则只能在亮度方面做下变换。而在保持相同 SDR 图像的时候,三通道的 Gainmap 能够更精细的控制 HDR 图像。

有什么影响?根据 Bezold–Brücke 和 Hunt 效应,当亮度变化时,颜色的色相和彩度都会随之变化,单通道 Gainmap 仅能对亮度控制,保证 HDR 不变,下变换到 SDR 就受到限制,保持 SDR 的效果不变,能存储的 HDR 内容就受到限制,它们的色相(指的是 RGB 空间上的,而非感知)不能不同。

另外,在量化精度方面,三通道的 Gainmap 也具有一定的优势,通过元数据中,记录三个通道各自的最大和最小值,能够控制每个通道的归一化时的压缩程度及 Gamma。

对 Gainmap 的压缩

当对 Gainmap 进行压缩,尤其是降分辨率操作(大部分手机都会降分辨率至原来的 50% 长宽),可能会出现与主图无法对齐的现象,导致合成出来的 HDR 图片产生伪色、断层。同样的,对主图的压缩也可能导致与 Gainmap 无法对齐,尤其是高频部分,在点光源等典型 HDR 场景会更明显。

压缩 Gainmap 的方法和压缩普通图像基本相同,比如 JPG,JPEG XL,HEIC 这些,但 Gainmap 又和普通图像不太一样,因此有人提出了一些特别针对性的方法,最简单的有对 Gainmap 额外应用一个 Gamma 函数,也有一些用到机器学习的方法,比如这篇使用 MLP 来压缩 Gainmap 的文章。

T. D. Canham, S. Tedla, M. J. Murdoch, and M. S. Brown, “Gain-MLP: Improving HDR Gain Map Encoding via a Lightweight MLP,” Mar. 14, 2025, arXiv:2503.11883.

其它的封装方法

JPEG XL 是最新的一种 JPEG 格式,拥有很高的压缩率,并且支持很多新特性,其中就包括双层格式。

HEIF 和 AVIF 也支持双层格式,是以附加图的形式保存在元数据中,比如 iPhone 拍摄的 HDR 图片就是以该形式保存,解码的时候需要先找到对应的 Tag,再提取 Gainmap,开启了高效存储的 OPPO 手机拍摄的照片也是以 HEIF 存储,但 Tag 不相同。

TIFF 也是可以把多个帧保存在一起的,因此也可以保存 Gainmap。

在 Adobe Camera Raw 或者 Lightroom 输出 HDR 图片时,选择“最大兼容”后,导出的就是以上格式。

小结和未来

Gainmap 已经被广泛的采用,它向前兼容,各种新封装也广泛支持,很方便的存储下变换结果。

但从 HDR 的角度来看,它像 SDR 一样是相对亮度的,但又受到 HDR 峰值亮度的制约,用户调整显示屏亮度时,显示效果可能会明显变化。如果显示屏的 HDR 能力不足 Gainmap 中规定的最大值,这些中间情况的色调映射下变换具体实践也比较模糊。

另外,存储 Gainmap 需要两张 8 Bit 图像,在压缩效率方面可能不如单一 10 或 12 Bit 的纯 HDR 图像。多数 JPG 格式的云服务(CDN 上的图像压缩,社交媒体的审查)还可能会错过 Gainmap,因为它位于第一个 JPG 流结束之后。

接下来,我会写一些常见的 Gainmap 格式的特点和使用,并整理和提供一些 Python 代码。