1. 帧间预测里最容易被忽略的两个模式Skip 和 Direct做视频编解码的朋友尤其是啃过 H.264/AVC 标准的大概率都有过这种体验帧内预测还好无非就是九种模式挨个算一遍 SATD 或者 SAD选最小的那个就完事了。但一到帧间预测尤其是 B 片和 P 片里的 Skip 模式和 Direct 模式很多人就开始犯迷糊了——运动矢量到底从哪来的参考帧索引又是怎么定的为什么有时候明明没传 MV解码器却能“凭空”恢复出一个运动矢量这篇文章就专门把 H.264/AVC 中Skip 模式和Direct 模式下运动矢量MV和参考帧索引ref_idx的推导过程掰开揉碎讲清楚。如果你正在做编解码器的开发、优化或者正在写自己的 H.264 解码器再或者你是个对视频压缩底层原理感兴趣的学生这篇内容应该能帮你省下不少翻标准文档的时间。我会尽量用大白话把推导逻辑讲透同时给出可以直接对照代码实现的步骤。先说结论Skip 和 Direct 这两个模式本质上都是“不传或少传运动信息”的编码工具。它们的核心思想是利用时间域上的相关性和空间域上的相关性让解码器自己把 MV 和 ref_idx 推出来。推得准码率就省下来了推不准残差就大反而得不偿失。所以理解推导规则是写好编码器决策逻辑的前提。2. 为什么需要 Skip 和 Direct从码率账本说起2.1 一个 MV 到底要花多少比特在 P 片中每个宏块如果走普通帧间预测需要传输运动矢量差MVD、参考帧索引ref_idx以及残差系数。MVD 本身经过熵编码后在低码率场景下可能占到整个宏块开销的 30% 到 50%。你想想一个 16x16 的宏块如果纹理很平坦、运动又很规则传一堆 MVD 和残差其实非常浪费。Skip 模式就是针对这种情况设计的整个宏块不传任何运动信息也不传残差系数。解码器根据邻居宏块的 MV 推导出一个预测 MV直接拿来做运动补偿重建像素就是预测像素。这在会议视频、监控画面这种背景大面积静止或匀速平移的场景里能省下大量比特。2.2 Direct 模式在 B 片里的特殊地位B 片比 P 片多了一个方向选择——前向、后向、双向。如果每个宏块都要显式传两个方向的 MV 和 ref_idx开销直接翻倍。Direct 模式允许 B 片宏块不传 MV 和 ref_idx由解码器根据已解码信息推导出前向和后向的运动信息。它分两种时间 DirectTemporal Direct和空间 DirectSpatial Direct。这两种模式的推导逻辑完全不同也是本文的重点。从编码器角度看Skip 和 Direct 是“零运动开销”或“极低运动开销”的候选模式。在做率失真优化RDO时它们通常和普通 Inter 16x16、Inter 16x8 等模式一起参与比较。如果 Skip/Direct 的率失真代价更低就选它。理解推导规则才能正确计算它们的率失真代价。3. Skip 模式的 MV 和 ref_idx 推导P 片与 B 片的差异3.1 P 片 Skip中值预测一把梭P 片中的 Skip 模式推导规则相对简单。对于当前宏块取左边宏块 A、上边宏块 B、右上宏块 C如果 C 不可用就用 D 替代的运动矢量。然后对这三个 MV 的每个分量分别取中值mvL0_x Median(mvA_x, mvB_x, mvC_x)mvL0_y Median(mvA_y, mvB_y, mvC_y)参考帧索引 ref_idx 呢P 片 Skip 模式下ref_idx 固定为 0。也就是说它只参考最近的那一帧。这个设计很好理解Skip 模式追求极致的低开销如果还要传 ref_idx那就不是 Skip 了。固定 ref_idx0 虽然不够灵活但在大多数低运动场景下够用。注意这里的中值预测和普通 Inter 模式的 MVP运动矢量预测用的是同一套邻居宏块。区别在于普通 Inter 模式会传 MVD 来修正而 Skip 模式直接拿预测值当最终 MV。3.2 B 片 Skip双向 MV 的推导逻辑B 片的 Skip 模式就复杂一些了。它需要推导出前向L0和后向L1两个 MV以及对应的 ref_idx。推导过程分两种情况情况一如果邻居宏块中存在后向 MV即使用了 L1 参考帧那么对 L0 的 MV取邻居中所有 L0 MV 的中值如果某个邻居没有 L0 MV则其 L0 MV 视为 0。对 L1 的 MV取邻居中所有 L1 MV 的中值如果某个邻居没有 L1 MV则其 L1 MV 视为 0。ref_idx 方面L0 的 ref_idx 取邻居中 L0 ref_idx 的最小值通常为 0L1 同理。情况二如果邻居宏块中不存在后向 MV那么 B 片 Skip 退化为类似 P 片 Skip 的行为只推导 L0 的 MVL1 的 MV 置为 0且 ref_idx 都取 0。此时实际上只做前向预测。这里有个容易踩的坑邻居宏块的可用性判断。如果左边宏块是帧内预测它的 MV 视为 0如果上边宏块不存在比如当前宏块在图像最上面一行也要做相应处理。标准里对“不可用”有明确定义实现时必须严格对照否则会出现解码器与编码器不一致的问题。3.3 实操中的注意事项在实际写代码时Skip 模式的 MV 推导需要维护一个“邻居 MV 缓存”。每解码完一个宏块就要把它的 MV 和 ref_idx 写入缓存供后续宏块使用。这个缓存的更新时机很关键必须在当前宏块重建完成之后、下一个宏块开始之前更新。如果顺序搞错会导致 MV 预测出现偏差进而影响整个帧的重建质量。另外中值预测的计算要注意整数溢出问题。MV 分量是有符号的范围通常在 -8192 到 8191 之间取决于 profile 和 level。取中值时用 int 类型就够了但如果你用 uint16 存储一定要先转成有符号数再比较。4. Direct 模式深度拆解时间 Direct 与空间 Direct4.1 时间 Direct 模式的核心思想时间 Direct 模式Temporal Direct是 B 片中最常用的 Direct 模式之一。它的核心思想是利用后向参考帧中对应位置宏块的运动信息来推导当前宏块的前向和后向 MV。具体来说假设当前宏块在 B 帧中的位置是 (x, y)后向参考帧通常是 P 帧或 I 帧中同一位置的宏块为 colocated 宏块。如果这个 colocated 宏块使用的是 L0 预测即它有一个指向更早帧的 MV那么这个 MV 就记为 mvCol它指向的参考帧记为 refCol。然后根据当前帧与前后参考帧的时间距离做线性缩放设当前帧与 L0 参考帧的时间距离为 td即当前帧 POC 减去 L0 参考帧 POC当前帧与 L1 参考帧的时间距离为 tb即 L1 参考帧 POC 减去当前帧 POC。前向 MVmvL0 mvCol * tb / td后向 MVmvL1 mvCol * (td - tb) / td注意这里的符号方向。标准里对 td 和 tb 的定义有明确的正负号规则实现时如果搞反了MV 方向就会完全错误。4.2 时间 Direct 的 ref_idx 推导ref_idx 的推导相对直接L0 的 ref_idx 取 colocated 宏块的 ref_idx即 refColL1 的 ref_idx 固定为 0。这是因为时间 Direct 假设后向参考帧中对应位置的运动信息可以“借用”过来而 L1 参考帧通常就是最近的那一帧。但这里有个特殊情况如果 colocated 宏块的 MV 超出范围或者 colocated 宏块是帧内预测那么时间 Direct 模式会退化为“前向 MV 为 0后向 MV 为 0”的情况相当于不做运动补偿直接用当前帧的预测像素。这种退化情况在实现时必须处理否则会读到无效的 MV。4.3 空间 Direct 模式的推导逻辑空间 Direct 模式Spatial Direct不依赖后向参考帧的 colocated 宏块而是完全利用当前帧内邻居宏块的运动信息来推导。它的推导过程分三步第一步检查邻居宏块 A、B、C 的 ref_idx。如果 A、B、C 中至少有一个使用了 L0 且 ref_idx 为 0且至少有一个使用了 L1 且 ref_idx 为 0则进入双向推导否则进入单向推导。第二步双向推导时L0 的 MV 取邻居中 L0 MV 的中值L1 的 MV 取邻居中 L1 MV 的中值。ref_idx 都取 0。第三步单向推导时如果邻居中只有 L0 信息则只推导 L0 MVL1 MV 置 0反之亦然。空间 Direct 的优势在于它不依赖后向参考帧因此在随机访问场景下更鲁棒。但它的预测精度通常不如时间 Direct因为邻居宏块的运动信息不一定能很好地代表当前宏块。4.4 两种 Direct 模式的对比与选择特性时间 Direct空间 Direct依赖信息后向参考帧 colocated 宏块当前帧邻居宏块MV 精度较高利用时间相关性较低利用空间相关性鲁棒性依赖后向帧可用性不依赖后向帧适用场景常规 B 片编码随机访问、低延迟场景计算复杂度需要访问 colocated 宏块只需邻居信息编码器在选择时通常会根据序列特性做自适应。比如在低延迟配置下后向参考帧可能不可用此时空间 Direct 更合适而在常规广播场景下时间 Direct 往往能带来更好的压缩效率。5. 从标准到代码推导过程的实现要点5.1 数据结构设计实现 Skip 和 Direct 推导首先需要设计好数据结构。建议为每个宏块维护以下信息mvL0[16]、mvL1[16]每个 4x4 子块的运动矢量H.264 支持 4x4 到 16x16 的分区refIdxL0[16]、refIdxL1[16]每个子块的参考帧索引predMode预测模式帧内、帧间、Skip、Direct邻居可用性标志对于 colocated 宏块还需要额外存储其 mvCol 和 refCol。这些信息在解码后向参考帧时就要保存下来供后续 B 帧使用。5.2 时间距离的计算时间距离 td 和 tb 的计算必须使用 POCPicture Order Count。POC 是 H.264 中用来标识帧显示顺序的变量它和编码顺序无关。计算时要注意td Clip3(-128, 127, POC_curr - POC_L0)tb Clip3(-128, 127, POC_curr - POC_L1)如果 td 为 0则直接令 mvL0 mvColmvL1 0避免除零错误。这个边界条件在实际码流中虽然少见但必须处理。5.3 MV 缩放的整数运算标准里给出的缩放公式是mvL0 (mvCol * tb td/2) / td mvL1 (mvCol * (td - tb) td/2) / td注意这里的除法是整数除法且要处理负数舍入问题。C 语言中整数除法是向零截断而标准要求的是向负无穷方向舍入。所以实现时通常需要自己写一个带舍入的除法函数或者用查表法加速。实操心得在 x86 平台上可以用(a b/2) / b来近似但当 a 为负数时结果可能差 1。建议直接用标准里的公式配合条件判断修正。5.4 邻居宏块的可用性判断邻居宏块不可用的情况包括超出图像边界、属于不同 slice、是帧内预测宏块。对于帧内宏块其 MV 视为 0ref_idx 视为 -1表示无效。在取中值时如果所有邻居都无效则 MV 直接置 0。这个判断逻辑在实现时容易写错尤其是 slice 边界的情况。建议单独写一个函数is_mb_available(x, y)把所有边界条件集中处理。6. 常见问题与排查技巧实录6.1 解码器与编码器 MV 不一致这是最常见的问题。表现是解码出来的画面在 Skip/Direct 宏块处出现明显块效应或错位。排查思路检查邻居 MV 缓存的更新时机。必须在当前宏块重建完成后立即更新不能提前也不能延后。检查中值预测的输入顺序。标准里对 A、B、C、D 的选取有明确规则顺序错了结果就错了。检查时间 Direct 的 td/tb 计算。POC 的获取是否正确有没有用编码顺序代替显示顺序6.2 时间 Direct 模式下 colocated 宏块不可用如果后向参考帧中对应位置的宏块是帧内预测或者该帧本身是 I 帧那么 colocated 宏块不可用。此时标准规定mvCol 0refCol -1最终推导出的 mvL0 和 mvL1 都为 0。实现时要显式处理这种情况不能直接读内存。6.3 MV 缩放溢出当 td 很大而 tb 也很大时mvCol * tb 可能溢出 16 位甚至 32 位。建议用 64 位整数做中间计算最后再截断到有效范围。标准里对 MV 的范围有约束超出范围的 MV 要 clip 到 [-8192, 8191]。6.4 常见问题速查表问题现象可能原因排查方法Skip 宏块画面错位邻居 MV 缓存未更新打印每个宏块的 MV对比参考解码器Direct 宏块出现块效应td/tb 计算错误检查 POC 获取逻辑解码器崩溃colocated 宏块指针为空增加空指针判断画面整体偏移MV 缩放舍入错误用标准公式重新实现除法随机访问时花屏空间 Direct 推导错误检查邻居 ref_idx 判断逻辑6.5 独家避坑技巧我在实现 H.264 解码器时踩过最大的坑是时间 Direct 模式下colocated 宏块的 MV 需要从后向参考帧的运动补偿结果中获取而不是从码流中直接读取。因为后向参考帧可能本身也是 B 帧或 P 帧它的 MV 是经过解码重建后的最终 MV。如果直接从码流读可能会读到未修正的 MVD导致推导错误。另一个坑是B 片 Skip 模式在邻居没有 L1 信息时L1 的 MV 不是简单置 0而是要根据 L0 的 MV 做反向推导。标准里对此有详细规定但很多开源实现为了简化直接置 0导致码流兼容性问题。7. 性能优化与扩展思考7.1 查表法加速 MV 缩放MV 缩放涉及除法和乘法在软件解码器中是热点操作。可以用查表法预计算 td 和 tb 的所有可能组合td 范围 -128 到 127tb 范围 -128 到 127把缩放因子存成定点数表。这样每次缩放只需要一次乘法和一次移位速度能提升 3 到 5 倍。7.2 SIMD 并行化Skip 和 Direct 的推导是按宏块进行的但宏块内的 4x4 子块可以并行处理。用 SSE 或 AVX 指令可以同时计算多个子块的 MV 中值和缩放。不过要注意邻居依赖关系可能导致数据竞争需要仔细设计数据布局。7.3 与 HEVC 的对比HEVC 中的 Merge 模式和 AMVP 模式本质上是对 H.264 Skip/Direct 的改进和扩展。Merge 模式相当于 Skip 的增强版支持更多候选 MVAMVP 则把 MVP 和 MVD 分开处理。理解 H.264 的推导逻辑对学习 HEVC 的帧间预测非常有帮助。7.4 实际编码中的模式选择策略在编码器中Skip 和 Direct 模式通常作为 RDO 的候选。我的经验是对于静态背景或匀速运动区域优先尝试 Skip对于有遮挡或复杂运动的 B 片区域优先尝试时间 Direct在低延迟配置下空间 Direct 更稳妥。具体阈值需要根据序列特性调优没有万能参数。最后分享一个小技巧在调试 Skip/Direct 推导时可以先把所有邻居 MV 打印出来手动算一遍中值和缩放再和代码输出对比。这个方法虽然笨但能快速定位是逻辑错误还是数据错误。另外建议用标准测试序列如 Foreman、Mobile做验证这些序列的运动特性覆盖比较全面能暴露大部分边界问题。