色域、YUV 与 HDR
色域、YUV 与 HDR
1. 三个字段,一张地图
先把整篇的骨架摆出来。一份图像数据要被正确解释,必须知道三件事:
| 字段 | 回答的问题 | 本系列在哪讲 |
|---|---|---|
| standard | 这里的"红"到底有多红?(色域) | 本篇第 2~4 节 |
| transfer | 码值和光量之间怎么换算?(传递函数) | 第 8 篇,本篇第 7 节补 HDR |
| range | 0~255 全用,还是只用 16~235? | 本篇第 6 节 |
三者描述的是不同维度,所以才拆成独立位域——这就是问题 1 的答案。BT.2020 的原色可以配 PQ,也可以配 HLG;BT.709 相关数据既可能使用 full range,也可能使用 limited range。不过"能按位拼接"不等于任意组合都有标准语义,工程上应优先使用平台已经定义的合法组合。任何一个字段搞错,画面都会不对,但错法各不相同:
- standard 错 → 颜色偏(红的不够红、绿的发荧光)
- transfer 错 → 明暗关系整体不对(发灰或死黑)
- range 错 → 对比度不对(黑的不够黑、白的发灰)
2. 色域:六个数为什么够
先问:颜色是什么
人眼里有三种感知颜色的细胞(视锥细胞),分别对长、中、短波长敏感。任何进入眼睛的光,最终都被压缩成三个数——这三种细胞各自的响应强度。
这件事有个重要推论:两束光谱完全不同的光,只要让三种细胞产生相同的响应,人眼就分辨不出。屏幕之所以能用红绿蓝三个灯模拟出各种颜色,全靠这一点。
CIE 在 1931 年把"三个响应"标准化成了三个数 ,再归一化成两个坐标:
把所有能看到的颜色画在 平面上,就是那张著名的马蹄形图。
色域 = 图上的一个三角形
图:黑色马蹄形边界上是最纯的单色光,内部是人眼能看到的全部颜色。三个三角形是三套标准各自能表达的范围。
一块屏幕只有红绿蓝三个原色(primaries)。 它能显示的颜色,是这三个原色按不同比例混合出来的——几何上就是以这三个点为顶点的三角形内部。三角形之外的颜色,这块屏幕物理上做不出来。
所以定义一个色域,只需要说清三个顶点在哪:
这就是问题 2 的答案——六个数就是三角形的三个顶点坐标。
白点:再加一对坐标
还差一件事:"R=G=B=1 时应该是什么白"。这叫白点(white point),也是一个 坐标。上面三个色域都用 D65(),大致对应正午日光。
三个原色 + 一个白点 + 传递函数参数,一个色彩空间就定死了。sRGB 的参数在第 8 篇已经给出:原色就是上面那六个数,白点是 D65,传递函数是第 8 篇那条分段曲线。
3. 色域转换:绕道 XYZ
现在的问题是:一张 sRGB 的图,怎么在 Display P3 的屏幕上正确显示?
关键认识:(R, G, B) = (1, 0, 0) 这组数字本身没有颜色含义。它的意思是"把这块屏幕的红色原色开到最亮"——而 sRGB 的红和 P3 的红是不同的颜色(三角形顶点位置不一样)。
所以转换的思路是:
sRGB 的 (R,G,B) → XYZ(绝对的、跟设备无关的颜色) → P3 的 (R',G',B')XYZ 是通用中间语言。 任何色域都能转进 XYZ,也都能从 XYZ 转出来。
矩阵怎么来
转换矩阵由原色与白点直接解出:从三个原色和白点,解出 RGB→XYZ 的 3×3 矩阵。
它的思路不复杂:矩阵的每一列应该是"某个原色开到最亮时的 XYZ 值"(这正是第 2 篇讲的"矩阵的列 = 基向量的去处"!)。原色的 已知,缺的只是各自的亮度权重,而白点给了三个约束(R=G=B=1 时必须得到白点的 XYZ)——三个未知数三个方程,解出来就完了。
用 lab_09 按这套办法算 sRGB,得到:
盯住中间那一行。
这三个数你见过无数次。在线性 RGB 上,它们是 BT.709/sRGB 的相对亮度权重;BT.709 也把它们写成 、。对线性 RGB 做点积 ,算的就是相对亮度。
它们不是拍脑袋定的经验值,就是 RGB→XYZ 矩阵的第二行。 因为 这个分量在 CIE 的定义里代表相对亮度,所以对线性 RGB 来说,"算相对亮度"和"算 XYZ 的 分量"是同一件事。绿色权重最大(0.7152),是因为人眼对绿光最敏感。
视频 YCbCr 里还会看到同一组系数,但那时通常作用在经过传递函数编码的 上,得到的是 luma ,不是这里的线性亮度 。数字相同,不代表运算空间和物理含义相同。
两个矩阵一乘就完事
第 2 篇讲过的"多个变换预先乘成一个",在这里又用了一次——运行时只需要一次 3×3 乘法。输出前乘一个 4×4 颜色矩阵,就是这类转换的容身之处。
对角线接近 1、非对角接近 0,说明 sRGB 和 P3 挺接近;非对角项越大,转换时颜色改动越多。
⚠️ 这个矩阵必须作用在线性值上。 第 8 篇的铁律在这里同样成立:完整流程是 EOTF 解码 → 乘矩阵 → OETF 编码。直接拿编码值乘这个矩阵是错的,而且错得不明显——颜色会有细微偏差,很难靠肉眼定位。
4. 色域外的颜色怎么办
P3 的图放到 sRGB 屏幕上,会有一部分颜色落在 sRGB 三角形之外——物理上显示不出来。
处理办法有两类:
- 裁剪(clip):超出的分量直接钳到边界。简单、快,但饱和区域会丢层次(几种不同的饱和红全变成同一个红)。
- 色域映射(gamut mapping):整体压缩一下,牺牲一点饱和度换取层次保留。
转换后的分量可能变成负数,这表示该颜色落在目标 RGB 原色能用非负分量表示的色域之外。负分量不是某个灯真的发出"负光",但在中间计算和扩展色域表示中可以保留,用来维持颜色关系;若过早逐通道 clamp,可能改变色相和亮度。到最终受限输出时,色域外的颜色被裁切或重映射;中间出现的负分量不必立即截断。
5. YUV:把 luma 和色度拆开
颜色解释标签的第三个字段和 YUV 直接相关,先说 YUV 本身。
动机:人眼对明暗细节的空间分辨力,远高于对颜色变化的分辨力。既然如此,颜色信息就可以少存一点。
RGB 里三个通道各自都混着明暗和颜色信息,没法单独砍。所以先换个表示法。BT.709 的数字视频通常从非线性 计算:
是 luma,也就是经过传递函数编码后的明暗信号;、 是"蓝色差"和"红色差"。日常代码常把撇号省掉仍写成 Y,但它和上一节从线性 RGB 得到的 CIE 相对亮度 不是同一个量。
图: 平面几乎包含了全部可辨认的明暗内容,两个色度平面看起来平淡得多。最右边是 按 4:2:0 抽样(横竖各减半)之后再放大回去的样子——几乎看不出差别。
4:2:0 就是"色度横竖各减半"。省了多少?
| 每像素字节 | |
|---|---|
| RGB888 | 3 |
| YUV 4:4:4 | 3 |
| YUV 4:2:0 |
省一半。而且在典型观看条件下很难看出明显损失,因此视频编解码器、相机输出和硬件合成器都会使用色度抽样。视频最常见的存储格式是 YUV 4:2:0。
顺带解释一个混淆:YUV、YCbCr、YPbPr 严格说是不同场景下的名字(模拟/数字、分量视频),日常代码里基本混用。真正要小心的是系数:BT.601(标清,)和 BT.709(高清,)用错会让画面明显偏色。这也是颜色解释标签一定要带 standard 字段的原因之一——光看像素数据,你无法知道它当初是用哪套系数转的。
6. range:为什么只用 16~235
limited range 不占满全部码值。按位深 计,Y 大约用 到 ,色度大约用 到 。8 位时就是:
| 分量 | 有效码值 |
|---|---|
| luma(Y) | 16~235 |
| 色度(Cb、Cr) | 16~240 |
这就是问题 3 的答案:这是模拟电视时代留下的遗产。当年的信号需要在有效画面之外留出余量(headroom / footroom),用来放同步信号,也用来容纳滤波后可能轻微过冲的值。数字化的时候这个约定被原样继承了下来,视频领域至今如此。
结果就是一个极其常见的 bug:
| 情况 | 症状 |
|---|---|
| limited range 的数据被当成 full range | 对比度不足——码值 16 被当成灰而不是黑,235 也到不了最亮,整体像蒙了层雾 |
| full range 的数据被当成 limited range | 对比度过高——0 和 255 被扩展到有效范围之外再裁剪,暗部亮部细节被削平 |
这是"半懂地带"最典型的一个坑:不会崩溃、不会报错,只是"这个视频看起来怪怪的"。而且它常常出现在 RGB↔YUV 转换、或者视频图层送进合成器的边界上——只要颜色解释标签在某一层被丢掉或写错,就会发生。
7. HDR:PQ 与 HLG
最后一块。
SDR 的隐含假设
第 8 篇讲的 sRGB 传递函数本身是相对的,并不单独规定所有屏幕的物理峰值;sRGB 的标准参考显示环境常引用约 80 尼特白点,而现代 SDR 视频工作流又常把参考白放在约 100 尼特。为了和 HDR 曲线做直观对比,下面统一用 100 尼特 SDR 参考白来画示意图。
现实世界的亮度范围远不止如此:夜空约 0.001 尼特,室内照明几百尼特,晴天的云和阳光下的反光可以到几千甚至上万尼特。传统 SDR 制作只能把这些场景亮度映射到有限的显示范围——高亮物体很容易一起挤到接近参考白的位置。
HDR 要做的事:让编码值覆盖更大的真实亮度范围。这需要一条新的传递函数。
图:纵轴是对数刻度的真实亮度。同一个编码值,在三条曲线下对应的亮度差了两个数量级。
PQ 与 HLG 的根本区别
| PQ(ST 2084) | HLG | |
|---|---|---|
| 编码的是 | 绝对亮度 | 相对亮度 |
| 码值 0.5 的含义 | "请发出约 92 尼特的光",跟屏幕无关 | "发出你峰值亮度的某个百分比" |
| 范围 | 最高 10000 尼特 | 由屏幕峰值决定 |
| 曲线设计依据 | 人眼在各亮度下的最小可分辨差异 | 暗部与传统 gamma 兼容,亮部转对数 |
| 向下兼容 SDR 屏 | ❌ 不兼容,必须做 tone mapping | ✅ 基本兼容,SDR 屏直接播放大致能看 |
| 典型场景 | 流媒体、蓝光、游戏 HDR | 广播电视直播 |
HLG 的名字就说明了设计:Hybrid Log-Gamma——混合了 Gamma(暗部)和 Log(亮部)。暗部沿用传统 gamma 曲线,所以老设备直接播放不会错得离谱;亮部切换成对数扩展动态范围。这个"向下兼容"的特性,对必须同时服务新旧电视的广播是刚需。
PQ 不做这个妥协,换来的是绝对性:内容制作时说"这里要 1000 尼特",任何 PQ 屏幕都会尽力发出 1000 尼特。代价是遇到峰值不够的屏幕,必须做一次 tone mapping 把超出的部分压回来——这是 M2 第 8 篇的主题。
绝对 vs 相对,是理解 HDR 的关键分水岭。 很多 HDR 相关的困惑("为什么同一个片子在两台电视上亮度差这么多""为什么 HDR 内容截图之后变得灰蒙蒙"),追到底都是这条线两侧的东西被混着处理了。
自测
问题 1:为什么拆成三个独立字段?
因为色域/矩阵系数、传递函数、码值范围描述的是三个不同维度,需要分别编码:BT.2020 原色可配 PQ 也可配 HLG,BT.709 相关数据也可能采用不同 range。拆成位域可以复用各维度,但不是任意位组合都有标准语义,应优先使用平台定义的合法组合。三者各自错了的症状不同:色域错→偏色,传递函数错→明暗关系错,range 错→对比度错。
问题 2:那六个数是什么?
CIE 1931 色度图上三个原色的 坐标——也就是这套标准能表达的那个三角形的三个顶点。再加上白点,一个 RGB 色彩空间的原色与参考白就定下来了。转换矩阵由原色与白点直接解出,而那个矩阵的第二行,就是常见的 0.2126 / 0.7152 / 0.0722。
问题 3:limited range 为什么浪费码值?
模拟电视时代的遗产:有效画面之外要留余量放同步信号、容纳滤波过冲。数字化时原样继承,视频领域沿用至今。搞错的症状是对比度整体不对——不崩溃、不报错,只是"看起来怪怪的"。
以后再看到一组颜色解释标签,你应该能把它拆成三段读:色域是什么形状的三角形、传递函数是哪条曲线、码值范围是哪一段。而两组标签之间的转换,就是这三件事各做一次换算——每一件都不能漏,漏了任何一件都会得到一张"说不上哪不对"的画面。
1. sRGB 和 Display P3 的白点相同,红原色不同。同样的 (1, 1, 1),两块屏幕显示的白一样吗?(1, 0, 0) 呢?
(1,1,1) 一样(白点相同就是这个意思);(1,0,0) 不一样——P3 的红原色在色度图上更靠外,是一个更饱和的红。
这正是"RGB 数值本身没有颜色含义"的直接体现:它只是"把这块屏幕的原色开到多亮",而不同标准的原色本来就是不同的颜色。
2. 0.2126 / 0.7152 / 0.0722 这三个数从哪来?
是 sRGB(BT.709)RGB→XYZ 矩阵的第二行。
CIE 定义里 分量代表相对亮度,所以对线性 RGB而言,"算相对亮度"和"算 XYZ 的 分量"是同一件事。绿色权重最大,是因为人眼对绿光最敏感。若同一组系数作用在视频的非线性 上,得到的是 luma ,不能和线性亮度混为一谈。
它们不是经验值,是从三个原色和白点解出来的。换一套原色(比如 BT.601 的),这三个数就变成 。
3. YUV 4:2:0 相比 RGB888 省了多少?为什么几乎看不出画质损失?
省一半:RGB888 每像素 3 字节,4:2:0 是 字节。
看不出损失,是因为人眼对颜色变化的空间分辨力远低于对明暗细节的。 平面保持原分辨率,被抽掉的只是色度——而色度本来就"看不太清"。
4. 一个视频图层画面发灰、黑色不够黑。最可能是哪个字段错了?
range。
limited range 的数据被当成 full range 处理时,码值 16 只会被解释成约 的灰,235 也达不到 full range 的最亮,结果是黑发灰、白不够白。
反过来,full range 数据若按 limited range 解码,0 和 255 会被扩展到合法范围之外并在输出时裁剪,症状是对比度过高、暗部亮部细节被削平。
对照另外两个字段:色域错会偏色,传递函数错会让明暗关系整体不对,症状不一样。
5. PQ 和 HLG 最根本的区别是什么?为什么广播电视偏爱 HLG?
PQ 编码绝对亮度,HLG 编码相对亮度。
PQ 的码值 0.5 意味着"发出约 92 尼特",跟屏幕无关;HLG 的码值意味着"发出你峰值的某个百分比"。
广播偏爱 HLG,是因为它向下兼容:暗部沿用传统 gamma 形状,同一路信号送到老的 SDR 电视上直接播放大致能看。广播必须同时服务新旧设备,不能为 HDR 单开一路信号。
PQ 不做这个妥协,遇到峰值不足的屏幕必须靠 tone mapping 压缩。
6. 做 sRGB → Display P3 转换时,直接拿 8 位码值乘那个 3x3 矩阵,对吗?
不对。 色域转换矩阵必须作用在线性值上。
完整流程是三步:EOTF 解码到线性 → 乘色域转换矩阵 → OETF 编码回码值。
直接乘编码值不会崩溃,只会产生细微的颜色偏差——这是第 8 篇那条铁律在色域转换上的又一次体现,也是最难靠肉眼定位的一类错误。
小结
| 概念 | 一句话 | 要点 |
|---|---|---|
| 颜色解释标签三段 | 色域/矩阵系数、传递函数、码值范围三个维度 | standard | transfer | range |
| 色域 | 色度图上的一个三角形 | 三个原色的 + 白点 |
| XYZ | 跟设备无关的通用中间语言 | 那两个互逆矩阵 |
| 转换矩阵怎么来 | 矩阵的列 = 原色的 XYZ;白点给出约束 | 由原色与白点直接解出 |
| 相对亮度权重的来历 | 线性 RGB→XYZ 矩阵的第二行 | 0.2126 / 0.7152 / 0.0722 |
| 色域转换 | 两个矩阵预乘成一个 3×3 | 输出前乘一个 4×4 颜色矩阵 |
| ⚠️ 前提 | 矩阵必须作用在线性值上 | EOTF → 矩阵 → OETF |
| 色域外 | 最终输出时裁剪或做色域映射;中间负分量不必立即截断 | 色域外的颜色被裁切或重映射 |
| YUV | luma 和色度分开,色度可以少存 | 4:2:0 省一半 |
| 系数陷阱 | BT.601 与 BT.709 用错会偏色 | 所以必须带 standard 字段 |
| limited range | 模拟电视时代的余量遗产 | 16~235;错了对比度整体不对 |
| PQ | 绝对亮度,最高 10000 尼特,不兼容 SDR | ST 2084 |
| HLG | 相对亮度,暗部兼容传统 gamma | 广播首选 |
延伸阅读
- GAMES101 Lecture 20:Color and Perception:https://games-cn.org/intro-graphics/
- Charles Poynton《Color FAQ》(色彩工程的经典问答):https://poynton.ca/ColorFAQ.html
- ITU-R BT.2100(PQ 与 HLG 的正式定义):https://www.itu.int/rec/R-REC-BT.2100

觉得有用?关注公众号「阿豪讲Framework」
Android 系统开发,新文章第一时间推送。