H.265/HEVC 幀內編碼詳解

luckylan發表於2024-05-09

1. 影像壓縮概述

在介紹 HEVC 影像壓縮之前,先回顧一下影像壓縮技術的發展路線。

H.265/HEVC 幀內編碼詳解

H.265/HEVC 及其之前的編碼標準的發展時間線
H.265 又名 HEVC,在 2013 年被接受為 ITU-T 標準,用於影片壓縮。眾所周知,影片包含一幀一幀的影像。與大多數影片壓縮標準一樣,HEVC 把幀分為兩種:
  • 獨立幀 (I幀) :採用幀內編碼 (intra-frame coding) ,不依賴於其它任何幀。
  • 非獨立幀 (P幀或B幀) :採用幀間編碼 (inter-frame coding) ,依賴於其它幀的重建影像。
因此,幀內編碼也可以用於影像壓縮。HEVC 幀內編碼被用於 HEIF 影像格式中,在同樣的影像質量(失真)下,它的大小比 JPEG, JPEG2000, WEBP 等影像格式更小。本文詳解 HEVC 幀內編碼。

1.1 畫素座標系

影像可以表示為畫素的二維陣列,每個畫素的位置用 (𝑦,𝑥) 座標表示 (如下圖) 。其中 𝑦∈[0,height−1] ; 𝑥∈[0,width−1] 。 height 和 width 分別為影像高度和寬度。

H.265/HEVC 幀內編碼詳解
影像的座標編號

在程式語言中,可以用二維陣列儲存一張影像,其中 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,步驟包括分塊、變換、量化和熵編碼。

H.265/HEVC 幀內編碼詳解
圖1:基礎的影像編碼 (例如JPEG) : 編碼端
H.265/HEVC 幀內編碼詳解
圖2:基礎的影像編碼原理舉例

圖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→將得到的畫素塊放入影像。

H.265/HEVC 幀內編碼詳解
圖3:基礎的影像編碼 (例如JPEG) : 解碼端

1.3 基於預測 (prediction) 的影像壓縮

H.265/HEVC 幀內編碼詳解
圖:(a)原始影像;(b) HEVC產生的預測影像;(c) 殘差影像;(d) 重建影像

基礎的影像壓縮方法 (圖1) 對每個塊進行獨立的壓縮,沒有利用塊之間的相似性。

而預測技術 (prediction) 會用已編碼的鄰塊去預測當前正在編碼的塊,生成預測塊,用當前的原始畫素塊減去預測塊得到殘差塊,對殘差塊進行變換、量化、編碼,以獲得更好的壓縮效果。JPEG 後續的主流影像編碼規範都使用了預測技術 (包括HEVC)。

H.265/HEVC 幀內編碼詳解
圖4:基於預測的影像壓縮 : 編碼端

圖4是基於預測的影像壓縮的編碼端的框圖。為了進行預測,編碼端還需要引入一部分解碼端的功能 (反量化和反變換),目的是產生重建影像 (reconstructed image) ,重建影像與解碼端解碼出的影像完全相同。當編碼器需要編碼一個畫素塊時,需要執行:

  • 預測:從重建影像中獲取當前塊的相鄰畫素,進行預測,獲得預測塊 (predicted block)。
  • 計算殘差:用原始畫素塊減去預測塊,得到殘差塊 (residual block)。
  • 變換、量化、編碼。
  • 重建殘差:對量化後的係數塊進行反量化,反編碼,得到重建的殘差塊。
  • 重建影像:重建殘差塊與預測塊相加,得到重建畫素塊,存入重建影像。它將繼續作為後續塊的相鄰畫素而參與預測。

圖5展示了一個例子。在壓縮當前塊時,重建影像中只有其左側和上側的畫素,而下側和右側的畫素不存在 (還沒有被編碼和重建) ,因此我們只能使用左側和上側的畫素進行預測。

預測的模式往往有多種 (HEVC 有35種預測模式) ,不同的預測模式會生成不同的預測塊。針對每個塊,編碼器需要挑選最合適的一種預測模式 (直觀而言,需要挑選一種預測模式,使得預測塊與原始畫素塊的相似度儘量高) 。在圖5的例子中,花瓣的邊緣是一個斜向右上的紋理,因此編碼器選擇使用相鄰畫素向右偏上傳播的方式產生預測塊 (將來我們會看到,它是HEVC中一種角度模式)。另外,編碼器除了編碼係數塊本身外,還需要把它選擇的預測模式編號寫入碼流,這樣解碼器才能知道應該以哪種預測模式產生預測塊,從而正確地生成重建影像 (解碼後的影像)。

從圖5可以看出,與不基於預測的方法相比,基於預測的編碼器產生的變換後的係數塊更小,因此編碼後能產生更小的資料量。

H.265/HEVC 幀內編碼詳解
圖5:不基於預測的影像壓縮 vs. 基於預測的影像壓縮

基於預測的影像壓縮的解碼端的步驟如圖6,它比編碼端少了變換和量化這兩個步驟。在輸出畫素的同時,解碼端還需要維護重建影像,作為後續待解碼塊的相鄰畫素參與預測。

H.265/HEVC 幀內編碼詳解
圖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 的層次進行劃分。

H.265/HEVC 幀內編碼詳解
圖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。
H.265/HEVC 幀內編碼詳解
圖8:在碼流中,split_cu_flag 用來控制 CU 是否劃分為 4 個 sub-CU ;part_mode 用來控制 CU 是否劃分為 4 個 PU;split_tu_flag 用來控制 CU 是否劃分為 4 個 TU。

在沒有歧義的地方不需要編碼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 進行預測,生成預測塊。
H.265/HEVC 幀內編碼詳解
圖10:從重建影像中獲取邊界畫素,然後進行預測,這裡展示了35種預測模式所產生的預測塊

2.1 獲取邊界畫素

在預測前,需要從重建影像中獲取待預測塊 (待預測TU) 的邊界畫素,用來預測當前 TU 。

如圖11,對於尺寸為 (N×N) 的 TU ,需要獲取 (4×N+1) 個邊界畫素,包括:

  • 左上邊界畫素 (1個)
  • 左邊界畫素 (N個)
  • 左下邊界畫素 (N個)
  • 上邊界畫素 (N個)
  • 右上邊界畫素 (N個)
H.265/HEVC 幀內編碼詳解
圖11:邊界畫素

邊界畫素並不一定存在。以下兩種情況會導致邊界畫素不存在:

  • 邊界畫素處於影像之外。例如圖12(a),當前 TU 緊鄰影像左側,則左上邊界畫素、左邊界畫素、左下邊界畫素都不存在。
  • 邊界畫素未編碼。例如圖12(b),一個CU被劃分為4個TU。對於其中的 TU2,它的左下邊界畫素應該來自 TU3 ,但 TU3 並未被編碼 (換句話說,其重建畫素並未被寫入重建影像) ,因此 TU2 的左下邊界畫素不存在。
H.265/HEVC 幀內編碼詳解
圖12:兩種可能導致邊界畫素不存在的因素。

2.1.1 判斷邊界畫素是否存在

首先我們分析左邊界、上邊界和左上邊界的存在性。考慮到 CU, TU 四叉樹劃分的掃描順序,對於任何當前掃描到的 TU ,其左側,上側,左上側的畫素只要在影像內,則一定已經被編碼了。因此,只要看 TU 是否在影像邊界,就能判斷它的左邊界、左上邊界、上邊界是否存在:

  • 只有當 TU 緊鄰影像左側時,左邊界不存在;
  • 只有當 TU 緊鄰影像上側時,上邊界不存在;
  • 只有當 TU 緊鄰影像左側或緊鄰影像上側時,左上邊界不存在。

其次我們分析左下邊界和右上邊界的存在性。對於最頂層的 block (也即 CTU) :

  • 任何 CTU 的左下邊界都不存在,因為 CTU 的掃描順序是從左到右,從上到下,當我們正在編碼一個 CTU 時,其左下方的 CTU 必然未被編碼。
  • 只有當 CTU 緊鄰影像上側或緊鄰影像右側時,CTU 的右上邊界不存在。其它情況下 CTU 右上邊界一定存在,因為上方一行的 CTU 已被編碼。
H.265/HEVC 幀內編碼詳解
圖13

而針對 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來確定是否需要進行濾波。

H.265/HEVC 幀內編碼詳解

對於需要濾波的情況,有兩種濾波方式:常規濾波和強濾波。碼流頭部的 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。

H.265/HEVC 幀內編碼詳解

其中:

  • PLANAR 模式適合預測影像線性變化的區域。它使用水平和垂直方向的兩個濾波器,並將二者的平均值作為某一個畫素的預測值,這能讓預測畫素平緩變化。
  • DC 模式適合預測影像的平坦區域,它使用左邊界畫素和上邊界畫素 (共 2×N 個畫素的平均值) 作為 TU 中每個畫素的預測值。
  • 角度模式 (angular mode) 有33種,編號2~34。思路是讓邊界畫素沿著一定的角度“蔓延”到待預測的 TU 中,不同模式的“蔓延角度”不一樣。由於只有左下方到右上方的邊界畫素,所以“蔓延角度”只包含半個圓周:從斜向右上 45° 順時針轉到斜向左下 45° 。如圖14,HEVC把這半個圓周劃分為 33 個角度,對應 33 種角度模式。角度模式適合預測影像中的紋理,例如物體的邊緣、人的毛髮等。圖10可以給你一個直觀的感受。
H.265/HEVC 幀內編碼詳解
圖14:33種角度模式的“蔓延角度”示意圖。圖中的數字是 HEVC 的預測模式的編號。

邊緣濾波:在預測演算法中,對於 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 charuint8_t 型別),取值範圍為 0~255 。經過減法後,殘差塊中每個畫素的可能的取值範圍為 -255~+255 ,推薦使用 32-bit 有符號整數 (C語言中的 intint32_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 大小下 𝑎,𝑏 的取值。

H.265/HEVC 幀內編碼詳解

對於 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 charuint8_t 型別) ,取值範圍為 0~255;而重建後的殘差塊的資料型別為 32-bit 有符號整數 (C語言中的 intint32_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 就是綜合考慮失真和大小的指標,越大則效果越差。

H.265/HEVC 幀內編碼詳解

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 。其中原本用浮點數表示的 λ 值變成了兩個整數常數,分別作為 distbits 的權重。另外還需要在乘法和加法時避免溢位 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; pmode) {                // 遍歷所有預測模式
    碼流物件 coder_tmp <- coder_backup;           // 使用備份的碼流物件 coder_backup 來嘗試,不能用 coder 來嘗試,因為它已經編碼了當前最佳方案。
    
    ProcessTU(pmode, coder_tmp, cu左上的sub-cu);  // 注意 ProcessTU 也是個遞迴函式,它會嘗試 TU 的所有遞迴劃分方案,使用 pmode 作為預測模式
    ProcessTU(pmode, coder_tmp, cu右上的sub-cu);
    ProcessTU(pmode, coder_tmp, cu左下的sub-cu);
    ProcessTU(pmode, coder_tmp, cu右下的sub-cu);
    
    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 持續保持為最佳的碼流物件。
    }
  }
  
  
  //--------------------------------------------------------------------------------------------------------------------------------------------------------
  // step4 : 嘗試不劃分為 sub-CU , 但劃分為4個 PU (PART_NxN) 。
  //--------------------------------------------------------------------------------------------------------------------------------------------------------
  
  if ( cu.size == 8 ) {                           // 只有 8x8 的 CU (最小的CU) 能劃分為 4 個 PU
  
    // 注意這種情況下 PU 的大小一定為 4x4 ,剛好對應最小的 TU (4x4) ,因此不必呼叫 ProcessTU 來遞迴嘗試
    sub_pmode1 = 遍歷35種預測模式,搜尋 cu左上的sub-PU 的最佳預測模式,具體省略(仍然使用RD-cost指標)
    sub_pmode2 = 遍歷35種預測模式,搜尋 cu右上的sub-PU 的最佳預測模式,具體省略(仍然使用RD-cost指標)
    sub_pmode3 = 遍歷35種預測模式,搜尋 cu左下的sub-PU 的最佳預測模式,具體省略(仍然使用RD-cost指標)
    sub_pmode4 = 遍歷35種預測模式,搜尋 cu右下的sub-PU 的最佳預測模式,具體省略(仍然使用RD-cost指標)
    
    碼流物件 coder_tmp <- coder_backup;           // 使用備份的碼流物件 coder_backup 來嘗試,不能用 coder 來嘗試,因為它可能已經編碼了 CU 劃分為多個 sub-CU 的方案。
    
    coder_tmp中編碼當前CU,包括4PU的預測模式 sub_pmode1~sub_pmode4 、量化係數塊等資訊
    
    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 持續保持為最佳的碼流物件。
    }
  }
  
  cu.重建塊 = best重建塊                          // 最後,用一直以來維護的最佳重建塊更新當前 CU 的重建塊
  // 注:該函式結束時,coder 物件中已經編碼了當前CU在最佳方案下的碼流,後續 CU 的編碼應該在 coder 物件的基礎上繼續進行。
}

以上只是虛擬碼。實現為C程式碼時,還有更多細節,詳見我 Github 上的程式碼實現。

簡單起見,我的程式碼中將 CU 中 TU 的最大劃分深度設為 1 (也即碼流頭部的 SPS 中的 max_transform_hierarchy_depth_intra=1),這種情況下,一個 CU 最多隻能劃分為 4 個 sub-TU ,而 sub-TU 不會再遞迴劃分為更小的 TU 。

6.3 編碼器的遞迴搜尋: 一個例子

為了幫助讀者理解遞迴搜尋,我將編碼器對一個CTU的搜尋過程展示如下。

// 編碼器搜尋一個 CTU 的最優方案的過程日誌 -------------------------------------------------------------------------
ProcessCU開始: CU(y= 0 x= 0 size=32)  初始化設定最優RDcost=99999
      ProcessCU開始: CU(y= 0 x= 0 size=16)  初始化設定最優RDcost=99999
            ProcessCU開始: CU(y= 0 x= 0 size= 8)  初始化設定最優RDcost=99999
               把 CU(y= 0 x= 0 size= 8) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是 0 (RDcost=369), 小於之前最優RDcost (99999), 更新為當前最優方案
               把 CU(y= 0 x= 0 size= 8) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是 1 (RDcost=748), 不小於之前最優RDcost (369)
               把 CU(y= 0 x= 0 size= 8) 分成4個PU, 對每個PU分別嘗試35種預測模式, 4個最佳的預測模式={ 0,19,31,18}, RDcost=801, 不小於之前最優RDcost (369)
            ProcessCU結束: CU(y= 0 x= 0 size= 8), 最終 RDcost=369
            ProcessCU開始: CU(y= 0 x= 8 size= 8)  初始化設定最優RDcost=99999
               把 CU(y= 0 x= 8 size= 8) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是 4 (RDcost=238), 小於之前最優RDcost (99999), 更新為當前最優方案
               把 CU(y= 0 x= 8 size= 8) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是 2 (RDcost=270), 不小於之前最優RDcost (238)
               把 CU(y= 0 x= 8 size= 8) 分成4個PU, 對每個PU分別嘗試35種預測模式, 4個最佳的預測模式={ 3, 2,34, 3}, RDcost=309, 不小於之前最優RDcost (238)
            ProcessCU結束: CU(y= 0 x= 8 size= 8), 最終 RDcost=238
            ProcessCU開始: CU(y= 8 x= 0 size= 8)  初始化設定最優RDcost=99999
               把 CU(y= 8 x= 0 size= 8) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是26 (RDcost=217), 小於之前最優RDcost (99999), 更新為當前最優方案
               把 CU(y= 8 x= 0 size= 8) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是26 (RDcost=232), 不小於之前最優RDcost (217)
               把 CU(y= 8 x= 0 size= 8) 分成4個PU, 對每個PU分別嘗試35種預測模式, 4個最佳的預測模式={25,17,22,20}, RDcost=291, 不小於之前最優RDcost (217)
            ProcessCU結束: CU(y= 8 x= 0 size= 8), 最終 RDcost=217
            ProcessCU開始: CU(y= 8 x= 8 size= 8)  初始化設定最優RDcost=99999
               把 CU(y= 8 x= 8 size= 8) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是 0 (RDcost=225), 小於之前最優RDcost (99999), 更新為當前最優方案
               把 CU(y= 8 x= 8 size= 8) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是10 (RDcost=269), 不小於之前最優RDcost (225)
               把 CU(y= 8 x= 8 size= 8) 分成4個PU, 對每個PU分別嘗試35種預測模式, 4個最佳的預測模式={26,24,17, 7}, RDcost=282, 不小於之前最優RDcost (225)
            ProcessCU結束: CU(y= 8 x= 8 size= 8), 最終 RDcost=225
         已嘗試把 CU(y= 0 x= 0 size=16) 拆分成4個CU, 總 RDcost=369+238+217+225=1049  暫時以該方案作為當前最優方案
         把 CU(y= 0 x= 0 size=16) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是 0 (RDcost=818), 小於之前最優RDcost (1049), 更新為當前最優方案
         把 CU(y= 0 x= 0 size=16) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是 2 (RDcost=951), 不小於之前最優RDcost (818)
      ProcessCU結束: CU(y= 0 x= 0 size=16), 最終 RDcost=818
      ProcessCU開始: CU(y= 0 x=16 size=16)  初始化設定最優RDcost=99999
            ProcessCU開始: CU(y= 0 x=16 size= 8)  初始化設定最優RDcost=99999
               把 CU(y= 0 x=16 size= 8) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是 1 (RDcost=214), 小於之前最優RDcost (99999), 更新為當前最優方案
               把 CU(y= 0 x=16 size= 8) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是 0 (RDcost=237), 不小於之前最優RDcost (214)
               把 CU(y= 0 x=16 size= 8) 分成4個PU, 對每個PU分別嘗試35種預測模式, 4個最佳的預測模式={ 2, 3,14,17}, RDcost=370, 不小於之前最優RDcost (214)
            ProcessCU結束: CU(y= 0 x=16 size= 8), 最終 RDcost=214
            ProcessCU開始: CU(y= 0 x=24 size= 8)  初始化設定最優RDcost=99999
               把 CU(y= 0 x=24 size= 8) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是26 (RDcost=416), 小於之前最優RDcost (99999), 更新為當前最優方案
               把 CU(y= 0 x=24 size= 8) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是 0 (RDcost=503), 不小於之前最優RDcost (416)
               把 CU(y= 0 x=24 size= 8) 分成4個PU, 對每個PU分別嘗試35種預測模式, 4個最佳的預測模式={ 1,16,19,21}, RDcost=525, 不小於之前最優RDcost (416)
            ProcessCU結束: CU(y= 0 x=24 size= 8), 最終 RDcost=416
            ProcessCU開始: CU(y= 8 x=16 size= 8)  初始化設定最優RDcost=99999
               把 CU(y= 8 x=16 size= 8) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是 0 (RDcost=216), 小於之前最優RDcost (99999), 更新為當前最優方案
               把 CU(y= 8 x=16 size= 8) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是 0 (RDcost=247), 不小於之前最優RDcost (216)
               把 CU(y= 8 x=16 size= 8) 分成4個PU, 對每個PU分別嘗試35種預測模式, 4個最佳的預測模式={ 0,26, 7,19}, RDcost=223, 不小於之前最優RDcost (216)
            ProcessCU結束: CU(y= 8 x=16 size= 8), 最終 RDcost=216
            ProcessCU開始: CU(y= 8 x=24 size= 8)  初始化設定最優RDcost=99999
               把 CU(y= 8 x=24 size= 8) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是 8 (RDcost=1986), 小於之前最優RDcost (99999), 更新為當前最優方案
               把 CU(y= 8 x=24 size= 8) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是 3 (RDcost=1404), 小於之前最優RDcost (1986), 更新為當前最優方案
               把 CU(y= 8 x=24 size= 8) 分成4個PU, 對每個PU分別嘗試35種預測模式, 4個最佳的預測模式={ 0,16,23, 3}, RDcost=1345, 小於當前最優RDcost (1404), 更新為當前最優方案
            ProcessCU結束: CU(y= 8 x=24 size= 8), 最終 RDcost=1345
         已嘗試把 CU(y= 0 x=16 size=16) 拆分成4個CU, 總 RDcost=214+416+216+1345=2192  暫時以該方案作為當前最優方案
         把 CU(y= 0 x=16 size=16) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是 3 (RDcost=5377), 不小於之前最優RDcost (2192)
         把 CU(y= 0 x=16 size=16) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是15 (RDcost=2717), 不小於之前最優RDcost (2192)
      ProcessCU結束: CU(y= 0 x=16 size=16), 最終 RDcost=2192
      ProcessCU開始: CU(y=16 x= 0 size=16)  初始化設定最優RDcost=99999
            ProcessCU開始: CU(y=16 x= 0 size= 8)  初始化設定最優RDcost=99999
               把 CU(y=16 x= 0 size= 8) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是 1 (RDcost=219), 小於之前最優RDcost (99999), 更新為當前最優方案
               把 CU(y=16 x= 0 size= 8) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是17 (RDcost=289), 不小於之前最優RDcost (219)
               把 CU(y=16 x= 0 size= 8) 分成4個PU, 對每個PU分別嘗試35種預測模式, 4個最佳的預測模式={ 1,16,22,12}, RDcost=344, 不小於之前最優RDcost (219)
            ProcessCU結束: CU(y=16 x= 0 size= 8), 最終 RDcost=219
            ProcessCU開始: CU(y=16 x= 8 size= 8)  初始化設定最優RDcost=99999
               把 CU(y=16 x= 8 size= 8) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是24 (RDcost=1300), 小於之前最優RDcost (99999), 更新為當前最優方案
               把 CU(y=16 x= 8 size= 8) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是 0 (RDcost=654), 小於之前最優RDcost (1300), 更新為當前最優方案
               把 CU(y=16 x= 8 size= 8) 分成4個PU, 對每個PU分別嘗試35種預測模式, 4個最佳的預測模式={17,31,19,22}, RDcost=649, 小於當前最優RDcost (654), 更新為當前最優方案
            ProcessCU結束: CU(y=16 x= 8 size= 8), 最終 RDcost=649
            ProcessCU開始: CU(y=24 x= 0 size= 8)  初始化設定最優RDcost=99999
               把 CU(y=24 x= 0 size= 8) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是26 (RDcost=181), 小於之前最優RDcost (99999), 更新為當前最優方案
               把 CU(y=24 x= 0 size= 8) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是 0 (RDcost=221), 不小於之前最優RDcost (181)
               把 CU(y=24 x= 0 size= 8) 分成4個PU, 對每個PU分別嘗試35種預測模式, 4個最佳的預測模式={31,26,19,29}, RDcost=306, 不小於之前最優RDcost (181)
            ProcessCU結束: CU(y=24 x= 0 size= 8), 最終 RDcost=181
            ProcessCU開始: CU(y=24 x= 8 size= 8)  初始化設定最優RDcost=99999
               把 CU(y=24 x= 8 size= 8) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是28 (RDcost=1302), 小於之前最優RDcost (99999), 更新為當前最優方案
               把 CU(y=24 x= 8 size= 8) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是27 (RDcost=907), 小於之前最優RDcost (1302), 更新為當前最優方案
               把 CU(y=24 x= 8 size= 8) 分成4個PU, 對每個PU分別嘗試35種預測模式, 4個最佳的預測模式={29,28,23,27}, RDcost=937, 不小於之前最優RDcost (907)
            ProcessCU結束: CU(y=24 x= 8 size= 8), 最終 RDcost=907
         已嘗試把 CU(y=16 x= 0 size=16) 拆分成4個CU, 總 RDcost=219+649+181+907=1958  暫時以該方案作為當前最優方案
         把 CU(y=16 x= 0 size=16) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是14 (RDcost=4343), 不小於之前最優RDcost (1958)
         把 CU(y=16 x= 0 size=16) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是28 (RDcost=3091), 不小於之前最優RDcost (1958)
      ProcessCU結束: CU(y=16 x= 0 size=16), 最終 RDcost=1958
      ProcessCU開始: CU(y=16 x=16 size=16)  初始化設定最優RDcost=99999
            ProcessCU開始: CU(y=16 x=16 size= 8)  初始化設定最優RDcost=99999
               把 CU(y=16 x=16 size= 8) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是 3 (RDcost=1641), 小於之前最優RDcost (99999), 更新為當前最優方案
               把 CU(y=16 x=16 size= 8) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是 4 (RDcost=1332), 小於之前最優RDcost (1641), 更新為當前最優方案
               把 CU(y=16 x=16 size= 8) 分成4個PU, 對每個PU分別嘗試35種預測模式, 4個最佳的預測模式={ 4, 5, 3, 0}, RDcost=1254, 小於當前最優RDcost (1332), 更新為當前最優方案
            ProcessCU結束: CU(y=16 x=16 size= 8), 最終 RDcost=1254
            ProcessCU開始: CU(y=16 x=24 size= 8)  初始化設定最優RDcost=99999
               把 CU(y=16 x=24 size= 8) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是33 (RDcost=1670), 小於之前最優RDcost (99999), 更新為當前最優方案
               把 CU(y=16 x=24 size= 8) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是 0 (RDcost=875), 小於之前最優RDcost (1670), 更新為當前最優方案
               把 CU(y=16 x=24 size= 8) 分成4個PU, 對每個PU分別嘗試35種預測模式, 4個最佳的預測模式={34,31, 6, 0}, RDcost=919, 不小於之前最優RDcost (875)
            ProcessCU結束: CU(y=16 x=24 size= 8), 最終 RDcost=875
            ProcessCU開始: CU(y=24 x=16 size= 8)  初始化設定最優RDcost=99999
               把 CU(y=24 x=16 size= 8) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是 0 (RDcost=322), 小於之前最優RDcost (99999), 更新為當前最優方案
               把 CU(y=24 x=16 size= 8) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是27 (RDcost=395), 不小於之前最優RDcost (322)
               把 CU(y=24 x=16 size= 8) 分成4個PU, 對每個PU分別嘗試35種預測模式, 4個最佳的預測模式={34,29, 1,28}, RDcost=421, 不小於之前最優RDcost (322)
            ProcessCU結束: CU(y=24 x=16 size= 8), 最終 RDcost=322
            ProcessCU開始: CU(y=24 x=24 size= 8)  初始化設定最優RDcost=99999
               把 CU(y=24 x=24 size= 8) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是26 (RDcost=359), 小於之前最優RDcost (99999), 更新為當前最優方案
               把 CU(y=24 x=24 size= 8) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是26 (RDcost=335), 小於之前最優RDcost (359), 更新為當前最優方案
               把 CU(y=24 x=24 size= 8) 分成4個PU, 對每個PU分別嘗試35種預測模式, 4個最佳的預測模式={24,27,24,26}, RDcost=415, 不小於之前最優RDcost (335)
            ProcessCU結束: CU(y=24 x=24 size= 8), 最終 RDcost=335
         已嘗試把 CU(y=16 x=16 size=16) 拆分成4個CU, 總 RDcost=1254+875+322+335=2787  暫時以該方案作為當前最優方案
         把 CU(y=16 x=16 size=16) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是 5 (RDcost=5745), 不小於之前最優RDcost (2787)
         把 CU(y=16 x=16 size=16) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是 0 (RDcost=4150), 不小於之前最優RDcost (2787)
      ProcessCU結束: CU(y=16 x=16 size=16), 最終 RDcost=2787
   已嘗試把 CU(y= 0 x= 0 size=32) 拆分成4個CU, 總 RDcost=818+2192+1958+2787=7754  暫時以該方案作為當前最優方案
   把 CU(y= 0 x= 0 size=32) 當作1個TU, 嘗試35種預測模式, 其中最佳預測模式是 0 (RDcost=20321), 不小於之前最優RDcost (7754)
   把 CU(y= 0 x= 0 size=32) 分成4個TU, 嘗試35種預測模式, 其中最佳預測模式是 3 (RDcost=15969), 不小於之前最優RDcost (7754)
ProcessCU結束: CU(y= 0 x= 0 size=32), 最終 RDcost=7754

以上搜尋過程最終產生了下圖所示的 CU/PU/TU 劃分策略。讀者可以對照著看,檢驗自己的理解是否正確。

H.265/HEVC 幀內編碼詳解
圖:以上搜尋過程產生的CU/PU/TU劃分

以上講解的是最詳盡的編碼器搜尋方法,時間開銷較大。很多 HEVC 編碼器會用一些方法來對搜尋樹進行剪枝,例如使用速度較快的哈達瑪變換 (DWHT) 來從 35 個預測模式中挑選幾個合適的預測模式,只對這幾個預測模式進行 RD-cost 的評估。而不像以上的虛擬碼計算了全部 35 種預測模式的 RD-cost 。HEVC 搜尋樹的剪枝方法有較大的研究價值,感興趣的讀者可以閱讀相關論文資料。

7. 總結

本文詳細講解了 HEVC 幀內編碼的整體原理、CU/PU/TU劃分結構、預測、變換、量化。並對編碼器的實現 (RD-cost計算、遞迴搜尋最佳方案) 進行了概述。後續我會繼續寫完 CABAC 編碼的章節 (第5節)。

相關文章