【从零实现一个H.264码流解析器】(五):解析图像参数集PPS的句法元素

前面我们解析了序列参数集SPS,它也是第一个出现的NALU中包含的内容。下面我们开始解析第二个NALU,由上篇生成的trace文件也可以看到,它的nalu->nal_unit_type等于8,为图像参数集PPS。

有了解析SPS的铺垫,解析PPS就轻松自然多了。解析PPS同SPS一样,同样分为两大步:

  • (1)数据存放:定义与h264文档相匹配的PPS的数据结构,在这里我们依然选用结构体
  • (2)解析实现:从nalu->buf中逐个解析句法元素,存放在结构体实例中

下面首先实现第一步,我们将PPS的结构体定义放在parset.h中,解析实现放在parset.c中。

1、数据存放

我们参照h264协议文档,将pps的句法元素定义为结构体pps_t。相对sps,pps的结构体较为简洁,它并没有引用其他的结构体。

1.1 pps_t

/**
Picture Parameter Set
@see 7.3.2.2 Picture parameter set RBSP syntax
*/
typedef struct
{
   int pic_parameter_set_id;                            // ue(v)
   int seq_parameter_set_id;                            // ue(v)
   int entropy_coding_mode_flag;                        // u(1)
   int bottom_field_pic_order_in_frame_present_flag;    // u(1)
   
   /*  —————————— FMO相关 Start  —————————— */
   int num_slice_groups_minus1;                         // ue(v)
//  if( num_slice_groups_minus1 > 0 ) {
       int slice_group_map_type;                        // ue(v)
//      if( slice_group_map_type = = 0 )
//          for( iGroup = 0; iGroup <= num_slice_groups_minus1; iGroup++ )
               // num_slice_groups_minus1取值范围[0, 7],见附录A
               int run_length_minus1[8];                // ue(v)
//      else if( slice_group_map_type = = 2 )
//          for( iGroup = 0; iGroup < num_slice_groups_minus1; iGroup++ ) {
               int top_left[8];                         // ue(v)
               int bottom_right[8];                     // ue(v)
//      else if( slice_group_map_type = = 3 | | slice_group_map_type = = 4 | | slice_group_map_type = = 5 ) {
           int slice_group_change_direction_flag;       // u(1)
           int slice_group_change_rate_minus1;          // ue(v)
//      } else if( slice_group_map_type = = 6 ) {
           int pic_size_in_map_units_minus1;            // ue(v)
//          for( i = 0; i <= pic_size_in_map_units_minus1; i++ )
               int *slice_group_id;                     // u(v)
   /*  —————————— FMO相关 End  —————————— */
   
   int num_ref_idx_l0_default_active_minus1;            // ue(v)
   int num_ref_idx_l1_default_active_minus1;            // ue(v)
   int weighted_pred_flag;                              // u(1)
   int weighted_bipred_idc;                             // u(2)
   
   int pic_init_qp_minus26;                             // se(v)
   int pic_init_qs_minus26;                             // se(v)
   int chroma_qp_index_offset;                          // se(v)
   
   int deblocking_filter_control_present_flag;          // u(1)
   int constrained_intra_pred_flag;                     // u(1)
   int redundant_pic_cnt_present_flag;                  // u(1)
   
//  if( more_rbsp_data( ) ) {
       int transform_8x8_mode_flag;                     // u(1)
       int pic_scaling_matrix_present_flag;             // u(1)
//      if( pic_scaling_matrix_present_flag )
           /*
           for( i = 0; i < 6 +
            ( ( chroma_format_idc != 3 ) ? 2 : 6 ) * transform_8x8_mode_flag; i++ ) {
           */
               int pic_scaling_list_present_flag[12];    // u(1)
//              if( pic_scaling_list_present_flag[ i ] )
//                  if( i < 6 )
                       int ScalingList4x4[6][16]; // 二维数组遍历
                       int UseDefaultScalingMatrix4x4Flag[6];
//                  else
                       int ScalingList8x8[6][64];
                       int UseDefaultScalingMatrix8x8Flag[6];
       int second_chroma_qp_index_offset;                  // se(v)
} pps_t;

pps_t中有FMO相关的几个数组:run_length_minus1[]、top_left[]、bottom_right[],它们的数组大小都由num_slice_groups_minus1的取值范围决定。而num_slice_groups_minus1的取值范围,根据其语义查询附录A可得为[0, 7],因此这几个数组容量为8。

后面scaling_list相关的几个数组大小,推理方法和sps中的scaling_list一致,因此不再赘述。

值得一提的是,pps中有一个数组,我们并没有直接写明它的数组大小,而是写成了指针的形式,那就是(int *)slice_group_id。这里我们使用c语言的特性,将指针当做数组使用。而这里没有指定数组大小的原因是,它的大小与解码的图像尺寸有关。

当然我们可以一开始就使用一个较大值,但是我们选择在解析时,根据解析到的以映射单元为单位的图像尺寸,动态初始化数组大小,具体到这里也即指针(int *)slice_group_id的大小

1.2 allocPPS()

而在pps的初始化函数中,我们只是将指针pps->slice_group_id置为NULL。

// 初始化pps结构体
pps_t *allocPPS(void)
{
   pps_t *pps = calloc(1, sizeof(pps_t));
   if (pps == NULL) {
       fprintf(stderr, "%s\n", "Alloc PPS Error");
       exit(-1);
   }
   pps->slice_group_id = NULL;
   return pps;
}

1.3 freePPS()

在释放pps时,会先判断pps->slice_group_id是否为NULL,进而再去释放。这一步的判断操作,需要结合后面保存pps的操作来理解。

// 释放pps
void freePPS(pps_t *pps)
{
   if (pps->slice_group_id != NULL) {
       free (pps->slice_group_id);
   }
   free(pps);
}

2、解析实现

PPS的句法元素解析同样分为两步:

  • (1)解析句法元素,存入结构体实例pps
  • (2)保存解析后的结构体实例pps,以便后续使用

为此实现函数:processPPS()

/**
处理PPS,包含两步:
先解析、后保存
*/
void processPPS(bs_t *b)
{
   pps_t *pps = allocPPS();
   // 0.解析
   parse_pps_syntax_element(pps, b);
   // 1.保存
   save_pps_as_available(pps);
   
   freePPS(pps);
}

它接收从read_nal_unit()传进来的,码流读取工具句柄bs_t,并负责控制解析和保存流程。

2.1 parse_pps_syntax_element()

解析的过程,同样参照h264协议中句法元素的语法结构即可,不过有些小点需要注意的,我们先看代码再说。

/**
解析pps句法元素
[h264协议文档位置]:7.3.2.2 Picture parameter set RBSP syntax
*/
void parse_pps_syntax_element(pps_t *pps, bs_t *b)
{
   // 解析slice_group_id[]需用的比特个数
   int bitsNumberOfEachSliceGroupID;
   
   pps->pic_parameter_set_id = bs_read_ue(b, "PPS: pic_parameter_set_id");
   pps->seq_parameter_set_id = bs_read_ue(b, "PPS: seq_parameter_set_id");
   pps->entropy_coding_mode_flag = bs_read_u(b, 1, "PPS: entropy_coding_mode_flag");
   pps->bottom_field_pic_order_in_frame_present_flag = bs_read_u(b, 1, "PPS: bottom_field_pic_order_in_frame_present_flag");
   
   /*  —————————— FMO相关 Start  —————————— */
   pps->num_slice_groups_minus1 = bs_read_ue(b, "PPS: num_slice_groups_minus1");
   if (pps->num_slice_groups_minus1 > 0) {
       pps->slice_group_map_type = bs_read_ue(b, "PPS: slice_group_map_type");
       if (pps->slice_group_map_type == 0)
       {
           for (int i = 0; i <= pps->num_slice_groups_minus1; i++) {
               pps->run_length_minus1[i] = bs_read_ue(b, "PPS: run_length_minus1[]");
           }
       }
       else if (pps->slice_group_map_type == 2)
       {
           for (int i = 0; i < pps->num_slice_groups_minus1; i++) {
               pps->top_left[i] = bs_read_ue(b, "PPS: top_left[]");
               pps->bottom_right[i] = bs_read_ue(b, "PPS: bottom_right[]");
           }
       }
       else if (pps->slice_group_map_type == 3 ||
                pps->slice_group_map_type == 4 ||
                pps->slice_group_map_type == 5)
       {
           pps->slice_group_change_direction_flag = bs_read_u(b, 1, "PPS: slice_group_change_direction_flag");
           pps->slice_group_change_rate_minus1 = bs_read_ue(b, "PPS: slice_group_change_rate_minus1");
       }
       else if (pps->slice_group_map_type == 6)
       {
           // 1.计算解析slice_group_id[]需用的比特个数,Ceil( Log2( num_slice_groups_minus1 + 1 ) )
           if (pps->num_slice_groups_minus1+1 >4)
               bitsNumberOfEachSliceGroupID = 3;
           else if (pps->num_slice_groups_minus1+1 > 2)
               bitsNumberOfEachSliceGroupID = 2;
           else
               bitsNumberOfEachSliceGroupID = 1;
           
           // 2.动态初始化指针pps->slice_group_id
           pps->pic_size_in_map_units_minus1 = bs_read_ue(b, "PPS: pic_size_in_map_units_minus1");
           pps->slice_group_id = calloc(pps->pic_size_in_map_units_minus1+1, 1);
           if (pps->slice_group_id == NULL) {
               fprintf(stderr, "%s\n", "parse_pps_syntax_element slice_group_id Error");
               exit(-1);
           }
           
           for (int i = 0; i <= pps->pic_size_in_map_units_minus1; i++) {
               pps->slice_group_id[i] = bs_read_u(b, bitsNumberOfEachSliceGroupID, "PPS: slice_group_id[]");
           }
       }
   }
   /*  —————————— FMO相关 End  —————————— */
   
   pps->num_ref_idx_l0_default_active_minus1 = bs_read_ue(b, "PPS: num_ref_idx_l0_default_active_minus1");
   pps->num_ref_idx_l1_default_active_minus1 = bs_read_ue(b, "PPS: num_ref_idx_l1_default_active_minus1");
   
   pps->weighted_pred_flag = bs_read_u(b, 1, "PPS: weighted_pred_flag");
   pps->weighted_bipred_idc = bs_read_u(b, 2, "PPS: weighted_bipred_idc");
   
   pps->pic_init_qp_minus26 = bs_read_se(b, "PPS: pic_init_qp_minus26");
   pps->pic_init_qs_minus26 = bs_read_se(b, "PPS: pic_init_qs_minus26");
   pps->chroma_qp_index_offset = bs_read_se(b, "PPS: chroma_qp_index_offset");
   
   pps->deblocking_filter_control_present_flag = bs_read_u(b, 1, "PPS: deblocking_filter_control_present_flag");
   pps->constrained_intra_pred_flag = bs_read_u(b, 1, "PPS: constrained_intra_pred_flag");
   pps->redundant_pic_cnt_present_flag = bs_read_u(b, 1, "PPS: redundant_pic_cnt_present_flag");
   
   // 如果有更多rbsp数据
   if (more_rbsp_data(b)) {
       pps->transform_8x8_mode_flag = bs_read_u(b, 1, "PPS: transform_8x8_mode_flag");
       pps->pic_scaling_matrix_present_flag = bs_read_u(b, 1, "PPS: pic_scaling_matrix_present_flag");
       if (pps->pic_scaling_matrix_present_flag) {
           int chroma_format_idc = Sequence_Parameters_Set_Array[pps->seq_parameter_set_id].chroma_format_idc;
           int scalingListCycle = 6 + ((chroma_format_idc != YUV_4_4_4) ? 2 : 6) * pps->transform_8x8_mode_flag;
           for (int i = 0; i < scalingListCycle; i++) {
               pps->pic_scaling_list_present_flag[i] = bs_read_u(b, 1, "PPS: pic_scaling_list_present_flag[]");
               if (pps->pic_scaling_list_present_flag[i]) {
                   if (i < 6) {
                       scaling_list(pps->ScalingList4x4[i], 16, &pps->UseDefaultScalingMatrix4x4Flag[i], b);
                   }else {
                       scaling_list(pps->ScalingList8x8[i-6], 64, &pps->UseDefaultScalingMatrix8x8Flag[i], b);
                   }
               }
           }
       }
       pps->second_chroma_qp_index_offset = bs_read_se(b, "PPS: second_chroma_qp_index_offset");
   }else {
       pps->second_chroma_qp_index_offset = pps->chroma_qp_index_offset;
   }
}

注意到其中大部分句法元素的解析,都和协议文档一模一样。只是其中pps->slice_group_id[]的解析,我们需要注意两点:

(1)pps->slice_group_id[I]的解析方式为u(v),其中的v需要动态计算,根据slice_group_id的语义可知v的计算方式为:Ceil( Log2( num_slice_groups_minus1 + 1 ) ),也即对num_slice_groups取以2为底的指数,然后对其指数向上取整。

开始我们已经说过num_slice_groups_minus1的取值范围为[0, 7],因此Ceil( Log2( num_slice_groups_minus1 + 1 ) )的结果无非三种情况:1、2、3。因此我们不需要专门写一个函数调用,在这里直接手撸一遍即可。

(2)第二点就是开始说过的指针pps->slice_group_id的初始化问题了,这里我们直接指定其大小,为刚刚解析到的pic_size_in_map_units

另外,最后一个句法元素second_chroma_qp_index_offset的解析也需注意,因为在h264语法结构表中,只指定了句法元素的解析过程,并没有指定它们的默认值。因此根据其语义,需要在没有解析到的时候,赋予其默认值pps->chroma_qp_index_offset。这种赋默认值的方式,我们在后面解析slice_header时也会很常见。

2.2 save_pps_as_available

此时pps已经解析完毕,我们只需将它保存即可。同样,我们定义一个全局变量Picture_Parameters_Set_Array[],它的大小256,即为pps_id的取值范围[0, 255]。

// 因为pps_id的取值范围为[0,255],因此数组容量最大为256,详见7.4.2.2
pps_t Picture_Parameters_Set_Array[256];

类似sps,我们也需要将pps以pps_id为索引,存入Picture_Parameters_Set_Array[],以便后续slice引用。

void save_pps_as_available(pps_t *pps)
{
   // 2.更新同一个pps_id对应的pps时再释放
   if (Picture_Parameters_Set_Array[pps->pic_parameter_set_id].slice_group_id != NULL) {
       free(Picture_Parameters_Set_Array[pps->pic_parameter_set_id].slice_group_id);
   }
   
   // 0.保存sps
   memcpy (&Picture_Parameters_Set_Array[pps->pic_parameter_set_id], pps, sizeof (pps_t));

   // 1.不释放由pps->slice_group_id指向的内存,交由Picture_Parameters_Set_Array[pps->pps_id]使用
   Picture_Parameters_Set_Array[pps->pic_parameter_set_id].slice_group_id = pps->slice_group_id;
   pps->slice_group_id = NULL;
}

注意到相比sps的保存,这里多了两步,也即注释标注的1、2步。

其中第1步很好理解,这里我们将pps保存到内存,当然指针pps->slice_group_id指向的内存,也应移交给Picture_Parameters_Set_Array[pps->pps_id]使用,因此我们只需将指针pps->slice_group_id置NULL,却并不释放其指向的内存。而在freePPS()中释放slice_group_id时,也需判断slice_group_id是否为NULL。

第2步则是出于长远考虑,如果将来更新了相同pps_id的pps,则那时需要将slice_group_id指向的内存释放。

3、trace文件

解析完成后生成的trace文件如下(只贴pps部分):

Annex B NALU len 5, forbidden_bit 0, nal_reference_idc 3, nal_unit_type 8

@ PPS: pic_parameter_set_id                               (  0)
@ PPS: seq_parameter_set_id                               (  0)
@ PPS: entropy_coding_mode_flag                           (  0)
@ PPS: bottom_field_pic_order_in_frame_present_flag       (  0)
@ PPS: num_slice_groups_minus1                            (  0)
@ PPS: num_ref_idx_l0_default_active_minus1               (  9)
@ PPS: num_ref_idx_l1_default_active_minus1               (  9)
@ PPS: weighted_pred_flag                                 (  0)
@ PPS: weighted_bipred_idc                                (  0)
@ PPS: pic_init_qp_minus26                                (  0)
@ PPS: pic_init_qs_minus26                                (  0)
@ PPS: chroma_qp_index_offset                             (  0)
@ PPS: deblocking_filter_control_present_flag             (  0)
@ PPS: constrained_intra_pred_flag                        (  0)
@ PPS: redundant_pic_cnt_present_flag                     (  0)

本文源码地址如下(H264Analysis_05中):

1、GitHub:https://github.com/Gosivn/H264Analysis

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值