jpeg格式说明与解码学习
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
 
                    
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号