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

Posted 智驱力人工智能

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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的扩展名。

下文以小狗图像为例,详述图片压缩具体过程,图像分辨率是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中将图像分为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编码是在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比特,压缩比为(64*8-65)/(64*8)*100%=87.3%

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

智驱力-科技驱动生产力

常见图片格式详解---JPEG

JPEG简介

JPEG是一种比较成熟的有损的图像压缩格式,经过JPEG压缩,图像质量会有所损失,但是,人眼不容易分辨出来这种差别。jpeg图像在质量和存储空间得到了一个相对平衡的状态。不过jpeg文件在组织方式上略显复杂,详细请向下看。


JPEG文件的存储方式

jpeg文件是按照段的格式来组织存储的,每一个文件由多个段组成,每个段代表不同的信息。同时,每个段也有自己唯一的标识符。标识符是由两个字节所组成,格式如0x FF XX,其中XX代表的是不同的类型。例如,SOI(start of image),表示图像的开始,其段头的标识符为 0X FF D8。而整个jpeg图片的组织便是由诸多这些不同类型的段和经过JPEG压缩后的数据而组成。如果解析,需要根据这些段不同的头类型来做相应的处理。

主要的JPEG段

> 1. SOI

SOI(start of image):值 0xFF D8,标记图像的开始。


> 2.APP0(应用程序标记)

APP0:值 0x FF E0,应用程序标记。组织结构如下:
技术分享图片


> 3.SOF

SOF(start of frame):值0x FF C0,图像帧开始标记。

技术分享图片

4SOS
技术分享图片

在SOS之后,便是具体的按照jpeg编码的压缩数据。该部分内容需要使用jpeg相应的解码库去实现解码。

在jpeg文件中,如果遇到了0xFF D9 , 那就表明整个文件读取结束了。该字段也有个名称叫做EOI(end of image),占用两个字节。

当然了,整个jpeg文件还有其他的段,仿照上述便同样可以分析,剩下的学习敬请诸君发挥您的主观能动性啦,希望本篇能对你有所启发。

以上是关于JPEG图像压缩详解和代码实现的主要内容,如果未能解决你的问题,请参考以下文章

常见图片格式详解---JPEG

逐步 JPEG 压缩的代码

构建一个JPEG解码器:帧和比特流

构建一个JPEG解码器:帧和比特流

构建一个JPEG解码器:帧和比特流

Opencv 图像处理:图像基础操作与灰度转化