最近在看GitHub上的一個很火的項目是:ImageSharp。這是一個純.net core的圖像處理庫,沒有使用其他的任何依賴。在看這個項目過程中激發了我對圖像文件編碼解碼的興趣。於是從最簡單的BMP圖像開始看,到GIF格式卡了一段時間(主要卡在lzw編碼過程和數據塊中),到最后的JPEG格式(PNG格式不打算看了),經歷了半個月時間才梳理出個大概。趁着這個熱乎勁,我想寫下關於JPEG格式的系列文章,文章目錄暫定如下:
ImageSharp源碼詳解之JPEG編碼原理(1)JPEG介紹
ImageSharp源碼詳解之JPEG壓縮原理(3)DCT變換
ImageSharp源碼詳解之JPEG壓縮原理(4)量化
ImageSharp源碼詳解之JPEG壓縮原理(6)C#源碼解析及調試技巧
1.JPEG介紹
JPEG(Joint Photographic Experts Group)是聯合圖像專家小組的英文縮寫。它由國際電話與電報咨詢委員會CCITT(The International Telegraph and Telephone Consultative Committee)與國際標准化組織ISO於1986年聯合成立的一個小組,負責制定靜態數字圖像的編碼標准。
小組一直致力於標准化工作,開發研制出連續色調、多級灰度、靜止圖像的數字圖像壓縮編碼方法,即JPEG算法。JPEG算法被確定為國際通用標准,其適用范圍廣泛,除用於靜態圖像編碼外,還推廣到電視圖像序列的幀內圖像壓縮。而用JPEG算法壓縮出來的靜態圖片文件稱為JPEG文件,擴展名通常為*.jpg、*.jpe*.jpeg。
JPEG專家組開發了兩種基本的壓縮算法、兩種數據編碼方法、四種編碼模式。具體如下:
壓縮算法:
1有損的離散余弦變換(Discrete Cosine Transform,DCT);
2 無損的預測技術壓縮。
數據編碼方法:
1哈夫曼編碼;
2算術編碼;
編碼模式:
1基於DCT順序模式:編/解碼通過一次掃描完成;
2基於DCT遞進模式:編/解碼需要多次掃描完成,掃描效果從粗糙到精細,逐級遞進;
3無損模式:基於DPCM,保證解碼后完全精確恢復到原圖像采樣值;
4層次模式:圖像在多個空間多種分辨率進行編碼,可以根據需要只對低分辨率數據作解碼,放棄高分辨率信息。
在我閱讀的源碼中,我關注的是離散余弦變換、哈夫曼編碼、基於DCT順序模式的編碼,這也是JPRG圖像常用的技術。
整體文件的大致結構如下:
SOI(0xFFD8)
APP0(0xFFE0)
[APPn(0xFFEn)]可選
DQT(0xFFDB)
SOF0(0xFFC0)
DHT(0xFFC4)
SOS(0xFFDA)
壓縮數據
EOI(0xFFD9)
我們解碼的時候大致都是按照上面順序進行解碼,關於上面這些標記,大家可以從文章結尾參考資料中看到他們的詳細信息,這里我不對這些標記展開描述,在后面用到的時候會提到。
2.JPEG壓縮的大致過程
2.1 編碼
對於一副圖像,編碼器首先需要填充這個圖像的一些頭信息,量化表,霍夫曼表。我們可以看ImageSharp中JpegEncoderCore這個類里面的的Encode方法如下:

1 ... 2 // Write the Start Of Image marker. 3 this.WriteApplicationHeader(metadata); 4 // Write Exif and ICC profiles 5 this.WriteProfiles(metadata); 6 // Write the quantization tables. 7 this.WriteDefineQuantizationTables(); 8 // Write the image dimensions. 9 this.WriteStartOfFrame(image.Width, image.Height, componentCount); 10 // Write the Huffman tables. 11 this.WriteDefineHuffmanTables(componentCount); 12 // Write the image data. 13 this.WriteStartOfScan(image); 14 // Write the End Of Image marker. 15 this.buffer[0] = JpegConstants.Markers.XFF; 16 this.buffer[1] = JpegConstants.Markers.EOI; 17 stream.Write(this.buffer, 0, 2); 18 stream.Flush(); 19 }
這一系列都是圍繞着WriteStartOfScan這個方法展開,這個方法就是對於圖像數據進行編碼,過程如下圖:
注意我們看到過程中YUV采樣后面是DCT變換、量化、熵編碼。實際過程中,YUV采樣過程中就包含后面DCT變換、量化、熵編碼這三個過程,只不過我們在描述的時候將其分開。在ImageSharp源碼中我們可以看到它使用的是標准霍夫曼表來進行編碼,這也是一般JPEG編碼器常用的方法,但這樣就和常規的霍夫曼編碼不一致,我們可以通過其他資料學習到霍夫曼編碼需要對原始數據遍歷兩次,一次構建霍夫曼樹,一次進行編碼。我在谷歌的guetzli項目中看到了針對不同圖像數據,構建不同霍夫曼樹結構的方法。
2.2 解碼
作為編碼的互逆過程,大致流程如下:
雖然我們了解了如何編碼,就能大致知道如何解碼,但是ImageSharp源碼中,對於解碼和編碼在代碼實現還是有區別的,后續我只分析JPEG的編碼過程。
3.后續
后面計划是先把量化和熵編碼的相關文章寫完,然后把imagesharp這個項目中調試技巧做一下分析解讀,最后再寫DCT變換。DCT涉及到傅里葉變換,如果要深入理解,其背后復雜的數學知識不是我所能講解的,在這一章盡量講解代碼技巧,數學公式不敢牽涉太多。
參考文獻:
學習JPEG編碼需要參考大量的資料,這些資料側重點都不一樣,需要相互印證。對我來說看代碼比看文獻更快,我的方法就是通過調試代碼來驗證我的猜想,下面是我看過的一些資料,后面文章不在贅述:
1.itu-t81.pdf JPEG文件格式,標准的霍夫曼編碼參考的是這個資料,在ImageSharp源碼中可以下載獲取。
2.impulseadventure 這個網站里面可以找到一個叫JPEGsnoop的開源軟件,可以分析JPEG各個標記的具體數值,同時這個網站里面很多教程都是關於JPEG的編碼和解碼,我文章里面一些圖片都來自上面。
3.JPEG壓縮原理,一位大牛的博客,值得一看。
4.FluxJpeg 這個源碼只針對JPEG格式進行編碼與解析,所以看起來相對ImageSharp簡單很多,我前期就是先對照資料看的這個源碼,后面換成ImageSharp。
5.數據壓縮導論(第4版) 這本書很好,理論講解的很深入,例子舉的也很容易看懂。
6.實用數字信號處理-從原理到應用 這本書關於DFT的講解很深入,值得一看。