4、色彩空间与转换:BT.2020、DCI-P3、sRGB色彩空间详解、色彩转换矩阵

色彩空间,说白了就是颜色的「坐标系」。同一个红色,在不同坐标系里数值完全不同。做HDR编码,如果连色彩空间都没搞对,那后面的码率控制、色调映射全是白搭。

我刚开始接触HDR时,就犯过一个低级错误——把BT.2020的源当成sRGB去编码,结果出来的画面颜色完全「飘」了,肤色发紫,草地像荧光棒。从那以后,我养成了一个习惯:拿到任何素材,第一件事就是确认它的色彩空间元数据。

4.1 三大色彩空间:BT.2020、DCI-P3、sRGB

先看一张对比图,直观感受一下它们的覆盖范围:

三大色彩空间色域覆盖对比 sRGB / BT.709 DCI-P3 BT.2020 波长范围:380nm ~ 780nm

这张图里,最外面那个大三角形是BT.2020,中间是DCI-P3,最小的虚线三角形是sRGB。你想想看,BT.2020的色域几乎是sRGB的两倍。这意味着什么?意味着你在sRGB里看到的「最红」,在BT.2020里只是「偏红」而已。

属性 BT.2020 DCI-P3 sRGB / BT.709
应用场景 UHD HDR、广播级 数字影院、HDR显示器 网页、SDR、传统显示
色域覆盖率 约 75.8% CIE 1931 约 45.5% CIE 1931 约 35.9% CIE 1931
白点 D65 (6500K) D65 (影院常用DCI白点) D65 (6500K)
伽马/EOTF PQ / HLG 伽马 2.6 / PQ 伽马 2.2
位深要求 10-bit / 12-bit 10-bit 推荐 8-bit 可接受

核心要点:BT.2020 是 HDR 的「终极目标」色彩空间,但当前大多数消费级显示器还覆盖不了它的全部色域。DCI-P3 是现实中的「最佳折中」,sRGB 则是向下兼容的底线。

4.2 色彩转换矩阵:从数学到工程

色彩空间转换,本质上就是矩阵乘法。把RGB三通道的值,通过一个3x3矩阵,映射到另一个色彩空间的RGB值。

公式长这样:

[R']   [ a11  a12  a13 ]   [R]
[G'] = [ a21  a22  a23 ] * [G]
[B']   [ a31  a32  a33 ]   [B]

听起来简单?但坑全在系数里。不同标准定义的矩阵系数不一样,而且转换前必须先做「归一化」和「去伽马」。

4.2.1 BT.2020 转 sRGB 的完整流程

我个人习惯把转换拆成三步:

  1. 去伽马(Inverse EOTF):把非线性编码值还原成线性光强
  2. 矩阵变换:从源色彩空间转到目标色彩空间
  3. 重伽马(EOTF):把线性值再编码成目标空间的非线性值

举个例子,BT.2020 转 sRGB 的矩阵系数(线性光域):

// BT.2020 到 sRGB 的转换矩阵(线性域)
// 来源:ITU-R BT.2087
const float mat_bt2020_to_srgb[3][3] = {
    { 1.6605, -0.5876, -0.0728 },
    { -0.1246,  1.1329, -0.0083 },
    { -0.0182, -0.1006,  1.1187 }
};

⚠️ 我曾经踩过的坑:直接拿非线性值做矩阵乘法!结果颜色完全错乱。记住:矩阵变换必须在线性光域进行。非线性域里的矩阵乘法,数学上就不成立。

4.2.2 常见转换矩阵速查表

源 → 目标 矩阵系数(行主序) 参考标准
BT.2020 → BT.709 1.6605, -0.5876, -0.0728
-0.1246, 1.1329, -0.0083
-0.0182, -0.1006, 1.1187
BT.2087
BT.709 → BT.2020 0.6274, 0.3293, 0.0433
0.0691, 0.9195, 0.0114
0.0164, 0.0880, 0.8956
BT.2087
DCI-P3 → BT.2020 0.8225, 0.1775, 0.0000
0.0332, 0.9668, 0.0000
0.0171, 0.0724, 0.9105
SMPTE RP 431-2
DCI-P3 → sRGB 1.2249, -0.2249, 0.0000
-0.0420, 1.0420, 0.0000
-0.0197, -0.0786, 1.0983
Display P3 标准

💡 实战技巧:如果你在做实时编码,别每次都算矩阵乘法。把转换矩阵预计算好,存成查找表(LUT)。我一般在初始化阶段就把 10-bit 的 1024 级映射表算好,运行时直接查表,速度能快 3-5 倍。

4.3 色彩空间转换的工程陷阱

理论讲完了,聊聊实际工程里那些让人头疼的问题。

4.3.1 色域裁剪(Gamut Clipping)

BT.2020 里的某个颜色,在 sRGB 里可能根本不存在。这时候怎么办?

最简单的做法是「硬裁剪」——超出范围的直接设成边界值。但这样会导致颜色细节丢失,尤其是高饱和度的红色和蓝色区域。

我推荐的做法是「软映射」:

// 软映射示例:保留色相,降低饱和度
float soft_clamp(float c, float max_val) {
    if (c <= max_val) return c;
    // 超出部分做非线性压缩
    return max_val + (c - max_val) * 0.3f;
}

4.3.2 位深与精度损失

8-bit 的 sRGB 转 10-bit 的 BT.2020,看起来是「升级」,但实际转换过程中,矩阵乘法会产生小数。如果直接截断,精度损失会累积。

我的做法是:

  • 转换过程中使用 16-bit 或 32-bit 浮点中间缓存
  • 最后输出前再做 dither(抖动处理)
  • 避免多次来回转换——每转一次,精度就丢一点

4.3.3 元数据同步

色彩空间转换不只是像素的事。编码器里的 VUI(视频可用性信息)参数必须同步更新。我见过有人像素转对了,但码流里写的还是 BT.709,结果解码器按错误的空间去显示,画面全乱套。

检查清单:

  • ✅ 像素数据已完成矩阵变换
  • ✅ VUI 中的 colour_primaries 已更新
  • ✅ transfer_characteristics 已更新
  • ✅ matrix_coefficients 已更新
  • ✅ 如果使用 HDR10,SEI 中的 SMPTE ST 2086 元数据也要同步

4.4 实时编码中的色彩空间策略

做实时编码时,色彩空间转换不能拖慢主流程。我一般这样设计管线:

实时编码色彩空间转换管线 源色彩空间 去伽马 (Inverse EOTF) 3x3 矩阵变换 (线性域) 重伽马 (EOTF) ⚠️ 关键约束 矩阵变换必须在线性光域进行 转换前后必须同步更新 VUI 元数据

在实际项目中,我通常把色彩空间转换放在解码之后、编码之前。如果源和目标都是 HDR(比如 BT.2020 PQ 转 DCI-P3 PQ),那去伽马和重伽马可以省略,直接做矩阵变换就行——因为 PQ 本身就是线性光域的编码。

💡 性能优化建议:

  • 使用 SIMD(如 ARM NEON、x86 AVX2)加速矩阵乘法,单指令能处理 4-8 个像素
  • 预计算 10-bit LUT,把矩阵变换和伽马合并成一次查表
  • 如果 GPU 可用,用 compute shader 做逐像素并行转换

4.5 总结:色彩空间转换的「黄金法则」

做了这么多年 HDR 编码,我总结了几条铁律:

  1. 先确认,再转换——永远不要假设源的色彩空间
  2. 线性域是桥梁——所有矩阵变换都在线性光域做
  3. 精度留余量——中间计算用 16-bit 或 32-bit 浮点
  4. 元数据同步——像素变了,VUI 和 SEI 也得跟着变
  5. 测试覆盖所有色域边界——高饱和色最容易出问题