Generic Image Library Tutorial 日本語訳

この文章はGeneric Image Library Tutorialの日本語訳です。 この日本語訳は原文の著者や権利者とまったく関係がありません。 また、原文と訳文の正確さに関して、訳者は一切の責任を負いません。

原文:

Generic Image Library Tutorial
http://stlab.adobe.com/gil/html/giltutorial.html

原文のVersion:

2.1

原文のライセンス:

Boost Software License, Version 1.0 (2008)
MIT License (2005-2007)

訳者:

Hiroaki Nishihara (hiro.nishihara@gmail.com)

訳文のLicense:

Boost Software License, Version 1.0

翻訳日時:

2014年6月22日〜2014年7月1日

Repository:

https://github.com/hironishihara/GILTutorial-ja


Generic Image Library チュートリアル

著者:

Lubomir Bourdev (bourdev@adobe.com)
Hailin Jin (hljin@adobe.com)
Adobe Systems Incorporated

Version:

2.1

作成日時:

2007年9月15日

Generic Image Library (GIL)は、画像に適用されるアルゴリズムから画像の形式を抽象化することで、書き上げたアルゴリズムが様々な形式の画像でも動作することを可能にします。 また同時に、特定の形式の画像に特化したアルゴリズムに匹敵する速度で動作するコードの生成も実現します。 本文章は、GILの使用に関するジャンプスタートを提供します。 ただし、ライブラリの内部設計については説明しませんし、ライブラリの全体像については取り扱いません。 詳細なライブラリ設計についての文章は、GILのWebサイトhttp://opensource.adobe.com/gilを参照ください。

インストール

最新バージョンのGILは、GILのWebサイト http://opensource.adobe.com/gil からダウンロードすることができます。 GILはBoostへの統合が承認されており、近いうちに http://www.boost.org からBoostをインストールする際にGILも同時にインストールされるようになるでしょう。 GILはヘッダーファイルだけで構成されており、他のライブラリのリンクは不要です。 また、Boostのビルドも必要ありません。 ほとんどのプロジェクトでは、boost/gil/gil_all.hppをインクルードするだけで十分です。

例題 - Gradient画像の算出

このチュートリアルは、Gradient画像を算出するというGILの使用例を通して進めていくことにしましょう。 始めは極めてシンプルでジェネリックでないコードからスタートし、それを少しずつジェネリックなコードにしていきます。 まずは水平方向のGradientから始めることとし、Gradientの最もシンプルな近似と思われる中心差分を使います。 位置xにあるPixelのGradientは、その両隣のPixelの差分の1/2で近似されます。

D[x] = (I[x-1] - I[x+1]) / 2

簡単のために、境界ケース(すなわち、対象のPixelが画像の端にあって隣接Pixelの片方が定義されていないケース)は無視することにしましょう。 この文章のフォーカスは、GILの使い方であり、上質なGradient画像生成アルゴリズムの作り方ではないのです。

インタフェースとグルーコード

入力は8ビット符号なしグレイスケール画像、出力は8ビット符号付きグレイスケール画像として始めましょう。 まずは、GILを使って作られたアルゴリズムのインタフェースがどのような感じなのか示します。

#include <boost/gil/gil_all.hpp>
using namespace boost::gil;

void x_gradient(const gray8c_view_t& src, const gray8s_view_t& dst) {
    assert(src.dimensions() == dst.dimensions());
    ...    // compute the gradient
}

gray8c_view_tは入力画像の型です。Pixelがread-only (cの文字がそれを表しています)の8ビットグレイスケールViewです。 出力は8ビット符号付き(sの文字がそれを表しています)整数型のグレイスケール画像です。 GILが定める型の命名規則については、付録を参照ください。

GILはImageとImage Viewを区別します。 GILのImage Viewは、長方形格子状のPixelの範囲を指し示す、浅く軽いViewです。 ViewはPixelへのアクセスを提供しますが、Pixelそのものではありません。 ViewのコピーコンストラクションはPixelのディープコピーではありません。 Image Viewに付加されたconst性はPixelまで伝播しないので、Image Viewは常にconst参照で使用すべきです。 Viewがmutableであるかread-only (immutable)であるかは、Viewの型のプロパティです。

一方、GILのImageは、所有権と関連づけられたViewの一種であり、Pixelのコンテナです。 すなわち、Imageのコンストラクタ/デストラクタはPixelのメモリの確保/解放を行い、コピーコンストラクタはPixelのディープコピーを行い、operator==はPixelのディープな比較を行います。 Imageに付加されたconst性はPixelまで伝播するので、Imageのconst参照はPixelの編集を許しません。

ほとんどのGILアルゴリズムはImage Viewの上で動作します。Imageが必要になることはめったにありません。 GILの設計は、STLの設計とよく似ています。 GILのImageはSTLにおけるstd::vectorなどといったコンテナに相当し、GILのImage ViewはSTLにおけるRange(しばしば、begin()end()のようなIteratorの組で表現されています)に相当します。 STLアルゴリズムがRangeの上で動作するのと同じ様に、GILアルゴリズムはImage Viewの上で動作するのです。

GILのImage Viewは生データ(widthとheight、1行あたりのバイト数、かたまりごとにポインタで表現されたPixelデータ)から構成することができます。 ここで、自身のコードとGILのグルーをどのように提供するかを示します。

void ComputeXGradientGray8(const unsigned char* src_pixels, ptrdiff_t src_row_bytes, int w, int h,
                                   signed char* dst_pixels, ptrdiff_t dst_row_bytes) {
    gray8c_view_t src = interleaved_view(w, h, (const gray8_pixel_t*)src_pixels,src_row_bytes);
    gray8s_view_t dst = interleaved_view(w, h, (     gray8s_pixel_t*)dst_pixels,dst_row_bytes);
    x_gradient(src,dst);
}

このグルーコードはとても高速であり、2つのViewはとても軽量(上記の例では16バイト)です。 それぞれのViewは、左上隅のPixelを示すポインタと3個の整数(width、height、1行あたりのバイト数)から構成されています。

はじめてのインプリメンテーション

処理速度への影響がわかりやすくなるように、水平方向のgradientをまずは次のように計算します。

void x_gradient(const gray8c_view_t& src, const gray8s_view_t& dst) {
    for (int y=0; y<src.height(); ++y)
        for (int x=1; x<src.width()-1; ++x)
            dst(x,y) = (src(x-1,y) - src(x+1,y)) / 2;
}

Image Viewのoperator(x,y)を使用して与えられた座標のPixel参照を取得し、そこに両隣のPixelの差分の1/2を代入します。 operator()はグレイスケールPixelの参照を返します。 グレイスケールPixelはそのChannel (srcにおけるunsigned char)と変換可能であり、Channelからのコピーコンストラクションが可能です。(これが可能なのはグレイスケールPixelだけです。) 上記のコードは読みやすいのですが、それほど高速ではありません。というのも、2個の引数を取るoperator()が2次元格子上の座標を算出する際に和と積を用いているからです。 上記のコードをより高速にしたバージョンは次の様になります。

void x_gradient(const gray8c_view_t& src, const gray8s_view_t& dst) {
    for (int y=0; y<src.height(); ++y) {
        gray8c_view_t::x_iterator src_it = src.row_begin(y);
        gray8s_view_t::x_iterator dst_it = dst.row_begin(y);

        for (int x=1; x<src.width()-1; ++x)
            dst_it[x] = (src_it[x-1] - src_it[x+1]) / 2;
    }
}

このコードでは、各行の先頭を指すように初期化されたPixel Iteratorを使用します。 GILのIteratorはランダムアクセス走査Iteratorです。 もしランダムアクセスIteratorに詳しくないのであれば、ひとまずポインタだと考えておきましょう。 事実、上記の例における2個のIteratorはCポインタであり、operator[]はポインタの高速なインデクシングを行う演算子です。

垂直方向のgradientを計算するコードも、上記のコードにとてもよく似ています。

void y_gradient(const gray8c_view_t& src, const gray8s_view_t& dst) {
    for (int x=0; x<src.width(); ++x) {
        gray8c_view_t::y_iterator src_it = src.col_begin(x);
        gray8s_view_t::y_iterator dst_it = dst.col_begin(x);

        for (int y=1; y<src.height()-1; ++y)
            dst_it[y] = (src_it[y-1] - src_it[y+1])/2;
    }
}

各行のループを回すかわりに各列のループを回し、その中で垂直方向に移動するIteratorであるy_iteratorを作成します。 ただし、垂直方向に隣接するPixel間のメモリ上での距離はその画像の1行分のバイト数に等しいので、垂直方向Iteratorにシンプルなポインタを使用することはできません。 ここでGILはサイズが8バイトの特別なステップIterator (Cポインタとステップ幅の値を含みます)を使用します。 このItaratorのoperator[]はインデクス値とステップ幅の値との積を求めます。

しかし、上記のバージョンにおけるy_gradientは、そのメモリアクセスパターンが原因で、x_gradientと比べて非常に低速です。 垂直方向の画像の走査が、多くのキャッシュを無駄にするからです。 より効率的でキャッシュフレンドリなバージョンでは、垂直方向のループの内部で各行の処理を反復します。

void y_gradient(const gray8c_view_t& src, const gray8s_view_t& dst) {
    for (int y=1; y<src.height()-1; ++y) {
        gray8c_view_t::x_iterator src1_it = src.row_begin(y-1);
        gray8c_view_t::x_iterator src2_it = src.row_begin(y+1);
        gray8s_view_t::x_iterator dst_it = dst.row_begin(y);

        for (int x=0; x<src.width(); ++x) {
            *dst_it = ((*src1_it) - (*src2_it))/2;
            ++dst_it;
            ++src1_it;
            ++src2_it;
        }
    }
}

このサンプルコードでは、Iteratorのoperator[]を通してインクリメントと間接参照を行うかわりに、Iteratorそのものを用いる代替手段を示しています。

Locatorの使い方

残念なことに、このキャッシュフレンドリなバージョンでは、入力Viewの中で2個のIteratorを扱うという余計な手間が掛かっています。 ここで私たちが行いたいのは、各Pixelの上下に隣接するPixelへのアクセスです。 そういった相対アクセスは、GILのLocatorによって行うことができます。

void y_gradient(const gray8c_view_t& src, const gray8s_view_t& dst) {
    gray8c_view_t::xy_locator src_loc = src.xy_at(0,1);
    for (int y=1; y<src.height()-1; ++y) {
        gray8s_view_t::x_iterator dst_it  = dst.row_begin(y);

        for (int x=0; x<src.width(); ++x) {
            (*dst_it) = (src_loc(0,-1) - src_loc(0,1)) / 2;
            ++dst_it;
            ++src_loc.x();                  // each dimension can be advanced separately
        }
        src_loc+=point2<std::ptrdiff_t>(-src.width(),1);    // carriage return
    }
}

最初の行では、入力Viewの2行目の先頭を指し示すLocatorを作成しています。 GILのPixel Locatorは、水平方向と垂直方向のどちらにも移動できること以外は、Iteratorとよく似ています。 上記のコードにある通り、src_loc.x()src_loc.y()はそれぞれ水平方向Iteratorの参照と垂直方向Iteratorの参照をそれぞれ返すので、それらを使ってLocatorを好きな方向に動かすことができます。 加えて、Locatorはoperator+=operator-=を用いることで、両方向へ同時に移動することが出来ます。 Image Viewと同じように、Locatorは現在位置を基準にした相対的オフセットで指定されるPixelへの参照を返すoperator()(この演算子は2個の引数を取ります)を提供します。 例えば、src_loc(0,1)は現在指し示しているPixelの下側に隣接するPixelの参照を返します。 Locatorはとても軽量なオブジェクトであり、上記の例ではわずか8バイトです。 現在のPixelを指す生ポインタと、ある行から次の行までのバイト数を表す整数型(垂直移動の際のステップ数として使用します)で構成されています。 ++src_loc.x()の呼び出しは、Cポインタのインクリメント1回に相当します。 ところが、上記のコードの内部では必要以上の計算が行われてしまっています。 src_loc(0,1)のコードは2方向のPixelオフセットを計算をしなければならず、低速なのです。 と言いつつ、両隣のPixelとのオフセットはPixelの座標にかかわらず一定であることに着目しましょう。 パフォーマンス向上のために、GILはこのオフセットをキャッシュして再利用することができるのです。

void y_gradient(const gray8c_view_t& src, const gray8s_view_t& dst) {
    gray8c_view_t::xy_locator src_loc = src.xy_at(0,1);
    gray8c_view_t::xy_locator::cached_location_t above = src_loc.cache_location(0,-1);
    gray8c_view_t::xy_locator::cached_location_t below = src_loc.cache_location(0, 1);

    for (int y=1; y<src.height()-1; ++y) {
        gray8s_view_t::x_iterator dst_it = dst.row_begin(y);

        for (int x=0; x<src.width(); ++x) {
            (*dst_it) = (src_loc[above] - src_loc[below])/2;
            ++dst_it;
            ++src_loc.x();
        }
        src_loc+=point2<std::ptrdiff_t>(-src.width(),1);
    }
}

この例におけるsrc_loc[above]はポインタの高速なインデクシングに相当しており、このコードは効率的です。

ジェネリックなGILアルゴリズムの作成

x_gradientのコードをよりジェネリックにしていきましょう。 まず、同じChannel数をもつどのようなImage Viewでも動作すべきです。 gradientの計算は各Channelが独立に計算されます。 新しいインタフェースがどのようになるのか示します。

template <typename SrcView, typename DstView>
void x_gradient(const SrcView& src, const DstView& dst) {
    gil_function_requires<ImageViewConcept<SrcView> >();
    gil_function_requires<MutableImageViewConcept<DstView> >();
    gil_function_requires<ColorSpacesCompatibleConcept<
                                typename color_space_type<SrcView>::type,
                                typename color_space_type<DstView>::type> >();

    ... // compute the gradient
}

この新しいアルゴリズムでは、テンプレートのパラメータとして入力のImage Viewと出力のImage Viewを取ります。 このアルゴリズムは、GILのビルトインImage Viewとユーザ定義のImage Viewのどちらでも使うことができます。 そして、上記の関数内の最初の3行は任意です。 この3行では、2個の仮引数がそれぞれ有効なGIL Image Viewであること、2個目の仮引数のImage Viewがmutableであること、2個のImage ViewのColor Spaceの間に互換性があることを、boost::concept_checkを用いて保証しています。

GILは、ビルトインコンストラクトの中でも、boost::concept_checkの使用を要求していません。 ユーザが、独自のChannel、Color Space、Iterator、Locator、View、Imageを使うことは自由です。 しかし、その他の部分を担うGILと一緒に動作させるためには、独自のコンストラクトはboost::concept_checkによる要求のセットを満たさなければなりません。 言い換えると、独自のコンストラクトは、関連するGIL Conceptに基づいたModelでなければなりません。 GIL Conceptはユーザガイドの中で定義されています。

C++のテンプレートとジェネリックプログラミングの最大の欠点のひとつは、コンパイルエラーの意味を読み取ることが非常に困難だということです。 これは、型の判定が先延ばしされていることの副作用です。 ジェネリックな引数が関数からの要求を満たしていない可能性がありますが、その不一致は、コードの中の見慣れない、問題とほとんど関係のない、幾重にもネストされた関数コールがきっかけとなって発生するのです。 GILでは、この問題を軽減するためにboost::concept_checkを用います。 先の3行は、テンプレートのパラメータが関連するConceptを満たした有効なModelであるかどうかを調べています。 Modelが正しくない場合、問題に近く追跡が簡単なgil_function_requiresのなかでコンパイルエラーが発生します。 加えて、これらのチェックはコンパイル時に行われ、パフォーマンスへの影響はありません。 コンセプトチェックを用いることの欠点は、コンパイル時間に深刻なインパクトを与える場合があるということです。 このことから、GILはデバッグモードで且つBOOST_GIL_USE_CONCEPT_CHECKが定義されている場合にだけコンセプトチェックを行います。 (ちなみに、BOOST_GIL_USE_CONCEPT_CHECKはデフォルトでOFFです。)

ジェネリックな関数の中身は、型が決まっている一般の関数の中身とよく似ています。 もっとも大きな違いは、Pixelの各Channelをループして各Channel毎にgradientの計算が必要だということです。

template <typename SrcView, typename DstView>
void x_gradient(const SrcView& src, const DstView& dst) {
    for (int y=0; y<src.height(); ++y) {
        typename SrcView::x_iterator src_it = src.row_begin(y);
        typename DstView::x_iterator dst_it = dst.row_begin(y);

        for (int x=1; x<src.width()-1; ++x)
            for (int c=0; c<num_channels<SrcView>::value; ++c)
                dst_it[x][c] = (src_it[x-1][c]- src_it[x+1][c])/2;
    }
}

単純な各Channelのループは、パフォーマンスの問題になる可能性があります。 GILは、各Channelの操作を次のように抽象化することができます。

template <typename Out>
struct halfdiff_cast_channels {
    template <typename T> Out operator()(const T& in1, const T& in2) const {
        return Out((in1-in2)/2);
    }
};

template <typename SrcView, typename DstView>
void x_gradient(const SrcView& src, const DstView& dst) {
    typedef typename channel_type<DstView>::type dst_channel_t;

    for (int y=0; y<src.height(); ++y) {
        typename SrcView::x_iterator src_it = src.row_begin(y);
        typename DstView::x_iterator dst_it = dst.row_begin(y);

        for (int x=1; x<src.width()-1; ++x)
            static_transform(src_it[x-1], src_it[x+1], dst_it[x],
                               halfdiff_cast_channels<dst_channel_t>());
    }
}

static_transformはChannelレベルのGILアルゴリズムのひとつです。 この他には、static_genreratestatic_fillstatic_for_eachといったアルゴリズムがあります。 これらのアルゴリズムは、generatetransformfillfor_eachといったSTLアルゴリズムと同等なChannelレベルのアルゴリズムです。 GILのChannelアルゴリズムは、ループを回すことがないように、静的な再帰を用います。 これらのアルゴリズムは、各Channelの単純なループを決して行いません。 上記の例などであれば、モダンなコンパイラ(たとえVisual Studio 8であっても)はChannelレベルのループを行うことはないでしょう。 しかし、GILのChannelレベルのアルゴリズムを用いるもうひとつの利点は、メモリ上での順序ではなくセマンティックなChannel順序を用いるということです。 例を挙げると、上記のコードは入力ViewがRGBで出力ViewがBGRであっても問題なく適合します。

型が異なるImageに対して、ジェネリックなコードをどのように用いるのか示します。

// Calling with 16-bit grayscale data
void XGradientGray16_Gray32(const unsigned short* src_pixels, ptrdiff_t src_row_bytes, int w, int h,
                                  signed int* dst_pixels, ptrdiff_t dst_row_bytes) {
    gray16c_view_t src=interleaved_view(w,h,(const gray16_pixel_t*)src_pixels,src_row_bytes);
    gray32s_view_t dst=interleaved_view(w,h,(     gray32s_pixel_t*)dst_pixels,dst_row_bytes);
    x_gradient(src,dst);
}

// Calling with 8-bit RGB data into 16-bit BGR
void XGradientRGB8_BGR16(const unsigned char* src_pixels, ptrdiff_t src_row_bytes, int w, int h,
                                 signed short* dst_pixels, ptrdiff_t dst_row_bytes) {
    rgb8c_view_t  src = interleaved_view(w,h,(const rgb8_pixel_t*)src_pixels,src_row_bytes);
    rgb16s_view_t dst = interleaved_view(w,h,(    rgb16s_pixel_t*)dst_pixels,dst_row_bytes);
    x_gradient(src,dst);
}

// Either or both the source and the destination could be planar - the gradient code does not change
void XGradientPlanarRGB8_RGB32(
           const unsigned short* src_r, const unsigned short* src_g, const unsigned short* src_b,
           ptrdiff_t src_row_bytes, int w, int h,
           signed int* dst_pixels, ptrdiff_t dst_row_bytes) {
    rgb16c_planar_view_t src=planar_rgb_view (w,h, src_r,src_g,src_b,         src_row_bytes);
    rgb32s_view_t        dst=interleaved_view(w,h,(rgb32s_pixel_t*)dst_pixels,dst_row_bytes);
    x_gradient(src,dst);
}

これらの例が示す通り、入力と出力はともにインタリーブ形式であってもプラナー形式であっても構いませんし、それらのChannle深度は(入力から出力に代入可能であれば)どのようであれ構いませんし,それらのColor Spaceは(互換性さえあれば)どのようであっても構いません。

GIL 2.1は、6ビットRGB222 Imageや1ビットGray1 Imageといったバイト単位ではないChannelをもつImageについてもネイティブに扱うことができます。 GILのアルゴリズムは、ネイティブにこれらのImageへ適用することができます。 このようなImageの用例ついては、デザインガイドやサンプルファイルを参照ください。

Image Viewの変換

y_gradientを計算する方法のひとつに、画像を90度回転させてx_gradientを計算し、その結果を逆方向に90度回転させて元の向きに戻すというものがあります。 それをGILでどのように行うかを次に示します。

template <typename SrcView, typename DstView>
void y_gradient(const SrcView& src, const DstView& dst) {
    x_gradient(rotated90ccw_view(src), rotated90ccw_view(dst));
}

rotated90ccw_viewは、あるImage Viewを引数に取り、それを90度反時計回りに回転させたImage Viewを返します。 これは、GILのView変換関数の一例です。 GILは、軸に沿った回転、転置、垂直方向または水平方向の反転、矩形領域の切り出し、色空間の変換、サブサンプリングなど、様々なView変換関数を提供します。 View変換関数は浅く、高速です。 View変換関数では、Pixelのコピーは行わず、Pixelにアクセスする際の”座標系”を変更するだけです。 例を挙げると、rotated90cw_viewはオリジナルのViewの垂直方向Iteratorを水平方向IteratorとしてもつViewを返します。 先に挙げたy_gradientを計算するコードはメモリのアクセスパターンが原因で低速でしたが、rotated90cw_viewを用いたコードは全く遅くなりません。

もうひとつの例として、カラー画像のN番目Channelのgradientを計算してみましょう。

template <typename SrcView, typename DstView>
void nth_channel_x_gradient(const SrcView& src, int n, const DstView& dst) {
    x_gradient(nth_channel_view(src, n), dst);
}

nth_channel_viewは、あらゆるViewを引数に取り、そのN番目Channelだけをもつ単Channel (グレイスケール) Viewとして返すView変換関数です。 インタリーブRGB Viewの場合、この関数から返されるViewはインクリメントされたときに2つのChannelを飛び越す水平方向IteratorをもつスキップViewです。 プラナーRGB Viewに用いられた場合には、Cポインタが水平方向Iteratorになっている、シンプルなグレイスケールViewが返されます。 View変換関数は、互いを連結させることが可能です。 例えば、Viewの2番目ChannelのY方向gradientを計算する場合、次のようにします。

y_gradient(subsampled_view(nth_channel_view(src, 1), 2,2), dst);

GILでは、連結された複数のViewを簡略化できる場合があります。 例を挙げると、入れ子になった2つのサブサンプルView (X軸方向でスキップするViewとY軸方向でスキップするView)は、その2つのViewのステップの積をステップとする1つのサブサンプルViewに置き換えることが可能です。

1次元Pixel Iterator

ここでもう一度x_gradientの話に戻りましょう。 多くのImage Viewアルゴリズムでは各Pixelに同じ処理を行います。GILは、そのような一連の手順を抽象化します。 振り返ってみると、先ほどのx_gradientアルゴリズムでは、画像の最初の列にあるPixelと最後の列にあるPixelについてはスキップするという変則的なアクセスパターンを用いていました。 これをどのようにして規則的なアクセスパターンに書き直すことができるのかを知っておくのも有意義でしょう。 GILでこれを実現するには、View内の全てのPixelに対して処理を行うバージョンを作成し、それを最初の列と最後の列を除いたサブイメージに対して適用するという方法をとります。

void x_gradient_unguarded(const gray8c_view_t& src, const gray8s_view_t& dst) {
    for (int y=0; y<src.height(); ++y) {
        gray8c_view_t::x_iterator src_it = src.row_begin(y);
        gray8s_view_t::x_iterator dst_it = dst.row_begin(y);

        for (int x=0; x<src.width(); ++x)
            dst_it[x] = (src_it[x-1] - src_it[x+1]) / 2;
    }
}

void x_gradient(const gray8c_view_t& src, const gray8s_view_t& dst) {
    assert(src.width()>=2);
    x_gradient_unguarded(subimage_view(src, 1, 0, src.width()-2, src.height()),
                         subimage_view(dst, 1, 0, src.width()-2, src.height()));
}

subimage_viewはGIL View変換関数のもうひとつの例です。 subimage_viewは、入力Viewと矩形領域(ここでは、x_miny_minwidthheight)を引数にとり、入力Viewの指定した矩形領域を指し示すViewを返します。 上記の実装であれば、元のViewに対して処理を行っていたバージョンと比較しても、測定できるようなパフォーマンスの低下は生じません。

また、全Pixelに対して処理を行うx_gradient_unguardedは、よりコンパクトに書き換えることができます。

void x_gradient_unguarded(const gray8c_view_t& src, const gray8s_view_t& dst) {
    gray8c_view_t::iterator src_it = src.begin();
    for (gray8s_view_t::iterator dst_it = dst.begin(); dst_it!=dst.end(); ++dst_it, ++src_it)
      *dst_it = (src_it.x()[-1] - src_it.x()[1]) / 2;
}

GILのImage Viewは、View内の全てのPixelを左から右かつ上から下の順序で走査する1次元Pixel Iteratorを返すbegin()end()という関数を提供します。 これらの1次元Pixel Iteratorは完璧な”キャリッジリターン”を行います。 すなわち、各行の末尾に未使用のバイトがあればスキップします。 そしてこのことから、1次元Pixel Iteratorはわずかに最適化の余地を残しています。というのは、これらの1次元Pixel Iteratorは行の末尾を考慮するために自身の現在位置を記録しておく必要があるのです。 1次元Pixel Iteratorのインクリメントを行う演算子は追加のチェック(現在位置は行の末尾か?)を行っており、代わりとして2段にネストされたループを用いる場合には、このチェックをさけることが出来ます。 これらの1次元Pixel Iteratorは、さらに軽量な水平方向Iteratorを返すx()という関数(前のコードで使用)をもっています。 水平方向Iteratorは行の末尾についての情報をもっていません。 今回のケースでは、水平方向Iteratorは生のCポインタです。 この例について言えば、ふたつの隣接PixelはImage Viewが指し示す範囲外に配置されている可能性があることから、それらに適切にアクセスするためには水平方向Iteratorを用いなければなりません。

STL-Styleアルゴリズム

GILは、STL-Styleの多くのアルゴリズムを提供しています。 例えば、std::transformは、出力コンテナの指定範囲にある各要素に対して、対応する入力要素にジェネリック関数を適用した結果をセットするSTLアルゴリズムです。 これまで挙げてきた例では、出力Image Viewの各Pixelに対して、対応する入力Pixelの両隣のPixelの差分の1/2を割り当てる処理を行ってきました。 計算部分を関数オブジェクトとして抽象化すると、この処理はGILのtransform_pixel_positionを用いて次のように書くことができます。

struct half_x_difference {
    int operator()(const gray8c_loc_t& src_loc) const {
        return (src_loc.x()[-1] - src_loc.x()[1]) / 2;
    }
};

void x_gradient_unguarded(const gray8c_view_t& src, const gray8s_view_t& dst) {
    transform_pixel_positions(src, dst, half_x_difference());
}

GILは、Image ViewにおけるSTLのstd::for_eachstd::transform相当のものとして、for_each_pixeltransform_pixelを提供します。 また、ジェネリック関数に対してPixel参照の代わりにPixel Locatorを渡す、for_each_pixel_positionstransform_pixel_positionsも提供します。 このことは、渡されたPixel Locatorを通じて隣接Pixelを使用する、さらに強力な関数を可能にします。 GILアルゴリズムは、(1次元Pixel Iteratorを用いた1段のループではなく)より効率的な2段にネストされたループを用いて反復を行います。

色変換

画像の各色平面のgradientを計算するのではなく、明度のgradientを計算したい場合も多々あります。 言い換えると、カラー画像からグレイスケール画像に変換して、その結果のgradientを計算したい場合です。 32ビット浮動小数点型RGB画像の明度のgradientをいかに算出するか、次に示します。

void x_gradient_rgb_luminosity(const rgb32fc_view_t& src, const gray8s_view_t& dst) {
    x_gradient(color_converted_view<gray8_pixel_t>(src), dst);
}

color_converted_viewは、あらゆるImage Viewを引数に取り、(テンプレートのパラメータで指定した)目標のColor SpaceとChannel深度をもったViewを返す、GILのView変換関数です。 ここで示した例では、color_converted_viewは32ビット浮動小数点型RGB Pixelを指すViewから8ビット整数型グレイスケールViewを構築します。 他のView変換関数と同様に、color_converted_viewは浅く、非常に高速です。 この関数はデータのコピーも色変換も行いません。 そのかわりに、この関数はPixelへのアクセス毎にそのPixelの色変換を実行するViewを返します。

このアルゴリズムのジェネリックなバージョンで、Color Spaceをグレイスケールに変換してもそのChannel深度は変更しないほうがよいでしょう。 入力画像と同じChannel型をもつグレイスケールPixelを構築してそのPixelへ色変換することで、これを実現します。

template <typename SrcView, typename DstView>
void x_luminosity_gradient(const SrcView& src, const DstView& dst) {
    typedef pixel<typename channel_type<SrcView>::type, gray_layout_t> gray_pixel_t;
    x_gradient(color_converted_view<gray_pixel_t>(src), dst);
}

目標のColor SpaceとChannel型が偶然にも入力のそれらと同じだった場合、色変換は必要ありません。 GILはこのようなケースを検出し、色変換のコードが呼び出されるのを完璧に回避します。 つまり、このような場合のcolor_converted_viewは、入力Viewそのものを返すのです。

Image

上記の例はパフォーマンス上の問題を抱えています。 x_gradientは、ほぼ全ての入力Pixelを2回ずつ間接参照しており、それが原因で上記のコードは色変換を各Pixelで2回実行しています。 色変換を行った画像を一時的なバッファにコピーしてそのgradientを計算する(この方法であれば、色変換は各Pixelで1回で済みます)ほうが効率的な場合もあるかもしれません。 ジェネリックでないバージョンは、次のようになります。

void x_luminosity_gradient(const rgb32fc_view_t& src, const gray8s_view_t& dst) {
    gray8_image_t ccv_image(src.dimensions());
    copy_pixels(color_converted_view<gray8_pixel_t>(src), view(ccv_image));

    x_gradient(const_view(ccv_image), dst);
}

まず、入力画像と同じサイズの8ビットグレイスケールImageを構築します。 そして、この一時的なImageに色変換Viewから取得した各Pixelの値をコピーします。 最後に、一時的なImageのread-onlyなViewに、私たちが作成したx_gradientを適用します。 上記の例で示されているように、GILは、あるImageを引数に取り、そのPixelを指し示すmutableなViewとimmutableなViewを返す、グローバル関数viewconst_viewを提供します。

上記のコードのジェネリックバージョンは、少々トリッキーです。

template <typename SrcView, typename DstView>
void x_luminosity_gradient(const SrcView& src, const DstView& dst) {
    typedef typename channel_type<DstView>::type d_channel_t;
    typedef typename channel_convert_to_unsigned<d_channel_t>::type channel_t;
    typedef pixel<channel_t, gray_layout_t>  gray_pixel_t;
    typedef image<gray_pixel_t, false>       gray_image_t;

    gray_image_t ccv_image(src.dimensions());
    copy_pixels(color_converted_view<gray_pixel_t>(src), view(ccv_image));
    x_gradient(const_view(ccv_image), dst);
}

まず、出力ViewのChannel型を取得するためにchannel_typeというメタ関数を使用します。 メタ関数とは、型を扱う関数です。 GILのメタ関数は、テンプレートのパラメータを自身のパラメータとして取り、ネストされたtypedefで定義された型を返します。 今回のケースで言えば、上記の例の中のchannel_typeは、Image Viewの型を引数に取り、そのImage Viewに関連づけられているChannel型を返す、ひとつの引数を取るメタ関数です。

Pixel、Pixel Iterator、Locator、View、Imageといった関連づけられたPixel型をもつGILコンストラクトは全てPixelBasedConceptに基づいたModelであり、このことは、channel_typecolor_space_typechannel_mapping_typenum_channelsといったPixelのプロパティの問い合わせを行うメタ関数一式をGILが提供するということを意味します。

出力ViewのChannel型を取得した後、(それが符号付き整数型だった場合を考慮して)符号を取り除くメタ関数を用い、それから、グレイスケールPixelの型をつくるためにそのChannel型を使用します。 出来上がったPixelの型からImageの型を作成します。 GILのImageクラスは、Pixelの型と画像がプラナー形式なのかを示すboolean(インタリーブ形式の場合、falseを指定)でテンプレート化されています。 GILの単Channel (グレイスケール) Imageは、常にインタリーブ形式でなければなりません。 GILで型を構築する方法はいくつかあります。 直接的にクラスを具体化する代わりに、型生成メタ関数を使用することもできます。 上記のコードと次に示すコードは等価です。

template <typename SrcView, typename DstView>
void x_luminosity_gradient(const SrcView& src, const DstView& dst) {
    typedef typename channel_type<DstView>::type d_channel_t;
    typedef typename channel_convert_to_unsigned<d_channel_t>::type channel_t;
    typedef typename image_type<channel_t, gray_layout_t>::type gray_image_t;
    typedef typename gray_image_t::value_type gray_pixel_t;

    gray_image_t ccv_image(src.dimensions());
    copy_and_convert_pixels(src, view(ccv_image));
    x_gradient(const_view(ccv_image), dst);
}

GILは、GILの型を生成するメタ関数一式を提供します。 image_typeは、与えられたChannel型、Color Layout、プラナー形式/インタリーブ形式を決めるオプション(インタリーブ形式がデフォルトに指定されています)からImageの型を構築するメタ関数です。 GILは、ひとつ以上のプロパティを変更した(それ以外は元のままの)与えられた型からGILコンストラクトの型を構築するderived_pixel_reference_typederived_iterator_typederived_view_typederived_image_typeといったメタ関数ももっています。

Imageの型からは、Pixelの型を取得するために、ネストされたtypedefであるvalue_typeを使うことが出来ます。 GILのImage、Image View、Locatorは、ネストされたtypedefであるvalue_typeをもっており、Pixelの型とそのPixelへの参照を取得するための参照をもっています。 あるPixel Iteratorをもっている場合には、そのiterator_traitsからImageの型やImage Viewの型やLocatorの型を得ることができます。 色変換を伴ったcopy_pixelsの短縮表記バージョンである、copy_and_converted_pixelsアルゴリズムにも注目しておいてください。

Virtual Image View

ここまでは、メモリ上に保存されたPixelをもつ画像を扱ってきました。 GILは、合成関数を含む任意の画像についてのImage Viewを作成することが出来ます。 これを実演するために、マンデルブロ集合の画像を作ってみましょう。 最初に、与えられた画像中の座標(x,y)におけるマンデルブロ集合の値を計算する関数オブジェクトを作成する必要があります。

// models PixelDereferenceAdaptorConcept
struct mandelbrot_fn {
    typedef point2<ptrdiff_t>   point_t;

    typedef mandelbrot_fn       const_t;
    typedef gray8_pixel_t       value_type;
    typedef value_type          reference;
    typedef value_type          const_reference;
    typedef point_t             argument_type;
    typedef reference           result_type;
    BOOST_STATIC_CONSTANT(bool, is_mutable=false);

    mandelbrot_fn() {}
    mandelbrot_fn(const point_t& sz) : _img_size(sz) {}

    result_type operator()(const point_t& p) const {
        // normalize the coords to (-2..1, -1.5..1.5)
        double t=get_num_iter(point2<double>(p.x/(double)_img_size.x*3-2, p.y/(double)_img_size.y*3-1.5f));
        return value_type((bits8)(pow(t,0.2)*255));   // raise to power suitable for viewing
    }
private:
    point_t _img_size;

    double get_num_iter(const point2<double>& p) const {
        point2<double> Z(0,0);
        for (int i=0; i<100; ++i) {     // 100 iterations
            Z = point2<double>(Z.x*Z.x - Z.y*Z.y + p.x, 2*Z.x*Z.y + p.y);
            if (Z.x*Z.x + Z.y*Z.y > 4)
                return i/(double)100;
        }
        return 0;
    }
};

ここで、200x200 PixelのマンデルブロViewを構築するために、GILのvirtual_2d_locatorと共にこの関数オブジェクトを用います。

typedef mandelbrot_fn::point_t point_t;
typedef virtual_2d_locator<mandelbrot_fn,false> locator_t;
typedef image_view<locator_t> my_virt_view_t;

point_t dims(200,200);

// Construct a Mandelbrot view with a locator, taking top-left corner (0,0) and step (1,1)
my_virt_view_t mandel(dims, locator_t(point_t(0,0), point_t(1,1), mandelbrot_fn(dims)));

合成関数によるViewは実態をもつViewと同じように扱うことが出来ます。 例として、マンデルブロ集合を90度回転させたViewのgradientを計算するために私たちのx_gradientアルゴリズムを実行してみましょう。

gray8s_image_t img(dims);
x_gradient(rotated90cw_view(mandel), view(img));

// Save the Mandelbrot set and its 90-degree rotated gradient (jpeg cannot save signed char; must convert to unsigned char)
jpeg_write_view("mandel.jpg",mandel);
jpeg_write_view("mandel_grad.jpg",color_converted_view<gray8_pixel_t>(const_view(img)));

ふたつのファイルがどのようになっているか示します。

マンデルブロ集合

実行時に型を指定するImageとImage View

ここまで、テンプレート化されたImage Viewのgradient画像を算出するジェネリック関数を作成してきました。 しかし、Color SpaceやChannel深度などのImage Viewのプロパティをコンパイル時に利用できないといった場合だってあるかもしれません。 GILのdynamic_image extensionは、variantとも呼ばれる実行時に型が決まるGILコンストラクトとGILアルゴリズムが共に動作することを可能にします。 GILは、実行時に決まる型でインスタンス化されるImageのModelであるany_imageと、実行時に決まる型でインスタンス化されるImage ViewのModelであるany_image_viewを提供します。 このようなメカニズムは、any_pixelany_pixel_iteratorなどのような上記以外のvariantを作成するためのものは用意されていません。 copy_pixelsが2個の引数の片方もしくは両方にvariantを取ることが可能であるように、ほとんどのGILアルゴリズムと全てのView変換関数は、実行時に決まる型でインスタンス化されるImage Viewやアルゴリズムと共に動作します。

variantなImage Viewを引数に取るx_luminosity_gradientアルゴリズムを作ってみましょう。 簡単のために、入力Viewだけvariantにすることができるようにしてみましょう。 (複数のvariantを使った例は、複数のvariantを引数に取るようにオーバーロードしたGILのImage Viewアルゴリズムを参照ください。)

まず始めに、テンプレート化された出力Viewをもち、テンプレート化された入力Viewを引数に取るアプリケーションオペレータをもつ、関数オブジェクトを作成する必要があります。

#include <boost/gil/extension/dynamic_image/dynamic_image_all.hpp>

template <typename DstView>
struct x_gradient_obj {
    typedef void result_type;        // required typedef

    const DstView& _dst;
    x_gradient_obj(const DstView& dst) : _dst(dst) {}

    template <typename SrcView>
    void operator()(const SrcView& src) const { x_luminosity_gradient(src, _dst); }
};

つづいて、Image View variantを引数に取り、それを関数オブジェクトに渡すGILのapply_operationを呼び出す、x_luminosity_gradientのオーバーロードを提供します。

template <typename SrcViews, typename DstView>
void x_luminosity_gradient(const any_image_view<SrcViews>& src, const DstView& dst) {
    apply_operation(src, x_gradient_obj<DstView>(dst));
}

any_image_view<SrcViews>は、Image View variantです。 これは、variantが取りうるViewの型を列挙したリストであるSrcViewsでテンプレート化されています。 インスタンスがメモリのブロックの中に含まれるのと同じように、srcには現在インスタンス化されている型のインデクスが含まれています。 apply_operationはインデクスによるswitch文判定を実施し、各ケースごとに正しいViewの型へメモリをキャストし、そのViewと共に関数オブジェクトを実行します。 variant上で呼び出されたアルゴリズムは、switch文1個分のオーバーヘッドをもちます。 Image Viewの各Pixelに処理を実行するアルゴリズムは、variantと共に使用した場合にも、これといったパフォーマンスの低下は起こりません。

ここで、いかにvariantを構築し、いかにアルゴリズムを呼び出すのかを示します。

#include <boost/mpl/vector.hpp>
#include <boost/gil/extension/io/jpeg_dynamic_io.hpp>

typedef mpl::vector<gray8_image_t, gray16_image_t, rgb8_image_t, rgb16_image_t> my_img_types;
any_image<my_img_types> runtime_image;
jpeg_read_image("input.jpg", runtime_image);

gray8s_image_t gradient(runtime_image.dimensions());
x_luminosity_gradient(const_view(runtime_image), view(gradient));
jpeg_write_view("x_gradient.jpg", color_converted_view<gray8_pixel_t>(const_view(gradient)));

この例の中では、8ビット/16ビットでRGB/グレイスケールのImageになることができるImage variantを作成しています。 つづいて、画像をファイルに記録されているColor SpaceとChannel深度のまま読み込む、GILのI/O extensionを使っています。 読み込んだ画像が用意したいずれImageの型とも一致しなかった場合には、例外が投げられます。 そして、算出するgradientを記憶するために8ビット符号付き(すなわち、char) Imageを構築し、x_gradientを呼び出します。 最後に、その結果をもうひとつのファイルに保存します。 JPEG I/Oが符号付きcharをサポートしていないことから、8ビット符号無しViewに変換してから保存します。

jpeg_read_imagedimensionsviewconst_viewといった、テンプレート化された型とvariantな型の両方で動作する関数やメソッドがどのように振る舞うか注目しましょう。 variantなImageであればview(img)はvariantなViewを返す一方で、テンプレート化されたImageであればview(img)はテンプレート化されたViewを返します。 例を挙げると、view(runtime_image)の戻り値の型はany_image_view<Views>です。(ここのViewsは上記4種類のImageに対応する4種類のViewを並べたリストです。) また、const_view(runtime_image)は、4種類のread-onlyなViewの型によるany_image_viewを返します。

variantの使用に関して、ひとつ注意があります。 1個のvariantを引数に取るアルゴリズムをインスタンス化するとき、variantが取りうるあらゆる型でアルゴリズムがインスタンス化されます。 2個のvariantを引数に取るアルゴリズムでは、それぞれの入力型が取りうる全ての組み合わせでアルゴリズムがインスタンス化されます! これは、コンパイル時間と実行ファイルのサイズに多大な影響を与える可能性があります。

まとめ

このチュートリアルでは、GILによるジェネリックで効率的な画像処理アルゴリズムの記述に関する挑戦の一端をお見せしました。 シンプルなアルゴリズムを対象に、どのようにしてそのアルゴリズムを形式(Channel深度、Color Space、Channel順、プラナー/インタリーブ)が様々に異なる画像と共に動作させるのか示しました。 そのアルゴリズムが完全に実体のないVirtual Imageと共に動作すること、実行時に型の決まるImageと共に動作することを実演しました。 これに関連するプレゼンテーション動画では、複雑な状況であろうともGILアルゴリズムから生成されたアセンブリが、ハンドコーディングで特定の形式に特化したC言語によるアルゴリズムから生成されたアセンブリに匹敵することを実演しています。

ここまで、簡単なアルゴリズムを用いていたにもかかわらず、完璧にジェネリックで完璧に最適化されたコードの作成からは程遠いところにいます。 特に、現状のアルゴリズムはホモジーニアスImage(すなわち、中のPixelが全て同じ型のChannelをもっているImage)の上で動作するものになっています。 例えば、565 RGBフォーマットのように異なるChannelをもつ画像も存在します。 GILは、このようなヘテロジーニアスPixelを扱うConceptやアルゴリズムも提供しています。 x_gradientをヘテロジーニアスPixelに対応させるという拡張に関しては、これを読んでいるみなさんの練習問題とすることにしましょう。 その他にも、これまでgradientの値を計算した後には、それを目的のChannelの型へ単純にキャストしてきました。 しかし、これは必ずしも望ましい操作でなかったかもしれません。 例えば、入力Channelが[0..1]範囲の浮動小数点型で、出力Channelがunsigned char型であった場合、x_gradientの各Pixelをキャストしたものは全て0か1になるでしょう。 そうではなく、算出された結果を出力Channelのレンジに合わせてスケールしたいと考えるのではないでしょうか。 このようなケースでは、GILのChannelレベルのアルゴリズムが便利かもしれません。 例えば、channel_convertは入力Channelの値を出力Channel型のレンジに線形でスケーリングします。

パフォーマンスをさらに向上させるためにやることがたくさんあります。 ここまで行ってきたようなChannelレベルの操作は、アトミックなChannelレベルのアルゴリズムに抽象化され、具体的なChannel型に向けたパフォーマンスを考慮したオーバーロードが提供されるでしょう。 例えば、1行分のPixelを同時に処理するためにプロセッサ固有の操作が使われるかもしれませんし、データの先取りが行われるかもしれません。 このような最適化は、ジェネリックアルゴリズムがパフォーマンスに特化することで実現されるでしょう。 最後に、日を追うごとにどんどん良くなってはいますが、コンパイラは、関数のインライン化をしなかったり、いくつかの変数をレジスタに置いたりと、いくつかのケースでジェネリックなコードの完璧な最適化に失敗します。 パフォーマンスが問題になる場合には、異なるコンパイラにかけてみるのも良いかもしれません。

付録

GILが定める型の命名規則

あらかじめ定められている(ジェネリックでない)GILの型は、次に示す命名規則に従っています。

ColorSpace + BitDepth + [f | s]+ [c] + [_planar] + [_step] + ClassType + _t

このColorSpaceは色要素の順序を表します。 例えば、rgbbgrcmykrgbaなどがあります。 BitDepthは色Channelのビット深度を表します。 例えば、81632などがあります。 Channel型は、デフォルトで、符号なし整数型になります。 sが付くと符号付き整数型であることを表し、fが付くと(常に符号付きの)浮動小数点型であることを表します。 cは、imutableなPixelを扱うオブジェクトであることを表します。 _planarは、(インタリーブ形式ではなく)プラナー形式であることを表します。 _stepは、(逆向きや、数個とばし等)少し変わった方法でデータの走査を行うImage View、Locator、Iteratorであることを表します。 ClassTypeは、_image (Image)、_view (Image View)、_loc (Pixelの2次元Locator)、_ptr (Pixel Iterator)、_ref (Pixel参照)、_pixel (Pixel値)のいずれかとなっています。

bgr8_image_t             a;    // 8-bit interleaved BGR image
cmyk16_pixel_t;          b;    // 16-bit CMYK pixel value;
cmyk16c_planar_ref_t     c(b); // const reference to a 16-bit planar CMYK pixel x.
rgb32f_planar_step_ptr_t d;    // step pointer to a 32-bit planar RGB pixel.