Jyrki Alakuijala 博士,Google, Inc.2023 年 3 月 9 日
摘要
WebP 无损是一种用于无损压缩 ARGB 图片的图片格式。通过 无损格式精确地存储和恢复像素值,包括 颜色值。适用于序列的通用算法 数据压缩 (LZ77)、前缀编码和颜色缓存用于 批量数据压缩。解码速度比 PNG 更快 而且压缩率要比使用这两种技术所能达到的 最新的 PNG 格式。
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 个字节:对于“RIFF”为 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 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 | 平均值 2(Average2(L, TR), T) |
6 | Average2(L, TL) |
7 | 平均值 2(L, T) |
8 | 平均值 2(TL, T) |
9 | 平均值 2(T, TR) |
10 | Average2(Average2(L, TL), Average2(T, TR)) |
11 | 选择(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 像素 非同凡响。最右列中的像素是使用模式预测的 [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
使用一个带符号的 8 位整数计算得出,该整数表示
3.5 定点数和 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 无损编码中存在动态概念:颜色缓存。)
颜色编制转换会检查图片中的唯一 ARGB 值的数量。如果该数字低于阈值 (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 表示
这四个像素合并在一起,每个像素的范围为 [0..3]。值为 3
表示合并了 8 个像素,每个像素的范围为 [0..1],
也就是二进制值。
这些值会打包到绿色组件中,如下所示:
width_bits
= 1:对于每个 x 值,其中 x ☰ 0 (mod 2),则会显示绿色的 位于 x 处的值的 4 个最低有效位 x / 2 处的绿色值,并将 x + 1 处的绿色值置于 x / 2 处绿色值的 4 个最高有效位。width_bits
= 2:对于每个 x 值,其中 x ☰ 0 (mod 4),则会显示绿色的 位于 x 处的 2 个最低有效位 x / 4 处的绿色值以及 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 元前缀代码”)。
- 预测器图片:存储预测器转换的元数据(请参阅 "Predictor Transform")。
- 颜色转换图片:由
ColorTransformElement
值创建 (在“Color Transform”中定义)针对不同的块 图片。 - 颜色索引图片:大小为
color_table_size
的数组(最多 256 个字符) ARGB 值),用于存储颜色索引转换的元数据(请参阅 “Color Indexing Transform”)。
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;
距离映射
如前所述,距离代码是一个数字,表示 要从中复制像素。该小节 定义了距离代码和上一个 Pixel。
大于 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
color 决定。系统会按 (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、red 和
如果生成了同一元前缀代码内的所有像素,则蓝色可能为空
使用颜色缓存不过,这种情况不需要进行特殊处理,因为
空前缀代码可编码为包含单个符号 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,则表示图片使用多个元前缀代码。这些元层 前缀代码以熵图像的形式存储(如下所述)。
像素的红色和绿色部分定义了一个 16 位元前缀代码, ARGB 图像的特定块。
熵图片
熵图像定义了将哪些前缀代码用于 图片。
前 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 作为长度前缀代码。
- 从比特流中读取额外的位数。
- 根据长度前缀代码和读取的额外位确定向后引用长度 L。
- 使用前缀代码 #5 从比特流读取距离前缀代码。
- 读取与比特流之间的距离的额外位。
- 根据距离前缀代码确定后向引用距离 D 和读取的额外位。
- 从当前位置(减去 D 像素)开始的像素序列中复制 L 像素(按扫描线顺序)。
- 如果 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