小红书都能发 HDR Live Photo 了,为什么前端网页不行?
最近给 afilmory 写了 HDR 图片(准确来说是 UltraHDR)和 Live Photo(准确来说是 motion photo)的支持。然后又做了一个可以在线合成 HDR 图片和 Live Photo 的网页工具:https://enter-tainer.github.io/libultrahdr-rs/ 。把这些石吃明白之后,感到 HDR 图片的现状实在是太抽象了!
How HDR Live Photo Works
HDR 图片
我们先从 HDR 图片开始。我们平常看到的图片一般都是 SDR 的。但是如果你碰巧有一款比较新的手机,又经常使用小红书,应该经常会发现一些 HDR 的 post。点进去之后明显能感觉到图片比周围的 UI 元素亮了一圈。这就是 HDR 图片了。现在几乎所有比较新的手机都会默认拍摄 HDR 照片。主要分为安卓和苹果两派,苹果率先发明,安卓迅速 copy。两者用 gainmap 的方式解决了一个核心的问题:HDR 图片在 SDR 显示器上显示的时候,应该怎么办?
传统的 HDR 图片的思路大概是使用一条叫做 HLG 或者 PQ 的 gamma 曲线来存储峰值更高的亮度信息(PQ 里面直接 encode 了物理的亮度),同时使用 10bit 或者更高的色深来存储更多的颜色信息。听起来这种方式似乎已经很够用了,为什么大家还要发明新的东西呢?原因是并非所有人都有 HDR 显示器。如果你用 SDR 显示器来显示 HDR 图片,结果往往会不太好。如果不做 tone mapping,直接把超出亮度范围的部分 clip 到最大值,就会导致图片里面的高光部分完全丢失细节变成了一片白。而如果做了 tone mapping,就需要 tradeoff 怎么把更大的动态范围压缩到 sdr 的范围里面去。这个过程往往会导致图片的对比度下降,毕竟算法是写死的,不懂你拍摄的时候的想法。算法还能有你聪明!
于是果子提出了 gainmap 的概念,就是在 SDR 照片的 metadata 里面额外塞一些数据,从而把图片变成 HDR。这样认识这些元数据的设备就可以把图片显示成 HDR,而不认识的设备、或者是不支持 HDR 显示的设备,就可以直接显示成 SDR。同时,这里的 HDR 和 SDR 显示结果都经过了还算不错的 tone mapping,避免了自动映射比较菜的问题。所以 HDR 和 SDR 都可以有比较不错的显示效果。
那么具体是什么 metadata 呢?遗憾的是我作为安卓人从未真正拥有过果子手机,所以苹果的实现方式就不太清楚细节了。但是安卓的实现方式是开放的,所以可以在 https://developer.android.com/media/platform/hdr-image-format 这里看到。简单来说,安卓的 HDR 图片是把一张 SDR jpg 图片和一张 gainmap jpg 打包在一起,形成一个 jpg。gainmap 描述了 SDR 里面每个位置的亮度应该被“提升”多少,可以想象假如 gainmap 是个灰度的图片,码值越大(越白)的地方,这个位置就应该变得更亮。
后来 Adobe 推动了 gainmap 的标准化,形成了 ISO 21496-1。这一标准也被安卓和苹果采用了,所以现在的 HDR 图片基本上都是这个标准了。
Live Photo
Live Photo 是苹果发明的一个概念,后来安卓也跟进了。简单来说,Live Photo 就是把一张静态照片和一段短视频打包在一起。这样在支持 Live Photo 的设备上,就可以看到照片“动”起来的效果。苹果的实现方式非常的粗暴,每一个图片旁边,都有一个同名的 mov 文件,里面存储了 Live Photo 的视频部分。这两个文件会通过一些 metadata 来关联起来。可以想象这会让发送 live photo 变得很麻烦。而安卓这版则是继续把“往 jpg 里面塞数据”的思路发扬光大,直接把 mp4 append 到了图片的末尾,填充一些 metadata 之后就得到了传说中的 motion photo 格式。
那么进一步的,假如一张 jpeg 照片里面既有 gainmap,又有视频,这就是 HDR Live Photo 了!更加丧心病狂的是,你的 mp4 视频也可以是 HDR 的,于是就得到了完整的 HDR Live Photo。现在 oppo 手机应该默认会拍摄这样的照片。
问题
HDR Live Photo 听起来似乎很美好,但是:
- 只能在手机相册里面看这些图片吗?我想在网页上展示怎么办?
- 如果我不用手机拍照呢?用相机怎么拍出来这种效果?
网页展示 - afilmory
实际上现在的浏览器(除了 firefox😅)都支持了 HDR 图片的展示,只要你用了 img 标签,浏览器就会自动处理。而 Live Photo 就稍微复杂一些,浏览器只会显示图片部分。需要我们自己写代码来解析 metadata,找到视频的 offset,再把视频的部分切出来,然后用 video 标签播放。afilmory 就是这么做的。https://github.com/Afilmory/afilmory/pull/153
HDR 也是类似,可以通过 inspect metadata 来检查是否含有 gainmap,如果有的话,就直接用 img 标签加载就行了。
自己拍摄 - libultrahdr-rs
相机可以拍摄 raw 格式的数据,记录了原始的亮度信息。而大家常用的 lightroom 也支持以 HDR 模式修图,甚至可以导出成 jpg 的 HDR 格式,这个 jpeg 正好就是 ISO 21496-1 标准的 gainmap jpg + sdr jpg 的打包格式!
这样就结束了,吗?
Lightroom 的工作流存在一个很大的问题,就是你只能把 HDR 或 SDR 之一作为 target 对图片进行调整,而不能实现 UltraHDR 最开始的愿景:让一张图片在 HDR 和 SDR 设备上都能有最优的显示效果。在 Lightroom 中,如果你选择使用 HDR,那么 base 的 SDR 图片就会是自动生成的。如果你选择用 SDR 模式,那么就。。没有 gainmap 了。
理论上来说,我们可以先针对 HDR target 和 SDR target 分别导出两张图片。然后想办法手工把它们合并到一起,这样就能得到我们真正想要的东西了。在 LLM 的时代,确实可以这样做,比如可以把 ultrahdr 的 spec 粘贴进对话框,然后祈祷 AI 帮你生成正确的代码。但这样还是感觉太灵车了。
经过调研之后,发现这个市场还是太小众了。只有一个用 swift + metal shader 写的苹果人 apptoGainMapHDR。好在 Google 的 UltraHDR reference 实现 libultrahdr 恰好就有这个功能。于是我就指挥 codex 为它实现了一套 rust binding。并且用 rust 实现了 HDR 图片合并的功能。
wasm 编译
这个东西的形态应该是什么样的呢?cli 工具开发起来很爽,但是每次都在命令行里面输很长的命令会比较痛苦。而网页是个不错的选择,可以直接拖拽图片上传,然后在线合成下载,也可以写出来比较漂亮的 ui。那么问题就变成了,怎么把一个 c++ 和 rust 混合的库编译成 wasm 并且在浏览器上运行。在之前 typst 的 wasm plugin 编译过程中其实已经有一些这样的经验了。总的来说,把一份代码编译成 wasm 有几种思路:
- 编译目标是 wasm32-unknown-unknown,这个目标是裸机环境,什么都没有:syscall,libc,… 如果不是专门为这个 target 写的代码,基本没法用
- 编译目标是 wasm32-unknown-emscripten,一般 C++ 生态喜欢用这个东西。提供了模拟的 libc 和模拟的环境。但是这个东西会自带一坨神奇的 js 代码,ABI 也没有明确的定义,不太指望能用它编译这种混合的库
- 编译目标是 wasm32-unknown-unknown,但是使用 wasm-bindgen。这个其实比上一个更坏,它干的事情和 emscripten 差不多,但是它强行假装自己是一个裸机 target。实际上它应该是 wasm32-unknown-wasm-bindgen!这种情况下 cpp 代码也没办法正常编译
- 编译目标是 wasm32-wasip1. 这个 target 在裸机的基础上提供了一族 syscall,是个类似于 unix 的环境。可以看到它的提供的 API(或者说,syscall)里面有很多熟悉的老朋友:比如
open,read_link之类的。同时编译器为这个 target 实现了 libc,所以用这个 target 几乎可以编译所有的 C/C++ 代码。编译后的代码也可以在任何支持 wasi 的运行时上运行。
正好浏览器里面也有模拟的 wasi 的运行时,所以这里就把 libultrahdr-rs 编译成了 wasm32-wasip1 target,然后在浏览器里面用这个 wasi shim 来运行。最终就得到了一个可以在浏览器里面运行的 HDR 图片合成工具。
所以总结一下相机拍摄 HDR 图片的流程:
- 用相机拍摄 raw 照片
- 用 lightroom 之类的软件分别导出 HDR target 和 SDR target 的 jpg
- 用 libultrahdr-rs 合成最终的 HDR jpg
合成 Motion Photo
有时候我们也会想自己合成 Live Photo。比如拍摄了一段延时摄影,把主图作为 Live Photo 的图片,延时的视频作为 Live Photo 的视频部分合成在一起。这部分就是朴实无华的手艹元数据了。好在安卓厂商和 App(小红书)都比较靠谱,即使图片没有塞奇怪的私有字段也认。只要你得图片是符合 motion photo 标准的,就能被识别为 动态图
大概的思路就是先 parse 出 jpg 的各个 section。再把 mp4 文件 append 到 jpg 的末尾。然后修改 jpg 的 metadata 加上 motion photo 的相关字段。最后把修改后的 jpg 写回去就行了。虽然听起来很容易但是实现起来还是有挺多坑的,比如修改 metadata 会导致 offset 发生变化。而 HDR 的 motion photo 还需要额外的 manipulate 元数据的逻辑。
总结:
- 可以在 https://gallery.mgt.moe/ 看到我拍的一些 HDR/Live Photo 照片(前提是你的设备支持 HDR)。
- 可以在 https://enter-tainer.github.io/libultrahdr-rs/ 在线合成 HDR 图片和 Live Photo