天天看點

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格式為例講解圖像壓縮的過程。JPEG的檔案格式一般有兩種檔案擴充名:.jpg和.jpeg,這兩種擴充名的實質是相同的,我們可以把.jpg的檔案改名為.jpeg,而對檔案本身不會有任何影響。嚴格來講,JPEG的檔案擴充名應該為.jpeg,由于DOS時代的8.3檔案名命名原則,就使用了.jpg的擴充名。

下文以小狗圖像為例,詳述圖檔壓縮具體過程,圖像分辨率是320x264。首先看下圖:

JPEG圖像壓縮詳解和代碼實作

通常我們看到的彩色圖像是三通道或四通道圖像。三通道圖像是指有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)            

結果展示

JPEG圖像壓縮詳解和代碼實作

2. 降采樣

由于人眼對色度不敏感,直接将U、V分量進行色度采樣,JPEG壓縮算法采用YUV 4:2:0的色度抽樣方法。4:2:0表示對于每行掃描的像素,隻有一種色度分量以2:1的抽樣率存儲,也就是說每隔一行/列取值,偶數行取U值,奇數行取V值,UV通道寬度和高度分别降低為原來的1/2。

JPEG圖像壓縮詳解和代碼實作

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           

結果展示

JPEG圖像壓縮詳解和代碼實作

3. 離散餘弦變換(DCT)

人類視覺對高頻資訊不敏感,利用離散餘弦變換可分析出圖像中高低頻資訊含量,進而壓縮資料。

JPEG中将圖像分為8*8的像素塊,對每個像素塊利用離散餘弦變換進行頻域編碼,生成一個新的8*8的數字矩陣。對于不能被8整除的圖像大小,需對圖像填充使其可被8整除,通常使用0填充。由于離散餘弦變換需要定義域對稱,是以先将矩陣中的數值左移128,使值域範圍在[-128, 127]。

二維離散餘弦變換公式為:

JPEG圖像壓縮詳解和代碼實作
JPEG圖像壓縮詳解和代碼實作

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)           

結果展示

JPEG圖像壓縮詳解和代碼實作

4. 量化

每個8*8的像素塊經離散餘弦變換後生成一個8*8的浮點數矩陣,量化的過程則是去除矩陣中的高頻資訊,保留低頻資訊。JPEG算法提供了兩張标準化系數矩陣,分别處理亮度資料和色差資料,表示 50% 的圖像品質。

JPEG圖像壓縮詳解和代碼實作

量化的過程:使用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)           

結果展示

JPEG圖像壓縮詳解和代碼實作

5. ZIGZAG排序

排序規則如圖:

JPEG圖像壓縮詳解和代碼實作

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格式的圖像檔案。

智驅力-科技驅動生産力

繼續閱讀