與具體的編碼數據空間相比,jpeg文件頭占據非常小乃至可以忽略不計的大小。
仍然拿JPEG解碼--(1)JPEG文件格式概覽中的《animal park》這張圖片來舉例,從跳過SOS(FF DA)的TAG開始——offset=0x153,
就真正進入了編碼數據區域,如下圖所示:

其占據的比例為:0x153/0x9721 = 339/38689 = 0.876%,還不到1%,其他jpeg圖片也是類似情況。
但是,就是這么小的數據區域,卻是至關重要的地方,某些關鍵的地方一個字節出錯了的話,解碼就會出錯(例如huffman table
中數據),或者重建出的yuv圖像異常(例如quantization table中數據)!
本篇是該系列的第三篇,主要介紹jpeg頭信息解析,其中除了huffman table重建較復雜外,其他TAG的解析都比較容易。
1. APP0——FF EO
先貼出這段區域:

從ASCII值可以看出,保存了JFIF——JPEG File Interchange Format(JPEG文件交換格式),后面的幾個字節應該是version信
息吧,沒深究。
2. DQT——FF DB

量化表有兩個,上面貼圖只高亮了其中一個表。
從offset=0x16開始的兩個字節(0x00 43)為這段區域的size=67,后面的一個字節為表的ID——0x00=0(可以看到第二張表中對
應位置offset=0x5D處為0x1)。
跳過前面三字節從offset=0x19處開始的64字節,即為量化表中量化值。其中需要說明的是,量化值是固定為64字節的,因為按8X8
進行DCT變換的。
工具解析的結果如下:

需要補充兩點:
A.亮度信號的Y分量使用DQT表一,UV分量使用表二。
B.亮度信號通常采用細量化(量化值較小),對應位置處,表一通常比表二值要小。此量化原因是人眼對亮度信號比較敏感,采用顆粒度
較細來量化,細量化引入的一個問題會消耗更多的數據空間。
3. SOF——FF C0

在該JPEG解碼系列中第一篇已經詳細介紹過了,不再贅述。工具解析如下:

4. DHT——FF C4

共有四張表,上面只貼出第一張表。
DHT表的重建有些復雜,涉及底層更多關於數據壓縮領域的知識,可以參考“范式霍夫曼編碼”相關材料,本博文不再做介紹該編碼原理。
但會針對具體個例進行說明,如果重建霍夫曼表。這是至關重要的一環,因為關系着后面霍夫曼解碼,如果表有誤,后面會解碼異常。
4.1 表分類
重建霍夫曼表。一般分為四個表:DC0,DC1,AC0,AC1,因為Y分量使用兩個表:DC0+AC0,而UV分量也使用兩個表:DC1+AC1。
4.2 幾個名詞及解釋
這個幾個名詞是個人按照自己的理解來定義的,讀者需按照這個來解讀,因為我的解碼工具就是按照這個來使用的。
例如,parseDHT顯示的下圖:

幾個名詞:序號(SequenceNum)、碼字長度(CodeWidth)、碼字(Code)、信源值/權值(CodeVal)。
SequenceNum:序號,依次遞增,從0到totalCodeCnt-1。totalCodeCnt值不確定,取決於編碼端編碼出的數量。
上面圖示的第一列數字,是依次遞增的,多一個編碼數據就多一行。
CodeWidth: 編碼數據的寬度(碼字寬度——二進制數據的bit位寬),寬度都是從2開始,最大為16。當某個碼字Code的寬度為16時,表示用16位的
編碼數據來表示某個像素值(確切講並不是像素值,而是RLE的值!),當然,其出現的概率非常低,否則會出現編碼數據量大於信源數據量了。
另外,碼字寬度必須是依次遞增的,中間不可能有跳變,因為霍夫曼編碼理論上會盡量用較窄的碼字來表示信源。-->也有可能產生跳變!
但一般概率較低,曾經遇到過。
相同碼字寬度的若干碼字,其碼字依次遞增。例如,圖示第3-5行,碼字寬度為3,其對應的碼字為0x4,0x5,0x6,即二進制:100,101,110。
Question:每當碼字寬度加一時,碼字如何變化?
上一個碼字值加1后,末尾再補一個零(即——加1右移)。當寬度增加二時,先將上一個碼字值加1后再補兩個零。增加三時類似,但出現
概率極低。例如上圖中,從CodeWidth中2->3過渡,Code值變化為:01 -> 100;從CodeWidth的3 -> 4過渡,Code值變化為:110 ->1110。
值得注意的一點:碼字寬度,不一定都是依次遞增,有可能產生跳變,目的是使后面的碼字不溢出,也就是補兩個或多個零的情況。
Code: 碼字,全部碼字要求各不相等。因其是編碼數據,而霍夫曼編碼要求讀取的完整的n位比特位的碼字Code,不能與其他碼字Code的前n位相等,
因此寬度值從2位的00開始。
例如,上面碼字Code中間的四行分別為:0x5,0x6,0xe,0x1e,(二進制表示:101,110,1110,11110)的編碼數據,其真正代表的CodeVal信源
值為4,3,5,6。由此也可以看出,信源值4出現的頻率/概率最高(如果僅僅這四個做比較的話是這樣(再極端情況是:P4=P3>P5>P6),如果通盤比較,
當然是第一行0x1出現的概率最大),因為要用最小(最窄)的編碼數據來表示頻率最高的信源值。這是huffman編碼理論中的一個核心概念——出現概率最大
的值的編碼寬度最窄,這樣最利於壓縮數據。
CodeVal:信源值(應該是接近信源的值,不是量化后的值,其值是RLE行程編碼值,由兩部分組成,高四位和低四位)。
即編碼內容,也是霍夫曼樹葉子節點權值,在解碼時需要用Code來恢復出這種值。
該值的寬度由量化精度決定,通常為8位,代表yuv圖形單個像素值采樣精度為8位,該值是唯一的,不能重復。——》 描述錯誤,不是這種情況,后面再解釋。
4.3 重建步驟
以例子來展示,不使用《animal park》,使用如下這一串值(紅色➕號分割不同意義的值,我自己添加的):
FF C4 00 1D 00 00 03 01 01 01 01 01 01 01 00 00 00 00 00 00 00 + 04 05 06 03 02 01 00 09 07 08
step1. 剔除掉表示size的00 1D以及表示table_id的00,剩余: 00 03 01 01 01 01 01 01 01 00 00 00 00 00 00 00 + 04 05 06 03 02 01 00 09 07 08
其中,前16個數值表示含義————碼字寬度(CodeWidth)為n的碼字(Code)的數量,其中n從1遞增到16(可以表示為該位置的index,但是其是從1開始遞增),
因為最小寬度為1,最大寬度為16。
通常,寬度為1的碼字不會使用,而編碼是從2位開始,例如第一個碼字通常為0b00,來表示出現頻率最高的那個信源值。有些位置上的值為0,表示該碼字寬度無
對應的碼字,像第一個位置和最后7個位置的0,就沒有對應的碼字。
從上面分析,可以得到結論:總共使用碼字的數量————16個位置上的數值之和,也即是totalCodeCnt=10(3+1+1+1+1+1+1+1),也是霍夫曼樹中葉子節點的個數。
step2. 前16個字節后面的若干個字節數據:04 05 06 03 02 01 00 09 07 08
其表示碼字寬度依次遞增時所對應的信源值(CodeVal),其數量必然等於totalCodeCnt,因為一個有效碼字(前16Byte不為0的)對應一個信源值。
step3. 對應關系生成
前16Bytes的第2個位置的03,代表碼字寬度為2的碼字數量為3,那么其分別為:0b00,0b01,0b10,其對應的信源值分別為后面的0x04,0x05,0x06
。。。。。。3。。。 。01。。。。。。。。3。。。 。。。1 。。。。。。。0b110。。。。。。。。。。。。。。。。。。。 。0x03
。。。。。。4。。。。 01。。。。。。。。4。。。。。。1。。。。。。。。 0b1110。。。。。。。。。。。。。。。。。。。0x02
以此類推,直到最后一個碼字寬度為9的碼字0x1fe,以及其代表的信源值0x08。
4.4 重建算法
本人工具提供了一個重建huffman表的算法,感興趣的可以參考。寫的不是太簡潔,但能正常重建DHT。
1 //rebuild huffman table 2 int parseDHT(ABitReader* abr, struct jpegParam* param) 3 { 4 printf("(%s : %d), DHT offset:%#x\n", __func__, __LINE__, abr->getOffset()); 5 int len = abr->getBits(16); 6 len -= 2; 7 while (len>0) 8 { 9 uint8_t idx = abr->getBits(8); 10 uint8_t idx_high = idx>>4; 11 uint8_t idx_low = idx & 0x0f; 12 13 //idx_hight represent DC or AC: 0-DC, 1-AC 14 //idx_low represent color id: 0-Y, 1-uv 15 //[0][x] -- DC table, [0][0]:DC0, [0][1]:DC1 16 //[1][x] -- AC table, [1][0]:AC0, [1][1]:AC1 17 //generate pHTCodeCnt[idx_high][idx_low] 18 uint8_t *pCodeCnt = (uint8_t*)malloc(16); 19 int i, j; 20 int total_code_cnt = 0; 21 printf("\ttable id: [%d][%d]--[%s%d], dump more detail info...\n", idx_high, idx_low, idx_high==0?"DC":"AC", idx_low); 22 printf("\tCodeCntOfNBits:\t"); 23 for (i=0; i<16; i++) { 24 int code_cnt = abr->getBits(8); 25 pCodeCnt[i] = code_cnt; 26 total_code_cnt += code_cnt; 27 printf("%2d ", code_cnt); 28 } 29 printf("\n\ttotal code cnt: %d\n", total_code_cnt); 30 param->HTCodeRealCnt[idx_high][idx_low] = total_code_cnt; 31 param->pHTCodeCnt[idx_high][idx_low] = pCodeCnt; 32 33 uint8_t *pWidth = (uint8_t *)malloc(total_code_cnt); 34 param->pHTCodeWidth[idx_high][idx_low] = pWidth; 35 printf("\tValidCodeWidth:\t"); 36 for (i=0, j=0; i<16; i++, j=0) { 37 while (j++ < pCodeCnt[i]) { 38 uint8_t tmp = *pWidth++ = i+1; 39 printf("%2d ", tmp); 40 } 41 } 42 puts(""); 43 44 pWidth = param->pHTCodeWidth[idx_high][idx_low]; 45 46 //generate pHTCode[idx_high][idx_low] 47 uint16_t *pCode = (uint16_t*)malloc(2*total_code_cnt); //huffman code width: 2~16 bits -> may 1 bits! but HuffmanDecode3 can not handle this! 48 param->pHTCode[idx_high][idx_low] = pCode; 49 bool init_flag = false; 50 for (i=0; i<16; i++) { 51 int j = 0; 52 uint16_t tmp; 53 while (j++ < pCodeCnt[i]) { 54 if ((i==1 || i==0) && (j==1) && (init_flag==false)) { 55 *pCode = 0; //init val 56 init_flag = true; 57 } else if (j == 1) { //first add x bits 58 int k = i; 59 int shift_bits = 1; 60 while(pCodeCnt[--k] == 0) { 61 shift_bits++; 62 } 63 tmp = (*pCode+1)<<shift_bits; 64 *++pCode = tmp; 65 } else { 66 tmp = *pCode + 1; 67 *++pCode = tmp; 68 } 69 //printf("i:%d, j:%d, (%d , %d) => %#x\n", i, j, pCodeCnt[i], pWidth[i], *pCode); 70 } 71 } 72 73 //generate pHTCodeVal[idx_high][idx_low] 74 uint8_t *pCodeVal = (uint8_t*)malloc(total_code_cnt); //huffman code width: 2~16 bits 75 param->pHTCodeVal[idx_high][idx_low] = pCodeVal; 76 for (i=0; i<total_code_cnt; i++) { 77 *pCodeVal++ = abr->getBits(8); 78 } 79 80 printf("\t-----------------huffman table: [%d][%d]---------------------\n", idx_high, idx_low); 81 82 pWidth = param->pHTCodeWidth[idx_high][idx_low]; 83 pCode = param->pHTCode[idx_high][idx_low]; 84 pCodeVal = param->pHTCodeVal[idx_high][idx_low]; 85 puts("\t[SequenceNum] (CodeWidth, Code) -> CodeVal"); 86 for (i=0; i<total_code_cnt; i++) { 87 printf("\t[%11d] (%9d, %#6x) -> %#7x\n", i, *pWidth++, *pCode++, *pCodeVal++); 88 } 89 len -= (17+total_code_cnt); 90 puts("\t---------------------------------------------------------------------------------------------"); 91 } 92 return 0; 93 }
5. SOS——FF DA

SOS主要描述了分量號與幾個DHT表的對應關系,以及編碼profile。
其含義如代碼所示:
1 //map comp_id to huffman_table 2 int parseSOS(ABitReader* abr, struct jpegParam* param) 3 { 4 printf("(%s : %d), SOS offset:%#x\n", __func__, __LINE__, abr->getOffset()); 5 int len = abr->getBits(16); 6 int comp_cnt = abr->getBits(8); 7 int i; 8 for (i=0; i<comp_cnt; i++) { 9 param->ht_comp_id[i] = abr->getBits(8); 10 CHECK_EQ(param->ht_comp_id[i], i+1); 11 int ht_idx = abr->getBits(8); 12 param->ht_idx[i] = ht_idx; 13 int dc_idx = ht_idx>>4; 14 int ac_idx = ht_idx & 0x0f; 15 printf("\tcolor_id[%d] use DC_table[%d], AC_table[%d]\n", param->ht_comp_id[i], dc_idx, ac_idx); 16 } 17 uint32_t baseline_flag = abr->getBits(24); 18 puts("\tonly support baseline profile! should pass for most cases!"); 19 CHECK_EQ(baseline_flag, 0x003f00); 20 21 return 0; 22 }
需要注意的是,最后3Bytes是描述所用的profile,但是基本上都是使用的baseline,這個值是固定的。
工具解析結果如下:

6. 其他補充
有些圖片帶縮略圖——thumbnail,其存在的目的是解碼大圖太耗費時間,而如果jpeg圖片中還嵌套着另外一份小圖(縮略圖),則利用該小圖解碼可以縮短解碼大圖時間,
來達到盡快展示給用戶看的目的。
jpeg文件格式支持這種方式,該小圖其實也是一幅分辨率較小的jpeg圖片,其一般會放在APP1下的Exif段。
帶thumbnail的圖片,thumbnail的部分,其完整保留了前面幾個小節介紹的各個TAG段。文件中,如果搜索"FF E0",會發現找到兩個,一個是主圖的,另外一個是縮略圖的。
