Jyrki Alakuijala 博士,Google, Inc.2023 年 3 月 9 日
摘要
WebP 无损是一种用于无损压缩 ARGB 图片的图片格式。通过 无损格式精确地存储和恢复像素值,包括 颜色值。适用于序列的通用算法 数据压缩 (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 位组成的无符号整数。在显示转换行为的代码中,这些值在以下位中编码:alpha 位于 31..24 位,红色位于 23..16 位,绿色位于 15..8 位,蓝色位于 7..0 位;不过,该格式的实现可以在内部自由使用其他表示法。
一般来说,WebP 无损图片包含标头数据、转换信息和实际图片数据。标头包含图片的宽度和高度。WebP 无损图像经过四种不同类型的转换, 。比特流中的转换信息包含数据 应用相应的反转换所需的操作。
2 个术语
- ARGB
- 一个像素值,由 Alpha、红色、绿色和蓝色值组成。
- ARGB 图片
- 一个包含 ARGB 像素的二维数组。
- 颜色缓存
- 一个小型哈希地址数组,用于存储最近使用的颜色,以便使用更短的代码调用它们。
- 颜色索引图片
- 可使用小整数(在 WebP 无损压缩中最多为 256)编入索引的颜色一维图像。
- 颜色转换图片
- 一个二维低分辨率图片,其中包含有关颜色分量相关性的数据。
- 距离映射
- 更改 LZ77 距离,使 二维邻近性。
- 熵图片
- 一张二维子分辨率图片,指示应该使用哪种熵编码方式 在图片中的相应方格中使用,也就是说,每个像素都是一个元图像, 前缀代码。
- LZ77
- 一种基于字典的滑动窗口压缩算法,用于发射符号或将其描述为过去符号的序列。
- 元前缀代码
- 一个小整数(最多 16 位),用于将元前缀中的元素编入索引 表格。
- 预测器图片
- 一个二维低分辨率图片,用于指明图像中特定方块使用了哪个空间预测器。
- 前缀代码
- 一种经典的熵编码方式,使用的位数较少 获取更频繁的代码。
- 前缀编码
- 一种对较大整数进行熵编码的方法,该方法使用熵编码对整数的几个位进行编码,并对其余位进行原始编码。这样一来,即使符号范围较大,熵代码的说明也能保持相对较小。
- 扫描行顺序
- 像素的处理顺序(从左到右以及从上到下),从 与左上角像素相比较完成一行后,从 左列。
3 RIFF 标题
标头开头是 RIFF 容器。其中包括 以下 21 个字节:
- 字符串“RIFF”。
- 分块长度的 32 位小端字节序值,即整个大小 数据块的 RIFF 标头进行控制。通常,此值等于载荷大小(文件大小减去 8 个字节:4 个字节用于“RIFF”标识符,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 Predictor 转换
预测器转换可用于减少熵,方法是利用 相邻像素往往是相关的。在预测器转换中, 当前像素值是根据已解码的像素预测的(在扫描线上 顺序),并且只对残差值(实际值 - 预测值)进行编码。绿色的 组件定义 14 个预测项中的哪一个用于 ARGB 图像的特定块。预测模式决定了要使用的预测类型。我们将图片分成正方形,将所有像素分成 使用相同的预测模式。
预测数据的前 3 位以数字形式定义块宽度和高度 位。
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 transform_width = DIV_ROUND_UP(image_width, 1 << size_bits);
转换数据包含图片每个块的预测模式。它是一种低分辨率图片,其中像素的绿色分量定义了 14 个预测器中的哪个预测器将用于 ARGB 图片的特定分块中的所有 block_width * block_height
像素。此次分辨率图片是采用
与第 5 章中介绍的方法相同。
块列数 transform_width
用于二维编制索引。对于像素 (x, y),可以通过以下公式计算相应的滤镜块地址:
int block_index = (y >> size_bits) * transform_width +
(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 | T |
3 | 土耳其 |
4 | 团队负责人 (TL) |
5 | Average2(Average2(L, TR), T) |
6 | 平均值 2(L、TL) |
7 | 平均值 2(L, T) |
8 | Average2(TL, T) |
9 | 平均值 2(T, TR) |
10 | Average2(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 像素,最左列的所有像素均为 T 像素。
针对最右列上的像素寻址 TR-pixel 是例外情况。最右列上的像素是使用模式 [0..13] 进行预测的,就像不在边界上的像素一样,但与当前像素在同一行上的最左侧像素会被用作 TR 像素。
将预测值的每个通道相加,即可获得最终像素值 与编码残差值相关联。
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 值。 Pixel。颜色转换将绿色 (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
使用表示 3.5 固定点数的符号 8 位整数和符号 8 位 RGB 颜色通道 (c) [-128..127] 进行计算,并定义如下:
int8 ColorTransformDelta(int8 t, int8 c) {
return (t * c) >> 5;
}
从 8 位无符号表示法 (uint8) 到 8 位有符号的转换
在调用 ColorTransformDelta()
之前需要一 (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 颜色索引转换
如果不存在太多唯一的像素值,创建颜色索引数组并将像素值替换为数组的索引可能更高效。颜色 Indexing 转换就可以做到这一点。(在 WebP 无损的情况下, 具体而言,不要将其称为 Palette 转换,因为 WebP 无损编码中存在动态概念:颜色缓存。)
颜色索引转换会检查 图片。如果该数字低于阈值 (256),则会创建一个由这些数字组成的数组 ARGB 值,之后会将像素值替换为 对应的索引:像素的绿色通道替换为 所有 alpha 值均设为 255,所有红色和蓝色值均设为 0。
转换数据包含颜色表大小和颜色表中的条目。解码器会按如下方式读取颜色编制索引转换数据:
// 8-bit value for the color table size
int color_table_size = ReadBits(8) + 1;
颜色表使用图像存储格式本身进行存储。可以通过读取图片(不含 RIFF 标头、图片大小和转换)来获取颜色表,假设高度为 1 像素,宽度为 color_table_size
。颜色表始终采用减法编码,以降低图像熵。增量
的调色板颜色包含的熵通常远远小于
这样可以为较小的图片节省大量内存。在解码过程中,可以通过分别按每个 ARGB 组件对之前的颜色组件值进行加法,并存储结果的最低 8 位,来获取颜色表中的每个最终颜色。
图片的逆变换只需将像素值(即颜色表的索引)替换为实际的颜色表值即可。索引编制 根据 ARGB 颜色的绿色分量确定。
// Inverse transform
argb = color_table[GREEN(argb)];
如果索引大于或等于 color_table_size
,则使用 argb 颜色值
应设置为 0x00000000(透明黑色)。
如果颜色表很小(等于或少于 16 种颜色),则会有几个像素 捆绑到单个像素中像素捆绑包中包含几个(2 个、4 个或 8 个) 像素转换为单个像素,从而可分别缩小图片宽度。像素 通过捆绑,可以对数据进行更高效的联合分布熵编码, 相邻像素并给模型提供一些类似于算术编码的好处, 熵代码,但只有当唯一值不超过 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 表示合并了 4 个像素,每个像素的范围为 [0..3]。值为 3
表示合并了 8 个像素,每个像素的范围为 [0..1],
也就是二进制值。
这些值会打包到绿色组件中,如下所示:
width_bits
= 1:对于每个 x 值(其中 x ≡ 0 [mod 2]),x 处的绿色值会放置在 x / 2 处绿色值的 4 个最低有效位,而 x + 1 处的绿色值会放置在 x / 2 处绿色值的 4 个最高有效位。width_bits
= 2:对于每个 x 值(其中 x ≡ 0 [mod 4]),x 处的绿色值会放置在 x / 4 处绿色值的 2 个最低有效位,x + 1 到 x + 3 处的绿色值会依次放置在 x / 4 处绿色值的更高有效位。width_bits
= 3:对于每个 x 值(其中 x ≡ 0 [mod 8]),x 处的绿色值会放置在 x / 8 处绿色值的最低有效位,x + 1 到 x + 7 处的绿色值会依次放置在 x / 8 处绿色值的更高有效位。
读取此转换后,image_width
将按 width_bits
下采样。这会影响后续转换的大小。可以使用 DIV_ROUND_UP
计算新大小,如前面所定义。
image_width = DIV_ROUND_UP(image_width, 1 << width_bits);
5 图片数据
图像数据是按扫描行顺序排列的像素值数组。
5.1 图片数据的作用
我们会以五种不同的角色使用图片数据:
- ARGB 图片:存储图片的实际像素。
- 熵图片:存储元前缀代码(请参阅 “Decoding of 元前缀代码”)。
- 预测器映像:存储预测器转换的元数据(请参阅“预测器转换”)。
- 颜色转换图片:由
ColorTransformElement
值创建 (在“Color Transform”中定义)针对不同的块 图片。 - 颜色编制索引图片:大小为
color_table_size
的数组(最多 256 个 ARGB 值),用于存储颜色编制索引转换的元数据(请参阅“颜色编制索引转换”)。
5.2 图像数据编码
图片数据的编码与其角色无关。
系统会先将图片划分为一组固定大小的块(通常为 16x16 块)。其中每个块都使用自己的熵码进行建模。此外, 可能有多个块共用相同的熵码。
原因:存储熵代码会产生费用。可以最大限度地降低 如果统计类似的块共用熵代码,从而存储该代码, 仅一次。例如,编码器可以通过使用统计属性对块进行分组来查找相似的块,也可以通过重复联接一对随机选择的块来查找相似的块,前提是这能减少编码图片所需的总位数。
每个像素都采用以下三种可能的方法之一进行编码:
- 前缀编码的字面量:每个通道(绿色、红色、蓝色和 Alpha)都独立进行熵编码。
- LZ77 向后引用:从 图片。
- 颜色缓存代码:使用较短的乘法哈希代码(颜色缓存) 索引)。
以下各小节将详细介绍上述各项。
5.2.1 前缀编码的字面量
像素存储为绿色、红色、蓝色和 Alpha 的带前缀编码值(按此顺序)。如需了解详情,请参阅第 6.2.3 节。
5.2.2 LZ77 向后参考
向后引用是长度和距离代码的元组:
长度和距离值使用 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 行且位于当前像素左侧最多 8 列或右侧最多 7 列的像素。[此类像素的总数 =
7 * (8 + 1 + 7) = 112
]。 - 与当前像素位于同一行的像素数,最多 8 个
列。[
8
个这样的像素]。
距离代码 distance_code
与相邻像素偏移量 (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)
相邻像素,即当前像素上方的像素(0 像素
X 方向上的差异和 Y 方向上的 1 像素差异)。
同样,距离代码 3
表示左上角像素。
解码器可以将距离代码 distance_code
转换为扫描线顺序距离 dist
,如下所示:
(xi, yi) = distance_map[distance_code - 1]
dist = xi + yi * image_width
if (dist < 1) {
dist = 1
}
其中 distance_map
是上述映射,image_width
是宽度
图片尺寸(以像素为单位)。
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。不过,这条规则有一个例外情况,即单叶节点树,其中叶节点值标记为值 1,其他值为 0。
6.2.2 元前缀代码解码
如前所述,此格式允许对 图片的不同块。元前缀代码是用于指明在图片的不同部分使用哪些前缀代码的索引。
只有在图片用作 ARGB 图片的角色时,才能使用元前缀代码。
有两种可能的元前缀代码,以 1 位 值:
- 如果此位为零,则整个图片中只使用一个元数据前缀代码。系统不会再存储任何数据。
- 如果该位为 1,则图像使用多个元前缀代码。这些元标记 前缀代码以熵图像的形式存储(如下所述)。
像素的红色和绿色分量定义了 ARGB 图片的特定块中使用的 16 位元前缀代码。
熵图片
熵图像定义了将哪些前缀代码用于 图片。
前 3 位包含 prefix_bits
值。熵图片的尺寸派生自 prefix_bits
:
int prefix_bits = ReadBits(3) + 2;
int prefix_image_width =
DIV_ROUND_UP(image_width, 1 << prefix_bits);
int prefix_image_height =
DIV_ROUND_UP(image_height, 1 << prefix_bits);
其中 DIV_ROUND_UP
如之前所定义。
接下来的位包含宽度 prefix_image_width
和高度的熵图像
prefix_image_height
。
元前缀代码的解释
您可以通过从熵图像中查找最大的元前缀代码来获取 ARGB 图片中的前缀代码组的数量:
int num_prefix_groups = max(entropy image) + 1;
其中 max(entropy image)
表示存储在
图像的熵。
由于每个前缀代码组包含 5 个前缀代码,因此前缀代码的总数为:
int num_prefix_codes = 5 * num_prefix_groups;
根据 ARGB 图片中的像素 (x, y),我们可以获取相应的前缀 按如下方式使用:
int position =
(y >> prefix_bits) * prefix_image_width + (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),如“解码编码图像
数据”。
6.2.3 解码熵编码的图片数据
对于图像中的当前位置 (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 作为长度前缀代码。
- 从比特流中读取长度的额外位。
- 根据长度前缀码和 读取额外的位。
- 使用前缀代码 #5 从比特流读取距离前缀代码。
- 读取与比特流之间的距离的额外位。
- 根据距离前缀代码和读取的额外位确定向后引用距离 D。
- 从开始的像素序列中复制 L 像素(按扫描行顺序) 减去 D 像素。
- 如果 S >= 256 + 24
- 使用 S - (256 + 24) 作为颜色缓存的索引。
- 从该索引处的颜色缓存中获取 ARGB 颜色。
7 形式的总体结构
下图显示了 Augmented Backus-Naur Form (ABNF) 中的格式 RFC 5234 RFC 7405。其中并未涵盖所有细节。图片结束 (EOI) 仅隐式编码为像素数(image_width * image_height)。
请注意,*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