HDR详解
从物理原理到 Android 播放器实现的完整链路。
一、核心概念:一个像素是怎么发光的


像素的物理结构
一个像素 = 三个子像素(Sub-pixel)
┌─────────────────┐
│ R │ G │ B │ ← 三个独立的发光单元
└─────────────────┘
- LCD:三个子像素共享背光,液晶+滤光片控制透光量
- OLED:三个子像素各自独立发光,电流控制亮度
RGB 值的真正含义
RGB 值不是"绝对颜色",而是三个旋钮的比例。要还原出真实视觉效果,必须知道三件事:
| 参数 | 决定什么 | 类比 |
|---|---|---|
| 色域 (Color Primaries) | 旋钮背后接的是什么灯泡(子像素发什么颜色的光) | 尺子的单位 |
| 传输函数 (Transfer Function) | 旋钮刻度和实际亮度的对应关系 | 尺子的量程 |
| 位深 (Bit Depth) | 旋钮有多少格刻度 | 尺子的精度 |
从 RGB 值到子像素发光
RGB 编码值 (如 10bit: 0~1023)
↓ EOTF 曲线(传输函数)
线性光强度 (0~1 的比例)
↓ × 子像素最大发光能力
物理发光 (nits)
三个通道独立计算,互不影响。EOTF 是同一条曲线套三遍。
二、高画质的五个要素
| 要素 | 含义 | SDR (BT.709) | HDR (BT.2100) |
|---|---|---|---|
| 分辨率 | 像素数量 | 1920×1080 | 3840×2160+ |
| 位深 | 每通道编码精度 | 8bit (1677万色) | 10/12bit (10亿+色) |
| 帧率 | 时间维度清晰度 | 30/60fps | 60/120fps |
| 色域 | 能表示的颜色范围 | BT.709 (~35%可见色) | BT.2020 (~75%可见色) |
| 亮度(动态范围) | 最暗到最亮的范围 | 0~100 nits | 0~10000 nits |
HDR 的核心突破是第5项------前4项在 BT.2020 中已标准化,唯独亮度一直停留在 100 nits。
三、色域详解

马蹄图与三角形
人眼能看到的所有颜色构成马蹄形区域。任何一套 RGB 系统在马蹄图上画一个三角形(三个顶点 = 三个基色的色度坐标),三角形内的颜色能显示,外面的不行。
主流色域对比
| 色域 | R 色度 | G 色度 | B 色度 | 覆盖 |
|---|---|---|---|---|
| BT.709/sRGB | (0.640, 0.330) | (0.300, 0.600) | (0.150, 0.060) | ~35% |
| DCI-P3 | (0.680, 0.320) | (0.265, 0.690) | (0.150, 0.060) | ~45% |
| BT.2020 | (0.708, 0.292) | (0.170, 0.797) | (0.131, 0.046) | ~75% |
关键理解
同样的 RGB(255,0,0),在不同色域的屏幕上发出的红光不一样:
- BT.709 屏幕的红色子像素 → 发色度 (0.640, 0.330) 的红(偏橙)
- BT.2020 屏幕的红色子像素 → 发色度 (0.708, 0.292) 的红(更纯)
色域是硬件物理决定的,出厂就定了,软件改不了。如果视频色域 ≠ 屏幕色域,需要做矩阵转换。
色域转换
源 RGB → XYZ(用源色域矩阵)→ 目标 RGB(用目标色域逆矩阵)
BT.709 RGB → XYZ:
| 0.4124 0.3576 0.1805 | | R | | X |
| 0.2126 0.7152 0.0722 | × | G | = | Y |
| 0.0193 0.1192 0.9505 | | B | | Z |
XYZ 是设备无关的绝对色彩空间,作为中间桥梁。
四、传输函数(EOTF)详解

为什么需要非线性编码
人眼对亮度的感知是对数的(韦伯-费希纳定律):
- 1→2 nits:感觉"明显变亮"
- 1000→1001 nits:完全感觉不到
如果线性编码,暗部精度不够(色带),亮部浪费编码空间。传输函数把更多编码值分配给人眼敏感的暗部。
三种主要曲线
Gamma (SDR)
编码 (OETF): V = L^(1/2.2)
解码 (EOTF): L = V^2.2
V=0.5 → L = 0.5^2.2 = 0.217 → 21.7 nits (参考白100nits时)
量程: 0~100 nits
PQ (HDR10, SMPTE ST 2084)
解码 (EOTF): L = 10000 × [max(V^(1/m2) - c1, 0) / (c2 - c3·V^(1/m2))]^(1/m1)
V=0.508 → L ≈ 100 nits
V=0.752 → L ≈ 1000 nits
V=1.0 → L = 10000 nits
量程: 0~10000 nits (绝对值)
- 由 Dolby 提出,基于人眼视觉实验(Barten 模型)
- 每个编码步长 = 人眼刚好能分辨的最小亮度差异
- 10bit PQ 理论上全亮度范围无可见色带
HLG (广播)
下半段: Gamma(兼容 SDR)
上半段: 对数(扩展高光)
量程: 相对值(自适应屏幕峰值亮度)
- BBC/NHK 提出,为广播设计
- SDR 电视直接显示也不会太离谱
PQ vs HLG 对比
| PQ | HLG | |
|---|---|---|
| 亮度定义 | 绝对值 (nits) | 相对值 |
| SDR 兼容 | 差(发灰) | 好 |
| 适用场景 | 流媒体、电影 | 广播、直播 |
| 提出者 | Dolby | BBC + NHK |
SDR vs HDR 的根本区别
不是 bit 数的区别,是"量程"的区别:
SDR: 8bit (256格) 覆盖 0~100 nits → 每格 ≈ 0.4 nits
HDR: 10bit (1024格) 覆盖 0~10000 nits → 非线性分配,暗部密集
如果把 HDR 数据当 SDR 显示(用 Gamma 解读 PQ)→ 画面发灰(最常见的 HDR 问题)。
五、HDR 元数据
为什么需要元数据
RGB + 传输函数已经能算出每个像素的亮度。但元数据解决的是 Tone Mapping 的决策问题------告诉屏幕"这个内容整体有多亮",让压缩更精准。
静态元数据 (HDR10)
整部片子一组数据,写在文件头:
MaxCLL = 1500 nits (最亮像素)
MaxFALL = 800 nits (最亮帧平均亮度)
Mastering Display:
- 主显示器色度坐标 (RGB + 白点)
- 最大/最小亮度
动态元数据 (Dolby Vision / HDR10+)
每帧/每场景独立的亮度信息:
场景A (夜晚): 最亮 200 nits → 屏幕不压缩,细节全保留
场景B (爆炸): 最亮 4000 nits → 屏幕按需压缩
HDR 标准对比
| 标准 | 元数据 | 位深 | 授权 | 效果 |
|---|---|---|---|---|
| HDR10 | 静态 (MaxCLL/MaxFALL) | 10bit | 免费开放 | 基础 |
| HDR10+ | 动态 (逐场景) | 10bit | 免费 (三星) | 较好 |
| Dolby Vision | 动态 (逐帧 RPU) | 12bit | 需授权 | 最好 |
| HLG | 无需额外元数据 | 10bit | 免费 | 广播用 |
六、Tone Mapping
是什么
把屏幕显示不了的亮度范围,压缩到屏幕能显示的范围内。
视频内容: 0~4000 nits
屏幕能力: 0~1000 nits
Tone Mapping: 非线性压缩 4000→1000
为什么不能等比缩小
等比 ÷4: 100nits人脸→25nits(太暗看不清)
非线性: 暗部基本不动,高光大幅压缩
谁来做
| 场景 | 执行者 |
|---|---|
| 屏幕支持 HDR | 不需要 Tone Mapping,直通 |
| 屏幕不支持,系统处理 | SurfaceFlinger / HWC |
| 播放器自己做 | OpenGL shader 中实现 |
完整流程
HDR RGB (PQ, BT.2020)
↓ Step1: PQ EOTF → 线性光 (nits)
↓ Step2: 色域转换 BT.2020 → BT.709
↓ Step3: 压缩亮度 (用 MaxCLL 作为参考)
↓ Step4: Gamma OETF 编码
SDR RGB (Gamma, BT.709) → 屏幕正常显示
七、YUV 与色彩矩阵
YUV→RGB 转换矩阵
不同标准定义不同系数:
BT.601: R = Y + 1.402×V, G = Y - 0.344×U - 0.714×V, B = Y + 1.772×U
BT.709: R = Y + 1.5748×V, G = Y - 0.1873×U - 0.4681×V, B = Y + 1.8556×U
BT.2020: R = Y + 1.4746×V, G = Y - 0.1646×U - 0.5714×V, B = Y + 1.8814×U
用错矩阵 → 颜色严重偏色。
Color Range
| 类型 | Y 范围 | UV 范围 | 场景 |
|---|---|---|---|
| Limited (TV) | 16~235 | 16~240 | 视频(默认) |
| Full (PC) | 0~255 | 0~255 | JPEG、PC 截图 |
搞错 → 黑不够黑、白不够白。
八、完整播放链路(从文件到屏幕)
视频文件
↓ 解封装 (Demux)
│ 提取: color_primaries / color_trc / colorspace / color_range / HDR metadata / DV info
↓
解码
│ 输出: 10bit YUV + 色彩参数
↓
色彩处理
│ 屏幕支持HDR → 直通 (设置 DataSpace = BT2020_PQ)
│ 屏幕不支持 → Tone Mapping + 色域转换
↓
渲染 (Surface / OpenGL)
│ 设置 DataSpace 告诉系统这是 HDR 还是 SDR
↓
SurfaceFlinger
│ 识别 HDR layer → 通知 HWC 切换 HDR 模式
↓
屏幕
│ PQ 模式: 用 PQ 曲线驱动子像素,峰值 1000~2000+ nits
│ SDR 模式: 用 Gamma 曲线驱动,峰值 ~300 nits
九、Android 硬解 HDR 实现
关键参数设置
java
MediaFormat format = MediaFormat.createVideoFormat("video/hevc", w, h);
format.setInteger(KEY_COLOR_TRANSFER, COLOR_TRANSFER_ST2084); // PQ
format.setInteger(KEY_COLOR_STANDARD, COLOR_STANDARD_BT2020); // 色域
format.setInteger(KEY_COLOR_RANGE, COLOR_RANGE_LIMITED); // range
format.setByteBuffer(KEY_HDR_STATIC_INFO, hdrStaticInfoBuffer); // MaxCLL等
硬解 + Surface 直通
MediaCodec.configure(format, surface, null, 0)
↓ 解码
releaseOutputBuffer(index, true) // render=true
↓ MediaCodec 自动设置 buffer DataSpace = BT2020_PQ
SurfaceFlinger 识别 HDR layer → 直通到屏幕
必须用 SurfaceView(不是 TextureView),否则 HDR 信息可能丢失。
Dolby Vision 硬解
mime = "video/dolby-vision"
查找 DV 解码器 (名字含 "dolby")
配置 dv_profile / dv_level
↓ DV 解码器内部处理 RPU 动态元数据
↓ 输出带 DV DataSpace 的 buffer
屏幕启用 DV 显示管线
DV 的动态元数据在码流 RPU 中,不需要外部传递。
十、Android OpenGL HDR 渲染
必须做的事
c
// 1. EGL Surface 声明 HDR
EGLint surfaceAttribs[] = {
EGL_GL_COLORSPACE_KHR, EGL_GL_COLORSPACE_BT2020_PQ_EXT,
EGL_NONE
};
// 2. 设置 DataSpace
ANativeWindow_setBuffersDataSpace(window, ADATASPACE_BT2020_PQ);
// 3. Framebuffer 精度 10bit 或 FP16
EGL_RED_SIZE=10, EGL_GREEN_SIZE=10, EGL_BLUE_SIZE=10
Shader 中的处理
glsl
// 如果输出 HDR (屏幕支持):
// YUV→RGB (BT.2020矩阵) → 保持 PQ 编码 → 直接输出
// 如果输出 SDR (需要 Tone Mapping):
// YUV→RGB → PQ EOTF → 线性光 → Tone Mapping → 色域转换 → Gamma 编码
十一、参数丢失后果速查
| 丢失参数 | 后果 | 严重度 |
|---|---|---|
| color_trc (传输函数) | 画面发灰 (PQ 当 Gamma 解读) | ⭐⭐⭐⭐⭐ |
| color_primaries (色域) | 颜色偏色/过饱和 | ⭐⭐⭐⭐ |
| colorspace (YUV矩阵) | 颜色严重偏色 | ⭐⭐⭐⭐ |
| color_range | 对比度异常 | ⭐⭐⭐ |
| bit_depth 截断 | 暗部色带 | ⭐⭐⭐ |
| MaxCLL/MaxFALL | Tone Mapping 效果差 | ⭐⭐ |
| DataSpace 未设置 | 画面发灰 | ⭐⭐⭐⭐⭐ |
最常见的 HDR 问题就是"发灰"------传输函数信息在某个环节丢失。
十二、关键名词速查
| 缩写 | 全称 | 作用 |
|---|---|---|
| EOTF | Electro-Optical Transfer Function | 电信号→光(显示器侧解码) |
| OETF | Opto-Electronic Transfer Function | 光→电信号(摄像机侧编码) |
| PQ | Perceptual Quantizer (ST 2084) | HDR 绝对亮度 EOTF |
| HLG | Hybrid Log-Gamma | 广播兼容 HDR EOTF |
| MaxCLL | Maximum Content Light Level | 整片最亮像素 |
| MaxFALL | Maximum Frame-Average Light Level | 最亮帧平均亮度 |
| RPU | Reference Processing Unit | DV 逐帧动态元数据 |
| DataSpace | Android 色彩空间标记 | 告诉系统 buffer 的色彩属性 |
| HWC | Hardware Composer | Android 硬件合成 |
| SF | SurfaceFlinger | Android 显示合成进程 |
| CIE XYZ | 设备无关绝对色彩空间 | 色域转换的中间桥梁 |
| EDID | Extended Display Identification Data | 显示器能力自报 |
十三、一句话串联
拍摄时:真实亮度通过 OETF 压缩为 RGB 数值,标注色域+传输函数+元数据存入文件。
播放时:解码出 RGB 数值 → 用传输函数还原亮度 → 用色域确定颜色 → 如果屏幕能力不够就 Tone Mapping → 驱动子像素发光。
整条链路的核心就是:确保每个环节都知道"这些数字代表什么"。丢了任何一个标注,显示就会出错。