图像编解码器中的像素数据处理全解析

本文深入探讨图像像素数据的编码与解码过程,涵盖色彩空间转换、预测模式、块变换、量化技术等核心压缩算法,解析现代图像格式如何通过复杂技术实现高效存储与传输。

在上一篇文章中,我们重点讨论了人类如何观看图像以及设备如何显示图像。现在让我们放大视角,探索图像的微小构建模块——像素及其背后的字节。

什么是图像像素?

要显示图像,设备需要从数据源获取图像不同部分的颜色信息。对设备而言,大多数图像只是网格,该网格中的每个单元格就是一个像素。像素是一个短暂的值;它没有大小或形状。由硬件和软件决定如何表示这些像素。

存储像素数据的方式有无限多种,因此设备需要理解图像格式,即数据的组织方式。在网络上,有几种常见格式:JPEG、AVIF、WebP、JPEG XL、PNG 等等。像素有几个重要属性:

  • 它们排列在网格中以形成图像,并且顺序很重要。
  • 每个像素保存颜色和亮度信息。
  • 这些值是离散的,并受图像格式定义的位数限制。

注意: AVIF 可以使用 8、10 或 12 位来存储像素数据。较新的 JPEG 格式 JPEG XL 支持每个颜色通道最多 32 位。最常见的设置是每个通道 8 位,值范围从 0 到 255。这为每个颜色分量仅提供 256 种可能的"色调"——总共约 1600 万种可能的颜色。

了解图像像素和设备像素之间的区别很重要。图像像素是图像数据的一部分,而设备像素是依赖于显示器的物理像素。设备像素的工作方式可能有所不同:例如,在 OLED 屏幕中,每个像素都是一个微小的发光二极管;在 LCD 中,它是由背光控制的晶体;在电子墨水显示屏中,它由形成图像的带电粒子组成。

显卡中的图像

为了让显示器显示图像,显卡需要将该图像作为信号发送到显示器。一个简化的解释是,显卡将图像像素数据存储为数字数组。让我们看看如何在 JavaScript 中操作像素数据:

1
// 示例代码:操作像素数据

提取的 ImageData 是一个连续的无符号 1 字节整数类型化数组。在示例中,图像不包含任何颜色。相反,每个图像像素有四个分量,所有有趣的信息都存储在第四个分量中——Alpha 通道。通过读取每第四个元素,我们可以通过 Alpha 通道扫描整个图像。

注意: Alpha 通道存储有关图像透明度的信息。值为 255 表示像素完全不透明,而 0 表示完全透明。

图像规格

例如,让我们看一个非常小的 100×133 像素图像:

如果不存在 Alpha 通道——意味着每个像素只有三个分量——并且我们使用 8 位颜色,图像的原始大小应约为 40 KB。但实际文件小于 3 KB。很快原因就会清楚。对于较大的图像,这种差异变得更加显著。

现在想象一下,不是单个图像,而是需要渲染一个短的 60 秒视频。以每秒 24 帧计算,总共是 1,440 帧。每帧 38 KB,总大小约为 55 MB。显然,需要有效地打包像素。

为了处理这个问题,已经开发了许多图像和视频格式:BMP、GIF、JPEG、PNG、JPEG2000、多个版本的 MPEG、H.264、VP8、VP9 和 AV1。因此,数据首先被编码,然后被解码——处理这个的组件称为编解码器(编码器,解码器)。

编码可以在 CPU 或 GPU 上进行。通常,图像数据位于 RAM 中,CPU 对其进行解码,结果发送到 GPU 以便在屏幕上显示。但当负载较重时——比如视频播放——上传压缩的图像数据更高效。在这种情况下,GPU 自行进行解码。

视频由两种类型的帧组成:内部帧和间帧。内部帧基本上是经过压缩的完整图像,而间帧存储关于图像部分如何从一帧移动到下一帧的信息。

注意: 更准确地说,间帧包含运动矢量。

内部帧压缩的工作原理与图像压缩相同。AVIF、HEIF 和 WebP 等格式实际上基于相关视频编解码器中使用的内部帧压缩。通常,编解码器的发展速度比图像格式快,因此图像格式常常成为视频编解码器开发的副产品。例如,在现代图像格式尚未被浏览器广泛支持时(现在情况已非如此),使用单帧视频代替图像。

图像编码器如何工作

编码器生成的位序列称为比特流。为了传输或存储这些数据,需要将其分割成块。这些块根据格式有不同的名称。例如,在 AV1 视频和 AVIF 图像中,它们称为 OBU(开放比特流单元)。每个单元都有自己的结构,以便解码器知道如何处理它。

但图像通常不仅仅是一个比特流单元序列。如前所述,它还可以包括 ICC 配置文件和其他元数据——如图像尺寸、色彩空间、相机设置、日期、时间和位置。所有这些都需要打包到一个容器中。容器有自己的规范,不同的媒体格式可以重用同一个容器。例如:

  • AVIF 是 HEIF 容器中的 AV1 字节流。
  • WebP 是 RIFF 容器中的 VP8 字节流(或无损 WebP)。

有时,图像格式定义自己的容器。PNG、JPEG 和 JPEG XL 将布局作为文件格式规范的一部分。

图像解码器如何工作

解码器通过读取文件开头来识别格式,文件开头通常包含一个魔数——几个用于识别格式的字节。对于 AVIF,这个魔数是 ftypavif。然后它遵循容器布局来查找相关数据。通常,元数据在前,然后是实际的字节流。让我们通过一些松鼠图像来看看这是如何工作的 😀

图像在文件开头包含大量元数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
width: 1536
height: 1536
bands: 3
format: uchar
coding: none
interpretation: srgb
xoffset: 0
yoffset: 0
xres: 2.83465
yres: 2.83465
filename: ./squirrel.jpg
vips-loader: jpegload
jpeg-multiscan: 1
interlaced: 1
jpeg-chroma-subsample: 4:2:0

如果其中大部分内容目前还不理解也没关系——其中一些条目将在后面解释。如您所见,宽度和高度存储在容器的开头。这对浏览器特别有用,浏览器可以快速在页面上为图像分配空间。

注意: 不要让浏览器计算图像的高度和宽度——这可能会导致令人不快的布局偏移。相反,在 <img> 元素上设置宽度和高度属性。

深入探讨编码器

那么编码器如何将 38 KB 的图像数据压缩到只有 3 KB?编码器使用一系列技术和算法,这些技术和算法利用了我们对图像的感知方式以及图像的结构方式。让我们逐一探讨。

有损和无损压缩方法

有两种压缩图像的方法:

  • 有损: 编码后的图像在此过程中会更改或删除一些信息,并且不再与原始图像相同。
  • 无损: 图像可以完全重建。

有损和无损压缩方法可以结合使用,有时可以用更少的字节提供更好的结果。例如,Alpha 通道(控制不透明度)可以进行无损压缩,而颜色通道使用有损压缩以提高效率。下表总结了几种流行的图像格式:

格式 压缩类型 备注
PNG 无损 始终无损
JPEG 有损 始终有损
WebP 混合 可以是有损 VP8 帧或使用不同算法的无损
AVIF 有损或无损
JPEG XL 有损或无损

色度子采样

人类对亮度的敏感度远高于对颜色的敏感度。YCbCr 色彩空间利用这一事实,将亮度(亮度)与颜色信息分离,从而实现更高效的压缩。编码器不是存储每个像素的完整颜色细节,而是跳过一些颜色数据。然后解码器根据附近像素的颜色重建它。这种技术称为色度子采样。

色度子采样使用三个数字来描述,对应一个 4×2 像素网格:

  • 第一个数字始终是 4,表示区域的宽度。
  • 第二个数字显示第一行存储了多少个色度样本(Cb 和 Cr)。
  • 第三个数字显示第二行存储了多少个色度样本。

AVIF 支持以下色度子采样格式:

  • 4:4:4 – 无子采样;保留所有颜色信息。
  • 4:2:2 – 丢弃一半的颜色信息。
  • 4:2:0 – 仅保留原始颜色数据的四分之一。

这是一个有损转换的例子。一旦应用,原始图像无法完全恢复,但对观看者来说仍然看起来相当不错,同时节省了存储空间和带宽。

利用空间局部性

让我们看一个代码示例来演示下一种技术。有两个画布:一个带有随机像素,另一个带有纯色。

1
// 示例代码:两个画布对比

尝试分别保存两个图像:右键单击每个并选择"另存为图像"。即使使用效率不高的浏览器编解码器,第一个图像也将约为 35 KB,而第二个图像只有 500 字节。大小相差 70 倍!原因是第一个图像中的像素之间没有关系。

在真实图像中,相邻像素通常以某种方式相关。它们可能共享相同的颜色,形成渐变,或遵循某种模式。此属性称为空间局部性。

图像包含的信息越少,压缩效果越好。编解码器充分利用此属性,将图像划分为区域并分别压缩每个区域。例如,JPEG 总是将图像分割成 8×8 块。更现代的图像格式允许灵活的块大小。例如,AVIF 使用从 4×4 到 128×128 的块。编解码器通常坚持使用 2 的幂次方块大小,以保持计算简单高效。

对于具有纯蓝色天空的图像,将该区域划分为更大的块更有效,这减少了存储不必要细节的需要。相同的规则适用于任何纯背景。但是编码器如何决定在何处以及如何分割图像?有几种技术可以用于此,我们接下来可以探讨。

AVIF 使用递归方法将图像分割成块。它首先将图像分割成称为超级块的较大块。然后,它分析每个块的内容,并决定是进一步分割还是保持原样。为了管理这种分割,AVIF 使用四叉树数据结构。四叉树也常用于地图应用程序中存储位置并有效查找附近对象。

您可以通过递归地将图像分割成四个正方形来自己构建四叉树。对于每个正方形,计算平均颜色和与平均值的偏差。如果偏差太高,则再次分割正方形。

1
// 示例代码:四叉树构建

预测模式

块包含的数据越少,压缩效果越好。尝试通过单击下面画布上的不同单元格来玩一个 8×8 像素块。几次点击后,标记为绿色的部分图案将被复制。不是存储网格中的每个像素,而是通过仅存储实际图像与最匹配图案之间的差异来节省空间。

1
// 示例代码:预测模式演示

现代图像编解码器使用相同的想法。它们预测每个块的基本结构,然后仅压缩实际块内容与预测之间的差异,称为残差。大多数图像都有一些底层结构,编解码器通过分析渐变或重复纹理等模式来利用这一点,以做出更好的预测。

块内的像素通常形成与其相邻值相关的模式。因此,编解码器不是使用静态模式,而是使用块的顶行和左列作为参考进行预测。以图像中的左侧示例为例:正方形内的像素与某些周围像素非常相似。不同的编解码器使用各种预测模式来找到表示此结构并最小化误差的最佳方法。例如:

  • 该块可以预测为其相邻像素的平均值。
  • 它可以在特定方向上重复像素(如图所示)。
  • 它可能组合多个相邻像素以创建更准确的预测。
  • 更高级的预测器考虑块内每个像素的坐标以优化结果。
  • 甚至更高级的模式通过递归地从先前的行和列预测像素来更进一步。
  • 编码器不是仅依赖参考像素,而是使用先前预测的像素生成补丁。

通过改进这些预测,编解码器可以显著减少需要存储或传输的数据量。例如,AVIF 的一种预测模式基于左邻参考像素、右上参考像素以及块内像素的坐标。预测不仅限于空间结构——颜色通道也可以连接。这个想法在 AVIF 和 JPEG XL 中都有使用。相关的预测模式,基于亮度的色度预测,根据相应的 Y(亮度)通道的信息预测颜色通道(UV)。

一些编解码器不是预测像素,而是存储复制另一个块内容的指令。这对于屏幕截图或具有重复图案的图像特别有用,因为它减少了冗余。预测模式的数量因编解码器而异。AVIF 有很多(71 种),WebP 较少(10 种),而 JPEG 根本不使用预测。

块变换

编解码器进行预测,然后找到与该预测的像素差异。接下来,编解码器需要有效地存储这些差异,这时就是丢弃一些数据的时候了,即应用有损变换。您可以使用相同的图案类比来思考。现在有一组固定的图案,目标是找出组合它们以获得接近原始块的最佳方式。这就像通过堆叠多个图层来构建图像,每个图层具有不同的不透明度——每个图层都对最终结果有贡献,诀窍是猜出每个图层的正确不透明度。

实现这一点的数学方法称为离散余弦变换(DCT)。为什么这有助于减小文件大小?人类对高频分量(如微小点或精细纹理)不太敏感。该变换分离这些高频和低频部分,使得更容易决定可以安全丢弃哪些细节。一种著名的可视化方法是通过 DCT 基函数,它显示不同频率分量如何对图像做出贡献。

注意: 虽然这是一个有用的说明,但请记住,现代编解码器将变换应用于残差,而不是原始图像。

让我们通过一个例子看看这是如何工作的。想象一个 8×8 块,其中心有一条水平 2 像素厚的线:

1
// 示例代码:DCT 变换演示

当然,实际的实现是高效且优化的:

  • 编解码器不是一次性将 DCT 应用于整个块,而是首先对行使用两个一维 DCT,然后对列使用。
  • 一些编解码器支持替代变换。例如,AVIF 可以使用正弦变换代替 DCT。
  • 编解码器使用快速算法,如基于蝶形算法的快速 DCT。
  • 变换步骤使用 SIMD 指令针对向量操作进行了优化,这使得 CPU 能够用一个命令对多个值应用相同的操作。
  • DCT 也可以直接在 GPU 上执行。AV1 编解码器(AVIF 基于此)支持这一点。

在更高级的编解码器中,变换可以应用于块的部分而不是整个块。例如,AVIF 允许将一个块细分(最多两次)为更小的块,并对每个子块分别应用变换。

在此阶段,由于浮点计算中的舍入,会引入小的有损误差。但仅此步骤并不能显著减小文件大小。

量化

为了准备图像进行压缩,编解码器将浮点系数转换为整数,并将每个系数除以一个特定的数字。目标是使除法后得到尽可能多的零——数据越少意味着压缩效果越好。这些数字称为量化参数。JPEG 中使用的一组流行的量化值如下所示:

亮度量化矩阵 QY:

1
2
3
4
5
6
7
8
[ 16  11  10  16  24  40  51  61 ]
[ 12  12  14  19  26  58  60  55 ]
[ 14  13  16  24  40  57  69  56 ]
[ 14  17  22  29  51  87  80  62 ]
[ 18  22  37  56  68 109 103  77 ]
[ 24  35  55  64  81 104 113  92 ]
[ 49  64  78  87 103 121 120 101 ]
[ 72  92  95  98 112 100 103  99 ]

色度量化矩阵 QC:

1
2
3
4
5
6
7
8
[ 17  18  24  47  99  99  99  99 ]
[ 18  21  26  66  99  99  99  99 ]
[ 24  26  56  99  99  99  99  99 ]
[ 47  66  99  99  99  99  99  99 ]
[ 99  99  99  99  99  99  99  99 ]
[ 99  99  99  99  99  99  99  99 ]
[ 99  99  99  99  99  99  99  99 ]
[ 99  99  99  99  99  99  99  99 ]

这些参数对于亮度和颜色通道是不同的。量化参数存储在图像容器中,对于 JPEG,质量取决于我们如何缩放这些参数。系数越大,应用后得到的零就越多。

在更高级的编解码器中,过程要复杂得多。使用专门的算法来正确舍入量化的结果。此外,可以将不同的量化参数应用于不同的块集,从而允许图像不同部分的质量发生变化。

额外内容:隔行扫描

由于不同的系数代表不同级别的细节,因此几乎可以免费获得隔行扫描(也称为渐进式渲染)。为此,系数被重新排序,以便具有粗略(基本)细节的系数首先被存储(和加载)。因此,图像以多个细节层构建,称为扫描。第一次扫描显示图像的粗略、通常模糊的版本。每个后续扫描逐步添加更多细节。

对于大多数图像格式,这种重新排序不会增加文件大小,因此这是一个有用的技巧。但要小心隔行 PNG——它们使用不同的方法,确实会增加文件大小。

熵编码

现在我们接近最后一步,是时候解释为什么纯色图像与随机噪声相比如此之小。原因是熵编码。在编解码器获得量化系数之后,下一步是尽可能缩小二进制大小。块的系数以锯齿形顺序扫描,并且使用来自熵编码家族的算法对结果序列进行编码。想法很简单:对出现更频繁的值使用更少的位。让我们尝试自己编码一个基本版本。

假设我们有这个数字字符串: 124 124 124 124 10 123 123 1 1 2

第一步是计算每个数字在序列中出现的频率。

频率
124 4
1 2
123 2
2 1
10 1

现在我们可以将最短的二进制代码分配给最频繁的值,如下所示:

1
2
3
4
5
124 - 0
1 - 10
123 - 110
2 - 1110
10 - 1111

我们可以仅使用 24 位而不是 88 位对字符串进行编码 000011111101101010101110 🎉。这就是 JPEG 实现压缩的方式。使用的算法称为霍夫曼编码。一些编解码器使用更高级的算法,如算术编码或非对称数字系统。算术编码允许压缩更接近理论极限,与霍夫曼编码相比实现更高的压缩率。

注意: 此限制由数据的香农熵定义。它表示编码该数据所需的最小位数。

后处理

有损压缩会引入各种伪影,因为它去除了高频分量。这可能导致几种可见效果,例如:

  • 块效应 – 可见的方形图案,尤其是在平坦区域
  • 模糊边缘
  • 颜色失真 – 不准确或偏移的颜色
  • 振铃效应 – 边缘周围的光晕或回声状图案
  • 蚊式噪声 – 边缘周围的小闪烁点或嗡嗡声

为了减少这些伪影,一些编码器添加后处理过滤器。这些过滤器由解码器在重建图像像素时应用。过滤器设置存储在字节流中,因此解码器可以在解码期间读取并应用它们。您可能记得应用高斯模糊到图像以减少噪声的旧技巧。这是一个类似的想法,但现代编解码器使用更先进的算法。

  • 去块过滤器 – 例如,JPEG XL 中使用的 Gabor 过滤器
  • 维纳过滤器 – 在 AV1 中用于减少噪声
  • CDEF(约束方向增强过滤器) – 有助于减少振铃伪影

通常,如果使用多个过滤器,它们会形成一个具有精确应用顺序的流水线。例如,CDEF 在去块过滤器之后应用。

总结

在本文中,我们探讨了图像编码的工作原理,它如何与人类感知相关联,以及解码如何从比特重建图像。大多数编解码器遵循相同的核心思想,但它们使用不同的方法和技术。它们支持更多的块大小、额外的预测模式、各种类型的变换、不同的编码算法,并且通常包括一个后处理流水线来优化最终图像。

明显的结论是,现代编解码器更复杂(有时更慢),但它们通常产生更好的结果。然而,“更好"并不总是容易定义。一些问题仍然存在:编解码器复杂性和输出质量之间的权衡是什么?哪些编解码器在不同情况下效果最好?

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计