もう何も怖くないvImage
どうも、吉村です。
最近画像を大変良く扱うiOSアプリ制作に従事しておりました。
そこでvImageが大変訳に立ちましたので、ちょっとそのノウハウをまとめたいと思います。
1、vImageとは何か?
簡単に言えば、iOS, Macプラットフォームで動く、ハードウェアを超絶活かした爆速画像処理API郡のことです。
うまく使うと、消費電力の削減や、処理の大幅高速化につながります。
得意なことは、低レベルな画像処理です。
APIをざっと眺めると何となく得意なことが分かるとおもいます。
色々ありますが、私が最近よく使ったのは、
画像の回転・拡大縮小・反転、画像処理カーネルの畳み込み演算等です。
こういった処理を非常に高品質かつ超高速に実行できるvImageは、
非常に便利でもあり、頼もしく安心感があります。
ただし、APIに少し癖があり、少々使いにくい側面もあります。
2、基盤
まずはvImageで使う画像のデータ構造を紹介します。
vImage_Buffer
こいつです。
これはvImageで画像を扱うための構造体であり、
vImage_Types.hヘッダーを参照すると、以下の様に定義されています。
1 2 3 4 5 6 7 |
typedef struct vImage_Buffer { void *data; /* Pointer to the top left pixel of the buffer. */ vImagePixelCount height; /* The height (in pixels) of the buffer */ vImagePixelCount width; /* The width (in pixels) of the buffer */ size_t rowBytes; /* The number of bytes in a pixel row, including any unused space between one row and the next. */ }vImage_Buffer; |
これを見る限り、以下の事が分かります。
・dataが画像の生データへのポインタ。左上から右下へ並んでいる。
・width, heightのピクセル数
・rowBytesが1行のバイト数
・メモリの管理機構を持たない
UIImageやCGImageRef等と比べると、ずいぶんと割り切ってシンプルな構造になっています。
この説明には、
void *dataのデータ型が明記されていませんが、
rowBytesの存在と、vImageのAPI自体にフォーマットの指定があるためです。
また余談ですが、vImageのヘッダーは、予想以上に丁寧なコメントが多く、
困ったらヘッダーを読む、というスタイルがオススメです。
さて、vImageに処理をさせる為には、
汎用画像フォーマットと、vImage_Buffer構造体を相互に自由に変換できなければなりません。
一応、iOS7からはvImageのAPIに変換用の関数が追加されたのですが、
あまり使い勝手が良くないのと、さほど高速でもなかったので、現段階では自身でラッパーを書いています。
少し長いので、gistにします。
ヘッダー
https://gist.github.com/Ushio/c672442388fb54665f5a
実装
https://gist.github.com/Ushio/5a184e11a1302fb8fd2d
これを使えば、
1 2 3 4 |
NSString *path = [[NSBundle mainBundle] pathForResource:@"Lenna.png" ofType:nil]; NSData *data = [NSData dataWithContentsOfFile:path]; WAMVImageBufferARGB8888 *srcImageBuffer = [[WAMVImageBufferARGB8888 alloc] initWithData:data]; vImage_Buffer *vImageBuffer = srcImageBuffer.vImageBuffer; |
のようにvImage_Bufferを作成し、
1 2 3 |
[dstImageBuffer scopedCreateCGImage:^(CGImageRef image) { _imageView.image = [UIImage imageWithCGImage:image]; }]; |
のように簡単にCGImageRefを作成する事が出来ます。
非同期でないのにblocksになっている理由は、CGImageRefの所有権問題を低減させるためです。
これで自由にvImage_Buffer構造体を構築することが出来る様になりました。
3、API
分かりやすい最初のAPIとして
vImageRotate90_ARGB8888
を題材にして、vImageAPIの基礎を紹介します。
vImageRotate90_ARGB8888は画像を90度回転させるAPIです。
宣言はこうなっています。
1 |
vImage_Error vImageRotate90_ARGB8888( const vImage_Buffer *src, const vImage_Buffer *dest, uint8_t rotationConstant, Pixel_8888 backColor, vImage_Flags flags ); |
さて、細かく見てみましょう。
まずは名前に着目しましょう。
vImageの関数は基本的に、
vImage [処理の名前] _ [vImage_Bufferのフォーマット]
の形式で統一されています。
なので、名前こそ長いものの、恐れることは何もありません。むしろ分かりやすいと言えます。
次に引数です。
src, destは入力と出力なので良いですね、どちらもvImage_Bufferです。
constの存在からsrcには変更がない、ということが分かります(当然かもしれませんが)。
次のrotationConstantは、回転数です。名前から分かります。
backColorは、サイズが合わないときの色ですね、
大抵はサイズを合わせるので、適当に、
uint8_t backcolor[] = {255, 255, 255, 255};
等としておきましょう。
最後にvImage_Flagsです。
これは、kvImageNoFlagsや、kvImageHighQualityResampling等色々あるのですが、
それぞれのAPIごと有効なフラグが違います。
これはそれぞれのAPIリファレンスを参照しましょう。
APIごとにオプションを定義すると、定数がいたずらに増えてしまうので、
ある程度共通でオプションを定義している、ということですね。
vImageの関数の引数の構造は、命名規則とおなじく、ほとんどの関数で、以下の様になっています。
[入力画像 vImage_Buffer] [出力画像 vImage_Buffer], [関数固有パラメータ], [vImage_Flags]
vImageの関数には引数が多くて嫌になりそうですが、
このように整理して考えると、さほどややこしくないことが分かります。
APIの構造を統一することの大切さを実感できる、良いお手本でもありますね。
戻り値は、vImage_Errorで、エラーコードが返ってくるので、
面倒でもハンドリングしておくと良いでしょう。
もうここまで理解がすすめば、
あとはもうリファレンスを自由に飛び回り、APIを呼びまくって見るのが良いでしょう。
4、最後に
いくつか良く使ったvImage APIと、Tipsを紹介して終わろうとおもいます。
vImageConvolve_ARGB8888
こちらは畳み込み演算を実行する関数です。N x Nのカーネルを使えます。
ガウシアンブラーなんかは、N x 1のカーネルと、1 x Nのカーネルをそれぞれ1回ずつ、合計2回呼び出せば、実現できますね。
vImageAffineWarp_ARGB8888
こちらは行列を使って、画像をトランスフォームします。
vImage_AffineTransformが肝になりますが、
こちらはCGAffineTransformと基本同じ構成なので、CoreGraphicsのAPIで行列を作ることをオススメします。
また、この関数は浮動小数点演算を使うため、
90度回転等シンプルな処理の場合は、vImageRotate90_ARGB8888等の方が圧倒的に高速です。
kvImageHighQualityResamplingを状況に応じてつけると良いでしょう。
vImageRotate_ARGB8888
これは回転のみに特化した関数です。vImageAffineWarp_ARGB8888と同様、vImageRotate90_ARGB8888でまかなえる場合はこちらを使いましょう。
vImageLookupTable_Planar8toPlanarF
こちらはテーブルを使ったフォーマット変換です。数値計算で変換するよりも高速です。
vImageScale_ARGB8888
拡大縮小をします。倍率は、destのサイズで決まります。
kvImageHighQualityResamplingを状況に応じてつけると良いでしょう。
vImageVerticalReflect_ARGB8888
上下反転します。
状況によっては以下のコードで済みますので、この関数を使う必要がない場合もあります。
1 2 3 4 5 6 7 8 |
void MyFastVerticalReflect( vImage_Buffer *buf ) { // Point to the last scanline buf->data += (buf->height - 1) * buf->rowBytes; // Make rowBytes negative buf->rowBytes = -buf->rowBytes; } |
vImageHorizontalReflect_ARGB8888
左右反転します。
vImageHistogramCalculation_ARGB8888
ヒストグラムを計算します。
いかがだったでしょうか。
vImageは適切な場面で活用すれば、非常に協力に我々をサポートしてくれます。
是非みなさんも触ってみてはいかがでしょうか。