Last update 1999/08/07

VAFXImg入門

(C)平山直之
無断転載は禁止、リンクはフリー
誤字脱字の指摘は歓迎


はじめに

VAFXImgは我ながら非常に便利なライブラリだと自負しているのですが、やはり人に使ってもらうにはドキュメントが不足していると言わざるを得ません。そこで、

  1. 仕組み

  2. 使い方

について、それなりに説明します。

こんな人が対象

VAFXImgはC++の「テンプレート」という機能を使っていますので、普段C/C++コンパイラをCコンパイラとして使ってる人も、C++コンパイラとして使わなければなりません。VAFXImgをただ使うだけなら大して難しくはないので、C++をbetter Cとして使うのもよいかと思います。これ以後もそのような前提で説明します。よく分からないことがあったらBBSで質問してください。

※要するにCが解っている人向け※

VAFXImgの目的

VAFXImgとは、2Dイメージの処理を

  1. データ構造

  2. マッピングのアルゴリズム

  3. データ構造へのアクセス方法

の3つの要素に分割し、それぞれを組み合わせることによって最終的な処理を行うように直交性を重視して設計されたライブラリです。

なんのための直交性?

なぜVAFXImgで直交性を重視したのか? それは、プログラムの再利用性を最重要視したためです。

イメージ処理と一言で言っても、データ構造にはいろいろなものがあります。Windowsに絞って考えても、DIB、DDB、パレットBMP、フルカラーBMP、トップダウンBMP、ボトムアップBMP、とさまざまな種類があります。しかもそれらの属性は完全に独立するものではなく、いくつかの属性が組み合わさっているものなのです。

一方、イメージ処理のアルゴリズムには、矩形転送、幾何図形の描画、アフィン変換など、こちらもさまざまなものがあり、必要です。

これまでは、こうしたデータ構造アルゴリズムの中でパラメタライズする方法がありませんでした。標準ライブラリのqsortよろしく関数ポインタを使えば不可能ではなかったでしょうが、一般に画像処理アルゴリズムでは、膨大な数の繰り返しが行われます。すなわち、画素に対していちいち関数が呼び出される(しかもアセンブラレベルで言えば間接アドレッシングで)ことになるのです。画像処理が必要な分野自体がゲームなど処理速度を要するものであったため、そのようなアプローチは現実的には不可能でした。

ですから、イメージ処理ライブラリの作成者は、おのおののデータ構造に対して、またアルゴリズム、アクセス方法に対して、ほとんど変わらない関数を全て生成することを余儀なくされていたのです。

例として、拡大縮小回転関数に「パレット/フルカラービットマップ」「αブレンディングあり/なし」などの属性が必要になることを考えてみましょう。この場合、

ZoomPaletteBMP(PImage src,PImage dest,...)
ZoomPaletteBMPAlphaBlending(PImage src,PImage dest,...)
ZoomFullColorBMP(PImage src,PImage dest,...)
ZoomFullColorBMPAlphaBlending(PImage src,PImage dest,...)

の4つの関数が必要になることになります。もちろん、これに要素が増えれば増えるだけ、バイバインを垂らしたまんじゅうのごとく必要な関数の数が増えていくことになるのです。例えば、パレットBMPをフルカラーBMPに展開する関数を作るとしたらどうなるでしょうか?

この事実は、保守に大きな負担を与えます。アルゴリズムに一つバグを発見したら、それを実装したすべての関数を修正しなければならないからです。また要素が増えるたびに関数を増やさなければならないということは、「ライブラリ化」というそもそもの目的に対して大きな障害となります。結局面倒になって、ローカルなソースにコピーして必要なところだけ改ざんする、という結果になりがちだからです。

これらのことから、これまでは、「特定のデータ構造」を対象に「特定の処理を行う」ものしか作られませんでした。周辺の要素に多少の差があっても、描画アルゴリズム自体はそれほど大きく変わるものではないのに、これは大変にもったいないことです。

しかしわれわれは今や、テンプレートという優れた武器を手にしています。このテンプレートを使えば、今までは困難だったこうしたアルゴリズムの再利用が、実用的なレベルで可能になるのです。

なぜテンプレート?

C++のテンプレート機能の特長はさまざまな資料で語られていると思いますが、私がこのライブラリの構築においてもっとも重要であると考えたのは、「テンプレートが関数のインライン展開と組み合わせると大きな力を発揮する」点です。

先ほど説明したように、関数ポインタを利用すれば同じようなことは不可能ではありません。しかし、膨大な繰り返しが行われる画像処理の中では関数呼び出しは重すぎる処理であり、避けなければなりません。

そこでテンプレートをうまく使うと、こうした関数がインライン展開されることを期待できるのです。

このことは、私が思い付いたことではありません。前例があるのです。そう、STLです。私は、概念的には、シーケンス(つまり1次元のデータ)の処理システムであるSTLを、2次元空間に同じやり方で適用しただけなのです。

VAFXImg利用の準備

思想は理解して頂けたと思いますので、能書きはこのくらいにして、実際に使ってみましょう。

VAFXImgはヘッダファイルで提供されます。当該ヘッダファイルをインクルードすれば事足ります。もしVAFXImg.hppの置かれている場所にインクルードパスが通っているなら、

#include <VAFXImg.hpp>

とします。

ファイルの読み書きもしたい場合には、ついでに

#include <VAFXWUty.hpp>

としてください。おっと、言い忘れてましたが、VAFXではリソース類(ファイル、メモリ、Windowsリソースなど)へのアクセスも一般化して直交にしています。詳しくはVAFXUtilの「class chunk」「class memory_adapter」を参考にしてください。VAFXWUtyにはchunkから継承したresource_adapter/file_mappingが含まれています。iostream版も作るべきなのでしょうが、使わないのでほっといてあります。誰か作ったら私に教えてください。

ネームスペース

C++の新機能に、「ネームスペース」というものがあります。これはライブラリなどでの名前の衝突を避けるための機能ですが、面倒なので説明は省略します。知りたい人はC++の本を読んで下さい。とりあえずここでは、まじないだと思って、ソースファイルの最初のほう(ヘッダファイルの次くらい)に

using namespace vafximg;

と書いておいてください。

名前空間が解る人への説明

ちなみに、私はusingしないで、いちいちvafximg::と指定しています。タイピングが面倒でなければそうした方がよいと思います。

使用

準備

さて、イメージ処理ライブラリであるからには、イメージがないと話になりません。VAFXImgに盛り込まれているアルゴリズムは、必要なインターフェイスを持つオブジェクトならばどれにでも適用できるように作られていますが、全く何もないのも不便なので、ほぼ最低限必要なインターフェイスだけを実装したシンプルなクラスがあらかじめ定義されています。vafx::Imageクラスです。

まず、このクラスを使って、イメージオブジェクトを作ります。

vafximg::Image<24> img;

としてください。これで、24ビットカラーのイメージオブジェクトが作られます。

vafx::Imageオブジェクトは生成直後は大きさが0×0なので、ファイル等から読み込む(自動的に設定されます)か大きさを指定してください。ファイルから読み込むには、

img.InitWith(vafxwuty::file_mapping("test.bmp"));

としてください。指定したchunkをWindows DIBとみなして読み込みます。

大きさを指定するには、

img.SetExtent(st2d::Extent(320,240));

とします。これで準備はOKです。

補足:

VAFXImgでは、Imageはパレットを含みません。実用上その方がオーバーヘッドが小さくて便利だと判断したからです。また、イメージの動的なビット数の変更などにも対応してません。VAFXImgの主な使用目的がゲームであり、必要ないと判断したからです。

ただし、繰り返しになりますが、VAFXImgのアルゴリズムは必要なインターフェイスを持つオブジェクトならどんなオブジェクトにでも適用できます。それが仮想関数であっても内部で関数を呼び出すものでも構わないのです。ですから、スピードを心配する必要がなければ、Imageの代わりにそのようなインテリジェントなクラスを作って、そのインスタンスにVAFXImgのアルゴリズムを適用することも不可能ではありません。

加工

次に、仕組みを説明しながらこのイメージを加工してみましょう。

点を打つ

点を打つときには、

img.SetPixel(st2d::Point(17,24),vafximg::Pixel<24>(255,0,0));

などとします。

ちょっと面倒ですね。でも意味を理解していれば省略は可能です。

仕組みを説明します。テンプレートが分からない人は飛ばしてください。

vafximg::Image::SetPixelは、次のように宣言されています。

void SetPixel(const st2d::Point& aPoint,PixelType aPixel);

aPointは、もちろん点を打つ場所です。上の例では、(17,24)という引数を渡してコンストラクトしたst2d::Pointのテンポラリオブジェクトを渡しています。

aPixelは、理由が分からないと思いますので、説明します。

まず、aPixelの型であるPixelTypeは、Imageクラスのスコープで

typedef Pixel<Bits> PixelType;

と定義されています。BitsはImageインスタンスの生成に使ったときのビット数ですので、つまり上で定義されているPixelクラス、この場合ではPixel<24>と等しいということです。ですから、この場合

void SetPixel(const st2d::Point& aPoint,vafximg::Pixel<24> aPixel);

と読み替えて差し支えありません。上の例では、Pointと同じように、(255,0,0)の引数でコンストラクトしたPixelのテンポラリオブジェクトを渡しているというわけです。

Pixelというクラスの存在意義が分からない、と言う質問があったので答えておきます。Pixelは、アルゴリズムの適用先データ構造の要素という概念を一般化し、インターフェイスを規定するために存在しています。アルゴリズムの方は実は気にしてませんので、適用対象クラスのスコープでPixelTypeとして定義されてさえいれば、別にPixelでなくてもよいのです。また、インターフェイスと言っても、基本的にスカラ型で使えるものだけです。

という視点でPixelの定義を見れば、データの変換とピクセルレベルのユーティリティくらいしか用意してないのがわかると思います。型のパラメタライズのためにピクセルという概念を抽象化してるだけ、と捉えてください。

別の見方で言えば、24ビットカラーのビットマップの操作のときに、3バイトのスカラ型がないので

struct RGBTriple {
    BYTE red;
    BYTE green;
    BYTE blue;
};

という構造体を作って(勿論パディングを0にして)そのポインタでメモリを操作するのに使うのと似ています。

コピー

次に、イメージからイメージに矩形データをコピーしてみましょう。img2というオブジェクトを作って、データを読み込んだとします。

矩形データのコピーには、VAFXImgに含まれる「CopyRect」アルゴリズムを用います。

CopyRectは、次のように宣言されています。

template <class Section1,class Section2,class Mapper>
void CopyRect(
    Section1& dest,    st2d::Point dest_point,
    Section2& src,     st2d::Rect  src_rect,
    Mapper mapper);

dest_point/src_rectは分かると思いますので、その他のパラメータについて説明しましょう。

まず、destとsrcの型が、Section1・Section2という異なるテンプレートパラメータになっているのがわかりますね。これはすなわち、destとsrcのデータ構造が違ってても構わない、ということを意味します。具体的に言えば、Mapper次第で「パレットイメージをRGBイメージに展開する」「選択されているところだけ塗りつぶす」などの用途に使えるということです。

次にMapperですが、これがキモになります。

VAFXImgでは、次のような定義のものを「Mapper」といいます。

  • そのオブジェクトが「mapper」という名前であるとすると、そのオブジェクトを利用したレキシカルな状況で「mapper(dest,src)」という記述が可能なもの。ただしこのとき、dest・srcの方は呼び出し元に依存する。

具体的に言えば、

  • 関数ポインタ

  • operator()を定義したクラス(構造体)

などが当てはまります。

試しに、「ただコピーするMapper」を使ってみましょう。自分で定義しても構いませんが、vafximg::CopyPixelを使うのが便利です。

vafximg::CopyPixelは、次のように定義されています。

template <class T>
struct CopyPixel {
    void operator()(T& dest,const T& src)const{dest=src;}
};

実に簡単ですね。つまり、

int main(void)
{
    vafximg::CopyPixel<BYTE> cp;
    BYTE d,s;

    s=10;
    cp(d,s);
    printf("%d",d");

    return 0;
}

とすると「10」と表示される、そんなクラスです。

これを使ってCopyRectを呼び出してみましょう。

vafximg::CopyRect(
    img,      st2d::Point(0,0),
    img2,     img2.GetRect( ),
    vafximg::CopyPixel<24>()); // ←テンポラリオブジェクトを渡している

これでOKです。こうすることで、img2の矩形内の各点(img2.GetRect( )はimg2全体を表す矩形を返すので、結局im2のすべての点)で

vafximg::CopyPixel::operator(dest,src);

が呼び出されるので、結果としてimg2の画像がimgにコピーされるというわけです。

このとき最適化がかかっていれば、vafximg::CopyPixel::operatorはCopyRectの中にインライン展開されますので、汎用性の高さの割に非常に高速に処理されます。どのくらい高速かと言うと、このままゲームにしても特に問題ない(例:FullSpeed)くらいです。アルゴリズム的な最適化はどのアルゴリズムでも時間を計りながらかなり施してありますので、機能を落とさずにこれ以上速くするのはけっこう難儀だと思います。

呼び出しが面倒なのが難点ですね。よく使う呼び出し形式があれば、それを関数にしてしまいましょう。

他のアルゴリズムも大体これと同じ考え方ですので、ソースを眺めれば使い方はなんとなくわかると思いますので、研究してみてください。見返りはあると思います。

規約

VAFXImgのそれぞれの要素は、だいたい次のような規約を守って作られています。逆にいうと、これらの規約を守って作られたオブジェクトは、VAFXImgの組み込みオブジェクトと組み合わせて使うことができる、ということです。

データ構造

アルゴリズム

仕様的には特になし。

若干の命名規約(といってもあまり厳密ではない)があります。

マッパ/プロセッサ

TIPS

vafximg::Imageの内部構造はDIB互換ですので、GetBuffer()でアドレスを取ればStretchDIBitsToDeviceに使えます。

BITMAPINFO*            i=(BITMAPINFO*)new BYTE[
        sizeof(BITMAPINFOHEADER)+
        sizeof(RGBQUAD)*256];
memset(i,0,sizeof(BITMAPINFOHEADER)+sizeof(RGBQUAD)*256);

BITMAPINFOHEADER& info	=*((BITMAPINFOHEADER*)(i));
info.biSize            	=(DWORD)sizeof(info);
info.biWidth        	=aSrcImage.GetWidth();
info.biHeight        	=-aSrcImage.GetHeight();
info.biPlanes        	=1;
info.biBitCount        	=24;
info.biCompression    	=BI_RGB;
    
HDC dc=GetDC();
StretchDIBits(
    dc,
    aDestRect.left,aDestRect.top,
    aDestRect.Width(),aDestRect.Height(),
    aSrcRect.left,aSrcRect.top,
    std::_MIN(aSrcImage.GetWidth(),aSrcRect.Width()),
    std::_MIN(aSrcImage.GetHeight(),aSrcRect.Height()),
    aSrcImage.GetBuffer(),
    i,
    DIB_RGB_COLORS,
    SRCCOPY);
ReleaseDC(dc);

delete i;

前後はありますが、だいたいこんな感じです。そのうちVAFXWUtyに標準関数として入れるかもしれません。

筆者からのお願い

アルゴリズムやドット打ちクラスでいいのができたら、是非私に連絡してください。VAFXImgに取り込みたいと思います。とくに矩形の自由変換なんかほしいです。

ではまたお会いしましょう。


(C) 1998 Naoyuki Hirayama. All rights reserved.