JPEG图⽚存储格式与元数据解析
1. .jpg, .png, .gif
说到图⽚,我们⾸先会想到,⼏种常见图⽚格式,如:.jpg, .png, .gif 等。
但当我门在说图⽚的格式时,除了在说图⽚⽂件的后缀不同,还有什么不同呢?
可能会有⼈不明⽩,为什么图⽚的格式是压缩标准? 图⽚为什么要压缩? 难道存储在我们个⼈电脑的图⽚都是压缩的?
没错,不管是存储在我们个⼈电脑,⼿机,还是在⽹络上图⽚其实都是经过压缩后的图⽚数据。
那么,压缩前的原始图像数据⼜是什么样的? 以及为什么要对图像进⾏压缩?
2. 原始图像数据
不管是什么格式,或采⽤什么样的压缩标准,原始的图像数据其实都是⼀样的,⽽且也符合我门直观的
理解。
例如,⼀张 4 × 4 (宽度和⾼度都是 4 个像素)的彩⾊图⽚,未压缩的的原始图像数据,就是⼀个 4 × 4 矩形⽹格,每⼀个⽹格代表⼀个像素。
⽽彩⾊图⽚的每⼀个像素,⼜是由 红,绿,蓝 三基⾊构成,如下图右边所⽰,红绿蓝,对应于 r g b 三个数值,也就是我常说的 RGB ⾊彩模式。
RGB,我们在计算机视觉领域,⼜称为颜⾊通道,彩⾊图像有三个通道值,每个颜⾊通道,都是⼀个 0~255 的整数值,占⽤⼀个字节(Byte)的存储空间。
一起看星星因此,我们很容易计算上⾯这张 4×4 彩⾊图⽚占⽤的存储空间为 4 × 4 × 3 = 48 字节 (Bytes) 。换算成我们熟悉的 KB,就是 48 / 1024 = 0.046875 KB,不到 0.1 KB。
事实上,我们很少见到这么⼩的图⽚,甚⾄在我们的个⼈电脑和⼿机上,根本⽆法正常看到这么⼩的图⽚。这⾥为了⽅便理解和计算,做了技术上的处理,⽽不是真实看到的图⽚⼤⼩。
拓展:按照在电脑上常⽤的分辨率 72 ppi (Pixels Per Inch:像素每英⼨),即 每 2.54 厘⽶ 容纳 72 个像素,或者说,⼀个像素占⽤的屏幕尺⼨是 0.35 毫⽶,那么上⾯ 4 × 4 图⽚,在屏幕上 1:1 显⽰,占⽤屏幕的物理尺⼨只有 1.4 × 1.4 毫⽶。显然,⽤⾁眼是⽆法看清的。
在理解⼀张 4 × 4 的彩⾊图⽚占⽤存储空间⼤⼩,我们同样的⽅式计算如下,320 × 320 的彩⾊图⽚,这个⼤⼩在我们⽇常⽣活,也不算⼀张⼤图,相当于我们⽤作头像的⼤⼩。
我相信我们可以很快得出结果,320 × 320 × 3 = 300 KB ,相当于上⾯ 4 × 4 图⽚的 6000 多倍。
可能⼤家觉得这张图⽚还不算⼤。我测算,⽤⾃⼰的 iPhone 8 Plus 正常拍摄⼀张⼿机照⽚,它的⼤⼩是 3024 × 4032 ,这样⼀张图⽚在未压缩的情况下,所占⽤的存储空间⼤⼩是 3024 × 4032 × 3 = 35 MB 。⽽实际,如下图,在我的 Mac 上看到的图⽚, 只有 6.8 M ,也就是说,我们在使⽤⼿机拍摄照⽚后,在保存在相册之前,相机程序已经⾃动对我们拍摄的照⽚照⽚进⾏了压缩,这⾥的压缩⽐是 35 / 6.8 = 5,压缩⽐并不是⼀个固定值,也就是说同样⼤⼩的不同照⽚,在经过相同的压缩处理后,占⽤磁盘的空间也是不⼀样的。
事实上,图像压缩在数字图像处理领域,是应⽤最为普遍的和成功的,⼤部分图⽚查看器,编辑器,⽹页浏览器,等与图⽚相关的应⽤程序,乃⾄,开发⼈员使⽤图⽚处理库,底层都使⽤了图像的压缩和解压缩算法,并且对于⽤户,或者上层的应⽤开发⼈员,是完全透明的,以⾄于我们觉察不到它的存在。
PS: 图像的压缩和解压缩,也称之为,编码和解码,其实是同⼀个意思,并且适⽤于数字视频的编解码
3. 图像压缩
如果,⼤家对上⽂,将⼿机拍摄的⼀张原始图像是 35 MB 压缩保存后是 6.8MB ,没有太⼤的概念。
那么,我们不妨再⽤电影举例,⼀部宽⾼为 720 × 480(彩⾊),帧率为 30 帧/秒,时长为 2 ⼩时的电影,其未压缩前的⼤⼩是:
720 × 480 × 3 (字节/像素) × 30 (帧/秒) × 3600 (秒/⼩时) × 2⼩时 = 209 GB
不考虑⾳频,电影的画⾯,本质就是由⼀张张连续显⽰的图像构成,每⼀副图像,我们称之为⼀帧)
也就是⼀个 1TB 的移动硬盘,只能装下不到 5 部这样的清晰度⼀般的电影。这显然是不能接受,也与我们⽇常⽣活对电影存储的认知不符。
因此,我们要感到庆幸,对图像和视频的压缩算法,⽆时⽆刻不在为我们的数字⽣活服务。我们没有觉察到,但⼀定不能忽视它的存在。
3.1 存储在磁盘上真实图像的⼆进制数据
事实上,图像的压缩或编码,本质就是为了解决图像在存储和⽹络传输过程的空间消耗,让有限的磁盘和⽹络带宽,存储和传送海量的数字图像和视频提供了技术后盾。
那么压缩后的图⽚数据到底长啥样?
我们依然使⽤前⽂⽤到的那只可爱的 ⼩狗狗 图⽚,它在我电脑上⽂件名为 dog.jpeg。
我们知道,不同于普通⽂本⽂件,图⽚在计算机⾥存储形式,是⼆进制⽂件。
在 linux 和 MacOS 系统上,我们可以借助⼀个命令⾏⼯具 hexdump 来查看任何⼆进制⽂件,包括图⽚。
读者,可以将下⾯这张图⽚ 保存到 ⾃⼰的电脑上。
在命令⾏界⾯,进⼊ dog.jpeg ⽂件所在⽬录,运⾏如下命令:
hexdump dog.jpeg
# 输出结果如下(中间数据已省略,只显⽰开头和结尾各两⾏):
# 0000000 ff d8 ff e0 00 10 4a 46 49 46 00 01 01 00 00 48
# 0000010 00 48 00 00 ff e1 00 8c 45 78 69 66 00 00 4d 4d
# ....
丹麦历史# 0004780 04 12 48 f5 a5 70 0b 82 18 7c 8c 30 39 cf 4e be
# 0004790 f5 11 82 30 c3 f7 47 00 12 39 3c 50 08 ff d9
Tips: 如果想对显⽰的格式进⾏控制,可以尝试增加如下选项,格式化显⽰输出:
hexdump -e '16/1 "%02X "' -e '"\n"' dog.jpeg
hexdump -e '16/1 "%02X " " | "' -e '16/1 "%_p" "\n"' dog.jpeg
我们看到的输出如下图所⽰(中间内容省略,这⾥只截取了开头和结尾各两⾏):
图中,红线框圈住的部分,是图⽚数据的字节流编址,可以看作是为了查看⽅便,添加的⾏号,红框右边的才是图⽚的真实存储字节流,并且每⾏显⽰ 16 个字节。当然不管是“⾏号”还是图⽚数据,为了显⽰的简介性,默认都是⽤了16进制。
这⾥我忽略红框中的“⾏号”,只关注图⽚字节流数据。
这⾥要注意的是,图中数据是⼀⾏⾏显⽰的,并且每⾏中,字节间都有空格,其实,这⾥还是为了⽅便查看才这样显⽰的,真实存储的数据并⾮⼀⾏⼀⾏,字节间也没有空格,所谓字节流,就是图⽚数据字节都是连续不间断的,串成⼀条线,在程序⾥,体现为⼀个⼀维的字节数组。
为了验证这点,我们不妨⽤实践说话。
在⼀台已经安装了Python(MacOS 内置了 Python 2)机器,启动命令⾏,输⼊ python 进⼊ python 交互式编辑环境。使⽤如下 python 代码,查看 图⽚ dog.jpeg 的⼆进制字节流。
with open("dog.jpeg","rb")as f:
image_bytes_data = f.read()
image_bytes_data[:16]
image_bytes_data[-16:]
运⾏输出如下:
image_bytes_data 以字节为单位,保存着图⽚⼆进制数据,可以使⽤切⽚,查看前 16 个字节 和最后 16 个字节。通过与前⽂使⽤hexdump 查看的数据对⽐,可以看出是⼀致的。细⼼的读者可能发现,
在 hexdump 显⽰结果的最后⼀⾏,只有 15 个字节。因此,这⾥看到最后 16 个字节,是从倒数第⼆⾏最后⼀个字节开始的。
3.2 图像⼆进制数据格式
我们已经知道如何通过命令⾏⼯具 hexdump 和 python 脚本查看图⽚的⼆进制数据,并且我们知道这不是图⽚原始的⼆维RGB阵列数据,⽽是经过压缩后,⽅便存储和⽹络传输⽤的⼀维⼆进制字节流。
那么这些字节数据,到底代表什么意思,我们使⽤的图⽚应⽤程序如何根据这些数据,解压缩或解码,还原成,计算机显⽰器可以显⽰的⼆维 RGB 像素阵列呢?
本⽂仅仅对字节流数据组成格式,各部分代表的含义进⾏简单介绍,以对图⽚存储数据解码有个基本认识,对于解码部分的完整实现,超出本⽂的讨论范围,感兴趣,推荐参考专业书籍或开源图⽚编解码器。
3.2.1 标记数据
⾸先,还是引⽤前⽂的数据截图:
我们注意到⽤橙⾊线框框着的两个部分,ff d8 表⽰图⽚数据开始,英⽂缩写 SOI (Start Of Image),ff d9 表⽰图⽚数据的结尾,英⽂缩写 EOI (End Of Image)。
我们不难发现,两者都是以 ff 开头。事实上,图⽚存储的数据,⼤体只包含两类数据,⼀类是 ff 开头,后跟1个字节, 这个字节既不能等于 0 也不能等于 ff,表⽰不同类型的标记(Marker)数据,⽽剩下的就是图⽚的压缩数据或编码数据。
由于标记数据记录着图⽚的元数据,同时决定了,图⽚压缩数据如何解码。因此我们重点介绍标记数据。
为了通过编程实践,更好地理解不同的标记数据,我们根据已经掌握的标记数据特点,即,标记数据都是以 ff 开头,后跟⼀个, 既不能等于0 也不能等于 ff 的字节 类型。编写如下 python 脚本来提取图⽚中的标记数据。
Warm Tips:如下代码,请在 python3 环境下运⾏
干炒牛河将以下 python 脚本复制,保存到⽂件 view_dog_marker_data.py
# 从磁盘读取图⽚⼆进制字节流数据
with open('dog.jpeg','rb')as f:
image_data = f.read()
image_data =['%x'% image_data[i]for i in range(len(image_data))]
# 解析标记数据,保存到字典 tagmarker
tagmarker =dict()
tag =''
tag_start =False
data_start =False
for i, b in enumerate(image_data):
if len(b)==1:
b ='0'+ b
if b =='ff':
tag_start =True
continue
关于爱国名言if tag_start:
if b !='ff'and(b !='00'):
瘦脸针副作用tag ='ff'+ b
if not tag in tagmarker:
tagmarker[tag]=list()
tag_start =False
data_start =True
continue
else:
tag_start =False
tagmarker[tag].append('ff')
if data_start:
tagmarker[tag].append(b)
# 查看解析后,有那些标记数据,以及对应数据的长度
for tag, arr in tagmarker.items():
s =len(arr)
if s ==0:
print(tag)
continue
print("{}:\t{}\tbytes".format(tag, s))
发布评论