 | 级别: 初级 Michael Still (mikal@stillhq.com), 高级软件工程师, Tower Software Engineering
2002 年 3 月 01 日 TIFF 是一种极其常见但又相当复杂的光栅图像格式。Libtiff 是一种 TIFF 规范的标准 ANSI C 实现,它是免费的并且可以在许多操作系统上工作。本文讨论了 TIFF 的一些缺陷并指导您使用 libtiff 库。本文还演示了如何将 libtiff 用于绘制黑白图像的示例。
TIFF(标记图像文件格式(Tag Image File Format))是一种最初由 Adobe 提出的光栅图像格式。 光栅图像格式将图片存储为描述像素状态的位图,而不是记录图元(譬如,线和曲线)的长度和位置。Libtiff 是 TIFF 规范的标准实现之一,目前它因其速度、强大的功能和源代码的易于获取而被广泛使用。
本文专注于黑白 TIFF 图像;以后可能会有文章讨论彩色图像。
TIFF 挑战
大多数文件格式规范定义文件表示的一些基本规则。 例如,PNG 文档(TIFF 的一个竞争者)始终使用大尾数法。然而,TIFF 没有这种规定。下面的示例列出了某些看起来很基本而 TIFF 并未定义的内容:
- 字节次序:大尾数法还是小尾数法
- 图像字节内的位填充次序:最高位首先填充还是最低位首先填充
- 给定的黑白象素值的含义:0 代表黑还是白?
创建 TIFF 文件很容易,因为几乎不需要对您已有的数据进行任何转换。 另一方面,它也意味着读入其它应用程序创建的随机 TIFF 会很困难 ― 所以您必须编码所有可能的组合,以确信得到可靠的产品。
那么,如何编写一个应用程序来读入 TIFF 格式的所有可能的不同排列呢? 最重要的是要记住
决不要假设正在读入的图像数据的格式。
写 TIFF 文件
首先,我将向您演示如何写出 TIFF 文件。然后,我将演示如何将 TIFF 文件读回程序中。
写操作的基础设施
传统上, 位图在代码内由一个字符数组表示。这是因为在大多数操作系统上,一个字符能很好地映射成一个字节。 在清单 1 中,我设置了 libtiff 并创建一个简单的缓冲区,使之包含我稍后可以写到磁盘上的图像。 您可以下载该代码,作为 write-infrastructure.c(请参阅本文后面的
参考资料)。
清单 1. 设置基础设施(write-infrastructure.c)
#include <stdio.h>
#include <tiffio.h>
int
main (int argc, char *argv[])
{
char buffer[32 * 9];
}
|
上面的代码相当简单。要使用 libtiff,只需要包含 tiffio.h 头文件。要编译它, 请使用命令
gcc foo.c -o foo -ltiff -lm 。
-ltiff 是包含名为 libtiff 的库的命令,该库必需在库路径中。一旦开始显式地指定库, 还必需添加
-lm ,它是数学库。这里定义的 char buffer 将是黑白图像, 所以我们接下来定义其中的一个。
写图像
为了凑足那个令人生厌的示例, 我现在很乐意向您提供迄今为止可能是画得最差的悉尼港大桥的图片。在清单 2 中,图像已经在图像缓冲区中,我们只需将它保存到磁盘上的文件中。 该示例先以写方式打开一个 TIFF 图像,然后将该图像放到该文件中。
请注意,为了清晰起见,我省略了该图像的实际十六进制值;如果对它们感兴趣, 可以从这段代码的可下载版本 write.c(请参阅
参考资料)中获得它们。
清单 2. 写操作的代码(write.c)
#include <stdio.h>
#include <tiffio.h>
int main(int argc, char *argv[]){
// Define an image
char buffer[25 * 144] = { /* boring hex omitted */ };
TIFF *image;
// Open the TIFF file
if((image = TIFFOpen("output.tif", "w")) == NULL){
printf("Could not open output.tif for writing\n");
exit(42);
}
// We need to set some values for basic tags before we can add any data
TIFFSetField(image, TIFFTAG_IMAGEWIDTH, 25 * 8);
TIFFSetField(image, TIFFTAG_IMAGELENGTH, 144);
TIFFSetField(image, TIFFTAG_BITSPERSAMPLE, 1);
TIFFSetField(image, TIFFTAG_SAMPLESPERPIXEL, 1);
TIFFSetField(image, TIFFTAG_ROWSPERSTRIP, 144);
TIFFSetField(image, TIFFTAG_COMPRESSION, COMPRESSION_CCITTFAX4);
TIFFSetField(image, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISWHITE);
TIFFSetField(image, TIFFTAG_FILLORDER, FILLORDER_MSB2LSB);
TIFFSetField(image, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
TIFFSetField(image, TIFFTAG_XRESOLUTION, 150.0);
TIFFSetField(image, TIFFTAG_YRESOLUTION, 150.0);
TIFFSetField(image, TIFFTAG_RESOLUTIONUNIT, RESUNIT_INCH);
// Write the information to the file
TIFFWriteEncodedStrip(image, 0, buffer, 25 * 144);
// Close the file
TIFFClose(image);
}
|
(在我的 Linux 机器上使用
xview 命令将不能显示输出图像。事实上, 我还没发现哪一个以 group 4 fax 方式压缩的黑白图像可以使用那个程序显示出来。 在发表本文时,我仍无法找到这个程序的修正。)
无论如何,样本代码演示了使用 libtiff API 的基本要素。值得注意的几点是:
- 提供给 libtiff 和从 libtiff 返回的缓冲区都是每个字节中包含 8 个像素。 因而,您必须能够抽取您所感兴趣的像素。在这里使用掩码、右移和左移运算符很方便。
-
TIFFOpen 函数与您熟悉的
fopen 函数非常相似。
- 在开始将图像写出之前,我们需要为相当多的字段设置值。 这些字段为 libtiff 提供有关图像大小和形状的信息,以及在图像内压缩数据的方法。 在开始将图像数据交给 libtiff 之前,需要设置这些字段。还可以为许多其它字段设置值; 在本示例中,我使用了接近最少数量的字段。
-
TIFFWriteEncodedStrip 是实际将图像插入文件的函数调用。这个调用将未压缩的图像数据插入文件。 这意味着在将图像数据写入文件之前,libtiff 将先对它进行压缩。 如果您已经对数据进行压缩,那么可以看一看
TIFFWriteRawStrip 。
- 最后,用
TIFFClose 关闭文件。
如果您对悉尼港大桥的外观感到好奇,可以看一下
图 1,它是该图片的副本。 (我不得不作弊并将它转换成 JPEG,因为大多数 Web 浏览器不支持 TIFF。)
 |
有关 libtiff 函数调用的更多信息
如果您需要有关本文中提到的任何 libtiff 函数调用的更多信息,请参阅与该库一起提供的详尽的帮助页。
请记住,帮助手册页面中的大小写很重要,所以需要正确拼写函数名中的字母大小写。 例如,它是
TIFFOpen ,而不是
tiffopen 。
|
|
图 1. Michael Still 绘制的悉尼港大桥
读 TIFF 文件
可靠地读 TIFF 文件比起写它们要困难得多。 遗憾的是,本文篇幅有限,无法讨论所有的重要问题,所以一些问题需要留到以后的文章中讨论。Web 上也有大量讨论有关问题的页面。我所喜爱的一些页面包括在本文末尾的
参考资料一节中。
使读取黑白 TIFF 图像变得最复杂的问题是 TIFF 文件本身中可能存在的几种不同存储模式。Libtiff 不阻止您使用这些模式,所以您必须能够自己处理它们。TIFF 支持的三种模式是单条图像、条纹图像和平铺图像:
单条图像
象它的名称表述的那样,这是条纹图像的特例。在这种情况中,所有位图都存储在一个大的块中。 我在 Windows 机器上体验过单条图像的可靠性问题。通常建议一个未压缩的条不要占据 8 K 字节以上, 黑白图像限制为单条中最多为 65536 个像素。
条纹(或多条)图像
图像的水平块存储在一起。多个条垂直地连接以形成完整的位图。
图 2演示了这个概念。
图 2. 悉尼港大桥的条纹图像
平铺图像
象盥洗室墙壁那样,它是由瓷砖组成的。
图 3演示了这种表示法, 它可用于非常大的图像。当您想在任何时候只操纵图像的一小部分时,平铺特别有用。
图 3. 悉尼港大桥的平铺图像
平铺图像不太常见,所以在本文中我将集中讨论条纹图像。请记住,单条图像只是多条图像的子集。
读操作的基础设施
读入 TIFF 图像时要记住的最重要的事情是要灵活。读操作示例(下面的清单 3)的基本概念与写操作示例(上面的清单 2) 相同,主要区别在于读操作示例需要处理许多可能的输入图像。除了条纹和平铺之外, 灵活性的最重要事情是光度解释(photometric interpretation)。幸运的是,对于黑白图像,只要考虑两种光度解释; 而对于彩色及某种程度的灰度级图像,则有更多的光度解释。
什么是
光度解释?缓冲区中图像的表示法确实是一件很随意的事情。 我可能对位图进行编码,让 0 表示黑色(
TIFFTAG_MINISBLACK ), 而您可能喜欢用 1 表示黑色(
TIFFTAG_MINISWHITE )。TIFF 允许这两种表示, 所以代码必须能够处理这两种情况。 在下面的示例中,我假设了内部缓冲区必需是
MINISWHITE 形式, 所以我们要转换
MINISBLACK 形式的图像。
另一件要记住的大事情是
填充次序,即字节中的第一位是最高位的值还是最低位的值。 清单 3 也正确地处理了这两种情况。我假设了缓冲区的第一位是最高位的值。TIFF 图像可以是大尾数法也可以是小尾数法,但 libtiff 会为我们处理。 感到欣慰的是,libtiff 还支持各种压缩算法,所以您不必担心它们。目前为止, 这些都是 TIFF 的最令人头疼之处,所以仍值得花时间来使用 libtiff。清单 3 是可下载的 read.c(请参阅
参考资料)。
清单 3. 读操作的代码(read.c)
#include <stdio.h>
#include <tiffio.h>
int main(int argc, char *argv[]){
TIFF *image;
uint16 photo, bps, spp, fillorder;
uint32 width;
tsize_t stripSize;
unsigned long imageOffset, result;
int stripMax, stripCount;
char *buffer, tempbyte;
unsigned long bufferSize, count;
// Open the TIFF image
if((image = TIFFOpen(argv[1], "r")) == NULL){
fprintf(stderr, "Could not open incoming image\n");
exit(42);
}
// Check that it is of a type that we support
if((TIFFGetField(image, TIFFTAG_BITSPERSAMPLE, &bps) == 0) || (bps != 1)){
fprintf(stderr, "Either undefined or unsupported number of bits per sample\n");
exit(42);
}
if((TIFFGetField(image, TIFFTAG_SAMPLESPERPIXEL, &spp) == 0) || (spp != 1)){
fprintf(stderr, "Either undefined or unsupported number of samples per pixel\n");
exit(42);
}
// Read in the possibly multiple strips
stripSize = TIFFStripSize (image);
stripMax = TIFFNumberOfStrips (image);
imageOffset = 0;
bufferSize = TIFFNumberOfStrips (image) * stripSize;
if((buffer = (char *) malloc(bufferSize)) == NULL){
fprintf(stderr, "Could not allocate enough memory for the uncompressed image\n");
exit(42);
}
for (stripCount = 0; stripCount < stripMax; stripCount++){
if((result = TIFFReadEncodedStrip (image, stripCount,
buffer + imageOffset,
stripSize)) == -1){
fprintf(stderr, "Read error on input strip number %d\n", stripCount);
exit(42);
}
imageOffset += result;
}
// Deal with photometric interpretations
if(TIFFGetField(image, TIFFTAG_PHOTOMETRIC, &photo) == 0){
fprintf(stderr, "Image has an undefined photometric interpretation\n");
exit(42);
}
if(photo != PHOTOMETRIC_MINISWHITE){
// Flip bits
printf("Fixing the photometric interpretation\n");
for(count = 0; count < bufferSize; count++)
buffer[count] = ~buffer[count];
}
// Deal with fillorder
if(TIFFGetField(image, TIFFTAG_FILLORDER, &fillorder) == 0){
fprintf(stderr, "Image has an undefined fillorder\n");
exit(42);
}
if(fillorder != FILLORDER_MSB2LSB){
// We need to swap bits -- ABCDEFGH becomes HGFEDCBA
printf("Fixing the fillorder\n");
for(count = 0; count < bufferSize; count++){
tempbyte = 0;
if(buffer[count] & 128) tempbyte += 1;
if(buffer[count] & 64) tempbyte += 2;
if(buffer[count] & 32) tempbyte += 4;
if(buffer[count] & 16) tempbyte += 8;
if(buffer[count] & 8) tempbyte += 16;
if(buffer[count] & 4) tempbyte += 32;
if(buffer[count] & 2) tempbyte += 64;
if(buffer[count] & 1) tempbyte += 128;
buffer[count] = tempbyte;
}
}
// Do whatever it is we do with the buffer -- we dump it in hex
if(TIFFGetField(image, TIFFTAG_IMAGEWIDTH, &width) == 0){
fprintf(stderr, "Image does not define its width\n");
exit(42);
}
for(count = 0; count < bufferSize; count++){
printf("%02x", (unsigned char) buffer[count]);
if((count + 1) % (width / 8) == 0) printf("\n");
else printf(" ");
}
TIFFClose(image);
}
|
这段代码先打开图像并检查我们是否可以处理它。然后,它读入图像的所有条并将它们一起附加到一个大的内存块中。 如果需要,它还翻转位,直到光度解释是我们可以处理的那个, 而且如果填充次序不对,它还进行必要的位交换处理。 最后,我们的样本将图像输出为由十六进制值组成的一系列行。请记住,每个值都表示实际图像中的 8 个像素。
结束语
在本文中, 我向您演示了如何使用 libtiff 来读写一些简单的黑白图像,并介绍了一些您要知道的关键问题。 在开始用 libtiff 编码之前,请记住要考虑应该对您的图像使用什么压缩算法 ― 对于黑白色,group 4 fax 很合适, 但对于彩色使用什么压缩算法确实取决于您的需要。
参考资料
关于作者  | |  | Michael Still 已在图像处理领域工作了好多年,包括为澳大利亚政府部门管理和开发大型图像数据库的几年。 目前,他正在为 Tower Software 工作,该公司制造名为 TRIM 的世界领先的 EDMS 和唱片管理包。Michael 还是 Panda(一种开放源码的 PDF 生成 API)的开发人员,以及 comp.text.pdf USENET FAQ 文档的维护者。 可以通过
mikal@stillhq.com与 Michael 联系。
|
对本文的评价
|  |