JPEG图像压缩详解和代码实现

一、图像存储

为了有效的传输和存储图像,需要对图像数据进行压缩。依据图像的保真度,图像压缩可分为无损压缩和有损压缩。

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的扩展名。

下文以小狗图像为例,详述图片压缩具体过程,图像分辨率是320×264。首先看下图:

通常我们看到的彩色图像是三通道或四通道图像。三通道图像是指有RGB三个通道,R:红色,G:绿色,B:蓝色。四通道图像是在三通道的基础上加了Alpha通道,Alpha通道用来衡量一个像素的透明度。当Alpha为0时,该像素完全透明;当Alpha为255时,该像素完全不透明。四通道图像只有PNG格式支持。

图中小狗是三通道图像,有320×264个像素点,每个像素点由三个值表示,如上图右侧小狗眼睛部分,黑色区域每个通道的像素值较小如(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中将图像分为8*8的像素块,对每个像素块利用离散余弦变换进行频域编码,生成一个新的8*8的数字矩阵。对于不能被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. 量化

每个8*8的像素块经离散余弦变换后生成一个8*8的浮点数矩阵,量化的过程则是去除矩阵中的高频信息,保留低频信息。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编码是在8×8的块内部进行的,而对DC系数的DPCM编码是在整个图像上若干个8×8的块之间进行的。

差值编码原理:样值与前一个(相邻)样值的差值,则这些差值大多数是很小的或为零,可以用短码来表示;而对于出现几率较差的差值,用长码表示,这样可以使总体码数下降;采用对相邻样值差值进行变字节长编码的方式称为差值编码,又称为差分脉码调制(DPCM)。

8×8的图像块经过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编码采用查表的方式来高效地完成。

    上文中8×8像素块的中间格式:

  • 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。

  • 因此,这个8×8的亮度像素块信息压缩后的数据流为1110110010,0101,10110010,100000,0100,11000,11000,0101,11000,11000,11000,1010。总共65比特,压缩比为(64*8-65)/(64*8)*100%=87.3%

    以上是JPEG压缩的整个过程,最终将所有编码结果整合并按JPEG规范格式存储,即可得到jpg格式的图像文件。

    智驱力-科技驱动生产力

    物联沃分享整理
    物联沃-IOTWORD物联网 » JPEG图像压缩详解和代码实现

    发表评论