【数据压缩9】Lab5 JPEG原理分析及JPEG解码器的调试
文章目录
一、实验目的
掌握JPEG编解码系统的基本原理。初步掌握复杂的数据压缩算法实现,并能根据理论分析需要实现所对应数据的输出。
二、JPEG原理分析
JPEG是一种针对数字图像的有损压缩标准方法,由于JPEG编码算法可以在提供较大的压缩比的同时,保持较好的显示质量,所以该算法逐渐成为最为熟知和广泛使用的数字图像格式和通用标准。JPEG标准本身并没有规定具体的颜色空间,只是对各分量分别进行编码,通常将高度相关RGB颜色空间转换到相关性较小的YUV颜色空间。
1. JPEG编解码原理
JPEG编码的过程如上图所示,解码是编码的逆过程。
(1)零偏置电平下移(Level Offset)
对于灰度级为2^n 的像素,通过减去2^(n-1),将无符号整数变为有符号数,即值域变为正负对称。
目的:将绝对值大的数出现的概率大大减小,提高编码效率。
(2)离散余弦变换(8×8 DCT)
先将图像分为8×8的像块,如果图像的宽(高)不是8的整数倍,使用图像边缘像素填充,以不改变频谱分布。然后对每一个子块进行DCT(Discrete Cosine Transform)。DCT变换使用下式计算,C为变换核矩阵:
目的:实现能量集中和去相关,便于去除空间冗余,提高编码效率。DCT是一种无损变换,不压缩图像(输出的是系数)这样做是在为下一步的量化做准备。
(3)量化(Quantization)
量化是编码流程中唯一会引入误差也是唯一会带来压缩效果的步骤,决定压缩质量,也只有因此是JPEG压缩编码算法的核心。JPEG标准中采用中平型均匀量化,输入DCT系数,输出量化系数。
由于人眼对亮度信号比对色差信号更敏感,因此JPEG使用了两种量化表:亮度量化值和色差量化值;根据人眼对低频敏感,对高频不太敏感的视觉特性,对低频部分采取较细的量化,对高频部分采取较粗的量化,减少了视觉冗余。
- 建议量化表:基于人的生理感知阈值实验,50%质量的亮度量化矩阵如下
- 真正的量化表 = 缩放因子 × 基本量化表(质量因子Q越高,缩放因子S越低,图像质量越好)
- 质量因子≤ 50:缩放因子= 50 / 质量因子
- 质量因子> 50:缩放因子 = 2 – 质量 因子/ 50
但经过量化,子块中的大多数高频区域的系数的量化结果都为0。量化矩阵并不是固定的,可以根据要求的质量的不同而进行调整。
(4)DC系数——差分编码(Differential indices)
我们注意到,8×8像块经过DCT后得到的DC系数有两个特点:一是系数的值较大;二是相邻像块的DC系数存在相关性(即存在冗余)。根据这个特点,JPEG标准采用了DPCM(差分脉冲编码调制),以对相邻图像块之间量化DC系数的差值DIFF进行编码:
DIFF
k
=
DC
k
−
DC
k
−
1
(1-5)
\text {DIFF}_k = \text {DC}_k - \text {DC}_{k-1} \tag{1-5}
DIFFk=DCk−DCk−1(1-5)
(5) AC系数——之字形扫描和游程编码(Zig-Zag+RLE)
之字形扫描
经过DCT变换后,AC系数大多集中在左上角的低频分量区。因此采用Z字形按频率的高低顺序读出,可以出现很多连零的情况,便于使用RLE(Run Length Encoding,游程编码),若最后的数据均为0,则直接给出EOB(End of Block)。
游程编码
当遇到很多连续的0时,为缩短数据长度,编码非零系数level和它之前0的个数run。(Run,Size)
例如0, -2, -1, -1, -1, 0, 0, -1, EOB表示为:(1, -2), (0, -1), (0, -1), (0, -1), (2, -1), EOB。
(6)Huffman编码
对DC系数DPCM的结果和AC系数RLE的结果进行Huffman编码,分成类别,类似指数Golomb编码。类别ID采用一元码编码,类内索引采用定长码编码。共有亮度DC、亮度AC、色差DC、色差AC四张码表。
2. JPEG文件格式
JPEG文件以segment的形式组织,其中每个segment以一个marker开始,而每个marker均以0xFF和一个marker的标识符开始,随后为2字节的marker长度(不包含marker的起始两字节)和对应的payload(SOI和EOI marker只有2字节的标识符)。
注意,连续的0xFF字节并不是marker的起始标志,而是用来填充的特殊字符。
此外,部分中,0xFF后若为0x00,则跳过此字节不予处理。
(1)开始与结束
SOI | 0xFF, 0xD8 | none | Start Of Image |
---|---|---|---|
EOI | 0xFF, 0xD9 | none | End Of Image |
(2)APP0 segment(0xFFE0)
应用程序保留标记 0
字节数 | 含义 | 取值 |
---|---|---|
2 | APP0块的长度 | 0x00 10,16字节 |
5 | “JFIF”+”0” | 0x00 |
各1 | Major version和Minor version | 0x01 和 0x01 |
1 | X和Y方向密度单位 | 0x00,表示无单位 |
各2 | 水平方向和垂直方向像素密度 | 0x00 01和0x00 01 |
(3)APPn segment
应用细节信息
APPn | 0xFF, 0xEn | variable size | Application-specific |
---|
(4)量化表
两张量化表,一张亮度一张色度
DQT | 0xFF, 0xDB | variable size | Define Quantization Table(s) |
---|
长度 | 含义 | 取值 |
---|---|---|
2B | 量化表长度 | 0x00 43,67字节不包括FFDB |
4bit | 量化精度(0:8位;1:16位) | 0,8bit量化 |
4bit | 量化表ID,只能取0~3 | 两张量化表,一张亮度00一张色度01 |
64B | 实际数据 |
(5)帧图像开始SOF0(Start Of Frame)
SOF0 | 0xFF, 0xC0 | variable size | Start Of Frame (baseline DCT) |
---|
长度 | 含义 | 取值 |
---|---|---|
2B | SOF长度 | 0x00 11,不含0xFF C0,17字节 |
1B | 精度,表示每个数据样本的位数(通常是8位) | 0x08,8位 |
2B | 图像高度(单位:像素) | 0x04 00,1024像素数 |
2B | 图像宽度(单位:像素) | 0x04 00,1024像素数 |
1B | 颜色分量数(1:灰度图;3:YCrCb或YIQ;4:CMYK),JFIF中使用YCrCb,故该字段恒为3 | 0x03,YCrCb |
3B | Y颜色分量信息(颜色分量ID+水平与垂直采样因子+该分量使用的量化表ID) | 0x01 11 00 |
3B | Cr颜色分量信息(颜色分量ID+水平与垂直采样因子+该分量使用的量化表ID) | 0x02 11 01 |
3B | Cb颜色分量信息(颜色分量ID+水平与垂直采样因子+该分量使用的量化表ID) | 0x03 11 01 |
(6)Huffman表
(7)SOS
Short name | Bytes | Payload | Name |
---|---|---|---|
SOS | 0xFF, 0xDA | variable size | Start Of Scan |
三、JPEG相关代码调试
-
逐步调试JPEG解码器程序。将输入的JPG文件进行解码,将输出文件保存为可供YUVViewer观看的YUV文件。
-
程序调试过程中,应做到:
-
理解程序设计的整体框架
-
理解三个结构体的设计目的(两张量化表和四张哈夫曼码表)
struct huffman_table
struct component
struct jdec_private
-
理解在视音频编解码调试中TRACE的目的和含义
会打开和关闭TRACE,会根据自己的要求修改TRACE
-
-
以txt文件输出所有的量化矩阵和所有的HUFFMAN码表。
-
输出DC图像并统计其概率分布。
-
输出某一个AC值图像并统计其概率分布。
1. 分层结构(三个结构体)
JPEG压缩编码算法的一大特点就是采用了分层结构设计的思想,下面说明tinyjpeg-internal.h
中三个结构体:
struct huffman_table
struct huffman_table用来存储哈夫曼码表,分为快查找表和慢查找表,主要目的是提高解码效率。
/* tinyjpeg-internal.h */
//哈夫曼码表
struct huffman_table
{
/* Fast look up table, using HUFFMAN_HASH_NBITS bits we can have directly the symbol,
* if the symbol is <0, then we need to look into the tree table */
short int lookup[HUFFMAN_HASH_SIZE];
/* code size: give the number of bits of a symbol is encoded */
unsigned char code_size[HUFFMAN_HASH_SIZE];
/* some place to store value that is not encoded in the lookup table
* FIXME: Calculate if 256 value is enough to store all values
*/
uint16_t slowtable[16-HUFFMAN_HASH_NBITS][256];
};
struct component
struct component用来保存一个MCU(最小结构单元)的信息,每处理完一个MCU,值都会更新。储存当前8×8像块中有关解码的信息。
/* tinyjpeg-internal.h */
struct component
{
unsigned int Hfactor; // 水平采样因子
unsigned int Vfactor; // 垂直采样因子
float* Q_table; // 指向该8×8块使用的量化表
struct huffman_table *AC_table; // 指向该块使用的AC Huffman表
struct huffman_table *DC_table; // 指向该块使用的DC Huffman表
short int previous_DC; // 前一个块的直流DCT系数
short int DCT[64]; // DCT系数数组
#if SANITY_CHECK
unsigned int cid;
#endif
};
struct jdec_private
JPEG数据流结构体,用于存储JPEG图像宽高、数据流指针、Huffman码表等内容,并包含struct huffman_table
和struct component
。
定义了指向三个components的指针和三个components结构体;定义了图像的宽高、码流长度、始末指针,还有三张量化表(实际只用到两张,亮度+色度)、DC Huffman表和AC Huffman表各四张(实际各用两张:亮度DC、亮度AC、色度DC、色度AC)。
/* tinyjpeg-internal.h */
struct jdec_private
{
/* Public variables */
uint8_t *components[COMPONENTS]; /* 分别指向YUV三个分量的三个指针 */
unsigned int width, height; /* 图像宽高 */
unsigned int flags;
/* Private variables */
const unsigned char *stream_begin, *stream_end;
unsigned int stream_length;
const unsigned char *stream; /* 指向当前数据流的指针 */
unsigned int reservoir, nbits_in_reservoir;
struct component component_infos[COMPONENTS];
float Q_tables[COMPONENTS][64]; /* quantization tables */
struct huffman_table HTDC[HUFFMAN_TABLES]; /* DC huffman tables */
struct huffman_table HTAC[HUFFMAN_TABLES]; /* AC huffman tables */
int default_huffman_table_initialized;
int restart_interval;
int restarts_to_go; /* MCUs left in this restart interval */
int last_rst_marker_seen; /* Rst marker is incremented each time */
/* Temp space used after the IDCT to store each components */
uint8_t Y[64*4], Cr[64], Cb[64];
jmp_buf jump_state;
/* Internal Pointer use for colorspace conversion, do not modify it !!! */
uint8_t *plane[COMPONENTS];
};
2. 解码流程框架
loadjpeg.c
/* 读取JPEG文件,进行解码,并存储结果 */
int convert_one_image(const char *infilename, const char *outfilename, int output_format)
{
FILE *fp;
unsigned int length_of_file; // 文件大小
unsigned int width, height; // 图像宽、高
unsigned char *buf; // 缓冲区
struct jdec_private *jdec;
unsigned char *components[3];
/* 1. 将JPEG读入缓冲区 */
fp = fopen(infilename, "rb");
if (fp == NULL)
exitmessage("Cannot open filename\n");
length_of_file = filesize(fp);
buf = (unsigned char *)malloc(length_of_file + 4);
if (buf == NULL)
exitmessage("Not enough memory for loading file\n");
fread(buf, length_of_file, 1, fp);
fclose(fp);
/* Decompress it */
jdec = tinyjpeg_init(); // 初始化
if (jdec == NULL)
exitmessage("Not enough memory to alloc the structure need for decompressing\n");
/* 2. 解析JPEG文件头 */
if (tinyjpeg_parse_header(jdec, buf, length_of_file)<0)
exitmessage(tinyjpeg_get_errorstring(jdec));
/* 3. 计算图像宽高 */
tinyjpeg_get_size(jdec, &width, &height);
/* 4. 解码 */
snprintf(error_string, sizeof(error_string),"Decoding JPEG image...\n");
if (tinyjpeg_decode(jdec, output_format) < 0) // 解码实际数据
exitmessage(tinyjpeg_get_errorstring(jdec));
/*
* Get address for each plane (not only max 3 planes is supported), and
* depending of the output mode, only some components will be filled
* RGB: 1 plane, YUV420P: 3 planes, GREY: 1 plane
*/
tinyjpeg_get_components(jdec, components);
/* 5. 按照指定的输出格式保存输出文件 */
switch (output_format)
{
case TINYJPEG_FMT_RGB24:
case TINYJPEG_FMT_BGR24:
write_tga(outfilename, output_format, width, height, components);
break;
case TINYJPEG_FMT_YUV420P:
write_yuv(outfilename, width, height, components);
break;
case TINYJPEG_FMT_GREY:
write_pgm(outfilename, width, height, components);
break;
}
/* Only called this if the buffers were allocated by tinyjpeg_decode() */
tinyjpeg_free(jdec);
/* else called just free(jdec); */
free(buf);
return 0;
}
tinyjpeg.c
中包含子函数
(1)解析JPEG文件头函数——tinyjpeg_parse_header
- 解析marker标识——parse_JFIF
- 解析量化表DQT——parse_DQT
得到量化表长度(可能包含多张量化表);
得到量化表的精度;
得到及检查量化表的序号(只能是 0 —— 3);
得到量化表内容( 64 个数据)。
- 建立量化表——build_quantization_table
- 解析SOF——parse_SOF
得到每个 sample 的比特数、长宽、颜色分量数;
得到每个颜色分量的 ID、水平采样因子、垂直采样因子、使用的量化表序号(与 DQT 中序号对应)。
- 解析Huffman码表DHT——parse_DHT
得到 Huffman 表的类型( AC、 DC)、序号,依据数据重建 Huffman 表。
- 建立Huffman码表——build_huffman_table
- 解析SOS——parse_SOS
得到解析每个颜色分量的 DC、 AC 值所使用的 Huffman 表序号(与 DHT中序号对应)
(2) 解析JPEG实际数据——tinyjpeg_decode
依据每个分量的水平、垂直采样因子计算 MCU 的大小,并得到每个 MCU 中 8*8宏块的个数
(3)解析MCU——decode_MCU_2x2_3planes
对每个 MCU 解码(依照各分量水平、垂直采样因子对 MCU 中每个分量宏块解码)。
对每个宏块进行 Huffman 解码,得到 DCT 系数;
对每个宏块的 DCT 系数进行 IDCT,得到 Y、 Cb、 Cr;
遇到 Segment Marker RST 时,清空之前的 DC DCT 系数。
四、实验结果
1. 将输入的JPG文件进行解码,将输出文件保存为YUV文件
- 命令行参数
- 在
write_yuv()
中添加输出yuv文件的相关代码
/**
* Save a buffer in three files (.Y, .U, .V) useable by yuvsplittoppm
*/
static void write_yuv(const char *filename, int width, int height, unsigned char **components)
{
FILE *F;
char temp[1024];
snprintf(temp, 1024, "%s.Y", filename);
F = fopen(temp, "wb");
fwrite(components[0], width, height, F);
fclose(F);
snprintf(temp, 1024, "%s.U", filename);
F = fopen(temp, "wb");
fwrite(components[1], width*height/4, 1, F);
fclose(F);
snprintf(temp, 1024, "%s.V", filename);
F = fopen(temp, "wb");
fwrite(components[2], width*height/4, 1, F);
fclose(F);
//-----------------输出yuv文件---------------------//
snprintf(temp, 1024, "%s.YUV", filename);
F = fopen(temp, "wb");
fwrite(components[0], width, height, F);
fwrite(components[1], width * height / 4, 1, F);
fwrite(components[2], width * height / 4, 1, F);
fclose(F);
//-----------------输出yuv文件---------------------//
}
- YUV文件
out.YUV
2. 调试TRACE
-
目的:TRACE文件中包含了很多程序运行过程中的中间变量和解析(如Huffman码表)的情况,便于debug(这样远比打断点的效率要高)。
#if TRACE /* TRACE */ #endif
-
开关:1为打开TRACE,0为关闭TRACE。在
tinyjpeg.h
文件中,我们可以看到TRACE已经是处于打开的状态
#define TRACE 1 // 打开TRACE #define TRACEFILE "trace_jpeg.txt" // TRACE文件的文件名
-
执行程序后,得到的
trace_jpeg.txt
文件如下
3. 输出量化矩阵和Huffman码表
四张Huffman码表已有
两张量化表待填充
static void build_quantization_table(float *qtable, const unsigned char *ref_table)
{
/* Taken from libjpeg. Copyright Independent JPEG Group's LLM idct.
* For float AA&N IDCT method, divisors are equal to quantization
* coefficients scaled by scalefactor[row]*scalefactor[col], where
* scalefactor[0] = 1
* scalefactor[k] = cos(k*PI/16) * sqrt(2) for k=1..7
* We apply a further scale factor of 8.
* What's actually stored is 1/divisor so that the inner loop can
* use a multiplication rather than a division.
*/
int i, j;
static const double aanscalefactor[8] = {
1.0, 1.387039845, 1.306562965, 1.175875602,
1.0, 0.785694958, 0.541196100, 0.275899379
};
const unsigned char *zz = zigzag;
for (i=0; i<8; i++)
{
for (j=0; j<8; j++)
{
*qtable++ = ref_table[*zz++] * aanscalefactor[i] * aanscalefactor[j];
//------------------添加输出量化表的TRACE-------------------//
#if TRACE
*zz--;
fprintf(p_trace,"%-6d",ref_table[*zz++]);
if (j == 7)
{
fprintf(p_trace, "\n");
}
#endif
//-----------------添加输出量化表的TRACE--------------------//
}
}
}
4. 输出DC图像 和某一个AC值图像,并统计其概率分布
输出文件如下:
- 得到的DCT数组中存储了图像DCT变换后的数据(系数),DCT[0]为直流系数,其余为交流系数。
- 从图中可以看出,直流系数很大。DC系数的取值范围[-512,512],为了便于观察需要(+512单极化——>÷4压缩)映射到[0,255]。AC分量的值较小,+128处理。
dcImgBuff = (unsigned char)((priv->component_infos->DCT[0] + 512.0) / 4 + 0.5); // DCT[0]为DC系数;DC系数范围-512~512;变换到0~255
acImgBuff = (unsigned char)(priv->component_infos->DCT[1] + 128); // 选取DCT[1]作为AC的observation;+128便于观察
- 对于8x8的宏块,取每一个宏块DCT变换后左上角的第一个值为DC分量,输出图像的大小为128x128(原图像为1024x1024)。
- 根据能量守恒,DC系数的取值范围与DCT变换核矩阵的大小有关,比原信号更大。
- 实现了能量集中:前几个系数有值且比较大,后面的系数值趋于0,图像越会灰说明值为0(零电平已+128)。
// 计算概率分布函数
int main()
{
const int width = 256;
const int height = 256;
FILE* yuv = fopen("DCT2.yuv", "rb");// 原图
FILE* yfile = fopen("DCT2.txt", "w");// 原图概率分布
double* freq_y = (double*)malloc(sizeof(double) * 256);// 原图概率分布
unsigned char* ybuf = (unsigned char*)malloc(sizeof(unsigned char) * width * height); // 原图
unsigned char* ubuf = (unsigned char*)malloc(sizeof(unsigned char) * width * height * 0.25); //4:2:0
unsigned char* vbuf = (unsigned char*)malloc(sizeof(unsigned char) * width * height * 0.25);
fread(ybuf, sizeof(unsigned char), width * height, yuv);
fread(ubuf, sizeof(unsigned char), width * height * 0.25, yuv);
fread(vbuf, sizeof(unsigned char), width * height * 0.25, yuv);
int cout_y[256] = { 0 };
int cout_e[256] = { 0 };
for (int i = 0; i < width * height; i++) {
cout_y[ybuf[i]]++;
freq_y[ybuf[i]] = double(cout_y[ybuf[i]]) / (width * height);// 原图概率分布
}
for (int i = 0; i < 256; i++) {
fprintf(yfile, "%f\n", freq_y[i]);
}
fclose(yuv);
fclose(yfile);
}
五、总结
1. 变换编码的思想
输入原始信号,经过变换编码,输出一些系数表示。
- 去中心化:将相关的有记忆的信号,分解为互不相关的基向量的线性组合,组合系数互不相关,可以对各个系数单独量化(标量量化)。
- 稀疏化(能量集中):对原始信号只用少数幅值较大的系数表示。
- 可逆:可以重构输入信号。
2. 变换的理解
几何意义
矩阵运算中线性变换的本质是向量的旋转和伸缩
物理意义
- 能量的重新分配。
- 信号的分解与合成,一维是各基向量(各次谐波分量)的线性组合,二维基图像的线性组合。
- 离散余弦变换DCT变换核矩阵,各个元素为变换系数,决定对应位置的分量的幅值。
3. 变换的意义
将信号分解为不相关的基图像的线性组合,得到不相关的系数即消除了相关性。各个系数使用不同的量化器,也就是可以单独优化,误差可叠加,局部最优也是整体最优,将串联问题转换为可加性问题。
转化为每个量化器的比特分配Q问题,在码率R的约束条件下,最小化每一个量化误差D,找到使Dn最小的Qn,Dn(Qn)的关系。