Fork me on GitHub

H.264/AVC编解码技术及JM源码分析(一)——H.264分层编码与语法结构

接下来的一系列文章将会分别讲解H.264/AVC中所用到的一些编解码技术,同时会结合源码进行分析。本文主要讲解H.264的分层编码以及相关的语法结构

H.264的分层编码

  H.264标准实现将压缩编码和网络传输分离,设计了分层结构,主要包括网络抽象层(Network Abstraction Layer,NAL)和视频编码层(Video Coding Layer,VCL)。其中VCL是真正进行视频编解码的层次,它又可进一步层层细分为条带层、宏块层等等;而NAL是在VCL基础上进行一层封装,以方便传输和储存。相关的结构如图所示。
H.264分层编码

H.264的语法结构

字节流NAL单元

  H.264编码得到文件即是一个NAL码流,其组织格式包括Annex B字节流格式以及RTP包格式,这里主要用到的是Annex B字节流格式,其基本组成单位是一个个的字节流NAL单元(byte stream NAL Unit)。字节流NAL单元主要由一些起始码+NALU+可能存在的结尾比特组成,其中NALU是主要数据所在。

重要语法元素

  • leading_zero_8bits: 可能出现在第一个NALU之前的一个值为0的前缀字节,不过据实验观察,暂时没有发现出现过该字节。
  • zero_byte: 零字节,当NALU为SPS、PPS时会出现。此外,由于一帧图像可能分为一个或多个slice,每一个slice对应一个NALU,因此在一帧图像的第一个slice的NALU中会出现。
  • start_code_prefix_one_3bytes: 固定的3个字节的NALU起始码,每个NALU必然存在,值为0x00 00 01。结合zero_byte可知,一个NALU的起始码可能为0x00 00 01或0x00 00 00 01,因此读取NALU只需读取两个起始码之间的数据即可。
  • nal_unit: 一个NALU的数据,后面详述。
  • trailing_zero_8bits: 和leading_zero_8bits相似的零字节,但是出现在结尾处,通常编码结果也没有包含该部分。

NALU

  首先需要说明几个概念:数据包特串(String of Data Bit,SODB)指的是编码器编码出来的最原始的码流;原始字节序列载荷(Raw Byte Sequence Payload,RBSP)指的是通过字节对齐把SODB变为整字节的形式,并加上一些;NALU则是在RBSP的基础上再加上各种头部数据,并进行了防止RBSP出现与起始码相同的数据的防竞争操作。

重要语法元素

  • forbidden_zero_bit: 值为0的一个bit。
  • nal_ref_idc: 指示NALU优先级的2个bit,取值为0-3,值越大优先级越高,对于SPS、PPS和图像数据等的NALU该值必须大于0。
  • nal_unit_type: 指示NALU类型的5个bit,标准中有总结各种类型的NALU表格。
  • rbsp_byte: RBSP数据。
  • emulation_prevention_three_byte: 防竞争字节,值为0x03。当RBSP数据中出现连续的两个零字节(0x00 00)时,在其后插入0x03。

RBSP

  前面说到RBSP有多种类型,如SPS、PPS和Slice,因此会有不同的语法结构,但是它们尾部都有一个共同的语法结构:rbsp_trailing_bits。该元素即SODB和RBSP的唯一区别所在,其包括:

  • rbsp_stop_one_bit: 值为1的1个bit。
  • rbsp_alignment_zero_bit: 用作字节对齐的值为的0若干bit。

序列参数集(Sequence Paramater Set,SPS)

  包含编码序列的一些参数。

重要语法元素

  • profile_idc、level_idc: 指明编码所用的profile和level,这两者主要是对编码支持的特性和能力做出的约束规定,具体可查看标准。
  • seq_parameter_set_id: 指明该SPS的ID,该ID由PPS引用。
  • max_num_ref_frames: 指明参考帧队列可以包含的帧数的最大值,该元素的值最大为16。
  • pic_width_in_mbs_minus1、pic_height_in_map_units_minus1: 以宏块为单位的图像宽度和高度分别减1。
  • frame_cropping_flag: 指明是否需要对图像进行裁剪。
  • frame_crop_left_offset、frame_crop_right_offset、frame_crop_top_offset、frame_crop_bottom_offset: 若要裁剪,则指明左右上下裁剪的宽度。

图像参数集(Picture Paramater Set,PPS)

  包含编码图像的一些参数。

重要语法元素

  • pic_parameter_set_id: 指明该PPS的ID,该ID由Slice的RBSP引用。
  • seq_parameter_set_id: 所引用的SPS的ID。
  • entropy_coding_mode_flag: 熵编码标志,为0时表明采用CAVLC编码,为1时采用CABAC编码。
  • num_slice_groups_minus1: slice组的个数减1,同时可用于表明是否采用slice组模式,当值为0时实际上表明不采用slice组模式,因为只有一个slice组。
  • num_ref_idx_l0_default_active_minus1、num_ref_idx_l1_default_active_minus1: 分别指明前向和后向参考帧队列当前实际包含的帧数。与max_num_ref_frames的区别在于max_num_ref_frames指明了最大值,用于分配内存;而此处的两个元素是程序运行时动态更新的实际值。
  • weighted_pred_flag: 指明P、SP slice是否进行加权预测。
  • weighted_bipred_idc: 指明B slice是否进行加权预测。
  • pic_init_qp_minus26: 亮度分量的初始化QP值减26。H.264中最终的QP通过三个值计算得出,分别在PPS、slice头和宏块中给出相应值,此处是一个初始值。
  • deblocking_filter_control_present_flag: 值为1时表示显式传递控制滤波强度的参数;为0时使用默认强度。
  • constrained_intra_pred_flag: 为0时允许帧内编码的宏块使用帧间编码的宏块进行预测;为1时禁止。
  • transform_8x8_mode_flag: 为1时表示使用8$\times$8变换代替4$\times$4变换。

没有分割的Slice(slice_layer_without_partitioning_rbsp)

  本文主要针对该种slice形式的RBSP。slice包含了视频编码的数据,主要包括了slice_header、slice_data和rbsp_slice_trailing_bits三个部分,其中结尾元素上文已经介绍,因此主要介绍前两者。

slice_header

  slice_header是头部信息,包含了该slice的相关参数。

重要语法元素

  • first_mb_in_slice 指明该slice中第一个宏块的位置。
  • slice_type: 指明slice的类型,例如2代表I slice,0代表P slice,1代表B slice。
  • pic_parameter_set_id: 所引用的PPS的ID,以上文对应。
  • idr_pic_id : IDR帧才有的ID标识。
  • direct_spatial_mv_pred_flag: 当B帧使用Direct模式进行预测时,指明使用空域预测方式还是时域预测方式。1:空域;0:时域。
  • num_ref_idx_active_override_flag: 指明是否重载当前帧的参考帧队列长度,若是,则会出现新的num_ref_idx_l0_default_active_minus1、num_ref_idx_l1_default_active_minus1进行指定。
  • slice_qp_delta: 上面提到的,这是slice头中指明的QP偏移。
  • disable_deblocking_filter_idc: 又一个控制滤波的参数。

slice_data

  slice_data是slice的主体部分,包含了真正的视频编码数据。

重要语法元素

  • cabac_alignment_one_bit 采用CABAC时,要求slice头的数据要先对齐,因此会先出现若干bit的1来进行字节对齐。
  • mb_skip_run: 采用CAVLC时方存在,携带该元素的P/B宏块指明该宏块之前一共有几个连续的P_Skip/B_Skip宏块,skip宏块不包括任何编码数据,仅仅利用预测信息进行重构。
  • mb_skip_flag: 采用CABAC时方存在,每个skip宏块均包含该元素表明自身是skip宏块。
  • macroblock_layer : 宏块层,包括所有宏块的数据。(重要)
  • end_of_slice_flag: 为0时表明该slice仍有宏块未编码;为1时表示已到达slice结尾。

macroblock_layer

  macroblock_layer是最重要的宏块编码数据所在。

重要语法元素

  • mb_type: 宏块类型,种类较多,不仅指明是I/P/B宏块,还指明了其宏块分割方式和预测模式。
  • pcm_alignment_zero_bit: 当宏块类型为I_PCM时出现,其要求先用0进行字节对齐。I_PCM是一种特殊的宏块,它不经过变换量化等步骤,而是直接传输像素值,属于比较少见的类型。
  • pcm_sample_luma、pcm_sample_chroma: I_PCM宏块中包含亮度和色度像素值的数组变量。
  • sub_mb_pred: 子宏块(8$\times$8大小)预测的语法结构,即一个宏块内的4个8$\times$8子宏块内部会分别进行预测。(重要)
  • transform_size_8x8_flag : 满足一定条件时会出现该元素表明是否采用8x8变换,若不存在该元素,默认不采用。
  • mb_pred: 宏块预测的语法结构,宏块预测与子宏块预测只会存在一个。(重要)
  • coded_block_pattern: 非Intra_16$\times$16模式时存在,用于指明8$\times$8块是否含有非零系数。共有6位,前两位分别代表UV分量的两个8$\times$8块,后四位从低到高代表Y分量的四个8$\times$8块(按扫描顺序)。值为0代表无非零系数,1代表至少有一个非零系数。Intra_16$\times$16模式时coded_block_pattern隐含在mb_type中。该元素的主要作用时提高解码效率。
  • mb_qp_delta: 如前所述,宏块层指明的QP偏移,范围是[-26, +25]此时每一个slice第一个宏块的计算公式为:

$$QP_0 = pic\_init\_qp\_minus26 + 26 + slice\_qp\_delta + mb\_qp\_delta$$
而后续宏块QP的计算公式为:
$$QP_n = (QP_{n-1} + mb\_qp\_delta + 52)\ \%\ 52$$
注意n-1是从编码顺序角度而言的。

  • residual: 残差编码数据。(重要)

mb_pred

重要语法元素

  • prev_intra4x4_pred_mode_flag,rem_intra4x4_pred_mode:: intra_4$\times$4预测时,用于指示预测的预测模式(有点拗口)是否等于真正的预测模式。1代表相等;0代表不相等,此时需要额外用rem_intra4x4_pred_mode指定真实模式。该种做法的目的在于两者相等时可以减少编码数据量。
  • intra_chroma_pred_mode: 当采用帧内预测时存在该元素,用于指明色度分量的预测模式。
  • ref_idx_l0、ref_idx_l1: 前、后向参考帧队列,通过其中的索引号mbPartIdx可以找到参考帧。
  • mvd_l0、mvd_l1: 前、后向运动矢量差值,包括水平和垂直两个分量。其中compIdx=0为水平方向的值,compIdx=1位垂直方向的值。

sub_mb_pred

重要语法元素

  • sub_mb_type: 子宏块类型。
  • ref_idx_l0、ref_idx_l1: 同mb_pred。
  • mvd_l0、mvd_l1: 同mb_pred。

residual

  该过程首先会查看熵编码方式,根据熵编码的不同处理有所不同。然后查看是否是Intra_16x16模式,若是则首先将16个DC系数取出单独进行编码,然后再分别对每个4$\times$4块剩下的AC系数进行编码;若不是则对每个4$\times$4块的16个系数进行编码。Y分量编码结束再一次编码U、V分量。

源码分析

  在H.264/AVC的官方参考程序JM中,存在着编码和解码所需的配置文件,这些文件以.cfg为后缀(当然也可以直接在命令行中设置参数而不使用配置文件)。上述许多语法元素的值实际上就是在配置文件中指定的,而在源码中主要由config_common.h、configfile.h和configfile.c对配置文件中的各种参数进行读取和解析。下面以JM 19.0为例进行源码分析。

1
2
3
4
5
6
7
8
9
10
typedef struct {
char *TokenName; //!< name
void *Place; //!< address
int Type; //!< type: 0-int, 1-char[], 2-double
double Default; //!< default value
int param_limits; //!< 0: no limits, 1: both min and max, 2: only min (i.e. no negatives), 3: special case for QPs since min needs bitdepth_qp_scale
double min_limit;
double max_limit;
int char_size; //!< Dimension of type char[]
} Mapping;

  Mapping这个结构体是整个参数解析的基础,其主要作用是将配置文件中的一个参数映射到程序的输入参数中的一个变量,其中输出参数是一个InputParameters类型的结构体,包含了所有的输入参数。Mapping Map[]这个数组则保存了所有参数的映射关系,此处以数组中的第一个元素为例解释Mapping中重要变量的含义:

{“ProfileIDC”, &cfgparams.ProfileIDC, 0, (double) PROFILE_IDC, 0, 0.0, 0.0,}

  • TokenName: 配置文件中的参数名,如ProfileIDC
  • Place: 程序中的参数变量,此处是其地址,如&cfgparams.ProfileIDC,cfgparams即是InputParameters结构体,ProfileIDC是其中一个变量
  • Type: 参数变量的数据类型
  • Default: 参数变量的默认值,如用PROFILE_IDC这个宏作为默认值
  • param_limits: 参数变量值的限制,英文注释中清晰地列出了4种相关限制,0为无限制
  • min_limit/max_limit: 与上一个变量对应,表明了所能取的最小/大值,无限制时这两个值均为0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void Configure (VideoParameters *p_Vid, InputParameters *p_Inp, int ac, char *av[])
{
char *content = NULL;
int CLcount, ContentLen, NumberParams;
char *filename=DEFAULTCONFIGFILENAME;

//中间省略若干代码

memset (&cfgparams, 0, sizeof (InputParameters));
//Set default parameters.
printf ("Setting Default Parameters...\n");
//设置参数的默认值
InitParams(Map);

// Process default config file
CLcount = 1;

if (ac>=3)
{
if (0 == strncmp (av[1], "-d", 2))
{
filename=av[2];
CLcount = 3;
}
if (0 == strncmp (av[1], "-h", 2))
{
JMHelpExit();
}
}
printf ("Parsing Configfile %s", filename);
//读取配置文件到一个buffer中并返回
content = GetConfigFileContent (filename);
if (NULL==content)
error (errortext, 300);
//解析配置文件参数并映射到输入参数变量中
ParseContent (p_Inp, Map, content, (int) strlen(content));
printf ("\n");
free (content);

// Parse the command line

while (CLcount < ac)
{
//省略若干代码
}
printf ("\n");
}

  Configure函数是解析参数的主要函数,传入的参数包括:视频参数p_Vid, 输入参数p_Inp, 命令行参数个数ac, 命令行参数av。主要调用InitParams、GetConfigFileContent、ParseContent三个函数实现整个参数解析过程。