参考文档:https://developer.android.com/media/platform/hdr-image-format
识别 UltraHDR
文件中的第一个 JPEG 流为“主图像”,在主图像的 APP1 中寻找是否有 XMP 数据包,并在其中寻找是否有 hdrgm:Version="1.0"
,其中,hdrgm
指的是命名空间标识符 http://ns.adobe.com/hdr-gain-map/1.0/
。如果有,则该文件就可以被认为是符合 UltraHDR 标准的。如果是严格意义上的 UltraHDR(符合谷歌标准的),这个 XMP 数据包中还应该有关于 GContainer
的内容,用于描述容器内包含的各个部分,例如主图像(Primary)和增益图(GainMap)及其数据长度。
比如以下是 OPPO Find X6 Pro 拍摄的 JPG 图片中的第一个 XMP 数据包,符合 UltraHDR 和 GContainer 的要求。
<x:xmpmeta
xmlns:x="adobe:ns:meta/"
x:xmptk="Adobe XMP Core 5.1.2">
<rdf:RDF
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description
xmlns:Container="http://ns.google.com/photos/1.0/container/"
xmlns:Item="http://ns.google.com/photos/1.0/container/item/"
xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/"
hdrgm:Version="1.0">
<Container:Directory>
<rdf:Seq>
<rdf:li
rdf:parseType="Resource">
<Container:Item
Item:Semantic="Primary"
Item:Mime="image/jpeg"/>
</rdf:li>
<rdf:li
rdf:parseType="Resource">
<Container:Item
Item:Semantic="GainMap"
Item:Mime="image/jpeg"
Item:Length="401160"/>
</rdf:li>
</rdf:Seq>
</Container:Directory>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
多帧图像老将:MPF
相比自定义的 GContainer,更通用的多图像格式(Multi-Picture Format,MPF)由 CIPA 于 2009 年提出,用于在一个 JPEG 文件中标识与组织多帧图像,例如低分辨率预览、3D 成像、连拍帧等。现在,MPF 也常用于在 HDR 图像内标记并定位 Gainmap。MPF 信息存放在主 JPEG 的 APP2 段中,其组织方式与 EXIF 的 IFD 结构类似。
APP2 段的基本结构如下:
- 起始标记为 FF E2,后跟 2 字节段长度。
- 接着是 4 字节 MPF 标识符 “4D 50 46 00”(即 “MPF\0”)。从标识符之后的位置起,定义为 MPF 的“偏移基准”(base,记作 0)。
- 随后 4 字节为大小端序标记,与 TIFF 相同(例如大端为 4D 4D 00 2A)。
- 再往后 4 字节给出首个 IFD 的偏移(相对 MPF 基准);若 IFD 紧随其后,则该值为 8。
进入 MPF IFD 后:
- 先用 2 字节给出条目(Entry)数量。
- 每个 IFD 条目占 12 字节,常见条目包括:
- MPFVersion(类型为 UNDEFINED,长度 4,通常为 ASCII “0100”)
- NumberOfImages(类型为 LONG,长度 1)
- MPEntry(类型为 UNDEFINED;条目本身存放一个相对偏移,指向实际的 MP Entry 数组)
- IFD 末尾还有 4 字节指向“下一个 IFD”的偏移(无则为 0)。
MP Entry 数组用于描述文件中的每一幅图像,每个条目固定占 16 字节,通常包含:4 字节属性、4 字节图像数据长度、4 字节数据偏移(相对 MPF 基准,指向该图像 SOI)、以及两个 2 字节的依赖项索引(没有依赖则为 0)。
下面是一个示例(十六进制)并附简要注释:
FF E2 00 58 # APP2 与段长度
4D 50 46 00 # "MPF\0" 标识符(其后位置定义为 MPF 偏移基准 0)
4D 4D 00 2A # 大端序标记(与 TIFF 相同)
00 00 00 08 # 首个 IFD 的偏移(相对基准),8 表示紧随其后
00 03 # IFD 条目数量:3
B0 00 00 07 00 00 00 04 30 31 30 30 # MPFVersion:类型 07(UNDEFINED),长度 4,"0100"
B0 01 00 04 00 00 00 01 00 00 00 02 # NumberOfImages:类型 04(LONG),值为 2
B0 02 00 07 00 00 00 20 00 00 00 32 # MPEntry:类型 07,长度 32,数据偏移为 0x32
00 00 00 00 # 下一 IFD 偏移:无(0)
# MP Entry 数组(每条 16 字节)
00 03 00 00 00 5B ED A0 00 00 00 00 00 00 00 00 # 第 1 幅(主图):长度 0x005BEDA0,偏移 0(主图 SOI 在 MPF 之前,偏移不可为负,故记 0)
00 00 00 00 00 06 1F 08 00 5B E4 87 00 00 00 00 # 第 2 幅(Gainmap):长度 0x00061F08,偏移 0x005BE487(相对 MPF 基准,指向第二个 JPEG 流的 SOI)
该示例中:
- 第二幅图像(Gainmap)的长度 0x00061F08 与 XMP 中记录的 Gainmap 大小(401160)一致;其数据偏移 0x005BE487 指向第二个 JPEG 流的 SOI,相对于 MPF 基准计算。
- 第一幅(主图)给出了完整码流长度 0x005BEDA0;由于主图的 SOI 出现在 MPF 基准之前,数据偏移字段不可为负,因此记为 0。
更多细节可参考 CIPA 的官方文档《DC-x007-2009 Multi-Picture Format》。
深入探索 Gainmap
咬文嚼字环节:UltraHDR 指的是用 GContainer 在主图中标识位置,含 XMP 的 Gainmap 不是 UltraHDR 定义的,而是在 Adobe 的标准中定义的。
理清该图片是否符合 UltraHDR 标准,是否由 GContainer 或 MPF 定位了 Gainmap 的位置之后,我们就可以据此寻找 Gainmap 和它的元数据了。
Gainmap 也是用 XMP 来记录元数据的,我们在找到的 Gainmap JPG 流中,寻找有 XMP 的 APP1 标记段。
<x:xmpmeta
xmlns:x="adobe:ns:meta/"
x:xmptk="Adobe XMP Core 5.1.2">
<rdf:RDF
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description
xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/"
hdrgm:Version="1.0"
hdrgm:GainMapMin="0"
hdrgm:GainMapMax="2.16048"
hdrgm:Gamma="1"
hdrgm:OffsetSDR="0"
hdrgm:OffsetHDR="0"
hdrgm:HDRCapacityMin="0"
hdrgm:HDRCapacityMax="2.16048"
hdrgm:BaseRenditionIsHDR="False"
hdrgm:OplusScale="4.47065"/>
</rdf:RDF>
</x:xmpmeta>
同样的,也找到了一个命名空间指向 adobe 的 gainmap 标准的 hdrgm,其中记录了 Gainmap 所需的各种元数据(OplusScale 是 OPPO 自己的私有字段)。
接下来将结合 Gainmap 的编码过程,简单讲讲这些参数的含义。首先,这是一个单通道 Gainmap,其编码过程一般用 Gainmap 记录下亮度的差异(Luminance),亮度根据 RGB 空间对应的基色坐标,由 RGB 像素值加权组合而来,其实就是 RGB -> XYZ 矩阵的第二行(就是计算一个 Y 值)。三通道的话直接在 RGB 上逐通道操作即可。
我们把转化为亮度后的 HDR 和 SDR 记为 Yhdr 和 Ysdr,他们都需要转换到线性空间,并具有相同的尺度。
$$ \text{pixel gain} = \frac{Y_{\text{hdr}} + \text{offset hdr}}{Y_{\text{sdr}} + \text{offset sdr}} $$其中,两个 offset 的作用有几点,一般来说,可以取 1/64 作为偏移量:
- 为了确保 Y hdr + offset hdr 的结果恒为正数,以满足后续对数运算的要求。
- 避免 Y sdr 为 0 导致的除法问题。
- 提升暗部的编码精度
然后对 pixel gain 以 2 为底取对数,并记录此时的 GainMapMin 和 GainMapMax。
对 pixel gain 进行归一化到 0-1 的过程中,UltraHDR 使用的是 max/min content boost 而不是刚才记录的 Gainmap max/min,区别在于 offset 的影响,个人认为使用 Gainmap 是更好的做法。之后需要把 0-1 以外的部分截断。
什么是 content boost:这是控制 HDR 内容亮度的参数,可以由创作者直接定义,Google 用了一个词叫 Implement-Defined。相比客观的计算 HDR 和 SDR 的亮度比例,这个可自定义的参数能够实现一些主观效果,例如,如果希望 HDR 内容的每个像素都比 SDR 内容更亮,可以将 min content boost 设定为 1。
之后,对归一化的 pixel gain 使用一个幂函数,即 Gamma,多数情况下,Gamma 都可以取 1,如果 Gainmap 中包含大量细节,可以用一个稍大的 Gamma。
最后,把这个 0-1 范围内的 Gainmap 拉伸到 0-255,并编码为 JPEG 图像,JPEG 质量建议不低于 85-90。
至于 HDR Capacity Min/Max,他们代表了显示设备的 HDR 能力,我并不是很理解为什么一个显示设备相关的量会放在图片文件里,也许是希望记录下和 HDR 视频工作流中的“调色设备”类似的思路,将创作者的显示设备信息记录下来,辅助进行色调映射。多数时候,该值会被设置成和 Gainmap Min/Max 相同。
另外,还有一个标识主图是 SDR 还是 HDR 的字段 BaseRenditionIsHDR
,通常为 False。
解码和色调映射
此处我们再引入一个变量:Display Boost,它记录了当前显示设备的 HDR 白点与 SDR 白点亮度之比,比较类似于 Apple 的 Headroom 概念。比如 SDR 白点为 100 nits,HDR 白点为 1600 nits,此时,Display Boost 即为 16。
它将参与 UltraHDR 的解码显示部分,主要是当 Display Boost 小于 Content Boost,也就是当前设备无法完全显示出内容的 HDR 亮度时。引入了一个权重,来控制 HDR 部分的亮度,使其能够在显示屏的能力范围之内。
权重的计算方法如下,其中的量都是在 log2 非线性空间下的。
$$ \text{weight} = \frac{\text{max display}-\text{min capacity}}{\text{max capacity}-\text{min capacity}} $$然后截断到 0-1 范围内,1 表示有能力显示完整 HDR 内容。它将应用在之前 HDR,SDR 和两个偏移量计算出的那个 log2 后的图 G 上。
编码时使用:
$$ G = \log_2 \frac{\text{HDR}+k_{\text{hdr}}}{\text{SDR}+k_{\text{sdr}}} $$解码时使用:
$$ \text{HDR} = (\text{SDR}+k_{\text{sdr}}) \cdot 2^{G \cdot \text{weight}}-k_{\text{hdr}} $$当权重为 1 时,计算出的 HDR 即为先前编码的 HDR,当权重小于 1 时,得到的 HDR 则是根据屏幕实际显示能力,对 Gainmap 部分进行降低,显示一个“中间”版本,直到权重为 0,显示一个完全 SDR 的版本。
对 ISO 21496-1 的兼容性
ISO 21496-1 是增益图类型 HDR 静态图像的国际标准,不再采用 APP1 中的 XMP 存储 Gainmap 的元数据,而是在 APP2 中定义了一个专门的元数据段来遵循该标准,元数据本身基本相同。
UltraHDR 1.1 版本兼容了 ISO 21496-1,实际上就是既有 APP1 中的 XMP,又有 APP2 中的国际标准标识段。
终结战国时代的先驱
在 UltraHDR 之前,双层的 HDR 静态图像处在战火纷飞的战国时代,各手机品牌有各自的标准,互不兼容。
谷歌在安卓 13 引入了 UltraHDR 标准,以 GContainer 和 MPF 指导 Gainmap 的位置,帮助解码器正确找到 Adobe Gainmap 标准的第二帧图像。用一种非常简洁的方式,在很少改动的情况下,就优雅的实现了向后兼容。
目前,几乎所有的手机相册(出厂时是 Android 13 或更新,或 OLED 屏的 iPhone),Chromium 内核的浏览器都能支持该格式及其大部分变体。
虽然受限于 JPEG 较低的压缩率,8bit 位深和有损压缩,UltraHDR 仍称得上是对 JPG 的一次很好的改进。
接下来的双层 HDR 格式,就该慢慢过渡到采用 ISO 21496-1 标准的 HEIF,AVIF 甚至是 JPEG XL 了。但 JPEG 作为最广泛使用的图像格式,仍将长期存在和大量使用。