【從零實現一個H.264碼流解析器】(五):解析圖像參數集PPS的句法元素

原文鏈接:https://mp.weixin.qq.com/s/GXkq88XrHW8Pug-376T5BA

前面我們解析了序列參數集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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章