情報科でない学科の友人が、プログラミングの課題で困っていたのでまとめます。

課題

TIFF形式の画像ファイルが与えられる。 この画像を平滑化や2値化、ラベリングなどを行って画像を解析せよ。 言語はc言語とする。

つまずいたところ

c言語でのTIFFファイル読み込み

友人は授業ではppmファイルを例としてずっとやってきたのでTIFFファイルの読み込みがうまくできないとのことでした。

ppmはテキスト形式なので、中身が「255 128 …」といった数字の羅列として読み書きできます。そのため、特別なライブラリを使わなくてもfscanf で簡単に読み込めていたのでしょう。

一方で、TIFFは複雑な構造を持つバイナリ形式です。 単にバイナリとして記録されているだけでなく、データの並び順がファイルによって違ったり、データが圧縮されていたりします。さらに「タグ」と呼ばれる目次情報を辿らないと、どこに画像データがあるのかすら分かりません。

参考Tiffの話

これらを全て考慮して一から自作の読み込み処理を書くのは、プログラミング初心者にはあまりに荷が重すぎます。

opencv

opencvを使えばいいじゃないかと思いましたが、C言語でということを強調されたみたいです。 C++が前提なのでopencvは今回は使えなかったです。

LibTIFFを使う

結論から言うと、LibTIFFというTIFFファイルを読み書きするための標準的なライブラリを使うことで読み込みができます。

環境構築の壁

公式サイトを見ると、「ソースコードをダウンロードして make コマンドでビルドしろ」と書いてあります。しかし、これはプログラミング初心者にはかなりハードルが高い作業です。

また、課題を出された友人の環境はCygwinでしたが、Cygwinでのライブラリ管理はなかなかに厄介です(私自身、過去に環境構築でつまずいて苦戦した経験があります)。

そこで今回は、環境構築の手間を最小限にするため、WSL2(Ubuntu)を使うことにしました。Ubuntuなら apt install 一発でライブラリが入るので、これが最も簡単で確実な方法だと思います。

WSLのインストール

Windowsのコマンドプロンプト(またはPowerShell)を起動して、以下のコマンドを実行します。

wsl --install -d Ubuntu

実行後、画面の指示に従ってユーザー名とパスワードを設定してください。

GCCとLibTIFFのインストール

Ubuntuのターミナル(コマンドプロンプトの表示が緑色になった状態)で、以下のコマンドを順番に実行して、開発環境とライブラリをインストールします。

sudo apt update
sudo apt install libtiff-dev

インストール後、念のため以下のコマンドで確認しておきます。

gcc -v

エラーが出ずにバージョン情報が表示されればOKです。

VSCodeの設定

友人もエディタはVSCodeを使っていたので、そのままWSL上での開発でも使えるようにします。

  1. VSCodeの拡張機能から「WSL」を検索してインストールします。
  2. インストール後、画面左下の「><」のような青いアイコンをクリックし、「Connect to WSL」を選択します。
  3. または、Ubuntuのターミナル上で、開発したいディレクトリに移動して code . と打てば、その場所でVSCodeが開きます。

これで、Windows上にいながらLinux環境でコードを書いて実行できるようになりました。

LibTIFFの使い方

まずは、ライブラリが正しく認識されているか、最小限のコードで確認してみます。

コンパイルテスト

以下のようなファイル(test.c)を作成します。

#include <stdio.h>
#include <tiffio.h>

int main() {
    printf("LibTIFF version: %s\n", TIFFGetVersion());
    return 0;
}

コンパイルする際は、最後に -ltiff オプションをつけてライブラリをリンクさせます。

gcc test.c -ltiff
./a.out

これでバージョン情報が表示されれば、準備万端です。

画像読み込みと簡単な例

詳しくはLibTIFF - TIFF Library and Utilities — LibTIFF 4.7.1 documentationを頑張って読み解けば全部書いてあります。

画像の読み込みはTIFFOPENで行います。

TIFF* tif = TIFFOpen("input.tif", "r");
if (tif == NULL) {
    // 読み込み失敗
    return;
}

そこから慣れ親しんだRGBA(赤・緑・青・透明度)を取得するには、 TIFFReadRGBAImage 、TIFFGetA などを使用することで行えます。

// pixelはuint32_t型の変数(raster配列の中身)
int r = TIFFGetR(pixel); // 赤
int g = TIFFGetG(pixel); // 緑
int b = TIFFGetB(pixel); // 青
int a = TIFFGetA(pixel); // 透明度

以下は、画像を読み込んで2値化し、新しいTIFFファイルとして保存するサンプルプログラムです。

#include <stdio.h>
#include <stdlib.h>
#include <tiffio.h>

int main() {
    // 設定
    char *input_file = "input.tif";
    char *output_file = "output.tif";
    int threshold = 128; // 2値化のしきい値

    // 1. TIFFファイルを開く
    TIFF* tif = TIFFOpen(input_file, "r");
    if (!tif) {
        printf("ファイルが開けませんでした: %s\n", input_file);
        return 1;
    }

    // 画像の幅と高さを取得
    uint32_t w, h;
    TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &w);
    TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &h);
    printf("読み込み成功: %dx%d\n", w, h);

    // 2. 読み込み用のメモリを確保
    // RGBA形式で全てのピクセルを読み込むためのバッファ
    uint32_t* raster = (uint32_t*)_TIFFmalloc(w * h * sizeof(uint32_t));
    
    if (raster != NULL) {
        // 画像データを読み込む
        if (TIFFReadRGBAImage(tif, w, h, raster, 0)) {
            
            // 結果保存用のメモリ(白黒なので1ピクセル1バイトで確保)
            unsigned char* out_data = (unsigned char*)malloc(w * h);

            for (uint32_t y = 0; y < h; y++) {
                for (uint32_t x = 0; x < w; x++) {
                    // rasterは画像の下から順にデータが入っている
                    // 処理しやすいように、raster[y*w + x] でアクセス
                    uint32_t pixel = raster[y * w + x];
                    
                    // RGBAから各色を取り出す
                    int r = TIFFGetR(pixel);
                    int g = TIFFGetG(pixel);
                    int b = TIFFGetB(pixel);
                    
                    // 単純平均でグレーにする
                    int gray = (r + g + b) / 3;

                    // 2値化処理:閾値を超えたら白(255)、それ以外は黒(0)
                    // 保存時は上から順に書き込むため、座標を反転させて格納する
                    out_data[(h - 1 - y) * w + x] = (gray > threshold) ? 255 : 0;
                }
            }

            // 3. 保存処理
            TIFF* out = TIFFOpen(output_file, "w");
            if (out) {
                // 保存するためのタグ設定
                TIFFSetField(out, TIFFTAG_IMAGEWIDTH, w);
                TIFFSetField(out, TIFFTAG_IMAGELENGTH, h);
                TIFFSetField(out, TIFFTAG_SAMPLESPERPIXEL, 1); // モノクロなので1
                TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, 8);   // 8bit
                TIFFSetField(out, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT);
                TIFFSetField(out, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
                TIFFSetField(out, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK); // 黒が0

                // 1行ずつ書き込む
                for (uint32_t i = 0; i < h; i++) {
                    TIFFWriteScanline(out, &out_data[i * w], i, 0);
                }

                TIFFClose(out);
                printf("保存しました: %s\n", output_file);
            }
            free(out_data);
        } else {
            printf("画像の読み込みに失敗しました\n");
        }
        _TIFFfree(raster);
    }

    TIFFClose(tif);
    return 0;
}

まとめ

初心者にC言語でTIFFを扱わせるのは、ファイル構造の理解やライブラリのリンクなど、かなりハードルが高いと感じました。 WSL2(Ubuntu)とLibTIFFを使うことで可能です。