jpeg格式說明與解碼學習

jpeg格式說明與解碼學習


本文更加註重JPEG格式的具體解碼實現,並不涉及編碼實現(比如DCT、熵編碼之類的,這些在很多書中都有詳細的介紹,我就不贅述了)

參考資料

中文資料

英文資料

格式介紹

以下內容翻譯自英文維基百科,我覺得挺不錯的。

JPEG由segment組成,segment的開頭是marker;marker由0xFF開頭,後一個決定了marker的類型。marker後會跟兩個字節的的長度,表示其後的數據量,有時會用連續的FF進行填充。

概念釋義

不保證正確,只是個人的理解

  1. JPG與JPEG:一樣的意思,都是表示Joint Photographic Experts Group開發的這套JPEG標準所描述的圖片,只是早期DOS系統只支持3位擴展名,而Apple支持多位擴展名纔有所區別。後面即使Windows同樣支持多位擴展名也沒有改變這個習慣。因爲Windows用戶更多,所以主流爲jpg,但是像是.jpeg、.JPG和.JPEG都是可以的。
  2. JPG與JFIF:早期的JPEG格式只說明了圖片如何壓縮爲字節流以及重新解碼爲圖片的過程,但是沒有說明這些字節是如何在任何特定的存儲媒體上封存起來的。因此建立了相關的額外標準JFIF(JPEG File Interchange Format)。後來這個標準變爲主流,現在所使用的JPEG文件基本都是符合JFIF標準的。

關於0xFF

從上面這句話可以看出,0xFF在JPEG文件中是十分重要。如果讀取中出現了0xFF,根據後面的值有多種可能。

  1. 後面的值爲0xFF,這時候視爲一個0xFF看待,繼續讀取。
  2. 後面的值爲0x00,這時候表示這個FF在數據流中,跳過。
  3. 其他能夠表示marker的值,將其視爲marker開頭處理。
  4. 其他值,跳過。

整體格式

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所述,對於單一的哈夫曼表應該有三個部分:

  1. 哈夫曼表ID和類型:長度爲1byte。這個自己的值一般只有四個,0x00、0x01、0x10、0x11。其中高4位表示直流/交流,低4位表示id。
  2. 不同位數的碼字數量,JPEG文件的哈夫曼編碼只能是116位。這個字段的16個字節分別表示116位的編碼碼字在哈夫曼樹中的個數
  3. 編碼內容:這個字段記錄了哈夫曼樹上各個葉子結點的權重。因此,上一字段的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位數字。
接着寫下所有的哈夫曼編碼對應的值,編碼結束。

這裏有幾個問題:

  1. 生成的Huffman編碼如果大於16位的話就不在範圍之內了,這個是有可能的,這個問題提到了這種情況。裏面的答主提到實際中後面會留有一定的空間,同時大多數JPEG編碼器使用的是基於統計學制作的標準的哈夫曼編碼表。(注:普通的jpeg不會遇到這種情況,因爲它們編碼的對象爲size,一般不會超過16位,更多情況下不會多於10個點)
  2. 從解碼的角度考慮,存儲的時候哈夫曼編碼條目不一定能夠還原回去。這意味着應該要對生成的哈夫曼樹進行處理,保證三個特點: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

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