IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Linux  >

用 libtiff 进行图形编程,第 2 部分

现在要上点色

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

Michael Still (mikal@stillhq.com), 高级软件工程师, Tower Software Engineering

2002 年 6 月 01 日

TIFF 是极为普遍但却很复杂的光栅图像格式。Libtiff 是 TIFF 规范的标准实现,它免费而且可以在许多操作系统上运行。本文将向您展示如何将 libtiff 用于灰度和彩色图像做图。

本文继续 前一篇关于利用 libtiff 编程实现黑白图形的文章讲述如何构成灰度和彩色图像。我们假设您已经读过并理解了有关黑白图像的那篇文章中的代码。

首先,我们要回顾一下关于如何存储彩色和灰度图像数据的理论。这个理论适用于所有成像格式。然后会讲讲使用 libtiff 的细节。

几个术语

图像是由像素组成的。在黑白成像中,像素将是 0 和 1 两个值中的一个。这可以用一个比特位来表示。但是,对于灰度和彩色图像,像素需要存储范围更大的值;如果像素有 255 级灰度,那么我们需要 8 位来存储该像素。每个值都叫做一个样本。TIFF 在名为 TIFFTAG_BITSPERSAMPLE 的标记中表达值的大小。黑白的会是 1,而灰度就要大些的数字。

对于彩色图像,我们甚至需要存储更多的信息。每个像素,我们需要存储红、绿和蓝的值。这些值中的每个都存在独立的 样本中。因此,我们需要定义 TIFFTAG_SAMPLESPERPIXEL 。对于黑白或灰度来说是 1,但对于彩色图像通常会是 3。我们还需要定义每个样本的大小,因此我们仍需要设定 TIFFTAG_BITSPERSAMPLE 的值。





回页首


彩色和灰度存储理论

我们需要首先理解图像数据在内存中的格式才能够支持彩色和灰度图像。彩色和灰度图像有两类主要的表示方法。我将通过描述灰度进行解释,然后再将其扩展到彩色。

直接存储像素数据

要是您还记得在 前面一篇文章中黑白图像像素信息的存储方式就太好了,这些信息正是存储在条纹中的。对于灰度和彩色图像,您也可以这么做,但是这样表示图像数据效率很低。例如,在图像背景为单一颜色的情况下,有许多像素的值是相同的。如果像素数据在条纹中存储,则这样的值将浪费大量的空间。

令人欣慰的是,有一种存储图像数据的方式更为高效。试想一张简单的四色、每像素 24 位的图像。如果我们建立了一张四种色彩的值(表示这些色彩的长度为 24 位的值)的查找表,那么我们只需要把与色彩有关的数字项存储在图像条本身内。这只要占用 2 位,而不会占满 24 位。

计算大致是这样:1,000 × 1,000 像素的 24 位彩色图像的存储要占 2400 万位。如果是四色图像,则相同的图像要用 400 万位存储条纹数据,另外 98 位存储色彩表。这些数字没有把文件格式的头尾信息考虑在内,并且是按未压缩位图计算的。查找表的优点是很明显的。这种样式的查找表之所以被称为 调色板,很可能是因为这正是画家们带在身边的东西。

灰度图像也可以使用这个概念。唯一的区别在于,调色板中的“色彩”只有各种明暗的灰色。

libtiff 中的压缩算法

在 libtiff 中可以使用一些压缩算法。下面的表方便对其分类。

表 1. Libtiff 压缩算法

压缩算法 最适合的场合 TIFFTAG
CCITT Group 4 Fax 和 Group 3 Fax列出本项是为完整起见。如果您正给黑白图像编码,那么您很可能正在使用 CCITT fax 压缩方法。这些压缩算法不支持彩色。 COMPRESSION_CCITTFAX3, COMPRESSION_CCITTFAX4
JPEGJPEG 压缩最适合于大的图像,如照片。不过,通常这种压缩会有所损失(因为在压缩过程中会去掉图像数据)。这导致 JPEG 非常不适合于压缩那些仍需要能认得出的文本。另外还要记住,损失是累积的 ― 请参阅下一部分以更多的了解这一点。 COMPRESSION_JPEG
LZW 这是 GIF 图像中使用的压缩算法。由于需要 Unisys 的授权,所以 libtiff 已经取消了对这种压缩工具的支持。如果您想再加上这部分,有一些补丁可以利用,但是将要与您的编码集成的大部分程序都不再支持 LZW。 COMPRESSION_LZW
Deflate这是 gzip 压缩算法,它也可以用于 PNG。对于彩色图像,我推荐使用这种压缩算法。 COMPRESSION_DEFLATE

损失累积?

为什么有损压缩算法(比如,JPEG)导致的数据损失会累积起来?请想象一下,您使用 JPEG 压缩了一张图像。然后,比如说,您要给这张图像添加一个条形码,于是您将该图像解压缩,在添加条码之后,重新压缩它。此时,就会带来一些新的损失。您可以想像,如果您压缩的次数足够多,那么最终会得到的图像将是一个大色块。

这是否成为问题取决于您的数据的类型。为了测试这是个有多严重的问题,我写了一个反复解压缩和再次压缩图像的简单的 libtiff 程序。我发现,对于反复压缩,图片数据的弹性要大得多。


图 1. 压缩前的图片
IBM logo

图 2. 压缩前的文本样本
一些文本样本

我使用的代码的 JPEG 压缩“质量”级别为 25%,这是一种调整压缩算法的数据损失的方式。质量越低,压缩率越高。默认值为 75%。


图 3. 经 200 次反复压缩之后的图片
经 200 次反复压缩的 IBM 徽标

图 4. 经 200 次反复压缩之后的文本
经 200 次反复压缩的一些文本的图片,




回页首


写彩色图像

现在,我们要将一张彩色图像写到磁盘上。请记住,这只是一个简单的示例,还可以进行大量的修饰。


清单 1. 写彩色图像
#include <tiffio.h>
#include <stdio.h>
int main(int argc, char *argv[]){
  TIFF *output;
  uint32 width, height;
  char *raster;
  // Open the output image
  if((output = TIFFOpen("output.tif", "w")) == NULL){
    fprintf(stderr, "Could not open outgoing image\n");
    exit(42);
  }
  // We need to know the width and the height before we can malloc
  width = 42;
  height = 42;
  if((raster = (char *) malloc(sizeof(char) * width * height * 3)) == NULL){
    fprintf(stderr, "Could not allocate enough memory\n");
    exit(42);
  }
  // Magical stuff for creating the image
  // ...
  // Write the tiff tags to the file
  TIFFSetField(output, TIFFTAG_IMAGEWIDTH, width);
  TIFFSetField(output, TIFFTAG_IMAGELENGTH, height);
  TIFFSetField(output, TIFFTAG_COMPRESSION, COMPRESSION_DEFLATE);
  TIFFSetField(output, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
  TIFFSetField(output, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB);
  TIFFSetField(output, TIFFTAG_BITSPERSAMPLE, 8);
  TIFFSetField(output, TIFFTAG_SAMPLESPERPIXEL, 3);
  // Actually write the image
  if(TIFFWriteEncodedStrip(output, 0, raster, width * height * 3) == 0){
    fprintf(stderr, "Could not write image\n");
    exit(42);
  }
  TIFFClose(output);
}

这段代码展示我们在理论部分讨论过的一些内容。图像的每个像素有三个样本,每个样本八位。也就是说,图像是 24 位 RGB 图像。如果这是一张黑白或灰度图像,那么这个值会是 1。 PHOTOMETRIC_RGB 标记声明图像数据存储在条纹本身(而不是通过调色板)。稍候我们将详细讨论。

其它的每像素样本个数?

在我的示例中,有每像素有 3 个样本。如果这是一张黑白图像,或灰度图像,那么我们每像素有一个样本。

还有其它一些值也是有效的;例如,有时人们会存储一个给定像素的透明度值,即 alpha 通道(alpha channel)。这样每像素就要有四个样本了。

每像素可能会有任意数目的样本,如果您需要补充进来像素的附加信息,这将很有好处。 请注意,这么做会中止那些做了愚蠢的假设的图像浏览器 ― 我曾为一位前雇主编写了一些代码来除去 alpha 通道之类的东西,这样他们的 PDF 生成器就不会坏了。

在此讨论的另一件趣事是,图像的平面配置。我们在此指定了 PLANARCONFIG_CONTIG ,也就是说给定的像素的红绿蓝信息一起分在图像数据的条纹中。另一个选项是 PLANARCONFIG_SEPARATE ,图像的红色样本存在一起,蓝色样本存在一起,最后绿色样本存在一起。





回页首


写有调色板的彩色图像

那么,我们怎么才能写出这张图像的有调色板的版本呢?Libtiff 使这相当容易,我们所要做的就是将值 TIFFTAG_PHOTOMETRIC 改成 PHOTOMETRIC_PALETTE 。由于只是一个词的改动,所以确实不值得在本文中包括一个示例。





回页首


读彩色图像

现在我们所要做的就是,弄清楚如何可靠的读取其他人的彩色和灰度图像,对此我们成竹在胸。起初,我很想使用 TIFFReadRGBAStrip()TIFFReadRGBBSTile() 调用了事,它们向调用者掩盖了一些潜在的问题。然而,这两个函数存在一些限制,在 TIFFReadRGBAStrip() 手册页中对这些限制的陈述如下:


TIFFReadRGBAStrip() 手册页节录
   TIFFReadRGBAStrip reads a single strip of a strip-based image into memory,
   storing  the  result  in  the  user supplied RGBA raster. The raster is
   assumed to be an array of width times   rowsperstrip   32-bit   entries,
   where   width   is  the  width  of  the  image (TIFFTAG_IMAGEWIDTH) and
   rowsperstrip is the maximum lines in a strip (TIFFTAG_ROWSPERSTRIP).
   The  strip  value  should  be  the  strip  number  (strip  zero  is the
   first) as returned by the TIFFComputeStrip function, but always for sample 0.
   Note  that  the  raster  is  assume  to  be  organized  such  that the pixel
   at location (x,y) is raster[y*width+x];  with  the  raster  origin in the
   lower-left hand corner of the strip. That is bottom  to  top  organization.
   When reading a partial last strip in the file the last line of the image
   will begin at the beginning of the buffer.
   Raster  pixels  are  8-bit packed red, green, blue, alpha samples. The
   macros TIFFGetR, TIFFGetG, TIFFGetB,  and  TIFFGetA  should  be used to
   access individual samples. Images without Associated Alpha matting
   information have a constant Alpha of 1.0 (255).
   See  the TIFFRGBAImage(3T) page for more details on how various image types
   are converted to RGBA values.
NOTES
   Samples must be either 1, 2, 4, 8, or 16 bits. Colorimetric samples/pixel
   must be either 1, 3, or 4 (i.e. SamplesPerPixel minus ExtraSamples).
   Palette  image  colormaps that appear to be incorrectly written as 8-bit
   values are automatically scaled to 16-bits.
   TIFFReadRGBAStrip  is  just  a wrapper around the more general
   TIFFRGBAImage(3T) facilities. It's main  advantage  over  the similar
   TIFFReadRGBAImage() function is that for large images a single buffer
   capable  of  holding  the  whole  image doesn't need to be allocated, only
   enough for one strip. The TIFFReadRGBATile() function does a similar
   operation for tiled images.

关于这个函数有两个很怪的地方。第一,它将(0,0)定义在和我们一直以来在写的其它代码都不同的位置上。在以前的代码中,(0,0)点一直在图像的左上角。这个调用却将(0,0)定义在左下角。另外一点限制是并不支持每个样本所有有效的比特位值。如果您认为这些奇怪的地方不可接受,那么请记住您仍可以使用 TIFFReadEncodedStrip() ,可以按我在前一篇文章中将其用于黑白图像的方式来使用。


清单 2. 利用 TIFFReadEncodedStrip() 读彩色图像
#include <stdio.h>
#include <tiffio.h>
int main(int argc, char *argv[]){
  TIFF *image;
  uint32 width, height, *raster;
  tsize_t stripSize;
  unsigned long imagesize, c, d, e;
  // Open the TIFF image
  if((image = TIFFOpen(argv[1], "r")) == NULL){
    fprintf(stderr, "Could not open incoming image\n");
    exit(42);
  }
  // Find the width and height of the image
  TIFFGetField(image, TIFFTAG_IMAGEWIDTH, &width);
  TIFFGetField(image, TIFFTAG_IMAGELENGTH, &height);
  imagesize = height * width + 1;
  
  if((raster = (uint32 *) malloc(sizeof(uint32) * imagesize)) == NULL){
    fprintf(stderr, "Could not allocate enough memory\n");
    exit(42);
  }
  // Read the image into the memory buffer
  if(TIFFReadRGBAStrip(image, 0, raster) == 0){
    fprintf(stderr, "Could not read image\n");
    exit(42);
  }
// Here I fix the reversal of the image (vertically) and show you
// how to get the color values from each pixel
  d = 0;
  for(e = height - 1; e != -1; e--){
    for(c = 0; c < width; c++){
      // Red = TIFFGetR(raster[e * width + c]);
      // Green = TIFFGetG(raster[e * width + c]);
      // Blue = TIFFGetB(raster[e * width + c]);
    }
  }
  free(raster);
  TIFFClose(image);
}





回页首


高级论题

现在我们已经讲完了读取和写入能想到的任何图像格式的基本方法,最后有两个论题。

展开的示例

如果您需要有关增加 libtiff 文件输入和输出函数的 hook 的更多信息,请看一下 Panda(我的 PDF 库)中的 images.c 文件。Panda 的 Web 页面可以在本文后面的 参考资料中找到。

将 TIFF 数据存储于文件以外的地方

到此为止,所有的示例都通过文件来读取和写入。在很多场合,您并不想将图像数据存储于文件中,但还想使用 libtiff 和 tiff。例如,您有一些客户身份证的照片,那就要存储在数据库中。

我最熟悉的示例是 PDF 文档,您可以把图像嵌入文档之中。如果想的话,这些图像可以在 TIFF 的子集中,而黑白图像首选 TIFF。

Libtiff 允许您用自己的文件输入和输出函数来替换库中的相应函数。这要通过 TIFFClientOpen() 方法来实现。以下是一个示例(请注意代码无法编译,只是为了描述主要概念才展示出来的):


清单 3. 使用 TIFFClientOpen
#include <tiffio.h>
#include <pthread.h>
// Function prototypes
static tsize_t libtiffDummyReadProc (thandle_t fd, tdata_t buf, tsize_t size);
static tsize_t libtiffDummyWriteProc (thandle_t fd, tdata_t buf, tsize_t size);
static toff_t libtiffDummySeekProc (thandle_t fd, toff_t off, int i);
static int libtiffDummyCloseProc (thandle_t fd);
// We need globals because of the callbacks (they don't allow us to pass state)
char *globalImageBuffer;
unsigned long globalImageBufferOffset;
// This mutex keeps the globals safe by ensuring only one user at a time
pthread_mutex_t convMutex = PTHREAD_MUTEX_INITIALIZER;
...
TIFF *conv;
// Lock the mutex
pthread_mutex_lock (&convMutex);
globalImageBuffer = NULL;
globalImageBufferOffset = 0;
// Open the dummy document (which actually only exists in memory)
conv = TIFFClientOpen ("dummy", "w", (thandle_t) - 1, libtiffDummyReadProc,
            libtiffDummyWriteProc, libtiffDummySeekProc,
            libtiffDummyCloseProc, NULL, NULL, NULL);
// Setup the image as if it was any other tiff image here, including setting tags
...
// Actually do the client open
TIFFWriteEncodedStrip (conv, 0, stripBuffer, imageOffset);
// Unlock the mutex
pthread_mutex_unlock (&convMutex);
...
/////////////////// Callbacks to libtiff
...
static tsize_t
libtiffDummyReadProc (thandle_t fd, tdata_t buf, tsize_t size)
{
  // Return the amount of data read, which we will always set as 0 because
  // we only need to be able to write to these in-memory tiffs
  return 0;
}
static tsize_t
libtiffDummyWriteProc (thandle_t fd, tdata_t buf, tsize_t size)
{
  // libtiff will try to write an 8 byte header into the tiff file. We need
  // to ignore this because PDF does not use it...
  if ((size == 8) && (((char *) buf)[0] == 'I') && (((char *) buf)[1] == 'I')
     && (((char *) buf)[2] == 42))
    {
    // Skip the header -- little endian
    }
  else if ((size == 8) && (((char *) buf)[0] == 'M') &&
       (((char *) buf)[1] == 'M') && (((char *) buf)[2] == 42))
    {
    // Skip the header -- big endian
    }
  else
    {
    // Have we done anything yet?
    if (globalImageBuffer == NULL)
    if((globalImageBuffer = (char *) malloc (size * sizeof (char))) == NULL)
        {
          fprintf(stderr, "Memory allocation error\n");
          exit(42);
        }
    // Otherwise, we need to grow the memory buffer
    else
    {
      if ((globalImageBuffer = (char *) realloc (globalImageBuffer,
                             (size * sizeof (char)) +
                             globalImageBufferOffset)) == NULL)
        fprintf(stderr, "Could not grow the tiff conversion memory buffer\n");
            exit(42);
    }
    // Now move the image data into the buffer
    memcpy (globalImageBuffer + globalImageBufferOffset, buf, size);
    globalImageBufferOffset += size;
    }
  return (size);
}
static toff_t
libtiffDummySeekProc (thandle_t fd, toff_t off, int i)
{
  // This appears to return the location that it went to
  return off;
}
static int
libtiffDummyCloseProc (thandle_t fd)
{
  // Return a zero meaning all is well
  return 0;
}

变彩色为灰度

您如何将彩色图像转换成灰度的呢?我最早的答案是取红、绿和蓝的平均值。这个答案是错的。现实是,人眼看某些色彩的能力要超过看另外的一些色彩。为了得到精确的灰度表示,您需要在彩色样本上应用不同的系数。适当的系数是,红色 0.299,绿色 0.587,而蓝色是 0.114。





回页首


结束语

本文中,我讨论了如何利用 libtiff 编写针对灰度和彩色图像的程序。我向您展示一些代码样本,应该会对您起步有所帮助。您现在知道的应当足够愉快的使用 libtiff 进行编码了。



参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.

  • 请阅读 Michael 写的关于使用 libtiff 进行黑白图形编程的 前一篇文章developerWorks,2002 年 3 月)。


  • 请下载执行本文提到的任务的源文件:
  • 请下载 libtiff 源代码,也许可以在 libtiff Web 站点上找到用在您选择的操作系统上的二进制包。


  • 如想更多的了解有关增加 libtiff 文件输入和输出函数的 hook 的信息,请看一下 Michael 的 Panda 页面上的 images.c 文件。


  • 请查阅 Poynton's Color FAQ,这是关于转换成灰度的讨论。


  • 请查找 developerWorks上 Linux 专区的 更多 Linux 方面的文章


关于作者

Michael 已经在图像处理领域工作了多年,其中有两年在为澳大利亚政府部门管理和开发大型图像数据库。现在他为 Tower Software 公司工作,该公司制造名为 TRIM 的世界领先的 EDMS 和唱片管理包。Michael 还是 Panda(一种开放源码的 PDF 生成 API)的开发人员,以及 comp.text.pdf USENET 常见问题解答文档的维护者。可以通过 mikal@stillhq.com与 Michael 联系。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款