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与EOI

JPEG文件总是以标记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与COM

APPn是为应用程序保留的段,当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 DQT

DQT即定义扫描量化表(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 DHT

DHT即定义霍夫曼表(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 SOFn

SOFn,即帧图像开始段(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与RSTn

DRI即定义复位间隔(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 SOS

SOS段即扫描开始段(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解码方法

待更
(以下为未整理的废稿)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//以下是在一次 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变换的顺序:
zigzag

END

参考文献:
[1]itu-t81.pdf
[2]JPEG编码过程详解
[3]JPEG中的范式霍夫曼编码
[4]JPEG编解码过程详解
[5]参考书:Compressed Image File Formats JPEG, PNG, GIF, XBM, BMP (John Miano)