JPEG格式壓縮算法


  • 一、JPEG原理概述
  • 二、JPEG原理詳細分析及壓縮算法過程
    • 1、Color Model Conversion (色彩模型)
    • 2、DCT (Discrete Cosine Transform 離散余弦變換)
    • 3、數據量化
    • 4、重排列 DCT 結果
    • 5、基於差分脈沖編碼調制的DC編碼
    • 6、RLE編碼
    • 7、范式Huffman編碼
    • 8、JPEG壓縮過程總結
  • 三、JPEG存儲格式
  • 四、JPEG壓縮的GPU優化

一、JPEG原理概述

JPEG 是 Joint Photographic Experts Group 的縮寫,即 ISO 和 IEC 聯合圖像專家組,負責靜態圖像壓縮標准的制定,這個專家組開發的算法就被稱為 JPEG 算法,並且已經成為了大家通用的標准,即 JPEG 標准。 JPEG 壓縮是有損壓縮,但這個損失的部分是人的視覺不容易察覺到的部分,它充分利用了人眼對計算機色彩中的高頻信息部分不敏感的特點,來大大節省了需要處理的數據信息。

JPEG格式在圖片中的地位相當於mp3格式在音樂中的地位一樣,都是對於原始數據的有損壓縮,舉個例子,一張1000*1000的圖片,RGB三個通道各占一個字節,所以未經壓縮的情況下其圖像信息的大小大約為1000*1000*3=3000000字節,約等於2.86MB,這個文件大小看過小說的人都能想象到,大概應該是150w字到200w字,而經過JPEG壓縮后,其大小能達到300KB左右,壓縮比一般為1:8左右。而JPEG如何實現如此高的壓縮比呢?前面提到過JPEG是有損壓縮,所謂有損,就是把圖片中不重要,人眼對其不敏感的東西過濾掉,以達到壓縮文件大小的目的,比如12345.0000000001這個數字,我們可以將其視為12345,而忽略的部分就是因為其包含的信息太少。接下來,在存儲過程中,我們可以使用一些特殊的方式對存儲結構進行優化,以達到進一步壓縮文件大小的目的。所以,對原始圖像信息進行JPEG編碼的過程就分為兩大步:第一步,去除視覺上的多余信息,即空間冗余度;第二步,去除數據本身的多余信息,即結構冗余度。

二、JPEG原理詳細分析及壓縮算法過程

整個JPEG編碼中主要涉及的內容包括:Color Model Conversion (色彩模型)、DCT (Discrete Cosine Transform 離散余弦變換)、數據量化、重排列 DCT 結果、基於差分脈沖編碼調制的DC編碼、RLE編碼和范式Huffman編碼。接下來我們詳細講解一下。

1、Color Model Conversion (色彩模型)

在圖像處理中,為了利用人的視角特性,從而降低數據量,通常把 RGB 空間表示的彩色圖像變換到其他色彩空間。現在我們采用的色彩空間變換有三種:YIQ,YUV 和 YCrCb。

對計算機而言,計算機用的數字域的色彩空間變換與電視模擬域的色彩空間變換不同,它們的分量使用 Y、Cr 和 Cb 來表示,所以需要將RGB轉化為YCrCb,其轉換關系如下: 

這里的Y表示亮度(Luminance),Cb和Cr分別表示綠色和紅色的“色差值”。

從這里,就可以看出,計算出來的 Y、Cr 和 Cb 分量,會出現大量的小數,即浮點數,從而導致了在JPEG 編碼過程中會出現大量的浮點數的運算,當然經過一定的優化,這些浮點數運算可以用移位與加法這些計算機能更快速處理的方式來對其進行編碼

注意一點,實際上,JPEG 算法與色彩空間無關,色彩空間是涉及到圖像采樣的問題,它和數據的壓縮並沒有直接的關系。JPEG 算法處理的彩色圖像是單獨的彩色分量圖像,因此它可以壓縮來自不同色彩空間的數據,如 RGB,YcbCr 和 CMYK。

人眼對構成圖像的不同頻率成分具有不同的敏感度,這個是由人眼的視覺生理特性所決定的。如人的眼睛含有對亮度敏感的柱狀細胞1.8億個,含有對色彩敏感的椎狀細胞0.08億個,由於柱狀細胞的數量遠大於椎狀細胞,所以眼睛對亮度的敏感程度要大於對色彩的敏感程度。比如下面這張圖:

可以明顯看到,亮度圖的細節更加豐富。JPEG把圖像轉換為YCbCr之后,就可以針對數據得重要程度的不同做不同的處理。這就是為什么JPEG一般使用這種顏色空間的原因。

2、DCT (Discrete Cosine Transform 離散余弦變換)

前面我們提到過,人眼對計算機色彩中的高頻信息部分不敏感,所以如果能將圖像中的高頻部分過濾掉就可以實現對圖像的壓縮了。問題是,我們如何將色彩域的圖像轉換到頻域上呢?在數字通信原理中接觸到過快速傅立葉變換,離散余弦變換就是傅立葉變換的另外一種形式,傅立葉變換可以將時域信號轉化成頻域信號,其源於傅立葉曾經提出的一個著名想法,他認為任何周期性的函數,都可以分解為一系列的三角函數的組合,這個想法當時被拉格朗日所質疑,他提出三角函數無論如何組合,都無法表達帶有“尖角”的函數,雖然最后拉格朗日是正確的,但是只要三角函數足夠多,我們就可以無限逼近最終結果,舉個例子,來看一下如何用三角函數描述一個矩形方波:

 

當我們要處理的不再是函數,而是一堆離散的數據時,那么傅里葉變化出來的函數只含有余弦項,這種變換稱為離散余弦變換。舉個例子,有一組一維數據[x0,x1,x2,…,xn-1],那么可以通過DCT變換得到n個變換級數Fi:

此時原始數據Xi可以通過離散余弦變換變化的逆變換(IDCT)表達出來:

也就是說,經過DCT變換,可以把一個數組分解成數個數組的和,如果我們數組視為一個一維矩陣,那么可以把結果看做是一系列矩陣的和:

舉個例子,我們有一個長度為8的數字,為50,55,67,80,-10,-5,20,30,經過DCT轉換,得到8個級數為287.0,106.3,14.2,-110.8,9.2,65.7,-8.2,-43.9,根據公式把這個數組轉換為8個新的數組的和,如果我們使用圖像來表達的話,就可以發現DCT轉換的有趣之處了:

 

從上圖可以看出,看似雜亂的數據經過DCT變換之后會變成幾個工整變化的數據,而DCT轉換后的數組中第一個是直線數據,所以稱之為直流數據,簡稱DC,其余數據為交流數據,簡稱AC。

在JPEG壓縮算法中,先會把整個圖像分割成8*8的圖像塊,再對每一個圖像塊進行DCT變換,而二維DCT變換公式為:

我們來用一個極端的例子看一下DCT變換的威力究竟有多大:

經過DCT變換之后,整個圖像的能量都被集中在了左上角的直流分量中。再來看一個普遍一點的例子:

可以明顯看出,DCT變化后,矩陣被分成了直流分量和交流分量兩個部分,而一直到這里,整個過程都是可逆的,圖像仍然是無損的,而這一步為后面的圖像壓縮起了鋪墊作用,所以DCT變換是JPEG壓縮算法的核心。

3、數據量化

經過前兩步之后,整個圖像會分解成若干個8*8的小矩陣,而每個小矩陣又分為Y、Cb、Cr三個分量,我們以一個Y分量矩陣為例:

現在的問題在於如何在可以損失一些精度的情況下用更少的空間存儲這些浮點數呢?答案就是量子化。舉個例子,比如在游戲中處理角色的面朝方向時,一般不用0到2π這樣的浮點數,而是把方向分成16個區間,用整數來表示,這樣一個方向只需要4個bit。JPEG提供的量子化算法如下:


其中G是我們要處理的圖像矩陣,Q是量化系數矩陣,在JPEG算法中提供了兩張標准量化系數矩陣,分別用於處理亮度數據和色差數據:

經過量子化后,原矩陣變為了:

我們可以看到,經過壓縮之后出現了大面積的0,這十分有利於數據的壓縮,在實際的壓縮過程中,我們還可以將這個矩陣乘以一個系數,代表壓縮率,以控制出現更多或更少的0,進而控制壓縮質量,系數的取值是0到1之間的實數。總體上來說,DCT 變換實際是空間域的低通濾波器。對亮度分量采用細量化,對色差分量采用粗量化。

4、重排列 DCT 結果

在量化之后,8*8的矩陣仍然是二維矩陣,如何調整我們DCT的結果能更高地提升壓縮率呢?觀察量化后的矩陣,我們發現大量信息都集中在左上角,所以我們采用ZigZag編排,如圖:

其結果就變為了:−26,−3,0,−3,−3,−6,2,−4,1 −4,1,1,5,1,2,−1,1,−1,2,0,0,0,0,0,−1,−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的個數,這樣我們就可以更好地進行數據壓縮。

5、基於差分脈沖編碼調制的DC編碼

觀察8*8矩陣DCT之后的DC分量和AC分量,我們可以看出,DC分量的值很明顯大於AC分量,而且對於相鄰的8*8矩陣,其DC分量的系數值變化不大,這是因為圖片的能量基本都集中在低頻分量中,而圖片大部分是有連續性的,即相鄰矩陣中能量變化比較平穩,所以我們采用差分脈沖調制編碼(DPCM)技術,對相鄰圖像塊之間量化DC系數的差值進行編碼。

6、RLE編碼

Run Length Encoding,行程編碼又稱“運行長度編碼”或“游程編碼”,它是一種無損壓縮編碼。例如:5555557777733322221111111,這個數據的一個特點是相同的內容會重復出現很多次,那么就可以用一種簡化的方法來記錄這一串數字,如(5,6)(7,5)(3,3)(2,4)(1,7)即為它的行程編碼。行程編碼的位數會遠遠少於原始字符串的位數。舉個例子來看一下:57,45,0,0,0,0,23,0,-30,-16,0,0,1,0,0,0,0 ,0 ,0 ,0,..,0,可以表示為:(0,57) ; (0,45) ; (4,23) ; (1,-30) ; (0,-16) ; (2,1) ; EOB。即每組數字的頭一個表示0的個數,而且為了能更有利於后續的處理,必須是 4 bit,就是說,只能是 0~15,這是這個行程編碼的一個特點。JPEG使用了1個字節的高4位來表示連續“0”的個數,而使用它的低4位來表示編碼下一個非“0”系數所需要的位數,跟在它后面的是量化AC系數的數值。其中(0,0)和(15,0)比較特殊,(0,0)代表塊結束,(15,0)代表已經有16個連續的0。

7、范式Huffman編碼

DPCM和RLE編碼后,為了進一步壓縮,我們采用范式Huffman編碼,用一個例子來看一下ZIGZAG后的數據是如何壓縮的:

由於第一個是DC分量,采用DPCM技術,所以我們假設他的上一個矩陣的DC分量值為0,即35代表了其差值,則我們對原始數據的AC分量進行RLE編碼后結果為:

接下來我們要處理的是括號右邊的數據,JPEG提供了一張標准的碼表:

所以我們的原始數據變成了:

將括號內的前兩個字進行合並,變成一個字節:

接下來我們就要使用哈夫曼編碼了,假設我們現在的Huffman表如下,DC分量:

AC分量: 

 

經過哈夫曼編碼后,數據變為了:

綜上,最終我們使用了10個字節的空間保存了原本64字節的數據,至此,整個JPEG壓縮算法全部結束。

8、JPEG壓縮過程總結

所以,我們最后總結一下整個JPEG壓縮圖片的過程:

  • 將整張圖片分為若干8*8的矩陣
  • 對每個8*8矩陣進行DCT變換
  • 對DCT后的矩陣進行量子化
  • 重新進行ZIGZAG排序
  • 將DC分量和AC分量分別進行DPCM和RLE編碼
  • 將整體信息進行Huffman編碼

三、JPEG存儲格式

要注意的是,前面我們講的都是JPEG壓縮算法,這個標准只說明了如果將圖片壓縮成字節流以及重新解碼成圖片的過程,而JPEG標准定義的文件存儲格式是JIF,但是其有一定缺陷,使用率不高,而后陸續又出現了不同的文件存儲格式,如JFIF、EXIF等,但是他們都遵循JIF。

前面講了整個JPEG壓縮算法的全過程,顯然,JPEG大體的存儲格式就基本清晰了,我們至少需要字段去存儲量化表、哈夫曼表和壓縮后的數據。接下來我們就看一下JPEG格式的存儲格式到底是什么樣的:

JPEG以0xFF為Marker,當遇到0xFF就需要判斷:

  • 如果是0X00,表示0XFF是圖像流的組成部分;需要進行譯碼;
  • 如果是0XD0~0XD7,組成RSTn標記,需要忽視整個RSTn標記,即不對當前0XFF和緊接着的0XDn兩個字節進行譯碼,並按RST標記的規則調整譯碼變量;
  • 如果是0XFF,忽略當前0XFF,對后一個0XFF進行判斷;
  • 如果是已有的頭部標記,則進行對應的譯碼;
  • 如果是其它數值,忽然當前0XFF,並保留緊接着此數值用於譯碼;

而頭部標記碼及其含義如下:

SOI,Start Of Image, 圖像開始,標記代碼為固定值0XFFD8,用2字節表示;

APP0,Application 0, 應用程序保留標記0,標記代碼為固定值0XFFE0,用2字節表示;該標記碼之后包含了9個具體的字段:

(1)數據長度:2個字節,用來表示(1)--(9)的9個字段的總長度,即不包含標記代碼但包含本字段;

(2)標示符:5個字節,固定值0X4A6494600,表示了字符串“JFIF0”;

(3)版本號:2個字節,一般為0X0102,表示JFIF的版本號為1.2;但也可能為其它數值,從而代表了其它版本號;

(4)X,Y方向的密度單位:1個字節,只有三個值可選,0:無單位;1:點數每英寸;2:點數每厘米;

(5)X方向像素密度:2個字節,取值范圍未知;

(6)Y方向像素密度:2個字節,取值范圍未知;

(7)縮略圖水平像素數目:1個字節,取值范圍未知;

(8)縮略圖垂直像素數目:1個字節,取值范圍未知;

(9)縮略圖RGB位圖:長度可能是3的倍數,保存了一個24位的RGB位圖;如果沒有縮略位圖(這種情況更常見),則字段(7)(8)的取值均為0;

APPn, Application n, 應用程序保留標記n(n=1---15),標記代碼為2個字節,取值為0XFFE1--0XFFEF;包含了兩個字段:

(1)數據長度,2個字節,表示(1)(2)兩個字段的總長度;即,不包含標記代碼,但包含本字段;

(2)詳細信息:數據長度-2個字節,內容不定;

DQT,Define Quantization Table, 定義量化表;標記代碼為固定值0XFFDB;包含9個具體字段:

(1)數據長度:2個字節,表示(1)和多個(2)字段的總長度;即,不包含標記代碼,但包含本字段;

(2)量化表:數據長度-2個字節,其中包括以下內容:

(a)精度及量化表ID,1個字節,高4位表示精度,只有兩個可選值,0:8位;1:16位;低4位表示量化表ID,取值范圍為0--3;

(b)表項,64*(精度取值+1)個字節,例如,8位精度的量化表,其表項長度為64*(0+1)=64字節;

本標記段中,(2)可以重復出現,表示多個量化表,但最多只能出現4次;

SOFO,Start Of Frame, 幀圖像開始,標記代碼為固定值0XFFC0;包含9個具體字段:

(1)數據長度:2個字節,(1)--(6)共6個字段的總長度;即,不包含標記代碼,但包含本字段;

(2)精度:1個字節,代表每個數據樣本的位數;通常是8位;

(3)圖像高度:2個字節,表示以像素為單位的圖像高度,如果不支持DNL就必須大於0;

(4)圖像寬度:2個字節,表示以像素為單位的圖像寬度,如果不支持DNL就必須大於0;

(5)顏色分量個數:1個字節,由於JPEG采用YCrCb顏色空間,這里恆定為3;

(6)顏色分量信息:顏色分量個數*3個字節,這里通常為9個字節;並依此表示如下一些信息:

(a)顏色分量ID: 1個字節;

(b)水平/垂直采樣因子:1個字節,高4位代表水平采樣因子,低4位代表垂直采樣因子;

(c)量化表:1個字節,當前分量使用的量化表ID;

本標記段中,字段(6)應該重復出現3次,因為這里有3個顏色分量;

DHT,Define Huffman Table定義Huffman表,標記碼為0XFFC4;包含2個字段:

(1)數據長度,2個字節,表示(1)--(2)的總長度,即,不包含標記代碼,但包含本字段;

(2)Huffman表,數據長度-2個字節,包含以下字段:

(a)表ID和表類型,1個字節,高4位表示表的類型,取值只有兩個;0:DC直流;1:AC交流;低4位,Huffman表ID;需要提醒的是,DC表和AC表分開進行編碼;

(b)不同位數的碼字數量,16個字節;

(c)編碼內容,16個不同位數的碼字數量之和(字節);

本標記段中,字段(2)可以重復出現,一般需要重復4次。

DRI,Define Restart Interval,定義差分編碼累計復位的間隔,標記碼為固定值0XFFDD;

包含2個具體字段:

(1)數據長度:2個字節,取值為固定值0X0004,表示(1)(2)兩個字段的總長度;即,不包含標記代碼,但包含本字段;

(2)MCU塊的單元中重新開始間隔:2個字節,如果取值為n,就代表每n個MCU塊就有一個RSTn標記;第一個標記是RST0,第二個是RST1,RST7之后再從RST0開始重復;如果沒有本標記段,或者間隔值為0,就表示不存在重開始間隔和標記RST;

SOS,Start Of Scan,掃描開始;標記碼為0XFFDA,包含2個具體字段:

(1)數據長度:2個字節,表示(1)--(4)字段的總長度;

(2)顏色分量數目:1個字節,只有3個可選值,1:灰度圖;3:YCrCb或YIQ;4:CMYK;

(3)顏色分量信息:包括以下字段,

(a)顏色分量ID:1個字節;

(b)直流/交流系數表ID,1個字節,高4位表示直流分量的Huffman表的ID;低4位表示交流分量的Huffman表的ID;

(4)壓縮圖像數據

(a)譜選擇開始:1個字節,固定值0X00;

(b)譜選擇結束:1個字節,固定值0X3F;

(c)譜選擇:1個字節,固定值0X00;

本標記段中,(3)應該重復出現,有多少個顏色分量,就重復出現幾次;本段結束之后,就是真正的圖像信息了;圖像信息直到遇到EOI標記就結束了;

EOI,End Of Image,圖像結束;標記代碼為0XFFD9;

四、JPEG壓縮的GPU優化

CUDA是Nvidia出的面向GPU編程的平台,在CUDA中,將PC稱為Host端,顯卡稱為Device端,提供了__global__宏用來定義核函數進行GPU運算,以及許多malloc、free、memcpy函數用來申請、釋放顯存,或將數據在內存和顯存間傳送,用<<<N,M>>>來指定開啟N個線程塊,每個塊內M個線程來執行核函數。

舉個例子看一下:

 1 #include <stdio.h>
 2 #include<cuda_runtime.h>
 3  
 4 //__global__聲明的函數,告訴編譯器這段代碼交由CPU調用,由GPU執行
 5 __global__ void add(const int *dev_a,const int *dev_b,int *dev_c)
 6 {
 7     int i=threadIdx.x;
 8     dev_c[i]=dev_a[i]+dev_b[i];
 9 }
10  
11 int main(void)
12 {
13     //申請主機內存,並進行初始化
14     int host_a[512],host_b[512],host_c[512];
15     for(int i=0;i<512;i++)
16     {
17         host_a[i]=i;
18         host_b[i]=i<<1;
19     }
20  
21     //定義cudaError,默認為cudaSuccess(0)
22     cudaError_t err = cudaSuccess;
23  
24     //申請GPU存儲空間
25     int *dev_a,*dev_b,*dev_c;
26     err=cudaMalloc((void **)&dev_a, sizeof(int)*512);
27     err=cudaMalloc((void **)&dev_b, sizeof(int)*512);
28     err=cudaMalloc((void **)&dev_c, sizeof(int)*512);
29     if(err!=cudaSuccess)
30     {
31         printf("the cudaMalloc on GPU is failed");
32         return 1;
33     }
34     printf("SUCCESS");
35     //將要計算的數據使用cudaMemcpy傳送到GPU
36     cudaMemcpy(dev_a,host_a,sizeof(host_a),cudaMemcpyHostToDevice);
37     cudaMemcpy(dev_b,host_b,sizeof(host_b),cudaMemcpyHostToDevice);
38  
39     //調用核函數在GPU上執行。數據較少,之使用一個Block,含有512個線程
40     add<<<1,512>>>(dev_a,dev_b,dev_c);
41     cudaMemcpy(&host_c,dev_c,sizeof(host_c),cudaMemcpyDeviceToHost);
42     for(int i=0;i<512;i++)
43         printf("host_a[%d] + host_b[%d] = %d + %d = %d\n",i,i,host_a[i],host_b[i],host_c[i]);
44     cudaFree(dev_a);//釋放GPU內存
45     cudaFree(dev_b);//釋放GPU內存
46     cudaFree(dev_c);//釋放GPU內存
47     return 0 ;
48 }

所幸的是CUDA已經將GPU的多線程優化封裝到了函數中,我們只需要直接調用NPP庫內的函數即可。

經過測試,使用GPU優化后其效率是Golang中graphics庫的10倍,是resize庫的5倍左右。

 

-------------------------------------------------------------------------------------------------------------------------------------

參考資料:

  1. https://www.ibm.com/developerworks/cn/linux/l-cn-jpeg/index.html
  2. https://blog.csdn.net/shelldon/article/details/54234433
  3. 《GPU高性能編程CUDA實戰》

說明:本文參考了眾多網絡資料整理而成,感謝各位大牛的付出! 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM