本篇介紹
在JPEG檔案內排列的順序,範例參考Understanding and Decoding a JPEG Image using Python,參考規格書補漏,並實作JPEG decoder。
JPEG有4種壓縮格式
每種壓縮格式的佈局都不一,我們預設圖片為Baseline JPEG。
上方為Ange Albertini的圖,這顯示出JPEG內重要的區段(segment),大部分的標記(marker)後面接著不含標記的區段長度,較為重要的標記有:
Hex | Marker | Marker Name | Description |
---|---|---|---|
0xFFD8 | SOI | Start of Image | |
0xFFE0 | APP0 | Application Segment 0 | JFIF - JFIF JPEG image / AVI1 - Motion JPEG (MJPG) |
0xFFDB | DQT | Define Quantization Table | |
0xFFC0 | SOF | Start of Frame 0 | Baseline DCT |
0xFFC4 | DHT | Define Huffman Table | |
0xFFDA | SOS | Start of Scan | |
0xFFD9 | EOI | End of Image |
SOI
: 圖檔起始EOI
: 圖檔結束SOS
並沒有提及壓縮資料的長度,所以解碼器會一直讀取直到EOI
。
嘗試寫個簡單的decoder,除了讀到SOI
、EOI
要進行特別處理,其餘只要遵循讀完2 bytes標記,再讀2 bytes的區段長度的規則:
0xFFE0
)略過,不影響解碼
0xFFDB
)ISO/IEC 10918-1:199, 39-40
Baseline:
Parameter | Value |
---|---|
Lq | 2+65n |
Pq | 0 (8-bit ) |
Tq | 0~3,(0: Luminance, 1:Chrominance) |
實際佈局
使用全是1的quantization table進行測試
Quantization table可使用預設的表,加上quality factor
使用方式如下:
0xFFC0
)ISO/IEC 10918-1:199, 35-36
與上面的圖交戶對照
Parameter | Description | Value (For Baseline DCT) |
---|---|---|
SOFn | Marker | 0xffc0 |
Lf | Frame header length | Lf+P+Y+X+Nf=8 bytes, Ci+Hi+Vi+Tqi=3 bytes, 8+3*3(Lf)=17 |
P | Sample precision | 8 |
Y | Number of lines (Height) | 2 |
X | Number of sample per line (Width) | 6 |
Nf | Number of image components in frame | Y, Cb, Cr => Nf=3 |
Ci | Component identifier | 1~3 (1=Y, 2=Cb, 3=Cr, 4=I, 5=Q) |
Hi | Horizontal sampling factor | 略 |
Vi | Vertical sampling factor | 略 |
Tqi | Quantization table destination selector, component所對應的量化表 | Y對應到0,CbCr共用一張表對應到1 |
關於Y
, X
, Hi
, Vi
的意義,還需要參照ISO/IEC 10918-1:199 A.1.1
假設圖片512x512且有3個components (Y
, Cb
, Cr
),要求出每個component是由多少個xi*yi
的sample組成(sample就是我們常在說構成圖片的"點",在Baseline中1 sample=8-bit):
X=512
, Y=512
, Hmax=4
, Vmax=2
, 那每個component的xi
, yi
會由decoder計算,分別為:
所以在上方表格中,對應的xi
, yi
:
會發現chroma儲存的資訊水平、垂直上都減半,也就是YUV 4:2:0
輸出一小段測試
0xFFC4
)ISO/IEC 10918-1:199, 40-41
Parameter | Description | Value (For Baseline DCT) |
---|---|---|
Lh | Huffman table definition length | 2+17+mt |
Tc | Table class | 0=DC table, 1=AC table |
Th | Huffman table destination identifier | 0-1 (0=Luminance, 1=Chroma)。其實沒有規範,只要與Quantization table和Scan header的destination對應起來就好。 |
Li | Number of Huffman codes of length i | 0-255 |
Vij | Value associated with each Huffman code | 0-255 |
先前提到JPEG將Huffman table拆成codeword length、decoded value兩部份存,合計16+n bytes,就是在講Li、Vij。
回顧範例,試著寫encoder/decoder。
Standard允許一個marker後方接多組Huffman table
Huffman decoder的部份覺得micro-jpeg-visualizer寫的不錯,索幸拿來用,整份程式只有250行不依賴任何套件。
GetCode
需要逐位元讀取data,所以另外建立Stream
以便循序讀取資料上的每一位元
假設在Huffman table中0b000
對應0x04:
將length
, elements
解出來丟進HuffmanTable()
建表得到Huffman tree - root
,便能用GetCode
將codeword解碼。
0xFFDA
)ISO/IEC 10918-1:199, 37-38
在進行解碼前,先對一些函數進行修改。
JPEG在色域轉換後會減去128才進行DCT,將[0, 255]
對應到[-128, 127]
,因此解碼時需先補上128。
Image
儲存圖像資料的物件Image
只有在進行色域轉換時才以3維陣列處理,平時以1維陣列img
儲存Y、Cb、Cr的2維資料。DrawMatrix
將已解碼完成的部份資料渲染到整塊畫布。
decodeSOS
To help resiliency in the case of data corruption, the JPEG standard allows JPEG markers to appear in the huffman-coded scan data segment. Therefore, a JPEG decoder must watch out for any marker (as indicated by the
0xFF
byte, followed by a non-zero byte). If the huffman coding scheme needed to write a0xFF
byte, then it writes a0xFF
followed by a0x00
– a process known as adding a stuff byte.
JPEG允許scan data出現marker(0xFFXX
)來增加容錯性,為了和marker進行區別,寫入0xFF
的資料會寫入成0xFF00
,這是在進行Huffman decoding前要進行留意到的細節。
解碼流程:
0xFF00
為0xFF
BuildMatrix
查表解碼,在YCbCr 4:4:4下就是Y, Cb, Cr, Y, Cb, Cr…DrawMatrix
渲染在畫布img
上BuildMatrix
BuildMatrix
的功能是重排DC/AC coeficient的順序,並進行inverse DCT。
micro-jpeg-visualizer的實作有兩處值得檢視:
0
(EOB
),那就沒有必要讀addition bits的必要。在GetBitN
的實作中,傳入0
不會從Stream
讀取任何位元,而是返回0
;DecodeNumber(0, 0)
也是直接返回0
,這樣的實作方式省去判斷code
是否為0。
0xF0
)要將offset(l
)加上16並讀取下一組,但實作上是分成 +15、+1兩次進行,也省下判斷式。
為了比較好釐清JPEG的實作,這邊刻意改寫成:
自己沒有注意到兩處小技巧在改寫時忽略掉幾點,解碼時在幾個MCU後解碼錯誤,因為是Bit-stream非常難除錯,最後還是靠比對原始程式碼發現這點。整體程式碼如下:
DCT
實作前一章的內容。
我們需要一張8倍數長寬的圖片作為解碼器測試用
decode
後產生的是RGB不同分量的二維矩陣的一維陣列,因此還需要用dstack
疊成一個三維陣列
pyplot
展示結果
JPEG對於非8倍數寬度圖像的做法,就是將其擴展至8倍數,至於如何擴展由實作決定,有以鏡像方式填充(padding),為何是以鏡像方式?以留黑邊影像為例,黑邊在進行轉換到頻域時會產生分佈極廣的係數(回想step function經過fourier transform的結果),這會產生大量不同的位元需要編碼,因此以鏡像方式填補。
以8x2的黑白影像分別以重複(edge)、鏡像(reflection)、對稱(symmetric)進行填補,使用numpy.pad實作。
結果顯示reflect和symmetric所產生的係數更為集中,如果我們計算一下三者的entropy,會發現reflect和symmetric能夠帶給我們更好的壓縮比。
本篇介紹資料在JPEG中的佈局並實作decoder,未來有空的話將實作encoder,需要處理SOI、DQT、SOF、DHT、SOS、EOI幾個重要的marker,因為注重在quality factor和圖片差異的關係而非壓縮率,因此Luma、Chroma採用的Huffman table採用ISO/IEC 10918-1:199的Table K.3~K.6,quantization table則使用Table K.1~K.2,實作參考ghallak。
解碼過程中也發現,縱使處理MCU不用耗費太多時間,JPEG decoder的速度仍會受限於Huffman decoder的限制,因為必須解碼當前的符號才知道下一個符號的起始點。t0rakka加入RST marker區隔MCU,達到多執行緒decode,效率驚人,可惜並非所有圖片都有加入RST,使用上較為受限。