Jyrki Alakuijala 博士,Google LLC,2023 年 3 月 9 日
摘要
WebP 無損壓縮是一種圖片格式,用於無損壓縮 ARGB 圖片。無失真格式會儲存及還原像素值,包括 Alpha 值為 0 的像素顏色值。這種格式會使用子解析度的圖片 (重複嵌入格式本身),儲存與圖片相關的統計資料,例如使用的熵碼、空間預測器、色域轉換和色彩表。依序資料壓縮 (LZ77)、前置字串編碼和色彩快取的通用演算法會用於壓縮大量資料。事實證明,解碼速度比 PNG 更快,使用今日的 PNG 格式所能處理的壓縮速度也比現在高出 25%。
1 簡介
本文件說明 WebP 無損圖片的壓縮資料表示法。此為有關 WebP 失真編碼器與解碼器實作的詳細參考資料。
在本文件中,我們大量使用 C 程式設計語言語法來描述位元流,並假設有可讀取位元的函式 ReadBits(n)
。系統會根據包含這些位元組的串流,以自然順序讀取位元組,並以最小可佔的位元優先順序讀取每個位元組的位元。當同時讀取多個位元時,系統會根據原始資料以原始資料建立整數。傳回的整數中最重要的位元也是原始資料中最重要的位元。因此,陳述式
b = ReadBits(2);
相當於以下兩個陳述式:
b = ReadBits(1);
b |= ReadBits(1) << 1;
我們假設每個顏色元件 (Alpha、紅色、藍色和綠色) 都使用 8 位元位元組。我們將對應類型定義為 uint8。整個 ARGB 像素是以 uint32 類型表示,這是由 32 位元組成的無正負號整數。在顯示轉換行為的程式碼中,這些值會編入下列位元編碼中:以位元 31..24 表示 alpha、23..16 位元的紅色、位元 15..8 以及位元 15..8 的紅色;不過這種格式的實作可以在內部使用其他表示法。
整體而言,WebP 無損圖片包含標頭資料、轉換資訊和實際圖片資料。標頭包含圖片的寬度和高度。WebP 無損圖片可進行四種不同類型的轉換,然後再進行熵編碼。位元串流中的轉換資訊包含套用相應反向轉換所需的資料。
2 命名
- ARGB
- 由 Alpha、紅色、綠色和藍色值組成的像素值。
- ARGB 圖片
- 包含 ARGB 像素的 2D 陣列。
- 色彩快取
- 用於儲存最近使用顏色的小型雜湊陣列,可透過較短的程式碼辨識。
- 彩色索引圖片
- 可使用小整數建立索引的 1D 色彩圖片 (在 WebP 失真率中最多為 256)。
- 彩色轉換圖片
- 二維子解析度的圖片,包含色彩元件關聯性的資料。
- 距離對應
- 將 LZ77 距離變更為 2D 鄰近範圍內像素的最小值。
- 熵圖片
- 2D 子解析度圖片,指出應在圖片中的個別正方形中使用哪一個熵編碼,也就是說,每個像素都是中繼前置字元代碼。
- LZ77
- 以字典為基礎的滑動視窗壓縮演算法,會發出符號或將符號描述為過去符號的序列。
- 中繼前置字元程式碼
- 小整數 (最多 16 位元),可以為中繼前置字串資料表中的元素建立索引。
- 預測者圖片
- 二維子解析度圖片,指出圖片中特定正方形會使用哪個空間預測器。
- 前置字元代碼
- 一種典型的熵編碼方式,只需要使用少量的位元,程式碼較頻繁使用。
- 前置字串編碼
- 此方式可將程式碼較大的整數壓縮,並使用熵程式碼編碼部分整數位元,並將剩餘的位元原始值加以編碼。如此一來,即使符號範圍較大,熵代碼的說明依然會相對較小。
- 掃描行訂單
- 像素的處理順序 (由左至右和由上而下),從左側像素開始。資料列完成後,請從下一列的左欄繼續操作。
3 RIFF 標頭
標頭開頭是 RIFF 容器。其中包含以下 21 個位元組:
- 字串「RIFF」。
- 區塊長度的 32 位元小值,這是由 RIFF 標頭控制的區塊的整個大小。這通常等於酬載大小 (檔案大小減 8 個位元組:「RIFF」ID 為 4 個位元組,以及儲存值本身的 4 個位元組)。
- 字串「WEBP」(RIFF 容器名稱)。
- 字串「VP8L」(FourCC 表示無損編碼圖片資料)。
- 小端代的 32 位元值,無損串流中的位元組數量。
- 1 位元組簽章 0x2f。
前 28 位元的位元串流會指定圖片的寬度和高度。寬度和高度會解碼為 14 位元整數,如下所示:
int image_width = ReadBits(14) + 1;
int image_height = ReadBits(14) + 1;
圖片寬度與高度的 14 位元精確度會將 WebP 無損圖片的大小上限限制為 16384×16384 像素。
alpha_is_used 位元只是提示,不會影響解碼。當圖片中的所有 Alpha 值為 255 時,應設為 0,否則應設為 1。
int alpha_is_used = ReadBits(1);
version_number 是 3 位元代碼,必須設為 0。任何其他值都應視為錯誤。
int version_number = ReadBits(3);
4 轉換
轉換是可反轉的圖片資料操控方式,藉由建立空間和色彩相關度模型,可減少剩餘的符號熵。可以讓最終壓縮更加密集。
圖片可以進行四種類型的轉換。1 位元表示有轉換。每個轉換都只能使用一次。這些轉換僅適用於主要層級的 ARGB 圖片,子解析度的圖片 (顏色轉換圖片、熵圖片和預測器圖片) 則沒有任何轉換,即便是 0 位元代表轉換結束。
一般來說,編碼器會使用這些轉換來減少殘差圖片中的 Shannon 熵。此外,您也可以根據熵最小化決定轉換資料。
while (ReadBits(1)) { // Transform present.
// Decode transform type.
enum TransformType transform_type = ReadBits(2);
// Decode transform data.
...
}
// Decode actual image data (Section 5).
如果有轉換,則接下來的兩個位元會指定轉換類型。轉換作業有四種類型。
enum TransformType {
PREDICTOR_TRANSFORM = 0,
COLOR_TRANSFORM = 1,
SUBTRACT_GREEN_TRANSFORM = 2,
COLOR_INDEXING_TRANSFORM = 3,
};
轉換類型後面接著轉換資料。「轉換」資料中包含套用反向轉換所需的資訊,且取決於轉換類型。系統會依照從位元串流讀取反向順序套用反向轉換,也就是優先套用。
接下來,我們會說明不同類型的轉換資料。
4.1 預測者轉換
預測器轉換可以利用鄰近像素通常有關聯的事實,藉此減少熵。在預測器轉換中,目前的像素值是從已解碼 (依掃描行順序) 解碼的像素預測,且只有實際值 (實際 - 預測) 經過編碼。像素的綠色元件會定義 ARGB 圖片的特定區塊內使用的 14 個預測工具。預測模式會決定要使用的預測類型。我們將圖片分為正方形,而正方形中的所有像素會使用相同的預測模式。
前 3 位元的預測資料會以位元數定義區塊寬度和高度。區塊欄數 block_xsize
會用於二維索引。
int size_bits = ReadBits(3) + 2;
int block_width = (1 << size_bits);
int block_height = (1 << size_bits);
#define DIV_ROUND_UP(num, den) (((num) + (den) - 1) / (den))
int block_xsize = DIV_ROUND_UP(image_width, 1 << size_bits);
轉換資料包含圖片中每個區塊的預測模式。這是一張子解析度的圖片,像素的綠色元件會定義 ARGB 圖片特定區塊內所有 block_width * block_height
像素使用的 14 個預測工具。這張子解析度圖片是使用第 5 章所述的相同技術進行編碼。
如果是像素 (x, y),可以透過下列方式計算各自的篩選器區塊位址:
int block_index = (y >> size_bits) * block_xsize +
(x >> size_bits);
預測模式共有 14 種,在每個預測模式中,目前的像素值將由一或多個已知值的鄰近像素預測而得。
我們選擇目前像素 (P) 的鄰近像素 (TL、T、TR 和 L),如下所示:
O O O O O O O O O O O
O O O O O O O O O O O
O O O O TL T TR O O O O
O O O O L P X X X X X
X X X X X X X X X X X
X X X X X X X X X X X
其中 TL 代表左上角,T 代表頂端,TR 代表右上角,L 則代表左。預測 P 的值時,所有 O、TL、T、TR 和 L 像素都已經過處理,P 像素和所有 X 像素皆不明。
根據上述的像素,不同的預測模式定義如下。
模式 | 目前像素每個管道的預測價值 |
---|---|
0 | 0xff000000 (代表 ARGB 中的純黑色) |
1 | L |
2 | 二 |
3 | TR |
4 | TL |
5 | 平均 2(平均 2(L、TR)、T) |
6 | 平均 2(L、TL) |
7 | 平均值 2(L, T) |
8 | 平均 2(TL, T) |
9 | 平均 2(T, TR) |
10 | Min2(Average2(L, TL)、Average2(T, TR)) |
11 | Select(L、T、TL) |
12 | ClampAddSubtractFull(L, T, TL) |
13 | ClampAddSubtractHalf(Average2(L, T)、TL) |
以下每個 ARGB 元件的 Average2
定義如下:
uint8 Average2(uint8 a, uint8 b) {
return (a + b) / 2;
}
「Select 預測器」的定義如下:
uint32 Select(uint32 L, uint32 T, uint32 TL) {
// L = left pixel, T = top pixel, TL = top-left pixel.
// ARGB component estimates for prediction.
int pAlpha = ALPHA(L) + ALPHA(T) - ALPHA(TL);
int pRed = RED(L) + RED(T) - RED(TL);
int pGreen = GREEN(L) + GREEN(T) - GREEN(TL);
int pBlue = BLUE(L) + BLUE(T) - BLUE(TL);
// Manhattan distances to estimates for left and top pixels.
int pL = abs(pAlpha - ALPHA(L)) + abs(pRed - RED(L)) +
abs(pGreen - GREEN(L)) + abs(pBlue - BLUE(L));
int pT = abs(pAlpha - ALPHA(T)) + abs(pRed - RED(T)) +
abs(pGreen - GREEN(T)) + abs(pBlue - BLUE(T));
// Return either left or top, the one closer to the prediction.
if (pL < pT) {
return L;
} else {
return T;
}
}
系統會為每個 ARGB 元件執行 ClampAddSubtractFull
和 ClampAddSubtractHalf
函式,如下所示:
// Clamp the input value between 0 and 255.
int Clamp(int a) {
return (a < 0) ? 0 : (a > 255) ? 255 : a;
}
int ClampAddSubtractFull(int a, int b, int c) {
return Clamp(a + b - c);
}
int ClampAddSubtractHalf(int a, int b) {
return Clamp(a + (a - b) / 2);
}
某些邊框像素有特殊的處理規則。如果進行預測轉換,無論這些像素的模式為 [0..13],最左邊像素的預測值為 0xff000000,頂端列的所有像素都是 L-pixel,最左側欄的所有像素都是 T-pixel。
但最右側欄的 TR-pixel 定址是例外狀況。最右側欄的像素是根據 [0..13] 模式預測而得,就像不是邊框上的像素一樣,但與目前像素位於同一列的最左側像素會改用 TR-pixel 格式。
系統會將預測值的每個管道加編碼殘餘值,藉此取得最終像素值。
void PredictorTransformOutput(uint32 residual, uint32 pred,
uint8* alpha, uint8* red,
uint8* green, uint8* blue) {
*alpha = ALPHA(residual) + ALPHA(pred);
*red = RED(residual) + RED(pred);
*green = GREEN(residual) + GREEN(pred);
*blue = BLUE(residual) + BLUE(pred);
}
4.2 色彩轉換
色彩轉換的目標是裝飾每個像素的 R、G 和 B 值。顏色轉換會原封不動地保留綠色 (G) 值,根據綠色值轉換紅色 (R) 值,再依據綠色值和紅色值轉換藍色 (B) 值。
如同預測器轉換的情況,首先圖片會分成多個區塊,且區塊中的所有像素也使用相同的轉換模式。每個區塊都有三種顏色轉換元素。
typedef struct {
uint8 green_to_red;
uint8 green_to_blue;
uint8 red_to_blue;
} ColorTransformElement;
實際色彩轉換會藉由定義色彩轉換差異來完成。顏色轉換差異取決於 ColorTransformElement
,對於特定區塊中的所有像素來說都是一樣。色彩轉換會減去差異。接著,反向色彩轉換只會加入差異值。
顏色轉換函式的定義如下:
void ColorTransform(uint8 red, uint8 blue, uint8 green,
ColorTransformElement *trans,
uint8 *new_red, uint8 *new_blue) {
// Transformed values of red and blue components
int tmp_red = red;
int tmp_blue = blue;
// Applying the transform is just subtracting the transform deltas
tmp_red -= ColorTransformDelta(trans->green_to_red, green);
tmp_blue -= ColorTransformDelta(trans->green_to_blue, green);
tmp_blue -= ColorTransformDelta(trans->red_to_blue, red);
*new_red = tmp_red & 0xff;
*new_blue = tmp_blue & 0xff;
}
ColorTransformDelta
是使用帶正負號的 8 位元整數,代表 3.5 位元固定編號與正負號的 8 位元 RGB 顏色通道 (c) [-128..127],其定義如下:
int8 ColorTransformDelta(int8 t, int8 c) {
return (t * c) >> 5;
}
如要呼叫 ColorTransformDelta()
,必須先從 8 位元未簽署的表示法 (uint8) 轉換為 8 位元帶正負號 (int8)。已簽署值應解讀為 8 位元二的互補數字 (即 uint8 範圍 [128..255] 對應至其轉換 int8 值的 [-128..-1] 範圍)。
乘法就是用更精度 (至少 16 位元精確度) 來完成。此處的位移作業的簽署擴充屬性並不重要;結果僅使用最低的 8 位元,而且符號的位移和未簽署的位移將彼此一致。
現在,我們會說明色彩轉換資料的內容,以便在解碼時套用反向色彩轉換,並復原原始的紅色和藍色值。與預測者轉換一樣,色彩轉換資料的前 3 位元包含圖片區塊的寬度和高度 (以位元為單位):
int size_bits = ReadBits(3) + 2;
int block_width = 1 << size_bits;
int block_height = 1 << size_bits;
顏色轉換資料的其餘部分包含 ColorTransformElement
例項,對應至圖片的每個區塊。每個 ColorTransformElement
'cte'
都會視為子解析度圖片中的像素,其 Alpha 元件為 255
,紅色元件為 cte.red_to_blue
,綠色元件為 cte.green_to_blue
,藍色元件為 cte.green_to_red
。
在解碼期間,系統會解碼區塊的 ColorTransformElement
例項,並將反向色彩轉換套用至像素的 ARGB 值。如先前所述,反向顏色轉換只是將 ColorTransformElement
值新增至紅色和藍色管道。Alpha 和綠色通道仍維持原樣。
void InverseTransform(uint8 red, uint8 green, uint8 blue,
ColorTransformElement *trans,
uint8 *new_red, uint8 *new_blue) {
// Transformed values of red and blue components
int tmp_red = red;
int tmp_blue = blue;
// Applying the inverse transform is just adding the
// color transform deltas
tmp_red += ColorTransformDelta(trans->green_to_red, green);
tmp_blue += ColorTransformDelta(trans->green_to_blue, green);
tmp_blue +=
ColorTransformDelta(trans->red_to_blue, tmp_red & 0xff);
*new_red = tmp_red & 0xff;
*new_blue = tmp_blue & 0xff;
}
4.3 減去綠色轉換
減去綠色轉換會從每個像素的紅色和藍色值減去綠色值。當這個轉換出現時,解碼器需要同時為紅色和藍色值新增綠色值。沒有任何與這項轉換相關聯的資料。解碼器會套用反向轉換,如下所示:
void AddGreenToBlueAndRed(uint8 green, uint8 *red, uint8 *blue) {
*red = (*red + green) & 0xff;
*blue = (*blue + green) & 0xff;
}
這個轉換是多餘的,因為可以使用顏色轉換來建立模型,但由於此處沒有額外的資料,因此減去綠色轉換可以使用少於完全爆炸的色彩轉換的位元進行編碼。
4.4 色彩索引轉換
如果沒有太多的不重複像素值,建立色彩索引陣列並用陣列索引取代像素值可能會更有效率。顏色索引轉換可以實現此目標。(在 WebP 無損編碼的情況下,我們明確不會呼叫這個調色盤轉換,因為 WebP 無損編碼中存在類似但動態的概念:色彩快取。)
顏色索引轉換會檢查圖片中不重複 ARGB 值的數量。如果這個數字低於門檻 (256),系統會建立這些 ARGB 值的陣列,用於將像素值替換為對應的索引:將像素的綠色管道替換為索引,將所有 Alpha 值設為 255,且所有紅色和藍色值都設為 0。
轉換資料包含顏色表格大小及顏色表中的項目。解碼器會讀取顏色索引轉換資料,如下所示:
// 8-bit value for the color table size
int color_table_size = ReadBits(8) + 1;
色彩表格會以圖片儲存格式本身儲存。假設高度為 1 像素且寬度為 color_table_size
,可透過讀取圖片 (不含 RIFF 標頭、圖片大小和轉換) 取得色彩表格。色彩表一律會減去編碼,以減少圖片熵。調色盤顏色的差異通常比顏色本身少很多,因此對於較小的圖片而言,可大幅節省成本。在解碼時,您可以分別新增每個 ARGB 元件之前的顏色元件值,並儲存結果的最小 8 位元,藉此取得顏色表中的每個最終顏色。
圖片的反向轉換只是將像素值 (為色彩資料表的索引) 替換成實際色彩表值。索引是根據 ARGB 顏色的綠色元件完成。
// Inverse transform
argb = color_table[GREEN(argb)];
如果索引等於或大於 color_table_size
,則 argb 顏色值應設為 0x00000000 (透明黑色)。
當顏色表格較小 (等於或少於 16 種顏色),系統會將數個像素組合成單一像素。像素組合會將多個像素 (2、4 或 8) 封裝為一個像素,分別降低圖片寬度。Pixel 套裝組合可讓相鄰像素的聯合發布熵編碼更有效率,並為熵程式碼提供部分類似編碼的好處,但只有不重複值不超過 16 個時才能使用。
color_table_size
指定合併的像素數量:
int width_bits;
if (color_table_size <= 2) {
width_bits = 3;
} else if (color_table_size <= 4) {
width_bits = 2;
} else if (color_table_size <= 16) {
width_bits = 1;
} else {
width_bits = 0;
}
width_bits
的值為 0、1、2 或 3。值為 0 表示圖片不會進行像素組合。值 1 表示組合兩個像素,且每個像素的範圍都是 [0..15]。值 2 表示結合了四個像素,且每個像素的範圍為 [0..3]。值為 3 表示組合八個像素,每個像素的範圍都是 [0..1],也就是二進位值。
這些值會包裝在綠色元件中,如下所示:
width_bits
= 1:在每個 x 值中,x 就可能 0 (mod 2) 將 x 的綠色值置於綠色值至少 4 位元的 4 位元中,x + 1 的綠色值會定位在 x / 2 中最顯著的 4 位元中。width_bits
= 2:在每個 x 值中,x 就可能 0 (mod 4) 將 x 的綠色值置於綠色值 ( x/4) 的 2 個最小顯著位元中,而綠色值位於 x + 1 到 x + 3 會依照綠色值中較顯著的位元定位。width_bits
= 3:在每個 x 值中,x 就可能 0 (mod 8) 將 x 的綠色值放在 x / 8 的最小重要位元中,而 x + 1 到 x + 7 的綠色值會定位在 x / 8 中更多綠色值的位元。
讀取這個轉換後,width_bits
會向下取樣 image_width
。這會影響後續轉換的大小。新大小可以使用 DIV_ROUND_UP
(如先前的定義) 計算。
image_width = DIV_ROUND_UP(image_width, 1 << width_bits);
5 圖片資料
圖片資料是掃描行順序中的像素值陣列。
5.1 圖片資料的角色
我們使用圖片資料的五個不同角色:
- ARGB 圖片:儲存圖片的實際像素。
- 熵圖片:儲存中繼前置字串程式碼 (請參閱「解碼中繼前置字串程式碼」)。
- 預測者圖片:儲存預測者轉換的中繼資料 (請參閱「預測者轉換」一文)。
- 色彩轉換圖片:由圖片的不同區塊的
ColorTransformElement
值 (在「色彩轉換」中定義) 建立。 - 色彩索引圖片:大小為
color_table_size
的陣列 (最多 256 個 ARGB 值),用於儲存色彩索引轉換的中繼資料 (請參閱「色彩索引轉換」)。
5.2 圖片資料的編碼
映像檔資料的編碼與角色無關。
圖片會先分割成一組固定大小的區塊 (通常是 16x16 區塊)。每個區塊都是使用自己的熵代碼建立模型。此外,多個區塊可能會共用相同的熵代碼。
原因:儲存熵程式碼會產生費用。如果統計類似的區塊共用熵程式碼,則只能儲存一次程式碼,因此費用可降至最低。例如,編碼器使用統計屬性進行分群,或重複彙整一對隨機選取的叢集,進而減少編碼圖片所需的整體位元量,藉此找到類似的區塊。
每個像素都會使用下列其中一種可能的方法進行編碼:
- 前置字串為文字:每個通道 (綠色、紅色、藍色和 Alpha 版本) 都會單獨進行熵編碼。
- LZ77 反向參考:從圖片的其他位置複製一系列的像素。
- 色彩快取程式碼:使用最近發現顏色的簡短乘法雜湊程式碼 (顏色快取索引)。
以下幾個子節會詳細說明各個部分。
5.2.1 前置字串編碼文字
像素會依序儲存為綠色、紅色、藍色和 Alpha 等前置字元值。詳情請參閱第 6.2.3 節。
5.2.2 LZ77 反向參考資料
反向參照是 length 和 距離碼 的元組:
- 長度代表要複製的掃描行中要複製的像素數量。
- 距離代碼是一個數字,指出先前看到像素的位置,也就是從該像素複製而來的像素。確切對應說明請參閱下文。
系統會使用 LZ77 前置字串編碼來儲存長度和距離值。
LZ77 前置字串編碼會將大型整數值分成兩個部分:「前置字串程式碼」和「額外位元」。前置字串程式碼是以熵程式碼儲存,而額外位元會按原樣儲存 (不含熵程式碼)。
理由:這種方法可減少熵程式碼的儲存空間需求。此外,較大的值通常很少見,因此會在圖片中使用額外的位元值。因此,這種方法可以帶來更佳的壓縮效果。
下表表示用來儲存不同值範圍的前置字串代碼和額外位元。
值範圍 | 前置字元程式碼 | 額外位元 |
---|---|---|
1 | 0 | 0 |
2 | 1 | 0 |
3 | 2 | 0 |
4 | 3 | 0 |
5..6 | 4 | 1 |
7..8 | 5 | 1 |
9.12 | 6 | 2 |
13.16 | 7 | 2 |
... | ... | ... |
3072..4096 | 23 | 10 |
... | ... | ... |
524289..786432 | 38 | 18 |
786433..1048576 | 39 | 18 |
從前置字串程式碼取得 (長度或距離) 值的虛擬程式碼,如下所示:
if (prefix_code < 4) {
return prefix_code + 1;
}
int extra_bits = (prefix_code - 2) >> 1;
int offset = (2 + (prefix_code & 1)) << extra_bits;
return offset + ReadBits(extra_bits) + 1;
距離對應
如前文所述,距離代碼是表示先前看到像素位置的數字,也就是從該像素複製而來的像素位置。這個子節會定義距離代碼與前一個像素位置的對應。
大於 120 的距離代碼表示掃描行順序中的像素距離,偏移值是 120。
最小距離代碼 [1..120] 具有特殊效果,僅供目前像素的鄰近區域使用。此區域由 120 個像素組成:
- 在目前像素上方 1 到 7 列的像素,最多為目前像素的左側或最多 7 欄。[這類像素總數 =
7 * (8 + 1 + 7) = 112
]。 - 與目前像素位於同一列,且最多至目前像素左側 8 欄的像素。[
8
這類像素]。
距離代碼 i
與鄰近像素偏移 (xi, yi)
之間的對應如下:
(0, 1), (1, 0), (1, 1), (-1, 1), (0, 2), (2, 0), (1, 2),
(-1, 2), (2, 1), (-2, 1), (2, 2), (-2, 2), (0, 3), (3, 0),
(1, 3), (-1, 3), (3, 1), (-3, 1), (2, 3), (-2, 3), (3, 2),
(-3, 2), (0, 4), (4, 0), (1, 4), (-1, 4), (4, 1), (-4, 1),
(3, 3), (-3, 3), (2, 4), (-2, 4), (4, 2), (-4, 2), (0, 5),
(3, 4), (-3, 4), (4, 3), (-4, 3), (5, 0), (1, 5), (-1, 5),
(5, 1), (-5, 1), (2, 5), (-2, 5), (5, 2), (-5, 2), (4, 4),
(-4, 4), (3, 5), (-3, 5), (5, 3), (-5, 3), (0, 6), (6, 0),
(1, 6), (-1, 6), (6, 1), (-6, 1), (2, 6), (-2, 6), (6, 2),
(-6, 2), (4, 5), (-4, 5), (5, 4), (-5, 4), (3, 6), (-3, 6),
(6, 3), (-6, 3), (0, 7), (7, 0), (1, 7), (-1, 7), (5, 5),
(-5, 5), (7, 1), (-7, 1), (4, 6), (-4, 6), (6, 4), (-6, 4),
(2, 7), (-2, 7), (7, 2), (-7, 2), (3, 7), (-3, 7), (7, 3),
(-7, 3), (5, 6), (-5, 6), (6, 5), (-6, 5), (8, 0), (4, 7),
(-4, 7), (7, 4), (-7, 4), (8, 1), (8, 2), (6, 6), (-6, 6),
(8, 3), (5, 7), (-5, 7), (7, 5), (-7, 5), (8, 4), (6, 7),
(-6, 7), (7, 6), (-7, 6), (8, 5), (7, 7), (-7, 7), (8, 6),
(8, 7)
舉例來說,距離代碼 1
表示鄰近像素的 (0, 1)
偏移值,也就是位於目前像素上方的像素 (X 方向為 0 像素差異,Y 方向為 1 像素差異)。距離代碼 3
也代表左上角像素。
解碼器可以將距離代碼 i
轉換為掃描行訂單距離 dist
,如下所示:
(xi, yi) = distance_map[i - 1]
dist = xi + yi * xsize
if (dist < 1) {
dist = 1
}
其中 distance_map
是上文說明的對應關係,xsize
則是圖片的寬度 (以像素為單位)。
5.2.3 色彩快取編碼
色彩快取會儲存最近在圖片中使用的一組顏色。
原因:這種方式有時,比起使用其他兩種方法 (如 5.2.1 和 5.2.2 所述),有效參照最近使用的顏色,有時可更有效率地參照這些顏色。
顏色快取代碼儲存如下。首先,有 1 位元值會指出是否使用色彩快取。如果這個位元為 0,表示沒有顏色快取代碼,也不會以編碼綠色符號和長度前置字元碼的前置字串程式碼進行傳輸。但是,如果這個位元為 1,則下一個動作會讀取顏色快取大小:
int color_cache_code_bits = ReadBits(4);
int color_cache_size = 1 << color_cache_code_bits;
color_cache_code_bits
會定義顏色快取的大小 (1 <<
color_cache_code_bits
)。color_cache_code_bits
允許的值範圍是 [1..11]。相容的解碼器必須指出其他值的位元串流毀損。
顏色快取是大小 color_cache_size
的陣列。每個項目會儲存一種 ARGB 顏色。(0x1e35a7bd * color) >> (32 -
color_cache_code_bits)
會將顏色編入索引,以便進行查詢。顏色快取中只會執行一次查詢,沒有衝突的解析。
在對圖片進行解碼或編碼時,所有顏色快取值中的所有項目都會設為零。顏色快取程式碼會在解碼時轉換為這個顏色。如要維護色彩快取的狀態,您必須插入每個像素,在快取中依反向參照或常值的形式,在快取中按照串流顯示順序插入該像素。
6 熵代碼
6.1 總覽
大部分資料都是使用標準前置字串程式碼編寫程式碼。因此,系統會透過傳送「前置字元代碼長度」來傳送代碼,這跟實際前置字元代碼相反。
具體來說,這種格式使用空間變化版本前置字元編碼。換句話說,圖片的不同區塊可能使用不同的熵程式碼。
原因:圖片的不同區域可能有不同的特性。因此,讓他們使用不同的熵代碼,您的壓縮效果可能也更佳。
6.2 詳細資料
編碼圖片資料由多個部分組成:
- 解碼及建構前置字串程式碼。
- 中繼前置字元代碼。
- 經過熵編碼的圖片資料。
任何指定的像素 (x, y) 都有五組相關聯的前置字元代碼,這些代碼按位元串流順序排序:
- 前置字元程式碼 #1:用於綠色通道、反向參照長度和顏色快取。
- 前置字元代碼 #2、#3 和 #4:分別用於紅色、藍色和 Alpha 通道。
- 前置字元代碼 #5:用於反向參照距離。
現在,我們將這組程式碼稱為「前置字元代碼群組」。
6.2.1 解碼及建構前置字元程式碼
本節說明如何從位元串流讀取前置字元代碼長度。
前置字串長度可以透過兩種方式編碼。使用的方法是透過 1 位元值指定。
- 如果這個位元是 1,表示這是簡單的程式碼長度代碼。
- 如果這個位元為 0,是一般程式碼長度代碼。
在這兩種情況下,串流中都有未使用的程式碼長度。這麼做可能效率低落,但因為格式允許使用。 描述的樹狀結構必須是完整的二進位樹狀結構。單一分葉節點會視為完整的二進位樹狀結構,可使用簡單的程式碼長度代碼或一般程式碼長度代碼進行編碼。使用一般程式碼長度碼編寫單一分葉節點時,只有一個程式碼長度為零,而單一分葉節點值會標示長度 1 (即使未使用單一分葉節點樹狀結構),即使並未使用任意位元。
簡易程式碼長度代碼
當特殊情況中,只有 1 或 2 個前置字串符號位於 [0..255] 且程式碼長度為 1
的範圍時,就可以使用這個變化版本。所有其他前置字串程式碼長度一律為零。
第一個位元代表符號的數量:
int num_symbols = ReadBits(1) + 1;
以下是符號值。
這個第一個符號是使用 1 或 8 位元編碼,視 is_first_8bits
的值而定。範圍分別為 [0..1] 或 [0..255]。系統會一律假設第二個符號 (如果有的話) 位於 [0..255] 範圍內,並使用 8 位元編碼。
int is_first_8bits = ReadBits(1);
symbol0 = ReadBits(1 + 7 * is_first_8bits);
code_lengths[symbol0] = 1;
if (num_symbols == 2) {
symbol1 = ReadBits(8);
code_lengths[symbol1] = 1;
}
這兩個符號必須不同。可使用重複的符號,但效率不彰。
注意:另一個特殊情況是「所有」前置字串長度為「零」 (空白前置字串) 時。舉例來說,如果沒有反向參照,距離的前置字串程式碼也可以空白。同樣地,如果同一個中繼前置碼程式碼中的所有像素都是使用顏色快取產生,則 Alpha、紅色、藍色的前置字串程式碼也可以空白。不過,此案例不需要特殊處理方式,因為空白前置字串程式碼可透過編碼的方式,例如含有單一符號 0
的程式碼。
正常碼長度代碼
前置字串程式碼的長度為 8 位元,如下所示。首先,num_code_lengths
會指定程式碼長度的數量。
int num_code_lengths = 4 + ReadBits(4);
程式碼長度本身使用前置字串進行編碼;較低層級程式碼長度為 code_length_code_lengths
則必須先讀取。其餘 code_length_code_lengths
(根據 kCodeLengthCodeOrder
中的訂單) 的其餘部分都是零。
int kCodeLengthCodes = 19;
int kCodeLengthCodeOrder[kCodeLengthCodes] = {
17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
};
int code_length_code_lengths[kCodeLengthCodes] = { 0 }; // All zeros
for (i = 0; i < num_code_lengths; ++i) {
code_length_code_lengths[kCodeLengthCodeOrder[i]] = ReadBits(3);
}
接下來,如果為 ReadBits(1) == 0
,每個符號類型 (A、R、G、B 和距離) 的不同讀取符號 (max_symbol
) 數量上限會設為字母表大小:
- G 管道:256 + 24 +
color_cache_size
- 其他常值 (A、R 和 B):256
- 距離代碼:40
否則,則會定義為:
int length_nbits = 2 + 2 * ReadBits(3);
int max_symbol = 2 + ReadBits(length_nbits);
如果 max_symbol
大於符號類型的字母大小,則點陣圖無效。
接著,系統會從 code_length_code_lengths
建立前置字串資料表,並用於讀取最多 max_symbol
個程式碼長度。
- 代碼 [0..15] 表示常值碼長度。
- 如果值為 0,表示未經過編碼。
- 值 [1..15] 代表相應程式碼的位元長度。
- 程式碼 16 會重複先前的非零值 [3..6] 次,也就是
3 + ReadBits(2)
次。如果在發出非零值之前使用代碼 16,則會重複的值 8。 - 程式碼 17 會發出長度為零的連續項目 [3..10],也就是
3 + ReadBits(3)
次。 - 程式碼 18 會發出長度為零的連續數量 [11..138],也就是
11 + ReadBits(7)
次。
讀取程式碼長度後,每個符號類型 (A、R、G、B 和距離) 的前置字元代碼就會以各自的字母大小組成。
一般程式碼長度代碼必須編寫完整的決策樹狀結構,也就是所有非零代碼的 2 ^ (-length)
總和必須確切為一。但這項規則有一個例外狀況,也就是單一分葉節點樹狀結構,分葉節點值會標示值 1,其他值為 0。
6.2.2 解碼中繼前置字串
如前文所述,此格式允許針對映像檔的不同區塊使用不同的前置字元碼。中繼前置字串是索引,用於識別要在圖片不同部分使用哪些前置字串。
「只有」將圖片用於 ARGB 映像檔的角色時,才能使用中繼前置字元碼。
中繼前置字元代碼有兩種可能,以 1 位元值表示:
- 如果這個位元為零,則圖片的任何地方都只會使用一個中繼前置字串程式碼。沒有儲存其他資料。
- 如果這個位元是 1,圖片會使用多個中繼前置字元程式碼。這些中繼前置字元碼會儲存為熵映像檔 (如下所述)。
像素的紅和綠色元件會定義 16 位元中繼前置字元程式碼,用於 ARGB 圖片的特定區塊。
熵圖片
熵映像檔會定義圖片的不同部分使用哪些前置字串代碼。
前 3 位元包含 prefix_bits
值。熵圖片的尺寸取自 prefix_bits
:
int prefix_bits = ReadBits(3) + 2;
int prefix_xsize = DIV_ROUND_UP(xsize, 1 << prefix_bits);
int prefix_ysize = DIV_ROUND_UP(ysize, 1 << prefix_bits);
其中 DIV_ROUND_UP
是較早定義的。
下一個位元包含寬度為 prefix_xsize
且高度 prefix_ysize
的熵圖片。
中繼前置字串程式碼的解釋
只要從熵圖片尋找最大中繼前置字串,即可取得 ARGB 映像檔中的前置字串群組數量:
int num_prefix_groups = max(entropy image) + 1;
其中 max(entropy image)
表示儲存在熵映像檔中最大的前置字串。
每個前置字串群組包含五個前置字串代碼,因此前置字串代碼的總數為:
int num_prefix_codes = 5 * num_prefix_groups;
考量到 ARGB 圖像中的像素 (x, y),我們可以取得對應的前置字串代碼,如下所示:
int position =
(y >> prefix_bits) * prefix_xsize + (x >> prefix_bits);
int meta_prefix_code = (entropy_image[position] >> 8) & 0xffff;
PrefixCodeGroup prefix_group = prefix_code_groups[meta_prefix_code];
我們假設 PrefixCodeGroup
結構確實存在,也就是一組包含五組前置字串代碼的結構。此外,prefix_code_groups
是 PrefixCodeGroup
的陣列 (大小為 num_prefix_groups
)。
然後,解碼器會使用前置字串群組 prefix_group
將像素 (x, y) 解碼,如「解碼 Entropy-Coded Image Data」(解碼 Entropy 編碼的圖片資料) 一節的說明。
6.2.3 解碼 Entropy 編碼的圖片資料
針對圖片中的目前位置 (x, y),解碼器會先識別對應的前置字串程式碼群組 (如上一節所述)。假設有前置字串群組,則像素會依照下列方式讀取及解碼。
接下來,使用前置字串 #1 讀取位元串流中的符號 S。請注意,S 是介於 0
至 (256 + 24 +
color_cache_size
- 1)
範圍中的任何整數。
S 的解釋取決於其值:
- 如果 S < 256
- 使用 S 做為綠色元件。
- 使用前置字串 #2 從位元串流讀取紅色。
- 使用前置字串 #3 從位元串流讀取藍色。
- 使用前置字串 #4 從位元串流讀取 Alpha。
- 如果 S >= 256 且 S < 256 + 24
- 使用 S - 256 做為長度的前置字元代碼。
- 從位元流中讀取額外位元。
- 從長度前置字串程式碼和額外位元讀取的反向參照長度 L。
- 使用前置字串 #5 讀取位元串流的距離前置字元程式碼。
- 讀取位元流之間的距離額外位元。
- 決定與距離前置字串程式碼的反向參照距離 D,以及讀取的額外位元。
- 從目前位置減去 D 像素的像素序列,複製 L 像素 (依掃描行順序)。
- 如果 S >= 256 + 24
- 使用 S - (256 + 24) 做為色彩快取的索引。
- 從該索引的色彩快取取得 ARGB 顏色。
7 整體格式結構
以下是擴展式 Backus-Naur 表單 (ABNF) RFC 5234 RFC 7405 中的格式。不會涵蓋所有詳細資料。圖片結尾 (EOI) 只會默示編碼為像素數量 (xsize * ysize)。
請注意,*element
表示 element
可重複 0 次以上。5element
表示 element
會重複出現 5 次。%b
代表二進位值。
7.1 基本結構
format = RIFF-header image-header image-stream
RIFF-header = %s"RIFF" 4OCTET %s"WEBPVP8L" 4OCTET
image-header = %x2F image-size alpha-is-used version
image-size = 14BIT 14BIT ; width - 1, height - 1
alpha-is-used = 1BIT
version = 3BIT ; 0
image-stream = optional-transform spatially-coded-image
7.2 轉換結構
optional-transform = (%b1 transform optional-transform) / %b0
transform = predictor-tx / color-tx / subtract-green-tx
transform =/ color-indexing-tx
predictor-tx = %b00 predictor-image
predictor-image = 3BIT ; sub-pixel code
entropy-coded-image
color-tx = %b01 color-image
color-image = 3BIT ; sub-pixel code
entropy-coded-image
subtract-green-tx = %b10
color-indexing-tx = %b11 color-indexing-image
color-indexing-image = 8BIT ; color count
entropy-coded-image
7.3 圖片資料的結構
spatially-coded-image = color-cache-info meta-prefix data
entropy-coded-image = color-cache-info data
color-cache-info = %b0
color-cache-info =/ (%b1 4BIT) ; 1 followed by color cache size
meta-prefix = %b0 / (%b1 entropy-image)
data = prefix-codes lz77-coded-image
entropy-image = 3BIT ; subsample value
entropy-coded-image
prefix-codes = prefix-code-group *prefix-codes
prefix-code-group =
5prefix-code ; See "Interpretation of Meta Prefix Codes" to
; understand what each of these five prefix
; codes are for.
prefix-code = simple-prefix-code / normal-prefix-code
simple-prefix-code = ; see "Simple Code Length Code" for details
normal-prefix-code = ; see "Normal Code Length Code" for details
lz77-coded-image =
*((argb-pixel / lz77-copy / color-cache-code) lz77-coded-image)
以下是可能的序列範例:
RIFF-header image-size %b1 subtract-green-tx
%b1 predictor-tx %b0 color-cache-info
%b0 prefix-codes lz77-coded-image