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格式,也就是说大端模式,高字节低地址,低字节高地址。

标签表

总表

缩写字节码名称注释
SOI0xFFD8Start of image文件开头
SOF00xFFC0Start of Frame0Baseline DCT-based JPEG所用的开头
SOF20xFFC2Start of Frame2Progressive DCT-based JPEG
DHT0xFFC4Define Huffman Tables指定一个或多个哈夫曼表
DQT0xFFDBDefine Quantization Table指定量化表
DRI0xFFDDDefine Restart IntervalRST中的marker
SOS0xFFDAStart of ScanScan的开头
RSTn0xFFDnRestartDRImarker中插入r个块
APPn0xFFEnApplication-specificExif JPEG使用APP1,JFIF JPEG使用APP0
COM0xFFFEComment注释内容
EOI0xFFD9End of Image图像的结束

表2 JPEG Start of Frame结构

字段名称长度注释
标记代码2 bytes固定值0xFFC0
数据长度2 bytesSOF 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 bytesSOS marker的长度,包含自身但不包含标记代码
颜色分量数1 bytes灰度级1,YCbCr或YIQ是3,CMYK是4
颜色分量信息颜色分量数*31byte的颜色分量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 bytesAPP0的长度,包含自身但不包含标记代码
标识符 identifier5 bytes固定的字符串"JFIF\0"
版本号2 bytes一般为0x0101或0x0102,表示1.1或1.2
像素单位 unit1 byte坐标单位,0为没有单位; 1 pixel/inch; 2pixel/inch
水平像素数目2 bytes
垂直像素数目2 bytes
缩略图水平像素数目1 byte如果为0则没有缩略图
缩略图垂直像素数目1 byte同上
缩略图RGB位图3n bytesn = 缩略图水平像素数目*缩略图垂直像素数目,这是一个24bits/pixel的RGB位图

表5 APPn应用程序保留标记

字段名称长度注释
标记代码2 bytes固定值0xFFE1-0xFFEF,n=1~15
数据长度2 bytesAPPn的长度,包含自身但不包含标记代码
详细信息(length-2) bytes内容是应用特定的,比如Exif使用APP1来存放图片的metadata,Adobe Photoshop用APP1和APP13两个标记段分别存储了一副图像的副本。

表6 DQT 定义量化表

字段名称长度注释
标记代码2 bytes固定值0xFFDB
数据长度2 bytesDQT的长度,包含自身但不包含标记代码
量化表(length-2)bytes下面为子字段
精度及量化表ID1 byte高4位为精度,只有两个可选值:0表示8bits,1表示16bits;低4位为量化表ID,取值范围为0~3
表项64*(精度+1)bytes

表7 DHT 定义哈夫曼表

字段名称长度注释
标记代码2 bytes固定值0xFFC4
数据长度2 bytesDHT的长度,包含自身但不包含标记代码
哈夫曼表(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

posted @ 2018-11-24 12:22  千灵域  阅读(1261)  评论(0)    收藏  举报