4、色彩空间与转换:BT.2020、DCI-P3、sRGB色彩空间详解、色彩转换矩阵
色彩空间,说白了就是颜色的「坐标系」。同一个红色,在不同坐标系里数值完全不同。做HDR编码,如果连色彩空间都没搞对,那后面的码率控制、色调映射全是白搭。
我刚开始接触HDR时,就犯过一个低级错误——把BT.2020的源当成sRGB去编码,结果出来的画面颜色完全「飘」了,肤色发紫,草地像荧光棒。从那以后,我养成了一个习惯:拿到任何素材,第一件事就是确认它的色彩空间元数据。
4.1 三大色彩空间:BT.2020、DCI-P3、sRGB
先看一张对比图,直观感受一下它们的覆盖范围:
这张图里,最外面那个大三角形是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 的完整流程
我个人习惯把转换拆成三步:
- 去伽马(Inverse EOTF):把非线性编码值还原成线性光强
- 矩阵变换:从源色彩空间转到目标色彩空间
- 重伽马(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 实时编码中的色彩空间策略
做实时编码时,色彩空间转换不能拖慢主流程。我一般这样设计管线:
在实际项目中,我通常把色彩空间转换放在解码之后、编码之前。如果源和目标都是 HDR(比如 BT.2020 PQ 转 DCI-P3 PQ),那去伽马和重伽马可以省略,直接做矩阵变换就行——因为 PQ 本身就是线性光域的编码。
💡 性能优化建议:
- 使用 SIMD(如 ARM NEON、x86 AVX2)加速矩阵乘法,单指令能处理 4-8 个像素
- 预计算 10-bit LUT,把矩阵变换和伽马合并成一次查表
- 如果 GPU 可用,用 compute shader 做逐像素并行转换
4.5 总结:色彩空间转换的「黄金法则」
做了这么多年 HDR 编码,我总结了几条铁律:
- 先确认,再转换——永远不要假设源的色彩空间
- 线性域是桥梁——所有矩阵变换都在线性光域做
- 精度留余量——中间计算用 16-bit 或 32-bit 浮点
- 元数据同步——像素变了,VUI 和 SEI 也得跟着变
- 测试覆盖所有色域边界——高饱和色最容易出问题