1. 影像壓縮概述
在介紹 HEVC 影像壓縮之前,先回顧一下影像壓縮技術的發展路線。
- 獨立幀 (I幀) :採用幀內編碼 (intra-frame coding) ,不依賴於其它任何幀。
- 非獨立幀 (P幀或B幀) :採用幀間編碼 (inter-frame coding) ,依賴於其它幀的重建影像。
1.1 畫素座標系
影像可以表示為畫素的二維陣列,每個畫素的位置用 (𝑦,𝑥) 座標表示 (如下圖) 。其中 𝑦∈[0,height−1] ; 𝑥∈[0,width−1] 。 height 和 width 分別為影像高度和寬度。
影像的座標編號在程式語言中,可以用二維陣列儲存一張影像,其中 img[y][x]
代表座標 (𝑦,𝑥) 處的一個畫素。
也可以用一維陣列儲存一張影像,按照從左到右,從上到下掃描的順序存放畫素,因此 img[width*y+x]
代表座標 (𝑦,𝑥) 處的一個畫素。
1.2 RGB 與 YUV
每個畫素有很多種表示方式,常用的有 RGB 和 YUV (YCbCr):
- RGB: 用三個數字 (分量) 來分別表示紅色的強度(R)、綠色的強度(G)和藍色的強度(B);
- YUV: 用三個數字 (分量) Y, U, V 來表示一個畫素,其中 Y 是亮度 (luminance),U 是藍色度 (chroma-blue, Cb),V 是紅色度 (chroma-red, Cr) 。
大多數影像用 8-bit 無符號數表示一個分量,取值範圍為 0~255 。因此,一個 RGB 畫素或 YUV 畫素都佔 3×8=24bit 。
以下公式可以把 RGB 轉化為 YUV (ITU-R BT.2020標準)。其中 round() 函式的功能是四捨五入到整數。
RGB轉YUV (ITU-R BT.2020標準)
影像感測器產生的畫素一般是 RGB。但計算機經常把它轉化為 YUV 格式來壓縮和儲存 (包括JPEG、WEBP、HEVC等壓縮格式)。現實中的影像的 U, V 兩個分量往往比較平滑,使得 YUV 格式相比於 RGB 格式更適合進行壓縮。
常見的 YUV 格式還會對 U, V 分量進行下采樣,例如最常用的下采樣方式是 YUV4:2:0 ,它會將 U, V 分量在橫向和縱向都下采樣兩倍,使得 U, V 的資料量都只有 Y 的資料量的四分之一,因此 YUV:4:2:0 的總資料量相比不進行下采樣的 YUV4:4:4 小 50%。
當 YUV 中的 U, V 分量都為畫素中值 (128) 時,影像是灰色的,此時 Y 值決定了畫素的亮度。
1.2 基礎的影像壓縮 (例如JPEG)
基礎的影像壓縮原理如圖1,步驟包括分塊、變換、量化和熵編碼。
圖1:基礎的影像編碼 (例如JPEG) : 編碼端圖2展示了一個例子:
- 分塊:待壓縮的影像被劃分成正方形的畫素塊,然後按從左到右,從上到下的順序遍歷每個塊 (JPEG 規定塊大小為 8x8 個畫素,但圖2中我們以 16x16 舉例) 。
- 減去中值:對於一個塊,每個畫素的取值範圍為 0~255 ,減去畫素中值 128 後,取值範圍變為 -128~127 。
- 變換:對它進行 DCT (離散餘弦變換),也即左乘和右乘一個變換矩陣,得到一個同樣大小的係數塊 (coefficient block) ,其中每個係數的絕對值代表了某個頻率下的能量。由於現實中影像的能量集中在低頻,在係數塊中,越遠離左上角,係數的絕對值一般越小。
- 量化:係數塊除以一個量化值或量化矩陣,然後四捨五入為整數。大部分遠離左上角的小系數會被量化為 0,1,2,-1,-2,... 這樣的適合壓縮的小數字。量化會導致失真,量化時除以的數越大,則壓縮率越高,但失真也越大。
- 熵編碼:使用遊程編碼 (RLC) 和變長編碼 (VLC) 編碼量化後的係數塊。其中 RLC 會跳過 0 值,只編碼連續出現的 0 的個數。而 VLC 給越小的數分配越短的編碼。由於小數字的出現機率大於大數字出現的機率,因此 VLC 能夠獲得壓縮效果。
上述流程是編碼端的步驟。解碼端需要反過來執行 (如圖3),也即解碼→反量化→反變換→加上中值128→將得到的畫素塊放入影像。
圖3:基礎的影像編碼 (例如JPEG) : 解碼端1.3 基於預測 (prediction) 的影像壓縮
基礎的影像壓縮方法 (圖1) 對每個塊進行獨立的壓縮,沒有利用塊之間的相似性。
而預測技術 (prediction) 會用已編碼的鄰塊去預測當前正在編碼的塊,生成預測塊,用當前的原始畫素塊減去預測塊得到殘差塊,對殘差塊進行變換、量化、編碼,以獲得更好的壓縮效果。JPEG 後續的主流影像編碼規範都使用了預測技術 (包括HEVC)。
圖4:基於預測的影像壓縮 : 編碼端圖4是基於預測的影像壓縮的編碼端的框圖。為了進行預測,編碼端還需要引入一部分解碼端的功能 (反量化和反變換),目的是產生重建影像 (reconstructed image) ,重建影像與解碼端解碼出的影像完全相同。當編碼器需要編碼一個畫素塊時,需要執行:
- 預測:從重建影像中獲取當前塊的相鄰畫素,進行預測,獲得預測塊 (predicted block)。
- 計算殘差:用原始畫素塊減去預測塊,得到殘差塊 (residual block)。
- 變換、量化、編碼。
- 重建殘差:對量化後的係數塊進行反量化,反編碼,得到重建的殘差塊。
- 重建影像:重建殘差塊與預測塊相加,得到重建畫素塊,存入重建影像。它將繼續作為後續塊的相鄰畫素而參與預測。
圖5展示了一個例子。在壓縮當前塊時,重建影像中只有其左側和上側的畫素,而下側和右側的畫素不存在 (還沒有被編碼和重建) ,因此我們只能使用左側和上側的畫素進行預測。
預測的模式往往有多種 (HEVC 有35種預測模式) ,不同的預測模式會生成不同的預測塊。針對每個塊,編碼器需要挑選最合適的一種預測模式 (直觀而言,需要挑選一種預測模式,使得預測塊與原始畫素塊的相似度儘量高) 。在圖5的例子中,花瓣的邊緣是一個斜向右上的紋理,因此編碼器選擇使用相鄰畫素向右偏上傳播的方式產生預測塊 (將來我們會看到,它是HEVC中一種角度模式)。另外,編碼器除了編碼係數塊本身外,還需要把它選擇的預測模式編號寫入碼流,這樣解碼器才能知道應該以哪種預測模式產生預測塊,從而正確地生成重建影像 (解碼後的影像)。
從圖5可以看出,與不基於預測的方法相比,基於預測的編碼器產生的變換後的係數塊更小,因此編碼後能產生更小的資料量。
圖5:不基於預測的影像壓縮 vs. 基於預測的影像壓縮基於預測的影像壓縮的解碼端的步驟如圖6,它比編碼端少了變換和量化這兩個步驟。在輸出畫素的同時,解碼端還需要維護重建影像,作為後續待解碼塊的相鄰畫素參與預測。
圖6:基於預測的影像壓縮:解碼端JPEG 後續的主流影像編碼規範都基於預測。例如 VP8 幀內編碼 (WEBP格式)、H.264/AVC 幀內編碼、H.265/HEVC 幀內編碼 (HEIF格式) 、AV1 幀內編碼 (AVIF格式) 。這些標準所使用的技術並沒有本質上的突破,只不過可選的塊大小越來越多、預測模式越來越多。例如,H.264/AVC 幀內編碼支援 4x4, 8x8, 16x16 的預測塊,每個塊最多可選 9 種預測模式。而 H.265/HEVC 支援 4x4, 8x8, 16x16, 32x32 的預測塊,每個塊可選 35 種預測模式。而為了達到更高的壓縮率,就需要編碼器搜尋各種塊大小和預測模式,從中挑選最優 (或次優) 的方案。因此,越新的標準的編碼效能越低,因為需要搜尋更多的塊大小和預測模式。
2. HEVC幀內編碼層次
HEVC 以塊 (block) 為單位進行預測和變換。對於幀內編碼,塊都是正方形的,大小可變。影像變化劇烈、紋理複雜的區域適合使用較小的塊,因為這樣每個小塊可以使用不同的預測模式,並進行細粒度的預測,可以讓預測更準確。而影像變化平緩的區域 (例如藍天) 適合使用較大的塊,因為這樣可以避免需要編碼過多的預測模式。
為了支援不同的塊大小,HEVC 把影像按 CTU, CU, PU, TU 的層次進行劃分。
圖7: CU, PU, TU 劃分舉例2.1 CTU (Coding Tree Unit)
HEVC 把一張影像劃分為多個 CTU 。CTU 的尺寸可以是 64x64, 32x32, 或 16x16 。具體的尺寸根據 HEVC 碼流頭部的資訊確定。對於一張影像,CTU的尺寸是固定的。
對於一幀影像,編碼器會按照從左到右,從上到下的順序掃描每個 CTU ,並對每個 CTU 進行編碼。
2.2 CU (Coding Unit)
每個 CTU 可以按四叉樹遞迴的方式劃分為多個 CU (sub-CU) 。CU 的尺寸可以是 64x64, 32x32, 16x16, 8x8 ,且只能小於等於 CTU 。例如:
- 一個 32x32 的 CTU 如果不劃分,則它本身就是一個 32x32 的 CU ;
- 一個 32x32 的 CTU 也可以劃分為 4 個 16x16 的 CU ;
- 一個 16x16 的 CU 又可以劃分為 4 個 8x8 的 CU ;
- 8x8 的 CU 不能再被劃分為更小的 CU,CU最小支援8x8。
對於可繼續劃分的 CU (也即大於等於 16x16 的 CU) ,需要在碼流中編碼一個語法元素 split_cu_flag
:
split_cu_flag=0
代表該 CU 不繼續劃分為 4 個 CU 。split_cu_flag=0
後面緊跟該 CU 的其它碼流內容;split_cu_flag=1
代表該 CU 要繼續劃分為 4個 CU 。split_cu_flag=1
後面緊跟 4 個 sub-CU 的碼流內容,順序為:先左上角的 sub-CU ,再右上角的 sub-CU ,再左下角的 sub-CU ,最後右下角的 sub-CU 。如圖8。
在沒有歧義的地方不需要編碼split_cu_flag
。例如,圖8中 CU3, CU4, CU5, CU6 這4個CU的頭部並沒有 split_cu_flag
,因為它們的大小是 8x8 ,已經不能再拆分為更小的 CU 。 而圖8中的 CU1, CU2, CU7 以及整個 32x32 的 CU 需要編碼 split_cu_flag
,因為它們的大小大於等於 16。
2.3 PU (Prediction Unit)
每個 CU 可以劃分為一個或多個 PU ,每個 PU 獨立持有一個預測模式 (prediction mode)。對於 8x8 的 CU ,可選兩種 PU 劃分方式 (part_mode
):
PART_2Nx2N
:不劃分,CU本身就是一個PU。PART_NxN
:劃分為4個PU。
而 64x64, 32x32 和 16x16 的 CU 只能選擇 PART_2Nx2N
,也即只能自身作為一個 PU。
CU劃分為PU:
- 編碼器進行塊劃分的目的主要是便於預測和變換。在進行預測和變換時,一個8x8的CU可以進行下一步劃分為預測單元(Predict Unit, PU)或TU(變換單元)。
- PU是進行預測的基本單元,即一個CU內的所有PU的預測方式相同都為幀內預測或都為幀間預測。
- CU到PU僅允許劃分一次,一共8種劃分方式,4種對稱劃分,2Nx2N(即不劃分,整個CU就是一個PU),2NxN, Nx2N, NxN;4種不對稱劃分,2NxnU, 2NxnD, nLx2N, nRx2N。不對稱劃分都是在1/4處進行劃分,例如32x32的塊進行2NxnU劃分會分成一個32x8的塊和一個32x24的塊,H.265規定只有在亮度CU尺寸大於等於16x16時才允許不對稱劃分(HEVC中只有PU可能為矩形,其他CTU、CU和TU均為方形)。
- 幀間預測的CU劃分成PU可以按上面8種任意模式劃分。而幀內預測的CU若尺寸大於8x8則只能按2Nx2N模式劃分,若幀內預測CU尺寸等於8x8可以按NxN劃分成4個4x4的PU,此時對應的兩個色度PU也為4x4而不是2x2因為H.265裡最小的塊為4x4。(由於將CU分割成四個大小相等的正方形PU在概念上等同於將相應的圖片塊分割成四個CU並將這些CU中的每一個編碼為單個PU。M/2 X M/2模式僅支援序列引數集中指示的最小CU大小)
- 注: PU 的劃分不是遞迴的,一個 CU 要麼不劃分,要麼劃分為4個PU,而這些PU不能再劃分為多個PU。
幀內預測模式,對於CU ,在碼流中編碼一個語法元素 part_mode
來指示它的 PU 劃分方式:
part_mode=PART_2Nx2N
時,說明 CU 自身就是一個 PU ,此時碼流中的part_mode
後面應該緊跟著1個預測模式 (也即該 PU 的預測模式),然後編碼 PU 的其它資料。如圖8中的 PU5, PU6 和 PU7 。part_mode=PART_NxN
時,說明 CU 被劃分為 4 個 PU ,此時碼流中的part_mode
位後面應該緊跟4個預測模式 (也即這4個PU 的預測模式) ,然後先後編碼 4 個 PU 的其它資料。如圖8中的 PU1~PU4 。
在沒有歧義的地方不需要編碼part_mode
。也即只需要對 8x8 的 CU 編碼 part_mode
語法元素,而不需要對 16x16, 32x32, 64x64 的 CU 編碼 part_mode
語法元素。
2.4 TU (Transform Unit)
每個 CU 可以按四叉樹遞迴的方式劃分為多個 TU 。TU 的尺寸可以是 32x32, 16x16, 8x8, 4x4 。
TU 是預測、變換的單元 (預測和變換的演算法都以 TU 為單位執行) 。而預測演算法的預測模式由 TU 所在的 PU 決定。
在幀內編碼中,TU 不能大於 PU (不能跨越 PU 的邊界),因此 TU 的劃分只存在以下幾種情況 :
- 對於 64x64 的 CU ,它必須劃分為 4 個 32x32 的 TU (因為 TU 的最大尺寸為 32x32) 。當然,這些 TU 也可以繼續遞迴劃分。劃分出的所有小 TU 都共享同一個預測模式,也即這個 64x64 CU 所對應的 64x64 PU 的預測模式。
- 對於 32x32, 16x16 的 CU 或 TU , 它可以不劃分 (自身就是一個 TU),也可以繼續劃分為更小的 TU 。當然,這些 TU 也可以繼續遞迴劃分。劃分出的所有小 TU 都共享同一個預測模式,也即它們所在的 32x32 CU 或 16x16 CU 所對應的 PU 的預測模式。
- 對於一個 8x8 CU,若它沒有劃分為 4 個 4x4 的 PU (
part_mode=PART_2Nx2N
), 則它可以不劃分 (自身就是一個 8x8 的 TU),也可以繼續劃分為 4 個 4x4 的 TU ,此時 4 個 TU 共享同一個預測模式,也即它們所在的 8x8 CU 所對應的 8x8 PU 的預測模式。圖8中最下面的例子就對應了這種情況。 - 對於一個 8x8 CU,若它被劃分為 4 個 4x4 的 PU (
part_mode=PART_NxN
) ,則它也必須劃分為 4 個 4x4 的 TU ,每個 TU 的預測模式就是他所在的 4x4 PU 的預測模式。
CU 中劃分 TU 的最大深度是可以設定的,由 HEVC 碼流頭部的 SPS 中的 max_transform_hierarchy_depth_intra
語法元素決定。例如,當 max_transform_hierarchy_depth_intra=0
時,表示任何CU都不能劃分為更小的TU。再例如,當 max_transform_hierarchy_depth_intra=1
時,表示任何CU最多隻能劃分成4個TU,而這些 TU 不能再劃分為更小的 TU 。
如圖8,需要在碼流中編碼一個 split_tu_flag
來決定這個 TU 是否要繼續劃分為 4 個 sub-TU 。split_tu_flag=1
代表劃分;split_tu_flag=0
代表不劃分。
在沒有歧義的地方不需要編碼split_tu_flag
,包括以下4種情況:
- 對於 64x64 的 CU ,不需要編碼
split_tu_flag
,因為它一定要劃分為 4 個 32x32 TU。 - 對於 4x4 的 TU ,不需要編碼
split_tu_flag
,因為它一定不能再劃分為更小的 TU。 - 對於被劃分為 4 個 4x4 PU 的 8x8 CU ,不需要編碼
split_tu_flag
,因為它一定要劃分為 4 個 4x4 TU 。 - 當受限於
max_transform_hierarchy_depth_intra
的取值,也即當前 TU 在 CU 中的深度等於設定的最大深度時,不需要編碼split_tu_flag
,因為它一定不能再劃分為更小的 TU。
注意:TU 才是預測和變換演算法所執行的單元。PU 雖然叫預測單元 (prediction unit) ,但如果一個 PU 內包含了多個 TU ,那麼不應該對整個 PU 執行預測演算法,而是對每個 TU 單獨執行預測演算法,只不過這些 TU 共享了該 PU 所指定的預測模式。
3. HEVC幀內預測
前文多次提到預測模式 (prediction mode),但沒有解釋具體含義。本節詳解 HEVC 的幀內預測演算法,包括:
- 如何獲取邊界畫素;
- 如何用邊界畫素對 TU 進行預測,生成預測塊。
2.1 獲取邊界畫素
在預測前,需要從重建影像中獲取待預測塊 (待預測TU) 的邊界畫素,用來預測當前 TU 。
如圖11,對於尺寸為 (N×N) 的 TU ,需要獲取 (4×N+1) 個邊界畫素,包括:
- 左上邊界畫素 (1個)
- 左邊界畫素 (N個)
- 左下邊界畫素 (N個)
- 上邊界畫素 (N個)
- 右上邊界畫素 (N個)
邊界畫素並不一定存在。以下兩種情況會導致邊界畫素不存在:
- 邊界畫素處於影像之外。例如圖12(a),當前 TU 緊鄰影像左側,則左上邊界畫素、左邊界畫素、左下邊界畫素都不存在。
- 邊界畫素未編碼。例如圖12(b),一個CU被劃分為4個TU。對於其中的 TU2,它的左下邊界畫素應該來自 TU3 ,但 TU3 並未被編碼 (換句話說,其重建畫素並未被寫入重建影像) ,因此 TU2 的左下邊界畫素不存在。
2.1.1 判斷邊界畫素是否存在
首先我們分析左邊界、上邊界和左上邊界的存在性。考慮到 CU, TU 四叉樹劃分的掃描順序,對於任何當前掃描到的 TU ,其左側,上側,左上側的畫素只要在影像內,則一定已經被編碼了。因此,只要看 TU 是否在影像邊界,就能判斷它的左邊界、左上邊界、上邊界是否存在:
- 只有當 TU 緊鄰影像左側時,左邊界不存在;
- 只有當 TU 緊鄰影像上側時,上邊界不存在;
- 只有當 TU 緊鄰影像左側或緊鄰影像上側時,左上邊界不存在。
其次我們分析左下邊界和右上邊界的存在性。對於最頂層的 block (也即 CTU) :
- 任何 CTU 的左下邊界都不存在,因為 CTU 的掃描順序是從左到右,從上到下,當我們正在編碼一個 CTU 時,其左下方的 CTU 必然未被編碼。
- 只有當 CTU 緊鄰影像上側或緊鄰影像右側時,CTU 的右上邊界不存在。其它情況下 CTU 右上邊界一定存在,因為上方一行的 CTU 已被編碼。
而針對 CTU 遞迴劃分出的小 block (CU 或 TU) ,可以在遞迴劃分的同時判斷它的左下邊界和右上邊界是否存在。如圖13,block0 被劃分為四個塊:block1,block2,block3和block4。在已知 block0 的各個邊界是否存在的情況下,可以判斷出 block1~block4 的邊界是否存在。對左下邊界,分析如下:
- block1 的左下邊界是否存在,取決於 block0 的左邊界是否存在,也即 block0 是否緊鄰影像左側;
- block2 的左下邊界一定不存在 (因為此時block3未被編碼);
- block3 的左下邊界是否存在,取決於 block0 的左下邊界是否存在;
- block4 的左下邊界一定不存在 (因為 block0 正下方的 block 一定未被編碼)。
對右上邊界,分析如下:
- block1 的右上邊界是否存在,取決於 block0 的上邊界是否存在,也即 block0 是否緊鄰影像上側;
- block2 的右上邊界是否存在,取決於 block0 的右上邊界是否存在;
- block3 的右上邊界一定存在 (因為此時block2已被編碼);
- block4 的右上邊界一定不存在 (因為 block0 正右方的 block 一定未被編碼)。
2.1.2 填充不存在的邊界畫素
當邊界畫素不存在時,需要進行填充,規則為:
- 當 (4×N+1) 個邊界畫素中只有一部分不存在時,用與它最近的存在的邊界畫素去填充。
- 當所有的邊界畫素都不存在時,用畫素中值 (128) 去填充。這種情況僅發生在 block 緊鄰影像最左上角時。
綜上所述,可以用如下 C 語言風格的虛擬碼來從重建影像中取樣邊界畫素,同時填充不存在的畫素:
// C語言風格的虛擬碼 // 函式:獲取一個block的邊界畫素 void getBorder ( int y, int x, // 引數:block的最左上的畫素在影像中的位置座標 (y,x) int sz, // 引數:block大小 int blb_exist, // 引數:block的左下邊界是否存在 int bar_exist, // 引數:block的右上邊界是否存在 uint8_t img_rcon [][], // 引數:二維陣列:重建影像 uint8_t ubla [1], // 引數:一維陣列:存放獲取到的左上邊界,共1個畫素 uint8_t ublb [], // 引數:一維陣列:存放獲取到的左邊界和左下邊界,共 2*sz 個畫素 uint8_t ubar [], // 引數:一維陣列:存放獲取到的上邊界和右上邊界,共 2*sz 個畫素 ) { int bl_exist = x > 0; // block 的左邊界是否存在:僅取決於block是否緊鄰影像左側 int ba_exist = y > 0; // block 的上邊界是否存在:僅取決於block是否緊鄰影像上側 int i; // 第1步:獲取左上邊界畫素 (1個畫素) --------------------------------------------------------- if (bl_exist && ba_exist) // 如果左上邊界存在 ubla[0] = img_rcon[y-1][x-1]; // 從重建影像中獲取它 else if (bl_exist) // 如果左上邊界不存在,但左邊界存在 ubla[0] = img_rcon[y ][x-1]; // 從重建影像中獲取左邊界最上面的那個畫素,填充左上邊界畫素 else if (ba_exist) // 如果左上邊界不存在,但上邊界存在 ubla[0] = img_rcon[y-1][x ]; // 從重建影像中獲取上邊界最左邊的那個畫素,填充左上邊界畫素 else // 如果左上邊界,左邊界,上邊界都不存在 ubla[0] = 128; // 用畫素中值填充左上邊界畫素 // 第2步:獲取左邊界畫素 (sz個畫素) --------------------------------------------------------- for (i=0; i<sz; i++) if (bl_exist) // 如果左邊界存在 ublb[i] = img_rcon[y+i][x-1]; // 從重建影像中獲取它們 else // 否則 ublb[i] = ubla[0]; // 用剛剛獲得的左上邊界畫素填充左邊界畫素 // 第3步:獲取左下邊界畫素 (sz個畫素) --------------------------------------------------------- for (i=sz; i<sz*2; i++) if (blb_exist) // 如果左下邊界存在 ublb[i] = img_rcon[y+i][x-1]; // 從重建影像中獲得它們 else // 否則 ublb[i] = ublb[sz-1]; // 用剛剛獲得的左邊界中的最下面的畫素填充左下邊界畫素 // 第4步:獲取上邊界畫素 (sz個畫素) --------------------------------------------------------- for (i=0; i<sz; i++) if (ba_exist) // 如果上邊界存在 ubar[i] = img_rcon[y-1][x+i]; // 從重建影像中獲得它們 else // 否則 ubar[i] = ubla[0]; // 用剛剛獲得的左上邊界畫素填充上邊界畫素 // 第5步:獲取右上邊界畫素 (sz個畫素) --------------------------------------------------------- for (i=sz; i<sz*2; i++) if (bar_exist) // 如果右上邊界存在 ubar[i] = img_rcon[y-1][x+i]; // 從重建影像中獲得它們 else // 否則 ubar[i] = ubar[sz-1]; // 用剛剛獲得的上邊界中的最右邊的畫素填充右上邊界畫素 }
2.2 邊界畫素的濾波
HEVC 規定在某些情況時,需要先對邊界畫素進行平滑濾波,然後再進行預測。對於 U, V 分量塊,在任何情況下都不用濾波。而對於 Y 分量塊,需要查詢表1來確定是否需要進行濾波。
對於需要濾波的情況,有兩種濾波方式:常規濾波和強濾波。碼流頭部的 SPS 中的 strong_intra_smoothing_enable_flag
語法元素規定了是否開啟強濾波,只有在開啟時並滿足一定條件時,才使用強濾波。本文不講解強濾波,配套程式碼裡也讓 strong_intra_smoothing_enable_flag=0
,在任何需要濾波的情況下都使用常規濾波。
常規濾波的方法是:把 (4×N+1) 個邊界畫素“放平”成一個 (4×N+1) 個元素的陣列。對於相鄰的3個元素,使用3抽頭的 FIR 濾波器進行濾波,抽頭引數為 [0.25, 0.5, 0.25] 。對於最左下邊和最右上邊的畫素,因為它們兩邊缺少畫素,無法進行濾波,所以不用濾波,直接複製。
常規濾波可以使用如下 C 語言風格的虛擬碼來實現:
// C語言風格的虛擬碼 // 函式:邊界畫素的常規濾波 void filterBorder ( int sz, // 引數:block 大小 uint8_t ubla [1], // 引數:一維陣列:存放獲取到的(未濾波的)左上邊界,共1個畫素 uint8_t ublb [], // 引數:一維陣列:存放獲取到的(未濾波的)左邊界和左下邊界,共 2*sz 個畫素 uint8_t ubar [], // 引數:一維陣列:存放獲取到的(未濾波的)上邊界和右上邊界,共 2*sz 個畫素 uint8_t fbla [1], // 引數:一維陣列:存放已濾波的左上邊界,共1個畫素 uint8_t fblb [], // 引數:一維陣列:存放已濾波的左邊界和左下邊界,共 2*sz 個畫素 uint8_t fbar [], // 引數:一維陣列:存放已濾波的上邊界和右上邊界,共 2*sz 個畫素 ) { int i; fbla[0] = ( 2 + ublb[0] + ubar[0] + 2*ubla[0] ) / 4;
// (2+A+2*B+C)>>2 等價於 (0.25*A+0.5*B+0.25*C) 。>>2 相當於除以4。另外,因為這裡使用整數運算,所以先+2再除以4,相當於進行了四捨五入。 fblb[0] = ( 2 + 2*ublb[0] + ublb[1] + ubla[0] ) / 4; fbar[0] = ( 2 + 2*ubar[0] + ubar[1] + ubla[0] ) / 4; for (i=1; i<sz*2-1; i++) { fblb[i] = ( 2 + 2*ublb[i] + ublb[i-1] + ublb[i+1] ) / 4; fbar[i] = ( 2 + 2*ubar[i] + ubar[i-1] + ubar[i+1] ) / 4; } fblb[sz*2-1] = ublb[sz*2-1]; // 對於最左下邊的1個畫素,不濾波,直接複製 fbar[sz*2-1] = ubar[sz*2-1]; // 對於最右上邊的1個畫素,不濾波,直接複製 }
2.3 預測演算法
有了 TU 的邊界畫素後,就可以執行預測演算法,生成 TU 的預測塊。HEVC 規定了 35 種預測模式 (不同預測模式會生成不同的預測塊),如表2。
其中:
- PLANAR 模式適合預測影像線性變化的區域。它使用水平和垂直方向的兩個濾波器,並將二者的平均值作為某一個畫素的預測值,這能讓預測畫素平緩變化。
- DC 模式適合預測影像的平坦區域,它使用左邊界畫素和上邊界畫素 (共 2×N 個畫素的平均值) 作為 TU 中每個畫素的預測值。
- 角度模式 (angular mode) 有33種,編號2~34。思路是讓邊界畫素沿著一定的角度“蔓延”到待預測的 TU 中,不同模式的“蔓延角度”不一樣。由於只有左下方到右上方的邊界畫素,所以“蔓延角度”只包含半個圓周:從斜向右上 45° 順時針轉到斜向左下 45° 。如圖14,HEVC把這半個圓周劃分為 33 個角度,對應 33 種角度模式。角度模式適合預測影像中的紋理,例如物體的邊緣、人的毛髮等。圖10可以給你一個直觀的感受。
邊緣濾波:在預測演算法中,對於 Y 分量的小於等於 16x16 的 TU 的 DC 模式 (模式1)、水平向右角度模式 (模式10) 和垂直向下角度模式 (模式26) ,需要採用一個簡單的邊緣濾波器,讓邊界畫素到預測出的 TU 之間的變化不那麼突兀(注意區分邊緣濾波和之前講過的邊界畫素濾波,前者作用於預測塊,後者作用於邊界畫素)。
想了解預測演算法的細節,請閱讀我提供的程式碼中的 predict
函式。其程式碼量並不大,其中 PLANAR模式 (模式0)、DC模式 (模式1)、水平向右角度模式 (模式10) 和垂直向下角度模式 (模式26) 各自只有不到10行;而其它的角度模式是一個合併的實現,程式碼只有 30 行。
2.4 計算殘差
用 TU 的原始畫素塊減去預測塊就能得到殘差塊 (residual block)。注意原始畫素塊和預測塊中的畫素都是 8-bit 無符號整數 (C 語言中的 unsigned char
或 uint8_t
型別),取值範圍為 0~255 。經過減法後,殘差塊中每個畫素的可能的取值範圍為 -255~+255 ,推薦使用 32-bit 有符號整數 (C語言中的 int
或 int32_t
型別) 來表示。
3. HEVC變換和反變換
本節講解 HEVC 的變換 (transform) 和反變換 (inverse transfrom) 。其中變換負責把殘差塊轉化為到頻域,得到係數塊 (coefficient block) 。然後經過量化 (quantize) 和反量化 (de-quantize) 後,反變換負責把頻域變回空間域,得到重建的殘差塊,再加上預測塊後,得到重建的畫素塊,作為後續塊預測的邊界畫素。
3.1 變換
HEVC 使用的變換有 DCT (離散餘弦變換) 和 DST (離散正弦變換) 。本質上都是兩次矩陣乘法:
變換公式𝑌=𝐶𝑋𝐶𝑇 ……變換公式
其中 𝑋 是殘差塊 (矩陣), 𝑌 是變換後的係數塊, 𝐶 是變換矩陣 (餘弦或正弦變換矩陣,是常係數矩陣), 𝐶𝑇 是 𝐶 的轉置。
為了降低計算複雜度,HEVC 規定使用整數近似的 DCT 或 DST ,也即上述公式中的所有矩陣元素都使用32位有符號整數 (C語言中的 int
型別) 來表示和計算。
另外,為了避免兩次矩陣乘法導致32位有符號整數的溢位,HEVC 規定在每次矩陣乘法後,將結果加一個值然後除以一個值 (相當於除法之後再四捨五入) 。實際上進行的計算是:
實際上採用的變換公式,其中全部使用整數計算𝑌=12𝑎(2𝑎−1+(12𝑏(2𝑏−1+𝐶𝑋))𝐶𝑇) ……HEVC 實際上採用的變換公式,其中全部使用整數計算
其中 𝑎,𝑏 都是標量常數整數。表3展示了在不同 TU 大小下 𝑎,𝑏 的取值。
對於 Y 分量的 4x4 的 block ,HEVC 採用 DST ,DST 變換矩陣為:
的變換矩陣𝐶=(2955748474740−7484−29−745555−8474−29) ...4x4 DST的變換矩陣
其它情況下,HEVC 都採用 DCT 。對於 U, V 分量的 4x4 的 block ,DCT 變換矩陣為:
的變換矩陣𝐶=(646464648336−36−8364−64−646436−8383−36) ...4x4 DCT的變換矩陣
對於 8x8 的 block ,DCT 變換矩陣為:
的變換矩陣𝐶=(646464646464646489755018−18−50−75−898336−36−83−83−36368375−18−89−50508918−7564−64−646464−64−646450−891875−75−1889−5036−8383−36−3683−833618−5075−8989−7550−18) ...8x8 DCT的變換矩陣
對於 16x16 的 block ,DCT 變換矩陣為:
的變換矩陣𝐶=(64646464646464646464646464646464908780705743259−9−25−43−57−70−80−87−9089755018−18−50−75−89−89−75−50−181850758987579−43−80−90−70−252570908043−9−57−878336−36−83−83−3636838336−36−83−83−363683809−70−87−25579043−43−90−57258770−9−8075−18−89−50508918−75−75188950−50−89−187570−43−8799025−80−575780−25−90−98743−7064−64−646464−64−646464−64−646464−64−646457−80−2590−9−874370−70−43879−902580−5750−891875−75−1889−50−5089−18−757518−895043−905725−87709−8080−9−7087−25−5790−4336−8383−36−3683−833636−8383−36−3683−833625−7090−80439−5787−8757−9−4380−9070−2518−5075−8989−7550−18−1850−7589−8975−50189−2543−5770−8087−9090−8780−7057−4325−9) ...16x16 DCT的變換矩陣
對於 32x32 的 block ,DCT 變換矩陣為:
的變換矩陣𝐶=(64646464646464646464646464646464646464646464646464646464646464649090888582787367615446383122134−4−13−22−31−38−46−54−61−67−73−78−82−85−88−90−90908780705743259−9−25−43−57−70−80−87−90−90−87−80−70−57−43−25−99254357708087909082674622−4−31−54−73−85−90−88−78−61−38−13133861788890857354314−22−46−67−82−9089755018−18−50−75−89−89−75−50−181850758989755018−18−50−75−89−89−75−50−1818507589886731−13−54−82−90−78−46−4387390856122−22−61−85−90−73−384467890825413−31−67−8887579−43−80−90−70−252570908043−9−57−87−87−57−94380907025−25−70−90−80−43957878546−13−67−90−73−2238828854−4−61−90−78−31317890614−54−88−82−382273906713−46−858336−36−83−83−3636838336−36−83−83−3636838336−36−83−83−3636838336−36−83−83−3636838222−54−90−6113788531−46−90−674738838−38−88−73−4679046−31−85−78−13619054−22−82809−70−87−25579043−43−90−57258770−9−80−80−9708725−57−90−43439057−25−87−7098078−4−82−73138567−22−88−61319054−38−90−46469038−54−90−31618822−67−85−1373824−7875−18−89−50508918−75−75188950−50−89−187575−18−89−50508918−75−75188950−50−89−187573−31−90−227867−38−90−138261−46−88−48554−54−8548846−61−82139038−67−78229031−7370−43−8799025−80−575780−25−90−98743−70−704387−9−90−258057−57−8025909−87−437067−54−783885−22−9049013−88−318246−73−616173−46−823188−13−90−49022−85−387854−6764−64−646464−64−646464−64−646464−64−646464−64−646464−64−646464−64−646464−64−646461−73−468231−88−1390−4−902285−38−785467−67−547838−85−22904−901388−31−824673−6157−80−2590−9−874370−70−43879−902580−57−578025−90987−43−707043−87−990−25−805754−85−488−46−618213−903867−78−2290−31−737331−902278−67−3890−13−826146−88485−5450−891875−75−1889−50−5089−18−757518−895050−891875−75−1889−50−5089−18−757518−895046−903854−903161−882267−851373−82478−78−482−73−1385−67−2288−61−3190−54−3890−4643−905725−87709−8080−9−7087−25−5790−43−4390−57−2587−70−980−80970−872557−904338−8873−4−6790−46−3185−781361−905422−8282−22−5490−61−1378−853146−90674−7388−3836−8383−36−3683−833636−8383−36−3683−833636−8383−36−3683−833636−8383−36−3683−833631−7890−61454−8882−38−2273−9067−13−4685−854613−6790−732238−8288−54−461−9078−3125−7090−80439−5787−8757−9−4380−9070−25−2570−9080−43−957−8787−57943−8090−702522−6185−9073−38−446−7890−8254−13−3167−8888−673113−5482−9078−46438−7390−8561−2218−5075−8989−7550−18−1850−7589−8975−501818−5075−8989−7550−18−1850−7589−8975−501813−3861−7888−9085−7354−31422−4667−8290−9082−6746−22−431−5473−8590−8878−6138−139−2543−5770−8087−9090−8780−7057−4325−9−925−4357−7080−8790−9087−8070−5743−2594−1322−3138−4654−6167−7378−8285−8890−9090−9088−8582−7873−6761−5446−3831−2213−4) ...32x32 DCT的變換矩陣
3.2 反變換
反變換是變換的逆操作,也是兩次矩陣乘法:
反變換公式𝑌=𝐶𝑇𝑋𝐶 ……反變換公式
其中 𝑋 是頻域的係數塊 (來自反量化的輸出), 𝑌 是重建出來的殘差塊, 𝐶 是變換矩陣,與變換中使用的變換矩陣相同, 𝐶𝑇 是 𝐶 的轉置。注意:反變換是左乘 𝐶𝑇 再右乘 𝐶 。而變換是左乘 𝐶 再右乘 𝐶𝑇 。
與變換相同,HEVC的反變換也都採用整數計算。同樣,為了避免兩次矩陣乘導致32位有符號整數溢位,HEVC 規定在每次矩陣乘法後,將結果加一個值然後除以一個值,並且把計算結果限制在範圍 [−32768,32767] 之內。因此,HEVC 的反變換實際上進行的計算是:
實際上採用的反變換公式,其中全部使用整數計算{𝑊1=12𝑐(2𝑐−1+𝐶𝑇𝑋)𝑊2=CLIP(𝑊1,−32768,32767)𝑊3=12𝑑(2𝑑−1+𝑊2𝐶)𝑌=CLIP(𝑊3,−32768,32767) ……HEVC 實際上採用的反變換公式,其中全部使用整數計算
對於任何的 TU 大小, 𝑐=7,𝑑=12 。
注:以上的 CLIP(X, -32768, 32767) 函式的功能是把矩陣 𝑋 中的所有值限制在範圍 [−32768,32767] 之內,具體執行的操作是:
- 如果該值小於 -32768 ,就把它設為 -32768;
- 如果該值大於 32767 ,就把它設為 32767;
- 其他情況下,值不變。
在 C 語言中,可以用以下語句實現 CLIP :
value = (value < -32768) ? -32768 : (value > 32767) ? 32767 : value;
3.3 變換/反變換的程式碼實現
以下是 HEVC 的 Y 分量的變換/反變換的虛擬碼。其中除以 2𝑎,2𝑏,2𝑐,2𝑑 的操作實際上是靠右移 (>>a
, >>b
, >>c
, >>d
) 實現的。
// C語言風格的虛擬碼 // HEVC 的變換和反變換 const int DST4_MAT [][] = { // 4x4 DST 變換矩陣 { 29, 55, 74, 84 }, { 74, 74, 0, -74 }, { 84, -29, -74, 55 }, { 55, -84, 74, -29 } }; // 注意!:這裡省略了變換矩陣 DCT8_MAT, DCT16_MAT 和 DCT32_MAT 的定義,具體見我的 github 的開原始碼 const int DCT8_MAT [][] = {......}; const int DCT16_MAT [][] = {......}; const int DCT32_MAT [][] = {......}; // 函式:矩陣乘法 void matMul ( const int sz, // 引數:矩陣大小 const int src1_transpose, // 引數:是否讓源矩陣1轉置後再參與矩陣乘 const int src2_transpose, // 引數:是否讓源矩陣2轉置後再參與矩陣乘 const int dst_sft, // 引數:計算結果右移 (>>) dst_sft 位,相當於除以 (2^dst_sft) const int dst_clip, // 引數:計算結果是否 CLIP 到 [-32768,32767] 範圍內 const int src1 [][], // 引數:源矩陣1 const int src2 [][], // 引數:源矩陣2 int dst [][] // 引數:計算結果矩陣 ) { const int dst_add = 1 << dst_sft >> 1; // dst_add = (2^(dst_sft-1)) int i, j, k; for (i=0; i<sz; i++) { for (j=0; j<sz; j++) { int s = dst_add; for (k=0; k<sz; k++) s += (src1_transpose ? src1[k][i] : src1[i][k]) * (src2_transpose ? src2[j][k] : src2[k][j]); s >>= dst_sft; // 相當於除以 2^dst_sft if (dst_clip) s = (s<-32768) ? -32768 : (s>32767) ? 32767 : s; dst[i][j] = s; } } } // 函式:HEVC Y分量的變換和反變換 void transform ( const int sz, // 引數: block 大小 const int inverse, // 引數: 0:變換 1:反變換 const int src [][], // 引數: 源矩陣 int dst [][] // 引數: 結果矩陣 ) { int tmp [64][64]; const int (*mat) [] = NULL; int a = 0 , b = 0 ; switch (sz) { case 4: mat = DST4_MAT; a = 1; b = 8; break; // 4x4 block case 8: mat = DCT8_MAT; a = 2; b = 9; break; // 8x8 block case 16: mat = DCT16_MAT; a = 3; b = 10; break; // 16x16 block case 32: mat = DCT32_MAT; a = 4; b = 11; break; // 32x32 block } if (!inverse) { matMul(sz, 0, 0, a, 0, mat, src, tmp); // C * X matMul(sz, 0, 1, b, 0, tmp, mat, dst); // C * X * CT } else { matMul(sz, 1, 0, 7, 1, mat, src, tmp); // CT * X matMul(sz, 0, 0, 12, 1, tmp, mat, dst); // CT * X * C } }
4. HEVC量化和反量化
4.1 量化引數
在使用 HEVC 進行影像壓縮時,使用者需要指定量化引數 (Quantize Parameter, 簡稱QP) ,QP 是整數,範圍為 0~51 ,越大則“量化力度”越大,也即壓縮率越高,但影像失真也嚴重。QP的值會被存在 HEVC 碼流頭部的 slice_header 欄位中,因為只有知道 QP 的取值,解碼器才能正確地進行反量化。
QP的取值決定了量化器對係數塊的除數,QP每大6,除數就變為原來的2倍。
為了簡化程式碼,我編寫的程式碼僅僅實現了 QP= {4,10,16,22,28} 這5種情況。我規定了一個引數 qpd6=0,1,2,3,4
來對應這五種情況 (滿足 QP=4+6×qpd6 )。
4.2 量化
HEVC 的量化可以非常簡單,虛擬碼如下:
// C語言風格的虛擬碼 // TU size 4x4 8x8 16x16 32x32 const int Q_SHIFT_TABLE [5] = { 5 , 4, 3, -1, 2}; // 函式:HEVC 量化 (最基礎的量化,沒有引入任何 RDOQ 方法) void quantize ( const int qpd6, // 引數: qpd6=0對應QP=4, qpd6=1對應QP=10, qpd6=2對應QP=16, qpd6=3對應QP=22, qpd6=4對應QP=28 const int sz, // 引數: 塊大小 const int src [][], // 引數: 輸入塊 (也即變換後的結果) int dst [][] // 引數: 量化結果 ) { const int q_sft = Q_SHIFT_TABLE[sz/8] + qpd6; // q_sft 與塊大小 (sz) 和 qpd6 都有關 const int q_add = 1 << q_sft >> 1; // q_add = (2^(q_sft-1)) int i, j; for (i=0; i<sz; i++) for (j=0; j<sz; j++) dst[i][j] = (src[i][j] + q_add) >> q_sft ; // >>q_sft 相當於 除以 (2^q_sft) }
可以看出,量化操作首先要根據塊大小和 qpd6
的值得出 q_sft
值, 然後將輸入塊中的所有元素右移 q_sft
位,相當於除以 2q_sft 。在右移之前,要先加上 q_add
,等效於在除以 2q_sft 後取最近的整數 (四捨五入為整數,而不是簡單地截斷為整數)。
為了提高壓縮率,量化也可以很複雜,比如採用 位元速率-質量最佳化量化方法 (rate-distortion optimized quantize, RDOQ) 。RDOQ 透過嘗試係數塊中的各個係數的最優量化值,來選擇最優或較優的量化方案(綜合考量壓縮後的碼流大小和失真,來作為評價指標)。原版的 RDOQ 的實現非常複雜,根據實際需求,RDOQ 還有各種簡化版本。本文不講解 RDOQ,但我在 GitHub 的程式碼使用了一種簡化的 RDOQ (大概80行C語言,效果不如完整的RDOQ,但優於上述最基礎的量化) ,有興趣可以去閱讀。
4.3 反量化
反量化是量化的逆操作,HEVC 的反量化非常簡單,虛擬碼如下:
// C語言風格的虛擬碼 // TU size 4x4 8x8 16x16 32x32 const int Q_SHIFT_TABLE [5] = { 5 , 4, 3, -1, 2}; // 函式:HEVC 反量化 void deQuantize ( const int qpd6, // 引數: qpd6=0對應QP=4, qpd6=1對應QP=10, qpd6=2對應QP=16, qpd6=3對應QP=22, qpd6=4對應QP=28 const int sz, // 引數: 塊大小 const int src [][], // 引數: 輸入塊 (也即量化演算法的結果) int dst [][] // 引數: 反量化結果 ) { const int q_sft = Q_SHIFT_TABLE[sz/8] + qpd6; // q_sft 與塊大小 (sz) 和 qpd6 都有關,與量化中的 q_sft 完全相同。 int i, j, t; for (i=0; i<sz; i++) for (j=0; j<sz; j++) { t = src[i][j] << q_sft; // <<iq_sft 相當於乘以 (2^iq_sft) dst[i][j] = (t<-32768) ? -32768 : (t>32767) ? 32767 : t; // 把結果 CLIP 在範圍 [-32768,32767] 內 } }
可以看出,首先要根據塊大小和 qpd6
的值得出 q_sft
值 (與量化中使用的 q_sft
完全相同), 然後將輸入塊中的所有元素左移 q_sft
位,相當於乘以 2q_sft ,最後再用 CLIP 函式把結果限制在範圍 [−32768,32767] 內。
4.4 反量化後重建畫素
反量化得到的是重建後的殘差塊,我們給重建殘差塊加上預測塊,就能得到重建畫素塊。由於中間經歷了有損的量化操作,所以重建畫素塊與原始畫素塊並不完全相同 (量化引數QP越大,則差異越大)。
在實際實現中,預測塊的資料型別為 8-bit 無符號整數 (C 語言中的 unsigned char
或 uint8_t
型別) ,取值範圍為 0~255;而重建後的殘差塊的資料型別為 32-bit 有符號整數 (C語言中的 int
或 int32_t
型別) 。二者相加得到的重建畫素塊中的極少數資料會超出 0~255 的範圍,因此還需要對重建畫素塊進行 CLIP 操作:
- 如果該值小於 0 ,就把它設為 0;
- 如果該值大於 255 ,就把它設為 255;
- 其他情況下,值不變。
這樣,最終生成的重建畫素塊可以用 8-bit 無符號整數表示。用來作為後續塊預測的邊界畫素。
5. CABAC編碼 (待補完)
HEVC採用的 CABAC 編碼 (上下文自適應二進位制算術編碼) 比 JPEG 採用的 VLC (變長編碼) 具有更高的壓縮率,但複雜度也更高。CABAC 編碼的使用是 HEVC 幀內編碼與 JPEG 的3個本質區別之一(另外兩個是預測和可變大小的塊)。
(CABAC編碼這一節我寫不動了,回頭再補充,如果想了解細節可以閱讀《新一代高效影片編碼H.265/HEVC 原理、標準與實現》) 。對於閱讀本文,讀者只需要知道:
- CABAC編碼器是無損的,參與編碼的資料能夠被解碼器原封不動地恢復。
- CABAC編碼器除了能夠編碼資料 (量化後的係數塊) ,也能編碼前文中提到的那些語法元素 (包括
split_cu_flag
,part_mode
, 預測模式號碼(0~34) ,split_tu_flag
等)。
6. HEVC幀內編碼端
前文講解了 HEVC 的所有基礎構件 (預測、變換、量化、CABAC編碼) ,本節我們終於可以把它們串起來,看看一個編碼器是如何對影像進行壓縮的。
HEVC 能達到較高壓縮率的原因是 "大力出奇跡" ——搜尋尋找眾多方案中最優 (或較優) 的。這些方案的不同之處體現在:
- 第 2 節中講過的 CU, PU, TU 的劃分方式的不同。從直觀的角度,影像變化平緩的區域 (例如藍天) 適合進行粗略的劃分 (使得大片的區域共用相同的預測模式,節省預測模式的編碼代價) ;影像變化劇烈的區域適合進行細緻的劃分,各個小塊各自使用不同的預測模式 (以編碼更多的預測模式的代價換取更精確的預測) 。這實際上是靠搜尋實現的:編碼器會遞迴地搜尋每個 CTU 的劃分方式,找出效果最好的一個。
- 第 3 節中講過的預測模式的不同。針對每個 PU ,編碼器最終只能選擇35種預測模式中的一種 (該PU中的所有TU會共享這個預測模式,但各自單獨進行預測),讓預測塊與原始畫素儘量匹配,從而達到更好的壓縮效果。
6.1 RD-cost指標
為了尋找最優的編碼方案,HEVC 規定了 RD-cost 指標 (Rate Distortion Cost) 用來評價各種方案的優劣。對任何一個 block (CTU, CU, PU, 或TU) ,我們都可以計算其 RD-cost,公式是:
計算公式RDcost=SSE(𝑋𝑅,𝑋)+𝜆×𝑏 ……RD-cost 計算公式
其中, 𝑋𝑅 是重建畫素塊; 𝑋 是原始畫素塊; SSE 是平方誤差和 (Sum-of-Squared-Error) 函式; 𝜆 是一個標量常係數,與量化引數 QP 有關 (如表4) ; 𝑏 是編碼這個塊所需的 bit 數量 (CABAC編碼器編碼該塊時輸出的bit數量) 。顯然, SSE(𝑋𝑅,𝑋) 衡量的是編碼這個塊帶來的失真; 𝜆×𝑏 衡量的是編碼後的大小,相加得到的 RD-cost 就是綜合考慮失真和大小的指標,越大則效果越差。
6.1.1 SSE的程式碼實現
SSE 的計算的虛擬碼如下,它會計算兩個塊的各項的差值,把它們平方後再相加。
// C語言風格的虛擬碼 // 函式:計算兩個塊的 SSE (distortion) int blkSumSquareDiff ( const int sz, // 引數:塊大小 const uint8_t src1 [][], // 引數:塊1 const uint8_t src2 [][] // 引數:塊2 ) { int i, j, diff, sum=0; for (i=0; i<sz; i++) for (j=0; j<sz; j++) { diff = (int)src1[i][j] - src2[i][j]; sum += diff * diff; } return sum; }
6.1.2 RD-cost 計算的程式碼實現
若用浮點數 (C語言中的 double
型別) 來表示 RD-cost ,則程式碼實現比較簡潔,也不用考慮溢位問題,程式碼如下。
// 函式:計算 RD-cost double calcRDcost (int qpd6, int dist, int bits) { // dist是失真(也即SSE) ,bits 是編碼一個塊所需的bit數量 static const double LAMBDA_TABLE [] = {0.00897694, 0.3590775, 1.43631, 5.74524, 22.98096}; // λ查詢表 double lambda = LAMBDA_TABLE[qpd6]; // 查表得到 λ 值 return (double)dist + lambda * bits; // RDcost = SSE + λ×b
}
考慮到 RD-cost 只是個評價指標,並不需要精確。為了簡化計算,避免使用浮點數,可以用如下純 int
型別整數的方法近似計算 RD-cost 。其中原本用浮點數表示的 λ 值變成了兩個整數常數,分別作為 dist
和 bits
的權重。另外還需要在乘法和加法時避免溢位 int
型別的最大值。
#define INT_MAX_VALUE (0x7FFFFFFF) // int 型別最大值 // 函式:計算 RD-cost ,純整數實現,並且避免 int 型別溢位 int calcRDcost (int qpd6, int dist, int bits) { // dist是失真(也即SSE) ,bits 是編碼一個塊所需的bit數量 static const int WEIGHT_DIST [] = {11, 11, 11, 5, 1}; static const int WEIGHT_BITS [] = { 1, 4, 16, 29, 23}; int weight1 = WEIGHT_DIST[qpd6]; //作為dist的權重 int weight2 = WEIGHT_BITS[qpd6]; //作為bits的權重 int cost1 = (INT_MAX_VALUE / weight1 <= dist) ? INT_MAX_VALUE : weight1 * dist; //避免乘法溢位 int cost2 = (INT_MAX_VALUE / weight2 <= bits) ? INT_MAX_VALUE : weight2 * bits; //避免乘法溢位 return (INT_MAX_VALUE - cost1 <= cost2) ? INT_MAX_VALUE : cost1 + cost2; //避免加法溢位 }
6.2 編碼器的遞迴搜尋
對於一張影像,編碼器會按照從左到右,從上到下的順序遍歷每個 CTU 。對於每個 CTU (注意CTU本身就是一個最頂層的CU),編碼器會用以下遞迴函式 ProcessCU
搜尋最優的編碼方案:
// 虛擬碼 : 遞迴搜尋,以最佳模式編碼一個 CU
函式 ProcessCU ( 碼流物件 coder, CU物件 cu ) {
碼流物件 coder_backup <- coder // 備份碼流物件。因為要搜尋多種方案,需要撤銷掉非最優的方案,所以必須備份,以便隨時重試
int best_rdcost = int型別的最大值
//--------------------------------------------------------------------------------------------------------------------------------------------------------
// step1 : 嘗試把當前 CU 劃分為多個 sub-CU
//--------------------------------------------------------------------------------------------------------------------------------------------------------
if ( cu.size > 8 ) { // 當前 CU 大於8,則允許劃分為 4 個小 CU ,嘗試劃分它們來進行編碼
ProcessCU(coder, cu左上的sub-cu); // 對左上的sub-CU,遞迴呼叫自身
ProcessCU(coder, cu右上的sub-cu); // 對右上的sub-CU,遞迴呼叫自身
ProcessCU(coder, cu左下的sub-cu); // 對左下的sub-CU,遞迴呼叫自身
ProcessCU(coder, cu右下的sub-cu); // 對右下的sub-CU,遞迴呼叫自身
best_rdcost = SSE(cu.重建塊, cu.原始塊) + λ * (coder.bitcount - coder_backup.bitcount) // 備份當前rdcost作為當前最佳的rdcost
best重建塊 = cu.重建塊 // 備份當前的重建影像作為目前最佳的重建影像
}
//--------------------------------------------------------------------------------------------------------------------------------------------------------
// step2 : 嘗試不劃分為 sub-CU , 也不劃分為多個 PU (PART_2Nx2N) , 也不劃分為多個 TU ,嘗試所有35種預測模式
//--------------------------------------------------------------------------------------------------------------------------------------------------------
for (pmode=0; pmode<35; pmode) { // 遍歷所有預測模式
cu.預測(pmode)
cu.變換()
量化係數塊 <- cu.量化()
cu.反量化()
cu.反變換()
cu.重建畫素塊()
碼流物件 coder_tmp <- coder_backup; // 使用備份的碼流物件 coder_backup 來嘗試,不能用 coder 來嘗試,因為它可能已經編碼了 CU 劃分為多個 sub-CU 的方案。
向coder_tmp中編碼當前CU,包括預測模式pmode、量化係數塊等資訊
rdcost = SSE(cu.重建塊, cu.原始塊) + λ * (coder_tmp.bitcount - coder_backup.bitcount)
if (rdcost < best_rdcost) { // 如果當前的 rdcost 比之前最佳的要好
best_rdcost = rdcost // 更新 best_rdcost
best重建塊 = cu.重建塊 // 更新最佳方案下的重建塊
coder = coder_tmp // 將當前最佳的碼流物件放入 coder ,讓 coder 持續保持為最佳的碼流物件。
}
}
//--------------------------------------------------------------------------------------------------------------------------------------------------------
// step3 : 嘗試不劃分為 sub-CU , 也不劃分為多個 PU (PART_2Nx2N) , 但劃分為多個 TU ,嘗試所有35種預測模式
//--------------------------------------------------------------------------------------------------------------------------------------------------------
for (pmode=0; pmode<35