前回の記事では、libtiffによるモノクロのグラフィックス・プログラミングについて解説しましたが、今回は、その続きとして、グレースケール画像、カラー画像を取り上げます。前回のモノクロの記事のコードを読み、理解しているものとして話を進めます。
まず、カラーやグレースケールの画像データの保存方法の理論について、少しおさらいしておきたいと思います。これは、すべての画像フォーマットにあてはまる理論です。おさらいを済ませたところで、libtiffの具体的な使い方について紹介したいと思います。
画像は多数のピクセルで構成されます。モノクロ画像の場合、ピクセルは0か1の2つの値のいずれかをとります。これは1ビットで表現できます。これに対して、グレースケール画像やカラー画像の場合、ピクセルは、もっと広い範囲の値を保存する必要があります。1ピクセルが255個のグレーのレベルをとるとすると、そのピクセルは8ビットで保存しなければならないことになります。これらの値の1つ1つのことをサンプルと呼びます。TIFFでは、この値のサイズをTIFFTAG_BITSPERSAMPLE というタグで表現します。これは、モノクロの場合は1であり、グレースケールの場合は、もっと大きな数になります。
カラー画像の場合は、さらに多くの情報を保存する必要があります。各ピクセルごとに、赤、緑、青の値を保存する必要があります。これらの値は、それぞれ別々のサンプルに保存されます。したがって、TIFFTAG_SAMPLESPERPIXEL を定義しなければならないことになります。これは、モノクロ画像、グレースケール画像の場合は1ですが、カラー画像の場合は通常3となります。また、各サンプルのサイズも定義する必要がありますので、さらにTIFFTAG_BITSPERSAMPLE の値を設定する必要もあります。
カラー画像、グレースケール画像をサポートする際に、まず理解しておくべきことは、メモリー中での画像データのフォーマットです。カラー画像、グレースケール画像の表現方法には、主に2つのやり方があります。以下では、これらの表現方法を、まずグレースケールについて説明し、次にそれをカラーに拡張する形で説明します。
前回の記事でモノクロ画像でのピクセル情報の保存方法を説明しましたが、それは複数のstripの形で保存されるのでした。グレースケール画像やカラー画像でも同じ方法を用いることができますが、この画像データの表現方法は、非常に非効率的です。たとえば、画像の背景がベタ (solid) である場合、数多くのピクセルが同じ値をとります。ピクセル・データがstripの形で保存される場合、同じ値をとる大量のスペースが無駄になります。
ありがたいことに、もっと効率的な画像データの保存方法があります。1ピクセルあたり24ビットで、4つの色だけを使用する単純な画像があったとします。4つの色の値からなるルックアップ・テーブルを作成すると (各色は、24ビット値で表現される)、画像strip自体には、色に対応するエントリー番号を保存するだけでよいことになります。この場合、まるまる24ビット使う代わりに、たった2ビットで済みます。
以下のような計算になります。1,000ピクセル×1,000ピクセルの24ビット・カラー画像を保存するには、2千4百万ビットが必要です。この画像が、色を4つ使用する画像だったとすると、stripデータの4百万ビットとカラー・テーブルの98ビットで済むことになります。これは、ファイル・フォーマットのヘッダー情報やフッター情報を含まない数字であり、また未圧縮のビットマップに対する数字です。ルックアップ・テーブルが有効であることは明白です。このようなスタイルのルックアップ・テーブルのことをパレットと呼びます。たぶん画家が持ち歩くものに由来してのことでしょう。
この考え方は、グレースケール画像にも成り立ちます。違うのは、パレットの中の「色」がグレーの濃淡となる点だけです。
libtiffには、いろいろな圧縮アルゴリズムが用意されています。下の表は、それらのアルゴリズムを整理したものです。
表1. Libtiffの圧縮アルゴリズム
| 圧縮アルゴリズム | 適している分野 | TIFFTAG |
|---|---|---|
| CCITT G4 Fax、G3 Fax | この項目は、整合性をもたせるために入れてあります。モノクロ画像を扱うコーディングを行う方は、たぶんCCITTファックスの圧縮手法を使用することになるでしょう。これらの圧縮アルゴリズムでは、カラーはサポートされません。 |
COMPRESSION_CCITTFAX3, COMPRESSION_CCITTFAX4
|
| JPEG | JPEG圧縮は、写真などの大きな画像に有効です。ただし、この圧縮の場合、通常、損失を伴います (圧縮過程の中で、画像データが棄てられるという意味で)。したがって、JPEGは、元どおりに読めなければならないテキストを圧縮するのには適していません。また、もう1つ注意しておくべきことは、損失が蓄積的であるという点です。これについては、次の節で詳しく説明します。 |
COMPRESSION_JPEG
|
| LZW | これは、GIF画像で使用されている圧縮アルゴリズムです。Unisysのライセンスが必要なため、この圧縮コーデックに対するサポートは、libtiffからは外してあります。やはりこの圧縮アルゴリズムをサポートしておきたいという方のために、パッチも用意されていますが、みなさんのコードと連携されるプログラムの大半は、LZWをサポートしなくなっています。 |
COMPRESSION_LZW
|
| Deflate | これはgzipの圧縮アルゴリズムで、PNGにも使用されているものです。カラー画像には、この圧縮アルゴリズムをお薦めしたいと思います。 |
COMPRESSION_DEFLATE
|
JPEGなどの損失型圧縮アルゴリズムでは、なぜ損失が蓄積されるのでしょうか。JPEGを使って画像を圧縮することを考えてみることにします。そして、画像に、たとえばバーコードを付加する必要があったとします。そこで画像の解凍を行い、バーコードを付加し、再度圧縮するものとします。再圧縮を行うと、新しい1群の損失が招来されます。これを何回も繰り返していくと、ついには大きなしみのような画像になってしまうことが想像できます。
これが問題となるかどうかは、データの種類によりけりです。これが、どの程度の問題なのかを調べるために、画像の解凍と再圧縮を繰り返すlibtiffのサンプル・プログラムを作成してみました。そこで発見したことは、図画 (pictures) のデータは、繰り返して圧縮を行った場合でも、ずっと復元性が高いということでした。
図1. 圧縮前の図画
図2. 圧縮前のサンプル・テキスト
私が使用したコードでは、JPEG圧縮アルゴリズムの損失を調整するための1つの方法である「品質」の等級を25%としました。品質を低くすればするほど、圧縮率は高くなります。デフォルトは75%です。
図3. 圧縮を200回繰り返した後の図画
図4. 圧縮を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);
}
|
このコードには、理論のところで説明したことがいくつか示されています。画像は1ピクセルあたり3サンプルであり、それぞれのサンプルに8ビットを使用します。したがって、この画像は24ビットのRGB画像です。これがモノクロ画像またはグレースケール画像なら、この値 (1ピクセルあたりのサンプル数) は1となるところです。タグPHOTOMETRIC_RGB は、画像データがstrip自体に保存されることを示します (パレット化されるのではなく)。これについては、後で説明します。
もう1つここで触れておきたいことに、画像の平面構成 (planar configuration) の問題があります。上のコードではPLANARCONFIG_CONTIG を指定していますが、これは、各ピクセルの赤、緑、青の情報を、画像データのstripに、いっしょにまとめて保存することを意味します。他の選択肢としてはPLANARCONFIG_SEPARATE がありますが、その場合は、画像の赤のサンプルがまとめて保存され、次に青のサンプルが、最後に緑のサンプルがまとめて保存されます。
さて、それではこの画像のパレット化バージョンは、どうやって記述するのでしょうか。libtiffを使えば、これもまったくわけがありません。TIFFTAG_PHOTOMETRIC の値をPHOTOMETRIC_PALETTE に変えれば良いだけの話です。変わるのは1語だけですので、コード・サンプルを掲載するまでもありません。
次に行うべきことは、他人の作成したカラー画像やグレースケール画像を安定的に読めるようにすることですが、これも大した問題ではありません。最初、呼び出し側がごちゃごちゃするのをある程度緩和しようと思い、よっぽどTIFFReadRGBAStrip() 呼び出しとTIFFReadRGBBSTile() 呼び出しの体裁を変更しようかとも思いましたが、これらの関数には、TIFFReadRGBAStrip() のmanページに書かれているように、少し制約があります。
TIFFReadRGBAStrip() のmanページからの抜粋
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) として定義しています。また、1サンプルあたりのビット数について、有効なすべての値をサポートしていないという制約もあります。こうした癖が気に入らないという方は、私が前回の記事でモノクロ画像を扱ったときと同じように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);
}
|
さて、どんな画像フォーマットでも基本的な読み書きができるようになったところで、最後に2つのことについて触れておきたいと思います。
これまでのコード・サンプルは、すべて、ファイルとの間で読み書きを行うものでした。画像データをファイルに保存するのではないが、libtiffやtiffを使用したいという場合は数多くあります。たとえば、カスタマーのIDカード用の写真を、データベースに保存しておきたいというような場合です。
私が最もよく扱う例はPDF文書で、画像を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を使ってどんどんコーディングを行えるだけの知識が得られたのではないでしょうか。
- libtiffによるモノクロのグラフィックス・プログラミングを取り上げたRead Michaelの前回の記事 (developerWorks、2002年3月)。
- 本稿で取り上げた処理を行うソース・ファイルは、以下からダウンロードできます。
- カラー画像の読み出し:read.c
- カラー画像の書き込み:write.c
- ファイル入出力関数のカスタマイズ:client.c
- 圧縮の繰り返し:recompress.c
- libtiffのWebサイトからlibtiffのソースがダウンロードできます。それぞれのオペレーティング・システムに対応したバイナリー・パッケージも見つかるはずです。
- libtiffの中でファイル入出力関数をフックする方法について詳しく知りたい方は、MichaelのPandaページにあるimages.cというファイルを参照してください。
- グレースケールへの変換方法については、Poynton's Color FAQを参照してください。
- developerWorks のLinuxゾーンには、他にもLinux関係の記事が多数掲載されています。
