2007年07月11日

目的と方向性

過去にツール用に作っていた自分用グラフィックライブラリもあるが、Cで書かれているし、主にコマンドラインツールを目的としていたので、あまり高速化などは意識していなかった。
で、最近またグラフィック関係のGUIのツールを作り出したのと、C++のが欲しいなぁと言うことで、作るか探すかすることにした。
ツール用なのでそこまで高速なものは要らないが、表示しながら作る関係上、速いに越したことはない。

投稿者 Takenori : 18:13 | トラックバック

ライブラリいくつか

むかーしテンプレートを使ったグラフィックライブラリとしてGTLとかあったけど、いつの間にかどこかへ行ってた。
で、似たようなのでVAFXというのもあった。( VAFXImg入門VAFXImgリファレンス )
他には、yaneSDKもテンプレートが使われていたように思う。
後、最近知ったMISTもそう。
吉里吉里3もその方向で検討されている。
ツール用途なら ImageMagick かとも思う。
とりあえず、ある程度知っているのはこれぐらいかな。

この中で MISTImageMagick は毛色が違う。

追記:
VIGRA : C++テンプレートベース、 MIT(X11)ライセンス
Image restoration and inpainting : BSD License
Anti-Grain Geometry (AGG) : GPL

link集/ライブラリ系/C++

投稿者 Takenori : 18:24 | トラックバック

2007年07月13日

矩形コピー

とりあえずは、矩形コピーが出来て、その時に拡大縮小とアルファブレンド、回転、カラーキーによる抜きが出来ればいい。

MIST を見てみたが、 CopyRect のようなものは見当たらない。
VIGRAcopyImage で出来るようだ。
拡縮や回転も出来るよう。
コピーなどでは右上と左下のイテレータを渡して矩形を指定している。
アルファブレンドはアクセサを作れば出来そう。
アクセサ は、src と dest にあり、イテレーターから値を読む、もしくは書き込むためにあり、デフォルトでは単純にコピーしているだけだ。
アルファブレンドを実現しようとしたら、この src と dest の両方を作らないと出来ない。 src 側でブレンド率をかけて、dest 側では加算するようにしないといけない。
カラーキーは、mask と言うプレディケート + イテレーターで出来そうだが、mask 用のイテレーターが別になっているので、イテレーターに工夫の必要がありそう。本来はマスク画像を指定するものと思われる。
ざっとソースとドキュメント見ただけなので、もっと効率的なやり方があるのかもしれないが、自分の用途では回りくどいな。
単純に関数オブジェクト渡して、それで src と dest を加工出来れば楽なのに。

ところで、MIST でも、VIGRA でも出てきたが kernel とはなんだろう?
何らかのフィルターをかける時に核として使われるような雰囲気だが、よくわからない。

知らなかったけど、GDI には回転用の API に PlgBlt とかあったのか。
と言うことは、これらを組み合わせれば、とりあえず目的は達成できるのか。
まあ、いろいろ組み合わせる場合はテンポラリに何度もコピーしないといけないので、重そうだけど。
・BitBlt
・StretchBlt(拡縮)
・TransparentBlt(透過)
・AlphaBlend(アルファブレンド)
・PlgBlt(回転)
( 参考 : Windowsプログラミング・ビットマップ描画 )

とりあえず必要なのは GDI で実装して、重いだろうから後のことを考えて自分で作っていくかな。
高度なことは MIST や VIGRA へのアダプタを作り、相互にイメージを変換できれば両方が持つ機能を実現できる。
後、大体は C++ Builder で作るので、TBitmap 用のも必要。

投稿者 Takenori : 12:11 | トラックバック

2007年07月17日

GDI を考える

拡大縮小 + アルファブレンド + 回転 + カラーキー の時、4回の copy と1回の fill が必要。
明らかに重そう……
と言うことで、それぞれの処理の組み合わせを考えて、コピー回数が出来るだけ少なくなるようにする。

拡大縮小 + アルファブレンド + 回転 + カラーキーの組み合わせは 16 個。
PlgBlt は、回転と拡大縮小を含められるので、両方ある場合と回転のみの場合は同じと考える。
単純コピーとカラーキーは同じと考える。
で、11パターン。
多い。
拡大縮小を回転で代用するとしても 7パターン。
多い。

7パターン全部考えて作りたくないな。
GDI での実装は止めて、コピーは自分で作るかな。

投稿者 Takenori : 23:38 | トラックバック

2007年07月30日

画像コピーにイテレーターを使うかどうか

画像コピー時に入力 ( src ) と出力 ( dest ) のイテレーターを渡して処理した方が良いかどうかを検討。

もし、イテレーターを渡し、そのイテレーターが1次元的なアクセスを提供するとした場合、イテレーターは、X座標、Y座標、コピー幅、コピー元(先)画像への参照の情報を持つ必要がある。
コピー幅、コピー元(先)画像への参照は、コピー領域を表すビューポートのようなオブジェクトを作れば、それへの参照で済ませられる。
ただ、それでも3つもデータを持つ必要が出来てしまう。
ピクセルを処理する関数オブジェクトへイテレーターがコピー渡しされることを考えると、あまりコピーされる情報が多いのは避けたい。

ポインタを1つだけ持つようにしようとするなら、イテレーターのクラス属性にX、Y座標などの情報を持ったクラスのリストを持ち、それへのポインタを持つようにし、イテレーターがそれらに変更を加えた時、共有を解除して新たにリストへ追加し、ポインタを変更するような作りにすれば、1つだけに出来てコピー時のオーバーヘッドは少ない。
ただ、++などでの変更時に余計な処理が要る (破棄時も)。
まあ、++される時は、イテレーターのコピーはなく1つだけになっているだろうと考えられるから、負荷増は少ないと思われる。

そもそもピクセルを処理する関数オブジェクトへイテレーターを渡す時、コピーされるのか?
ピクセルを処理する関数オブジェクトがインライン展開されるのならコピーされない可能性も高い。
ならば、何も気にせずイテレーターを渡してしまえばいいか?

イテレーターを関数オブジェクトに渡そうとした場合、ピクセルを処理する関数オブジェクトを多重に呼び出したい時に問題がある。
アルファブレンドとグレースケール化を同時に適用したいと考える。
この場合、グレースケール化した後、アルファブレンドした方が良さそうだ。
では、グレースケール化後のイテレーターが指す画像データはどこにあるのだろうか?
アルファブレンドを処理する関数オブジェクトはそのイテレーターをソース入力に取るはずだ。
中間画像を作るのはオーバーヘッドが大きすぎるので、その画素値は一時的なもののはず。
コピー先画像を指せばいいのだろうか?
たぶん、そうするとアルファブレンドは期待した結果を得られない。
同じ画像をブレンドしてしまうことになる。
ピクセルを処理する関数オブジェクトには、イテレーターではなく、画素値そのものを渡した方が良さそうだ。

そもそもなぜイテレーターを使うのか?
STL がイテレーターを使っていて、便利だしなんか良さそうだから?
イテレーターは、コンテナへのアクセス方法を統一することでアルゴリズム適用の幅を広げる。
そのためコンテナが異なっても、一部分だけでもアルゴリズムを適用しやすくなる。
では、画像の場合は?
ベクトル画像を使いたいのならコンテナは変わるかもしれないが、そうでない場合はほとんど同じではないだろうか?
少しぐらい異なっていてもインターフェイスを統一することは難しいことではなさそうだ。

アルゴリズムはピクセルを処理するもの以外に何があるだろう?
ピクセルを処理するのに相当するアルゴリズムは for_each()、transform() かな。
ソートや検索は特定のピクセルに対しては使わなさそうだ。
count() は使うかもしれない。
なんだかあんまり多くのアルゴリズムは使わなさそうだ。

画像コピーにイテレーターは使わないことにしよう。
単純コピー、拡大縮小コピー、回転(orアフィン変換)コピーなどのメソッドは分かれるだろうし、それごとのイテレーター作るのも、直接実装してしまうのも大差ないだろう。
イテレーターにするのは少し難しいし。

とか考えたのは数日前で、既にアフィン変換コピー ( 4頂点のテクスチャマッピング ) を直接実装してしまった。
でも、もう少し良い方法がありそうな気もするんだが……

投稿者 Takenori : 18:56 | トラックバック

2007年08月01日

ピクセルを処理する関数オブジェクト

ピクセルを処理する関数オブジェクトは、いくつかの種類に分けたほうが良さそうだ。
たとえば、カラーキーのようなフィルタとして働く関数オブジェクトの場合、フィルタリングされれば以降の処理は省ける。
dest = func2( dest, func1( dest, src ) );のように重ねて書くことも出来るが、フィルタリングされた後の処理は無駄になる。
if( mask(dest,src) ) dest = func( dest, src ); のような形にして、フィルタリングするような関数オブジェクトは別にしたほうが良さそうだ。
maskが常にtrueを返すと最適化で分岐が消えることも期待できるので、2つ似たようなのを書く必要はなくなるかもしれない。
でも、入出力を見てフィルタリングするような処理はカラーキーぐらいかも。

他には入力、出力位置を変えるもの。
ブラーや置換マップのような処理は、現在のXY座標以外を参照してピクセル値を返す。
そもそも、イテレーターを引数に取らないのであれば、これは別にするしかない。
VIGRA のアクセサに当たるものになると思う。

関数オブジェクトの重ね合わせはバインダーを使えばいいだけで簡単に出来るはず。
bind1stのようなもので2引数をとるのが既にあるのかどうかは知らないが。

投稿者 Takenori : 02:20 | トラックバック

イテレーターを取らないときの問題

関数オブジェクトの引数をイテレーターにした時に問題が発生することは考えていたが、取らない時の問題に気付いた。
MMXなどを使うバージョンをどうするのか?と言うこと。
イテレーターならポインタを得られるので一気に2ピクセル分読んでしまえばいい。
でも、画素値を直接渡してしまう場合困る。
ただ、この場合イテレーターを進めてしまうと、複数の関数オブジェクトを呼ぶときに問題が出る。
複数の関数オブジェクトを重ねるのを止めるべきか・・・

と言うか、関数オブジェクトに渡す画素値の型を__m64などとすればいいのか。
で、型が__m64の時はMMX版など。
拡縮、回転などの場合、元ピクセルは隣り合っているとは限らないから8バイト分などをポインタで読んでしまうのは問題がある。
だからその場合一度__m64などに入れたほうが良さそうだ。
出力はスキャンラインごとに書いていくだろうから、隣り合っているのであまり気にしなくても良さそうだ。
でも、そのような作りで速度出るのかどうかが少し気になる。
やってみないとな。

後、共通のソースコードにするのにアクセサだけでは対応出来なさそうだ。
ポインタの移動を行うオブジェクトが必要かもしれない。
って、それはイテレーター・・・
関数オブジェクトには、画素値を直接渡すのはいいとして、ポインタの移動を行う部分については再考の必要がありそうだ。
やっぱりそこはイテレーターか?

投稿者 Takenori : 02:51 | トラックバック

2007年08月18日

テンプレートのインライン展開と最適化

※ 2007/10/12 追記 : C++ Builder 2007 でも、Update 3 をあてることで以下のコードでも最適化され期待通り動くようになった。

関数オブジェクトを用いて画像のコピーを行う場合、テンプレートがインライン展開され、さらに最適化がかかって高速にコピーされることを期待していた。
が、C++ Builder 2007 でコンパイルとしたところ期待よりも1.5倍ぐらい遅いと言う結果になった。
以下、ソースコードの一部を抜粋。

template<typename T>
struct CPredicate : public std::binary_function<T,T,bool>
{
  inline bool __fastcall operator()( T d, T s ) { return true; }
};
template<typename T>
struct CFunctor : public std::binary_function<T,T,T>
{
  inline T __fastcall operator()( T d, T s ) { return s; }
};
// 以下コピーしている部分のみ抜粋。m は CPredicate で func は CFunctor
for( int dy = d_rect.top, sy = s_rect.top; dy < d_rect.bottom; ++dy, ++sy )
{
  const DWORD* src_pix = &(src_img[sy][s_rect.left]);
  DWORD* dest_pix = &(dest_img[dy][d_rect.left]);
  for( int x = 0; x < dest_w; ++x, ++dest_pix, ++src_pix )
  {
    if( m( *dest_pix, *src_pix ) )
      *dest_pix = func( *dest_pix, *src_pix );
  }
}

上記のようなコードで、CPredicate と CFunctor が使われた場合、ループの中は以下のようなコードと同じになることを期待した。

const DWORD* src_pix = &(src_img[sy][s_rect.left]);
DWORD* dest_pix = &(dest_img[dy][d_rect.left]);
for( int x = 0; x < dest_w; ++x, ++dest_pix, ++src_pix )
{
  *dest_pix = *src_pix;
}

が、800*600の32bit colorの画像5枚を10回コピーした結果を見ると 189 msecぐらいで、下の関数オブジェクトを使用しない版では、112 msecぐらいだった。
これは困る。
上のコードの結果は両方同じになってくれることを期待して作っているのに。

でも、これは C++ Builder 2007 の最適化が弱いだけかもしれないと Visual C++ 2005 で同じものをビルドしたところ、両方 112 msec ぐらいになった。
なんだ。
良かった。
C++ Builder 2007 では遅くなってしまうが、VC では問題ないようだ。
ついでに MinGW でも試したところ、VC と同じような結果になった。
C++ Builder 2007 が期待よりも遅いだけのようだ。

ツールの画像コピー周りの処理は VC で DLL を作って Builder から呼び出すような形の方がいいかもしれない。

投稿者 Takenori : 18:25 | トラックバック

アルファブレンドを計る

テンプレートのインライン展開と最適化で、VC と MinGW で期待通りの結果が得られたので、ついでにアルファブレンドの速度も計ることにした。
実行環境はAthlon 64 X2 3800+, DDR400のデュアルチャンネル。

コピー周りの処理は前回と一緒。
アルファブレンドは以下の式で行った。
1. dest = (src * alpha + dest * (256 - alpha) ) / 256

これをほぼそのまま実装したところ 234 msec ぐらいだった。
112 msec と比べるとだいぶ遅い。
これを以下のように式を変形して式3で実装したところ 210 msec。
2. dest = (src * alpha + dest * 256 - dest * alpha) / 256
3. dest = ((src - dest)*alpha)/256 + dest

そんなものか。
SIMD の組み込み関数を使用して、MMX や SSE2 で計ってみる。
SIMD の組み込み関数は、Linux* 版 インテル® C++ コンパイラ・ユーザーズ・ガイド のリファレンスの組み込み関数にリファレンスがある。
Linux 版 とあるけど、Windows でもそのままでいけた (Windows版のマニュアルもどこかにあると思うけど、どこにあるか知らない)

結果は、
MMX を使うと 128 msec
SSE2 を使うと 125 msec
単純コピーとの差 約10msecか。
800*600 を 50回も コピーしてこの結果なので、30 fps なら 800*600 を 12回はアルファブレンドコピーできるか。そんなに速いのか。

800*600 のコピーなので、アライメントされていること前提で書いたけど、実際は MMX の時は X が奇数値、SSE2 の時は4で割り切れない値の場合、別に処理する必要がある。
ただ、SSE3 が使えると _mm_lddqu_si128 を使えばアライメントされていない時でも、ある程度読込み速度が改善されるらしい。(指定アドレスの前後含めて256ビット読み込んで要らないところを捨てる処理とか)
でも、VC2005 ではまだ SSE3 の組み込み関数は使えなさそう。 pmmintrin.h をインクルードしたらないと言われた。
MinGW は問題ないよう。

ソースコードは以下の通り。
まだ、実際にBMPなどに書き出して確かめてはいない。
その内確かめて間違っていたら直す。
確認したら間違ったいたので修正した。
で、よく考えると MMX で 2pixel ずつ処理するメリットは少ないかも。
1pixel ずつの方が柔軟に出来るので良さそうな気がする。

class CAlphaBlendMMX : public std::binary_function<__m64,__m64,__m64>
{
private:
  const WORD op_val_;
  __m64 alpha_;
  __m64 zero_;

public:
  CAlphaBlendMMX( WORD op_val ) : op_val_(op_val)
  {
    alpha_ = _mm_set1_pi16(op_val);
    zero_ = _mm_setzero_si64();
  }

  __m64 operator()( __m64 d, __m64 s )
  {
    __m64 ms = _mm_unpacklo_pi8( s, zero_ ); // 00 mt 00 mt 00 mt 00 mt (下位半分)
    __m64 md = _mm_unpacklo_pi8( d, zero_ ); // 00 mt 00 mt 00 mt 00 mt (下位半分)
    ms = _mm_sub_pi16( ms, md ); // s - d;
    ms = _mm_mullo_pi16( ms, alpha_ ); // t * a
    ms = _mm_srai_pi16( ms, 8 ); // t >> 8
    __m64 mlo = _mm_add_pi16( ms, md ); // d + t

    ms = _mm_unpackhi_pi8( s, zero_ );
    md = _mm_unpackhi_pi8( d, zero_ );
    ms = _mm_sub_pi16( ms, md );
    ms = _mm_mullo_pi16( ms, alpha_ );
    ms = _mm_srai_pi16( ms, 8 );
    __m64 mhi = _mm_add_pi16( ms, md );
    return _mm_packs_pu16( mlo, mhi );
  }
};
class CAlphaBlendSSE2 : public std::binary_function<__m128i,__m128i,__m128i>
{
private:
  const WORD op_val_;
  __m128i alpha_;
  __m128i zero_;

public:
  CAlphaBlendSSE2( WORD op_val ) : op_val_(op_val)
  {
    alpha_ = _mm_set1_epi16(op_val);
    zero_ = _mm_setzero_si128();
  }

  __m128i operator()( __m128i d, __m128i s )
  {
    __m128i ms = _mm_unpacklo_epi8( s, zero_ );
    __m128i md = _mm_unpacklo_epi8( d, zero_ );
    ms = _mm_sub_epi16( ms, md );
    ms = _mm_mullo_epi16( ms, alpha_ );
    ms = _mm_srai_epi16( ms, 8 );
    __m128i mlo = _mm_add_epi16( ms, md );

    ms = _mm_unpackhi_epi8( s, zero_ );
    md = _mm_unpackhi_epi8( d, zero_ );
    ms = _mm_sub_epi16( ms, md );
    ms = _mm_mullo_epi16( ms, alpha_ );
    ms = _mm_srai_epi16( ms, 8 );
    __m128i mhi = _mm_add_epi16( ms, md );
    return _mm_packus_epi16( mlo, mhi );
  }
};

これより速くすることを考えるとすると dest が何度も参照されているので、それがキャッシュにのるように微小区間に区切って実行することかな?
それで本当に速くなるのか、どの程度速くなるのかはわからないが。

投稿者 Takenori : 21:35 | コメント (5) | トラックバック

2007年08月19日

小さく区切ってコピー

テンプレートのインライン展開と最適化アルファブレンドを計る でソースが間違っていたので、再計測して修正した。後、MMX と SSE2 のソースも直した。
で、今度は少しずつコピーする方法を試した。
800*600 32bit の画像 5枚を10回コピーしているので、5枚のコピーの時に1ラインずつに分けてコピーするようにしてみた。 50回分まとめてもいいけど、そこまでするとあんまり現実性もないかと思ってこうした。
で、以下がその結果。
分けてコピーしたものと、分けずにコピーしたものを並べている。

単純コピー - 66 msec, 112 msec, 1.7倍
ブレンド非SIMD - 158 msec - 210 msec, 1.3倍
ブレンドMMX - 89 msec - 128 msec, 1.4倍
ブレンドSSE2 - 89 msec - 127 msec, 1.4倍

かなり速くなった。
少しずつコピーするのは面倒だから別にいいかなぁと思っていたが、やったほうが良さそうだな。
でも、見てわかるように MMX と SSE2 ではほとんど差がない。
前も書いた気がするけど、やはりSSE2 は MMX を2回呼んでるだけか。
Athlon じゃなくて、Core 2 Duo とかだと速くなったりするのだろうか?
SSSE3とは を見てみると、以下のような記述がある。
-- 以下引用 --
 ちなみに、SSSE3自体の特徴ではないが、SSE3対応プロセッサは128ビットを同時に処理する命令を実行しても内部的には64ビット長のALUを利用して2回の処理で行なわれていたが、Intel Core 2シリーズではALUが128ビット長となり、同時に128ビットを処理できるようになっている。
-- 引用終わり --
やはり、Core 2 Duo を使うと倍速になるのかな。

もっと速くする方法はマルチスレッドかな。
マルチコアならば、コア数分速くなるはず。

投稿者 Takenori : 13:09 | トラックバック

マルチスレッドにしてコピー

小さく区切ってコピーの SSE2 版を上半分と下半分に分けて2スレッドにしてみた。
結果は 50 msec。
倍とは行かないまでも 89 msec が 50 msec なので 約 1.8倍。
速いなぁ。
まとめると以下のような感じ。

実行環境はAthlon 64 X2 3800+, DDR400のデュアルチャンネル。
コピー元、コピー先画像のアルファ値は無視、指定したアルファ値でブレンド処理。
これを 800 * 600 32bit の画像に対して 50回実行。

50 msec - 100% ( 2スレッド, 領域分割, SSE2 )
89 msec - 178% ( 領域分割, SSE2 )
127 msec - 254% ( SSE2 )
210 msec - 420% ( 非SIMD )

マルチスレッドや分割、SSE2 を考えずに書いた場合よりも4倍以上速い。
これらの処理を組み込むのを検討するのには十分すぎるぐらいの効果だな。
これが実際の用途でどの程度速くなのかはわからないが、やってみる価値はありそうだ。

投稿者 Takenori : 15:07 | トラックバック

2007年08月23日

SIMD クラスライブラリ

SIMD の組み込み関数の長ったらしい記述が面倒に感じたら行き着く先は一つ。
クラス化して演算子をオーバーロードして楽に記述できるようにする。
そしたら、うまくやれば MMX 版も SSE2 版もテンプレートを使って同じソースに出来る。
非 SIMD 版も同じソースに出来なくはないが、明らかに重くなりそうなのでそこは別にしたほうが良さそう。

で、組み込み関数を単にラップしただけのクラスを一部書いて面倒臭いと思いつつ、Linux 版 インテル C++ コンパイラ・ユーザーズ・ガイド をよく見ると既に SIMD クラスライブラリが提供されていた。
まあ、そりゃそうか。
MMX 版と SSE2 版でテンプレートを使ってソースを共通化できるようにメソッド名などを同じにしているかどうかはわからないので、その辺を見て問題なさそうなら SIMD クラスライブラリを使うと楽に書けそうだ。

投稿者 Takenori : 19:13 | トラックバック

2007年08月30日

関数オブジェクトと値のバインド

ピクセルを処理する関数オブジェクトは、アルファブレンド以外にも多数ある。
テンプレートを使用して、最適化によってインライン展開される恩恵を受けるためには、画像コピーメソッド呼び出し時点で静的に型が決定される必要がある。
この事からブレンド方法を値で指定して画像のコピーを行おうとした場合、素直に実装すると if ~ else、もしくは switch ~ case が多数表れることになってしまう。
そして、そのコードは単純に関数オブジェクトの型が違うだけで、他はまったく同じ事の繰り返しになってしまう。
とりあえずは、switch ~ case で書いたが、出来ればこれを避けたい。

まず思い浮かんだのは、テンプレートメタプログラミング。
テンプレートを使って出来そうな気がする。
Modern C++ Design を読みながら考える。
可能そうだ。
ファクトリーの Create を Copy メソッドに置き換えたようなものを作れば、値と型を関連付けられ、値によって関数オブジェクトの型が静的に決定され、画像のコピーが実行できる。
難点は、ソースコードが読み辛いこと。
この方法でも良かったのだが、もっと手軽な方法の方がいいなぁと考えていて気付いた。
マクロでやればいいんじゃないのかと。
例えば以下のような感じ。

#define VALUE2TYPE_FUNC_CALL( VAL, TYPE ) \
case VAL: \
{ \
  TYPE func(val); \
  image_copy(destBmp, destRect, srcBmp, srcRect, func, mask ); \
  break; \
}

switch( op )
{
  VALUE2TYPE_FUNC_CALL( DT_AlphaBlend, COperationAlphaBlend ) // アルファブレンド - 通常
  VALUE2TYPE_FUNC_CALL( DT_AddBlend, COperationAddBlend ) // 覆い焼き(リニア)
  VALUE2TYPE_FUNC_CALL( DT_SubBlend, COperationSubBlend ) // 焼き込み(リニア)
  VALUE2TYPE_FUNC_CALL( DT_MulBlend, COperationMulBlend ) // 乗算
  VALUE2TYPE_FUNC_CALL( DT_ScreenBlend, COperationScreenBlend ) // スクリーン
  VALUE2TYPE_FUNC_CALL( DT_OverlayBlend, COperationOverlayBlend ) // オーバーレイ
  VALUE2TYPE_FUNC_CALL( DT_HardLightBlend, COperationHardLightBlend ) // ハードライト
  VALUE2TYPE_FUNC_CALL( DT_SoftLightBlend, COperationSoftLightBlend ) // ソフトライト
  VALUE2TYPE_FUNC_CALL( DT_ColorDodgeBlend, COperationColorDodgeBlend ) // 覆い焼きカラー
  VALUE2TYPE_FUNC_CALL( DT_ColorBurnBlend, COperationColorBurnBlend ) // 焼き込みカラー
  VALUE2TYPE_FUNC_CALL( DT_LightenBlend, COperationLightenBlend ) // 比較(明)
  VALUE2TYPE_FUNC_CALL( DT_DarkenBlend, COperationDarkenBlend ) // 比較(暗)
  VALUE2TYPE_FUNC_CALL( DT_DiffBlend, COperationDiffBlend ) // 差の絶対値
  VALUE2TYPE_FUNC_CALL( DT_ExclusionBlend, COperationExclusionBlend ) // 除外
}

テンプレートを駆使するよりは遥かにわかりやすいし、それほど冗長でもない。
型の安全性などの面では劣るかもしれないが、これで十分な気がする。
でも、普通初めにこの方法を思い浮かべるよなぁと思う。
テンプレートにばかり目が行き過ぎていたか。

投稿者 Takenori : 15:17 | トラックバック

2007年09月01日

DirectX とか

いろいろと追加していっていると、だんだんソフトウェアで処理するのが面倒になってきた。
と、少し DirectX で描いてみる。 ( テクスチャは別のところで設定 )
射影変換行列には、左手座標系正射影行列を上下反転した形で設定している。

struct SpriteVertex
{
    float x, y, z;
    float tu, tv;
};
SpriteVertex vtx[4];
vtx[0].z = vtx[1].z = vtx[2].z = vtx[3].z = 0.0f;

vtx[0].x = -0.5f; // TL
vtx[0].y = -0.5f;
vtx[0].tu = (p.tx + 0.5f) / p.img_width;
vtx[0].tv = (p.ty + 0.5f) / p.img_height;

vtx[1].x = 0.5f; // TR
vtx[1].y = -0.5f;
vtx[1].tu = (p.tx + p.tw + 0.5f) / p.img_width;
vtx[1].tv = (p.ty + 0.5f) / p.img_height;

vtx[2].x = 0.5f; // BR
vtx[2].y = 0.5f;
vtx[2].tu = (p.tx + p.tw + 0.5f) / p.img_width;
vtx[2].tv = (p.ty + p.th + 0.5f) / p.img_height;

vtx[3].x = -0.5f; // BL
vtx[3].y = 0.5f;
vtx[3].tu = (p.tx + 0.5f) / p.img_width;
vtx[3].tv = (p.ty + p.th + 0.5f) / p.img_height;

g_MatStack->Push();

D3DXVECTOR3 center( 0.0f, 0.0f, 0.0f );
g_MatStack->LoadIdentity();
g_MatStack->Scale( p.w, p.h, 1.0f );
if( p.angle != 0.0f )
    g_MatStack->RotateAxis( &center, p.angle );
g_MatStack->Translate( p.x+p.w/2, p.y+p.h/2, p.z );
g_pd3dDevice->SetTransform( D3DTS_WORLD, g_MatStack->GetTop() );

g_pd3dDevice->SetFVF( D3DFVF_XYZ|D3DFVF_TEX1 );
g_pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE );
g_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, reinterpret_cast<void*>(vtx), sizeof(vtx[0]) );

g_MatStack->Pop();

やっぱり楽だなぁ。
同じようにして線を引いてみる。

struct LineVertex
{
    float x, y, z;
};
LineVertex vtx[4];
vtx[0].z = vtx[1].z = vtx[2].z = vtx[3].z = z;

float w2 = width / 2.0f;
float w = (float)(ex - sx);
float h = (float)(ey - sy);
float length = sqrt( (float)(w*w + h*h) );
float dy = w * w2 / length;
float dx = h * w2 / length;

vtx[0].x = sx - dx; // TL
vtx[0].y = sy + dy;
vtx[1].x = ex - dx; // TR
vtx[1].y = ey + dy;
vtx[2].x = ex + dx; // BR
vtx[2].y = ey - dy;
vtx[3].x = sx + dx; // BL
vtx[3].y = sy - dy;

g_MatStack->Push();
g_MatStack->LoadIdentity();
g_pd3dDevice->SetTransform( D3DTS_WORLD, g_MatStack->GetTop() );

g_pd3dDevice->SetRenderState( D3DRS_TEXTUREFACTOR, col );
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_SELECTARG1 );
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TFACTOR );
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_TFACTOR );
g_pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAOP, D3DTOP_DISABLE );

g_pd3dDevice->SetFVF( D3DFVF_XYZ );
g_pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE );
g_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, reinterpret_cast<void*>(vtx), sizeof(vtx[0]) );
g_MatStack->Pop();

最初計算ミスってて出なくて少し悩んだがやはり楽。
自前で線引かなくても ID3DXLine があるのか。

上位で DirectX とソフトウェアが簡単に切り替えられるようにするかな。

投稿者 Takenori : 21:47 | トラックバック

2007年11月30日

アルファブレンドを Core 2 Duo で計る

他でちょっと疑問に思うことがあったので、Core 2 Duo で SSE2 の効果を計る事にした。
対象としたのは 小さく区切ってコピー のソース。

Core 2 Duo E6750 で計測 した結果 ( パーセントはMMX を100とした場合 )

ブレンド非SIMD - 97 msec - 216 %
ブレンドMMX - 45 msec - 100 %
ブレンドSSE2 - 36 msec - 80 %


以前 Athlon 64 X2 3800+ で計った結果 ( パーセントは Core 2 Duo E6750 との差 )

ブレンド非SIMD - 158 msec - 163 %
ブレンドMMX - 89 msec - 198 %
ブレンドSSE2 - 89 msec - 247 %


やはり、Core 2 Duoでは、SSE2 は MMX より速いようだ。
また、Athlon 64 X2 3800+ の倍ぐらい Core 2 Duo E6750 は速いと感じていたが、それも大体合っている。

投稿者 Takenori : 03:33 | トラックバック

2007年12月10日

HDRI

ここ数日 HDRI にはまって、なぜかこんなページまで作ってしまった。
無駄にいろんなソフトでの複数露出写真からのHDRI画像の作り方が書いてあります。
でまあ、いろいろなソフトを試して思ったのは、全部イマイチ。と言うか、一長一短があってもどかしい。
HDRI の機能という面で言うのなら、オープンソースの Qtpfsgui がいろいろと出来ていいのだが、いかんせん遅いのとインターフェイスが使い辛いのが難点。
遅いと言っても並の遅さじゃない。画像がずれている時に自動で補正してくれるんだけど、それが致命的に遅い。
最近のでここまで遅いのは久しぶりに出会ったと言うぐらい。
後、トーンマッピングの時、パラメータいじった後、Apply を押すと結果の画像が表示されるのがもどかしい。
リアルタイムでプレビューできないの?と思う。
この2点さえクリアされれば、最もいいソフトになると思うんだけどなぁ。
ああ、後もう一点。保存する時になぜかフロッピーにアクセスするのは止めて欲しい。ギョッとする。

ズレの自動補正は、hugin の align_stack_image が使われている。
で、これが何をしているのか追ってみた。
まずエッジ抽出して、画像内の角を見つける。ガウシアンフィルタとか使っているっぽい。
見付かったすべての角のピクセルに対して、そのピクセルの周辺ブロックを比較対照のその周辺とひたすら比較して最も一致していそうなところを探すことで、ズレを検出しているっぽい。
比較時は、一度グレースケールに変換した後、ブロックの平均輝度を求め、グレースケールでその平均輝度との差を使って判定する。
比較は単純比較ではなくて、二乗和を使ってごにょごにょしてる。平均二乗誤差とかその辺りかな?良くわからないんだけど。
なんかそういう画像処理の知識があんまりないので自信がない。

で、画像はデジカメで撮った 2288x1712 とかのを5枚ぐらい突っ込むんだけど、それを素直に上の処理で自動補正する。
当たり前だけど遅い。
高速化するのなら……
JPEGに限定するのなら、グレースケール変換とかなしで、初めから輝度プレーンを取り出せばいろいろと省けそう。
毎回エッジ抽出とかしているけど、座標は使いまわせる気がする。
ただ、露出オーバーやアンダーの写真はつぶれてしまっている可能性が高いので、毎回やらないとうまくいかないかも。
いきなり元画像でやるのではなく、まず縮小画像でやってから位置をある程度絞込み、その後元サイズの画像で、その周辺だけやればかなり速くなるはず。
後、SIMD 化する。
たぶん、縮小画像のが一番効いてかなり速くなると思うんだけど……

投稿者 Takenori : 17:52 | トラックバック

カーネル

画像処理で時々出てくるカーネルってのがなんなのかわからなかったけど、調べたらいろいろ出てきてわかった。
TEOライブラリによる画像処理プログラミングガイド アルゴリズム編 ラプラシアンフィルタ・ガウシアンフィルタケアストリーム ヘルス株式会社 画像処理ソフトウェア のページでカーネルと書かれた付近や コンボリューションを用いた画像の平滑化、鮮鋭化とエッジ検出 を読むとわかる。
係数行列を使って畳み込み処理をすれば、画像をぼやけさせたりシャープにしたり、エッジ抽出が出来たりする。この 係数行列 をカーネルと言うらしい。
そんな単純な方法でいろいろなフィルタが書けたのね。
ぼやけさせる時は、横方向をやった後、その画像に対して縦方向をやっても同じ結果になるようなので、行列は2次元ではなく、1次元でも良い。1次元の方が計算量は少なくなる。
汎用的に書けるので、単純に行列を渡すインターフェイスさえあれば、利用者が自由にいろいろなフィルタを適用できると言うメリットは大きい。
この方法だと範囲が大きくなればなるほど遅くなってしまうが、利用者がいろいろいじれると言う意味ではあると有用かもしれない。

投稿者 Takenori : 18:06 | トラックバック

ブラー処理

ブラー処理は、その範囲が大きくなると処理が増える。
でも、吉里吉里2の矩形ブラー ( ボックスブラー、単純平滑化 ) は、範囲が大きくなってもそれほど負荷が増加しないらしい。
どういうアルゴリズムなんだろうと少し見てみた。
以下のようなアルゴリズムっぽい ( もう少し複雑なことをしてそう。そんなに詳しくは見ていない ) 。

以下の表の値が輝度値だとする。
合計値は、右端から始めて右の値を足し込んで行ったもの。
つまり、右の値 + 自分の輝度値 と言うのを右から順に行ったもの。
ピタゴラスの三角形みたいな感じ。

位置 0 1 2 3 4 5
1 1 1 1 1 1
合計値 6 5 4 3 2 1

で、0 の位置で、幅が4つの輝度値の合計値を得ようとしたら、自分の位置の合計値 - 右方向に4つ移動した位置の合計値 ( 4の位置 ) を計算すれば、合計値は 4 と出る。
幅が、2の場合は、自分の位置の合計値 - 右方向に2つ移動した位置の合計値 ( 2の位置 ) を計算すれば、合計値は 2 と出る。
この方法だと、足しこむ幅が変わっても負荷は変わらない ( ここで言っている幅は右側だけなので実際の半分 ) 。
ただ、端の方は別に処理しないといけないので、少し変わるが。

GPU 任せだと、単純に縮小して拡大するとか、少しずつ位置をずらしてアルファブレンドするなどと言う力技な方法もある。
もっとちゃんとシェーダーを使ってガウシアンフィルタ を実装してもいいんだが。

投稿者 Takenori : 19:20 | トラックバック

HDRI と エフェクト

もともとは複数の露出値で写真を撮って、トーンマッピングした写真が CG っぽくて、そのままゲーム中で使えるんじゃないの? ということで興味を持ったんだけど、HDRI を使ったエフェクトにも興味が出たので少し調べてみた。
エフェクトとしては、グレアと明順応、暗順応 ぐらい?
こんな感じらしい。
確かにまぶしく感じる。
木漏れ日や水面の輝きを表現すると美しそう。

ハイダイナミックレンジを使えばもっといろいろと出来そうな気もするけど、そんなでもないのかな。
ハイダイナミックレンジのことなどほとんど考えていなかったけど、考えてみるのもいいかも。
テンプレートベースなので、float になっても大丈夫だと思うが、8bit 前提で書いているところもあるはず。
ハイダイナミックレンジ画像同士の処理などは、通常の 8bit 画像とはまた違った効果が出るようなので、実装すると楽しいかもしれない。

投稿者 Takenori : 20:30 | トラックバック

 
Total : Today : Yesterday :