H.264 长期参考帧
H.264 长期参考帧
1. 参考帧管理概述
1.1 为什么需要参考帧管理
在视频编码中,P 帧和 B 帧需要参考其他已解码帧进行预测。解码器必须维护一个参考帧列表,存储可用于预测的已解码帧。
参考帧的作用:
┌─────────────────────────────────────────────────────┐
│ 当前帧 P 需要预测时: │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 参考帧 0 │ │ 参考帧 1 │ │ 参考帧 2 │ │
│ │ (I 帧) │ │ (P 帧) │ │ (P 帧) │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └───────────────┼───────────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 运动估计搜索 │ │
│ │ 找到最佳匹配块 │ │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 当前帧 P 的 │ │
│ │ 预测块 │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────┘
1.2 短期参考帧 vs 长期参考帧
H.264 定义了两类参考帧:
| 类型 | 英文 | 说明 | 生命周期 |
|---|---|---|---|
| 短期参考帧 | Short-Term Reference | 最近解码的帧,按解码顺序排列 | 有限,会被新帧替换 |
| 长期参考帧 | Long-Term Reference (LTR) | 标记为长期使用的帧 | 可跨多个 GOP 持续使用 |
时间线上的参考帧分布:
帧类型: I P P P I P P P
│ │ │ │ │ │ │ │
时间: 0s 1s 2s 3s 4s 5s 6s 7s
│ │ │ │ │ │ │ │
短期参考: [0] [1] [2] [3] [4] [5] [6] [7]
│ │ │ │
长期参考: [0]─────────────────────[4]─────────────────
└────── 帧间编码时可参考 ──────┘
帧 7 可以参考:
- 短期参考: 帧 4, 5, 6
- 长期参考: 帧 0, 4 (如果被标记为 LTR)
1.3 长期参考帧的价值
1. 场景切换时的压缩效率
场景切换场景:
帧序列: [室内场景] → [室外场景] → [室内场景]
I P P I P P P
0 1 2 3 4 5 6
不使用 LTR:
帧 6 只能参考 3,4,5(都是室外场景)
预测效率低,需要大量残差
使用 LTR:
帧 0 被标记为长期参考帧
帧 6 可以参考帧 0(室内场景)
预测效率高,残差少
2. 周期性场景优化
周期性内容示例:
- 旋转的物体
- 重复的动画
- 视频会议中的手势
如果内容在 T 秒后重复出现:
使用 LTR 保存 T 秒前的帧作为参考
可大幅提升压缩效率
3. 错误恢复
网络传输中 P 帧丢失:
正常情况:
I → P → P → [P丢失] → P → P
↓
后续帧无法解码(参考链断裂)
使用 LTR:
I(LTR) → P → P → [P丢失] → P → P
↓ ↑
└──── 可以跳过丢失帧,参考 I(LTR)
长期参考帧提供了"安全岛",即使中间帧丢失也能恢复
2. 参考帧列表结构
2.1 两个参考帧列表
H.264 维护两个参考帧列表:
参考帧列表结构:
┌─────────────────────────────────────────────────────┐
│ 解码图像缓冲区 (DPB) │
│ ┌───────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │ ST │ │ ST │ │ ST │ │ LT │ │ LT │ │ │
│ │ │idx=0│ │idx=1│ │idx=2│ │idx=0│ │idx=1│ │ │
│ │ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ │ │
│ │ │ │ │ │ │ │ │
│ │ ┌───┴───────┴───────┘ └───────┴───┐ │ │
│ │ │ │ │ │
│ │ │ List 0 (前向参考) │ │ │
│ │ │ [ST0, ST1, ST2, LT0, LT1] │ │ │
│ │ │ │ │ │
│ │ │ List 1 (后向参考,用于 B 帧) │ │ │
│ │ │ [ST2, ST1, ST0, LT0, LT1] │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
ST = Short-Term (短期参考帧)
LT = Long-Term (长期参考帧)
2.2 参考帧索引
编码时通过 ref_idx 指定使用哪个参考帧:
宏块参考帧选择:
mb_type = P_L0_16x16 时:
ref_idx_l0 = 0 → 使用 List 0 的第一个参考帧
ref_idx_l0 = 1 → 使用 List 0 的第二个参考帧
...
mb_type = B_Bi_16x16 时:
ref_idx_l0 = 0 → 前向参考 List 0[0]
ref_idx_l1 = 0 → 后向参考 List 1[0]
3. 长期参考帧句法
3.1 参考帧标记(Reference Picture Marking)
参考帧标记决定了已解码帧如何进入或退出参考帧列表。有两种模式:
参考帧标记模式:
┌─────────────────────────────────────────┐
│ adaptive_ref_pic_marking_mode_flag │
├─────────────────────────────────────────┤
│ = 0: 滑动窗口模式 (Sliding Window) │
│ - 自动管理,先进先出 │
│ - 简单但不够灵活 │
│ │
│ = 1: 自适应模式 (Adaptive MMCO) │
│ - 通过 MMCO 命令显式控制 │
│ - 支持长期参考帧操作 │
└─────────────────────────────────────────┘
3.2 MMCO(Memory Management Control Operation)
自适应模式下,通过 MMCO 命令管理参考帧:
dec_ref_pic_marking() {
if (nal_unit_type == 5) { // IDR 帧
no_output_of_prior_pics_flag // u(1)
long_term_reference_flag // u(1)
if (long_term_reference_flag) {
// IDR 帧作为长期参考帧
// LongTermFrameIdx = 0
// MaxLongTermFrameIdx = 0
} else {
// IDR 帧作为短期参考帧
// 清空所有长期参考帧
}
} else { // 非 IDR 帧
adaptive_ref_pic_marking_mode_flag // u(1)
if (adaptive_ref_pic_marking_mode_flag == 1) {
do {
memory_management_control_operation_id // ue(v)
switch (memory_management_control_operation_id) {
case 1: // 短期参考标记为长期参考
difference_of_pic_nums_minus1 // ue(v)
long_term_frame_idx // ue(v)
break
case 2: // 短期参考标记为长期参考(使用 LongTermPicNum)
long_term_pic_num // ue(v)
break
case 3: // 清空所有短期参考
break
case 4: // 清空指定长期参考
max_long_term_frame_idx_plus1 // ue(v)
break
case 5: // 清空所有参考帧
break
case 6: // 当前帧作为长期参考
long_term_frame_idx // ue(v)
break
}
} while (memory_management_control_operation_id != 0)
}
}
}
3.3 MMCO 命令详解
| MMCO ID | 操作 | 句法元素 | 说明 |
|---|---|---|---|
| 0 | 结束 | 无 | 表示 MMCO 命令列表结束 |
| 1 | 短期→长期 | difference_of_pic_nums_minus1, long_term_frame_idx | 将指定短期参考帧标记为长期参考 |
| 2 | 短期→长期(旧方式) | long_term_pic_num | 使用 LongTermPicNum 标记 |
| 3 | 清空短期 | 无 | 移除所有短期参考帧 |
| 4 | 限制长期 | max_long_term_frame_idx_plus1 | 清除索引 ≥ 该值的长期参考 |
| 5 | 清空所有 | 无 | 清空所有参考帧(相当于 IDR 效果) |
| 6 | 当前→长期 | long_term_frame_idx | 当前帧直接作为长期参考 |
MMCO 命令执行示例:
初始状态:
短期参考: [帧0, 帧1, 帧2]
长期参考: []
执行 MMCO=1 (将帧0标记为长期参考):
短期参考: [帧1, 帧2]
长期参考: [帧0(LT_idx=0)]
执行 MMCO=6 (当前帧3作为长期参考):
短期参考: [帧1, 帧2]
长期参考: [帧0(LT_idx=0), 帧3(LT_idx=1)]
执行 MMCO=4 (max_long_term_frame_idx_plus1=1):
长期参考索引 ≥ 1 的被清除
长期参考: [帧0(LT_idx=0)]
3.4 IDR 帧的长期参考标记
IDR 帧有特殊的参考帧标记方式:
IDR 帧标记:
long_term_reference_flag = 0:
- IDR 帧作为短期参考帧
- 清空所有长期参考帧
- 新序列开始的标准方式
long_term_reference_flag = 1:
- IDR 帧作为长期参考帧
- LongTermFrameIdx = 0
- 用于需要 IDR 作为长期锚点的情况
4. 长期参考帧索引
4.1 LongTermFrameIdx
每个长期参考帧有一个 LongTermFrameIdx:
长期参考帧索引分配:
┌─────────────────────────────────────────────────────┐
│ 长期参考帧列表 │
│ │
│ LongTermFrameIdx: 0 1 2 3 │
│ ┌─────┬─────┬─────┬─────┐ │
│ │ 帧0 │ 帧5 │ 帧10│ - │ │
│ │(LTR)│(LTR)│(LTR)│空位 │ │
│ └─────┴─────┴─────┴─────┘ │
│ │
│ MaxLongTermFrameIdx = 2 (最大有效索引) │
└─────────────────────────────────────────────────────┘
特点:
- 索引可以不连续
- 新帧可以覆盖旧的长期参考帧
- MaxLongTermFrameIdx 限制最大索引值
4.2 索引管理规则
长期参考帧索引管理:
1. 添加长期参考帧:
- 分配一个 LongTermFrameIdx
- 如果该索引已被占用,旧帧被替换
2. 删除长期参考帧:
- MMCO=4: 删除索引 ≥ max_long_term_frame_idx_plus1 的帧
- MMCO=5: 删除所有长期参考帧
3. 索引范围限制:
- 0 ≤ LongTermFrameIdx ≤ MaxLongTermFrameIdx
- MaxLongTermFrameIdx ≤ max_num_ref_frames - 1
5. 参考帧列表构建
5.1 List 0 构建规则
List 0 构建顺序:
步骤 1: 添加短期参考帧(按 PicNum 降序)
- 最近的帧排在前面
- 便于运动估计找到最佳匹配
步骤 2: 添加长期参考帧(按 LongTermPicNum 升序)
- 长期参考帧排在短期参考帧之后
示例:
短期参考: 帧10, 帧8, 帧6
长期参考: 帧0(LT), 帧4(LT)
List 0 = [帧10, 帧8, 帧6, 帧0(LT), 帧4(LT)]
└───短期────┘ └───长期────┘
5.2 List 1 构建规则(B 帧)
List 1 构建顺序:
步骤 1: 添加短期参考帧(按 PicNum 升序)
- 较早的帧排在前面
- 用于后向预测
步骤 2: 添加长期参考帧(按 LongTermPicNum 升序)
示例:
短期参考: 帧10, 帧8, 帧6
长期参考: 帧0(LT), 帧4(LT)
当前帧: 帧12
List 1 = [帧6, 帧8, 帧10, 帧0(LT), 帧4(LT)]
└─短期(升序)─┘ └───长期────┘
5.3 参考帧列表重排序
编码器可以通过 ref_pic_list_modification 调整参考帧顺序:
ref_pic_list_modification() {
if (slice_type % 5 != 2 && slice_type % 5 != 4) { // P 或 B 帧
ref_pic_list_modification_flag_l0 // u(1)
if (ref_pic_list_modification_flag_l0) {
do {
modification_of_pic_nums_idc // ue(v)
switch (modification_of_pic_nums_idc) {
case 0: // 短期参考,指定 abs_diff_pic_num_minus1
case 1: // 短期参考,指定 abs_diff_pic_num_minus1
case 2: // 长期参考,指定 long_term_pic_num
}
} while (modification_of_pic_nums_idc != 3)
}
}
// List 1 类似处理...
}
重排序示例:
原始 List 0: [ST0, ST1, ST2, LT0, LT1]
重排序命令:
modification_of_pic_nums_idc = 2, long_term_pic_num = 0
重排序后: [LT0, ST0, ST1, ST2, LT1]
目的: 将长期参考帧 LT0 移到最前面,优先使用
6. 长期参考帧的典型应用场景
6.1 场景切换优化
应用场景: 视频中存在场景切换
编码策略:
1. 检测到场景切换时,编码一个 I 帧
2. 将该 I 帧标记为长期参考帧
3. 后续 P 帧可以参考这个长期参考帧
时间线:
场景A 场景B 场景A
帧: I P P P I(LTR) P P P P P P
0 1 2 3 4 5 6 7 8 9 10
帧 8-10 编码时:
- 如果场景切换回 A
- 帧 8 可以参考帧 4(I-LTR),而不是帧 7
- 预测效率大幅提升
6.2 视频会议中的 LTR
视频会议场景特点:
- 背景相对稳定
- 人物可能有重复动作
- 需要低延迟
LTR 策略:
┌─────────────────────────────────────────────────────┐
│ 编码器决策逻辑: │
│ │
│ if (帧内容与 LTR 相似度高) { │
│ 优先使用 LTR 作为参考 │
│ 码率降低,质量提升 │
│ } else if (场景变化) { │
│ 更新 LTR 为当前 I 帧 │
│ } │
└─────────────────────────────────────────────────────┘
WebRTC 中的典型配置:
- 保留 1-2 个长期参考帧
- 周期性更新(如每 4-8 秒)
- 基于内容相似度决策
6.3 错误恢复与抗丢包
网络传输场景:
问题: 连续 P 帧参考链导致丢包后无法恢复
传统方案:
I → P → P → P → P → P → P → I → P → P
↑ 丢包
└─→ 后续帧全部花屏
LTR 方案:
I(LTR) → P → P → P → P → P → P → P → P
↓ ↑
└──── 丢包后可跳回 I(LTR) ───┘
具体实现:
1. 接收端检测到丢包
2. 发送 FIR (Full Intra Request) 或 PLI
3. 发送端可以:
- 编码新的 I 帧
- 或者让后续 P 帧参考 LTR(跳过丢失区域)
6.4 实时通信中的 LTR 使用
WebRTC 编码器中的 LTR 策略:
┌─────────────────────────────────────────────────────┐
│ 编码器 │
│ ┌───────────────────────────────────────────────┐ │
│ │ LTR 管理模块 │ │
│ │ │ │
│ │ 1. 周期性标记关键帧为 LTR │ │
│ │ - 间隔: 根据场景复杂度动态调整 │ │
│ │ - 典型值: 4-10 秒 │ │
│ │ │ │
│ │ 2. 场景切换时更新 LTR │ │
│ │ - 检测到场景切换 → 编码 I 帧 │ │
│ │ - 将 I 帧标记为新的 LTR │ │
│ │ │ │
│ │ 3. 响应接收端反馈 │ │
│ │ - 收到 PLI/FIR → 编码 I 帧 │ │
│ │ - 将新 I 帧标记为 LTR │ │
│ │ │ │
│ │ 4. 参考帧选择 │ │
│ │ - 优先从短期参考中选择 │ │
│ │ - 短期参考效果差时尝试 LTR │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
7. 句法元素总结
7.1 SPS 中的相关句法
seq_parameter_set_rbsp() {
...
max_num_ref_frames // ue(v) 最大参考帧数
...
}
- 决定了短期 + 长期参考帧的总数上限
- LTR 数量受此限制
7.2 Slice Header 中的相关句法
slice_header() {
...
frame_num // u(v) 短期参考帧的帧号
...
if (nal_unit_type == 5) { // IDR 帧
idr_pic_id // ue(v) IDR 图像标识
...
}
// 参考帧列表重排序
ref_pic_list_modification()
// 参考帧标记(非 IDR 帧且 nal_ref_idc != 0)
if (nal_ref_idc != 0)
dec_ref_pic_marking()
...
}
7.3 关键句法元素一览
| 句法元素 | 位置 | 编码方式 | 说明 |
|---|---|---|---|
| max_num_ref_frames | SPS | ue(v) | 最大参考帧数量 |
| frame_num | Slice Header | u(v) | 短期参考帧帧号 |
| idr_pic_id | Slice Header | ue(v) | IDR 图像标识 |
| adaptive_ref_pic_marking_mode_flag | Slice Header | u(1) | 标记模式选择 |
| memory_management_control_operation_id | Slice Header | ue(v) | MMCO 操作码 |
| difference_of_pic_nums_minus1 | Slice Header | ue(v) | 短期参考帧号差值 |
| long_term_frame_idx | Slice Header | ue(v) | 长期参考帧索引 |
| long_term_pic_num | Slice Header | ue(v) | 长期参考帧图像号 |
| max_long_term_frame_idx_plus1 | Slice Header | ue(v) | 最大长期索引+1 |
| long_term_reference_flag | Slice Header | u(1) | IDR 作为长期参考标志 |
| ref_idx_l0 | 宏块层 | te(v) | List 0 参考帧索引 |
| ref_idx_l1 | 宏块层 | te(v) | List 1 参考帧索引 |
8. 实现注意事项
8.1 编码器实现要点
编码器 LTR 管理:
1. LTR 选择策略
- 基于场景复杂度
- 基于运动分析
- 基于率失真优化
2. LTR 更新时机
- 周期性更新(固定间隔)
- 场景切换时更新
- 收到接收端请求时更新
3. 参考帧选择
- 对每个宏块评估最佳参考帧
- 考虑 LTR 作为备选
- 率失真优化决策
8.2 解码器实现要点
解码器 LTR 管理:
1. DPB (Decoded Picture Buffer) 管理
- 维护短期参考帧列表
- 维护长期参考帧列表
- 正确执行 MMCO 命令
2. 参考帧有效性检查
- 检查 ref_idx 是否有效
- 检查参考帧是否存在
- 处理参考帧缺失情况
3. 错误恢复
- 检测到错误时请求新的 LTR
- 发送 PLI/FIR 反馈
8.3 兼容性考虑
Profile 支持情况:
┌─────────────────┬──────────────────────────────────┐
│ Profile │ 长期参考帧支持 │
├─────────────────┼──────────────────────────────────┤
│ Baseline │ 支持(无 B 帧,List 1 不使用) │
│ Constrained │ 支持 │
│ Main │ 完整支持 │
│ High │ 完整支持 │
└─────────────────┴──────────────────────────────────┘
WebRTC 中的考虑:
- Constrained Baseline 不使用 B 帧
- 只需要管理 List 0
- LTR 仍然有效且重要
9. 总结
9.1 长期参考帧的核心价值
| 价值 | 说明 |
|---|---|
| 压缩效率 | 场景切换、周期性内容时提升预测效率 |
| 错误恢复 | 提供安全参考点,支持丢包恢复 |
| 灵活性 | MMCO 命令提供细粒度控制 |
| 兼容性 | 所有 Profile 都支持 |
9.2 关键句法流程
flowchart TB
A[解码帧] --> B{nal_ref_idc != 0?}
B -->|否| C[不作为参考帧]
B -->|是| D{IDR 帧?}
D -->|是| E{long_term_reference_flag?}
E -->|0| F[作为短期参考<br>清空长期参考]
E -->|1| G[作为长期参考<br>Idx=0]
D -->|否| H{adaptive_ref_pic_marking_mode?}
H -->|0| I[滑动窗口模式]
H -->|1| J[执行 MMCO 命令]
J --> K{MMCO 类型}
K -->|1| L[短期→长期]
K -->|3| M[清空短期]
K -->|4| N[限制长期索引]
K -->|5| O[清空所有]
K -->|6| P[当前→长期]
9.3 实践建议
- 编码器:合理设置 LTR 更新频率,平衡压缩效率和复杂度
- 解码器:正确实现 DPB 管理和 MMCO 命令处理
- 实时通信:利用 LTR 提升抗丢包能力,配合 PLI/FIR 机制
- 调试:使用工具检查参考帧列表状态,验证 MMCO 执行正确性
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 青羽川!
评论
