根据任务内容,我开始用wxMEdit查看图片文件,尝试研究计算机眼里的图片

神奇东西

我利用Windows画图制作了这么一张简单的3*4的图片。

里面的颜色都是由0或255组成,便于研究

1.PNG

神奇东西.png

果然十六进制不是给人类读的

但在上网一番学习后,我也对其有了一定的理解

Hpng

00行:00-07这8个字节是代表png格式的Header,或者说是png文件的固定开头,这样子电脑才能知道这是个png文件(01-03的ASCII转换过来就是PNG,有点意思)

现在再来了解一个东西:数据块

因为除开png头部分其他都可视作数据块的组成

数据块由下面四个部分组成

名称 字节数 解释
Length(长度) 4 指明Chunk Data的长度,单位:字节
Chunk Type Code(数据块类型码) 4 规定数据块类型,其名称即由ASCII对应字符确定
Chunk Data(数据块实际内容) 可变 最重要的内容部分,其长度由Length决定
CRC(循环冗余检测) 4 存储用来检测是否有错误的循环冗余码

其中,数据块还分为关键数据块和辅助数据块

关键数据块必须存在,辅助数据块可有可无。通常以大写字母开头的为关键数据块,以小写字母开头的为辅助数据块

那么现在我们就跟着上面我做了注释的图来初步了解一些数据块吧

1.IHDR

第00行的08-0B说明了有个数据实际内容长0x0D=13字节的数据块,是什么数据块呢?从接下去的0C-0F可以看出是IHDR数据块

这个数据块中的实际内容储存着这个png文件的一些重要信息

名称 字节数 解释 实例说明
Width 4 图片宽度,单位:像素 就如原图一样宽度只有3个像素
Height 4 图片高度,单位:像素 同理,高度是4个像素
Bit depth 1 图像深度:
索引彩色图像:1,2,4或8
灰度图像:1,2,4,8或16
真彩色图像:8或16
如第一个像素是(255,0,0)纯红,255需要8个bit表示
ColorType 1 颜色类型:
0:灰度图像, 1,2,4,8或16
2:真彩色图像,8或16
3:索引彩色图像,1,2,4或8
4:带透明度数据(或称α通道数据)的灰度图像,8或16
6:带透明度数据(或称α通道数据)的真彩色图像,8或16
文件中为06
Compression method 1 表示压缩算法。目前只支持 0,表示 Deflate/Inflate。Deflate/inflate 是一种结合了 LZ77 和霍夫曼编码的无损压缩算法,被广泛运用于 7-zip,zlib,gzip 等场景。 00
Filter method 1 代表在压缩前应用的过滤函数类型,目前只支持0 。过滤函数类型 0 里面包括了 5 种过滤函数。 00
Interlace method 1 代表图片数据是否经过交错,0代表没有交错,1代表交错。 00即没有交错

这个数据块的实际内容到此结束,共13字节正好,接下来4个字节是CRC校验,CRC校验暂且不是研究重点,知道有这个东西得占4个字节就好。

然后这个IHDR数据块就到此结束,让我们来看下一个数据块

2.sRGB

同理,从20行的01-04可以看出有一个数据实际内容长1个字节的数据块,再往后读四个字节05-08可以看出是sRGB数据块

这个数据块用于表示图片的色彩空间

对于这1个字节的数据实际内容,有

表达
00 表示感性的,用于展示照片等
01 表示相对色彩,用于展示图标等
02 表示饱和的,用于展示图表等
03 表示绝对色彩,用于展示图片原本的色彩

文件中的数据实际内容为00

之后4字节的0A-0D为CRC校验,这个数据块就到这里结束了

3.gAMA

可知它的实际内容长4个字节。它的功能与亮度调整,色彩管理,图像处理等方面有关。这个我们不细究。

CRC校验后数据块结束

4.pHYS

它的实际内容长9个字节,它规定的是图像的物理大小

前四个字节规定X轴上每个单位长度的像素数,后四个字节规定Y轴上每个单位长度的像素数,最后一个字节规定单位是什么

例图中,x轴上每个单位长度有0x1274=4724个像素

y轴上每个单位长度有0x1274=4724个像素

单位是什么呢?,下一个字节是0x01,它对应的单位是米。

也就是一米里塞4724个像素

随后CRC检验,数据块结束

5.IDAT

这个数据块是整个PNG最重要的部分,因为它储存着每个像素块的信息。毕竟没有像素块让计算机把图从何渲染起?

但PNG没有那么单纯,里面的信息是经过算法压缩的,所以储存的不是原始数据。不然里面就应该有一堆0x00和0xFF了

但事实上没有

通过一番折腾,我通过AI弄到了能够解码这个数据块的python程序,跑了一下

python程序解码IDAT数据块

抄出来

0x 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00 00 FF 00 00 FF 00 FF 00 FF 00 00 FF FF 02 00 FF
01 00 00 FF 01 FF 00 00 FF 00 00 01 00 00 00 FF FF
02 FF FF 00 00 00 00 00 02 FF FF FF 00 00 00 00 00
03 00 00 00 00

在前面我们知道,这个图片的色深是8bit,每个像素的组成是RGBA

每个行的起头是一个字节的过滤器字节,其代表过滤器的类型

过滤器字节 过滤器类型 过滤方式
0x00 保留原始数据
0x01 减去A
0x02 减去B
0x03 平均 根据A,B取平均,并向下取整
0x04 Paeth 使用最接近于 p = A + B − C 的 A、B 或 C 的数值
A,B,C是指相对X的位置

对于每一行像素,格式都是

1
(过滤器)[(R,G,B,A),(R,G,B,A),(R,G,B,A)...(R,G,B,A)]

了解了这些前置内容,我们就可以开始正式分析图片像素了

第一行像素

1
(00)[(FF,00,00,FF),(00,FF,00,FF),(00,00,FF,FF)]  //其中A的FF表示完全不透明

可以知道,这一行没有过滤器,

像素从左到右依次为红,黄,蓝,均不透明。与原图相符

第二行像素

1
(02)[(00,FF,00,00),(FF,01,FF,00),(00,FF,00,00)]

过滤器0x02表示与上面相减,那么我们通过把这一行与上一行相加就可以得到原始数据

1
(xx)[(FF,FF,00,FF),(FF,00,FF,FF),(00,FF,FF,FF)]  //其中FF+01溢出变为00

就可以得到这一行的像素是

黄,紫,青,均不透明。与原图相符

第三行像素

1
(01)[(00,00,00,FF),(FF,FF,FF,00),(00,00,00,00)]

过滤器0x01表示与左侧相减,其中第一个像素是原始数据(因为左侧没有像素)

那么从第二个像素开始,将每个像素的现数据与左侧像素(原数据)相加即可得到原始数据

1
(xx)[(00,00,00,FF),(FF,FF,FF,FF),(FF,FF,FF,FF)]

显然是黑,白,白,均不透明。与原图相符

第四行像素

1
(02)[(FF,FF,FF,00),(00,00,00,00),(00,00,00,00)]

过滤器0x02表示与上面相减,通过把这一行与上一行(原始)相加即可得到原始数据

1
(xx)[(FF,FF,FF,FF),(FF,FF,FF,FF),(FF,FF,FF,FF)]

很明显,这一行是白,白,白,均不透明。与原图相符

至此,四行像素我们都解析完毕了,均与原图相符。之后4个字节的CRC校验,数据块结束

(这一部分真的折腾了我好几天QAQ)

6.IEND

PNG文件的最后12个字节一定是0x[00 00 00 00 49 45 4E 44 AE 42 60 82]

我们仍可以用数据块的角度去看待它

0x[00 00 00 00]说明这个数据块实际内容长度为0。这个数据块的作用是标志文件结束,自然不需要什么内容

0x[49 45 4E 44]说明这个数据块是IEND

那既然实际内容长度为0,那就是没有实际内容

所以接下去的4个字节是CRC校验。因为前面的8个字节固定,所以CRC校验的4个字节也是固定的

就能知道PNG一定是以这12个字节结束

至此,PNG部分就算结束

2.BMP

同样一张图片,用Windows画图将其转换为BMP格式,用wxMEdit打开它。同样的,研究其数据结构

BMP

对其做上标记,使其更好阅读

BMP注释

我用方括号括出不同的数据块,在其中用横线或是尖括号标出其中的子块(触摸板画图不好操控,能看就行)

BMP的阅读方式比较怪,但总结一下就是

字节外从右往左,字节内从左往右

例如[AB CD]在BMP中要读作是[CD AB]

(这玩意我瞪了半天才明白,因为我发现像素一直对不上)

但不同的数据块我们仍然以从左往右的顺序进行分析

我们先将这个BMP分成三个大块

1.文件头(bit map file header)

2.位图信息头(bitmap information)

3.位图数据(pixel data)

(有些BMP文件还会出现调色板(color palette)数据块,但这里没有,我们先略过)

1.文件头

这一部分是BMP的固有开头,占14字节,其中记录了这些信息(蓝色方括)

字段名 大小(字节) 描述
FileType(文件类型) 2 描述其文件类型,固定为[42 4D],对应ASCII中的BM
FileSize(文件大小) 4 记录文件大小,单位为KB,文件中为[66 00 00 00]即0x66=102KB(查看文件属性,这个文件确实大小为102KB)
Reserved(保留位) 2 2位保留位预留给图片渲染应用,初始化必须为0(说实话我也不懂这是什么意思,但查到的资料就是这么说的)
Reserved(保留位) 2 同上
PixelDataOffset(从头到位图数据的偏移) 4 例图中的值为0x36=54,即记录在位图数据开始前一共有多少个字节(你可以看一下,位图数据的第一个字节是第3行第7个(紫色方括号),也就是在位图数据开始前有0x36个字节)

文件头数据块就到此结束

2.位图信息头

(橙色方括)

字段名 大小(字节) 描述
HeaderSize 4 信息头大小,单位是字节,例图中为0x28=40字节
ImageWidth 4 描述图片宽度,单位是像素,例图中为0x03=3符合事实
ImageHeight 4 描述图片高度,单位是像素,例图中为0x04=4符合事实
Planes 2 目标设备说明颜色平面数,总被设置为1
BitsPerPixel 2 定义每个像素需要多少个bit储存,一般有1、2、4、8、16、24、32这几种。例图中为0x18=24,因为三原色中每种颜色是0-255占8bit,一个像素就需要3*8=24bit。(这里没有透明度了)
Compression 4 图像的压缩类型,最常用的就是0(BI_RGB),表示不压缩(再压缩我又得折腾半年)
ImageSize 4 位图数据的大小,当用BI_RGB格式时,可以设置为0。例图中是0x30=48符合其大小
XpixelsPerMeter 4 水平分辨率,单位是像素/米,有符号整数。例图为0x1274=4724
YpixelsPerMeter 4 垂直分辨率,单位是像素/米,有符号整数.例图为0x1274=4724
TotalColors 4 位图使用的调色板中的颜色索引数,为0说明使用所有
ImportantColors 4 对图像显示有重要影响的颜色索引数,为0说明都重要

到此,位图信息头结束

3.位图数据

我用紫色方括号括了起来

为什么里面会有尖括号和划线之分呢?难道像素数据不都一个样吗?

因为BMP存在在一个机制

比特填充(bit padding)

BMP扫描引擎的最小单位是4字节,所以每一行的字节数必须是4的倍数,若有不足的部分会用0x00填充

例图中每一行有三个像素,即3*3=9个字节,不是4的倍数,所以每一行会有三个0x00填充进去以达到一行12个字节,即使它不表达任何东西

如果我们从左往右看像素块(绿尖括)的话,那么它在图像内对应的顺序是从左到右,从下到上。

在一个像素内(绿尖括内),我们可以理解为字节外从右往左,字节内从左往右的RGB排列

也可以理解为正常从左往右的BGR

毕竟这两者一模一样,怎么理解是个人主观的问题

总之,通过位图数据的内容,我们可以轻松还原出图像的内容

红色 绿色 蓝色
黄色(R+G) 紫色(R+B) 青色(G+B)
黑色 白色 白色
白色 白色 白色

至此,我们成功解读完了这个BMP文件的内容

3.JPEG

同样用Windows画图将例图保存为JPEG

JPEG

呜哇,看起来比前面两种都长都复杂

先让我们了解数据段的结构。其实跟PNG的数据块蛮类似的

以下是数据段的一般结构

名称 长度(字节) 说明
段标识 1 这一个字节固定为0xFF,它标志的数据段的起始
段类型 1 用这一个字节说明这是什么类型的数据段
段长度 2 这两个字节会记录'段长度'+'段内容'的长度,单位是字节
段内容 '段长度'-2 记录数据段的实际内容,但是它的长度一定会≤65533字节

有些数据段会只有段标识和段类型这两部分,如SOI和EOI

由于JPEG的压缩方式过于复杂,我在历经几天的折腾之后,仍不理解它的压缩原理以及解码方式。所以这里我只简单地标记数据块,并只作简单的讲解,无法涉及算法内容

JPEG标注

1.SOI

这个数据段只有段标识和段类型两块组成,固定为0x[FF D8]。这也是JPEG文件的固定开头

2.APP0

这个数据段一个分成如下几个部分

名称 长度(字节) 说明
段标识 1 固定为0xFF
段类型 1 E0即为APP0
段长度 2 例图中为0x0010=16,如果n不为0的话则是(16+3*n)D
以下为段内容
交换格式 5 例图为'JFIF',即例图中0x[4A 46 49 46 00]的ASCII对应字符(其中0x00是空字符)
主版本号 1 例图中为0x01,暂且不深究版本之间的区别
次版本号 1 例图中为0x01,暂且不深究版本之间的区别
密度单位 1 0=无单位
1=像素/英寸
2=像素/厘米
例图中为0x02
X像素密度 2 水平方向的密度 ,例图中为0x[00 2F],代表47像素/厘米(?我也不是很理解)
Y像素密度 2 垂直方向的密度
缩略图X像素 1 缩略图水平像素数目 ,例图中为0x00(应该就是没有缩略图)
缩略图Y像素 1 缩略图垂直像素数目 ,例图中为0x00(应该就是没有缩略图)
RGB缩略图 3*n 例图中不存在这一段,所以n=0。当前两段数据均大于0时,这段才会存在,这时n=缩略图像素总数='缩略图X像素'*'缩略图Y像素'

JFIF是JPEG File Interchange Forma的缩写,即JPEG文件交换格式。另外还有TIFF等格式,但很少用

像素密度的含义我也不是很理解

因为我把这张图放大64倍后用尺子在屏幕上量宽是3.4-3.5厘米之间,与像素密度不符(相符的话应该在4cm出头)。可能是指在打印机上的密度?

关于缩略图这东西,我查到的资料说是大部分JPEG都没这东西,也就是n一般都为0,所以就先不深究了

此外,一些JPEG图片可能包含APPn数据段(这里n不是上面那个n,是在1-E之间取值的一个数),它的段类型字节是0xEn。手机照片通常包含APP1,会记录时间,地点等信息。

3.DQT

这个图片文件中有两个DQT,我们都放在这里一起研究

名称 长度(字节) 说明
段标识 1 固定为0xFF
段类型 1 DQT为0xDB
段长度 2 其值为3+n,例图两处DQT中n=1,所以两处段长度均为0x43
以下为段内容
QT信息 1 以两位十六进制数的视角去看这一个字节,其中高位代表QT精度,低位代表QT编号。当高位是0H时代表精度是一个字节,如果不是0H的话精度就是两个字节。例图中第一个DQT数据段中这一字节是0x00,即QT精度为1字节,QT编号为0。第二个DQT数据段中这一字节是0x01,即QT精度为1字节,QT编号为1
QT内容 n 其中n=64*‘QT精度’=(0x40)*‘QT精度’,其中QT精度的单位是字节。例图中两处DQT数据段QT精度均为1字节,所以两处n=64=0x40

这个数据段应该是与压缩和解码有关,但我目前还不理解

4.SOF

名称 长度(字节) 说明
段标识 1 固定为0xFF
段类型 1 0xC0
段长度 2 例图中为0x0011即17字节
以下为段内容
样本精度 1 单位为bit,例图中为8,大多数软件不支持12和16。样本是单个像素的颜色分量,或者说一个样本就是一个组件。所以下面的组件ID,采样系数,量化表号等每个都占1个字节
图片高度 2 例图中为4,符合实际
图片宽度 2 例图中为3,符合实际
组件数量 1 1代表灰度图,3代表YCbCr/YIQ彩色图,4代表CMYK彩色图
我想这个组件数量是指色彩的组件数量,如灰度图只有亮度一个组件,所以只有黑白。也可以说RGB,YCbCr,YIQ是三个组件,CMYK是四个组件
以下部分会重复'组件数量'次
组件ID 1 1代表Y, 2代表Cb, 3代表Cr, 4代表I, 5代表Q
采样系数 1 把这一字节当成两位十六进制数来看,高位代表垂直采样系数,低位代表水平采样系数
量化表号 1 这个不是很理解有什么用

5.DHT

这玩意我到处查到的信息说是定义huffman表。但要我说出具体是干什么用的。。。有生之年吧,反正肯定跟压缩算法有关

名称 长度(字节) 说明
段标识 1 固定为0xFF
段类型 1 0xC4
段长度 2 其值为19+n
以下为段内容
HT信息 1 JPEG文件里有2类Haffman 表:一类用于DC(直流量),一类用于AC(交流量)。一般有4个表:亮度的DC和AC,色度的DC和AC。最多可有6个。
其中高位代表HT类型,0为DC表,1为AC表。低位代表HT号
HT位表 16 这16个数相加应≤256
HT值表 n n是HT位表里16个数的和

6.SOS

名称 长度(字节) 说明
段标识 1 0xFF
段类型 1 0xDA
段长度 2 其值=6+2×'扫描行内组件数量'
以下为段内容
扫描行内组件数量 1 必须属于[1,4],通常为3,例图中也为3
以下部分会重复'扫描行内组件数量'次
组件ID 1 1代表Y, 2代表Cb, 3代表Cr, 4代表I, 5代表Q
Huffman表号 1 看作两位十六进制
高位表示AC表号属于[0,3]
低位表示DC表号属于[0,3]
循环体结束
剩余部分 3 查到的资料表示作用未知

7.图片压缩数据

它确实不是常规数据块的格式。但暂且我也无法理解JPEG的压缩及解码方式,所以先略过

8.EOI

JPEG文件以0x[FF D9]这两个字节标志其结束

JPEG我暂且还只能写出这么多。如果我真的能理解它的解码方式的话我会补充

我尝试询问GPT,它一直说不出具体的解码方法。但它能在使用pillow库的情况下用Python写出解码程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from PIL import Image

# 打开JPEG文件
def open_jpeg(file_path):
try:
with Image.open(file_path) as img:
img = img.convert('RGB') # 转换为RGB格式
img.show() # 显示图像

# 获取像素数据
pixels = list(img.getdata())
width, height = img.size

# 输出每个像素的RGB值
for y in range(height):
for x in range(width):
r, g, b = pixels[y * width + x]
print(f"Pixel at ({x}, {y}): R={r}, G={g}, B={b}")

except Exception as e:
print(f"Error opening image: {e}")

# 使用示例
open_jpeg('path_to_your_image.jpg')

我尝试运行,得到输出

1
2
3
4
5
6
7
8
9
10
11
12
Pixel at (0, 0): R=254, G=0, B=9
Pixel at (1, 0): R=0, G=255, B=0
Pixel at (2, 0): R=0, G=2, B=254
Pixel at (0, 1): R=255, G=253, B=0
Pixel at (1, 1): R=251, G=0, B=251
Pixel at (2, 1): R=13, G=255, B=255
Pixel at (0, 2): R=0, G=2, B=5
Pixel at (1, 2): R=246, G=255, B=255
Pixel at (2, 2): R=255, G=249, B=253
Pixel at (0, 3): R=248, G=254, B=252
Pixel at (1, 3): R=255, G=255, B=248
Pixel at (2, 3): R=246, G=255, B=255

很明显,JPEG的压缩是有失真的