JPEG原理详解(附python实现)

  • Post author:
  • Post category:python




前言

图片压缩有多重要,可能很多人可能并没有一个直观上的认识,举个例子,一张800X800大小的普通图片,如果未经压缩,大概在1.7MB左右,这个体积如果存放文本文件的话足够保存一部92万字的鸿篇巨著《红楼梦》,现如今互联网上绝大部分图片都使用了JPEG压缩技术,也就是大家使用的jpg文件,通常JPEG文件相对于原始图像,能够得到1/8的压缩比,如此高的压缩率是如何做到的呢?

JPEG能够获得如此高的压缩比是因为使用了有损压缩技术,所谓有损压缩,就是把原始数据中不重要的部分去掉,以便可以用更小的体积保存,这个原理其实很常见,比如485194.200000000001这个数,如果我们用485194.2来保存,就是一种“有损”的保存方法,因为小数点后面的那个“0.000000000001”属于不重要的部分,所以可以被忽略掉。JPEG整个压缩过程基本上也是遵循这个步骤:

  1. 把数据分为“重要部分”和“不重要部分”
  2. 滤掉不重要的部分
  3. 保存



JPEG原理



步骤一:图像分割

JPEG算法的第一步,图像被分割成大小为8X8的小块,这些小块在整个压缩过程中都是单独被处理的。后面我们会以一张非常经典的图为例,这张图片名字叫做Lenna,据说是世界上第一张JPG图片,这张图片自从诞生之日开始,就和图像处理结下渊源,陪伴了无数理工宅男度过了的一个个不眠之夜,可谓功勋卓著,感兴趣的朋友可以在这里了解到这张图片的故事。




步骤二:颜色空间转换RGB->YCbCr

所谓“颜色空间”,是指表达颜色的数学模型,比如我们常见的“RGB”模型,就是把颜色分解成红绿蓝三种分量,这样一张图片就可以分解成三张灰度图,数学表达上,每一个8X8的图案,可以表达成三个8X8的矩阵,其中的数值的范围一般在[0,255]之间

在这里插入图片描述

不同的颜色模型各有不同的应用场景,例如RGB模型适合于像显示器这样的自发光图案,而在印刷行业,使用油墨打印,图案的颜色是通过在反射光线时产生的,通常使用CMYK模型,而在JPEG压缩算法中,需要把图案转换成为YCbCr模型,这里的Y表示亮度(Luminance),Cb和Cr分别表示绿色和红色的“色差值”。

“色差”这个概念起源于电视行业,最早的电视都是黑白的,那时候传输电视信号只需要传输亮度信号,也就是Y信号即可,彩色电视出现之后,人们在Y信号之外增加了两条色差信号以传输颜色信息,这么做的目的是为了兼容黑白电视机,因为黑白电视只需要处理信号中的Y信号即可。

根据三基色原理,人们发现红绿蓝三种颜色所贡献的亮度是不同的,绿色的“亮度”最大,蓝色最暗,设红色所贡献的亮度的份额为KR,蓝色贡献的份额为KB,那么亮度为





Y

=

K

R

R

+

(

1

K

R

K

B

)

G

+

K

B

B

Y=K_R·R+(1-K_R-K_B)·G+K_B·B






Y




=









K










R























R




+








(


1














K










R






























K










B


















)







G




+









K










B























B







根据经验,KR=0.299,KB=0.114,那么





Y

=

0.299

R

+

0.587

G

+

0.114

B

Y=0.299·R+0.587·G+0.114·B






Y




=








0.299







R




+








0.587







G




+








0.114







B







蓝色和红色的色差的定义如下:





C

b

=

1

2

B

Y

1

K

B

C_b=\frac{1}{2}·\frac{B-Y}{1-K_B}







C










b




















=



















2














1




































1










K










B






























B









Y



























C

r

=

1

2

R

Y

1

K

R

C_r=\frac{1}{2}·\frac{R-Y}{1-K_R}







C










r




















=



















2














1




































1










K










R






























R









Y























最终可以得到RGB转换为YCbCr的数学公式为:





Y

=

0.299

R

+

0.5870

G

+

0.114

B

C

b

=

0.1687

R

0.3313

G

+

0.5

B

C

r

=

0.5

R

0.4187

G

0.0813

B

Y=0.299·R+0.5870·G+0.114·B\\ C_b=-0.1687·R-0.3313·G+0.5·B\\ C_r=0.5·R-0.4187·G-0.0813·B






Y




=








0.299







R




+








0.5870







G




+








0.114







B









C










b




















=











0.1687







R













0.3313







G




+








0.5







B









C










r




















=








0.5







R













0.4187







G













0.0813







B







YCbCr模型广泛应用在图像和视频的压缩传输中,比如你可以留意一下电视或者DVD后面的接口,就可以发现色差接口。

在这里插入图片描述

这是有道理的,还记得我们在文章开始时提到的有损压缩的基本原理吗?有损压缩首先要做的事情就是“把重要的信息和不重要的信息分开”,YCbCr恰好能做到这一点。对于人眼来说,图像中明暗的变化更容易被感知到,这是由于人眼的构造引起的。视网膜上有两种感光细胞,能够感知亮度变化的视杆细胞,以及能够感知颜色的视锥细胞,由于视杆细胞在数量上远大于视锥细胞,所以我们更容易感知到明暗细节。比如说下面这张图

在这里插入图片描述



可以明显看到,亮度图的细节更加丰富。JPEG把图像转换为YCbCr之后,就可以针对数据得重要程度的不同做不同的处理。这就是为什么JPEG使用这种颜色空间的原因。



步骤三:离散余弦变换

在这里插入图片描述

​ 这次我们来介绍JPEG算法中的核心内容,离散余弦变换(Discrete cosine transform),简称DCT。

​ 离散余弦变换属于傅里叶变换的另外一种形式,没错,就是大名鼎鼎的傅里叶变换。傅里叶是法国著名的数学家和物理学家,1807年,39岁的傅里叶在他的一篇论文里提出了一个想法,他认为

任何周期性的函数,都可以分解为为一系列的三角函数的组合

,这个想法一开始并没有得到当时科学界的承认,比如当时著名的数学家拉格朗日提出质疑,三角函数无论如何组合,都无法表达带有“尖角”的函数,一直到1822年拉格朗日死后,傅里叶的想法才正式在他的著作《热的解析理论》一书中正式发表。

​ 金子总会闪光,傅里叶变换如今广泛应用于数学、物理、信号处理等等领域,变换除了它在数学上的意义外,还有其哲学上的伟大意义,那就是,世上任何复杂的事物,都可以分解为简单的事物的组合,而这个过程只需要借助数学工具就可以了。但是当年拉格朗日的质疑是正确的,三角函数的确无法表达出尖角形状的函数,不过只要三角函数足够多,可以无限逼近最终结果。比如下面这张动图,就动态描述了一个矩形方波,是如何做傅里叶分析的。


当我们要处理的不再是函数,而是一堆离散的数据时,并且这些数据是对称的话,那么傅里叶变化出来的函数只含有余弦项,这种变换称为离散余弦变换。举个例子,有一组一维数据[x0,x1,x2,…,xn-1],那么可以通过DCT变换得到n个变换级数Fi





F

m

=

k

=

0

n

1

x

k

c

o

s

[

π

n

m

(

k

+

1

2

)

]

,

m

=

0

,

1

,

.

.

.

,

n

1

F_m=\sum^{n-1}_{k=0}{x_kcos[\frac{\pi}{n}m(k+\frac{1}{2})]} ,m=0,1,…,n-1







F










m




















=

















k


=


0



















n





1






















x










k


















cos


[













n














π




















m


(


k




+















2














1




















)]



,




m




=








0


,




1


,







,




n













1







此时原始数据Xi可以通过离散余弦变换变化的逆变换(IDCT)表达出来





X

m

=

F

0

n

+

k

=

1

n

1

[

2

F

k

n

c

o

s

[

π

n

(

m

+

1

2

)

k

]

]

X_m=\frac{F_0}{n}+\sum^{n-1}_{k=1}{[\frac{2F_k}{n}cos[\frac{\pi}{n}(m+\frac{1}{2})k]]}







X










m




















=



















n















F










0






































+

















k


=


1



















n





1





















[













n














2



F










k




































cos


[













n














π




















(


m




+















2














1




















)


k


]]








也就是说,经过DCT变换,可以把一个数组分解成数个数组的和,如果我们数组视为一个一维矩阵,那么可以把结果看做是一系列矩阵的和

在这里插入图片描述

举个例子,我们有一个长度为8的数字,内容为50,55,67,80,-10,-5,20,30,经过DCT转换,得到8个级数为287.0,106.3,14.2,-110.8,9.2,65.7,-8.2,-43.9,根据公式2.3把这个数组转换为8个新的数组的和,如果我们使用图像来表达的话,就可以发现DCT转换的有趣之处了

数组0 img
[35.9,35.9,35.9,35.9,35.9,35.9,35.9,35.9]
数组1 img
[26.0,22.1,14.8,5.2,-5.2,-14.8,-22.1,-26.1]
数组2 img
[3.3,1.4,-1.4,-3.3,-3.3,-1.4,1.4,3.3]
img 数组3 img
[50,55,67,80,-10,-5,20,30] [-23.0,5.4,27.2,15.4,-15.4,-27.2,-5.4,23.0]
数组4 img
[1.6,-1.6,-1.6,1.6,1.6,-1.6,-1.6,1.6]
数组5 img
[9.1,-16.1,3.2,13.6,-13.6,-3.2,16.1,-9.1]
数组6 img
[-0.8,1.9,-1.9,0.8,0.8,-1.9,1.9,-0.8]
数组7 img
[-2.1,6.1,-9.1,10.8,-10.8,9.1,-6.1,2.1]

奥妙之处在于,经过DCT,数据中隐藏的规律被发掘了出来,杂乱的数据被转换成几个工整变化的数据。DCT转换后的数组中第一个是一个直线数据,因此又被称为“直流数据”,简称DC,后面的数据被称为“交流数据”,简称AC,这个称呼起源于信号分析中的术语。

在JPEG压缩过程中,经过颜色空间的转换,每一个8X8的图像块,在数据上表现为3个8X8的矩阵,紧接着我们对这三个矩阵做一个二维的DCT转换,二维的DCT转换公式为

在这里插入图片描述

DCT的威力究竟有多大,我们可以做一个实际的测试,比如一个所有数值都一样的矩阵,经过DCT转换后,将所有级数组合成一个新的矩阵

在这里插入图片描述

可以看到,经过DCT转换,矩阵的“能量”被全部集中在左上角上的直流分量F(0,0)上,其他位置都变成了0。

在实际的JPEG压缩过程中,由于图像本身的连贯性,一个8X8的图像中的数值一般不会出现大的跳跃,经过DCT转换会有类似的效果,左上角的直流分量保存了一个大的数值,其他分量都接近于0,我们以Lenna左上角第一块图像的Y分量为例,经过变换的矩阵为

在这里插入图片描述

可以看到,数据经过DCT变化后,被明显分成了直流分量和交流分量两部分,为后面的进一步压缩起到了充分的铺垫作用,可以说是整个JPEG中最重要的一步,后面我们会介绍数据量化。



步骤四:数据量化

经过上一节介绍的离散余弦变换,图像数据虽然已经面目全非,但仍然是处于“可逆”的状态,也就是说我们还没有进入“有损”的那一步。这次我们来玩真的,看一下数据中的细节是如何被滤去的。先来考察一下要对付的问题是什么,经过颜色空间转换和离散余弦变换,每一个8X8的图像块都变成了三个8X8的浮点数矩阵,分别表示Y,Cr,Cb数据,比如以其中某个亮度数据矩阵举例,它的数据如下

在这里插入图片描述

我们的问题是,在可以损失一部分精度的情况下,如何用更少的空间存储这些浮点数?答案是使用量子化(Quantization),简称量化。“量子”这个概念来自于物理学,意思是说连续的能量可以看做是一个个单元体的组合,看起来高端大气,其实很简单,比如游戏中在处理角色面朝方向时,一般并不是使用0到2π这样的浮点数,而是把方向分成16个区间,用0到16这样的整数来表示,这样只用4个bit就足够了。JPEG提供的量子化算法如下:





B

i

,

j

=

r

o

u

n

d

(

G

i

,

j

Q

i

,

j

)

;

i

,

j

=

0

,

1

,

2

,

.

.

.

,

7

B_{i,j}=round(\frac{G_{i,j}}{Q_{i,j}});i,j=0,1,2,…,7







B











i


,


j





















=








ro


u


n


d


(














Q











i


,


j
































G











i


,


j





































)


;




i


,




j




=








0


,




1


,




2


,







,




7







其中G是我们需要处理的图像矩阵,Q称作量化系数矩阵(Quantization matrices),JPEG算法提供了两张标准的量化系数矩阵,分别用于处理亮度数据Y和色差数据Cr以及Cb。

在这里插入图片描述 在这里插入图片描述
标准亮度量化表 标准色差量化表

其中round函数是取整函数,但考虑到了四舍五入,也就是说

在这里插入图片描述

比如上面数据,以左上角的-415.38为例,对应的量子化系数是16,那么round(-415.38/16)=round(-25.96125)=-26。最终得到的量子化后的结果为

在这里插入图片描述

可以看到,一大部分数据变成了0,这非常有利于后面的压缩存储。这两张神奇的量化表也是有讲究的,还记得我们在第一节中所讲的有损压缩的基本原理吗,有损压缩就是把数据中重要的数据和不重要的数据分开,然后分别处理。DCT系数矩阵中的不同位置的值代表了图像数据中不同频率的分量,这两张表中的数据时人们根据人眼对不不同频率的敏感程度的差别所积累下的经验制定的,一般来说人眼对于低频的分量必高频分量更加敏感,所以两张量化系数矩阵左上角的数值明显小于右下角区域。在实际的压缩过程中,还可以根据需要在这些系数的基础上再乘以一个系数,以使更多或更少的数据变成0,我们平时使用的图像处理软件在生成jpg文件时,在控制压缩质量的时候,就是控制的这个系数。

在进入下一节之前,矩阵的量化还有最后一步要做,就是把量化后的二维矩阵转变成一个一维数组,以方便后面的霍夫曼压缩,但在做这个顺序转换时,需要按照一个特定的取值顺序。

在这里插入图片描述

这么做的目的只有一个,就是尽可能把0放在一起,由于0大部分集中在右下角,所以才去这种由左上角到右下角的顺序,经过这种顺序变换,最终矩阵变成一个整数数组

-26,-3,0,-3,-2,-6,2,-4,1,-3,0,1,5,1,2,-1,1,-1,2,0,0,0,0,0,-1,-1,0,0,0,0,…,0,0

后面的工作就是对这个数组进行再一次的哈夫曼压缩,已得到最终的压缩数据。



步骤五:哈夫曼编码

JPEG压缩的最后一步是对数据进行哈弗曼编码(Huffman coding),哈弗曼几乎是所有压缩算法的基础,它的基本原理是根据数据中元素的使用频率,调整元素的编码长度,以得到更高的压缩比。

举个例子,比如下面这段数据

“AABCBABBCDBBDDBAABDBBDABBBBDDEDBD”

这段数据里面包含了33个字符,每种字符出现的次数统计如下


字符

A

B

C

D

E
次数 6 15 2 9 1

如果我们用我们常见的定长编码,每个字符都是3个bit。


字符

A

B

C

D

E
编码 001 010 011 100 101

那么这段文字共需要3*33 = 99个bit来保存,但如果我们根据字符出现的概率,使用如下的编码


字符

A

B

C

D

E
编码 110 0 1110 10 1111

那么这段文字共需要3

6 + 1

15 + 4

2 + 2

9 + 4*1 = 63个bit来保存,压缩比为63%,哈弗曼编码一般都是使用二叉树来生成的,这样得到的编码符合前缀规则,也就是较短的编码不能够是较长编码的前缀,比如上面这个编码,就是由下面的这颗二叉树生成的。

在这里插入图片描述

我们回到JPEG压缩上,回顾上一节的内容,经过数据量化,我们现在要处理的数据是一串一维数组,举例如下:


①原始数据
35,7,0,0,0,-6,-2,0,0,-9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,…,0

在实际的压缩过程中,数据中的0出现的概率非常高,所以首先要做的事情,是对其中的0进行处理,把数据中的非零的数据,以及数据前面0的个数作为一个处理单元。

①原始数据 35,7,0,0,0,-6,-2,0,0,-9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,…,0

②RLE编码
35 7 0,0,0,-6 -2 0,0,-9 0,0,…,0,8 0,0,…,0
①原始数据 35,7,0,0,0,-6,-2,0,0,-9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,…,0

②RLE编码
35 7 0,0,0,-6 -2 0,0,-9 0,0,…,0,8 0,0,…,0

如果其中某个单元的0的个数超过16,则需要分成每16个一组,如果最后一个单元全都是0,则使用特殊字符“EOB”表示,EOB意思就是“后面的数据全都是0”,

①原始数据 35,7,0,0,0,-6,-2,0,0,-9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,…,0

②RLE编码
35 7 0,0,0,-6 -2 0,0,-9 0,0,…,0,8 0,0,…,0
35 7 0,0,0,-6 -2 0,0,-9 0,0,…,0 0,0,8 0,0,…,0
(0,35) (0,7) (3,-6) (0,-2) (2,-9) (15,0) (2,8) EOB

其中(15,0)表示16个0,接下来我们要处理的是括号里右面的数字,这个数字的取值范围在-2047~2047之间,JPEG提供了一张标准的码表用于对这些数字编码:

Value Size Bits
0 0
-1 1 1 0 1
-3,-2 2,3 2 00,01 10,11
-7,-6,-5,-4 4,5,6,7 3 000,001,010,011 100,101,110,111
-15,…,-8 8,…,15 4 0000,…,0111 1000,…,1111
-31,…,-16 16,…,31 5 0 0000,…,0 1111 1 0000,…,1 1111
-63,…,-32 32,…,63 6 00 0000,… …,11 1111
-127,…,-64 64,…,127 7 000 0000,… …,111 1111
-255,…,-128 128,…,255 8 0000 0000,… …,1111 1111
-511,…,-256 256,…,511 9 0 0000 0000,… …,1 1111 1111
-1023,…,-512 512,…,1023 10 00 0000 0000,… …,11 1111 1111
-2047,…,-1024 1024,…,2047 11 000 0000 0000,… …,111 1111 1111

举例来说,第一个单元中的“35”这个数字,在表中的位置是长度为6的那组,所对应的bit码是“100011”,而“-6”的编码是”001″,由于这种编码附带长度信息,所以我们的数据变成了如下的格式。

在这里插入图片描述

括号中前两个数字分都在0~15之间,所以这两个数可以合并成一个byte,高四位是前面0的个数,后四位是后面数字的位数。

在这里插入图片描述

对于括号前面的数字的编码,就要使用到我们提到的哈弗曼编码了,比如下面这张表,就是一张针对数据中的第一个单元,也就是直流(DC)部分的哈弗曼表,由于直流部分没有前置的0,所以取值范围在0~15之间。

Length Value Bits
3 bits 04

05

03

02

06

01

00 (EOB)
000

001

010

011

100

101

110
4 bits 07 1110
5 bits 08 1111 0
6 bits 09 1111 10
7 bits 0A 1111 110
8 bits 0B 1111 1110

举例来说,示例中的DC部分的数据是0x06,对应的二进制编码是“100”,而对于后面的交流部分,取值范围在0~255之间,所以对应的哈弗曼表会更大一些

Length Value Bits
2 bits 01

02
00

01
3 bits 03 100
4 bits 00 (EOB)

04

11
1010

1011

1100
5 bits 05

12

21
1101 0

1101 1

1110 0
6 bits 31

41
1110 10

1110 11
12 bits 24

33

62

72
1111 1111 0100

1111 1111 0101

1111 1111 0110

1111 1111 0111
15 bits 82 1111 1111 1000 000
16 bits 09



FA
1111 1111 1000 0010



1111 1111 1111 1110

这样经过哈弗曼编码,并且序列化后,最终数据成为如下形式

在这里插入图片描述

最终我们使用了10个字节的空间保存了原本长度为64的数组,至此JPEG的主要压缩算法结束,这些数据就是保存在jpg文件中的最终数据。

这个系列的最后,我提供给大家一个简易的jpeg压缩算法的代码,这份代码用C++编写,以开源方式提供,放在了github上,可以到下面这个网址下载

img http://github.com/thejinchao/jpeg_encoder

使用方法很简单,像下面这样就可以了

#include "jpeg_encoder.h"
 
JpegEncoder encoder;
//输入的文件必须是24bit的bmp文件,尺寸必须是8的倍数
encoder.readFromBMP(inputFileName);
 
//第二个参数在1~199之间,代表文件压缩程度,数字越大,压缩后的文件体积越小
encoder.encodeToJPG(outputFileName, 50);

这份代码只是为了配合这个系列的文章,所以没有考虑优化,如果你想在实际工程中使用jpeg的压缩算法,还是使用被广泛应用的

libjpeg

或者

OpenJpeg



转载

以上内容转载自以下链接


https://thecodeway.com/blog/?p=69


https://thecodeway.com/blog/?p=353


https://thecodeway.com/blog/?p=480


https://thecodeway.com/blog/?p=522


https://thecodeway.com/blog/?p=690



补充说明



降采样

将RGB转换成YCbCr色彩空间后,还需要进行一个降采样操作。降采样可以减小色彩分辨率和亮度分辨率,从而减少压缩所需的数据量。在JPEG中,通常使用4:2:0的采样方案进行降采样,其中亮度分量Y不进行降采样,而色度分量Cb和Cr进行2×2的降采样,即每4个像素只保留一个色度分量,也就是说每隔一行/列取值,奇数行取Cr值,偶数行取Cb值,Cb 、Cr通道宽度和高度分别降低为原来的1/2。这样可以在保持图像清晰度的情况下,大幅度减少压缩数据的大小。

在这里插入图片描述



DCT定义域对称问题

DCT离散预先变换通常要求定义域对称,以确保其能够反转和重建输入信号。在信号处理中,通常要求变换函数具有对称性,这是因为对称性可以在一定程度上保留原始信号的特征。如果定义域不对称,可能会导致信息损失和变换质量降低。因此,在使用离散预先变换时,通常需要考虑定义域的对称性问题。

而在转换RGB到YCbCr时,R、G、B的取值范围是0到255,Y的取值范围是0-255,而Cb和Cr的取值范围是-128到127,因此需要在DCT变换前将Y的值减去128,以保证它们的取值范围是-128到127。



python实现

这里以大小为512*512的lena灰度图像为例,进行jpeg图像压缩及解压缩举例,lena灰度图如下

在这里插入图片描述



python代码

import cv2
import hashlib
import numpy as np
import matplotlib.pyplot as plt
from pyblake2 import blake2s
import base64

import matplotlib.pyplot as plt  # plt 用于显示图片
import numpy as np
import cv2
import copy

'''
jpeg压缩函数
data:要压缩的灰度图像数据流
quality_scale控制压缩质量(1-99),默认为50,值越小图像约清晰
return:得到压缩后的图像数据,为FFD9开头的jpeg格式字符串
'''


def compress(img_data, quality_scale=50):
    # 获取图像数据流宽高
    h, w = img_data.shape
    # 标准亮度量化表
    Qy = np.array([[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]], dtype=np.uint8)

    # 根据压缩质量重新计算量化表
    if quality_scale <= 0:
        quality_scale = 1
    elif quality_scale >= 100:
        quality_scale = 99
    for i in range(64):
        tmp = int((Qy[int(i / 8)][i % 8] * quality_scale + 50) / 100)
        if tmp <= 0:
            tmp = 1
        elif tmp > 255:
            tmp = 255
        Qy[int(i / 8)][i % 8] = tmp

    # Z字型
    ZigZag = [
        0, 1, 5, 6, 14, 15, 27, 28,
        2, 4, 7, 13, 16, 26, 29, 42,
        3, 8, 12, 17, 25, 30, 41, 43,
        9, 11, 18, 24, 31, 40, 44, 53,
        10, 19, 23, 32, 39, 45, 52, 54,
        20, 22, 33, 38, 46, 51, 55, 60,
        21, 34, 37, 47, 50, 56, 59, 61,
        35, 36, 48, 49, 57, 58, 62, 63]

    # DC哈夫曼编码表
    standard_dc_nrcodes = [0, 0, 7, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
    standard_dc_values = [4, 5, 3, 2, 6, 1, 0, 7, 8, 9, 10, 11]
    pos_in_table = 0;
    code_value = 0;
    dc_huffman_table = [0] * 16

    for i in range(1, 9):
        for j in range(1, standard_dc_nrcodes[i - 1] + 1):
            dc_huffman_table[standard_dc_values[pos_in_table]] = bin(code_value)[2:].rjust(i, '0')
            pos_in_table += 1
            code_value += 1
        code_value <<= 1

    # AC哈夫曼编码表
    standard_ac_nrcodes = [0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d]
    standard_ac_values = [0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12,
                          0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
                          0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08,
                          0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0,
                          0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16,
                          0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28,
                          0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
                          0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
                          0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
                          0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
                          0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
                          0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
                          0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,
                          0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
                          0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6,
                          0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5,
                          0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4,
                          0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2,
                          0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
                          0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
                          0xf9, 0xfa]

    pos_in_table = 0;
    code_value = 0;
    ac_huffman_table = [0] * 256

    for i in range(1, 17):
        for j in range(1, standard_ac_nrcodes[i - 1] + 1):
            ac_huffman_table[standard_ac_values[pos_in_table]] = bin(code_value)[2:].rjust(i, '0')
            pos_in_table += 1
            code_value += 1
        code_value <<= 1
    # 转成float类型
    img_data = img_data.astype(np.float64)
    # 存储最后的哈夫曼编码
    result = ''
    prev = 0
    # 分成8*8的块
    for i in range(h // 8):
        for j in range(w // 8):
            block = img_data[i * 8:(i + 1) * 8, j * 8:(j + 1) * 8] - 128
            block = cv2.dct(block)
            # 数据量化
            block[:] = np.round(block / Qy)
            # 把量化后的二维矩阵转成一维数组
            arr = [0] * 64
            notnull_num = 0
            for k in range(64):
                tmp = int(block[int(k / 8)][k % 8])
                arr[ZigZag[k]] = tmp;
                # 统计arr数组中有多少个非0元素
                if tmp != 0:
                    notnull_num += 1
            # RLE编码
            # 标识连续0的个数
            time = 0
            # 处理DC
            if arr[0] != 0:
                notnull_num -= 1
            data = arr[0] - prev
            data2 = bin(np.abs(data))[2:]
            data1 = len(data2)
            if data < 0:
                data2 = bin(np.abs(data) ^ (2 ** data1 - 1))[2:].rjust(data1, '0')
            if data == 0:
                data2 = ''
                data1 = 0
            result += dc_huffman_table[data1]
            result += data2
            prev = arr[0]
            for k in range(1, 64):
                # 有可能AC系数全为0 所以要先进行判断
                if notnull_num == 0:
                    # 添加EOB
                    result += '1010'
                    break
                if arr[k] == 0 and time < 15:
                    time += 1
                else:
                    # BIT编码
                    # 处理括号中第二个数
                    data = arr[k]
                    data2 = bin(np.abs(data))[2:]
                    data1 = len(data2)
                    if data < 0:
                        data2 = bin(np.abs(data) ^ (2 ** data1 - 1))[2:].rjust(data1, '0')
                    if data == 0:
                        data1 = 0
                        data2 = ''
                    # 哈夫曼编码,序列化
                    result += ac_huffman_table[time * 16 + data1]

                    result += data2
                    time = 0
                    # 判断是否要添加EOB
                    if int(arr[k]) != 0:
                        notnull_num -= 1
    # 补足为8的整数倍,以便编码成16进制数据
    if len(result) % 8 != 0:
        result = result.ljust(len(result) + 8 - len(result) % 8, '0')
    res_data = ''
    for i in range(0, len(result), 8):
        temp = int(result[i:i + 8], 2)
        res_data += hex(temp)[2:].rjust(2, '0').upper()
        if temp == 255:
            res_data += '00'
    result = res_data
    res = ''

    # 添加jpeg文件头
    # SOI(文件头),共89个字节
    res += 'FFD8'
    # APP0图像识别信息
    res += 'FFE000104A46494600010100000100010000'
    # DQT定义量化表
    res += 'FFDB004300'
    # 64字节的量化表

    for i in range(64):
        res += hex(Qy[int(i / 8)][i % 8])[2:].rjust(2, '0')
    # SOF0图像基本信息,13个字节
    res += 'FFC0000B08'
    res += hex(h)[2:].rjust(4, '0')
    res += hex(w)[2:].rjust(4, '0')
    # res+='01012200'
    # 采样系数好像都是1
    res += '01011100'
    # DHT定义huffman表,33个字节+183
    res += 'FFC4001F00'
    for i in standard_dc_nrcodes:
        res += hex(i)[2:].rjust(2, '0')
    for i in standard_dc_values:
        res += hex(i)[2:].rjust(2, '0')
    res += 'FFC400B510'
    for i in standard_ac_nrcodes:
        res += hex(i)[2:].rjust(2, '0')

    for i in standard_ac_values:
        res += hex(i)[2:].rjust(2, '0')

    # SOS扫描行开始,10个字节
    res += 'FFDA0008010100003F00'
    # 压缩的图像数据(一个个扫描行),数据存放顺序是从左到右、从上到下
    res += result
    # EOI文件尾0
    res += 'FFD9'
    return res


'''
jpeg解压缩
img:解压缩的jpeg灰度图像文件
return:返回解压缩后的图像原数据,为多维数组形式
'''


def decompress(img):
    # jpeg解码的所有参数都是从编码后的jpeg文件中读取的
    with open(img, 'rb') as f:
        img_data = f.read()
    res = ''
    for i in img_data:
        res += hex(i)[2:].rjust(2, '0').upper()

    ZigZag = [
        0, 1, 5, 6, 14, 15, 27, 28,
        2, 4, 7, 13, 16, 26, 29, 42,
        3, 8, 12, 17, 25, 30, 41, 43,
        9, 11, 18, 24, 31, 40, 44, 53,
        10, 19, 23, 32, 39, 45, 52, 54,
        20, 22, 33, 38, 46, 51, 55, 60,
        21, 34, 37, 47, 50, 56, 59, 61,
        35, 36, 48, 49, 57, 58, 62, 63]
    # 获取亮度量化表
    Qy = np.zeros((8, 8))
    for i in range(64):
        Qy[int(i / 8)][i % 8] = int(res[50 + i * 2:52 + i * 2], 16)
        # 获取SOF0图像基本信息,图像的宽高
    h = int(res[188:192], 16)
    w = int(res[192:196], 16)
    # 获取DHT定义huffman表
    standard_dc_values = res[246:270]
    standard_dc_nrcodes = [0] * 16
    for i in range(16):
        standard_dc_nrcodes[i] = int(res[214 + i * 2:216 + i * 2], 16)

    standard_ac_values = res[312:636]
    standard_ac_nrcodes = [0] * 16
    for i in range(16):
        standard_ac_nrcodes[i] = int(res[280 + i * 2:282 + i * 2], 16)
    # 生成dc哈夫曼表
    pos_in_table = 0;
    code_value = 0;
    reverse_dc_huffman_table = {}

    for i in range(1, 9):
        for j in range(1, standard_dc_nrcodes[i - 1] + 1):
            reverse_dc_huffman_table[bin(code_value)[2:].rjust(i, '0')] = standard_dc_values[
                                                                          pos_in_table * 2:pos_in_table * 2 + 2]
            pos_in_table += 1
            code_value += 1
        code_value <<= 1
        # 生成ac哈夫曼表
    pos_in_table = 0;
    code_value = 0;
    reverse_ac_huffman_table = {}

    for i in range(1, 17):
        for j in range(1, standard_ac_nrcodes[i - 1] + 1):
            reverse_ac_huffman_table[bin(code_value)[2:].rjust(i, '0')] = standard_ac_values[
                                                                          pos_in_table * 2:pos_in_table * 2 + 2]
            pos_in_table += 1
            code_value += 1
        code_value <<= 1

    # 获取压缩的图像数据
    tmp_result = res[656:-4]
    result = ''
    i = 0
    while i < len(tmp_result):
        tmp0 = tmp_result[i:i + 2]
        result += tmp0
        i += 2
        if (tmp0 == 'FF'):
            i += 2
    # 得到哈夫曼编码后的01字符串
    result = bin(int(result, 16))[2:].rjust(len(result) * 4, '0')

    img_data = np.zeros((h, w))
    pos = 0
    prev = 0
    for j in range(h // 8):
        for k in range(w // 8):
            # 逆dc哈夫曼编码
            # 正向最大匹配
            arr = [0]
            # 计算EOB块中0的个数
            num = 0
            for i in range(8, 2, -1):
                tmp = reverse_dc_huffman_table.get(result[pos:pos + i])
                # 匹配成功
                if (tmp):
                    pos += i
                    num += 1
                    # DC系数为0
                    if tmp == '00':
                        # 是差值编码 差点忘了加上prev
                        arr[0] = 0 + prev
                        prev = arr[0]
                        break
                    data1 = int(tmp[1], 16)
                    data2 = result[pos:pos + data1]
                    if data2[0] == '0':
                        # 负数
                        data = -(int(data2, 2) ^ (2 ** data1 - 1))
                    else:
                        data = int(data2, 2)
                    arr[0] = data + prev
                    prev = arr[0]
                    pos += data1
                    break
            # 逆ac哈夫曼编码
            while (num < 64):
                # AC系数编码长度是从16bits到2bits
                for i in range(16, 1, -1):
                    tmp = reverse_ac_huffman_table.get(result[pos:pos + i])
                    if (tmp):
                        pos += i
                        if (tmp == '00'):
                            arr += ([0] * (64 - num))
                            num = 64
                            break
                        time = int(tmp[0], 16)
                        data1 = int(tmp[1], 16)
                        data2 = result[pos:pos + data1]
                        pos += data1
                        # data2为空,赋值为0,应对(15,0)这种情况
                        data2 = data2 if data2 else '0'
                        if data2[0] == '0':
                            # 负数,注意负号和异或运算的优先级
                            data = -(int(data2, 2) ^ (2 ** data1 - 1))
                        else:
                            data = int(data2, 2)
                        num += time + 1
                        # time个0
                        arr += ([0] * time)
                        # 非零值或最后一个单元0
                        arr.append(data)
                        break
            # 逆ZigZag扫描,得到block量化块
            block = np.zeros((8, 8))
            for i in range(64):
                block[int(i / 8)][i % 8] = arr[ZigZag[i]]
            # 逆量化
            block = block * Qy
            # 逆DCT变换
            block = cv2.idct(block)
            img_data[j * 8:(j + 1) * 8, k * 8:(k + 1) * 8] = block + 128
    img_data = img_data.astype(np.uint8)
    return img_data


def main():
    # 原始图像路径,灰度图像
    img_path = './lena_gray.bmp'
    # 读取原始图像,cv2.imread()默认是用color模式读取的,保持原样读取要加上第二个参数-1,即CV_LOAD_IMAGE_GRAYSCALE
    # 得到图像原数据流
    img_data = cv2.imread(img_path, -1)
    cv2.imwrite('./jpeg_compress.jpg', img_data)
    img0 = cv2.imread('./jpeg_compress.jpg', -1)
    # 得到压缩后图像数据
    img_compress = compress(img_data, 50)
    # 存储压缩后的图像
    img_compress_path = './img_compress.jpg'
    with open(img_compress_path, 'wb') as f:
        f.write(base64.b16decode(img_compress.upper()))
    # jpeg图像解压缩测试
    img_decompress = decompress(img_compress_path)
    img1 = cv2.imread('./img_compress.jpg', -1)

    # 结果展示
    plt.rcParams['font.sans-serif'] = ['SimHei']  # 中文乱码
    # 子图1,原始图像
    plt.subplot(221)
    # imshow()对图像进行处理,画出图像,show()进行图像显示
    plt.imshow(img_data, cmap=plt.cm.gray)
    plt.title('原始图像')
    # 不显示坐标轴
    plt.axis('off')

    # 子图2,自己写的jpeg压缩后解码图像
    plt.subplot(222)
    plt.imshow(img_decompress, cmap=plt.cm.gray)
    plt.title('自写jpeg图像(自解压)')
    plt.axis('off')

    # 子图3,jpeg压缩后解码图像
    plt.subplot(223)
    plt.imshow(img0, cmap=plt.cm.gray)
    plt.title('官方jpeg图像')
    plt.axis('off')

    # 子图3,jpeg压缩后解码图像
    plt.subplot(224)
    plt.imshow(img1, cmap=plt.cm.gray)
    plt.title('自写jpeg图像(官方解压)')
    plt.axis('off')
    plt.show()


if __name__ == '__main__':
    main()




实验结果

在这里插入图片描述

左上为原始图像,右上是读取左图的数据后采取上文代码进行jpeg压缩后(质量控制为50)的新的jpeg图像,左下是cv2压缩的jpeg图像,右下是使用上文代码压缩cv2解压缩的结果



参考



颜色空间转换RGB->YCbCr



JPEG图片格式详解



DCT离散余弦变换





JPEG彩色图像压缩(python实现)