H264編碼原理以及I幀、B和P幀詳解, H264碼流結構分析


H264碼流結構分析

http://blog.csdn.net/chenchong_219/article/details/37990541

1、碼流總體結構:

h264的功能分為兩層,視頻編碼層(VCL)和網絡提取層(NAL)。H.264 的編碼視頻序列包括一系列的NAL 單元,每個NAL 單元包含一個RBSP。一個原始的H.264 NALU 單元常由 [StartCode] [NALU Header] [NALU Payload] 三部分組成,其中 Start Code 用於標示這是一個NALU 單元的開始,必須是"00 00 00 01" 或"00 00 01"。

 

 

其中RBPS有分為幾種類型:

 

 

NAL的解碼單元的流程如下:

 

 

2、 NAL Header:

占一個字節,由三部分組成forbidden_bit(1bit),nal_reference_bit(2bits)(優先級),nal_unit_type(5bits)(類型)。

forbidden_bit:禁止位。

nal_reference_bit:當前NAL的優先級,值越大,該NAL越重要。

nal_unit_type :NAL類型。參見下表

 

幾個例子:

 

 

3、 ffmpeg解析H264流程分析

這是一段實際的碼流

 

在上面的圖片中,共有三個起始碼:0x00 0000 01

const uint8_t*ff_h264_decode_nal(H264Context*h, const uint8_t *src,int *dst_length, int*consumed, int length)中分析過程為:

h->nal_ref_idc= src[0] >> 5;

h->nal_unit_type= src[0] & 0x1F;

此處src[0]即為06,寫成二進制位0000 0110,則h->nal_ref_idc = 0,h->nal_unit_type = 6

可以判斷這個NALU類型為SEI,重要性優先級為0。

src++;src向后移動一個位置,此時src指向圖中第一行第五列的數據05

length--;未處理數據長度減1

#defineSTARTCODE_TEST                                                 \

       if(i + 2 < length && src[i + 1] == 0 && src[i + 2]<= 3){     \

           if(src[i + 2] != 3){                                     \

               /* startcode, so we must bepast the end*/             \

               length =i;                                            \

           }                                                          \

           break;                                                     \

       }

   for(i = 0; i + 1 < length; i += 2) {

       if(src[i])

           continue;

       if(i > 0 && src[i - 1] == 0)

           i--;

       STARTCODE_TEST;

   }

上述分析:

        1)如果src[i] !=0 ,則繼續下一次循環,直到src[i] == 0,即等於下一個起始碼的第二個00為止,即圖中地址為000001a0h行的第3列,地址為0x00 00 01 a3(十進制為419),此時i為414,因為是從0x00 00 00 05地址開始的,此時則第一次執行continue下面的if語句,而且src[i-1]==0,則i--,此時i=413

        2)執行宏定義STARTCODE_TEST,進行起始碼檢測:src[i+ 1] =src[414]=0,src[i+2]=src[415]=0;

繼續執行,src[i+2] != 3,這里是進行競爭檢測(前面有講述,如果在編碼的過程中出現連續00 00 00時,將會在第三個00前插入一個03,避免和起始碼造成沖突),既然沒有發生競爭現象,則表明這個確實是下一個NALU的起始碼

        3)length是指當前NALU單元長度,這里不包括nalu頭信息長度,即一個字節,

length = i =413

后面的SEI解析不再贅述

(2)第二個 0x00 00 00 01:

 

h->nal_ref_idc= src[0] >> 5;

h->nal_unit_type= src[0] & 0x1F;

此時src[0]是指起始碼后面第一個數據67(地址為0x00 00 01a6h),寫成二進制為0110 0111,則h->nal_unit_type = 7,h->nal_ref_idc =3

則這一個NALU單元類型為SPS

下面開始分析SPS的長度,這一段數據很巧,包含了競爭檢測

依然是下面這段代碼:

src++;src向后移動一個位置,此時src指向圖中地址為00 0001 a7h的數據

length--;未處理數據長度減1

#defineSTARTCODE_TEST                                                 \

       if(i + 2 < length && src[i + 1] == 0 && src[i + 2]<= 3){     \

           if(src[i + 2] != 3){                                     \

               /* startcode, so we must bepast the end*/             \

               length =i;                                            \

           }                                                          \

           break;                                                     \

       }

   for(i = 0; i + 1 < length; i += 2) {

       if(src[i])

           continue;

       if(i > 0 && src[i - 1] == 0)

           i--;

       STARTCODE_TEST;

   }

分析:

1)自地址0x00 00 01 a7h開始查找src[i] ==0,此時src[i]的地址為0x00 00 01 b1h,此時src[i]==0,而src[i-1]=80 != 0,此時i = 10 ;

2)執行宏定義STARTCODE_TEST:if(i+2 <lengthn&&src[i+1]==0&&src[i+2]<=3)其中src[i+1]=src[11]=0,且src[i+2]=src[12]=3,繼續執行,if(src[i+2] !=3),顯然不滿足,執行break,跳出循環,此時 i=10

此時尚不能確定下一個nalu單元的起始碼在何處,因此當前nalu單元的長度也是未定的,

 

bufidx =h->nal_unit_type == NAL_DPC ? 1: 0;bufidx=0

si =h->rbsp_buffer_size[bufidx];

av_fast_padded_malloc(&h->rbsp_buffer[bufidx],&h->rbsp_buffer_size[bufidx],length+MAX_MBPAIR_SIZE);

dst =h->rbsp_buffer[bufidx];

以上是為當前NALU分配緩存,並清零

memcpy(dst,src, i);將當前確定的nalu內容拷貝到dst緩存中

si = di = i;目的緩存長度和源緩存長度=i=10

while (si + 2< length) {

       //remove escapes (very rare 1:2^22)

      if(src[si + 2] > 3) {

           dst[di++]= src[si++];

           dst[di++]= src[si++];

       }else if (src[si] == 0 && src[si + 1] == 0) {

           if(src[si + 2] == 3) { // escape

               dst[di++]  = 0;

               dst[di++]  = 0;

               si        += 3;

               continue;

           }else // next start code

               goto nsc;

       }

       dst[di++]= src[si++];

   }

nsc:

   memset(dst+ di, 0, FF_INPUT_BUFFER_PADDING_SIZE);

   *dst_length= di;

   *consumed  = si + 1; // +1 forthe header

   /*FIXME store exact number of bits in the getbitcontext

    *(it is needed for decoding) */

   returndst;

分析:src[0]=4D位於0x00 00 01a7h,src[10]=0位於0x00 00 01b1h后面的地址不再贅述,依次為起始位置

從圖中可以看出:

 

src[10]=00,src[11]=00,src[12]=03,src[13]=00,src[14]=80,src[15]=00,src[16]=00,src[17]=19,src[18]=47,src[19]=8C,src[20]=19,src[21]=50,src[22]=00,src[23]=00,src[24]=00

1)進入while循環,

 

此時si=10:

src[si+2] =src[12]=03且src[11]=00,src[10]=00,則執行:

dst[di++]=0:即dst[10]=0,di=11,dst[11]=0,di=12;

si+=3:si=13,然后continue,開始下一次循環

注:此處的03即為競爭檢測,最后將03跳過了

 

此時si=13:

src[15]=00,src[13]=00,src[14]=80,則執行

dst[di++]=src[si++];

即:dst[12]=src[13]=00,di=13,si=14;

 

此時si=14:

src[16]=00,src[15]=00,src[14]=80

則執行dst[di++]=src[si++];

即:dst[13]=src[14]=00,di=14,si=15

 

此時si=15:

src[15]=00,src[16]=00,src[17]=19

則執行:dst[di++] = src[si++];

        dst[di++]= src[si++];

                  dst[di++]= src[si++]

即:dst[14]=src[15]=00,di=15,si=16;

   dst[15]=src[16]=00,di=16,si=17;

   dst[16]=src[17]=19,di=17,si=18;

此時si=18:

src[18]=47,src[19]=8C,src[20]=19

執行:

   dst[di++] = src[si++];

    dst[di++]= src[si++];

dst[di++] =src[si++]

即:dst[17]=src[18]=47,di=18,si=19

   dst[18]=src[19]=8C,di=19,si=20

   dst[19]=src[20]=19,di=20,si=21

 

此時si=21:

src[21]=50,src[22]=00,src[23]=00

執行:

        dst[di++]= src[si++];

即dst[20]=src[21]=50,di=21,si=22

 

 

此時si=22:

src[22]=00,src[23]=00,src[24]=00

執行:

          goto nsc;

 

那就看一下nsc:

nsc:

        memset(dst+ di, 0, FF_INPUT_BUFFER_PADDING_SIZE);

        *dst_length= di;

   *consumed  = si + 1; // +1 forthe header

   /*FIXME store exact number of bits in the getbitcontext

    *(it is needed for decoding) */

   returndst;

此時di=21,si=22

memset()函數是向SPS單元尾部補零

dst_length=di=21,;即SPS單元的RBSP長度為21,不包括起始碼

consumed=si+1=23;表示SPS單元共消耗的字節數,加1表示SPS單元信息頭占一個字節,注意consumed比dst_length大1,這是由於競爭檢測時,將編碼時添加的一個字節的03過濾掉造成的。

 

 

H264編碼原理以及I幀、B和P幀詳解

 

 H264是新一代的編碼標准,以高壓縮高質量和支持多種網絡的流媒體傳輸著稱,在編碼方面,我理解的他的理論依據是:參照一段時間內圖像的統計結果表明,在相鄰幾幅圖像畫面中,一般有差別的像素只有10%以內的點,亮度差值變化不超過2%,而色度差值的變化只有1%以內。所以對於一段變化不大圖像畫面,我們可以先編碼出一個完整的圖像幀A,隨后的B幀就不編碼全部圖像,只寫入與A幀的差別,這樣B幀的大小就只有完整幀的1/10或更小!B幀之后的C幀如果變化不大,我們可以繼續以參考B的方式編碼C幀,這樣循環下去。這段圖像我們稱為一個序列(序列就是有相同特點的一段數據),當某個圖像與之前的圖像變化很大,無法參考前面的幀來生成,那我們就結束上一個序列,開始下一段序列,也就是對這個圖像生成一個完整幀A1,隨后的圖像就參考A1生成,只寫入與A1的差別內容。
    在H264協議里定義了三種幀,完整編碼的幀叫I幀,參考之前的I幀生成的只包含差異部分編碼的幀叫P幀,還有一種參考前后的幀編碼的幀叫B幀。
   H264采用的核心算法是幀內壓縮和幀間壓縮,幀內壓縮是生成I幀的算法,幀間壓縮是生成B幀和P幀的算法。

 

序列的說明
    在H264中圖像以序列為單位進行組織,一個序列是一段圖像編碼后的數據流,以I幀開始,到下一個I幀結束。
    一個序列的第一個圖像叫做 IDR 圖像(立即刷新圖像),IDR 圖像都是 I 幀圖像。H.264 引入 IDR 圖像是為了解碼的重同步,當解碼器解碼到 IDR 圖像時,立即將參考幀隊列清空,將已解碼的數據全部輸出或拋棄,重新查找參數集,開始一個新的序列。這樣,如果前一個序列出現重大錯誤,在這里可以獲得重新同步的機會。IDR圖像之后的圖像永遠不會使用IDR之前的圖像的數據來解碼。
        一個序列就是一段內容差異不太大的圖像編碼后生成的一串數據流。當運動變化比較少時,一個序列可以很長,因為運動變化少就代表圖像畫面的內容變動很小,所以就可以編一個I幀,然后一直P幀、B幀了。當運動變化多時,可能一個序列就比較短了,比如就包含一個I幀和3、4個P幀。

 

三種幀的說明

1、I幀
I幀:幀內編碼幀 ,I幀表示關鍵幀,你可以理解為這一幀畫面的完整保留;解碼時只需要本幀數據就可以完成(因為包含完整畫面)
I幀特點:
1)它是一個全幀壓縮編碼幀。它將全幀圖像信息進行JPEG壓縮編碼及傳輸;
2)解碼時僅用I幀的數據就可重構完整圖像;
3)I幀描述了圖像背景和運動主體的詳情;
4)I幀不需要參考其他畫面而生成;
5)I幀是P幀和B幀的參考幀(其質量直接影響到同組中以后各幀的質量);
6)I幀是幀組GOP的基礎幀(第一幀),在一組中只有一個I幀;
7)I幀不需要考慮運動矢量;
8)I幀所占數據的信息量比較大。

2、P幀 

P幀:前向預測編碼幀。P幀表示的是這一幀跟之前的一個關鍵幀(或P幀)的差別,解碼時需要用之前緩存的畫面疊加上本幀定義的差別,生成最終畫面。(也就是差別幀,P幀沒有完整畫面數據,只有與前一幀的畫面差別的數據)
P幀的預測與重構:P幀是以I幀為參考幀,在I幀中找出P幀“某點”的預測值和運動矢量,取預測差值和運動矢量一起傳送。在接收端根據運動矢量從I幀中找出P幀“某點”的預測值並與差值相加以得到P幀“某點”樣值,從而可得到完整的P幀。
P幀特點:
1)P幀是I幀后面相隔1~2幀的編碼幀;
2)P幀采用運動補償的方法傳送它與前面的I或P幀的差值及運動矢量(預測誤差);
3)解碼時必須將I幀中的預測值與預測誤差求和后才能重構完整的P幀圖像;
4)P幀屬於前向預測的幀間編碼。它只參考前面最靠近它的I幀或P幀;
5)P幀可以是其后面P幀的參考幀,也可以是其前后的B幀的參考幀;
6)由於P幀是參考幀,它可能造成解碼錯誤的擴散;
7)由於是差值傳送,P幀的壓縮比較高。

3、B幀

B幀:雙向預測內插編碼幀。B幀是雙向差別幀,也就是B幀記錄的是本幀與前后幀的差別(具體比較復雜,有4種情況,但我這樣說簡單些),換言之,要解碼B幀,不僅要取得之前的緩存畫面,還要解碼之后的畫面,通過前后畫面的與本幀數據的疊加取得最終的畫面。B幀壓縮率高,但是解碼時CPU會比較累。
B幀的預測與重構
B幀以前面的I或P幀和后面的P幀為參考幀,“找出”B幀“某點”的預測值和兩個運動矢量,並取預測差值和運動矢量傳送。接收端根據運動矢量在兩個參考幀中“找出(算出)”預測值並與差值求和,得到B幀“某點”樣值,從而可得到完整的B幀。
B幀特點
1)B幀是由前面的I或P幀和后面的P幀來進行預測的;
2)B幀傳送的是它與前面的I或P幀和后面的P幀之間的預測誤差及運動矢量;
3)B幀是雙向預測編碼幀;
4)B幀壓縮比最高,因為它只反映丙參考幀間運動主體的變化情況,預測比較准確;
5)B幀不是參考幀,不會造成解碼錯誤的擴散。

注:I、B、P各幀是根據壓縮算法的需要,是人為定義的,它們都是實實在在的物理幀。一般來說,I幀的壓縮率是7(跟JPG差不多),P幀是20,B幀可以達到50。可見使用B幀能節省大量空間,節省出來的空間可以用來保存多一些I幀,這樣在相同碼率下,可以提供更好的畫質。

 

壓縮算法的說明
h264的壓縮方法:
1.分組:把幾幀圖像分為一組(GOP,也就是一個序列),為防止運動變化,幀數不宜取多。
2.定義幀:將每組內各幀圖像定義為三種類型,即I幀、B幀和P幀;
3.預測幀:以I幀做為基礎幀,以I幀預測P幀,再由I幀和P幀預測B幀;
4.數據傳輸:最后將I幀數據與預測的差值信息進行存儲和傳輸。
    幀內(Intraframe)壓縮也稱為空間壓縮(Spatial compression)。當壓縮一幀圖像時,僅考慮本幀的數據而不考慮相鄰幀之間的冗余信息,這實際上與靜態圖像壓縮類似。幀內一般采用有損壓縮算法,由於幀內壓縮是編碼一個完整的圖像,所以可以獨立的解碼、顯示。幀內壓縮一般達不到很高的壓縮,跟編碼jpeg差不多。  
    幀間(Interframe)壓縮的原理是:相鄰幾幀的數據有很大的相關性,或者說前后兩幀信息變化很小的特點。也即連續的視頻其相鄰幀之間具有冗余信息,根據這一特性,壓縮相鄰幀之間的冗余量就可以進一步提高壓縮量,減小壓縮比。幀間壓縮也稱為時間壓縮(Temporal compression),它通過比較時間軸上不同幀之間的數據進行壓縮。幀間壓縮一般是無損的。幀差值(Frame differencing)算法是一種典型的時間壓縮法,它通過比較本幀與相鄰幀之間的差異,僅記錄本幀與其相鄰幀的差值,這樣可以大大減少數據量。
       順便說下有損(Lossy )壓縮和無損(Lossy less)壓縮。無損壓縮也即壓縮前和解壓縮后的數據完全一致。多數的無損壓縮都采用RLE行程編碼算法。有損壓縮意味着解壓縮后的數據與壓縮前的數據不一致。在壓縮的過程中要丟失一些人眼和人耳所不敏感的圖像或音頻信息,而且丟失的信息不可恢復。幾乎所有高壓縮的算法都采用有損壓縮,這樣才能達到低數據率的目標。丟失的數據率與壓縮比有關,壓縮比越小,丟失的數據越多,解壓縮后的效果一般越差。此外,某些有損壓縮算法采用多次重復壓縮的方式,這樣還會引起額外的數據丟失。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM