在编码器输出的码流中,数据的基本单位是句法元素,每个句法元素由若干比特组成,它表示某个特定的物理意义,例如:宏块类型、量化参数等。句法表征句法元素的组织结构,语义阐述句法元素的具体含义。所有的视频编码标准都是通过定义句法和语义来规范编解码器的工作流程。
编码器输出的比特码流中,每个比特都隶属某个句法元素,也就是说,码流是由一个个句法元素依次衔接组成的,码流中除了句法元素并不存在专门用于控制或同步的内容。在 H.264 定义的码流中,句法元素被组织成有层次的结构,分别描述各个层次的信息。下图表现了这种结构。
句法元素的分层结构有助于更有效地节省码流。例如,在一个图像中,经常会在各个片之间有相同的数据,如果每个片都同时携带这些数据,势必会造成码流的浪费。更为有效的做法是将该图像的公共信息抽取出来,形成图像一级的句法元素,而在片级只携带该片自身独有的句法元素。在H.264 中,句法元素共被组织成 序列、图像、片、宏块、子宏块五个层次。
H.264 的分层结构是经过精心设计的,与以往的视频编码标准相比有很大的改进,这些改进主要针对传输中的错误掩藏,在有误码发生时可以提高图像重建的性能。在以往的标准中,分层的组织结构如下图所示 ,它们如同 TCP/IP 协议的结构,每一层都有头部,然后在每层的数据部分包含该层的数据。
在这样的结构中,每一层的头部和它的数据部分形成管理与被管理的强依赖关系,头部的句法元素是该层数据的核心,而一旦头部丢失,数据部分的信息几乎不可能再被正确解码出来。尤其在序列层及图像层,由于网络中 MTU(最大传输单元)大小的限制,不可能将整个层的句法元素全部放入同一个分组中,这个时候如果头部所在的分组丢失,该层其他分组即使能被正确接收也无法解码,造成资源浪费。
在 H.264 中,分层结构最大的不同是取消了序列层和图像层,并将原本属于序列和图像头部的大部分句法元素游离出来形成序列和图像两级参数集,其余的部分则放入片层。 参数集是一个独立的数据单位,不依赖于参数集外的其他句法元素。下图描述了参数集与参数集外句法元素的关系,在图中我们可以看到,参数集只是在片层句法元素需要的时候被引用,而且,一个参数集并不对应某个特定的图像或序列,同一个序列参数集可以被多个序列中的图像参数集引用,同理,同一个图像参数集也可以被多个图像引用。只在编码器认为需要更新参数集的内容时,才会发送出新的参数集。在这种机制下,由于参数集是独立的,可以被多次重发或者采用特殊技术加以保护。
在图 7.3 的描述中,参数集与参数集外部的句法元素处于不同信道中,这是 H.264 的一个建议,我们可以使用更安全但成本更昂贵的通道来传输参数集,而使用成本低但不够可靠的信道传输其他句法元素,只需要保证片层中的某个句法元素需要引用某个参数集时,那个参数集已经到达解码器,也就是参数集在时间上必须先被传送。当然,在条件不允许的情况下,我们也可以采用妥协的办法:在同一个物理信道中传输所有的句法元素,但专门为参数集采用安全可靠的通信协议,如 TCP。当然,H.264 也允许我们为包括参数集在内的所有句法元素指定同样的通信协议,但这时所有参数集必须被多次重发,以保证解码器最终至少能接收到一个。在参数集和片使用同个物理信道的情况下,图 7.3 中的信道 1 和信道 2 应该被理解为逻辑上的信道,因为从逻辑上看,参数集与其它句法元素还是处于各自彼此独立的信道中。
H.264 在片层增加了新的句法元素指明所引用的参数集的编号,同时因为取消了图像层,片成为了信道 2 中最上层的独立的数据单位,每个片必须自己携带关于所属图像的编号、大小等基本信息,这些信息在同一图像的每个片中都必须是一致的。在编码时, H.264 的规范要求将参数集、片这些独立的数据单位尽可能各自完整地放入一个分组中被传送。
从表面上看来, H.264 关于参数集和片层的结构增加了编码后数据的冗余度(比如参数集必须多次重发,又如每个片都必须携带一部分相同的关于整个图像的信息,而这些数据完全是重复的),降低了编码效率,但这些技术的采用使得通信的鲁棒性大大增强,当数据传输中出现丢包,能够将使错误限制在最小范围,防止错误的扩散,解码后对错误的掩藏和恢复也能起到很好的作用。 一个片的丢失将不会影响其它片的解码,还可以通过该片前后的片来恢复该片的数据。
H.264 片层以下的句法元素的结构大体上和以往标准类似,但在相当多的细节上有所改进,所有的改进的目的不外乎两个:在错误发生时防止错误扩散、减少冗余信息提高编码效率。这两者往往是矛盾的, H.264 在这两者上的取舍显得颇具匠心。
图 7.3 所示的码流的结构是一种简化的模型,这个模型已经能够正确工作,但还不够完善,不适合复杂的场合。在复杂的通信环境中,除了片和参数集外还需要其他的数据单位来提供额外的信息。图 7.4 描述了在复杂通信中的码流中可能出现的数据单位。如前文所述,参数集可以被抽取出来使用其它信道。
在图 7.4 中我们看到,一个序列的第一个图像叫做 IDR 图像(立即刷新图像), IDR 图像都是 I图像。H.264 引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果在前一个序列的传输中发生重大错误,如严重的丢包,或其他原因引起数据错位,在这里可以获得重新同I P P P I P P P I P P P步。 IDR 图像之后的图像永远不会引用 IDR 图像之前的图像的数据来解码。
要注意 IDR 图像和 I 图像的区别,IDR 图像一定是 I 图像, 但 I 图像不一定是 IDR 图像。一个序列中可以有很多的 I 图像, I 图像之后的图像可以引用 I 图像之间的图像做运动参考。
在图 7.4 中,除了参数集与片外还有其它的数据单位,这些数据单位可以提供额外的数据或同步信息,这些数据单位也是一系列句法元素的集合。它们在解码过程中不是必需的,但却可以适当提高同步性能或定义图像的复杂特征。
1.句法元素与变量
编码器将数据编码为句法元素然后依次发送。在解码器端,通常要将句法元素作求值计算,得出一些中间数据,这些中间数据就是 H.264 定义的变量。如下图
图中, pic_width_in_mbs_minus1 是解码器直接从码流中提取的句法元素,这个句法元素表征图像的宽度,以宏块为单位。我们看到,为了提高编码效率, H.264 将图像实际的宽度减去 1 后再传送。
以 上 变 量 PicWidthInMbs 表 示 图 像 以 宏 块 为 单 位 的 宽 , 变 量 PicWidthInSamplesL 、PicWidthInSamplesC 分别表示图像的亮度、色度分量以像素为单位的宽。H.264 定义这些变量是因为在后续句法元素的提取算法或图像的重建中需要用到它们的值。在 H.264 中,句法元素的名称是由小写字母和一系列的下划线组成,而变量名称是大小写字母组成,中间没有下划线。
2.语法
句法是句法元素的组织结构,而对一个结构的描述必然少不了对应的语法,语法提供判断、循环等必要的描述方法。H.264 采用一种类 C 语法。
3.描述因子
描述子是指从比特流提取句法元素的方法,即句法元素的解码算法,每个句法元素都有相对应的描述子。由于 H.264 编码的最后一步是熵编码,所以这里的描述子大多是熵编码的解码算法。H.264定义了如下几种描述子:
描述子都在括号中带有一个参数,这个参数表示需要提取的比特数。当参数是 n 时,表明调用这个描述子的时候会指明 n 的值,也即该句法元素是定长编码的。当参数是 v 时,对应的句法元素是变长编码,这时有两种情况: i(v) 和 u(v) 两个描述子的 v 由以前的句法元素指定,也就是说在前面会有句法元素指定当前句法元素的比特长度;除了这两个描述子外,其它描述子都是熵编码,它们的解码算法本身能够确定当前句法元素的比特长度。
在网络传输的环境下,编码器将每个 NAL 各自独立、完整地放入一个分组,由于分组都有头部,解码器可以很方便地检测出 NAL 的分界,依次取出 NAL 进行解码。为了节省码流, H.264 没有另外在 NAL 的头部设立表示起始的句法元素。但是如果编码数据是储存在介质(如 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 时,应该检测是否出现下图左侧 中的四个字节序列,以防止它们和起始码竞争。如果检测到这些序列存在,编码器将在最后一个字节前插入一个新的字节: 0x03,从而使它们变成图 7.6 右测的样子。当解码器在 NAL 内部检测到有 0x000003 的序列时,将把 0x03 抛弃,恢复原始数据。
上图中的前两个序列我们前文中已经提到,第三个 0x000002 是作保留用,而第四个 0x000003是为了保证解码器能正常工作,因为我们刚才提到,解码器恢复原始数据的方法是检测到 0x000003就抛弃其中的 0x03,这样当出现原始数据为 0x000003 时会破坏数据,所以必须也应该给这个序列插入 0x03。
我们可以从句法表 7.1 中看到解码器在 NAL 层的处理步骤,其中变量 NumBytesInNALunit 是解码器计算出来的,解码器在逐个字节地读一个 NAL 时并不同时对它解码,而是要通过起始码机制将整个 NAL 读进、计算出长度后再开始解码。
nal_unit_type=5 时,表示当前 NAL 是 IDR 图像的一个片,在这种情况下, IDR 图像中的每个片的nal_unit_type 都应该等于 5。注意 IDR 图像不能使用片分区。
从 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)
序列参数集层句法
注意: 当 constraint_set0_flag,constraint_set1_flag,constraint_set2_flag 中的两个以上等于 1 时, A.2中的所有制约条件都要被遵从。
v = log2_max_frame_num_minus4 + 4
从另一个角度看,这个句法元素同时也指明了 frame_num 的所能达到的最大值:
MaxFrameNum = 2( log2_max_frame_num_minus4 + 4 )
变量 MaxFrameNum 表示 frame_num 的最大值,在后文中可以看到,在解码过程中它也是一个非常重要的变量。值得注意的是 frame_num 是循环计数的,即当它到达 MaxFrameNum 后又从 0 重新开始新一轮的计数。 解码器必须要有机制检测这种循环, 不然会引起类似千年虫的问题,在图像的顺序上造成混乱。
在如下的视频序列中本句法元素不应该等于 2:
- 一个非参考帧的接入单元后面紧跟着一个非参考图像(指参考帧或参考场)的接入单元
- 两个分别包含互补非参考场对的接入单元后面紧跟着一个非参考图像的接入单元.
- 一个非参考场的接入单元后面紧跟着另外一个非参考场,并且这两个场不能构成一个互补场对
MaxPicOrderCntLsb = 2( log2_max_pic_order_cnt_lsb_minus4 + 4 )
该变量在 pic_order_cnt_type = 0 时使用
FrameHeightInMbs = ( 2 – frame_mbs_only_flag ) * PicHeightInMapUnits
PictureHeightInMbs= ( 2 – frame_mbs_only_flag ) * PicHeightInMapUnits
map_units 的定义:
- 当 frame_mbs_only_flag 等于1时, map_units 指的就是宏块
- 当 frame_mbs_only_falg 等于0时
- 帧场自适应模式时, map_units 指的是宏块对
- 场模式时, map_units 指的是宏块
- 帧模式时, map_units 指的是与宏块对相类似的,上下两个连续宏块的组合体。