美文网首页
JPEG图像压缩详解和代码实现

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

作者: 智驱力AI | 来源:发表于2023-01-28 14:13 被阅读0次

    一、图像存储

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

    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格式的图像文件。

    智驱力-科技驱动生产力

    相关文章

      网友评论

          本文标题:JPEG图像压缩详解和代码实现

          本文链接:https://www.haomeiwen.com/subject/tigyhdtx.html