一、图像存储
为了有效的传输和存储图像,需要对图像数据进行压缩。依据图像的保真度,图像压缩可分为无损压缩和有损压缩。
1. 无损压缩
无损压缩的基本原理是相同的颜色信息只需保存一次。无损压缩保证解压以后的数据和原始数据完全一致,压缩时去掉或减少数据中的冗余,解压时再重新插到数据中,是一个可逆过程。无损压缩算法一般可以把普通文件的数据压缩到原来的1/2-1/4。
2. 有损压缩
有损压缩方式在解压后图像像素值会发生改变,解压以后的数据和原始数据不完全一致,是不可逆压缩方式。在保存图像时保留了较多的亮度信息,将冗余信息合并,合并的比例不同,压缩的比例也就不同。由于信息量减少了,所以压缩比可以很高,图像质量也会下降。
二、图像格式
常见有损的图像格式有:JPEG、WebP,常见无损的图像格式有:PNG、BMP、GIF。
通常以文件的后缀名来区分图片的格式,但有时并不准确。实际的图片格式可通过查看图片数据来确定(查看方式:Notepad++打开图片,选择“插件”->“插件管理”,安装“HEX-Editor”,安装后再次选择“插件”->“HEX-Editor”->“View in HEX”)。
以JPEG和PNG图像格式为例。JPEG格式以0xFF D8开头,以0xFF D9结尾。PNG格式以0x89 50 4E 47 0D 0A 1A 0A开头,其中50 4E 47是英文字符串“PNG”的ASCII码,以00 00 00 00 49 45 4E 44 AE 42 60 82结尾,标志着PNG数据流结束。
三、JPEG压缩
上文的图例是图像文件实际保存的数据,也就是图像压缩后的数据。本文以JPEG格式为例讲解图像压缩的过程。JPEG的文件格式一般有两种文件扩展名:.jpg和.jpeg,这两种扩展名的实质是相同的,我们可以把.jpg的文件改名为.jpeg,而对文件本身不会有任何影响。严格来讲,JPEG的文件扩展名应该为.jpeg,由于DOS时代的8.3文件名命名原则,就使用了.jpg的扩展名。
下文以小狗图像为例,详述图片压缩具体过程,图像分辨率是320x264。首先看下图:
通常我们看到的彩色图像是三通道或四通道图像。三通道图像是指有RGB三个通道,R:红色,G:绿色,B:蓝色。四通道图像是在三通道的基础上加了Alpha通道,Alpha通道用来衡量一个像素的透明度。当Alpha为0时,该像素完全透明;当Alpha为255时,该像素完全不透明。四通道图像只有PNG格式支持。
图中小狗是三通道图像,有320x264个像素点,每个像素点由三个值表示,如上图右侧小狗眼睛部分,黑色区域每个通道的像素值较小如(3,2,11),白点部分像素值较高如(114,116,117)。图中共84480个像素,每个像素用24位表示,若直接存储需要占用84480*24/8/1024=247.5KB,为了有效地传输和存储图像,有必要对图像做压缩。JPEG压缩步骤如下。
1. 色彩空间转换
JPEG采用YUV颜色空间,“Y”表示明亮度,也就是灰度值;“U”和“V”表示色度,用于描述图像色彩和饱和度。因为人眼对亮度比较敏感,而对于色度不那么敏感,可以在UV维度大量缩减信息,所以先将RGB的数据转换到YUV色彩空间。转换公式:
- Y = 0.299R + 0.587G + 0.114B
- U = 0.5R - 0.4187G - 0.0813G + 128
- V = -0.1687R - 0.3313G + 0.5B + 128
python 实现
import cv2
import numpy as np
# opencv 读取的图片是BGR顺序
image = cv2.imread('data/dog.jpg')
h, w, c = image.shape
# 色彩空间转换 BGR -> YUV
image_yuv = np.zeros_like(image, dtype=np.uint8)
for line in range(h):
for row in range(w):
B = image[line, row, 0]
G = image[line, row, 1]
R = image[line, row, 2]
Y = np.round(0.299*R + 0.587*G + 0.114*B)
U = np.round(0.5*R - 0.4187*G - 0.0813*G + 128)
V = np.round(-0.1687*R - 0.3313*G + 0.5*B + 128)
image_yuv[line, row, :] = (Y, U, V)
# 保存图像
cv2.imwrite('Y.png', image_yuv[:,:, 0])
cv2.imwrite('U.png', image_yuv[:,:, 1])
cv2.imwrite('V.png', image_yuv[:,:, 2])
cv2.imwrite('YUV.png', image_yuv)
结果展示
2. 降采样
由于人眼对色度不敏感,直接将U、V分量进行色度采样,JPEG压缩算法采用YUV 4:2:0的色度抽样方法。4:2:0表示对于每行扫描的像素,只有一种色度分量以2:1的抽样率存储,也就是说每隔一行/列取值,偶数行取U值,奇数行取V值,UV通道宽度和高度分别降低为原来的1/2。
python 实现
# 色彩空间转换 BGR -> YUV 4:2:0
def RGB2YUV420(image):
h, w, c = image.shape
image_y = np.zeros((h, w), dtype=np.uint8)
image_u = np.zeros(((h-1)//2+1, (w-1)//2+1), dtype=np.uint8)
image_v = np.zeros(((h-1)//2+1, (w-1)//2+1), dtype=np.uint8)
for line in range(h):
for row in range(w):
B = image[line, row, 0]
G = image[line, row, 1]
R = image[line, row, 2]
Y = np.round(0.299*R + 0.587*G + 0.114*B)
image_y[line, row] = Y
if line % 2 == 0 and row % 2 == 0:
U = np.round(0.5*R - 0.4187*G - 0.0813*G + 128)
image_u[line//2, row//2] = U
if line % 2 == 1 or line == h-1:
V = np.round(-0.1687*R - 0.3313*G + 0.5*B + 128)
image_v[line//2, row//2] = V
return image_y, image_u, image_v
结果展示
3. 离散余弦变换(DCT)
人类视觉对高频信息不敏感,利用离散余弦变换可分析出图像中高低频信息含量,进而压缩数据。
JPEG中将图像分为88的像素块,对每个像素块利用离散余弦变换进行频域编码,生成一个新的88的数字矩阵。对于不能被8整除的图像大小,需对图像填充使其可被8整除,通常使用0填充。由于离散余弦变换需要定义域对称,所以先将矩阵中的数值左移128,使值域范围在[-128, 127]。
二维离散余弦变换公式为:
python 实现
import math
def alpha(u):
if u==0:
return 1/np.sqrt(8)
else:
return 1/2
def block_fill(block):
block_size = 8
dst = np.zeros((block_size, block_size), dtype=np.uint8)
h, w = block.shape
dst[:h, :w] = block
return dst
def DCT_block(img):
block_size = 8
img = block_fill(img)
img_fp32 = img.astype(np.float32)
img_fp32 -= 128
img_dct = np.zeros((block_size, block_size), dtype=np.float32)
for line in range(block_size):
for row in range(block_size):
n = 0
for x in range(block_size):
for y in range(block_size):
n += img_fp32[x,y]*math.cos(line*np.pi*(2*x+1)/16)*math.cos(row*np.pi*(2*y+1)/16)
img_dct[line, row] = alpha(line)*alpha(row)*n
return np.ceil(img_dct)
def DCT(image):
block_size = 8
h, w = image.shape
dlist = []
for i in range((h + block_size - 1) // block_size):
for j in range((w + block_size - 1) // block_size):
img_block = image[i*block_size:(i+1)*block_size, j*block_size:(j+1)*block_size]
# 处理一个像素块
img_dct = DCT_block(img_block)
dlist.append(img_dct)
return dlist
img_dct = DCT(image_y)
结果展示
4. 量化
每个88的像素块经离散余弦变换后生成一个88的浮点数矩阵,量化的过程则是去除矩阵中的高频信息,保留低频信息。JPEG算法提供了两张标准化系数矩阵,分别处理亮度数据和色差数据,表示 50% 的图像质量。
量化的过程:使用DCT变换后的浮点矩阵除以量化表中数值,然后取整。量化表是控制JPEG压缩比的关键,可以根据输出图片的质量来自定义量化表,通常自定义量化表与标准量化表呈比例关系,表中数字越大则质量越低,压缩率越高。
python 实现
def quantization(blocks, Q):
img_quan = []
for block in blocks:
img_quan.append(np.round(np.divide(block, Q)))
return img_quan
img_quan = quantization(img_dct, Qy)
结果展示
5. ZIGZAG排序
排序规则如图:
python 实现
def zigzag(blocks):
block_list = []
for block in blocks:
zlist = []
w, h = block.shape
if w != h:
return None
max_sum = w + h - 2
for _s in range(max_sum + 1):
if _s % 2 == 0:
for i in range(_s, -1, -1):
j = _s - i
if i >= w or j >= h:
continue
zlist.append(block[i,j])
else:
for j in range(_s, -1, -1):
i = _s - j
if i >= w or j >= h:
continue
zlist.append(block[i,j])
block_list.append(zlist)
return block_list
zglist = zigzag(img_quan)
结果展示
[39.0, 4.0, -4.0, 0.0, -0.0, 2.0, -2.0, -1.0, -1.0, -1.0, 0.0, -0.0, -0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.0, -0.0, 0.0, 0.0, -0.0, 0.0, -0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.0, -0.0, 0.0, -0.0, 0.0, 0.0, -0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
6. 差分脉冲编码调制(DPCM)对直流系数(DC)编码
对像素矩阵做DCT变换,相当于将矩阵的能量压缩到第一个元素中,左上角第一个元素被称为直流(DC)系数,其余的元素被称为交流(AC)系数。JPEG将量化后的频域矩阵中的DC系数和AC系数分开编码。使用DPCM技术,对相邻图像块量化DC系数的差值进行编码;使用行程长度编码(RLE)对AC系数编码。需要注意的一点是,对AC系数的的RLE编码是在8x8的块内部进行的,而对DC系数的DPCM编码是在整个图像上若干个8x8的块之间进行的。
差值编码原理:样值与前一个(相邻)样值的差值,则这些差值大多数是很小的或为零,可以用短码来表示;而对于出现几率较差的差值,用长码表示,这样可以使总体码数下降;采用对相邻样值差值进行变字节长编码的方式称为差值编码,又称为差分脉码调制(DPCM)。
8x8的图像块经过DCT变换后,得到的直流系数特点:
- 系数值较大;
- 相邻图像块的系数值变换不大。
python 实现
def DPCM(zglist):
res_dpcm = []
for i in range(len(zglist)):
if i == 0:
res_dpcm.append(zglist[i][0])
continue
res_dpcm.append(zglist[i][0]-zglist[i-1][0])
return res_dpcm
res_dpcm = DPCM(zglist)
结果展示
[50.0, -2.0, -13.0, -7.0, -3.0, 0.0, -1.0, 0.0, -1.0, -2.0, -0.0, -1.0, 0.0, -1.0, -0.0, -1.0, 0.0, -0.0, -0.0, -0.0, -0.0, -0.0, 0.0, -0.0, -0.0, ..., -0.0, 0.0, -0.0, 0.0, -0.0]
7. DC系数中间格式
JPEG中为了更进一步节约空间,不直接保存数据的具体数值,而是将数据按照位数分为16组,保存在表里面。这也就是所谓的变长整数编码VLI。编码VLI表如下:
以第一个block和第二个block为例,DPCM结果是50,通过查找VLI编码表该值位于VLI表格的第6组,因此可以写成(6)(50)的形式,即为DC系数的中间格式。
8. 行程长度编码(RLC)对交流系数(AC)编码
具有相同颜色并且是连续的像素数目称为行程长度。RLC编码简单直观,编码/解码速度快。例如,字符串AAABCDDDDDDDDBBBBB 利用RLE原理可以压缩为3ABC8D5B。在JPEG编码中,使用的数据对是(两个非零AC系数之间连续0的个数,下一个非零AC系数的值)。注意,如果AC系数之间连续0的个数超过16,则用一个扩展字节(15,0)来表示16连续的0。
python 实现
def rlc(zglist):
res_ac = []
for i in range(len(zglist)):
ac = []
zg = zglist[i]
zero_num = 0
for k in range(1, len(zg)):
if zg[k] != 0:
ac.append((zero_num, zg[k]))
zero_num = 0
else:
zero_num += 1
if zero_num:
ac.append((0, 0))
res_ac.append(ac)
return res_ac
res_ac = rlc(zglist)
结果展示
zigzag结果:[50.0, -2.0, -13.0, -7.0, -3.0, 0.0, -1.0, 0.0, -1.0, -2.0, -0.0, -1.0, 0.0, -1.0, -0.0, -1.0, 0.0, -0.0, -0.0, -0.0, -0.0, -0.0, 0.0, -0.0, -0.0, ..., -0.0, 0.0, -0.0, 0.0, -0.0]
RLC编码结果:[(0, -2.0), (0, -13.0), (0, -7.0), (0, -3.0), (1, -1.0), (1, -1.0), (0, -2.0), (1, -1.0), (1, -1.0), (1, -1.0), (0, 0)]
9. AC系数中间格式
RLC编码结果:[(0, -2.0), (0, -13.0), (0, -7.0), (0, -3.0), (1, -1.0), (1, -1.0), (0, -2.0), (1, -1.0), (1, -1.0), (1, -1.0), (0, 0)]
对每组数据第二个数进行VLI编码,(0, -2.0)第二个数是-2.0,查找VLI编码表是第2组,所以可将其写(0, 2), -2.0。同理,AC系数中间格式可写成以下形式:
(0, 2), -2.0, (0, 4), -13.0, (0, 3), -7.0, (0, 2), -3.0, (1, 1), -1.0, (1, 1), -1.0, (0, 2), -2.0, (1, 1), -1.0, (1, 1), -1.0, (1, 1), -1.0, (0, 0)
10. 熵编码
JPEG基本系统规定采用Huffman编码。Huffman编码时DC系数与AC系数分别采用不同的Huffman编码表,对于亮度和色度也采用不同的Huffman编码表。因此,需要4张Huffman编码表才能完成熵编码的工作。具体的Huffman编码采用查表的方式来高效地完成。
上文中8x8像素块的中间格式:
- DC: (6)(50),数字6查DC亮度Huffman编码表是1110,数字50查VLI编码表是110010。
- AC: (0, 2), -2.0, (0, 4), -13.0, (0, 3), -7.0, (0, 2), -3.0, (1, 1), -1.0, (1, 1), -1.0, (0, 2), -2.0, (1, 1), -1.0, (1, 1), -1.0, (1, 1), -1.0, (0, 0),(0,2)查AC亮度Huffman编码表是01,-2.0查VLI编码表是01。
因此,这个8x8的亮度像素块信息压缩后的数据流为1110110010,0101,10110010,100000,0100,11000,11000,0101,11000,11000,11000,1010。总共65比特,压缩比为(648-65)/(648)*100%=87.3%
以上是JPEG压缩的整个过程,最终将所有编码结果整合并按JPEG规范格式存储,即可得到jpg格式的图像文件。
网友评论