HDR与SDR视频全链路解析:PQ/HLG、色域标签与色调映射实战
1. 概念对齐:HDR 与 SDR 的差别不在“亮”,在“范围”
HDR 视频与 SDR 视频这两个词,几乎每个折腾过片源、播放器、电视设置的人都见过,但真正能说清楚差别的人并不多。我见过太多人把 HDR 简单理解成“更亮”“更鲜艳”,也见过有人把 SDR 片源塞进 HDR 容器里当 HDR 卖,画面糊成一片还觉得是解码器不行。这篇东西想解决的就是这件事:把 HDR 视频和 SDR 视频从标准、编码、元数据到播放链路的每一段拆开讲清楚,让你拿到一个文件时,能判断它到底是什么、该怎么处理、出了问题往哪儿查。
内容适合三类人:一类是刚开始接触蓝光原盘、压制、转码的爱好者,想搞清楚那些“color_transfer=smpte2084”“10bit 4:2:0”“MaxCLL”到底是什么;一类是做视频内容、需要交付不同版本的技术人员,甲方要 HDR 版本又要 SDR 版本,你得知道两边不是简单调个亮度就行;还有一类是折腾家庭影院的玩家,设备都买了,画面就是不对劲,想找到是链路上哪一环掉了链子。三类人关注的点不一样,但底层是同一套东西。
1.1 三个维度:亮度范围、色域范围、量化精度
把 HDR 拆开,其实是三个互相独立的维度同时在扩,很多人只记住了第一个。亮度范围指画面能表达的明暗跨度,SDR 的参考白通常按 100 nit 设计,上限也就这么高;HDR 的内容母版动辄按 1000 nit、4000 nit 做,理论编码上限到 10000 nit。色域范围指能表达的颜色边界,SDR 长期用 BT.709,HDR 用 BT.2020,后者在 CIE 1931 色度图上的覆盖面积大概是前者的两倍多。量化精度指用多少个数字台阶去描述一段连续的亮度或颜色,SDR 常见 8bit,HDR 基本要求 10bit 起步。
这三个维度必须一起扩,只扩一个就会出问题。只提亮度的结果就是高光全糊成白块,因为 8bit 的码值台阶根本撑不住这么大的动态范围,暗部一根一根的色带就是这么来的。只扩色域的后果更微妙,在 BT.709 显示器上看 BT.2020 的内容,如果不做正确的色彩转换,颜色会偏得离谱,红色发橙、绿色发灰。所以后面讲编码参数时你会看到,色域标签(color_primaries)、传递函数(color_transfer)、矩阵系数(color_space)永远是一组出现,缺一个都会让播放端猜错。
我个人的经验是,判断一个文件是不是“真 HDR”,先别看它标称多少 nit,先看它的三个标签是不是自洽。标签写着 bt2020 / smpte2084 / bt2020nc,但内容是 8bit、码率只有 3 Mbps,那大概率是拿 SDR 拉伸出来的。反过来,一个 HLG 的 10bit 文件,码率也不高,但标签齐全、内容真实,它在支持 HLG 的电视上观感会很自然。这就是“范围”和“标签”的区别,范围是内容本身的物理属性,标签是告诉播放端该怎么解释这些数字的说明书。
1.2 为什么“能显示更亮”不等于“看得更清楚”
这里有个特别容易被忽略的常识:HDR 的价值主要不在亮的地方,而在亮和暗同时存在的时候。举个数:一个逆光的窗户场景,SDR 版里窗外是一片纯白(码值 235 顶死),窗内的人脸为了看得见被提亮,结果整张脸发灰、没有立体感。HDR 版里窗外可以是 800 nit、窗内人脸是 5 nit,两者差了一百多倍,但都在编码范围内,人脸保留了层次,窗外还留着云的纹理。
换句话说,HDR 扩的是动态范围的跨度,不是把整幅画面往上推。这解释了一个反直觉的现象:很多人第一次看 HDR 会觉得“怎么还没 SDR 亮”,因为播放端做了正确的色调映射,把整体亮度压在了显示器能力范围内,只是高光和暗部的相对关系更接近真实。真正该看的指标是“同一帧里最亮和最暗能差多少档”,不是绝对亮度值。
这也带出一个实操上的坑:调色或者转码时,如果为了让画面“看起来更 HDR”而猛提整体亮度,结果是中间调被推爆,肤色发白,画面失去层次。正确的做法是先确定参考白的位置,再让高光自然往上延伸。后面第 5 节讲色调映射参数时,会具体说这个“参考白”怎么在滤镜链里体现。
1.3 一条链路里的分岔点:从采集到显示
一条完整的视频链路是:采集 → 后期调色 → 编码 → 封装 → 传输 → 解码 → 显示。HDR 和 SDR 在这七段里,几乎每一段都有分岔。采集端可能用不同的 Log 曲线,后期的工作色彩空间可能是 Rec.2020 或者更大的线性空间,编码时传递函数选 PQ 还是 HLG,封装时要不要写元数据,传输时带宽不够要不要降码率降位深,解码端是硬解还是软解,显示端的能力上限是多少、由谁来做 tone mapping。
最要命的是最后一步:tone mapping 到底由谁做。理想情况下,内容带着元数据,显示设备知道自己的能力,自己决定怎么把 4000 nit 的内容压到 800 nit 的面板上。现实里可能播放器先压一遍,系统再压一遍,显示器又压一遍,三次映射叠加,画面必然发灰发闷。我见过不少人抱怨“HDR 片子看着像蒙了层雾”,最后查出来是播放器和系统双重映射,把播放器那一层关掉就正常了。这个排查思路在第 6、7 节会展开。
所以下面我不会按“标准文档”的顺序讲,而是按你实际会遇到的顺序:先搞懂传递函数和色域这些决定数据含义的东西,再看位深和采样这些决定数据精度的东西,然后看元数据这个决定播放端行为的东西,最后落到实操和排查。你可以按顺序读,也可以直接跳到自己在意的章节。
2. 传递函数与色域:HDR 真正的技术骨架
如果把 HDR 比作一套语言,色域是它的词汇量,传递函数是它的语法。词汇量决定你能说出多少种颜色,语法决定这些词怎么排列、怎么被理解。很多人只盯着“10bit”“HDR”这些标签,忽略了传递函数才是决定“这个数字代表多少亮度”的根本规则。同一个码值,在不同传递函数下代表的物理亮度能差几十倍,这才是为什么色彩标签错了一切都错。
2.1 Gamma 曲线不是随便定的
SDR 时代用的是 Gamma 曲线,常见 BT.1886 定义的 Gamma 2.4,也有用 2.2 的。它的作用是把人眼的非线性感知和显示设备的线性输出对齐:人眼对暗部变化敏感、对高光变化迟钝,所以编码时把更多码值分配给暗部,显示时再用反向曲线还原。这是一套在 100 nit 上限内工作得很好的方案,代价是它只能在有限范围内保持精度。
Gamma 的本质是相对的:码值 1.0 对应“最亮”,至于这个“最亮”是 100 nit 还是 300 nit,取决于显示设备。这带来一个好处,同一份内容在不同亮度的屏幕上都能看;也带来一个坏处,没法精确表达“这个像素应该是 800 nit 而不是 200 nit”。HDR 要的恰恰是后者,因为 HDR 的整个价值建立在“高光和中间调的绝对关系”上,如果最亮值浮动,那这个关系就散了。
所以 HDR 换了两套思路:PQ 走绝对亮度,HLG 走相对但兼容。这两套不是谁更高级的问题,是适用场景不同。下面分别说。
2.2 PQ:用绝对亮度说话的编码方式
PQ 全称 Perceptual Quantizer,定义在 SMPTE ST 2084 里,后来被 ITU-R BT.2100 采纳。它的核心是把 0 到 10000 nit 的绝对亮度,用一种符合人眼感知的曲线映射到 0 到 1 的归一化码值上。注意关键词是绝对:码值 0.7514 在 PQ 下永远代表 1000 nit,不管你的显示器是 300 nit 还是 1000 nit 面板,这个对应关系不变。显示器显示不了的部分,由 tone mapping 去压缩,而不是靠改曲线。
这条曲线的形状可以用公式算出来,逆变换(从亮度求码值)大概是:
N = ((c1 + c2 * Y^m1) / (1 + c3 * Y^m1)) ^ m2其中 Y 是归一化亮度(实际亮度除以 10000),m1 = 2610/16384 ≈ 0.1593,m2 = 2523/4096 × 128 ≈ 78.84375,c1 = 3424/4096 ≈ 0.8359,c2 = 2413/4096 × 32 ≈ 18.8516,c3 = 2392/4096 × 32 ≈ 18.6875。这些常数看着吓人,实际用的时候你不需要手算,但知道它们存在有好处,因为你在调参数时会看到“PQ 编码下 100 nit 大概是多少码值”这类问题。
我算过两组常用值,可以直接记:100 nit 在 PQ 下归一化约 0.5121,10bit 码值约 524;1000 nit 归一化约 0.7514,10bit 码值约 769。这两个数字非常有用。第一,它说明 SDR 的整个亮度范围只占 PQ 编码空间的中间一小段,上面一大截码值是留给高光的,所以 HDR 内容降位深到 8bit 时,高光部分的台阶会特别明显。第二,它说明把 SDR 内容“放进”PQ 容器时,如果不做亮度重映射,画面会整体发暗——因为原来码值 235 的白色现在只代表 100 nit,而显示端可能按 200 nit 去解释它。
注意:PQ 是绝对亮度编码,任何“把 SDR 直接标成 PQ”的操作,本质上都是在撒谎,播放端按 PQ 解释出来的画面一定是错的。正确的做法是用 zscale 之类的滤镜做真正的传递函数转换加色调映射。
2.3 HLG:带 SDR 兼容基因的方案
HLG 全称 Hybrid Log-Gamma,同样在 BT.2100 里定义。它的设计目标很明确:让同一份信号,在 SDR 电视上能看(虽然效果打折),在 HDR 电视上能发挥。做法是在曲线的低段用接近 Gamma 的形状,高段逐渐过渡到对数形状,整体是相对的,没有规定绝对亮度参考,由显示端按自己的峰值亮度去解释。
这个特性让 HLG 在广播场景里特别吃香,因为广播链路里你没法保证每个观众的设备能力,兼容性是第一位的。代价是 HLG 的“HDR 感”通常没有 PQ 那么强烈,因为它的高光延伸是相对的,显示端亮度不同,观感差异会比较大。我个人的观感是,HLG 更适合纪录片、体育、自然类内容,画面自然;PQ 更适合电影,因为电影母版本身就是在受控环境里按绝对亮度做的。
实操里有个细节:HLG 内容如果不带任何元数据,播放端默认按 BT.2100 的参考条件解释,问题不大;但如果一个文件标着 HLG 而实际内容是 PQ 转换过来的,画面会整体偏暗偏灰,因为两套曲线的中段形状不同。判断方法还是看标签加实际抽帧亮度统计,第 5 节会给具体命令。
2.4 BT.709 与 BT.2020 的实际差距
色域这块,数字比想象中悬殊。BT.709 在 CIE 1931 xy 色度图上覆盖的面积大约是 0.1124 平方单位,占整个可见色域的百分之三十多;BT.2020 覆盖约 0.2358,占比接近百分之七十六。也就是说,理论上 BT.2020 的词汇量是 BT.709 的两倍出头。
但这里必须泼一盆冷水:目前绝大多数消费级显示设备都覆盖不了完整的 BT.2020。常见的量子点面板大概能到 BT.2020 的七成左右,普通液晶更少。这意味着你看到的大多数“BT.2020 内容”,实际显示时都被显示器按自己的能力裁剪过了。这不是骗局,是物理限制,标准先定得宽一点,让内容有足够的余量,设备慢慢追。
对实操的影响是:做 HDR 内容时,色域标签必须写对,但不要指望观众看到完整效果;做 SDR 版本时,从 BT.2020 往 BT.709 转换不能简单裁剪,超出 BT.709 边界的颜色要做压缩映射,否则会直接切平,出现色块。常见的做法是用感知型色域映射算法,把超出部分按比例往内收,保持色相不变,牺牲一点饱和度。这个步骤在调色软件里通常叫“色域映射”或者“gamut mapping”,在命令行工具里也能做,第 5 节会给例子。
还有一点容易忽略:白点。BT.709 和 BT.2020 都用 D65,色度坐标是 (0.3127, 0.3290),所以在白点上两者是一致的。但如果你在转换过程中不小心把白点改了,整幅画面会偏冷或偏暖,而且这种偏移在所有色相上表现不一致,非常难查。我踩过一次,把一段素材从 BT.2020 转到 BT.709 时用了错误的矩阵,肤色全变成蜡黄,查了两天才发现是白点没保持。
3. 位深、色度采样与量化:容易被忽略的细节
前面说的是“数据代表什么”,这一节说“数据有多精细”。这两件事经常被混为一谈,结果是有人以为把 8bit 转成 10bit 就变 HDR 了。位深和采样决定的是精度和台阶,不改变数据的含义,格式转换如果只改位深不改标签和曲线,画质不会变好,文件倒是会变大一圈。
3.1 8bit 与 10bit 的真实差距
8bit 每通道 256 个台阶,10bit 是 1024 个,看着只是四倍,实际观感差距在暗部被放大。原因在于人眼对暗部的亮度变化特别敏感,而 Gamma 或者 PQ 曲线都把大量码值分配给了暗部,8bit 分到暗部的台阶依然不够用,于是就出现色带(banding)。天空渐变、烟雾、暗场墙面,这三个场景是色带的重灾区。
HDR 基本强制 10bit 起步,原因不只是暗部,还有高光。前面算过,PQ 编码下 100 nit 到 1000 nit 只占归一化码值 0.51 到 0.75 这一段,10bit 下对应 524 到 769,也就是 245 个台阶要描述十倍亮度变化。如果降到 8bit,同样的区间只有 61 个台阶,高光过渡必然出现一圈一圈的断层。这就是为什么 HDR 内容用 8bit 编码基本没有意义,哪怕码率给到很高也救不回来。
有人会问 12bit 是不是更好。理论上是的,杜比影院级别的母版会用 12bit,量化误差更小。但消费级分发基本停在 10bit,因为 12bit 的码率代价换来的观感提升在家庭环境里很难察觉,而且硬件解码支持也不普遍。我的建议是:分发用 10bit,母版和中间素材用 12bit 或更高,别在分发环节追求 12bit。
顺带说一个常见误解:把 8bit 素材“转”成 10bit,能不能消除色带?答案是基本不能。已有的色带是量化误差,转位深只是把误差原样搬到更大的台阶上,不会凭空长出细节。要真正消除,需要的是抖动(dither)或者去色带滤镜,那是在做视觉补偿,不是恢复信息。这点在做老片修复时特别重要,别指望改个位深参数就完事。
3.2 4:2:0 到底丢了什么
色度采样 4:2:0 的意思是:亮度(Y)全采样,两个色度分量(Cb、Cr)在水平和垂直方向都减半。1080p 的 4:2:0 素材,Y 是 1920×1080,Cb 和 Cr 各是 960×540。人眼对色度的分辨率本来就低于亮度,所以这个取舍在大部分画面里看不出来。
但在两类画面上会露馅:高饱和度的细线条和红蓝色的锐利边缘。前者比如彩色的细字、舞台灯光下的细密纹理,后者比如红色衣服的边缘、蓝色背景前的头发丝。这些位置会出现色度溢出或者边缘发虚,专业叫法是 chroma bleeding。HDR 内容的色域更宽、饱和度更高,4:2:0 的损失会比 SDR 时代更明显,所以有些高端分发会用 4:2:2,代价是码率上去一截。
实际工作里,我的选择是:分发用 4:2:0 10bit,这是兼容性和体积的平衡点;中间素材和调色输出用 4:2:2 或 4:4:4,避免多次转码累积损失。千万别在 4:2:0 的素材上反复做转码,每次转码都是一次色度重采样,五六次之后画面会明显发糊,尤其是红色区域。这个坑我在早期压制的时候踩得很惨,一堆素材来回导,最后成片颜色糊成一片,只能重做。
3.3 色域标签比数据本身更重要
这一条是整篇文章里我最想强调的:标签错了,数据再准也没用。视频流里有三个关键标签,分别告诉播放端“这些数字是什么颜色”“这些数字对应什么亮度曲线”“这些数字怎么从 YCbCr 还原成 RGB”。
| 标签字段 | 常见取值 | 作用 | 标错的后果 |
|---|---|---|---|
| color_primaries | bt709 / bt2020 | 定义 RGB 三原色的色度坐标 | 颜色整体偏移,红色发橙或发紫 |
| color_transfer | bt709 / smpte2084 / arib-std-b67 | 定义码值到亮度的映射曲线 | 画面整体过暗、过亮或发灰 |
| color_space(矩阵) | bt709 / bt2020nc | 定义 YCbCr 与 RGB 的换算矩阵 | 颜色饱和度异常,肤色失真 |
| color_range | tv / pc | 定义 16-235 还是 0-255 | 黑位发灰或暗部死黑 |
我遇到过最典型的一次:一段 BT.2020 的素材,封装时矩阵标成了 bt709,播放出来颜色浓得像加了滤镜,绿色植物直接变成荧光绿。原因很简单,BT.2020 的矩阵系数和 BT.709 不一样,用错误的系数还原,色度分量被放大了。改标签,一秒钟解决。
所以养成习惯:任何转码、封装、剪辑输出之后,都用工具把标签读一遍,确认三个字段自洽。具体命令在第 5 节。这一步花十秒钟,能省掉几小时的排查。
4. 元数据与主流 HDR 路线
元数据是 HDR 里最有“人为设计”味道的部分。它不参与画面数据的编码,而是额外告诉播放端:这份内容是按什么条件做的母版,最亮能到多少,平均亮度大概多少。播放端拿到这些信息,才能决定怎么把内容映射到自己的屏幕上。没有元数据,播放端只能猜,猜错就出问题。
4.1 静态元数据都在描述什么
静态元数据,顾名思义,整部片子共用一套,不随场景变化。最核心的三组信息是:
母版显示信息(定义在 SMPTE ST 2086),描述调色时用的监视器:三原色的色度坐标、白点坐标、最大亮度和最小亮度。常见写法是G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1),前面是色度坐标乘 50000 取整,L 后面是最大亮度和最小亮度乘 10000 取整,所以L(10000000,1)表示最大 1000 nit、最小 0.0001 nit。
内容光级(CTA-861.3 定义的 MaxCLL 和 MaxFALL)。MaxCLL 是整部片子里单个像素出现过的最高亮度,MaxFALL 是所有帧平均亮度的最大值。前者告诉播放端“最坏情况要多亮”,后者告诉它“整体大概多亮”。这两个值如果标得离谱,播放端的 tone mapping 就会判断失误。我见过标着 MaxCLL 4000 但实际内容最高只有 600 的片子,播放端按 4000 去压缩,结果整部片子被压得灰蒙蒙。
HDR10 就是这套组合的代表:BT.2020 色域 + PQ 传递函数 + 10bit + 静态元数据。它是目前兼容性最好的 HDR 基线格式,几乎所有 HDR 设备都支持。缺点是静态元数据对整部片子一视同仁,遇到明暗反差极大的片子(比如一半是夜景一半是雪景),用一套参数压全片,总有一半吃亏。
4.2 动态元数据:逐场景调整的思路
动态元数据的想法很直接:既然一部片子里每个场景的亮度分布差别很大,那就给每个场景甚至每一帧单独提供元数据,让播放端按当前场景的特性做映射。这样夜景不会被过度提亮,雪景也不会被过度压缩。
实现上有几种路径,差异主要在元数据是存在文件里还是随视频流传输、以及是否向下兼容。有一类是专有的商业方案,需要整条链路(内容、播放器、显示设备)都支持才能发挥,兼容性差但效果上限高。另一类是在 HDR10 基础上做扩展,把动态元数据附加进去,不支持扩展的设备会自动回落到静态元数据的 HDR10 基线。后者的思路更讨巧,兼容性友好,代价是效果没有专有方案那么极致。
对普通用户来说,这件事的实操意义是:如果你的设备不支持动态元数据,那这类内容在你这里就是普通 HDR10,不用纠结。如果你支持,那要注意播放链路里每一环都得放行,中间任何一个环节不认这个元数据,它就会被丢掉,效果退回基线。很多人觉得“动态元数据没用”,查下来是播放器把它吃了。
4.3 几条路线的对比
把常见路线放一张表里对照,比文字描述清楚:
| 路线 | 传递函数 | 元数据 | 兼容性 | 典型适用场景 |
|---|---|---|---|---|
| HDR10 基线 | PQ | 静态 | 极好,几乎全支持 | 通用分发、流媒体 |
| HDR10 扩展(动态元数据) | PQ | 动态,可回落 | 好,不支持则回落基线 | 高品质流媒体、新片 |
| HLG | HLG | 通常不带 | 极好,SDR 设备也能看 | 广播、直播、体育 |
| 专有动态方案 | PQ | 动态,全链路要求 | 差,需授权和全链路支持 | 高品质影碟、特定生态 |
这张表的用法是:拿到一个文件,先看它属于哪一行,就知道它对你的设备有什么要求。比如一个 HLG 的文件在老旧设备上也能放,只是效果打折;一个专有方案的文件在不支持的播放器上可能直接偏色或者拒播。
提示:不要迷信“格式越高级效果越好”。如果片源本身是 8bit 拉伸的,套上再高级的元数据也没用;反过来,一个制作规范的 HDR10 基线片子,在支持良好的设备上观感可以非常出色。
5. 实操:判定素材、转码与验证
理论说完了,这一节全是能直接抄的东西。我按实际操作顺序排:先判定素材到底是什么,再决定怎么处理,最后验证结果对不对。工具以命令行工具为主,因为参数透明、可复现,图形界面工具的原理是一样的,只是把参数藏在界面里。
5.1 先用 ffprobe 把素材看穿
拿到任何素材,第一步永远是读标签,不要凭文件名或者体积猜。命令很简单:
ffprobe -v error -select_streams v:0 \ -show_entries stream=codec_name,profile,width,height,pix_fmt,color_space,color_transfer,color_primaries,color_range,bit_rate,r_frame_rate \ -of default=noprint_wrappers=1 input.mkv看输出里的几个关键字段。pix_fmt是yuv420p10le说明 10bit,yuv420p是 8bit。color_transfer=smpte2084是 PQ,arib-std-b67是 HLG,bt709是 SDR。color_primaries=bt2020是宽色域,bt709是标准色域。
正常 HDR10 的组合应该是:yuv420p10le+bt2020+smpte2084+bt2020nc。只要有一个不匹配,就要追问为什么。我遇到过的几种情况:
pix_fmt=yuv420p但transfer=smpte2084:8bit 标 HDR,几乎肯定是拉伸出来的假 HDR。transfer=smpte2084但primaries=bt709:标签混乱,要么是转换时漏改,要么是制作不规范。- 三个标签全是
unknown:封装时没写,播放端只能猜,这种最容易出问题,需要手动指定。
如果标签缺失,ffprobe读不出来,还可以用抽帧加统计的方式反推。下面这条命令能算出整段视频的平均亮度(YAVG),粗略判断内容和标签是否匹配:
ffmpeg -i input.mkv -vf "select='not(mod(n,120))',signalstats,metadata=print:key=lavfi.signalstats.YAVG" -f null - 2>&1 | head -40如果标签写着 PQ,但抽出来的 YAVG 大量集中在 0.4 到 0.5 这个区间(10bit 码值 400-500 对应 PQ 下大概 50-100 nit),说明内容确实在 SDR 亮度范围内,很可能是假的。
5.2 编码参数与主控显示描述
如果是自己做 HDR 编码,x265 是目前最常用的选择。关键参数有三组,一个都不能省。下面是一条我常用的命令行骨架:
ffmpeg -i input.mov \ -c:v libx265 -preset slow -crf 18 \ -pix_fmt yuv420p10le \ -x265-params "hdr10=1:colorprim=bt2020:transfer=smpte2084:colormatrix=bt2020nc:\ master-display=G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1):\ max-cll=1000,400:hrd=1" \ -color_primaries bt2020 -color_trc smpte2084 -colorspace bt2020nc \ -c:a copy output.mkv几个参数的含义和取值依据:
master-display里的色度坐标要和调色监视器的实际参数一致,不是随便填。常见的 BT.2020 原色坐标是 R(0.680, 0.320)、G(0.265, 0.690)、B(0.150, 0.060),乘以 50000 取整就是上面那串数字。L 的第一个值是母版峰值亮度乘 10000,比如 1000 nit 写 10000000;第二个值是最小亮度乘 10000,0.0001 nit 写 1。填错会导致播放端算错映射曲线。
max-cll的两个值要基于实际内容统计,不能拍脑袋。理想做法是用工具扫一遍全片,取单帧最大亮度和帧平均亮度的最大值。粗略做法是抽样统计,或者按母版峰值给一个保守值。标高了画面发灰,标低了高光被削,两种都难看。
hrd=1是打开 HRD 信息,某些播放设备依赖它做缓冲判断,不加可能导致卡顿。这个参数在小文件上看不出差别,大文件上有用。
5.3 把 HDR 回落到 SDR 的色调映射
这是实操里最难的一步,也是最容易做砸的一步。HDR 转 SDR 不是简单降亮度,而是要把 1000 甚至 4000 nit 的动态范围,压缩进 100 nit 的容器里,同时尽量保持观感。核心是色调映射算法和参考白的选择。
用 zscale 滤镜链是目前命令行里最可控的做法:
ffmpeg -i input_hdr.mkv \ -vf "zscale=t=linear:npl=100,format=gbrpf32le,\ zscale=p=bt709,\ tonemap=tonemap=hable:desat=0,\ zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \ -c:v libx264 -crf 18 -preset slow \ -color_primaries bt709 -color_trc bt709 -colorspace bt709 \ output_sdr.mkv拆开看每一步在干什么。第一步zscale=t=linear:npl=100,把 PQ 曲线还原成线性光,npl指定参考白 100 nit,意思是“把 100 nit 映射成 SDR 的白色”。第二步转到浮点 RGB,避免中间过程量化损失。第三步zscale=p=bt709做色域转换,把 BT.2020 的颜色映射到 BT.709 边界内。第四步tonemap做动态范围压缩,算法有 hable、mobius、reinhard、clip、bt2390 几种可选。最后转回 YUV 4:2:0 并打上 SDR 标签。
算法怎么选,我的经验是这样的:
hable压缩感强,暗部保留好,适合大部分电影,缺点是饱和度会掉一点,可以用desat=0关掉额外的去饱和。mobius相对温和,高光滚降自然,适合本身亮度不极端的片子。bt2390是标准里定义的算法,最“正确”,但观感偏平,适合需要严格对照的场合。clip就是硬切,除了快速预览,不建议用于交付。
npl这个参数值得单独说。它决定了“多亮算白”。设成 100 是标准做法,但如果你发现成片整体偏暗,可以适当提到 150 或 200,让中间调亮起来;如果偏亮发灰,就降到 80。这个值没有绝对正确答案,取决于片源特性和目标观众的观看环境。我一般的做法是抽三到五个典型场景(日景、夜景、逆光、室内)各出一版对比,选一个平衡点,而不是只看一帧。
5.4 验证:截帧、波形与亮度统计
转完之后必须验证,别直接交付。三个检查动作,五分钟能做完。第一,重新ffprobe一遍,确认标签是bt709+bt709+bt709+yuv420p,别出现 HDR 内容套 SDR 标签这种四不像。第二,抽几个关键帧肉眼对比,特别是高光区(窗户、灯、天空)有没有被压成一片死白,暗部有没有糊成一团。第三,用signalstats看亮度分布,SDR 内容的 YAVG 正常应该在 90 到 140 之间(8bit 码值),YMAX 接近 235 说明高光保留得当,如果 YMAX 长期卡在 200 以下,说明映射压得太狠。
ffmpeg -i output_sdr.mkv -vf "select='eq(n,300)',signalstats,metadata=print" -vframes 1 -f null -这一步看似繁琐,但能抓住大部分问题。我有一次偷懒没验,交付后对方反馈“暗场看不见东西”,回头一查,tonemap 用了clip,暗部直接被切掉了。重做了两小时。从那以后,验证这一步我再也没省过。
6. 播放链路与显示端:为什么效果总对不上
内容做对了,播放端出问题的概率依然很高。HDR 的播放链路比 SDR 长,涉及解码、色彩管理、系统输出格式、显示器映射四五个环节,每一环都可能出错,而且症状相似,很容易误判。这一节按链路顺序说,方便你按症状定位。
6.1 硬件解码与色彩管理
解码本身通常不是问题,现代设备的硬解都支持 HEVC 10bit Main10 Profile。真正的问题是解码后的色彩管理由谁负责。理想的分工是:解码器只负责还原数据,播放器负责把数据按标签解释成正确的颜色,系统负责把颜色送到显示设备,显示设备负责最终的 tone mapping。
现实里这个分工经常被打破。有些播放器会自己做 tone mapping,有些交给系统,有些两者都做,结果就是双重映射。双重映射的典型症状是画面发灰、对比度低、高光没有力度,看着像隔了一层雾。判断方法很简单:找一个已知的 HDR 演示片(最好是自带亮度标尺的那种),在播放器里关掉“HDR 色调映射”选项,对比开关前后的画面。如果关掉之后画面明显更通透,说明之前是双重映射。
还有一个隐蔽的问题:系统输出格式。有些系统在开启 HDR 输出后,会把桌面和所有内容都按 HDR 输出,SDR 内容被“提升”到 HDR,画面会偏灰。正确做法是让系统根据内容自动切换,或者手动在播放 HDR 内容时开启。这个设置在系统显示设置里通常叫“HDR 开关”或者“自动 HDR”。
6.2 系统 HDR 开关与假 HDR
假 HDR 这个坑值得单独讲,因为它太常见了。三种典型形态:
第一种,8bit 拉伸。拿 SDR 素材,转成 10bit,标签改成 PQ/BT.2020,元数据随便填。这种文件在支持 HDR 的设备上会触发 HDR 模式,但因为内容本身没有高光信息,画面会显得平淡甚至偏暗,暗部还会因为位深转换出现色带。
第二种,容器套壳。内容完全是 SDR,封装时把色彩标签改成 HDR,本质是欺骗播放端。播放端按 PQ 解释,画面整体会暗一大截,因为 SDR 的码值在 PQ 下对应的亮度低得多。前面算过,SDR 白点 235 在 PQ 下大概只有 100 nit 出头,播放端如果按 1000 nit 的母版去映射,画面就废了。
第三种,元数据虚标。内容是真的 HDR,但 MaxCLL 标成 4000,实际最高只有 800。播放端按 4000 做映射,把本来不需要压的部分也压了,画面发闷。这种最隐蔽,因为文件确实有 HDR 内容,只是参数错了。
识别方法还是那套:ffprobe看标签组合,signalstats看实际亮度分布,两者对照。标签说 PQ 但亮度分布集中在 SDR 区间,就是假的;标签说 4000 nit 但抽帧统计最高只到 700,就是虚标。
6.3 显示器能力与 tone mapping 的取舍
最后一环是显示设备。这里有个残酷的现实:大部分消费级 HDR 显示器的峰值亮度远低于内容母版,所以 tone mapping 是必需的,不是可选的。区别只在于由谁做、做得好不好。
显示器自己做的 tone mapping,质量取决于厂商调校。有些设备的“HDR 模式”其实只是把背光拉满、对比度调高,根本不做真正的映射,结果就是高光过曝、暗部死黑。有些设备做得好,能根据内容元数据动态调整。判断方法:播放一段带高光细节的测试片(比如阳光下的水面、舞台灯光),看高光区是不是还有纹理。如果一片纯白,说明映射做得差或者根本没做。
播放器做的 tone mapping,可控性更好,因为你能调参数。缺点是不同播放器的算法差异大,同一个文件在两个播放器上观感可能完全不同。我的建议是:只让一个环节做 tone mapping,其他环节全部设成直通。具体是让播放器做还是让显示器做,取决于哪个质量更好,这需要你实际对比。我的经验是桌面环境让播放器做,客厅环境让显示器做,因为客厅的操作成本高,用户不会去调参数。
还有一个参数值得提:显示器亮度设置。很多人为了“HDR 效果”把亮度拉满,结果是 tone mapping 的输入范围变宽,中间调被压得更狠,画面反而更平。正确的做法是把显示器亮度设在一个舒适观看的水平,让 tone mapping 在合理的范围内工作。这个值因人而异,我的习惯是在暗环境里把峰值亮度设到设备能力的七成左右,观感最平衡。
7. 常见问题与排查速查
前面几节把原理和流程都铺开了,这一节收敛成可以随时查的东西。我按“症状 → 可能原因 → 验证方法”的顺序组织,遇到问题直接对号入座,能省掉大量试错。
7.1 症状到原因的反查思路
排查的核心逻辑是分段隔离:把链路切成内容、解码、播放器、系统、显示五段,每段单独验证。先确认内容本身没问题(ffprobe 标签自洽、抽帧亮度合理),再确认解码没问题(换个播放器试试),再确认播放器设置(关掉所有增强选项),再确认系统输出(HDR 开关状态),最后确认显示器(用已知正常的片源测试)。这个顺序不能乱,否则会在错误的环节浪费大量时间。
举个真实例子。有位朋友反馈他的 HDR 片子“整体发暗,暗部全黑”。我让他按顺序查:第一步 ffprobe,标签正常;第二步换播放器,症状一样,排除播放器;第三步看系统,发现系统 HDR 输出是关闭的,但播放器强制输出 HDR 信号,系统又把它当 SDR 二次转换,亮度被压了两次。打开系统 HDR 自动切换,问题解决。整个过程十分钟,如果一开始就去调显示器参数,可能调一晚上也调不对。
7.2 速查表
| 症状 | 最可能的原因 | 快速验证 |
|---|---|---|
| 画面整体发灰、像蒙雾 | 双重 tone mapping | 关掉播放器的 HDR 映射选项对比 |
| 画面整体偏暗、暗部死黑 | 系统 HDR 未开启或标签误导 | 检查系统 HDR 开关,ffprobe 看 transfer |
| 高光一片纯白无纹理 | 显示器映射差或 MaxCLL 虚高 | 换设备对比,检查元数据 |
| 暗部出现一圈圈色带 | 8bit 编码或位深不足 | ffprobe 看 pix_fmt |
| 颜色过饱和、荧光感 | 色彩矩阵标错 | 检查 color_space 是否为 bt2020nc |
| 肤色发黄或发青 | 白点或色域转换错误 | 检查 primaries 与转换矩阵 |
| 红色边缘发糊 | 4:2:0 色度采样损失 | 看是否多次转码,检查 pix_fmt |
| 播放卡顿、掉帧 | 硬解不支持或码率超带宽 | 换软解测试,看码率峰值 |
| HLG 内容偏暗 | 播放端按 PQ 解释 | 检查 transfer 是否为 arib-std-b67 |
| 同一文件两台设备观感差异大 | 各自 tone mapping 策略不同 | 统一映射责任方 |
这张表覆盖了我实际遇到过的大部分情况,但排查时别死套,症状可能叠加。比如“发灰 + 色带”同时出现,可能是假 HDR 加 8bit 双重问题,得分开解决。
7.3 几个踩过的坑
第一个坑是过度相信文件名。早期我拿到标着“HDR”的文件就直接按 HDR 流程处理,结果有一批是拉伸的,转出来颜色全错。后来养成习惯,任何文件先 ffprobe,两秒钟的事,能避免几小时的返工。
第二个坑是转码链太长。有一次做多语言版本,同一份素材在四五个工具之间来回导,每次都是 4:2:0 进 4:2:0 出,最后成片红色区域糊得厉害。教训是:中间环节尽量用高位深、高采样的格式,最后一步才降到分发格式。
第三个坑是元数据凭感觉填。刚开始做 HDR 编码时,MaxCLL 随手填 1000,结果有几部片子在暗环境观看时高光被压得没细节。后来老老实实用工具扫全片统计,虽然多花十几分钟,但成片质量稳定得多。
第四个坑是以为设备支持就万事大吉。有台设备标称支持 HDR10,实际映射算法很粗糙,高光全削。这种情况没法通过参数解决,只能换设备或者在播放器端做映射。买设备前最好带测试片去实测,别只看参数表。
最后分享一个我觉得最有用的小习惯:建立一个自己的测试片库,包含日景、夜景、逆光、高饱和、细线条、暗场渐变这几个典型场景,每段三十秒左右,带正确的 HDR 标签。每次改设置、换设备、做转码,都拿这几段过一遍,五分钟就能判断链路是否正常。这个习惯帮我抓住了至少三次隐蔽的配置错误,比任何理论都管用。