Last update 1999/08/07
(C)平山直之
無断転載は禁止、リンクはフリー
誤字脱字の指摘は歓迎
VAFXImgは我ながら非常に便利なライブラリだと自負しているのですが、やはり人に使ってもらうにはドキュメントが不足していると言わざるを得ません。そこで、
仕組み
使い方
について、それなりに説明します。
VAFXImgはC++の「テンプレート」という機能を使っていますので、普段C/C++コンパイラをCコンパイラとして使ってる人も、C++コンパイラとして使わなければなりません。VAFXImgをただ使うだけなら大して難しくはないので、C++をbetter Cとして使うのもよいかと思います。これ以後もそのような前提で説明します。よく分からないことがあったらBBSで質問してください。
※要するにCが解っている人向け※
VAFXImgとは、2Dイメージの処理を
データ構造
マッピングのアルゴリズム
データ構造へのアクセス方法
の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.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の組み込みオブジェクトと組み合わせて使うことができる、ということです。
データ構造
クラス/構造体でなければならない。
そのクラススコープで要素の型が「PixelType」という名でtypedefされていること。
そのクラススコープで要素へのポインタの型が「PixelPointer」という名でtypedefされていること。これは、インクリメント・デクリメント・整数の加算・整数の減算・参照が定義されている「ポインタのようなクラス」でも構わない。
- 以下のメンバ関数が定義されていること。青字のものは必ずしもなくともよい。
int GetWidth(void)const
- イメージの幅を返す。
int GetHeight(void)const
- イメージの高さを返す。
int GetPitch(void)const
- イメージのピッチ(横のバイト数)を返す。
アルゴリズムでは使わないので、成り立たないもの(例えばピクセルがメモリ上で順番に並んでいないのでPixelPointerで独自のクラスを定義しているもの)では無視してよい。st2d::Extent GetExtent(void)const
- イメージのサイズを(st2d::Extent(GetWidth(),GetHeight()))を返す。
アルゴリズムでは使わないの定義しなくてもよいが、あったほうが便利。st2d::Rect GetRect(void)const
- イメージ全体の矩形(st2d::Rect(0,0,GetWidth(),GetHeight()))を返す。
アルゴリズムでは使わないの定義しなくてもよいが、あったほうが便利。BYTE* GetBuffer(void)
- バッファ全体の先頭アドレスを返す。
アルゴリズムでは使わないので、成り立たないもの(例えばピクセルがメモリ上で順番に並んでいないのでPixelPointerで独自のクラスを定義しているもの)では無視してよい。
const BYTE* GetBuffer(void)const
- 上のconstバージョン。
PixelPointer GetAddress(const st2d::Point& aPoint)
- aPointの点を指すPixelPointerを返す。
const PixelPointer GetAddress(const st2d::Point& aPoint)const
- 上のconstバージョン。
void SetPixel(const st2d::Point& aPoint,PixelType aPixel)
- aPointにある要素にaPixelを代入。
const PixelType& GetPixel(const st2d::Point& aPoint)const
- aPointにある要素を得る。
アルゴリズム
仕様的には特になし。
若干の命名規約(といってもあまり厳密ではない)があります。
- 1つのイメージに処理を行う関数は、「Process〜」
- 2つのイメージに処理を行う関数は、「〜Map〜」「〜Copy〜」
- CopyよりMapの方が汎用的(反転などがある)。
- 引数はだいたい(dest、dest_params、src、src_params、general_params、mapper/proessor)の順。
- 一応、mapper/processorが内部状態を持っている可能性について配慮する(点線などに使われる)。具体的には、内部で別の関数を呼び出すときには、マッパーを参照呼び出しにするよう気をつける。非内部関数が値渡しになっているのは、非constの参照渡しだとテンポラリオブジェクトが生成されてwarningがうざったいため。
マッパ/プロセッサ
- プロセッサの場合、1引数(dest)をとる関数またはopertor()(dest)をオーバーロードしたクラス/構造体であること。
- マッパの場合、2引数(dest,src)をとる関数またはoperator()(dest,src)をオーバーロードしたクラス/構造体であること。
- srcは値渡しもしくはconst参照であること。
- 汎用的にするにはテンプレート関数/クラスであることが望ましい。
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.