以下内容翻译自英文维基百科,我觉得挺不错的。
JPEG由segment组成,segment的开头是marker;marker由0xFF开头,后一个决定了marker的类型。marker后会跟两个字节的的长度,表示其后的数据量,有时会用连续的FF进行填充。
不保证正确,只是个人的理解
从上面这句话可以看出,0xFF在JPEG文件中是十分重要。如果读取中出现了0xFF,根据后面的值有多种可能。
JPEG格式的大致顺序为:
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所述,对于单一的哈夫曼表应该有三个部分:
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的扫描顺序为从上到下,从左到右。
根据DCT变换,每个块都会得到一个DC分量。对于每个颜色空间的DC分量,计算差值,使得过去的值可以用现在的值加上之前的累加和来表示。
对于得到的这么一个差分序列,将它变为(size,amplitude)形式,其中amplitude的计算方式是:正数直接转换为二进制数,负数直接取反码。size表示amplitude的位数。再对每个size进行哈夫曼编码来压缩,就得到了最后的二进制流。
每个块有63个AC分量。对这些AC分量按照zigzag顺序进行游长编码(runlength,value),意义为跨越多少个0串之后到达怎样的值。因为后面编码的原因,runlength不能大于15。对于得到的游长编码,对其value进行DPCM编码,然后将runlength作为4位二进制数与DPCM的size一同合并为8位二进制数进行编码,amplitude单独编码,合并成一个二进制流。
对于得到的4个哈夫曼树进行编码。编码时按照哈夫曼编码字典序排列,位数短的在前,统计位数,然后写下前16位数字。
接着写下所有的哈夫曼编码对应的值,编码结束。
这里有几个问题:
然而绝大多数解码器都会使用标准的哈夫曼编码,下面是我所写的标准哈夫曼树
#接受值和长度
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