JPEG算法解密 JPEG原理詳解 (轉載 by jinchao)

(轉載,個人筆記)https://www.cnblogs.com/Arvin-JIN/p/9133745.html  (隨便誇一下CSDN的粘貼功能好強大)

圖片壓縮有多重要,可能很多人可能並沒有一個直觀上的認識,舉個例子,一張800X800大小的普通圖片,如果未經壓縮,大概在1.7MB左右,這個體積如果存放文本文件的話足夠保存一部92萬字的鴻篇鉅著《紅樓夢》,現如今互聯網上絕大部分圖片都使用了JPEG壓縮技術,也就是大家使用的jpg文件,通常JPEG文件相對於原始圖像,能夠得到1/8的壓縮比,如此高的壓縮率是如何做到的呢?
        JPEG能夠獲得如此高的壓縮比是因爲使用了有損壓縮技術,所謂有損壓縮,就是把原始數據中不重要的部分去掉,以便可以用更小的體積保存,這個原理其實很常見,比如485194.200000000001這個數,如果我們用485194.2來保存,就是一種“有損”的保存方法,因爲小數點後面的那個“0.000000000001”屬於不重要的部分,所以可以被忽略掉。JPEG整個壓縮過程基本上也是遵循這個步驟:
        1. 把數據分爲“重要部分”和“不重要部分”
        2. 濾掉不重要的部分
        3. 保存

步驟一:圖像分割


        JPEG算法的第一步,圖像被分割成大小爲8X8的小塊,這些小塊在整個壓縮過程中都是單獨被處理的。後面我們會以一張非常經典的圖爲例,這張圖片名字叫做Lenna,據說是世界上第一張JPG圖片,這張圖片自從誕生之日開始,就和圖像處理結下淵源,陪伴了無數理工宅男度過了的一個個不眠之夜,可謂功勳卓著,感興趣的朋友可以在這裏瞭解到這張圖片的故事。

 

步驟二:顏色空間轉換RGB->YCbCr


        所謂“顏色空間”,是指表達顏色的數學模型,比如我們常見的“RGB”模型,就是把顏色分解成紅綠藍三種分量,這樣一張圖片就可以分解成三張灰度圖,數學表達上,每一個8X8的圖案,可以表達成三個8X8的矩陣,其中的數值的範圍一般在[0,255]之間。

 

R >
G >
B >

        不同的顏色模型各有不同的應用場景,例如RGB模型適合於像顯示器這樣的自發光圖案,而在印刷行業,使用油墨打印,圖案的顏色是通過在反射光線時產生的,通常使用CMYK模型,而在JPEG壓縮算法中,需要把圖案轉換成爲YCbCr模型,這裏的Y表示亮度(Luminance),Cb和Cr分別表示綠色和紅色的“色差值”。
        “色差”這個概念起源於電視行業,最早的電視都是黑白的,那時候傳輸電視信號只需要傳輸亮度信號,也就是Y信號即可,彩色電視出現之後,人們在Y信號之外增加了兩條色差信號以傳輸顏色信息,這麼做的目的是爲了兼容黑白電視機,因爲黑白電視只需要處理信號中的Y信號即可。
        根據三基色原理,人們發現紅綠藍三種顏色所貢獻的亮度是不同的,綠色的“亮度”最大,藍色最暗,設紅色所貢獻的亮度的份額爲KR,藍色貢獻的份額爲KB,那麼亮度爲

(1.1)

        根據經驗,KR=0.299,KB=0.114,那麼

(1.2)

        藍色和紅色的色差的定義如下

(1.3)
(1.4)

        最終可以得到RGB轉換爲YCbCr的數學公式爲

(1.5)

        YCbCr模型廣泛應用在圖片和視頻的壓縮傳輸中,比如你可以留意一下電視或者DVD後面的接口,就可以發現色差接口。

        這是有道理的,還記得我們在文章開始時提到的有損壓縮的基本原理嗎?有損壓縮首先要做的事情就是“把重要的信息和不重要的信息分開”,YCbCr恰好能做到這一點。對於人眼來說,圖像中明暗的變化更容易被感知到,這是由於人眼的構造引起的。視網膜上有兩種感光細胞,能夠感知亮度變化的視杆細胞,以及能夠感知顏色的視錐細胞,由於視杆細胞在數量上遠大於視錐細胞,所以我們更容易感知到明暗細節。比如說下面這張圖

YY CbCb CrCr

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

 

步驟三:離散餘弦變換



        這次我們來介紹JPEG算法中的核心內容,離散餘弦變換(Discrete cosine transform),簡稱DCT。
        離散餘弦變換屬於傅里葉變換的另外一種形式,沒錯,就是大名鼎鼎的傅里葉變換。傅里葉是法國著名的數學家和物理學家,1807年,39歲的傅里葉在他的一篇論文裏提出了一個想法,他認爲任何週期性的函數,都可以分解爲爲一系列的三角函數的組合,這個想法一開始並沒有得到當時科學界的承認,比如當時著名的數學家拉格朗日提出質疑,三角函數無論如何組合,都無法表達帶有“尖角”的函數,一直到1822年拉格朗日死後,傅里葉的想法才正式在他的著作《熱的解析理論》一書中正式發表。
        金子總會閃光,傅里葉變換如今廣泛應用於數學、物理、信號處理等等領域,變換除了它在數學上的意義外,還有其哲學上的偉大意義,那就是,世上任何複雜的事物,都可以分解爲簡單的事物的組合,而這個過程只需要藉助數學工具就可以了。但是當年拉格朗日的質疑是正確的,三角函數的確無法表達出尖角形狀的函數,不過只要三角函數足夠多,可以無限逼近最終結果。比如下面這張動圖,就動態描述了一個矩形方波,是如何做傅里葉分析的。

 

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

(2.1)

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

(2.2)

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

(2.3)


        舉個例子,我們有一個長度爲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,根據公式2.3把這個數組轉換爲8個新的數組的和,如果我們使用圖像來表達的話,就可以發現DCT轉換的有趣之處了

[50,55,67,80,-10,-5,20,30]

   
  數組0
    [35.9,35.9,35.9,35.9,35.9,35.9,35.9,35.9]
  數組1
    [26.0,22.1,14.8,5.2,-5.2,-14.8,-22.1,-26.1]
  數組2
    [3.3,1.4,-1.4,-3.3,-3.3,-1.4,1.4,3.3]
  數組3
    [-23.0,5.4,27.2,15.4,-15.4,-27.2,-5.4,23.0]
  數組4
    [1.6,-1.6,-1.6,1.6,1.6,-1.6,-1.6,1.6]
  數組5
    [9.1,-16.1,3.2,13.6,-13.6,-3.2,16.1,-9.1]
  數組6
    [-0.8,1.9,-1.9,0.8,0.8,-1.9,1.9,-0.8]
  數組7
    [-2.1,6.1,-9.1,10.8,-10.8,9.1,-6.1,2.1]

        奧妙之處在於,經過DCT,數據中隱藏的規律被髮掘了出來,雜亂的數據被轉換成幾個工整變化的數據。DCT轉換後的數組中第一個是一個直線數據,因此又被稱爲“直流數據”,簡稱DC,後面的數據被稱爲“交流數據”,簡稱AC,這個稱呼起源於信號分析中的術語。
        在JPEG壓縮過程中,經過顏色空間的轉換,每一個8X8的圖像塊,在數據上表現爲3個8X8的矩陣,緊接着我們對這三個矩陣做一個二維的DCT轉換,二維的DCT轉換公式爲

(2.1)

        DCT的威力究竟有多大,我們可以做一個實際的測試,比如一個所有數值都一樣的矩陣,經過DCT轉換後,將所有級數組合成一個新的矩陣

 

        可以看到,經過DCT轉換,矩陣的“能量”被全部集中在左上角上的直流分量F(0,0)上,其他位置都變成了0。
        在實際的JPEG壓縮過程中,由於圖像本身的連貫性,一個8X8的圖像中的數值一般不會出現大的跳躍,經過DCT轉換會有類似的效果,左上角的直流分量保存了一個大的數值,其他分量都接近於0,我們以Lenna左上角第一塊圖像的Y分量爲例,經過變換的矩陣爲

 

        可以看到,數據經過DCT變化後,被明顯分成了直流分量和交流分量兩部分,爲後面的進一步壓縮起到了充分的鋪墊作用,可以說是整個JPEG中最重要的一步,後面我們會介紹數據量化。

 

步驟四:數據量化


        經過上一節介紹的離散餘弦變換,圖像數據雖然已經面目全非,但仍然是處於“可逆”的狀態,也就是說我們還沒有進入“有損”的那一步。這次我們來玩真的,看一下數據中的細節是如何被濾去的。先來考察一下要對付的問題是什麼,經過顏色空間轉換和離散餘弦變換,每一個8X8的圖像塊都變成了三個8X8的浮點數矩陣,分別表示Y,Cr,Cb數據,比如以其中某個亮度數據矩陣舉例,它的數據如下

        我們的問題是,在可以損失一部分精度的情況下,如何用更少的空間存儲這些浮點數?答案是使用量子化(Quantization),簡稱量化。“量子”這個概念來自於物理學,意思是說連續的能量可以看做是一個個單元體的組合,看起來高端大氣,其實很簡單,比如遊戲中在處理角色面朝方向時,一般並不是使用0到2π這樣的浮點數,而是把方向分成16個區間,用0到16這樣的整數來表示,這樣只用4個bit就足夠了。JPEG提供的量子化算法如下:

 

(3.1)

        其中G是我們需要處理的圖像矩陣,Q稱作量化係數矩陣(Quantization matrices),JPEG算法提供了兩張標準的量化係數矩陣,分別用於處理亮度數據Y和色差數據Cr以及Cb。

標準亮度量化表

標準亮度量化表

標準色差量化表

標準色差量化表


        其中round函數是取整函數,但考慮到了四捨五入,也就是說

(3.2)

        比如上面數據,以左上角的-415.38爲例,對應的量子化係數是16,那麼round(-415.38/16)=round(-25.96125)=-26。最終得到的量子化後的結果爲

        可以看到,一大部分數據變成了0,這非常有利於後面的壓縮存儲。這兩張神奇的量化表也是有講究的,還記得我們在第一節中所講的有損壓縮的基本原理嗎,有損壓縮就是把數據中重要的數據和不重要的數據分開,然後分別處理。DCT係數矩陣中的不同位置的值代表了圖像數據中不同頻率的分量,這兩張表中的數據時人們根據人眼對不不同頻率的敏感程度的差別所積累下的經驗制定的,一般來說人眼對於低頻的分量必高頻分量更加敏感,所以兩張量化係數矩陣左上角的數值明顯小於右下角區域。在實際的壓縮過程中,還可以根據需要在這些係數的基礎上再乘以一個係數,以使更多或更少的數據變成0,我們平時使用的圖像處理軟件在生成jpg文件時,在控制壓縮質量的時候,就是控制的這個係數。
        在進入下一節之前,矩陣的量化還有最後一步要做,就是把量化後的二維矩陣轉變成一個一維數組,以方便後面的霍夫曼壓縮,但在做這個順序轉換時,需要按照一個特定的取值順序。

        這麼做的目的只有一個,就是儘可能把0放在一起,由於0大部分集中在右下角,所以纔去這種由左上角到右下角的順序,經過這種順序變換,最終矩陣變成一個整數數組

-26,-3,0,-3,-2,-6,2,-4,1,-3,0,1,5,,1,2,-1,1,-1,2,0,0,0,0,0,-1,-1,0,0,0,0,…,0,0


        後面的工作就是對這個數組進行再一次的哈夫曼壓縮,已得到最終的壓縮數據。

 

步驟五:哈弗曼編碼


        JPEG壓縮的最後一步是對數據進行哈弗曼編碼(Huffman coding),哈弗曼幾乎是所有壓縮算法的基礎,它的基本原理是根據數據中元素的使用頻率,調整元素的編碼長度,以得到更高的壓縮比。
        舉個例子,比如下面這段數據

 

“AABCBABBCDBBDDBAABDBBDABBBBDDEDBD”

        這段數據裏面包含了33個字符,每種字符出現的次數統計如下

字符 A B C D E
次數 6 15 2 9 1

        如果我們用我們常見的定長編碼,每個字符都是3個bit。

字符 A B C D E
編碼 001 010 011 100 101

        那麼這段文字共需要3*33 = 99個bit來保存,但如果我們根據字符出現的概率,使用如下的編碼

字符 A B C D E
編碼 110 0 1110 10 1111

        那麼這段文字共需要3*6 + 1*15 + 4*2 + 2*9 + 4*1 = 63個bit來保存,壓縮比爲63%,哈弗曼編碼一般都是使用二叉樹來生成的,這樣得到的編碼符合前綴規則,也就是較短的編碼不能夠是較長編碼的前綴,比如上面這個編碼,就是由下面的這顆二叉樹生成的。

        我們回到JPEG壓縮上,回顧上一節的內容,經過數據量化,我們現在要處理的數據是一串一維數組,舉例如下:

①原始數據

35,7,0,0,0,-6,-2,0,0,-9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,…,0

        在實際的壓縮過程中,數據中的0出現的概率非常高,所以首先要做的事情,是對其中的0進行處理,把數據中的非零的數據,以及數據前面0的個數作爲一個處理單元。

①原始數據

35,7,0,0,0,-6,-2,0,0,-9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,…,0

②RLE編碼 35 7 0,0,0,-6 -2 0,0,-9 0,0,…,0,8 0,0,…,0

        如果其中某個單元的0的個數超過16,則需要分成每16個一組,如果最後一個單元全都是0,則使用特殊字符“EOB”表示,EOB意思就是“後面的數據全都是0”,

①原始數據

35,7,0,0,0,-6,-2,0,0,-9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,…,0

②RLE編碼 35 7 0,0,0,-6 -2 0,0,-9 0,0,…,0,8 0,0,…,0
35 7 0,0,0,-6 -2 0,0,-9 0,0,…,0 0,0,8 0,0,…,0
(0,35) (0,7) (3,-6) (0,-2) (2,-9) (15,0) (2,8) EOB

        其中(15,0)表示16個0,接下來我們要處理的是括號裏右面的數字,這個數字的取值範圍在-2047~2047之間,JPEG提供了一張標準的碼錶用於對這些數字編碼:

Value Size Bits
0 0
-1 1 1 0 1
-3,-2 2,3 2 00,01 10,11
-7,-6,-5,-4 4,5,6,7 3 000,001,010,011 100,101,110,111
-15,…,-8 8,…,15 4 0000,…,0111 1000,…,1111
-31,…,-16 16,…,31 5 0 0000,…,0 1111 1 0000,…,1 1111
-63,…,-32 32,…,63 6 00 0000,… …,11 1111
-127,…,-64 64,…,127 7 000 0000,… …,111 1111
-255,…,-128 128,…,255 8 0000 0000,… …,1111 1111
-511,…,-256 256,…,511 9 0 0000 0000,… …,1 1111 1111
-1023,…,-512 512,…,1023 10 00 0000 0000,… …,11 1111 1111
-2047,…,-1024 1024,…,2047 11 000 0000 0000,… …,111 1111 1111

        舉例來說,第一個單元中的“35”這個數字,在表中的位置是長度爲6的那組,所對應的bit碼是“100011”,而“-6”的編碼是”001″,由於這種編碼附帶長度信息,所以我們的數據變成了如下的格式。

①原始數據

35,7,0,0,0,-6,-2,0,0,-9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,…,0

②RLE編碼 35 7 0,0,0,-6 -2 0,0,-9 0,0,…,0,8 0,0,…,0
35 7 0,0,0,-6 -2 0,0,-9 0,0,…,0 0,0,8 0,0,…,0
(0,35) (0,7) (3,-6) (0,-2) (2,-9) (15,0) (2,8) EOB
③BIT編碼 (0,6, 100011) (0,3, 111) (3,3, 001) (0,2, 01) (2,4, 0110) (15,-) (2,4, 1000) EOB

        括號中前兩個數字分都在0~15之間,所以這兩個數可以合併成一個byte,高四位是前面0的個數,後四位是後面數字的位數。

①原始數據

35,7,0,0,0,-6,-2,0,0,-9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,…,0

②RLE編碼 35 7 0,0,0,-6 -2 0,0,-9 0,0,…,0,8 0,0,…,0
35 7 0,0,0,-6 -2 0,0,-9 0,0,…,0 0,0,8 0,0,…,0
(0,35) (0,7) (3,-6) (0,-2) (2,-9) (15,0) (2,8) EOB
③BIT編碼 (0,6, 100011) (0,3, 111) (3,3, 001) (0,2, 01) (2,4, 0110) (15,-) (2,4, 1000) EOB
(0x6,100011) (0x3,111) (0x33,001) (0x2,01) (0x24,0110) (0xF0,-) (0x24,1000) EOB

        對於括號前面的數字的編碼,就要使用到我們提到的哈弗曼編碼了,比如下面這張表,就是一張針對數據中的第一個單元,也就是直流(DC)部分的哈弗曼表,由於直流部分沒有前置的0,所以取值範圍在0~15之間。

Length Value Bits
3 bits 04
05
03
02
06
01
00 (EOB)
000
001
010
011
100
101
110
4 bits 07 1110
5 bits 08 1111 0
6 bits 09 1111 10
7 bits 0A 1111 110
8 bits 0B 1111 1110

        舉例來說,示例中的DC部分的數據是0x06,對應的二進制編碼是“100”,而對於後面的交流部分,取值範圍在0~255之間,所以對應的哈弗曼表會更大一些

Length Value Bits
2 bits 01
02
00
01
3 bits 03 100
4 bits 00 (EOB)
04
11
1010
1011
1100
5 bits 05
12
21
1101 0
1101 1
1110 0
6 bits 31
41
1110 10
1110 11
12 bits 24
33
62
72
1111 1111 0100
1111 1111 0101
1111 1111 0110
1111 1111 0111
15 bits 82 1111 1111 1000 000
16 bits 09

FA
1111 1111 1000 0010

1111 1111 1111 1110

        這樣經過哈弗曼編碼,並且序列化後,最終數據成爲如下形式

①原始數據

35,7,0,0,0,-6,-2,0,0,-9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,…,0

②RLE編碼 35 7 0,0,0,-6 -2 0,0,-9 0,0,…,0,8 0,0,…,0
35 7 0,0,0,-6 -2 0,0,-9 0,0,…,0 0,0,8 0,0,…,0
(0,35) (0,7) (3,-6) (0,-2) (2,-9) (15,0) (2,8) EOB
③BIT編碼 (0,6, 100011) (0,3, 111) (3,3, 001) (0,2, 01) (2,4, 0110) (15,-) (2,4, 1000) EOB
(0x6,100011) (0x3,111) (0x33,001) (0x2,01) (0x24,0110) 0xF0 (0x24,1000) EOB
④哈弗曼編碼 100 100011 100 111 1111 1111 0101 001 01 01 1111 1111 0100 0110 1111 1111 001 1111 1111 0100 1000 1010
⑤序列化

100100011100111111111110101001010111111111010001101111111100111111111010010001010

91 CF FE A5 7F D1 BF CF FA 45

        最終我們使用了10個字節的空間保存了原本長度爲64的數組,至此JPEG的主要壓縮算法結束,這些數據就是保存在jpg文件中的最終數據。

 

        這個系列的最後,我提供給大家一個簡易的jpeg壓縮算法的代碼,這份代碼用C++編寫,以開源方式提供,放在了github上,可以到下面這個網址下載

http://github.com/thejinchao/jpeg_encoder

        使用方法很簡單,像下面這樣就可以了

#include "jpeg_encoder.h"
 
JpegEncoder encoder;
//輸入的文件必須是24bit的bmp文件,尺寸必須是8的倍數
encoder.readFromBMP(inputFileName);
 
//第二個參數在1~199之間,代表文件壓縮程度,數字越大,壓縮後的文件體積越小
encoder.encodeToJPG(outputFileName, 50);

        這份代碼只是爲了配合這個系列的文章,所以沒有考慮優化,如果你想在實際工程中使用jpeg的壓縮算法,還是使用被廣泛應用的libjpeg或者OpenJpeg。甚至是PitBit.CN圖片在線壓縮

 

轉載自:

https://thecodeway.com/blog/?p=69

https://thecodeway.com/blog/?p=353

https://thecodeway.com/blog/?p=480

https://thecodeway.com/blog/?p=522

https://thecodeway.com/blog/?p=690

 原作者: jinchao  

 外文文章 :  How JPG Works   :    https://www.freecodecamp.org/news/how-jpg-works-a4dbd2316f35/#.y8yys5lh0

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章