Who are you?

JPEG函数库使用指南

这篇文档可以看成是Example那篇文档的一个辅助文档,若是要自己开发并将JPEG的解压缩功能用于自己的程序中,建议结合这两篇文档。更多细节参阅原文档。

概览

IJG JPEG库函数提供了C代码编写的程序用于读写JPEG压缩的图像文件,外围应用程序可以一次发送或是接收a scanline的未压缩的图像数据。另外,该函数库还可以解决颜色转换以及预处理/后置处理等问题。

IJG JPEG只能解决interchange JPEG数据流,特别是广泛使用的JFIF文件格式。它也可以被内嵌在更复杂的代码中用于处理interchange or abbreviated的JPEG数据流中。

库函数基本用法

数据格式

标准输入图像格式是像素点的矩形阵列,其中每一个像素有着相同数目的“组成”或说“样点”(颜色通道)。你必须指定每个像素点有多少组成以及这些组成成分的颜色空间解释。大部分的应用将会使用RGB数据(每一个像素点有3种颜色成分)或是灰度数据(每个像素点只有一个颜色成分)。必须强调的是RGB数据每个像素点有三个样点值,而灰度数据每个像素点为一个样点值。

在这没有提供颜色表的输入。JPEG文件通常都是全彩或是全灰的(或者有时也会是其他的颜色空间,例如CMYK)。我们可以通过扩展一个colormapped的图像到全颜色空间格式来实现将其颜色填满。然而由于抖动干扰的存在,JPEG通常对来自colormapped的源数据处理不是很好。

像素点通过scanlines存储,在每一个scanline中,从左到右排列。对于在每一个像素点中的组成成分,它们中行中是相邻排列的,例如对于24位的RGB颜色,其每一行排列为:R,G,B,R,G,B…每一个scanline是一个数据类型为JSAMPLE的数组,JSAMPLE类型通常表示无符号字符型,除非你改变了头文件jmorecfg.h。(我们也可以改变RGB像素点的排布方式,也就是R,G,B的排列方式,同样需要在jmorecfg.h中修改)。

通过一列指向scanlines开头的指针,一个2维像素点阵列就可以形成,因此这些scanlines在物理内存中是不需要相邻的。即使你每次只处理一条scanline,你也必须创建一个单个元素的指针数组来符合这个结构。指向JSAMPLE行的指针类型是JSAMPROW,而指向指针数组的指针类型位JSAMPARRAY。

在每一次调用过程中,库函数接收或是提供一个或是多个完整的scanlines。一次处理一行scanline的部分是不可能的。并且scanlines都是从上往下处理。如果一整个图像数据都在内存中,那么我们可以一次处理所有图像数据,但是最简单的还是一次处理一行scanline。

为了使结果最好,源数据数值的精度通过BITS_IN_JSAMPLE(通常是8位)来指定。例如,如果我们选择的数据是每通道6位的,那么我们应该在将数据传递给压缩器之前,将一个字节中每一个数值向左对齐。如果我们需要压缩的数据超过每通道8位 ,我们需要加入编译选项 BITS_IN_JSAMPLE=9 to 12(详情见Library compile-timoptions)

由解压缩器返回得到的数据格式与前面几乎相同,除了它对colormapped的数据输出也是支持的。(再次声明,一个JPEG文件是不能colormapped,但是我们可以让解压缩器通过颜色量化表产生colormapped output。)如果我们要求的是colormapped output,那么返回的数据阵列包含的是每个像素单个JSAMPLE,这个JSAMPLE的值是一个颜色表的索引。这个颜色表以一个2维的JSAMPARRAY呈现,其中每一行包含一个颜色组成的值,也就是说,colormap[i][j]表示的像素值(索引)j的第i个颜色成分的数值。注意,由于颜色表的索引值存储在JSAMPLEs中,颜色的最大数值是由JSAMPLE的大小限制的(例如,对于8位的JPEG库而言,最多 有256种颜色)。

JPEG压缩的具体细节

分配和初始化JPEG压缩对象

一个JPEG压缩对象就是一个struct jpeg_compress_struct。如果单个子例程序将执行整个JPEG压缩的话,那么这个结构体在调用的子例函数中可以作为一个局部变量。否则它可以使静态的会使从malloc()中分配。

我们同时还需要定义一个错误处理的结构体对象,即struct jpeg_error_mgr。如果我们想用自己的错误处理方式,我们一般需要将上述结构体嵌入到一个 更大的结构体当中(详情可见“Error handling”)。默认的错误处理方式将会打印出错误/警告信息,并且在发送严重错误后调用exit()退出。

我们必须初始化错误处理结构体,之后调用jpeg_create_compress来初始化JPEG对象的其他部分。

如果我们使用默认处理方式,那么这部分的代码应为

1
2
3
4
5
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
...
cinfo.err=jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);

Tips:调用jpeg_create_compress将会分配一小块内存,因此如果内存溢出的话它将会分配失败。在那种情况下,通过错误处理句柄,程序就会退出,这就是为什么错误处理方式必须首先初始化。

指定压缩数据的目的地(例如某个文件)

JPEG库函数将会把压缩数据传送到一个数据目的地模块中,这个模块知道如何将数据写入到标准输入输出流中。如果我们想进行其他操作的话,我们可以使用我们自己的目的地模块。但是在jpeg_start_compressjpeg_finish_compress之间我们不能改变数据目的地。

如果我们使用标准的目的地模块,我们首先必须打开目标输入输出流,对于这一步典型的代码应为:

1
2
3
4
5
6
7
FILE * outfile;
...
if((outfile=fopen(filename,"wb"))==NULL){
fprintf(stderr,"can't open %s\n",filename);
exit(1);
}
jpeg_stdio_dest(&cinfo,outfile);

其中最后一行调用了标准目的地模块。

设定压缩参数,包括图像大小以及颜色空间等

在开始压缩数据之前必须要为JPEG对象(cinfo structure)指定几个参数和缺省参数。

设定缺省参数之前需要指定的几个参数是:

  • 图像宽度:cinfo.image_width,
  • 图像高度:cinfo.image_height,JPEG支持的宽度和高度在两个方向上可为1~64K,
  • 图像的颜色通道数:cinfo.input_components(比如RGB图像为3,灰度图为1),
  • 图像颜色空间:cinfo.in_color_space(比如真彩色 JCS_RGB,灰度图JCS_GRAYSCALE)。

JPEG有大量的压缩参数可供选择,但是大部分应用并不需要知道这所有的参数,因此我们可以通过调动jpeg_set_defaults()来将参数全部设置为合理值,然后,如果我们想改变某个特定参数,我们就可以在其后进行改变。

注意:在调用jpeg_set_defaults()之前我们必须设置cinfo.in_color_space,因为默认设置依赖于源图像的颜色空间。但是其他三个参数直到调用jpeg_start_compress之前都不是必须的。另外,多次调用jpeg_set_defaults()是允许的。

对于24位的RGB源图像,典型的代码如下:

1
2
3
4
5
6
cinfo.image_width = Width; /* image width and height, in pixels */
cinfo.image_height = Height;
cinfo.input_components = 3; /* # of color components per pixel */
cinfo.in_color_space = JCS_RGB; /* colorspace of input image */
jpeg_set_defaults(&cinfo);
/* Make optional parameter settings here */

开始压缩:jpeg_start_compress(…)

调用jpeg_start_compress()将会开始压缩循环,这将会初始化一个内部状态,分配一个工作内存,并且发送一些JPEG数据流头部的字节内容。

这一部分的典型代码为:

1
jpeg_start_compress(&cinfo, TRUE);

注意:“TRUE”将会确保一个完整的可互换的数据流将被写入。另外,一旦调用了上述函数开始压缩,我们就不能改变任何JPEG的参数或是JPEG对象的某些域值,直到压缩循环结束。

压缩循环操作

我们通过调用jpeg_write_scanlines()可以一次写入一条或多条scanlines,在大部分应用中,一般以一次一条或几条为宜。另外,图像数据将按照从上到下的顺序写入(出于兼容性考虑)。

在JPEG对象的next_scanline域中存有到目前位置已经写入的scanlines的数目,因此我们可以直接使用该变量作为循环变量。

这一部分的典型代码位:

1
2
3
4
5
6
7
JSAMPROW row_pointer[1]; /* pointer to a single row */
int row_stride; /* physical row width in buffer */
row_stride = image_width * 3; /* JSAMPLEs per row in image_buffer */
while (cinfo.next_scanline < cinfo.image_height) {
row_pointer[0] = & image_buffer[cinfo.next_scanline * row_stride];
jpeg_write_scanlines(&cinfo, row_pointer, 1);
}

jpeg_write_scanlines()的返回值是实际写入的scanlines的数目,它等同于传递进去的scanlines的数目,所以通常我们可以忽略这个返回值,但在两种情况下它们有所不同(详见libjpeg.txt)。

结束压缩:jpeg_finish_compress(…)

在所有的图像数据都被写入以后,调用jpeg_finish_compress来完成压缩循环。同时它也会释放与JPEG对象相联系的工作内存。

这一部分的典型代码为:

1
jpeg_finish_compress(&cinfo);

注意:

  • 如果我们的模式是multi-pass模式,例如HUffman 编码优化,上述函数将会使用第一次传递的缓存数据执行额外的传递操作。在这种情况下,调用这个函数将会花费较长的时间完成操作。但是在默认参数设置下,这种情况将不会发生。
  • 在写入全部数目的scanlines之前调用上述函数将会发生错误,如果希望退出压缩,我们应该调用jpeg_abort()
  • 在完成压缩之后,我们可以将JPEG对象释放掉,也可以将它用于压缩其他图像,在那种情况下,我们只需重复步骤2,3,4。如果我们不改变目的地管理器,那么新的数据流将会被写入相同的目标文件中;如果我们不改变任何JPEG参数,新的数据就将会按照之前的参数设置写入。如果我们改变了颜色空间,我们需要调用jpeg_set_defaults()来适应新的颜色空间,进而我们需要重复第三部的所有操作。

释放JPEG压缩对象

我们可以通过调用jpeg_destroy_compress()来释放JPEG压缩对象,它将会释放所有的附属内存。或者我们也可通过调用jpeg_destroy()来释放,这个函数既可以使用与JPEG压缩对象也可以是解压缩对象。

这一部分的典型代码为:

1
jpeg_destroy_compress(&cinfo);

中止退出

如果我们想在压缩结束之前就中止循环,我们可以通过以下任意一种方式来清理内存:

  • 如果我们不再需要JPEG压缩对象,我们就像步骤7一样,直接释放内存。这在调用了jpeg_start_compress()之后都是合理的;
  • 如我我们打算重复利用JPEG对象,那么就调用jpeg_abort_compress()或者是jpeg_abort()。这将会使对象回到一个空闲状态,并且释放掉了所有的工作内存。在对象成功创建之后,任何时候调用jpeg_abort()都是允许的。

JPEG解压缩具体细节

分配和初始化JPEG解压缩对象

同初始化JPEG压缩对象类似,除了使用的结构体不同,错误处理方式也与前面的相同。另外,在IJG代码中,我们一般都会使用cinfo作为压缩和解压缩对象。

这一部分的典型代码:

1
2
3
4
5
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
...
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_decompress(&cinfo);

指定压缩数据的来源(例如一个文件)

正如前面所说的,JPEG库将会从数据源模块中读取压缩数据。函数库中已经包含了一个能从标准输入输出流中读取数据的数据源模块。如果我们使用标准数据源模块,我们必须先打开数据源的标准输入输出数据流。

这一步的典型代码如下:

1
2
3
4
5
6
7
FILE * infile;
...
if ((infile = fopen(filename, "rb")) == NULL) {
fprintf(stderr, "can't open %s\n", filename);
exit(1);
}
jpeg_stdio_src(&cinfo, infile); //invoke the standard source modue

Tip:在调用jpeg_read_header()之后和jpeg_finish_decompress()之前,我们一般不能改变数据流的来源。因此,如果我们从单一数据源文件中读取一系列JPEG文件,我们需要重复jpeg_read_header()jpeg_finish_decompress()之间的代码,但是不需要重新初始化JPEG对象以及数据源模块(也就是不需要改变输入数据的文件)

调用jpeg_read_header()获得图片的信息

该函数将会从数据数据中读取图像中的头文件部分,直到压缩数据的开始位置。返回得到的图像大小以及其他信息将会存储在JPEG对象中,也就是cinfo

这一部分的典型代码为:

1
jpeg_raed_header(&cinfo, TRUE);

Tip:如果我们只想获得图片的一些信息,那么我们可以在这一步就中止,我们需要调用jpeg_destroy()或是jpeg_abort()来完成这一操作

设置解压缩参数

在调用上上述步骤的函数后,jpeg_raed_header()已经设置了合适的默认解压缩参数(基于其获取到的图片信息)。如果我们想更改一些默认参数,我们可以在这一步设置。具体的设置可以参见Decompression parameter selection

开始解压缩:jpeg_start_decompress()

调用以该函数后,它将会初始化程序内部状态,分配工作内存以及准备返回值。

这一步的典型代码为:

1
jpeg_start_decompress(&cinfo);

另外,在这次调用之后,关于图像的所有信息就保存到了JPEG对象中,这些信息包括有:

  • output_width:图像的宽度
  • output_height:图像的高度
  • out_color_components:# of color components in out_color_space
  • output_components:每一个像素的颜色成分
  • colormap: the selected colormap, if any
  • actual_number_of_colors: number of entries in colormap

Tip:在调用该函数前,图像的各种信息是无法获取的,不过我们也可以在设置完解压缩参数后,调用jpeg_calc_output_dimensions()来使这些参数可用。另外,我们在这一步也需要分配缓存空间,具体可以参见Example-of-IJG-Code中的解压缩部分

解压缩循环操作

通过多次调用jpeg_read_scanlines()将解压缩数据读入内存中,返回值是实际读入的scanline的数目。另外,对于灰度JPEG图和彩色JPEG图,其返回的数据格式是不一样的。

这一部分的典型代码为:

1
2
3
4
5
6
while(cinfo.output_scanline<cinfo.output_height){
(void)jpeg_read_scanlines(&cinfo,buffer,1);
/* 假定put_scanline_someplace需要一个指针和样点数
* do something else */
put_scanline_someplace(buffer[0],row_stride);
}

结束解压缩:jpeg_finish_decompress()

调用上述函数将会完成解压缩循环,同时释放工作内存。

这一部分的典型代码为:

1
jpeg_finish_decompress(&cinfo);

其余注意事项同结束压缩。

释放JPEG解压缩对象

这一部分的典型代码为:

1
jpeg_destroy_decompress(&cinfo);

中止退出

同压缩部分

其他

关于ADVANCED FEATURES这一部分建议参阅原文档

参考: