jpeg格式說明與解碼學習
本文更加註重JPEG格式的具體解碼實現,並不涉及編碼實現(比如DCT、熵編碼之類的,這些在很多書中都有詳細的介紹,我就不贅述了)
參考資料
中文資料
- JPEG文件格式JFIF&Exif:很好的對JPEG格式的整體解釋,表格很清晰
- jpg格式舉例詳解:同樣有對JPEG各個marker的解釋,十分全面,甚至還有例子。
- JPEG文件編/解碼詳解:同樣有着很詳盡的說明,理論上十分深刻。
- jpeg圖像密寫研究:四篇文章,似乎不錯的樣子
- csdn下載:Jpeg官方文檔下載:JPEG官方文檔,雖然我沒看
英文資料
- StackOverflow: How to write jpeg file decoder:StackOverflow上的問題,幫助很大。
- jpeg deocder written in python:上面題目中一位答主用python寫的jpeg解碼器
- JPEG decoder written in C:普林斯頓大佬的JPEG解碼器
- JPEGsnoop:github上的開源軟件,可以探測分析JPEG上各個分量的具體數值。
- JPEG Huffman Coding Tutorial:上面那款軟件作者的文章,很詳細的介紹了哈夫曼編碼的過程與手動解碼的例子。
- JPEG encoder in Python:python寫的JPEG編碼器與解碼器
格式介紹
以下內容翻譯自英文維基百科,我覺得挺不錯的。
JPEG由segment組成,segment的開頭是marker;marker由0xFF開頭,後一個決定了marker的類型。marker後會跟兩個字節的的長度,表示其後的數據量,有時會用連續的FF進行填充。
概念釋義
不保證正確,只是個人的理解
- JPG與JPEG:一樣的意思,都是表示Joint Photographic Experts Group開發的這套JPEG標準所描述的圖片,只是早期DOS系統只支持3位擴展名,而Apple支持多位擴展名纔有所區別。後面即使Windows同樣支持多位擴展名也沒有改變這個習慣。因爲Windows用戶更多,所以主流爲jpg,但是像是.jpeg、.JPG和.JPEG都是可以的。
- JPG與JFIF:早期的JPEG格式只說明了圖片如何壓縮爲字節流以及重新解碼爲圖片的過程,但是沒有說明這些字節是如何在任何特定的存儲媒體上封存起來的。因此建立了相關的額外標準JFIF(JPEG File Interchange Format)。後來這個標準變爲主流,現在所使用的JPEG文件基本都是符合JFIF標準的。
關於0xFF
從上面這句話可以看出,0xFF在JPEG文件中是十分重要。如果讀取中出現了0xFF,根據後面的值有多種可能。
- 後面的值爲0xFF,這時候視爲一個0xFF看待,繼續讀取。
- 後面的值爲0x00,這時候表示這個FF在數據流中,跳過。
- 其他能夠表示marker的值,將其視爲marker開頭處理。
- 其他值,跳過。
整體格式
JPEG格式的大致順序爲:
- SOI
- APP0
- [APPn]可選
- DQT
- SOF0
- DHT
- SOS
- 壓縮數據
- EOI
JPEG中SOI和EOI中間爲Frame
Frame的頭包含了像素的位數,圖像的寬和高等信息。Frame下有Scan
Scan的頭包含每個掃描的分量數,分量ID,哈夫曼表等。Scan下有Segment和Restart,即壓縮數據的基本單位。
注:在JPEG文件格式中使用Motorola格式而不是Intel格式,也就是說大端模式,高字節低地址,低字節高地址。
標籤表
總表
縮寫 | 字節碼 | 名稱 | 註釋 |
---|---|---|---|
SOI | 0xFFD8 | Start of image | 文件開頭 |
SOF0 | 0xFFC0 | Start of Frame0 | Baseline DCT-based JPEG所用的開頭 |
SOF2 | 0xFFC2 | Start of Frame2 | Progressive DCT-based JPEG |
DHT | 0xFFC4 | Define Huffman Tables | 指定一個或多個哈夫曼表 |
DQT | 0xFFDB | Define Quantization Table | 指定量化表 |
DRI | 0xFFDD | Define Restart Interval | RST中的marker |
SOS | 0xFFDA | Start of Scan | Scan的開頭 |
RSTn | 0xFFDn | Restart | DRImarker中插入r個塊 |
APPn | 0xFFEn | Application-specific | Exif JPEG使用APP1,JFIF JPEG使用APP0 |
COM | 0xFFFE | Comment | 註釋內容 |
EOI | 0xFFD9 | End of Image | 圖像的結束 |
表2 JPEG Start of Frame結構
字段名稱 | 長度 | 註釋 |
---|---|---|
標記代碼 | 2 bytes | 固定值0xFFC0 |
數據長度 | 2 bytes | SOF marker的長度,包含自身但不包含標記代碼 |
精度 | 1 byte | 每個樣本數據的位數,通常是8位 |
圖像高度 | 2 bytes | 圖像高度,單位是像素 |
圖像寬度 | 2 bytes | 圖像寬度,單位是像素 |
顏色分量數 | 1 bytes | 灰度級1,YCbCr或YIQ是3,CMYK是4 |
顏色分量信息 | 顏色分量數*3 | 每個顏色分量:1 byte分量ID; 1byte水平垂直採樣因子(前4位爲水平採樣因子,後4位爲垂直採樣因子); 1byte當前分量使用的量化表ID |
表3 JPEG Start of Scan結構
字段名稱 | 長度 | 註釋 |
---|---|---|
標記代碼 | 2 bytes | 固定值0xFFDA |
數據長度 | 2 bytes | SOS marker的長度,包含自身但不包含標記代碼 |
顏色分量數 | 1 bytes | 灰度級1,YCbCr或YIQ是3,CMYK是4 |
顏色分量信息 | 顏色分量數*3 | 1byte的顏色分量id,1byte的直流/交流係數表號(高4位:直流分量所使用的哈夫曼樹編號,低4位:交流分量使用的哈夫曼樹的編號) |
壓縮圖像信息 | 3 bytes | |
1 byte | 譜選擇開始 固定爲0x00 | |
1 byte | 譜選擇結束 固定爲0x3f | |
1 byte | 譜選擇 在basic JPEG中固定爲00 |
注:SOS緊跟着就是壓縮圖像信息
表4 JPEG APP0 應用保留標記0
字段名稱 | 長度 | 註釋 |
---|---|---|
標記代碼 | 2 bytes | 固定值0xFFE0 |
數據長度 | 2 bytes | APP0的長度,包含自身但不包含標記代碼 |
標識符 identifier | 5 bytes | 固定的字符串"JFIF\0" |
版本號 | 2 bytes | 一般爲0x0101或0x0102,表示1.1或1.2 |
像素單位 unit | 1 byte | 座標單位,0爲沒有單位; 1 pixel/inch; 2pixel/inch |
水平像素數目 | 2 bytes | |
垂直像素數目 | 2 bytes | |
縮略圖水平像素數目 | 1 byte | 如果爲0則沒有縮略圖 |
縮略圖垂直像素數目 | 1 byte | 同上 |
縮略圖RGB位圖 | 3n bytes | n = 縮略圖水平像素數目*縮略圖垂直像素數目,這是一個24bits/pixel的RGB位圖 |
表5 APPn應用程序保留標記
字段名稱 | 長度 | 註釋 |
---|---|---|
標記代碼 | 2 bytes | 固定值0xFFE1-0xFFEF,n=1~15 |
數據長度 | 2 bytes | APPn的長度,包含自身但不包含標記代碼 |
詳細信息 | (length-2) bytes | 內容是應用特定的,比如Exif使用APP1來存放圖片的metadata,Adobe Photoshop用APP1和APP13兩個標記段分別存儲了一副圖像的副本。 |
表6 DQT 定義量化表
字段名稱 | 長度 | 註釋 |
---|---|---|
標記代碼 | 2 bytes | 固定值0xFFDB |
數據長度 | 2 bytes | DQT的長度,包含自身但不包含標記代碼 |
量化表 | (length-2)bytes | 下面爲子字段 |
精度及量化表ID | 1 byte | 高4位爲精度,只有兩個可選值:0表示8bits,1表示16bits;低4位爲量化表ID,取值範圍爲0~3 |
表項 | 64*(精度+1)bytes |
表7 DHT 定義哈夫曼表
字段名稱 | 長度 | 註釋 |
---|---|---|
標記代碼 | 2 bytes | 固定值0xFFC4 |
數據長度 | 2 bytes | DHT的長度,包含自身但不包含標記代碼 |
哈夫曼表 | (length-2)bytes | 以下爲哈夫曼表子字段 |
表ID和表類型 | 1 byte | 高4位:類型,只有兩個值可選,0爲DC直流,1爲AC交流;低4位:哈夫曼表ID,注意DC表和AC表是分開編碼的 |
不同位數的碼字數量 | 16字節 | |
編碼內容 | 上述16個不同位數的碼字的數量和 |
注:哈夫曼表可以重複出現(一般出現4次)
其他詳細內容可以看這篇文章,這是我在這一部分看到的比較全面的中文資料。
解碼
因爲時間原因,後面寫的不是很好,建議閱讀其他參考文獻。
哈夫曼表解碼
學習自這篇文章,裏面的例子寫的很好。
還有這篇英文文章,作者居然自己還寫了個分析器,十分厲害。
如上文表7所述,對於單一的哈夫曼表應該有三個部分:
- 哈夫曼表ID和類型:長度爲1byte。這個自己的值一般只有四個,0x00、0x01、0x10、0x11。其中高4位表示直流/交流,低4位表示id。
- 不同位數的碼字數量,JPEG文件的哈夫曼編碼只能是116位。這個字段的16個字節分別表示116位的編碼碼字在哈夫曼樹中的個數
- 編碼內容:這個字段記錄了哈夫曼樹上各個葉子結點的權重。因此,上一字段的16個數值之和就應該是本字段的長度,也就是哈夫曼樹中葉子節點的個數。
4個哈夫曼表中分別代表光學的DC編碼和AC編碼(Y)以及色彩的DC編碼和AC編碼(Cb&Cr)
哈夫曼表2條目中的16個數表示對應位數的編碼個數,比如第一個數表示哈夫曼編碼長度爲1的編碼個數,以此類推。
然後可以根據哈夫曼編碼中的編碼進行解碼:
1)第一個碼字必定爲0。
如果第一個碼字位數爲1,則碼字爲0;
如果第一個碼字位數爲2,則碼字爲00;
如此類推。
2)從第二個碼字開始,
如果它和它前面的碼字位數相同,則當前碼字爲它前面的碼字加1;
如果它的位數比它前面的碼字位數大,則當前碼字是前面的碼字加1後再在後邊添若干個0,直至滿足位數長度爲止。
編碼
因爲我寫的編碼器始終不能正常工作,我也不知道下面有哪些地方有錯誤,請注意。
二次採樣
一般JPEG都會使用4:2:0(或者稱作4:1:1),即每4個像素(2*2)的像素,Y值全留,從第一行掃描一個Cb值,從第二行掃描一個Cr值。對於一個16*16的塊,應該能夠產生4個Y塊,1個Cb塊和1個Cr塊,在實際編碼存放的時候按照[Y00 Y01 Y10 Y11 Cb Cr]的順序進行存放。
同時MCU的掃描順序爲從上到下,從左到右。
DC編碼
根據DCT變換,每個塊都會得到一個DC分量。對於每個顏色空間的DC分量,計算差值,使得過去的值可以用現在的值加上之前的累加和來表示。
對於得到的這麼一個差分序列,將它變爲(size,amplitude)形式,其中amplitude的計算方式是:正數直接轉換爲二進制數,負數直接取反碼。size表示amplitude的位數。再對每個size進行哈夫曼編碼來壓縮,就得到了最後的二進制流。
AC編碼
每個塊有63個AC分量。對這些AC分量按照zigzag順序進行遊長編碼(runlength,value),意義爲跨越多少個0串之後到達怎樣的值。因爲後面編碼的原因,runlength不能大於15。對於得到的遊長編碼,對其value進行DPCM編碼,然後將runlength作爲4位二進制數與DPCM的size一同合併爲8位二進制數進行編碼,amplitude單獨編碼,合併成一個二進制流。
哈夫曼編碼
對於得到的4個哈夫曼樹進行編碼。編碼時按照哈夫曼編碼字典序排列,位數短的在前,統計位數,然後寫下前16位數字。
接着寫下所有的哈夫曼編碼對應的值,編碼結束。
這裏有幾個問題:
- 生成的Huffman編碼如果大於16位的話就不在範圍之內了,這個是有可能的,這個問題提到了這種情況。裏面的答主提到實際中後面會留有一定的空間,同時大多數JPEG編碼器使用的是基於統計學制作的標準的哈夫曼編碼表。(注:普通的jpeg不會遇到這種情況,因爲它們編碼的對象爲size,一般不會超過16位,更多情況下不會多於10個點)
- 從解碼的角度考慮,存儲的時候哈夫曼編碼條目不一定能夠還原回去。這意味着應該要對生成的哈夫曼樹進行處理,保證三個特點:a.下一長度的哈夫曼樹必須能夠爲上一長度的哈夫曼樹加1補0後得到。
然而絕大多數解碼器都會使用標準的哈夫曼編碼,下面是我所寫的標準哈夫曼樹
#接受值和長度
def int2Bit(value,length):
return bin(value)[2:].zfill(length)
#接受十六進制字符串,轉換成長度*4的二進制字符串
def Hex2Bit(heximal):
bit = ''
for hex in heximal:
bit += int2Bit(int(hex,16),4)
return bit
def getStd(std_dict):
huffman = {}
baseline = 0
for (length,huffman_list) in std_dict.items():
for hexNum in huffman_list:
#基準對齊
huffmanStr = bin(baseline)[2:].zfill(length)
#填充
key = Hex2Bit(hexNum)
huffman[key] = huffmanStr
#進入下一個
baseline += 1
baseline <<=1
return huffman
def std_DC_LU():
std_dc_lu = {}
std_dc_lu[2] = ['00']
std_dc_lu[3] = ['01','02','03','04','05']
std_dc_lu[4] = ['06']
std_dc_lu[5] = ['07']
std_dc_lu[6] = ['08']
std_dc_lu[7] = ['09']
std_dc_lu[8] = ['0A']
std_dc_lu[9] = ['0B']
return getStd(std_dc_lu)
def get_dc_lu_dict():
std_dc_lu = {}
std_dc_lu[2] = ['00']
std_dc_lu[3] = ['01','02','03','04','05']
std_dc_lu[4] = ['06']
std_dc_lu[5] = ['07']
std_dc_lu[6] = ['08']
std_dc_lu[7] = ['09']
std_dc_lu[8] = ['0A']
std_dc_lu[9] = ['0B']
return std_dc_lu
def std_DC_CO():
std_dc_co = {}
std_dc_co[2] = ['00','01','02']
std_dc_co[3] = ['03']
std_dc_co[4] = ['04']
std_dc_co[5] = ['05']
std_dc_co[6] = ['06']
std_dc_co[7] = ['07']
std_dc_co[8] = ['08']
std_dc_co[9] = ['09']
std_dc_co[10] = ['0A']
std_dc_co[11] = ['0B']
return getStd(std_dc_co)
def get_dc_co_dict():
std_dc_co = {}
std_dc_co[2] = ['00','01','02']
std_dc_co[3] = ['03']
std_dc_co[4] = ['04']
std_dc_co[5] = ['05']
std_dc_co[6] = ['06']
std_dc_co[7] = ['07']
std_dc_co[8] = ['08']
std_dc_co[9] = ['09']
std_dc_co[10] = ['0A']
std_dc_co[11] = ['0B']
return std_dc_co
def std_AC_LU():
std_ac_lu = {}
std_ac_lu[2] = ['01','02']
std_ac_lu[3] = ['03']
std_ac_lu[4] = ['00','04','11']
std_ac_lu[5] = ['05','12','21']
std_ac_lu[6] = ['31','41']
std_ac_lu[7] = ['06','13','51','61']
std_ac_lu[8] = ['07','22','71']
std_ac_lu[9] = ['14','32','81','91','A1']
std_ac_lu[10] = ['08','23','42','B1','C1']
std_ac_lu[11] = ['15','52','D1','F0']
std_ac_lu[12] = ['24','33','62','72']
std_ac_lu[15] = ['82']
std_ac_lu[16] = ['09','0A',
'16','17','18','19','1A',
'25','26','27','28','29','2A',
'34','35','36','37','38','39','3A',
'43','44','45','46','47','48','49','4A',
'53','54','55','56','57','58','59','5A',
'63','64','65','66','67','68','69','6A',
'73','74','75','76','77','78','79','7A',
'83','84','85','86','87','88','89','8A',
'92','93','94','95','96','97','98','99','9A',
'A2','A3','A4','A5','A6','A7','A8','A9','AA',
'B2','B3','B4','B5','B6','B7','B8','B9','BA',
'C2','C3','C4','C5','C6','C7','C8','C9','CA',
'D2','D3','D4','D5','D6','D7','D8','D9','DA',
'E1','E2','E3','E4','E5','E6','E7','E8','E9','EA',
'F1','F2','F3','F4','F5','F6','F7','F8','F9','FA',
]
return getStd(std_ac_lu)
def get_ac_lu_dict():
std_ac_lu = {}
std_ac_lu[2] = ['01','02']
std_ac_lu[3] = ['03']
std_ac_lu[4] = ['00','04','11']
std_ac_lu[5] = ['05','12','21']
std_ac_lu[6] = ['31','41']
std_ac_lu[7] = ['06','13','51','61']
std_ac_lu[8] = ['07','22','71']
std_ac_lu[9] = ['14','32','81','91','A1']
std_ac_lu[10] = ['08','23','42','B1','C1']
std_ac_lu[11] = ['15','52','D1','F0']
std_ac_lu[12] = ['24','33','62','72']
std_ac_lu[15] = ['82']
std_ac_lu[16] = ['09','0A',
'16','17','18','19','1A',
'25','26','27','28','29','2A',
'34','35','36','37','38','39','3A',
'43','44','45','46','47','48','49','4A',
'53','54','55','56','57','58','59','5A',
'63','64','65','66','67','68','69','6A',
'73','74','75','76','77','78','79','7A',
'83','84','85','86','87','88','89','8A',
'92','93','94','95','96','97','98','99','9A',
'A2','A3','A4','A5','A6','A7','A8','A9','AA',
'B2','B3','B4','B5','B6','B7','B8','B9','BA',
'C2','C3','C4','C5','C6','C7','C8','C9','CA',
'D2','D3','D4','D5','D6','D7','D8','D9','DA',
'E1','E2','E3','E4','E5','E6','E7','E8','E9','EA',
'F1','F2','F3','F4','F5','F6','F7','F8','F9','FA',
]
return std_ac_lu
def std_AC_CO():
std_ac_co = {}
std_ac_co[2] = ['00','01']
std_ac_co[3] = ['02']
std_ac_co[4] = ['03','11']
std_ac_co[5] = ['04','05','21','31']
std_ac_co[6] = ['06','12','41','51']
std_ac_co[7] = ['07','61','71']
std_ac_co[8] = ['13','22','32','81']
std_ac_co[9] = ['08','14','42','91','A1','B1','C1']
std_ac_co[10] = ['09','23','33','52','F0']
std_ac_co[11] = ['15','62','72','D1']
std_ac_co[12] = ['0A','16','24','34']
std_ac_co[14] = ['E1']
std_ac_co[15] = ['25','F1']
std_ac_co[16] = ['17','18','19','1A',
'26','27','28','29','2A',
'35','36','37','38','39','3A',
'43','44','45','46','47','48','49','4A',
'53','54','55','56','57','58','59','5A',
'63','64','65','66','67','68','69','6A',
'73','74','75','76','77','78','79','7A',
'82','83','84','85','86','87','88','89','8A',
'92','93','94','95','96','97','98','99','9A',
'A2','A3','A4','A5','A6','A7','A8','A9','AA',
'B2','B3','B4','B5','B6','B7','B8','B9','BA',
'C2','C3','C4','C5','C6','C7','C8','C9','CA',
'D2','D3','D4','D5','D6','D7','D8','D9','DA',
'E2','E3','E4','E5','E6','E7','E8','E9','EA',
'F2','F3','F4','F5','F6','F7','F8','F9','FA',
]
return getStd(std_ac_co)
def get_ac_co_dict():
std_ac_co = {}
std_ac_co[2] = ['00','01']
std_ac_co[3] = ['02']
std_ac_co[4] = ['03','11']
std_ac_co[5] = ['04','05','21','31']
std_ac_co[6] = ['06','12','41','51']
std_ac_co[7] = ['07','61','71']
std_ac_co[8] = ['13','22','32','81']
std_ac_co[9] = ['08','14','42','91','A1','B1','C1']
std_ac_co[10] = ['09','23','33','52','F0']
std_ac_co[11] = ['15','62','72','D1']
std_ac_co[12] = ['0A','16','24','34']
std_ac_co[14] = ['E1']
std_ac_co[15] = ['25','F1']
std_ac_co[16] = ['17','18','19','1A',
'26','27','28','29','2A',
'35','36','37','38','39','3A',
'43','44','45','46','47','48','49','4A',
'53','54','55','56','57','58','59','5A',
'63','64','65','66','67','68','69','6A',
'73','74','75','76','77','78','79','7A',
'82','83','84','85','86','87','88','89','8A',
'92','93','94','95','96','97','98','99','9A',
'A2','A3','A4','A5','A6','A7','A8','A9','AA',
'B2','B3','B4','B5','B6','B7','B8','B9','BA',
'C2','C3','C4','C5','C6','C7','C8','C9','CA',
'D2','D3','D4','D5','D6','D7','D8','D9','DA',
'E2','E3','E4','E5','E6','E7','E8','E9','EA',
'F2','F3','F4','F5','F6','F7','F8','F9','FA',
]
return std_ac_co
使用的時候用get_dc/ac_co_lu_dict可以直接拿到寫入DHT用的哈夫曼表,裏面存放的都是2個16進制數字;使用std_DC/AC_CO/LU可以拿到編碼用的哈夫曼樹
編碼數據
編碼數據的安排是這樣的,每個8*8的塊按照順序存放,每個塊內三個顏色空間按順序存放,每個顏色空間內先存DC分量編碼,再存AC分量編碼
對於AC部分的編碼。
如果沒有AC分量沒有出現的話,其size置爲0後,後面可以不用跟任何東西(即爲EOB,熵編碼部分爲00)。
因爲scan的數據必須爲8的整數位,如果有不足的地方需要填1