Flashで画像処理するときのコツ

注意) レンダリングの高速化とは別レイヤーの話になります。

去年の記事でAPIレベルの考察はしていますが、今回はもう少し踏み込んで考えてみます。
get/setVector() vs get/setPixels()

その前に BitmapData.getVector() と BitmapData.getPixels() のシグネチャを再掲。
両APIともピクセルデータを一次元のコンテナに詰め込むメソッドです。

getPixels(rect:Rectangle):ByteArray
ピクセルデータの矩形領域からバイト配列を生成します。
getVector(rect:Rectangle):Vector.<uint>
ピクセルデータの矩形領域からベクター配列を生成します。

速度を比較すると get/setVector() の方が高速です。
が、
重要なのはAPIの実行速度ではなく、
「取得したデータがコンテナにどんな形で格納されているか」
これに尽きるのではないかと個人的には思っています。
もし、APIの実行速度の違いがボトルネックにはなっているとしたら、
それはおそらく設計からやり直す必要があります。
取得したデータに対してどんな操作を行うかの方が、パフォーマンスに大きく影響してくるからです。

getVector() では32bit、getPixels() では8bitのデータを1要素とするコンテナになっており、
つまり1要素がピクセル値かチャンネル値かの違いになっています。
取得できる情報の違いだけを見ると些細な違いに感じるかもしれませんが、
インデックス計算やその他いろいろな場面でパフォーマンスに影響してくる大事な要素です。

インデックス計算

例えば、座標(x, y)に対応するインデックスはそれぞれ以下のようになります。
(w を画像の幅、h を画像の高さとする)

これを見て分かるようにピクセル値へのアクセスにおいては、
ビットシフトを行わなくていい分 Vector の方が高速です。
ただし、このインデックス値はループの浅い所でオンメモリにキャッシュできますので、
実際はほとんどパフォーマンスに影響を与えません。

ここで、ピクセルデータを扱う時に以下のようなコード片をよく目にします。

これは uint のピクセル値(value)をRGB値に分解する計算になります。
(エフェクト系のFlashだとHSVへの変換などに利用されたりしてるっぽい? よく知らない;)
このような処理は一般的にループの深い所に置かれることが多いため、
チリも積もれば山となりパフォーマンスに影響を与えます。
また、上記の様な処理を hex2rgb(value:uint) みたいなメソッドでラップして、
ループ内で頻繁にコールしている作品をwonderflでたくさん見ました。
僕はFlash Playerについてよく知らないのですが、
JITが上手くインライン展開してくれてるのか不安なので手動で展開した方がいいと思います。
話が少しそれましたが、ここまでを整理すると、

Vector.<uint> に格納されたピクセルデータに対して処理する場合

  1. インデックス計算は高速 (ビットシフト不要)
  2. RGB値は自前で分解する必要がある

ByteArray に格納されたピクセルデータに対して処理をする場合

  1. インデックス計算は低速
  2. RGB値は自前で分解する必要がない (インデックスアクセスで取得)

1. のインデックス値については、シビアな処理が始まる前に一時変数にキャッシュすればいいので、
結果的にパフォーマンスに影響することはほとんどないと思います。
2. のRGB分解処理ついては、たとえループ内でインライン展開したとしても、
頻繁に繰り返される処理のためパフォーマンスに影響を与えます。

※ ちなみに、BitmapData.copyChannel() という面白いメソッドがありますが、
  着目領域(ROI)ベースなので、ピクセル毎の操作には利用できません。

また、自前でRGB分解して3つの変数をあちこちで使い回すのは効率が悪く、
それらはメモリ上に連続していないので、CPUのL2キャッシュにヒットしにくいです。

ここまでの情報から、シビアな画像解析を行う場合は ByteArray の方が有利だと考えられます。
が、
Vector と ByteArray ではインデックスアクセスそのものの速度に差があり、

Vector のインデックスアクセスの方が高速です。

ループやキャッシュをいろいろ工夫して小さなパフォーマンス稼ぎを重ねたところで、
インデックスアクセスの速度差であっという間にひっくり返されてしまいます。
ByteArray の [ ] 演算子は ByteArray.readByte() のシンタックスシュガーなんじゃないかと、、
そういう疑惑が湧いてきたりしましたが、符号の扱いもあるのでそれは違いますね。。

とにかくこれでは納得がいかないので、もう少し考えてみます。

範囲外参照

画像解析を行う場合、例えばブロックマッチングに代表されるような、
3×3px や 7×7px などの領域に対し、一律の処理を画像全体に渡って繰り返す手法があります。
こういった処理は画像端において注意する必要があります。
つまり、画像端に近い画素での画像外参照の扱いです。

上図の黒色に該当する部分の値をどう評価するか、ということですが、
一次元のコンテナに詰められているので、細かく地道に境界チェックを入れていくしかありません。
問題はデータ終端での Vector 、ByteArray での参照の違いです。

Vector のサイズを固定して範囲外にアクセスすると RangeError 例外が投げられるので、
範囲外参照に対応するためにインデックスの範囲チェックが常に必要になってきます。

ByteArray の場合、範囲外を参照したときは undefined が返されますが、
Actionscriptでは undefined の値を算術演算で利用すると結果は NaN になります。
そしてその NaN を整数として評価すると 0 になります。
つまり、
int(undefined + undefined)、uint(1 – undefined) などは 0
として評価されます。
範囲外の値を計算に利用した結果が 0 になるのを “自然な挙動” として扱うことにすれば、
データ終端チェックの条件文をループの中から取り除くことが出来ます。

※ ただし、このような方法を他言語で行うと危険なので十分注意

最適化の効き

色情報を利用した高度な画像解析処理を行う場合は、
ByteArray の方が速度面で Vector より優れていることが多かったです。
ただし、リリースビルドをすると速度が逆転することも多かったので、
Vector をコンテナとして使うとより高度に最適化されるようです。
ただ、これは選択する手法によるものなので Actionscript とは関係ありません。
画像解析手法それぞれの複雑さに依存することになると思います。

移植性

C言語からActionscript、ActionscriptからJavascript、、
移植性が高いのはやはり ByteArray です。
Javascript で例を挙げると、CanvasPixelArray オブジェクトに格納されるデータは、
RGBA, RGBA, RGBA, …
というように getPixels() で得られる ByteArray コンテナと同様に扱うことができます。
(アルファチャンネルの位置が ByteArray とは違うので注意)
参考:HTML Canvas 2D Context
また、C言語からの移植も ByteArray ならポインタを扱う要領で走査すればいいので簡単です。
Vector は高速ですが、慣れて変な癖が付くと他言語への移植に時間がかかってしまいます。

———-
まとめとして、
Vector, ByteArray コンテナそれぞれへのデータ格納方法の違いによって、
反復処理内での条件文の複雑さ、キャッシュの効きやすさが異なってくる場合があります。
つまり、getVector(), getPixels() などAPI自体の実行速度よりも、
返されたコンテナをどう運用するか
これがFlashにおける画像処理の肝になるんじゃないかと思っています。

また、ByteArray はネットワークを絡めた処理で威力を発揮しますので、
それについてはまた近いうちに考察したいと思います。

あわせて読む:

3 Thoughts

  1. ピンバック: しょーらぼ :: 【AS3.0】Webカメラで仮想物体を動かすよ(・∀・)

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です