抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

图片文件结构与PNG编码

图像存储

颜色在计算机的存储

  • 颜色模式

RGB色彩模式是颜色模式之一(除此之外还有YUV模式和CMYK模式等等)

在实际的开发中,我们可能还会遇到RGBA模式,其中的A代表的是alpha通道,一般表示透明度,值从0%-100%,0-表示完全透明,100-表示完全不透明。

RGB(255, 128, 196):有3个颜色分量,分别是R的值255,G的值128,B的值196,这三个分量分别用了8位存储。咱们前面提到过8位存储,可以存储256个值。

关于RGB的颜色表示还有一种16进制表示方式,比如刚才的RGB(255,128,196)换算成16进制为#FF80C4

  • 像素

每一个像素都有一个明确的位置和被分配的色彩值

  • 分辨率(Resolution)

又称解析度,可以细分为显示分辨率、图像分辨率、打印分辨率和扫描分辨率。

显示分辨率:分辨率 4000x3000

图像分辨率: 常用单位,dpi(点每英寸),ppi(像素每英寸)。1英寸=2.54厘米(cm),它的实质是描述了图像的真实大小。比如“水平分辨率 72dpi”

  • 图像文件格式

BMP(Bitmap):是Windows操作系统中的标准图像文件格式,采用位映射存储格式,无压缩,BMP文件的图像深度可选1、4、8、24位。BMP最大的特点就是没有压缩(所以它的图像大小你可以直接算出来)

JPEG联合图像专家小组(Joint Photographic Experts Group,JPEG)是一种针对图像的有损压缩标准方法。使用的一种失真压缩标准方法,24 bit真彩色,不支持动画、不支持透明色。

PNG便携式网络图形(Portable Network Graphics,PNG)是一种无损压缩的位图图形格式格式是无损(不失真)数据压缩的,PNG格式有8位、24位、32位三种形式,其中8位PNG支持两种不同的透明形式(索引透明和alpha透明)。

GIF图像互换格式(Graphics Interchange Format,GIF)是一种无损压缩的位图图形格式,采取8位色重现图像。

图片的基本概念

  • 位图(Bitmap)

又叫格栅图或者点阵图,是利用像素阵列来表示的图像。位图的每个像素都被分配了特定的位置和颜色值,颜色值可以由RGB组合或者灰度值表示。

  • 位深度(Bit Depth)

每个像素用来表示颜色信息的位数,位深度越大,颜色越逼真,占用空间也就越大。例如,位深度为1的位图的像素只能为黑色或者白色,位深度为8的图像像素有2^8^(即256)个可能的颜色值。

  • 颜色通道

RGB图像的像素由红绿蓝三个颜色通道组成,8位/通道的图像中,每个通道有256个可能值,因此每个像素有24位可能的值,通常将24位RGB位图成为真色彩位图。

色深度与位深度

色深度:一个颜色通道的位数,单位为 位/通道 。常有8bit、10bit

位深度:一个像素的位数

  • Alpha通道

在原有的图片编码基础上,添加像素的透明度信息,因为通常把RGB三种颜色信息分别成为三种颜色的通道,所以透明度信息也成为Alpha通道。

  • 矢量图(Vector)

用点、直线或者多边形等基于数学方程的几何图形表示的图像。

位图与矢量图

位图的放大实际上是每个像素的放大,单个像素的放大会使线条和形状参差不齐,导致我们看到的图片“模糊不清晰”。而矢量图通过路径和颜色来记录图片信息,放大之后并不会失真。

位图处理已经标准化,并且硬编码到显卡内,GPU能够处理位图。但矢量图的处理只能依靠CPU。

PNG编码与解码

PNG文件结构

PNG图像格式文件由文件署名和数据块(chunk)组成。

文件署名域(Signature)

一张 PNG 图片二进制数据的开头必须是这 8 字节:

0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a 转换为 10 进制是 137, 80, 78, 71, 13, 10, 26, 10

(Linux下我们可以用file命令直接查看文件的实际格式,但是他本质上也是利用文件头标志来进行文件类型判断的。)

数据块(Chunks)

有两种类型的数据块,一种是称为关键数据块(critical chunk);另一种叫做辅助数据块(ancillary chunks)。

每个数据块都包含图片的描述信息,数据块由四个部分组成:

名称 长度 说明
Length 4 bytes 指定 Chunk Data 的长度
Chunk Type 4 bytes 指定数据块类型
Chunk Data 可变 按照Chunk Type存放指定数据
CRC 4 bytes 检查是否有错误的循环冗余代码

这里要注意 Length 保存的是该数据块中 Chunk Data 的长度,也就是说,一个数据块的长度应该为 Length 的值加上12 bytes。

Chunk Type 由4个字符组成,:第一个字符是否大写(critical),决定了该数据块是否是关键(critical)数据块;第二个字符大写表示公开,小写表示私有;第三个字符规定必须大写;第四个字符大写则表示只能在关键数据块不变时被复制,小写则表示任何情况下能复制。

关键数据块

关键数据块定义了4个标准数据块,每个PNG文件都必须包含它们。

1、文件头数据块IHDR(header chunk)

包含有PNG文件中存储的图像数据的基本信息,并要作为第一个数据块出现在PNG数据流中,而且一个PNG数据流中只能有一个文件头数据块。它用13个字节按照顺序来表示图像数据的基本信息:

域的名称 长度 说明
Width 4 bytes 图片宽度,以像素为单位
Height 4 bytes 图片高度,以像素为单位
Bit Depth 1 byte 图像深度
Color Type 1 byte 颜色类型
Compression Method 1 byte 压缩算法
Filter Method 1 byte 滤波方法
Interlace Method 1 byte 扫描方法
  • Bit Depth代表图像深度。索引彩色图像:1,2,4或8;灰度图像:1,2,4,8或16;真彩色图像:8或16;
  • Color Type(颜色类型)PNG 图片一共有 5 种色彩类型,0 代表灰度颜色,2 代表用 RGB 表示颜色,即 (R, G, B)3 代表用色板表示颜色,4 代表灰度和透明度来表示颜色,6 代表用 RGB 和透明度表示颜色,即 (R, G, B, A)。色板的色彩类型里,每个像素是由 1 个色彩通道表示的。
  • Co2,mpression Method 代表了压缩算法。目前只支持 0,表示 deflate/inflate。(Deflate/inflate 是一种结合了 LZ77 和霍夫曼编码的无损压缩算法,被广泛运用于 7-zipzlibgzip 等场景。)
  • Filter Method 代表在压缩前应用的过滤函数类型,目前只支持 0。过滤函数类型 0 里面包括了 5 种过滤函数。
  • Interlace Method 代表图片数据是否经过交错,0 代表非隔行扫描,1 代表 Adam7。

2、调色板数据块PLTE(palette chunk)

它包含有与索引彩色图像(indexed-color image)相关的彩色变换数据,它仅与索引彩色图像有关,如果存在,要放在图像数据块(image data chunk)之前。真彩色的PNG数据流也可以有调色板数据块,目的是便于非真彩色显示程序用它来量化图像数据,从而显示该图像。结构如下:

颜色 字节 意义
Red 1 byte 0 = 黑色, 255 = 红
Green 1 byte 0 = 黑色, 255 = 绿色
Blue 1 byte 0 = 黑色, 255 = 蓝色

PLTE数据块是定义图像的palette信息,PLTE可以包含1~256个palette信息,每一个palette信息由3个字节组成,因此palette数据块所包含的最大字节数为768,palette的长度应该是3的倍数,否则,这将是一个非法的palette。

对于索引图像,palette信息是必须的,palette的颜色索引从0开始编号,然后是1、2……,palette的颜色数不能超过色深中规定的颜色数(如图像色深为4的时候,调色板中的颜色数不可以超过2^4=16),否则,这将导致PNG图像不合法。

3、图像数据块IDAT(image data chunk)

存储实际的图片数据,一个图片数据流可能包含多个连续顺序的 IDAT 数据块。创建 IDAT 数据块的步骤:

  1. 将扫描获得的图片信息及其大小储存,具体的图片信息和大小规则在 IHDR 中保存
  2. 采取 IHDR 指定的过滤方法,对图像数据进行过滤。目前只有定义了method 0
  3. 采取 IHDR 指定的压缩方法,对过滤的数据进行压缩

如果想要读取图片信息,将上面三个步骤逆序执行即可。

4、图像结束数据IEND(image trailer chunk)

它用来标记PNG文件或者数据流已经结束,并且必须要放在文件的尾部。

如果我们仔细观察PNG文件,我们会发现,文件的结尾12个字符看起来总应该是这样的:

00 00 00 00 49 45 4E 44 AE 42 60 82

不难明白,由于数据块结构的定义,IEND数据块的长度总是0(00 00 00 00,除非人为加入信息),数据标识总是IEND(49 45 4E 44),因此,CRC码也总是AE 42 60 82

表示数据块开始的IHDR必须放在最前面, 表示PNG文件结束的IEND数据块放在最后面,其他数据块的存放顺序没有限制。

辅助数据块

(cv查表)

符号 说明
bKGD 指定默认的背景颜色
cHRM 校准色度
dSIG 数字签名
eXIf Exif metadata 信息
gAMA 指定亮度的伽马校准值2,
hIST 色彩直方图,估算颜色的使用频率
iCCP ICC color profile
iTXt 以键值对方式存储可能的压缩文本和翻译信息
pHYs 指定像素大小或图片比例
sBIT 除了指定位深度(8,16 等等)外,保存原始数据以便恢复
sPLT 提出建议使用的调色板,可能有多个
sRGB 指明是否使用sRGB color space
sTER 储存立体视图(stereoscopic)的相关信息,
tEXt 保存作者等键值对的文本信息
tIME 上次修改时间
tRNS 保存透明信息,分为不透明、全透明、Alpha
zTXt 保存压缩后的文本信息以及对应的压缩方式标识

压缩

PNG的压缩分为两个步骤:预解析(过滤)和压缩

隔行扫描

隔行扫描(Interlacing),也称交错扫描(interleaving),是一种位图编码的方式,编码时不严格按照相邻行进行。当连接速度比较慢时,采用隔行扫描的图片可以渐进地进行显示,即先显示较为模糊的版本,然后逐渐变清晰。如图:

img

我们常用的 GIF、PNG、JPEG 都采用了隔行扫描的方式,其中 PNG 采取的是二维、七通道的Adam7,二维表示扫描支持竖直和水平方向,7代表一张图片会被拆分为7个子图片。

20190819143548.png

和 GIF 采取的一维、四通道算法相比,PNG 在网速相同的情况下,尤其是网速较慢时,能够更快地显示出图片的基本外貌。

Filter(过滤)

PNG编码使用差分对原始像素数据进行Filter,该过程无任何压缩损失,并且完全可逆。对图像来说,存储残差所需的比特远远小于实际图像所需,这也是差分编码的收益来源

  • 差分编码

[2,3,4,5,6,7,8]会变成[2,1,1,1,1,1,1],其中 [2, 3-2=1, 4-3=1, 5-4=1, 6-5=1, 7-6=1, 8-7=1]

如果数据是线性相关的(也就是说,值与前一个值有一些很小的差异),那么最终会将数据集的值转换为大量重复的小值,这些小值更容易被压缩。 PNG文件格式使用称为“filtering”的delta编码格式。基本上,对于每一个像素扫描线,当前像素都是按照与左边像素、上面像素和上面左侧像素的某种关系进行编码的。

对于不同的内容,须使用不同的Filter类型来提升压缩收益,过滤的重点是将大多数转换后的字节将聚集在零附近,从而为压缩引擎提供更小、更可预测的字节值范围来处理。

为了便于描述,规定了相邻像素相对当前像素的方位:左(A)、上(B)、左上(C)。

C B
A X

上文提到的 5 种过滤函数为:

过滤类型 存储值(O)=实际值(X)-预测值(R)
0 O = X
1 O = X - A
2 O = X - B
3 O = X - (A+B)/2
4 Peath(A,B,C)

Peath(A,B,C): Paeth预测器(A,B,C的线性函数)

Peath具体计算为

1
2
3
4
5
6
7
8
9
10
11
base = AboveRow[j] + LeftCol[i] – AboveRow[-1]
pLeft = Abs(base – LeftCol[i]) // abs(A-C)
pTop = Abs(base – AboveRow[j]) // abs(L-C)
pTopLeft = Abs(base – AboveRow[-1]) // abs(A+L-2*C)

if (pLeft <= pTop && pLeft <= pTopLeft)
pred[i][j] = LeftCol[i] // 使用左边参考像素值
else if(pTop <= pTopLeft)
pred[i][j] = AboveRow[j] // 使用上方参考像素值
else
pred[i][j] = AboveRow[-1] // 若上方与左边均判断为不存在边界的情况,则使用左上方的值

选择合适的过滤器?

没有。每一行进行增量测试压缩,保存最小的结果,然后对下一行重复,对于20行的图像相当于对整个图像进行五次过滤和压缩。对于将被传输和解码多次的图像来说,这可能是一个合理的权衡。

过滤器也很少用于低位深度(灰度)图像,尽管在极少数情况下将此类图像提升到 8 位然后过滤是有效的。但是,一般来说,过滤器类型 None 是最好的。

一些接近最优的经验法则:比如对调色板图像以及低于8位的灰度图像不使用过滤器;对于其他的图像,选择最小化绝对差和的滤波器;不是使用256模除(%),而是使用标准的带符号数学,然后获取abs值,并将它们全部添加到给定行中,然后比较其他筛选器类型的和,选择给出最小和的过滤器。

Deflate

正式的压缩阶段使用 Deflate 进行压缩,它由 Huffman 编码 和 LZ77 压缩构成。Deflate是一种压缩数据流的算法,任何需要流式压缩的地方都可以用(但我并没有去了解它具体的实现,所以就略过吧

解码

(图片和数据均来自一步一步解码 PNG 图片

示例图片

👆 这是一张我们接下去要解码的图片,但它太小了,放大了展示给大家看下。👇

img

二进制数据如下

0 ~ 3 4 ~ 7 8 ~ 11 12 ~ 15
0 ~ 15 137, 80, 78, 71 13, 10, 26, 10 0, 0, 0, 13 73, 72, 68, 82
16 ~ 31 0, 0, 0, 2 0, 0, 0, 2 2, 3, 0, 0 0, 15, 216, 229
32 ~ 47 183, 0, 0, 0 1, 115, 82, 71 66, 1, 217, 201 44, 127, 0, 0
48 ~ 63 0, 9, 112, 72 89, 115, 0, 0 11, 19, 0, 0 11, 19, 1, 0
64 ~ 79 154, 156, 24, 0 0, 0, 12, 80 76, 84, 69, 255 0, 0, 0, 255
80 ~ 95 0, 0, 0, 255 255, 255, 255, 251 0, 96, 246, 0 0, 0, 4, 116
96 ~ 111 82, 78, 83, 255 255, 255, 127, 128 144, 197, 89, 0 0, 0, 12, 73
112 ~ 127 68, 65, 84, 120 156, 99, 16, 96 216, 0, 0, 0 228, 0, 193, 39
128 ~ 143 168, 232, 87, 0 0, 0, 0, 73 69, 78, 68, 174 66, 96, 130

每个表格的单元格内有 4 字节数据,每个字节由 8 位组成,1 位代表的是 0 或者 1 的一个数字。

  • 签名部分
0 ~ 3 4 ~ 7
137, 80, 78, 71 13, 10, 26, 10
0x89, 0x50, 0x4e, 0x47 0x0d, 0x0a, 0x1a, 0x0a

这张图片的前 8 个字节满足PNG签名的要求。

  • 文件头数据块部分
8 ~ 11 12 ~ 15
0, 0, 0, 13 73, 72, 68, 82
长度 类型
13 IHDR
16 ~ 19 20 ~ 23 24 ~ 27 28
0, 0, 0, 2 0, 0, 0, 2 2, 3, 0, 0 0
width height depth, colorType, compression, filter interlace
2 2 2, 3, 0, 0 0
29 ~ 32
15, 216, 229, 183
CRC32 校验和
  • PLTE部分
75 ~ 78 79 ~ 82 83 ~ 86
255, 0, 0, 0 255, 0, 0, 0 255, 255, 255, 255

色板中包含的数据是 RGB 数据,以 R, G, B 的形式保存,这里一共 12 字节,表示了 4 个色块。得到的色板信息如下:

色板

1
[[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 255]]
  • tRNS部分
99 ~ 102
255, 255, 255, 127

这个数据块为色板提供透明信息,每个字节表示一个色块的透明信息。与色板组合后的色板如下:

色板

1
[[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [255, 255, 255, 127]]
  • IDAT部分

我们重点关注图像数据块的解码部分。

107 ~ 110 111 ~ 114
0, 0, 0, 12 73, 68, 65, 84
长度 类型
12 IDAT

声明 12 字节的 IDAT像素数据

115 ~ 118 119 ~ 122 123 ~ 126
120, 156, 99, 16 96, 216, 0, 0 0, 228, 0, 193

所有行的像素数据会经过 deflate 压缩算法压缩。所以,需要对这里的像素数据解压,这里直接使用了 zlib.inflate() 函数。

解压出来的像素数据是 Uint8Array:0, 16, 0, 176

扫描线 Scanline

一根扫描线包含图片一行像素的数据。我们知道这张图片的高度是 2,也就是像素数据中有 2 行扫描线。

一根扫描线由 1 字节的过滤函数标记和像素信息组成。像素信息一个接一个地排列,中间没有多余的空位。如果扫描线长度不足以填满字节的位数,最后几位会被补齐。一根扫描线的结构如下:

过滤函数 像素…[补齐…]
8 位 每像素位数 * 每行像素数 + 补齐

色彩类型 - 色彩通道 - 通道深度 - 每像素位数

色彩类型 色彩 每像素通道数 通道深度 每像素位数
0 灰度 1 1, 2, 4, 8, 16 1, 2, 4, 8, 16
2 真彩色(RGB) 3 8, 16 24, 48
3 色板 1 1, 2, 4, 8 1, 2, 4, 8
4 灰度和透明度 2 8, 16 16, 32
6 色彩色和透明度(RGBA) 4 8, 16 32, 64

这张图片的色彩类型是 3,所以每个像素包含 1 个色彩通道。又因为图片的通道深度是 2,所以我们知道每个像素是用 2 位来表示的。

解码扫描线

过滤函数 像素…[补齐…]
8 位 每像素 2 位 * 2 像素 + 4 位补齐 = 8 位
0 0 00010000 (16)
1 0 10110000 (176)

这张图片里面的过滤函数 0 表示这张图数据未经过滤。所以我们只要保留原始数据就行了。

扫描线像素

第 1 列 第 2 列 补齐…
0 00 01 0000
1 10 11 0000

这里每个像素中的数据表示了这个像素的颜色在色板中的索引。根据色板,我们可以还原出图片的像素信息:[[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [255, 255, 255, 127]]

图片像素

行\列 0 1
0 (255, 0, 0, 255) (0, 255, 0, 255)
1 (0, 0, 255, 255) (255, 255, 255, 127)

最后可以得到这样一个PNG图片信息:

1
2
3
4
5
imageData = {
width: 2,
height: 2,
data: [255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 127],
};

参考

PNG (Portable Network Graphics) Specification http://www.libpng.org/pub/png/spec/1.2/PNG-Contents.html

PNG文件格式 https://rimson.top/png-format/

PNG文件格式详解 https://blog.mythsman.com/post/5d2d62b4a2005d74040ef7eb/

Portable Network Graphics (PNG) Specification (Second Edition) https://www.w3.org/TR/PNG/

一步一步解码 PNG 图片 https://vivaxyblog.github.io/2019/12/07/decode-a-png-image-with-javascript-cn.html

评论