許雪嬌 江蘇省廣播電視集團有限公司
隨著視頻技術(shù)的不斷發(fā)展,人們對視頻圖像質(zhì)量的需求越來越高,數(shù)字視頻傳輸對帶寬和存儲的要求越來越高。這不但提高了對信息的傳送、傳播和存儲速度的要求,更是對視頻圖像壓縮性能的考驗。數(shù)字視頻的核心編碼標準在不斷地更新,國外視頻壓縮編碼的格式主要由二大組織制定。ITU-T視頻編碼專家組制定了H.26X,ISO/IEC運動圖像專家組制定了MPEG-X。HEVC/H.265是由以上兩大組織聯(lián)合制定的新一代高效視頻編碼標準,內(nèi)含先進的編碼技術(shù)及并行處理能力,在相同編碼質(zhì)量條件下,比H.264/AVC能夠節(jié)約近50%左右的碼流利用率。
本文設(shè)計并完成了對H.265編碼視頻文件的深度分析。包括視頻基本信息(圖像長寬、采樣方式、亮度和色度的采樣深度、幀數(shù)、層級信息等)、每個NALU基本信息(位置、字節(jié)長度、NAL類型、SLICE類型)和RBSP內(nèi)容的詳細解析(H.265標準文檔目前定義了41個nal_unit_type的類型,包括VPS、SPS、PPS、SEI、AUD 等)。
在H.265編碼格式中,視頻文件被分為一個個NAL,為復(fù)雜的視頻數(shù)據(jù)增加友好的網(wǎng)絡(luò)接口。視頻壓縮數(shù)據(jù)根據(jù)其內(nèi)容特性被分成具有不同特性的NAL單元(NALU),并對NALU的內(nèi)容特性進行標識,即所有的壓縮視頻數(shù)據(jù)都被封裝到不同NALU的荷載部分。NALU除了承載VPS、SPS、PPS等信息,主要承載視頻片(Slice)的壓縮數(shù)據(jù)。每個NALU又分為NAL頭和NAL荷載。
表1 H.265的NAL單元頭結(jié)構(gòu)
H.265的NAL單元頭(nal unit header)與H.264的單元頭不同,H.264只有一個字節(jié),而H.265的碼流開頭是00000001,后面跟兩個字節(jié)的nal_unit_header,H.265的NAL單元頭結(jié)構(gòu)如表1所示。
NAL頭由固定的4部分構(gòu)成,分別是forbidden_zero_bit,nal_unit_type, nuh_layer_id 和 nuh_temporal_id_plus1,各自占用 1,6,6,3個比特。其中nal_unit_type占6個比特,意味著它的取值從0-63,每個編號代表不同的NALU類型,如32代表視頻參數(shù)集VPS,35代表定界AUD。
在H.265編碼文件中,各個不同類型的NALU具有不同的NAL荷載,各NALU的類型由NAL頭中的nal_unit_type決定。根據(jù)nal_unit_type的不同,可分為:VPS,SPS,PPS,SEI,AUD,EOS,EOB等。視頻在編碼過程中輸出的包含不同內(nèi)容的壓縮數(shù)據(jù)比特流片段被稱為SODB。事實上,SODB指的是RBSP中的有效成分,去掉了RBSP結(jié)尾中為了取整的數(shù)字零。RBSP中可以包含一個SS的壓縮數(shù)據(jù)、VPS、SP、PPS以及補充增強信息等。
在H.265編碼視頻的碼流中,每一個NALU以0x000001作為起始碼,以0x000000作為結(jié)束碼。這就帶來了一個問題,在NAL荷載中一旦出現(xiàn)上述的字節(jié)流,就會造成沖突,使一幀意外結(jié)束。為了解決這個問題,H.265編碼規(guī)定將所有RBSP中非起始、結(jié)束碼的,會產(chǎn)生沖突的比特流作如下處理:
其中,0x000002為預(yù)留碼。需要注意的是,當RBSP數(shù)據(jù)的最后一個字節(jié)為0x00時,在數(shù)據(jù)結(jié)尾會加入0x03。
本H.265碼流分析軟件流程框圖見圖1。根據(jù)文件編碼格式判斷文件類型,對于判斷類型為H.265的視頻文件,由幀頭的起始碼來決定如何將視頻文件按幀分片,在一幀的數(shù)據(jù)中先讀取該幀中nal_unit_type的值,根據(jù)取值判斷該幀所屬的類型。然后對提取出的幀數(shù)據(jù)進行處理,按照幀的類型分別將此幀中的各個語法元素提取并分類存儲,包括在視頻參數(shù)集(VPS)中提取檔次和級別,在序列參數(shù)集(SPS)中提取圖像格式,量化信息,在片段(SS)中提取幀類型等等,所有幀都依此進行處理并保存生成的語法元素樹。
圖1 H.265碼流分析軟件流程框圖
圖2 視頻文件結(jié)構(gòu)和對應(yīng)的程序框圖
H.265視頻文件結(jié)構(gòu)和對應(yīng)的程序如圖2所示。H.265編碼的視頻碼流將壓縮視頻數(shù)據(jù)封裝成不同類型的NALU,即所有的壓縮視頻數(shù)據(jù)都被封裝到不同NALU的荷載部分。在界面層獲取碼流信息時,先要將其分割成一個個NALU。對于每一個被分割的NALU而言,要分為三部分進行處理。首先是NAL頭部分(NAL_Header),需要判斷該NAL的類型,序號,參考幀等等。類型信息在每幀的nalType參數(shù)中。其次,要根據(jù)NAL頭中判斷出的類型,對NAL荷載進行處理,將RBPS數(shù)據(jù)讀取進來以后,根據(jù)各自的編碼特點,用類BitReader進行處理。最后,根據(jù)前兩步處理好的數(shù)據(jù),用TreeMaker類來生成每個NALU的樹形圖。
程序中依照實現(xiàn)的功能主要分為Stream處理,NalUnit處理,Bit處理和TreeNode生成這幾大模塊,程序模塊劃分如圖3所示。Stream類負責初步處理一個視頻文件,生成視頻信息并掃描所有NALU信息。
首先對STREAM進行初始化,將文件所有內(nèi)容讀取至_byteFile字節(jié)數(shù)組內(nèi),提取讀到的數(shù)據(jù)中第一個NAL的第一個字節(jié),由前文的幀結(jié)構(gòu)可以得知,每個NAL的第一個字節(jié)的第1-7個比特中含有該文件的類型信息,如果nalType屬于在0和47之間的任意整數(shù),則判斷類型為H.265格式。判斷完文件類型后返回,若文件類型錯誤,軟件將給出WARNING警告。以上功能由JudgeVideoFile類實現(xiàn)。所有數(shù)據(jù)讀入數(shù)組后,將獲取所有NALU并生成NALU對象。具體流程是先將指針pos復(fù)位,用循環(huán)讀取持續(xù)_byteFile,邊讀邊判斷每個NAL的起始字節(jié),如果讀到01則查看01與它之前是否滿足0001或001,變量out用于返回這個NAL的起始字節(jié)數(shù)。以上功能由FindNextNal類實現(xiàn)。
圖3 程序模塊劃分
Stream處理的流程方法如圖4所示。ScanNalu()函數(shù)主要負責掃描_byteFile并分析其中所有NAL。首先定義新指針pos,將其指向第一個NAL的起始位置。如果指針位置為負數(shù),則返回false,找不到NAL。用循環(huán)搜索下一個NAL,每搜索到一個新的NAL,就新建一個NALU對象,將指針數(shù)組指向該NAL的開頭位置,用讀出的下一個NAL的開頭位置減去這一個NALU的開始位置,計算出這個NALU的長度。記錄下長度及NAL的內(nèi)容傳遞給NAL對象的_byteNALU變量,將此NAL加入_lstNALU中,最后指針指向下一個NAL,循環(huán)直到處理完成所有的碼流。當所有NAL都讀取完畢后,此時可以將stream信息生成文字內(nèi)容以備輸出界面調(diào)用。
Nalu處理(NALU類)包括NAL Header解析和rbsp解析兩部分功能,其中rbsp解析功能根據(jù)header中的type類型,調(diào)用不同的模塊進行解析(VPS,SPS,PPS,SEI,SS等)。
圖4 Stream處理流程圖
在對NALU解析時,要先進行準備工作,首先對NALU類進行定義。在每個NALU類中含有基礎(chǔ)信息包括序號,長度,偏移,文本說明,start code的長度;碼流信息包括存放NAL的字節(jié)數(shù)組,RBSP字節(jié)數(shù)組;文本格式的NAL字節(jié)顯示等等。
將NALU進行初始化,新建一個BitReader的對象_bitNALU,將NAL的字節(jié)數(shù)組傳入,指針指向第一個比特,為防止顯示界面雜亂,如果此NALU超過5000字節(jié),則只需將前5000字節(jié)放在待顯示的位置,接著處理NAL頭的數(shù)據(jù),此處調(diào)用BitStream中已經(jīng)編寫好的函數(shù)即可。為了避免幀中的數(shù)據(jù)和起始碼形成沖突,每個NAL中的RBSP都做了預(yù)處理,在讀取數(shù)據(jù)中的語法元素時,需要先將其恢復(fù)。
上述處理方法在NaluToRbsp程序段中有所體現(xiàn),從byteNALU的起始位之后開始檢測,count用來記錄0x00連續(xù)出現(xiàn)的個數(shù),每當讀取到0x03則判斷,不允許出現(xiàn)00000102這樣的組合,且03之后的一位不應(yīng)該大于03。如果判斷出沖突則將其記錄并更改,否則跳過這個0x03位。這樣,在將NALU轉(zhuǎn)換成RBSP的同時也檢測出了這些“競爭機制”并進行還原,以方便接下來的操作。
對nalType進行判斷,調(diào)用BitStream中對應(yīng)的解析方法處理數(shù)據(jù)。此處以VPS為例,如果CASE語句判斷出該NAL的NAL_UNIT_TYPE為32,對應(yīng)的類型就是視頻參數(shù)集VPS,程序轉(zhuǎn)到處理VPS的函數(shù)處繼續(xù)運行。
具體類型讀取方法,以讀取一個普通的SLICE為例,參考H.265標準文件《T-REC-H.265-201504-I!!PDF-E》中第7.3.6.1 節(jié)General slice segment header syntax中對碼流格式的定義,圖5說明了讀取數(shù)據(jù)的流程。
讀取一個新的SS時,首先要將指針POS復(fù)位,指向第0個比特位。首先根據(jù)H.265編碼定義表判斷第一個語法元素有沒有前置判斷因素,是否由于前面的FLAG標識而導致這個元素不存在,如果該元素存在,就根據(jù)編碼方法選擇讀取方式。如果是固定比特位,就調(diào)用BitReader中的函數(shù)ReadU1()或者ReadU3()等等;如果是不固定比特位數(shù),則分為兩種情況,無符號零階熵編碼調(diào)用BitReader中的函數(shù)ReadUE(),有符號零階熵編碼調(diào)用BitReader中的函數(shù)ReadSE()。該語法元素讀取完畢,指針POS移動至該語法元素末尾,如果指針已經(jīng)移動到整個片段SS的末尾,即將整個片段SS讀取完畢,如果是,則整個流程結(jié)束,如果沒有讀取完畢,則繼續(xù)循環(huán)讀取下一個語法元素,直到讀取完畢為止。
在讀取過程中,需要注意片段中slice_type表達的片段的類型,slice_type類型對照如表2所示。
由于H.265提升了編碼的效率,同樣的信息可以引用前面已經(jīng)表述的內(nèi)容,不用重復(fù)編碼,所以,大多數(shù)幀都有對前面幀的引用。在分析當前SS的程序中,要注意SS引用PPS的ID號和從PPS可找到引用的SPS的ID號。在編碼中引用的內(nèi)容被省略了,但是,程序在翻譯編碼時要將其翻譯出來。
BitReader類中包括字節(jié)流-比特流轉(zhuǎn)換模塊和比特流信息讀取模塊兩部分功能。這個類更多的是提供輔助性的功能,實現(xiàn)了byte字節(jié)數(shù)組轉(zhuǎn)為bit數(shù)組的方法,將所有碼流由十六進制轉(zhuǎn)換為二進制數(shù)據(jù),為其他部分的讀取數(shù)據(jù)提供方便。
同時,由于在H.265視頻編碼的規(guī)則中,每個語法元素并不是以字節(jié)為單位進行表達,而是以非整數(shù)字節(jié)的零散比特位為單位。所以,在BitReader類中,還提供了一些以比特為單位讀取數(shù)據(jù)的函數(shù),如ReadU()/ReadU1()/ReadUE()/ReadSE()等方法調(diào)用。在H.265視頻編碼的規(guī)則中,使用了一些不定比特數(shù)的語法元素,它們被稱為零階指數(shù)哥倫布編碼,讀取這類語法元素時,必須根據(jù)編碼特點,單獨用函數(shù)讀取。
在H.265視頻編碼標準中,由于語法元素是層層歸屬的,在表現(xiàn)形式上,比起常規(guī)的表格或是圖片,更傾向于展現(xiàn)語法元素的歸屬關(guān)系。而樹形結(jié)構(gòu)是典型的層次的嵌套結(jié)構(gòu),一個樹形結(jié)構(gòu)的外層和內(nèi)層有相似的結(jié)構(gòu),在編碼中更是能以遞歸的形式展現(xiàn),所以本軟件采用了樹型結(jié)構(gòu)來表達語法元素的歸屬關(guān)系。
圖5 讀取數(shù)據(jù)流程圖
表2 slice_type類型對照表
TreeMaker類包括了一個公有方法MakeTree()和多個nodeMaker模塊,可根據(jù)傳入的NALU對象來生成對應(yīng)的TreeView節(jié)點用以直接在界面中顯示。在TreeMaker類中,只要將tree調(diào)用,即可實現(xiàn)軟件右下角的展現(xiàn)每幀的語法元素的功能。
最后在用戶界面中,主要實現(xiàn)了將H.265視頻文件讀入,顯示NAL列表的信息,以十六進制顯示原始碼流,顯示STREAM流基本信息和以樹形結(jié)構(gòu)圖顯示任一NAL的語法元素分布這5種功能。軟件編輯界面示意圖如圖6所示。
在程序運行和軟件進行交互的過程中,為了完成用戶發(fā)出的指令,常常不可避免地需要從一個線程中調(diào)用另一個線程,而這種操作在C#中是絕不允許的,因為C#的編程邏輯認為這樣操作會出現(xiàn)循環(huán)調(diào)用,一旦程序的邏輯出現(xiàn)問題,就將陷入死循環(huán)。所以,軟件采用了一種特殊的邏輯。以TreeView界面的更新為例,當軟件操作觸發(fā)了TreeView的調(diào)用時,首先判斷這是否為界面線程的調(diào)用,當一個控件的InvokeRequired屬性值為真時,說明有一個創(chuàng)建它以外的線程想訪問,如果判斷為真的話,就新開辟一個線程,再調(diào)用一次TreeView的更新函數(shù),這時,由于之前已經(jīng)開辟過一個新的線程,不會產(chǎn)生沖突,所以程序會轉(zhuǎn)到TreeView真正的更新程序上運行。程序?qū)崿F(xiàn)的方法如圖7所示。
圖6 軟件編輯界面示意圖
圖7 調(diào)用新界面線程
其他更新界面進行顯示的程序中,使用的方法是類似的,這里不做贅述。
軟件完成后,用20多個不同大小的文件對軟件進行了測試,文件格式有hm10,h265和bin兩種,都是官方指定的H.265格式文件。文件大小從十幾K到幾兆不等。
在測試到suzie_qcif.h265文件,點選第4幀NAL_UNIT_PREFIX_SEI,生成該幀語法元素樹狀圖時,程序沒有響應(yīng)。推測該處程序陷入死循環(huán)中,在TreeMakeer.cs的C#程序段中,找到了出錯的程序。在程序進行到制作SEI的樹狀圖時,轉(zhuǎn)到了函數(shù)MakeSEINode中,首先添加了sei_payload這個語法元素,對payloadType進行判斷,不同的payloadType值會添加不同的數(shù)節(jié)點。但針對出錯的這個視頻文件來說,第四幀中的payloadType取值為5。原程序中只是在樹中添加了user_data_unregistered()節(jié)點,將程序擴充并修改后,添加了2個三層的子節(jié)點,使得樹狀圖中此節(jié)點可以被展開,從而消除錯誤。
最后經(jīng)核對,修改過錯誤的本軟件和商業(yè)化的H.265分析軟件Elecard HEVC Analyzer得出的結(jié)論完全相同,所以,本軟件均成功通過樣例測試。
本文提供了H.265編碼方式的詳細分析和基本設(shè)計思路。根據(jù)本文提供的思路和方法,可以完成一個清晰簡潔的H.265分析軟件,得到視頻文件的幀基本信息、語法元素信息和STREAM流信息,并將幀的樹狀結(jié)構(gòu)圖與原碼流信息進行對比查看,為研究H.265編碼方法提供了方便。
參考文獻:
[1]萬帥,楊付正.新一代高效視頻編碼 H.265/HEVC:原理、標準與實現(xiàn)[M].北京:電子工業(yè)出版社.2014:22-94,230-237,269-291.
[2]H.265標準文檔《T-REC-H.265-201504-I!!PDF-E》