前回のエントリー、FlashDevelop + GCCでANE入門の続きになります。今回はANE(ActionScript Native Extensions)で画像処理を行う際のいくつかの注意点などをメモしていこうと思います。ANEの作成手順については前エントリーを参照してください。
ANEのC APIでは、ASのBitmapDataを扱う為の構造体 FREBitmapData というものが提供されています。
1 2 3 4 5 6 7 8 |
typedef struct { uint32_t width; /* width of the BitmapData bitmap */ uint32_t height; /* height of the BitmapData bitmap */ uint32_t hasAlpha; /* if non-zero, pixel format is ARGB32, otherwise pixel format is _RGB32, host endianness */ uint32_t isPremultiplied; /* pixel color values are premultiplied with alpha if non-zero, un-multiplied if zero */ uint32_t lineStride32; /* line stride in number of 32 bit values, typically the same as width */ uint32_t* bits32; /* pointer to the first 32-bit pixel of the bitmap data */ } FREBitmapData; |
OpenCVでいう簡易IplImageのような構造です。学生時代にやったプログラミング演習とかでこんな構造体作って画像触ってた気がします。懐かしい。
で、あれ?座標のオリジンはどこ見ればわかるの?と思ったらなんとAIR 3.1から FREBitmapData2 という構造体が新しく追加されているようです。
1 2 3 4 5 6 7 8 9 |
typedef struct { uint32_t width; /* width of the BitmapData bitmap */ uint32_t height; /* height of the BitmapData bitmap */ uint32_t hasAlpha; /* if non-zero, pixel format is ARGB32, otherwise pixel format is _RGB32, host endianness */ uint32_t isPremultiplied; /* pixel color values are premultiplied with alpha if non-zero, un-multiplied if zero */ uint32_t lineStride32; /* line stride in number of 32 bit values, typically the same as width */ uint32_t isInvertedY; /* if non-zero, last row of pixels starts at bits32, otherwise, first row of pixels starts at bits32. */ uint32_t* bits32; /* pointer to the first 32-bit pixel of the bitmap data */ } FREBitmapData2; |
oh… 本当に学生のプログラミング演習課題の再提出みたいな、そんな対応。
新しく追加された isInvertedY というメンバが座標のオリジンを示しているようですが、この行き当たりばったりな対応を見る限り、Adobe内でリソース配分(人材他)に苦労してる様子がわかります。
しかもシビアな画像処理をいくつか試していた中で気付いたのですが、FREBitmapData に画像データをバインドする際にそのデータが壊れている時があったので(原因不明、単一色で塗りつぶした画像を渡してネイティブ側で全ピクセル値をログに出してみると(A, R, G, B) = (0, 0, 0, 0)で入っている画素がたまに出現した)、FREByteArray に画像データを入れてwidthとheightを併せてネイティブ側に渡す方針に変更しました。AS側での BitmapData#getPixels/setPixels と、ネイティブ側でのバイトオーダー判定等のコストが余計にかかってしまいますが、いくらか安全です。
画像にどこまで触れるかを知りたいだけなので、ここではソフトフォーカス風にレタッチするフィルタをANEで作成してみます。前回はPure Cで書いたのですが今回はC++を使います。
* 画像データバッファ用クラス (bitmap.hpp)
C++0xの std::tuple を利用していますが、普通に構造体でRGB値を扱った方が無難かも。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
/** bitmap.hpp **/ #ifndef ANE_BITMAP_H #define ANE_BITMAP_H #include <tuple> #include "FlashRuntimeExtensions.h" #define EXPORT __declspec(dllexport) enum { R, G, B }; namespace fre { /* pixel data */ typedef std::tuple<uint8_t, uint8_t, uint8_t> pixel; /* buffer class for bitmap data */ class BitmapBuffer { public: // constructor BitmapBuffer(const FREObject& image, int step) : _image(image), _step(step) { _ret = FREAcquireByteArray(image, &_ba); if(_ret==FRE_OK) { _data = pcast<uint32_t>(_ba.bytes); } } // destructor ~BitmapBuffer() { FREReleaseByteArray(_image); } bool empty() const { return _ret!=FRE_OK; } int size() const { return _ba.length; } // returns a reference to pixel template<typename T> T& at(int x, int y) { return (pcast<T>(_data + y*_step))[x]; } template<typename T> const T& at(int x, int y) const { return (pcast<const T>(_data + y*_step))[x]; } template <class T> T* pcast(void* p) { return static_cast<T*>(p); } // returns a pixel data (easy access) pixel operator()(int x, int y) const { uint32_t p = *(_data + y*_step + x); return pixel(p >> 8 & 0xff, p >> 16 & 0xff, p >> 24 & 0xff); } private: BitmapBuffer(const BitmapBuffer&); BitmapBuffer& operator=(const BitmapBuffer&); const FREObject& _image; const int _step; FREResult _ret; FREByteArray _ba; uint32_t* _data; }; } #endif |
デストラクタで FREReleaseByteArray() を呼び出して画像データをアンロックします。あとはデータのアドレス計算部分を隠蔽してアクセサを提供しているくらいの薄いラッパーになっています。
* ソフトフォーカスフィルタ (effect.cpp)
フィルタ処理の実体はPhotoshopでいうところの、元画像のレイヤーをコピーしてぼかしフィルタをかけ、それと元画像をスクリーンブレンドしたものと同じです。また、バイトオーダーがリトルエンディアンじゃなかったら何もしないという手抜き。。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
/** effect.cpp **/ #include "bitmap.hpp" using std::get; /* applies soft-focus effect */ FREObject effect(FREObject ctx, void* funcData, uint32_t argc, FREObject argv[]) { // validates byte-order and arguments int a = 0x00000001; if(!(*(char*)&a) /* little endian ? */ || argc != 5) return nullptr; // binds variables int32_t w, h, radius, offset; if(FREGetObjectAsInt32(argv[1], &w) || FREGetObjectAsInt32(argv[2], &h) || FREGetObjectAsInt32(argv[3], &radius) || FREGetObjectAsInt32(argv[4], &offset)) { return nullptr; } fre::BitmapBuffer bmp(argv[0], w); if(bmp.empty()) return nullptr; fre::pixel val, kval; const double divisor = 1.0/((2*radius + 1)*(2*radius + 1)); int px, py, r, g, b, buff[] = {0, 0, 0}; offset += 1; for(int y=0; y<h; ++y) { for(int x=0; x<w; ++x) { buff[0] = buff[1] = buff[2] = 0; // accumulates pixel data for image smoothing for(int ky=-radius; ky<=radius; ++ky) { py = y + ky; if(py <= 0 || h <= py) py = y; for(int kx=-radius; kx<=radius; ++kx) { px = x + kx; if(px <= 0 || w <= px) px = x; kval = bmp(px, py); buff[0] += get<R>(kval); buff[1] += get<G>(kval); buff[2] += get<B>(kval); } } // applies screen-blend with smoothed image val = bmp(x, y); r = get<R>(val); g = get<G>(val); b = get<B>(val); buff[0] *= divisor; buff[1] *= divisor; buff[2] *= divisor; r = r + buff[0] - (r*buff[0] >> 8) - offset; g = g + buff[1] - (g*buff[1] >> 8) - offset; b = b + buff[2] - (b*buff[2] >> 8) - offset; // saturation r = r < 0 ? 0 : r; g = g < 0 ? 0 : g; b = b < 0 ? 0 : b; bmp.at<uint32_t>(x, y) = b << 24 | g << 16 | r << 8; } } } FRENamedFunction _methods[] = { { (const uint8_t*)"effect", 0, effect } }; void _ctxInitializer(void* extData, const uint8_t* ctxType, FREContext ctx, uint32_t* numFunctionsToSet, const FRENamedFunction** functionsToSet) { *numFunctionsToSet = sizeof(_methods)/sizeof(FRENamedFunction); *functionsToSet = _methods; } void _ctxFinalizer(FREContext ctx) { } extern "C" { EXPORT void extInitializer(void** extDataToSet, FREContextInitializer* ctxInitializerToSet, FREContextFinalizer* ctxFinalizerToSet) { *extDataToSet = 0; *ctxInitializerToSet = _ctxInitializer; *ctxFinalizerToSet = _ctxFinalizer; } EXPORT void extFinalizer(void* extData) { } } |
C++0xの nullptr を使っていますが、もしC++0xを使わない場合は定数0を返す方法が良いです。NULLマクロはC++ではあまり推奨されていません。
また、この場合はコンパイラの最適化オプションを付けてコンパイルしないと、インライン展開などの最適化が行われずアセンブラコードが肥大化してパフォーマンスが低下してしまいます。なのでgccなら -O3 あたりを付けてコンパイルすることをオススメします。逆に最適化オプションを付けた場合、メモリアドレスを直接ガリガリ計算したコードと比較しても実行効率はほとんど変わらないことを確認しました。最近のGCCはすごいです。
AS側で特筆すべきところはありません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package com.example { import flash.display.BitmapData; import flash.external.ExtensionContext; import flash.utils.ByteArray; public class ImageEffect { private var ctx:ExtensionContext; public function ImageEffect() { ctx = ExtensionContext.createExtensionContext("com.example", null); } public function apply(bmp:BitmapData, radius:int, offset:int):void { var ba:ByteArray = bmp.getPixels(bmp.rect); ctx.call("effect", ba, bmp.width, bmp.height, radius, offset); ba.position = 0; bmp.setPixels(bmp.rect, ba); } public function dispose():void { return ctx.dispose(); } } } |
左が元画像、右がフィルタ適用後 (カーネルサイズ 9×9, 輝度オフセット -50)
※ ブログ用にJPEG圧縮してるので、実際はもっとふわっと(?した効果が出ています。
下の画像はミッドタウンのガレリア1Fから。クリスマスモード全開ですね。
パフォーマンスについて
実行速度はカーネルサイズが3×3程度の小ささだとデータ転送のコストが高く付いてASオンリーの方が速かったですが、7×7よりも大きなカーネルならANEを使った方が速かったです。空間効率的には小さなバッファをずらしながら畳み込みとブレンド処理を同時に行えばBitmapData全体のコピーは不要なので効率は良くなります。
バッファ用クラスを用意せずにユーザーコード内で生ポインタのアドレスをガリガリ計算する従来の書き方でももちろんOKです。C++は恐いけどCなら大丈夫という方にとってはわかりやすいかもしれません。VMがメモリ上で画像データが置かれているチャンクを移動させないようにロックしてるはずですが、画素操作が終わった後に FREReleaseByteArray() でアンロックするのを忘れないように注意。また、前述のコードより実行効率が良さそうに見えますが(コードの見た目から判断する限り)、最適化オプションを付けてコンパイルすれば実行効率の差はほとんど消えてなくなるので個人的にはオススメしない書き方です。
* 従来のC Likeな書き方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
/* applies soft-focus effect */ FREObject effect(FREObject ctx, void* funcData, uint32_t argc, FREObject argv[]) { int a = 0x00000001; if(!(*(char*)&a) || argc != 5) return 0; FREByteArray ba; int32_t w, h, radius, offset; if(FREAcquireByteArray(argv[0], &ba) || FREGetObjectAsInt32(argv[1], &w) || FREGetObjectAsInt32(argv[2], &h) || FREGetObjectAsInt32(argv[3], &radius) || FREGetObjectAsInt32(argv[4], &offset)) { return 0; } const double divisor = 1.0/((2*radius + 1)*(2*radius + 1)); const uint32_t* i; const uint32_t* kp; uint32_t* data = (uint32_t*)(ba.bytes); uint32_t* p; int r, g, b, px, py, buff[] = {0, 0, 0}; offset += 1; for(int y=0; y<h; ++y, data+=w) { for(int x=0; x<w; ++x) { p = data + x; buff[0] = buff[1] = buff[2] = 0; for(int ky=-radius; ky<=radius; ++ky) { py = y + ky; if(py <= 0 || h <= py) { kp = p; } else { kp = p + ky*w; } for(int kx=-radius; kx<=radius; ++kx) { px = x + kx; if(px <= 0 || w <= px) { i = kp; } else { i = kp + kx; } buff[0] += *i >> 8 & 0xff; buff[1] += *i >> 16 & 0xff; buff[2] += *i >> 24 & 0xff; } } r = *p >> 8 & 0xff; g = *p >> 16 & 0xff; b = *p >> 24 & 0xff; buff[0] *= divisor; buff[1] *= divisor; buff[2] *= divisor; r = r + buff[0] - (r*buff[0] >> 8) - offset; g = g + buff[1] - (g*buff[1] >> 8) - offset; b = b + buff[2] - (b*buff[2] >> 8) - offset; r = r < 0 ? 0 : r; g = g < 0 ? 0 : g; b = b < 0 ? 0 : b; *p = b << 24 | g << 16 | r << 8; } } FREReleaseByteArray(argv[0]); // 忘れないように } // ... 省略 |
さすがに原始的すぎるのでANE C APIだけを使ったライブラリを作るのはオススメしません。まともな画像処理をしたい場合は積極的に外部ライブラリを使っていきたいところです。例えばOpenCVの cv::Mat あるいは IplImage フォーマットに相互変換できる小さなアダプタ関数だけ用意して画像処理自体はOpenCVに任せた方がいいかと思います。
ANEを使ったAndroidアプリも作っていたのですが、インラインアセンブラでARM命令をぺたぺた書いたコードよりもC++で素朴に書いたコードの方が速い場合が多かったのでショックでした。「最適化はコンパイラに任せた方がいい」という意見を身に染みて実感。ただ、NEON命令(Advanced SIMD)を使えばさすがにもうちょっと速く動作すると思うのでそれも検証しておきたいです。
おまけ: FlashDevelopでのANE開発時の作業効率化
ANE開発で必要なファイル生成/配置などの作業はバッチで自動化させると楽ですが、圧縮ファイルの解凍は Lhaplus.exe を直接コマンドラインから叩けばできることを知ったのでこれをバッチに組み込んでいます。手動でaneを展開してextdirフォルダに置いたり、swcからlibrary.swfを取り出す作業も含めて自動化できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
## FlashDevelopプロジェクトに追加したバッチ処理の一部 set ane=hello.ane ## aneファイル名 set unzip="C:\Program Files\Lhaplus\Lhaplus.exe" ## コマンドラインから利用できる set from="C:\Users\Ryo\Desktop\%ane%" ## 解凍先はデフォルトでデスクトップになる set to=..\extdir if not exist %to% ( md %to% ) if exist .\%ane% ( copy .\%ane% .\%ane%.zip %unzip% .\%ane%.zip del .\%ane%.zip if exist %to%\%ane% ( rd /S /Q %to%\%ane% ) move %from% %to% ## デスクトップに解凍されたフォルダをextdirに移動 ) |