この日本語訳について
- 原文:
-
- 原文の著者:
- Lubomir Bourdev (lbourdev@adobe.com) and Hailin Jin (hljin@adobe.com)
Adobe Systems Incorporated
- 原文のライセンス:
-
- 翻訳者:
-
- この文章のライセンス:
-
- 翻訳日時:
- 2008年7月7日開始
- 2008年7月12日初稿公開
この文章はBoost.GILのチュートリアルを日本語に訳したものです.この訳はAdobeを含む原文の著者や権利者とまったく関係がありません.また,内容の正確さはいっさい保証されません.
Generic Image Library チュートリアル
著者:
- Lubomir Bourdev (lbourdev@adobe.com) and Hailin Jin (hljin@adobe.com)
Adobe Systems Incorporated
- Version:
- 2.1
- 作成日時:
- 2007年9月15日
Generic Image Library (GIL)はアルゴリズムによる画像生成を抽象化し,様々な画像フォーマットに対応したコードを特定のフォーマットに依存したコードに近いパフォーマンスで動作するように記述することを可能にするC++ライブラリです.
この文書はGILのjump-startをあなたに提供するでしょう.この文書はGILライブラリの内部デザインについては論じませんし,ライブラリのすべてを扱うこともしません.ライブラリのデザインについての詳細な文書はGILのウェブサイトhttp://opensource.adobe.com/gilで手に入れることができます.
最新versionのGILはGILのウェブサイトhttp://opensource.adobe.com/gilからダウンロードすることができます.GILはBoostライブラリに受理されており,ちかい将来にはhttp://www.boost.orgからBoostをインストールすることで簡単にインストールされるようになるでしょう.GILはヘッダファイル群だけで構成されていて,他のライブラリへのリンクを必要としません.ビルドの際にBoostライブラリを必要とすることもありません.ほとんどのプロジェクトではboost/gil/gil_all.hppをインクルードすればよいでしょう.
このチュートリアルはgradient画像を算出するというGILの使用例を通して進めていくことにします.ごく単純なジェネリックでないコードからスタートして,それを少しずつジェネリックなコードにしていきましょう.水平方向gradientのもっとも単純な近似である中心差分を使ってはじめます.ピクセルxにおけるgradientはその2つの隣接ピクセルによる差分の半分で近似することができます: D[x] = (I[x-1] - I[x+1])
/ 2
簡単のために境界条件(画像の端に存在し,どちらか一方の隣接ピクセルが定義されていないピクセル)は無視することとします.この文書の主題はどのようにGILを使うのかであり,どのようにしてよいgradient画像生成アルゴリズムをつくるかではありません.
まず,8-bit unsignedのグレイスケール画像を入力とし,8-bit signedのグレイスケール画像を出力として始めることにしましょう.ここで,我々のアルゴリズムへのインタフェースがどのようなものかを示します.
#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は入力するimage viewの型であり,ピクセルがread-only ("c" によって定められている)に指定された8-bitグレイスケールのviewです.出力は8-bit signed
("s"
によって定められている)整数型channelをもつグレイスケールのviewです.型の名前を決めるGILのネーミング規定については付録1をみてください.
GILではimageとimage viewを区別します.GILのimage
viewは矩形領域を構成するピクセル群への安全で軽快なviewです.それはピクセル群へのアクセスを提供しますが,ピクセル群そのものではありません.viewのコピーコンストラクトはピクセル群のdeep-copyではありません.image
viewに付加されたconst指定はピクセル群にまで適用されないので,image viewは常にconst参照によって用いるべきです.viewがmutableであるかread-only (immutable)であるかはviewの型によって決まる特性です.
一方,GILのimageは所有権をもつviewの一種です.imageはピクセル群のコンテナです.すなわち,そのコンストラクタ/デストラクタはピクセル群のメモリについて確保/解放を行い,コピーコンストラクタはピクセル群のdeep-copyを行い,operator==はピクセル群のdeep-compareを行います.imageでは自身に付加されたconst指定がピクセル群にも伝搬し適用されるので,imageのconst参照はピクセル群への変更を許可しません.
ほとんどのGILアルゴリズムはimage
viewに対する処理であり,imageが必要とされることはめったにありません.GILのデザインはSTLのデザインと非常によく似ています.STLにおいてGILのimageに相当するものとしてstd::vectorなどのコンテナが挙げられ,そのときGILのimage
viewはSTLにおける一組のiteratorで指定された処理対象の範囲に対応しています.STLアルゴリズムが指定範囲に対する処理であるのとちょうど同じように,GILのアルゴリズムはimage viewに対する処理なのです.
GILのimage viewは幅と高さ,一行あたりのバイト数,ひとつのポインタであらわされているピクセル群のメモリというraw dataから構成することもできます.ここに自身のコードと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); }
このコードはとても高速であり,上記の例で16-byteをもつ2つのviewはとても軽量です.それらは,最上左点を示すポインタと3つの整数値 (幅,高さ,一行あたりのバイト数) で構成されています.
処理速度についてのわかりやすさに重点を置いて,次に示すように水平方向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)を用い,そこに左右の隣接画素の半差分を代入しています.operator()はグレイスケールピクセルの参照を返します.グレイスケールピクセルはそのchannel型(この例ではsrcにとってunsigned
char)を変換可能であり,channelによるコピーコンストラクトが可能です.(これはグレイスケールピクセルにおいてだけです).上記コー
ドは読みやすいけれどさほど高速ではなく,その原因としてバイナリの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はrandom access iteratorです.もしrandom access 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を用いています.このとき隣接ピクセルのメモリ上での距離は各画像の1行あたりのbyte数と等しくなっていて,単純なポインタを用いることはできません.GILではここで8
byteの特別なstep iteratorを用います.これはCポインタとステップ幅をもっています.そのoperator[]はindex値にステップ幅を掛けています.
しかしながら,上記のy_gradientはそのメモリアクセスのパターンが原因でx_gradientに比べて非常に低速です.垂直方向への画像の横断は多くのキャッシュを無駄にすることになるからです.より効果的でキャッシュフレンドリなコードでは垂直方向のループ内で水平方向iteratorを移動させます.
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; } } }
このサンプルコードでは,operator[]によってインクリメントと参照外しを行う方法のかわりに,pixel iteratorを使うという方法を示しています.
残念なことに,このキャッシュフレンドリなコードでは扱いが面倒なiteratorを入力viewに対して2つも必要としています.全てのピクセルにおいてその上下の隣接ピクセルにアクセスしたいといった場合には,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のlocatorは,水平方向と垂直方向へともに移動できることを除いて,iteratorととてもよく似ています.上記のコードで示されるとおり,src_loc.x()とsrc_loc.y()は,任意の位置へlocatorを移動させるために用いられる,それぞれ水平方向と垂直方向のiteratorへの参照です.加えて,locatorは
operator+=とoperator-=を用いることで両方向へ同時に移動することができます.image
viewと同じように,locatorは現在の位置から相対的に指定された画素の参照をバイナリのoperator()で提供します.例えば,src_loc(0,1)は現在指し示しているピクセルの下側に隣接するピクセルの参照を返します.locatorは,上記の例において8
byteであるように,非常に軽量なオブジェクトです.それは,現在位置を示すポインタと垂直方向への移動に用いる1行先の同位置までの距離を示す整数値を含んでいます.++src_loc.x()はCポインタにおけるインクリメントに相当します.ところで,上記の例では必要以上の計算が行われています.コード内のsrc_loc(0,1)は2方向についての補正距離を計算しなければならず低速なのです.ここで,両隣ピクセルへの補正距離が現在の座標にかかわらず常に同じであることに着目しましょう.パフォーマンス向上のために,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]"が高速なポインタのインデクシング演算子に相当し,このコードは効率的なものになっています.
x_gradientのコードをさらにgenericなものにしていきましょう.image viewが同じchannel数であるかぎりどのような型でも動くようにするべきです.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型をtemplateのパラメータとして用います.ここでは,ユーザ定義のimage
view型とGILのimage
view型の両方の使用を認めます.最初の3行については任意です.これらはboost::concept_checkを用いて二つのパラメータが有効な
GILのimage view型であることを保証し,2行目のコードでは書き換え可能なview型であることを,その次の行はcompatible color
spaceであることを保証しています.
GILでは型のコンストラクタ内部でこれらを用いることを要求していません.あなたが定義するchannel, color space,
iterator, locator, view,
imageのコンストラクタ内部であなたがこれらを用いるかどうかは自由です.しかしながら,その他の部分をGILと組み合わせて使っていく際にはこれらの要求を満たすようにすべきです.言い換えれば,GIL conceptを満たすように設計すべきです.GIL
conceptはユーザガイドの中で定義されています.
C++におけるtemplateとジェネリックプログラミングの最大の欠点のひとつはコンパイルエラーの意味を読み取ることが非常に難しいことです.これは型の判定を遅らせることの副作用です.ジェネリック型で指定された引数は関数からの要求を満たしていない可能性をもちますが,その不一致は扱いづらいコードの中でもほとんど関係のない何重にもネストされた関数コールによって引き起こされていると考えられます.GILではこの問題を軽減するためにboost::concept_checkを用いています.上記の3行では,templateのパラメータが関係するconceptを満たした設計であるかどうかを調べています.設計が正しくない場合には,実際の問題により近く追跡がより簡単になるように
gil_function_requiresの内部でコンパイルエラーが発生します.加えて,これらのチェックを含んだコードはコンパイルが通れば,その先のパフォーマンスへの影響については考える必要がないのです.コンセプトチェックを用いることの欠点はコンパイル時間に重大な影響を与える場合があるということです.GILがdebug
modeでBOOST_GIL_USE_CONCEPT_CHECKが定義されている場合(デフォルトではoff)にだけコンセプトチェックを行っているのはこのような理由からです.
ジェネリック関数の本体は固定型の関数のものとよく似ています.もっとも大きな違いとしては,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_genrerate, static_fill,
static_for_eachなどがあります.これらはchannelレベルにおけるSTLアルゴリズムのgenerate, transform,
fill, for_eachとそれぞれ同等なものです.GILのchannelアルゴリズムはループを用いないために静的再帰を用いています.それは各channelへの単純なループは用いません.上記の例などでは,いくつかのモダンなコンパイラであれば
(visual studio 8であっても)
channelレベルのループは行われないでしょう.しかし,GILのchannelレベルアルゴリズムを用いるもうひとつの利点として,メモリ上の順序をもとにしたものではなくセマンティックなchannelの組に対してのアルゴリズムであることを挙げることができます.例えば,上記の例は入力がRGBで出力が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); }
これらの例が示すのは入力と出力のどちらに関しても,interleavedであってもplanarであっても,どのようなchannleの深度(入力から出力へと割当が可能であると仮定)でも,どのようなcompatible color spaceであってもかまわないということです.
GIL 2.1は,6-bit RGB222 imageや1-bit Gray1
imageといったbyte単位ではないchannelをもつimageについてもネイティブに扱うことができます.そしてGILアルゴリズムはこれらのimageをネイティブに受け付けます.このようなimageのさらに詳しい使い方についてはデザインガイドとサンプルファイルを参照してください.
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において,入力されたものを半時計回りにしたimage
viewを返します.これはGILにおけるview変換関数の一例です.GILは,軸での回転,viewの置換,垂直方向または水平方向への反転,矩形領域のサンプリング,色空間の変換,viewのサブサンプリングなど様々な変換関数を提供します.view変換関数は高速で浅いものです.これらはピクセルをコピーしませんし,これらは単にピクセルへのアクセス手順を編集しているにすぎません.例を挙げれば,rotated90ccw_viewは元となるviewの垂直方向のiteratorを自身の水平方向iteratorとしてもつviewを返します.かつてのy_gradientを算出するコードはメモリへのアクセスパターンが原因となって低速でしたが,rotated90ccw_viewを用いたコードでは少しも遅くなることはありません.
例をもうひとつ.カラーimageの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をsingle channel
(grayscale)のviewとして返すview変換関数です.例えば,この変換関数はinterleaved
RGB型のviewに対してはインクリメントされた際に2つのchannelをスキップする水平方向iteratorをもったviewであるskip
viewを返します.planar
RGB型のviewに用いられた場合には,返されるviewが水平方向iteratorとしてCポインタをもつ単なるgrayscale型のviewになります.image
viewの変換関数は互いにつなげることができます.例えば,viewの2番目のchannelについてy_gradientを算出する場合には次のようにします.
y_gradient(subsampled_view(nth_channel_view(src, 1), 2,2), dst);
GILでは連結されたviewを簡略化することができます.例えば,入れ子にされた2つのサブサンプリングview(X軸方向とY軸方向にそれぞれスキップするview)を,2つのviewから提供されたstep iteratorを自身のstep iteratorとする1つのサブサンプリングviewとして表現することができます.
いま一度x_gradientの話に戻りましょう.多くのimage
viewアルゴリズムではそれぞれのピクセルに対して同じ処理を行い,GILはそのなかで処理の抽象化を担っています.しかし,ここまでのアルゴリズムでは最初と最後の列についてスキップするという不規則なアクセスパターンをとっていました.これをどのように正規の手順へと書き換えるのかを知るのは有意義でよいことでしょう.GILでこれを実現するには,最初と最後の列を除いたsubimageを用意して,その全ピクセルに対して処理を行うという方法をとります.
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変換関数のもうひとつの例です.入力viewと矩形領域の指定(この例では,min_x,
min_y, width,
height)によって,入力view内の処理対象となる領域がviewとなって返ります.上記の書き換えでは,もとのviewに対して処理を行なっていたバージョンと比較しても,観測できるようなパフォーマンスの低下は生じません.
ここで,全ピクセルを対象とする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内の全ピクセルを左から右へ,上から下へ順に一次元走査するiteratorを示すbegin(),
end()という関数を提供します.これは,各行の末尾に存在する可能性がある未使用の領域はスキップして,完璧な"キャリッジリターン"を行います.ここではわずかな最適化の余地を残していて,それはこれらのiteratorが行の最後を考慮するために自身の位置を常に記録してお
くことに由来します.インクリメントを行うオペレータは(いま行の最後を指しているか)追加の判定を行っていて,この判定は代わりに2つの入れ子にされた
ループを用いる場合には避けられます.これらのiteratorは,以前のコードで用いたように,さらに軽量な水平方向iteratorを返すx()という関数をもっています.水平方向iteratorは行の終りについての情報をもっていません.今回の場合には,水平方向iteratorはCポインタとなっています.この例では,image viewの外に属している可能性がある隣接ピクセルへ適切にアクセスするために,水平方向iteratorを用いなければなりません.
GILはSTLに相当する多くのアルゴリズムを提供しています.例えば,std::transformは出力コンテナの指定範囲にある要素に対して,対応する入力コンテナの指定範囲にあるそれぞれの要素にジェネリック関数を適用した結果を割り当てるア
ルゴリズムです.これまでの例で,それぞれの出力ピクセルに対して対応する入力ピクセルの水平方向の隣接ピクセルによる半差分を割り当てるアルゴリズムをつくってきました.演算部分を関数オブジェクトとして抽象化すると,この処理は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はSTLのstd::for_eachやstd::transformのimage
viewでの処理に相当するものとして,for_each_pixelやtransform_pixelを提供しています.また,ジェネリック関数に
ピクセルへの参照の代わりにピクセルlocatorを渡して処理を行うfor_each_pixel_positionsや
transform_pixel_positionsといったアルゴリズムも提供しています.これは,渡されたlocatorを通じて隣接ピクセルの利用を行う,さらに強力な関数を可能にします.GILアルゴリズムは(1次元iteratorを用いた1重ループではなく)より効果的な2重のループを用いてピクセル群
への逐次処理を行います.
画像の各色平面に対してgradientを算出するかわりに明度値のgradientを算出したい場合があります.言い換えれば,カラー画像からグレイスケール画像へと変換した画像のgradientを算出したい場合です.ここで,32-bit float RGB
imageの明度の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はあらゆる型のviewを入力として受け取り,templateのパラメータとして指定された目標のcolor spaceとchannel深度をもつviewを返すGILのview変換関数です.ここで示した例では,32-bit float
RGB型のピクセルから8-bit integer
grayscale型のviewが作られています.これ以外のGILのview変換関数と同様に,color_converted_viewはとても高速で浅いものです.この関数ではいかなるデータのコピーも色変換も行われません.そのかわりに,ピクセルへのアクセスごとに色変換が実行されるviewが返されます.
このアルゴリズムのジェネリックバージョンでは,そのcolor spaceをグレイスケールへ変換するときにはchannel深度を維持するほうがいいかもしれません.入力と同じchannel深度をもつGILのグレイスケール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が入力のcolor spaceが同じだった場合に色変換は必要のない処理になります.GILではこのような場合を検出して色変換コードの呼び出しを回避します.すなわち,color_converted_viewは入力されたviewそのものを返すのです.
上記の例はパフォーマンス上の問題を抱えています.x_gradientは入力ピクセルのほとんどを2度参照していて,このことが上記のコードにおいて各ピクセルに2度色変換を実行してしまう原因となっています.一時的なバッファimageに色変換を行った結果をコピーしてそのimageに対してx_gradientを計算するようにすれば,1つのピクセルに対して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-bitグレイスケールimageを作成します.そして色変換したviewをこの一時的なimageにコピーします.最後にこの一時的なimageのread-onlyなviewにx_gradientを適用します.例が示すように,GILは書き換え可能なviewと書き換え不可なviewをそれぞれ返すグローバル関数であるviewとconst_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において,メタ関数はtemplateのパラメータを自身のパラメータとして,入れ子にされている型のtypedefを返します.今回の場合にはこの例の中のchannel_typeがimage view型をパラメータとして呼ばれ,そのimage
view型に結びつけられているchannel型を返しています.
GILのpixel型に結びつけられたpixel, pixel iterator, locator, view,
imageといったものは全てPixelBasedConceptにもとづいた設計がなされていて,それはこれらのものが,channel_type,
color_space_type, channel_mapping_type,
num_channelsなどといった,ピクセルの詳細を引き出すメタ関数のセットを提供することを示しています.
出力viewのchannel型を得た後に,(符号付き整数型であった場合のために)符号を取り除くメタ関数を用いて,そしてそれをグレイスケー
ルのpixel型を作るために使用します.pixel型からimage型を作ります.GILのimageクラスはpixel型とplanarかinterleavedかの真偽値によって規定されるようにテンプレート化されています.GILにおけるsingle-channel
(grayscale)
imagesは常にinterleavedと決まっています.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はそのようなメタ関数のひとつであり,与えられたchannle型とcolor
layout型とplanar/interleavedを決めるオプション(デフォルトはinterleaved)によってimage型を生成します.これによく似たそれぞれpixel reference, iterator, locator, image
viewの型を生成するメタ関数もあります.GILは,derived_pixel_reference_type,
derived_iterator_type, derived_view_type,
derived_image_typeといった,ひとつ以上の情報を変更しそれ以外の情報を維持したGILの型を生成するメタ関数をもっています.
image型からpixel型を得るために入れ子にされたtypedef value_typeを使用することができます.GILのimage, image view, locatorは入れ子にされたtypedef
value_typeと,pixel型とpixelへの参照を得るための参照をもっています.pixel
iteratorをもっている場合にはiterator_traitsを用いてpixel型を得ることができます.入力viewへの色変換処理を追加した
copy_pixelsを短く表現しているcopy_and_converted_pixelsにも注目しておきましょう.
これまではメモリ上にピクセルデータをもっている画像を扱ってきました.GILでは合成関数による画像を含む任意画像についてimage
viewの作成が可能です.これを実演するためにMandelbrot集合のviewを作ってみることにしましょう.まず,画像のlocation
(x,y)におけるMandelbrot集合の値を計算する関数オブジェクトを作成する必要があります.
// 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 pixelsのMendelbrotの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と同じように扱うことができます.例として,Mandelbrot集合のviewを90度回転させた
gradient画像を算出するためにこれまでに作ったx_gradientを用い,オリジナルのMandelbrot集合の画像と処理後の画像をそれぞれ保存してみましょう.
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)));
その2つのファイルがどのようなものになるか示します.
これまではテンプレート化されたimage
viewについてのgradient画像算出をおこなうジェネリック関数を作成してきました.しかしながら,コンパイル時においていくつかのcolor
spaceやchannel深度などimage
viewの詳細を利用できない場合もあります.GILのdynamic_image_extensionは変異体を呼ぶことによって,実行時に決定されるGILコンストラクトの動作を可能にしています.GILは即席のimageモデルをany_imageとして,即席のimage
viewモデルをany_image_viewとして提供しています.その仕組みはany_pixelやany_pixel_iteratorなど他の変異体を作成することで実現されています.copy_pixelsが一方もしくは両方の引数に変異体をとることが可能であるように,ほとんどのGILアルゴリズムと全てのview変換関数は実行時に決定するimage viewとbinaryアルゴリズムで動作します.
我々のx_gradientアルゴリズムに変異体のimage
viewを適用させてみましょう.簡単のために入力のimage viewだけが変異体である場合を試してみましょう.(例として,多重変異体を用いるようにオーバーロードした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をとり,GILのapply_operationから関数オブジェクトを呼び出すよう設計したx_luminostiy_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は変異体のimage viewです.それはSrcViewをテンプレート化したもので変異体がとることのできるimage
viewの一覧です.srcはメモリのブロックでインスタンスをもっているのと同様に,内部にいま具体化されている型のインデクスをもっています.apply_operationはインデクスによる切り換え判定を実施し,メモリを正しいview
typeへとキャストし,関数オブジェクトを呼び出します.変異体から呼び出されたアルゴリズムはひとつの切り換え判定処理によるオーバーヘッドをもっています.画像の各ピクセルに処理に演算をおこなうアルゴリズムは,変異体への適用においても実用上のパフォーマンス低下はありません.
変異体の構築とアルゴリズム呼び出しの方法を示します.
#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-bit RGBか16-bit RGBかgrayscaleのimageをとることができる変異体のimageを作成しています.ここではファイルからネイティブのcolor spaceとchannel深度で画像を読み込むGILのI/O
extensionを用いています.読み込んだimageが受け付ける型のいずれとも一致しない場合には例外が投げられます.gradient画像を保存
するために8-bit
signed型(すなわちchar型)のimageを作成してx_gradientを呼び出しています.そして最後に,もうひとつのファイルへ結果を書き出しています.JPEG I/Oがsigned char型に対応していないことから8-bit
unsigned型への変換を行ってから保存しています.
jpeg_read_image, dimensions, view,
const_viewなどといった関数やメソッドがどのようにテンプレートによる型と変異体型の両方で動作するのか注目しておきましょう.view(img)はテンプレート化されたimageにはテンプレート化されたviewを返し,変異体のimageに対してはview
variantを返します.例えば,view(runtime_iamge)の戻り値の型はany_image_viewであり,ここでViewsは4つ
のimage型に対応した4つのview型を列挙したものになっています.他にも,const_view(runtime_image)は4つのread-onlyなview型によるany_image_viewを返します.
変異体の使用に関して注意を述べます.変異体への処理を行うアルゴリズムの具体化は変異体が取りうる全ての型について効率的に行われます.binaryアルゴリズムでは,入力される2つの型が取りうる全ての組み合わせについて具体化が行われます!これはコンパイル時間と実行サイズに大きな影響を及ぼします.
このチュートリアルでは,ジェネリックかつ効率的な画像処理アルゴリズムをGILで記述する試みの一端を紹介しました.ごく単純なアルゴリズムを用いて,さまざまなビット深度,color
space,channel順序,planar形式やinterleaved形式といった異なる条件においても動作するように設計する方法を示しました.ここで示されたアルゴリズムが,例えば実行時に型が決定されるような,完全に抽象化された仮想imageにおいても動作することを示しました.関連する動画によるプレゼンテーションでも,複雑な行程をたどりはしますが,出来上がったアセンブリは
特定のimage型に特化した手書きによるC言語によるアルゴリズムと匹敵することを解説しています.
しかし,これほど簡単なアルゴリズムを用いたにもかかわらず,完璧にジェネリックで効率的なコードを構築するにはまだ遠いところにいます.特に,ここまでのアルゴリズムはhomogeneousな画像,すなわちピクセルがもつchannelの組が全て同じ型をもつ画像で動作するものでした.画像のなかに
は,例えばpacked 565 RGB
formatのように異なる型のchannelの組によって構成されものも存在します.GILはheterogeneousピクセルにおいて動作するコンセプトとアルゴリズムを提供していますので,それを用いたx_gradientの拡張については読者の練習問題として残しておくことにします.つぎに,これまではgradientの値を計算した後は単に目的のchannel型へキャストしてきました.これはかならずしも望んだ操作ではないかもしれません.
例えば,入力channelが幅 [0..1] の浮動小数点型で,出力channelが符号なし
char型であった場合に,キャストされた半差分の値はunsigned
char型における0か1になっているでしょう.しかしその一方で,出力channel型がもつ幅へと変換されていたらいいなと考えています.GILのchannelレベルのアルゴリズムは,このようなケースで便利かもしれません.例えば,channel_convert関数は入力channelの値を出力channel型がもつ幅に対応した値へと線形に変換します.
さらなるパフォーマンス向上のために多くのことが考えられます.半差分を例にすると,channelレベルの操作は各channelレベル毎のアルゴリズ
ムへと抽象化がなされ,パフォーマンスの具体的な程度はchannel型によって決まるようになるでしょう.1行のピクセル全てを同時に操作するといった,特定のプロセッサに依存した操作が用いられるようになるでしょう.もしかしたら,データの先読みも行われるかもしれません.これら全ての効率化はジェネリックアルゴリズムにおけるパフォーマンスへの特化によって実現されるでしょう.最後に,時を追う毎にどんどん良くなってはいますが,コンパイラはまだいくつかの場合に関数のインライン化をしなかったり変数をレジスタに置いたりとジェネリックなコードの完全な最適化に失敗します.パフォーマンスが問題になる場合には,違うコンパイラにかけてみるとよいかもしれません.
あらかじめ定められているGILの(ジェネリックでない)ネーミング規定は次のようなものです.
ColorSpace + BitDepth + [f | s]+ [c] + [_planar] + [_step] + ClassType + _t
ColorSpaceは構成要素の順序も示しています.例を挙げるとrgb, bgr, cmyk,
rgbaなどがあります.BitDepthはcolor channelのビット深度を示しています.例を挙げると8, 16, 32
などがあります.channelタイプはデフォルトでは符号なし整数型となっていて, sは符号あり整数型,
fは浮動小数点型(これが指定されたときは常に符号あり)です.cは変更不可のピクセルを対象としたオブジェクトであることを示します._planarは
(interleaved 形式と対をなす)
planar形式で構成されていることを示します._stepは特別な規則(例えば,逆順やひとつおきなど)に従って移動する特殊なimage
view, locator, iteratorであることを示します.ClassTypeは_image (image),_view
(image view),_loc (2次元のlocator),_ptr (pixel iterator),_ref (pixel
reference),_pixel (pixel value)があります.
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
|