参考《新一代视频压缩编码标准 毕厚杰 第7章 H.264的句法和语义》
一、句法元素与变量
编码器将数据编码为句法元素然后依次发送。在解码器端,通常要将句法元素作求值计算,得出一些中间数据,这些中间数据就是 H.264 定义的变量。如图 7.5:
图中,pic_width_in_mbs_minus1 是解码器直接从码流中提取的句法元素,这个句法元素表征图像的宽度,以宏块为单位。我们看到,为了提高编码效率,H.264 将图像实际的宽度减去 1 后再传送。
PicWidthInMbs = pic_width_in_mbs_minus1 + 1
PicWidthInSamplesL = PicWidthInMbs * 16
PicWidthInSamplesC = PicWidthInMbs * 8
以 上 变 量 PicWidthInMbs 表 示 图 像 以 宏 块 为 单 位 的 宽 , 变 量 PicWidthInSamplesL 、PicWidthInSamplesC 分别表示图像的亮度、色度分量以像素为单位的宽。H.264 定义这些变量是因为在后续句法元素的提取算法或图像的重建中需要用到它们的值。在 H.264 中,句法元素的名称是由小写字母和一系列的下划线组成,而变量名称是大小写字母组成,中间没有下划线。
二、语法
句法是句法元素的组织结构,而对一个结构的描述必然少不了对应的语法,语法提供判断、循环等必要的描述方法。H.264 采用一种类 C 语法。
判断
if ( 条件 )
{
…
}
else
{
…
}
与 C 语言类似,H.264 有三种循环体:
a)
do
{
…
}while ( 条件 )
b)
while ( 条件 )
{
…
}
c)
for ( 初始 ; 条件 ; 求值 )
{
…
}
三、描述子
描述子是指从比特流提取句法元素的方法,即句法元素的解码算法,每个句法元素都有相对应的描述子。由于 H.264 编码的最后一步是熵编码,所以这里的描述子大多是熵编码的解码算法。H.264定义了如下几种描述子:
- a) ae(v) 基于上下文自适应的二进制算术熵编码
- b) b(8) 读进连续的 8 个比特
- c) ce(v) 基于上下文自适应的可变长熵编码
- d) f(n) 读进连续的 n 个比特
- e) i(n)/i(v) 读进连续的若干比特,并把它们解释为有符号整数
- f) me(v) 映射指数 Golomb 熵编码
- g) se(v) 有符号指数 Golomb 熵编码
- h) te(v) 截断指数 Golomb 熵编码
- i) u(n)/u(v) 读进连续的若干比特,并将它们解释为无符号整数
- j) ue(v) 无符号指数 Golomb 熵编码
我们看到,描述子都在括号中带有一个参数,这个参数表示需要提取的比特数。当参数是 n 时,表明调用这个描述子的时候会指明 n 的值,也即该句法元素是定长编码的。当参数是 v 时,对应的句法元素是变长编码,这时有两种情况:i(v) 和 u(v) 两个描述子的 v 由以前的句法元素指定,也就是说在前面会有句法元素指定当前句法元素的比特长度;除了这两个描述子外,其它描述子都是熵编码,它们的解码算法本身能够确定当前句法元素的比特长度。
四、句法表
句法表定义了 H.264 的句法,指明在码流中依次出现的句法元素及它们出现的条件、提取描述子等。就象前文所提,句法表是分层嵌套的。
在句法表中的 C 字段表示该句法元素的分类,这是为片分区服务的,句法元素分类的具体含义在表 7.20 详细介绍。Descriptor 指定对应句法元素的描述子。
下面的表太长,可以参见原书……
五、VCL NAL
视频编码中采用的如预测编码、变化量化、熵编码等编码工具主要工作在slice层或以下,这一层通常被称为“视频编码层”(Video Coding Layer, VCL)。
相对的,在slice以上所进行的数据和算法通常称之为“网络抽象层”(Network Abstraction Layer, NAL)。设计定义NAL层的主要意义在于提升H.264格式的视频对网络传输和数据存储的亲和性。
六、NAL 层语义
1.起始码
在网络传输的环境下,编码器将每个 NAL 各自独立、完整地放入一个分组,由于分组都有头部,解码器可以很方便地检测出 NAL 的分界,依次取出 NAL 进行解码。为了节省码流,H.264 没有另外在 NAL 的头部设立表示起始的句法元素,我们从表 7.1 可以看到这点。
但是如果编码数据是储存在介质(如 DVD 光盘)上,由于 NAL 是依次紧密排列,解码器将无法在数据流中分辨每个 NAL的起始和终止,所以必须要有另外的机制来解决这个问题。针对这个问题,H.264 草案的附录 B 中指明了一种简单又高效的方案。当数据流是存储在介质上时,在每个 NAL 前添加起始码:0x000001
在某些类型的介质上,为了寻址的方便,要求数据流在长度上对齐,或必须是某个常数的倍数。考虑到这种情况,H.264 建议在起始码前添加若干字节的 0 来填充,直到该 NAL 的长度符合要求。
在这样的机制下,解码器在码流中检测起始码,作为一个 NAL 的起始标识,当检测到下一个起始码时当前 NAL 结束。H.264 规定当检测到 0x000000
时也可以表征当前 NAL 的结束,这是因为连着的三个字节的 0 中的任何一个字节的 0 要么属于起始码要么是起始码前面添加的 0。
添加起始码是一个解决问题的很好的方法,但上面关于起始码的介绍还不完整,因为忽略了一个重要的问题:如果在 NAL 内部出现了 0x000001 或是 0x000000 的序列怎么办?毫无疑问这种情况是致命的,解码器将把这些本来不是起始码的字节序列当作起始码,而错误地认为这里往后是一个新的 NAL 的开始,进而造成解码数据的错位!而我们做的大量实验证明,NAL 内部经常会出现这样的字节序列。
于是 H.264 提出了另外一种机制,叫做“防止竞争”,在编码器编码完一个 NAL 时,应该检测是否出现图 7.6 左侧 中的四个字节序列,以防止它们和起始码竞争。如果检测到这些序列存在,编码器将在最后一个字节前插入一个新的字节:0x03,从而使它们变成图 7.6 右测的样子。当解码器在 NAL 内部检测到有 0x000003 的序列时,将把 0x03 抛弃,恢复原始数据。
图 7.6 中的前两个序列我们前文中已经提到,第三个 0x000002 是作保留用,而第四个 0x000003是为了保证解码器能正常工作,因为我们刚才提到,解码器恢复原始数据的方法是检测到 0x000003就抛弃其中的 0x03,这样当出现原始数据为 0x000003 时会破坏数据,所以必须也应该给这个序列插入 0x03。
我们可以从句法表 7.1 中看到解码器在 NAL 层的处理步骤,其中变量 NumBytesInNALunit 是解码器计算出来的,解码器在逐个字节地读一个 NAL 时并不同时对它解码,而是要通过起始码机制将整个 NAL 读进、计算出长度后再开始解码。
2.forbidden_zero_bi
等于 0
3.nal_ref_idc
指示当前 NAL 的优先级。取值范围为 0-3, ,值越高,表示当前 NAL 越重要,需要优先受到保护。H.264 规定如果当前 NAL 是属于参考帧的片,或是序列参数集,或是图像参数集这些重要的数据单位时,本句法元素必须大于 0。但在大于 0 时具体该取何值,却没有进一步规定,通信双方可以灵活地制定策略。
4.nal_unit_type 指明当前 NAL unit 的类型,具体类型的定义如表 7.20:
nal_unit_type | NAL 类型 | C |
---|---|---|
0 | 未使用 | |
1 | 不分区、非 IDR 图像的片 | 2, 3, 4 |
2 | 片分区 A | 2 |
3 | 片分区 B | 3 |
4 | 片分区 C | 4 |
5 | IDR 图像中的片 | 2, 3 |
6 | 补充增强信息单元(SEI) | 5 |
7 | 序列参数集 | 0 |
8 | 图像参数集 | 1 |
9 | 分界符 | 6 |
10 | 序列结束 | 7 |
11 | 码流结束 | 8 |
12 | 填充 | 9 |
13..23 | 保留 | |
24..31 | 未使用 |
nal_unit_type=5 时,表示当前 NAL 是 IDR 图像的一个片,在这种情况下,IDR 图像中的每个片的nal_unit_type 都应该等于 5。注意 IDR 图像不能使用片分区。
5.rbsp_byte[i]
RBSP 的第 i 个字节。RBSP 指原始字节载荷,它是 NAL 单元的数据部分的封装格式,封装的数据来自 SODB(原始数据比特流)。SODB 是编码后的原始数据,SODB 经封装为 RBSP 后放入 NAL 的数据部分。下面介绍一个 RBSP 的生成顺序。
从 SODB 到 RBSP 的生成过程:
- 如果 SODB 内容是空的,生成的 RBSP 也是空的
- 否则,RBSP 由如下的方式生成:
- 1) RBSP 的第一个字节直接取自 SODB 的第 1 到 8 个比特,(RBSP 字节内的比特按照从左到右对应为从高到低的顺序排列,most significant),以此类推,RBSP 其余的每个字节都直接取自 SODB 的相应比特。RBSP 的最后一个字节包含 SODB 的最后几个比特,及如下的rbsp_trailing_bits()
- 2) rbsp_trailing_bits()的第一个比特是 1,接下来填充 0,直到字节对齐。(填充 0 的目的也是为了字节对齐)
- 3) 最后添加若干个 cabac_zero_word(其值等于 0x0000)
6.emulation_prevention_three_byte
NAL 内部为防止与起始码竞争而引入的填充字节 ,值为 0x03。