提示

返回博客列表

HDR 视频不只是"更亮":PQ/HLG、Tone Mapping 与交付检查

客户送来一批 HDR 素材,要出成片和 SDR 版本。我们按常规流程转了一版,客户回复:"画面发灰,颜色不对。"

我当时的判断是"色彩空间没设置对",加了 -color_primaries bt709 之类的参数——毫无改善。

后来才明白:HDR 和 SDR 的区别不在"亮度数值",而在"亮度数值怎么映射到显示"。HDR 用的是完全不同的传输曲线(PQ 或 HLG)和更广的色域(BT.2020)。直接把 HDR 当 SDR 处理,相当于把一段编码过的数据当成原始数据读——结果就是发灰、发暗、颜色寡淡。

正确的做法是 tone mapping(色调映射):把 HDR 的大动态范围压缩到 SDR 能表达的范围,同时尽量保持观感。这篇写完整流程。

TL;DR:HDR 的三个要素:10bit 位深 + BT.2020 广色域 + PQ/HLG 传输曲线。转 SDR 必须用 tone mapping(zscale + tonemap 滤镜),只设 -color_primaries 没用。PQ(SMPTE ST 2084) 是绝对亮度曲线,用于点播/电影;HLG 是相对曲线,向后兼容 SDR 显示器,用于直播/广电。两个关键参数:npl(峰值亮度,常用 100~1000)和 tonemap 算法(hable/mobius/linear/reinhard)。交付前要检查:位深、色域、传输曲线、MaxCLL/MaxFALL 元数据,并且必须在 SDR 显示器上看一遍。

目录

一、HDR 的三个要素

HDR(High Dynamic Range)不是一个参数,而是一组参数的组合:

要素 SDR HDR
位深 8bit(少数 10bit) 10bit 或 12bit
色域 BT.709 BT.2020
传输曲线(EOTF) BT.1886 / Gamma 2.4 PQ(ST 2084)或 HLG
峰值亮度参考 100 nits 1000~10000 nits

三者缺一不可。很多时候"看起来像 HDR 但其实就是发灰"的原因就是:素材是 BT.2020 色域 + 10bit,但传输曲线标记错了(或者没标),播放器按 BT.709 的曲线去解释 BT.2020 的数据——就是那个经典的"发灰"。

二、PQ 与 HLG:两条不同的曲线

PQ(Perceptual Quantizer,SMPTE ST 2084)

  • 绝对亮度:编码值直接对应"多少尼特"(nits);
  • 设计上限 10000 nits,实际内容通常按 1000~4000 nits 制作;
  • 用于:电影、点播、流媒体(Netflix/Apple/Disney 的 HDR 都是 HDR10 = PQ + 静态元数据);
  • 不向后兼容:在 SDR 显示器上直接放会非常暗(除非做转换)。

HLG(Hybrid Log-Gamma,BBC + NHK)

  • 相对亮度:编码值与"相对于显示器峰值"的比例有关;
  • 天然向后兼容:SDR 显示器直接放 HLG 信号,画面大致正常(只是不够"HDR");
  • 用于:直播、广播电视(体育赛事、春晚这类);
  • 不需要元数据,编码器负担小。

选择建议:

场景 用
点播、电影、宣传片 PQ / HDR10
直播、广电分发 HLG
两者都要 出 HLG 母版,分发时转 PQ(或者反过来)

三、怎么判断素材是不是 HDR

ffprobe -v error -select_streams v:0 \
  -show_entries stream=codec_name,profile,pix_fmt,color_primaries,color_transfer,color_space \
  -of default=noprint_wrappers=1 input.mp4

输出示例(HDR10):

codec_name=hevc
profile=Main 10
pix_fmt=yuv420p10le                ← 10bit
color_primaries=bt2020             ← BT.2020 色域
color_transfer=smpte2084           ← PQ 曲线(HLG 是 arib-std-b67)
color_space=bt2020nc

传输曲线的取值对照:

值 含义
bt709 SDR(Gamma ~2.4)
smpte2084 PQ / HDR10
arib-std-b67 HLG
linear 线性(少见)

如果 color_transfer 是 unknown:很可能是元数据丢了(素材本身是 HDR 但没标)。这时候要靠看画面判断:HDR 素材在普通播放器里会明显发灰发暗、对比度低、饱和度低。

四、HDR → SDR:Tone Mapping

核心滤镜链(ffmpeg 需要编译时带 --enable-libzimg):

ffmpeg -i hdr_input.mp4 \
  -vf "zscale=t=linear:npl=100,format=gbrpf32le,\
tonemap=tonemap=hable:desat=0,\
zscale=t=bt709:m=bt709:p=bt709:r=tv,format=yuv420p" \
  -c:v libx264 -crf 20 -preset slow \
  -c:a copy \
  sdr_output.mp4

三段滤镜的作用:

段 作用
zscale=t=linear:npl=100 把 PQ/HLG 数据解码成线性光(linear light),并指定峰值亮度 npl
format=gbrpf32le 转成浮点格式(必须,否则 tone mapping 会量化损失严重)
tonemap=tonemap=hable:desat=0 执行色调映射(真正的 HDR→SDR 转换)
zscale=t=bt709:m=bt709:p=bt709:r=tv 转回 SDR 的传输曲线/矩阵/色域/范围
format=yuv420p 转成 8bit 420(SDR 目标)

关键点:不能只做 zscale 而不做 tonemap——那样只是把曲线换了,高光会直接被削平(一片死白)。

注意滤镜链里不要有多余空格(因为反斜杠换行),前面几篇提过,这里再强调一次——我在这个滤镜链上因为行首空格浪费过半小时。

五、参数怎么调

npl(nominal peak luminance)

峰值亮度,单位 nits。它决定了"HDR 里最亮的多少尼特映射到 SDR 的白"。

值 效果
100 接近 SDR 标准,对比强烈,暗部细节保留好
200~300 常用的平衡点
1000 保留更多高光层次,但整体偏暗

我的经验:先从 npl=100 试(对大多数内容观感最好),如果高光部分过曝严重(比如天空全白),再往上调。

tonemap 算法

算法 特点 适合
hable 保留高光细节,对比度好 默认首选
mobius 类似 hable,高光过渡更柔和 高光多的素材
linear 简单线性压缩 快速预览
reinhard 保留暗部,高光压缩 暗场景
clip 直接削波(最差) 不要用
gamma 特殊用途

实测对比(同一段 HDR 风光素材,5 分制盲评):

算法 主观评分
linear 3.1
reinhard 3.6
hable 4.3
mobius 4.2

hable 和 mobius 都很好,差别很小。先用 hable。

desat(去饱和)

HDR 的广色域转到 SDR 时,如果直接映射,某些高饱和颜色会出现"荧光感"或者色块。desat 参数控制去饱和强度(0 = 不去饱和,越大越灰)。

一般设 0(现代 ffmpeg 的色域转换已经处理得不错),只有在颜色明显"溢出"时才加大。

六、HDR → HDR(保持 HDR 的转码)

如果要出 HDR 版本(比如把 4K HDR 转成 1080p HDR),不要做 tone mapping,保留 PQ/HLG:

ffmpeg -i hdr_4k.mp4 \
  -vf "scale=1920:1080:flags=lanczos,format=yuv420p10le" \
  -c:v libx265 -crf 20 -preset slow -pix_fmt yuv420p10le \
  -color_primaries bt2020 -color_trc smpte2084 -colorspace bt2020nc \
  -x265-params "hdr-opt=1:repeat-headers=1" \
  -c:a copy \
  hdr_1080p.mp4

关键:

  1. 保持 10bit(yuv420p10le);
  2. 显式指定色彩参数(-color_primaries / -color_trc / -colorspace);
  3. x265 加 hdr-opt=1(HDR 优化)和 repeat-headers=1(每帧重复头部,利于流媒体随机访问);
  4. 缩放算法用好一点的(HDR 的高频细节多,lanczos 比 bilinear 好)。

HDR10+ 和杜比视界的动态元数据会在重编码时丢失——除非用专门的工具链。这点要在交付说明里写清楚。

七、元数据:MaxCLL / MaxFALL / Master Display

HDR10 的静态元数据:

元数据 含义
MaxCLL Maximum Content Light Level(内容最亮像素的亮度,nits)
MaxFALL Maximum Frame-Average Light Level(最亮帧的平均亮度)
Master Display 制作时用的监视器色域和亮度(用于播放器适配)

从源里读:

ffprobe -v error -select_streams v:0 -show_frames -read_intervals "%+#1" \
  -show_entries frame=side_data_list -of json input.mp4

会看到 Mastering Display Metadata 和 Content Light Level Metadata。

写入(x265):

-x265-params "master-display=G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1):max-cll=1000,400"

参数含义:

  • G/B/R:三原色的色度坐标(乘以 100000);
  • WP:白点坐标;
  • L(max,min):监视器的最大/最小亮度(乘以 10000);
  • max-cll=1000,400:MaxCLL=1000, MaxFALL=400。

常用的一组值(DCI-P3 监视器、1000 nits):

master-display=G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1)
max-cll=1000,400

如果源里有这些元数据,最好保留(很多播放器靠它决定怎么显示)。ffmpeg 重编码时默认不会自动传递,要显式指定——这是很容易漏的一步。

八、杜比视界:别用 ffmpeg 硬来

杜比视界(Dolby Vision) 是一个独立的生态:

  • 有双层结构(基础层 + 增强层)或者单层(profile 5/8);
  • 动态元数据(每帧/每个场景的映射信息);
  • 需要杜比授权的编码工具,ffmpeg 只能做很有限的处理(主要是传递 RPU);
  • 不同 profile(5/8.1/8.4 等)的处理方式完全不同。

我的建议:

  1. 如果客户给的是 Dolby Vision 素材,先问清楚要出什么(HDR10?SDR?还是保留 DV?);
  2. 出 HDR10/SDR 相对容易(ffmpeg 能做);
  3. 保留 DV 的转码需要专门工具链(杜比提供的工具或者商业转码服务);
  4. 不要"以为"ffmpeg 转完还是 DV——绝大多数情况下 DV 元数据会丢,出来的只是 HDR10,而客户可能不知道。

这一点我在交付文档里会明确写:"本交付为 HDR10(PQ),不含 Dolby Vision 动态元数据"。避免客户事后发现"我的杜比视界呢"。

九、交付检查清单

# 1. 看色彩参数
ffprobe -v error -select_streams v:0 -show_entries \
  stream=pix_fmt,color_primaries,color_transfer,color_space -of default=noprint_wrappers=1 out.mp4

# 2. 看有没有 HDR 元数据
ffprobe -v error -select_streams v:0 -show_frames -read_intervals "%+#1" \
  -show_entries frame=side_data_list -of json out.mp4

# 3. 抽帧看实际画面
ffmpeg -ss 60 -i out.mp4 -frames:v 1 -q:v 2 check.png
检查项 HDR 交付 SDR 交付
位深 yuv420p10le yuv420p(8bit 通用)
色域 bt2020 bt709
传输曲线 smpte2084(PQ)或 arib-std-b67(HLG) bt709
元数据 MaxCLL / Master Display 无
主观 在 HDR 显示器上看 在普通显示器上看

最后一条最重要:一定要在真实的 SDR 显示器上看一遍转出来的 SDR 版本。我在自己的 HDR 显示器上检查 SDR 版本时看不出问题(显示器会自动处理),但客户在普通显示器上看到的就是发灰的。

交付时同时给一版 SDR 和 HDR 是最稳妥的做法(除非客户明确只要一种)——因为播放端的支持情况你控制不了。

十、坑清单

  1. 以为 HDR 就是"亮度高" → 它是曲线 + 色域 + 位深的组合。
  2. 只设 -color_primaries 不做 tone mapping → 发灰。必须 tone mapping。
  3. tone mapping 时不转浮点格式 → 量化损失严重,画面出现色带。加 format=gbrpf32le。
  4. npl 不调 → 默认可能不适合你的内容。从 100 开始试。
  5. HDR→HDR 时误做了 tone mapping → HDR 信息被压掉了。
  6. 忘了 -color_trc → 只设了 primaries,曲线还是错的。
  7. HDR 元数据(MaxCLL)没传递 → 播放器不知道怎么显示。显式写入。
  8. 8bit 输出 HDR → 会出现严重的色带。HDR 必须 10bit。
  9. 以为 ffmpeg 能处理杜比视界 → 基本不行。用专门工具,或者明确降级到 HDR10。
  10. 在 HDR 显示器上检查 SDR 版本 → 看不出问题。用普通显示器检查。
  11. 滤镜链里有空格 → 解析失败(反斜杠换行时行首不能有空格)。
  12. zscale 没有编译进 ffmpeg → no such filter。用带 libzimg 的构建。
  13. HLG 和 PQ 搞混 → 用错的曲线处理,画面完全不对。先 ffprobe 确认。
  14. 缩放在 tone mapping 之前 → 顺序影响不大,但一般先缩放(省算力),再 tone mapping。
  15. 没出 SDR 兜底版本 → 用户在不支持 HDR 的设备上看到发灰画面。

最后说说这件事的教训。

我一开始的误区是"用参数去解决一个流程问题"。

看到"颜色发灰",我的第一反应是"哪个参数没设对"——于是去试 -color_primaries、-colorspace、-pix_fmt。这些参数都是对的,但都不解决问题,因为它们只是"标签"(告诉播放器怎么解释数据),而真正的问题是数据本身需要转换(tone mapping)。

标签和转换是两回事:

  • 标签(color_primaries 等)= 告诉别人"我是什么";
  • 转换(tonemap / zscale)= 把数据从一种表示变成另一种表示。

这个区分在很多地方都有:

  • 音频的采样率标签 vs 重采样;
  • 视频的帧率标签 vs 帧率转换;
  • 字幕的编码声明 vs 实际转码。

"改标签"几乎总是比"做转换"简单,所以当出现问题时人会本能地先试改标签——但它解决不了根本问题。 现在我遇到这类问题的第一反应是:"这是标签问题还是数据问题?"——这一问能省掉大量无效的尝试。

还有一点:HDR 领域里"工具能力"和"标准"之间有巨大的鸿沟。ffmpeg 能处理 HDR10(PQ/HLG),但处理不了杜比视界、HDR10+ 这些带动态元数据的格式。知道自己工具的能力边界,比知道怎么用工具更重要——否则你会交付一个"看起来是 DV 其实 DV 信息已经丢了"的文件,而客户要到很久以后才会发现。

想亲手试试?用 VidDown 一键解析下载

粘贴视频链接即可解析,多平台支持、网页端即用;下载桌面客户端解锁海外平台本地解析,开通会员更享不限次下载。

顶部