JPEG结构及解码过程详解
0. 本文概述本文是笔者在研究JPEG编码格式及解码过程中的笔记,整合了许多网上的文章与参考文献(链接于文末).
同时本文也是为了应付USTC计网实验写一篇博客的要求
本文将介绍JPEG的格式(包括常用JPEG标识段及其结构),并详细介绍解码基线离散余弦变换编码JPEG(baseline DCT JPEG)与渐进式离散余弦变换编码JPEG(Progressive DCT Huffman Coding JPEG)的过程,这是两种最常用的JPEG编码格式,其他编码格式由于过于少见,许多格式转换软件甚至不支持它们.对于本文所介绍的两种JPEG编码格式,前者具有最广泛的兼容性,而后者在解码过程中具有从模糊到清晰的过程,加载速度较快,因此是大多数网站与应用的首选JPEG格式.
另外,对于本文并未提到或介绍不太详细的内容,你可以阅读由**国际电信联盟(ITU-T)与国际标准化组织(ISO)**制定的参考文献[1]详细了解
1.JPEG基本格式除被压缩的图片数据外,JPEG文件由许多以标记(Marker)开头的段(Segment)组成,常见的如SOI,APPn,DHT,DQT,SOFn,SOS,EOI等等.标记长度为2字节,且第一个字节总是0xFF.对于大多数段而言,紧跟在标记后的两字节就是该段的长度(大端格式,长度包含自身但不包括标记).我们将在下文中详细地介绍它们的大致顺序与结构.
对于JPEG中的数据均为大端格式,在压缩数据流处也是先读取字节高位
1.1 SOI与EOIJPEG文件总是以标记SOI(Start of Image)开始,并以EOI(End of Image)标记结束.这两个段除标记单独出现外没有其他的数据,在SOI与EOI之间的所有内容称为帧(Frame).
SOI结构:
Name
Length(Bytes)
Description
SOI
2
0xFFD8
EOI结构:
Name
Length(Bytes)
Description
SOI
2
0xFFD9
1.2 APPn与COMAPPn是为应用程序保留的段,当n=0时,该段一般用来保存24位RGB像素的缩略图,当n=1~15时,为应用程序保存一些特定数据.对于JPEG解码来说,这些段没有必要的作用,可以通过读取标记后的两个字节来跳过.
COM为图片的注释段,也没有实际意义,可以跳过.
APP0结构:
Name
Length(Bytes)
Description
APPn(n=0~15)
2
0xFFE0~0xFFEF
段长度
…
*
…(data)
…
缩略图或保留数据
COM结构:
Name
Length(Bytes)
Description
COM
2
0xFFFE
段长度
…
*
…(Comments)
…
注释
1.3 DQTDQT即定义扫描量化表(Define Quantization Table).JPEG格式在压缩时以一个颜色分量的8*8数据作为一个数据单元(data unit),进行DCT变换后再量化处理.扫描量化表储存了对于8*8个数据进行量化的具体系数.
一个DQT段可以定义多个量化表,也可以在多个DQT段中分别定义.对于一般JPEG,它最多只能同时定义4个DQT段,但是允许在不同的扫描(scan)前重新定义.
DQT结构:
Name
Length(Bytes)
Description
DQT
2
0xFFDB
DQT长度
…
*
DQT表1:
量化精度与量化表id
1
高4位:精度 0:8bit 1:16bit低4位:量化表id
64个量化表系数
64*(1+精度)
*
(更多DQT表…)
*
*
当读取完一个DQT表发现未达到该DQT段的长度时,提示接下来还有DQT表.
1.4 DHTDHT即定义霍夫曼表(Define Huffman Table).该表定义了压缩过程中用到的霍夫曼表.需要注意的是,JPEG压缩使用范式霍夫曼编码来压缩数据,这种压缩方法使得DHT段中只需要记录各编码长度所含的编码条目数,与被编码的原数据(1字节),就可以建立编码的霍夫曼树.
可参考文献[3]
类似DQT,DHT段也可以定义多个霍夫曼表,也可以在多个DHT段中分别定义.同样DHT也只能定义4个段,但允许重新定义.
DHT结构:
Name
Length(Bytes)
Description
DHT
2
0xFFC4
DHT长度
…
*
DHT表1:
表id与编码数据类型
1
高4位:编码数据类型 0:DC(直流) 1:AC(交流)低4位:霍夫曼表id
1~16编码长度所含的编码条目数
16
*
被编码的原数据
以上16个数值和
按照顺序的原数据字节
(更多DHT表…)
*
*
当读取完一个DHT表发现未达到该DHT段的长度时,提示接下来还有DHT表.
1.5 SOFnSOFn,即帧图像开始段(Start of Frame).n代表图像所用的编码格式,例如对于本文所研究的两种编码格式:基线DCT格式为SOF0,而渐进式DCT格式为SOF2.该段记录了图像的基本信息,如长宽、数据精度、颜色分量数等.SOF段必须在任何扫描开始前定义.
SOFn结构:
Name
Length(Bytes)
Description
SOFn
2
0xFFC0+n
SOF长度
…
*
图像数据精度
1
8/12/16位,大多数软件仅支持8位精度
图像高度
2
Y向像素数量
图像宽度
2
X向像素数量
颜色分量数
1
1(灰度图)/3(YCrCb)/4(CMYK)
颜色分量信息
(颜色分量数)*3
颜色ID(1B):从1计数采样因子(1B):高4位水平采样因子,低4位垂直采样因子量化表ID:该颜色分量所使用量化表ID
采样因子与在扫描中规定的*最小编码单元(Minumum Coded Unit,MCU)*的大小有关,我们之后详细叙述这个问题
1.6 DRI与RSTnDRI即定义复位间隔(Define Restart Interval),除标记与描述长度的数据外,仅有一个2字节数据(设为n),它描述了一个扫描每读取n个MCU后就应该重置(包括重置DC差分编码与字节重新对齐等),并读入2字节长的复位标记RSTn,并继续进行扫描读取,其中RSTn的n从0到7循环.
DRI与RSTn相关的段用于分隔压缩数据,以确保一块数据出错时不会影响下一块数据.DRI与配合的RSTn并不是必需的,且同样也可以在每次扫描前重新定义复位间隔.
DRI结构:
Name
Length(Bytes)
Description
DRI
2
0xFFDD
DRI长度
4
*
读取MCU复位间隔n
2
每在扫描中读取n个块就重置,并读取复位标记
RSTn结构:
Name
Length(Bytes)
Description
RSTn
2
0xFFD0+n,总应该从0开始0~7循环
1.7 SOSSOS段即扫描开始段(Start of Scan).本段后接一次扫描(即对图片压缩数据流的读取),因此定义了扫描的具体参数.一次扫描应该具备的参数有:扫描中参与的颜色分量数、各颜色分量所使用的霍夫曼表表号(包含直流分量的表号与交流分量的表号)、扫描的(经DCT变换后的)频谱段起始与逐次逼近系数(前一次/本次).
SOS(即扫描)可以有多次,对渐进式JPEG来说尤其如此,而基线式JPEG也可以有多次扫描,不过总是扫描全部频率,且不会有逐次逼近.换言之,对基线JPEG的扫描后三个字节只能是0x00 0x63 0x00,其多次扫描只是针对不同的颜色分量.
SOS结构:
Name
Length(Bytes)
Description
SOS
2
0xFFDD
SOS长度
…
*
该扫描所包含颜色分量数
1
1(灰度图)/3(YCrCb)/4(CMYK)
颜色分量信息
(颜色分量数)*2
颜色ID(1B):从1计数霍夫曼表表号:高4位DC表号,低4位AC表号
扫描频谱起始
1
扫描每一个数据单元(data unit)经DCT变换后的频段的开始
扫描频谱结尾
1
扫描每一个数据单元(data unit)经DCT变换后的频段的结尾
逐次逼近系数
1
仅针对渐进式编码定义:高4位为前一次(该频段)扫描的该系数(初值0),低四位为本次扫描的系数
在SOS之后就是压缩的数据流,JPEG格式定义:若扫描中出现了段标记的开头字节0xFF,必须在之后添加额外的0x00以表示0xFF是数据流的一部分.若扫描结束时不在对齐的字节上,则该字节剩余比特均为1,假如这恰好构成了一个0xFF,同样也要在结尾添加0x00.
扫描若有多个颜色分量,我们称之为交叉的(interleaved),否则称为非交叉的(non-interleaved).是否交叉扫描会对MCU大小的计算造成影响,具体是:若扫描非交叉,则只有单个颜色分量被扫描,MCU大小就是数据单元(data unit)的大小(8*8),否则需要根据扫描中所有颜色分量的采样系数计算MCU大小,这一点我们在下一节会详细介绍.对于逐次逼近系数,若被定义且前一次扫描的系数不为0,则在渐进式JPEG中,本次扫描的系数只能为 上一次系数-1。
1.8本节总结以上就是基线与渐进式JPEG中常见的标记与段,但是仅仅根据这些信息无法得出JPEG具体的解码方法.我们将在下面两节分别详细介绍基线JPEG与渐进式JPEG的编解码过程.
2 基线(Baseline)JPEG解码方法待更
3 渐进式(Progressive)JPEG解码方法待更(以下为未整理的废稿)
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465//以下是在一次 Refined AC Scan 中,对一块 8 * 8 块进行操作的伪代码:GLOBAL EOBRUN; //当前EOBRUN计数GLOBAL SSS; //本次AC扫描频段起始(Spectral Selection Start)GLOBAL SSE; //本次AC扫描频段结尾(Spectral Selection End)GLOBAL SUC_APRX;//该次扫描的 逐位逼近系数(Successive Approximation)GLOBAL BIT_STREAM; //扫描读取的比特流void ACRefinedScan(int Coef_Matrix[64]){ //Coef_Matrix : 经过了First AC Scan 的 8 * 8 像素块某分量系数矩阵 int i = SSS; //遍历扫描频段的变量 while(i <= SSE){ if(EOBRUN > 0){ //EOBRUN不是零,相当于skip该块的每个0系数 //但对非0系数的修正仍然需要进行 while(i <= SSE){ if(Coef_Matrix[i] != 0){ RefineAC(Coef_Matrix[i]); } i = i + 1; } EOBRUN = EOBRUN - 1; return; } //否则,调用霍夫曼表解码接下来的操作 int val = DecodeUsingHTable(BIT_STREAM); //假设霍夫曼解码出下一个值val //得到高位与低位 int lobits = val & 0x0F; int hibits = (val & 0xF0) >> 4 //如果低位为1,有0系数会被修正 if(lobits == 1){ int extra_bit = ReadRawBit(BIT_STREAM, 1); //邻接的比特是修正的正负号 while(hibits > 0 || Coef_Matrix[i] != 0){ if(Coef_Matrix[i] != 0){ //skip过程见到非0系数就修正 RefineAC(Coef_Matrix[i]); }else{ //否则见到0系数,skip计数减1 hibits = hibits - 1; } i = i + 1; } //在循环外i指向的应该是一个0系数,修正他 if extra_bit != 0 Coef_Matrix[i] = 1 << SUC_APRX; else Coef_Matrix[i] = -1 << SUC_APRX; i = i + 1; }else{ //低位为0,没有0系数会被修正 if(hibits == 0xF){ //val=0xF0,代表skip 16个0 while (hibits >= 0){ if Coef_Matrix[i] != 0 { RefineAC(Coef_Matrix[i]); }else{ hibits = hibits - 1; } } }else{ //val=0x00~0xE0,这代表EOB run EOBRUN = (1 << hibits) + ReadRawBit(BIT_STREAM, hibits); } } }}
0.5 test images下面是zigzag变换的顺序:
END参考文献:[1]itu-t81.pdf[2]JPEG编码过程详解[3]JPEG中的范式霍夫曼编码[4]JPEG编解码过程详解[5]参考书:Compressed Image File Formats JPEG, PNG, GIF, XBM, BMP (John Miano)