实验六、JPEG原理分析及JPEG解码器的调试
一、概述
1.实验目的:在分析清楚JPEG文件结构的基础上,调试JPEG解码器,理解JPEG编解码的原理以及具体的编程实现。
2.文章概述:本文将先从JPEG文件结构着手,再介绍JPEG编解码的流程,最后结合解码器程序来分析其具体实现并给出调试结果。
二、JPEG文件结构
1.为什么先谈JPEG编码?
想对JPEG文件进行解码,最最重要的便是了解其文件的结构。然而,不先了解编码的流程是很难以领会JPEG为什么要采用这样的文件结构。所以我觉得在文章的开头简单介绍一下JPEG编码的流程会能帮助读者更快的理解JPEG的精髓。
2.JPEG编码的简单介绍
在这里我简单介绍一下JPEG的编码流程,以之作为接下来介绍JPEG文件结构的铺垫。当然,由于是简单概述,很多技术细节会有谬误与疏漏,还请大家指出。
(1)彩色空间转换
一张图像进入JPEG编码器后,编码器将会对该图像进行彩色空间转换,将之转换到Y,Cb,Cr的彩色空间中。转换到YCbCr彩色空间的好处是,人眼对亮度信息更为敏感。在YCbCr空间中,对冗余的色度进行压缩,能够保证图像质量不甚下降而获得更好的压缩比。
(2)8*8分块与DCT变换
之后,编码器将对图像进行8*8的分块,以便进行之后的DCT(离散余弦变换),经DCT变换后,8*8的图像块将变为8*8的DCT系数块,而之后的操作便是针对这些DCT系数做的了。值得注意的是,当图像的宽高并不是8的倍数时,可对图像进行补0补至8的倍数,并在之后的解码中将这些0去掉即可。
(3)DCT变换后的量化
DCT变换后,去除了8*8DCT系数块将不同系数的相关性,如此,利用人眼对高频细节和色度细节不甚敏感的特性,我们能够对高频的AC分量以及色度分量进行粗量化而对低频的DC分量以及亮度信息进行细量化,从而获得更高的压缩比。
(4)编码
之后我们对DC系数进行DPCM编码,并利用量化后AC系数0值较多的特点对AC系数进行RLE游程编码。
DC系数与每个AC系数都会经过过霍夫曼编码获得各自的码表。
(5)总结
简单概述了一下编码的流程,我们可以看出编码会带来哪些对文件结构的需求。首先图像的宽高采样精度等基本信息是肯定会有的,此外,对8*8分块不同的量化需要的量化系数需要保存在JPEG文件中,而不同DCT系数所对应的霍夫曼码表也是需要保存在JPEG文件中的。
当然,由编解码带来的文件格式要求远远不止这么简单。但掌握这些概念已经能让我们快速产生一个对JPEG文件结构认知并能在此基础上调试JPEG解码器了。
3.JPEG文件结构的分析——段及段的类型
JPEG文件其基本的数据组织方式是段。而段的划分则是由段标记码来划分的,这样的标记码有数十种。
且一个以JFIF(JPEG File Interchange Format,JPEG文件交换格式)格式存储的JPEG文件,其段标记码往往以如下的方式出现。
首先是SOI(Start Of Image,0xFFD8),标识一个图像的开始,再便是APP0(application0,0xFFE0),之后便给出量化表DQT(Define Quantization Table,0xFFDB);
然后进入下一层的内容SOF(Start Of Frame,0xFFC0),给出其对应的霍夫曼码表DHT(Define Huffman Table,0xFFC4);
最后,进入扫描层SOS(Start Of Scan,0xFFDA),值得注意的是扫描层中为了防止DPCM的差错无限制的传递,还采用了DRI(Define Restart Interval,0xFFDD),来重置差分编码。而除此之外,JPEG的数据可视为三层,分别为图像层(image),帧层(frame)与扫描(scan)层。
接下来我们将用二进制浏览器打开一个典型的JPEG文件,亲自看一看JPEG文件结构的组织方式。
通过查找FF标识符,我们可以定位段标识符的位置(值得注意的是0xFF00并不是一个段标识符,而是JPEG标准中规定的一种比较特别的情况。)。
例如我们需要对这个文档的APP0(0xFFE0)段进行解读,首先,我们需要找到JPEG相关的技术文档《JPEG压缩编码标准》或者相关的其他文章作为参考。
我们给出的示例JPEG文件的APP0段的数据是这样的:
FFE0 0010 4A46 4946
0001 0100 0001 0001
0000
标记代码(2字节):0xFFE0 即APP0段,该段保存着文件应用相关的信息(APPn段可能保存拍摄该图片的相机的参数,甚至PS的版本信息等)
数据长度(2字节): 0x0010 即16字节
标示符(5字节): 0x4A46494600 即JFIF0
版本号(2字节): 0x0101 即1.1版本
X,Y方向密度单位(1字节): 0x00 即无单位
X,Y方向像素密度(2字节 + 2字节): 0x0001 0x0001 即X,Y方向像素密度都为1
缩略图RGB位图信息(2字节):0x0000 没有缩略图故为0
对其他段信息的解读也是类似的工作,只要有耐心,对照着参考资料,怎样的JPEG文件信息我们都是能够解读的。而当我们可以通过二进制获取JPEG文件的相关信息时,解码已经是水到渠成的工作了。接下来我们只需要实现相应的编码器即可。
三、JPEG解码器程序的分析
1.JPEG编码器所需的功能
在上面的一章节内容中,我们了解了JPEG的编码过程以及JPEG文件结构的组织。有了这部分知识的铺垫,我们很快就能够想到JPEG编码器要有的功能。首先,能读JPEG文件,提取JPEG文件中的自包含的信息以及已压缩的数据流;其次,便是能够根据这些自包含的信息解压缩的部分。
在实验二中我们已经实现了提取一个文件中自包含信息的功能;而之后的实验,我们对Huffman编码以及DPCM都有所涉猎,因此我打算重点分析一下DCT变换这一部分内容。
2.根据具体的程序分析解码过程
(1)mian()
主函数首先通过命令行,选择对应的输出图像格式,并将之作为参数传入响应的函数
convert_one_image(input_filename, output_filename, output_format);//转换一张图像
我们仅解码一张图像,故会选择,output_format变量为TINYJPEG_FMT_YUV420P/**
* main
*
*/
int main(int argc, char *argv[])
{
int output_format = TINYJPEG_FMT_YUV420P;
char *output_filename, *input_filename;
clock_t start_time, finish_time;
unsigned int duration;
int current_argument;
int benchmark_mode = 0;
#if TRACE
p_trace=fopen(TRACEFILE,"w");
if (p_trace==NULL)
{
printf("trace file open error!");
}
#endif
if (argc < 3)
usage();
current_argument = 1;
while (1)
{
if (strcmp(argv[current_argument], "--benchmark")==0)
benchmark_mode = 1;
else
break;
current_argument++;
}
if (argc < current_argument+2)
usage();
//根据命令行选择输出图像格式
input_filename = argv[current_argument];
if (strcmp(argv[current_argument+1],"yuv420p")==0)
output_format = TINYJPEG_FMT_YUV420P;
else if (strcmp(argv[current_argument+1],"rgb24")==0)
output_format = TINYJPEG_FMT_RGB24;
else if (strcmp(argv[current_argument+1],"bgr24")==0)
output_format = TINYJPEG_FMT_BGR24;
else if (strcmp(argv[current_argument+1],"grey")==0)
output_format = TINYJPEG_FMT_GREY;
else
exitmessage("Bad format: need to be one of yuv420p, rgb24, bgr24, grey\n");
output_filename = argv[current_argument+2];
start_time = clock();
if (benchmark_mode)
load_multiple_times(input_filename, output_filename, output_format);
else
convert_one_image(input_filename, output_filename, output_format);//转换一张图像
finish_time = clock();
duration = finish_time - start_time;
snprintf(error_string, sizeof(error_string),"Decoding finished in %u ticks\n", duration);
#if TRACE
fclose(p_trace);
#endif
return 0;
}