TypoGraphics Processing Engine

フォントで写真やイラストを再構成する画像処理プログラム。2年前にiOSアプリとして作ったものだが、アプリとしては重すぎるのでストアには公開できなかった・・・。

このプログラムでは様々なFONTや、単色のイラスト(マーク)などで写真を構築できる。画像処理としてはそれほど複雑なものではなく、単純にピクセルのテスト・アップデートの繰り返しである。ただしテスト・描画用のメモリを別にもつなど、高速化の工夫はいくつかやっている。あとはマルチスレッドとランダムアクセスをやればかなり早くなると思う。

アニメーションは画像処理のプロセスを単純に時間軸にそって出力した結果である。このままだと単調でつまんないのでインスタレーションとして再構築しようと思ってる。レンダリングマップをDBに保存して3D空間で一つ一つの文字を動かすみたいな。

Realtime slit scanの実装

スリットスキャンの静止画が撮れるアプリをiPhoneで作ってみたけど、やっぱ動画じゃないと面白くなかった。今回はPCのカメラ画像でリアルタイムにやっみようと思う。

スリットスキャンてこんなやつ。


Slit Scan from Stephen Boehnemann on Vimeo.

以下は、プログラム作成のためのメモ。

スリットスキャンを実装する場合、時間差で過去フレームを再生するわけだから、おそらくスリット数分のフレームを保存して置かなければならない。スリットの数Nとした場合、最低限確保が必要なメモリは下記の通り。

sizeof(unsigned char)*N*3*width*height

仮に、ソースサイズが1024×768で200スリットとすると、

200*3*1024*768 =471,859,200byte=450Mbyte

最近のPCは8Gぐらい普通に積んでるから1Gぐらい使うようなアプリでもいけるのかな?

問題になりそうなのは結果画像の生成速度。各フレームのリード自体は画像一枚分だから、そこまで重くないのかもしれない。スリットの動き・形状もいろいろ考えられるけど、リアルタイム処理の場合は上から下、または下から上ならピクセル配列的にも高速に処理できそうだ。

スリット幅が1pxを超えると、おそらくフレーム間のズレが目立つため理想はN=heightなのかな。その場合メモリは1.68GByte、 スレッドの遅延は768/30fpsで20秒以上かかってしまうことになる。ちょっと遅すぎるしメモリも絶望的だなぁ。とりあえずスリットの幅を広げてフレーム間をアルファ合成する方法でやってみようと思う。あとはカメラが30のFPS出ないようであればフレーム間がガタガタになってしまいそう。

この辺りの落とし所は探りながら。相当パワーのあるマシンが必要になりそうだ。

実装は次回。

kCVPixelFormatTypeについての考察

iPhoneで動画像処理を行う場合kCVPixelBufferPixelFormatTypeKeyの値には
kCVPixelFormatType_32BGRA を用いるよりkCVPixelFormatType_420YpCbCr8BiPlanarVideoRangeが遥かに高速である。内部的には yuv->rgb 変換をやっているイメージ。一般的にピクセル単位の処理を行う場合、BGRチャンネルは扱いやすいが、YUV420で同様の処理を実装してみる。ARとかOpenCVとかやるときには結局輝度(Y)だけ使うことが多くてYUVのほうが都合が良いこともある。となるとこういう処理もたまには必要だろう。

YUV420のバイト配列は [YYYYYYYY・・・YYYYUVUVUV・・・UVUVUV]とのこと。つまり適当にピクセルのアドレスをセットした場合、
Y = *(src+y*width+x);
U = *(src+(y>>1)*width+(x>>1<<1)+width*height);
V = *(src+(y>>1)*width+(x>>1<<1)+width*height+1);
となるはず。

・・・ではないらしい。

この手法でピクセルデータを取得した場合、sessionPresetがAVCaptureSessionPreset640x480の場合は正常に動作しているのだが、AVCaptureSessionPresetHigh,AVCaptureSessionPresetMediumのとき、UVチャンネルがどうしても8px(uv値上は4px)ほどずれている。何らかのpadding処理かと思ってもルールが不明だった。ちなみにlowの場合、CVPixelBufferGetのサイズは480×360で返却されるが、上記の処理を行う場合は480×368にすると画像が正常にと表示される。ますます混乱する。

いろいろ調べたところ、CVPixelBufferGetBaseAddressOfPlaneの第2引数が使えるようだ。CVPixelBufferGetPlaneCountの値が2を返すことから2枚のPlaneとして構成されていると推測。

つまり
uint8_t *y = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
uint8_t *uv = (uint8_t*)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);
である。うまくいった。

以下、座標x,yからrgb 取得するファンクション。
上で取得したYのアドレスとUVのアドレスを使えばOK

inline void getPixelColorAtPoint(int x,int y,color *out,uint8_t *src,uint8_t *srcUV=0 ){
 int t,Y,Cr,Cb;
 if(420YpCbCr8BiPlanarVideoRange){
 t = (y>>1) * mWidth + (x>>1<<1);
 Y = (-16 + *(src+ y * mWidth + x))*1.16438356;
 Cb = (-128 + *(srcUV+t))*1.1383928;
 Cr = (-128 + *(srcUV+t + 1))*1.1383928;
 out->r=clamp(Y+Cr+(Cr>>2)+(Cr>>3)+(Cr>>5));
 out->g=clamp(Y-((Cb>>2)+(Cb>>4)+(Cb>>5))-((Cr>>1)+(Cr>>3)+(Cr>>4)+(Cr>>5)));
 out->b=clamp(Y+Cb+(Cb>>1)+(Cb>>2)+(Cb>>6));
 }else{
 t=(y*mWidth+x)*mInDepth;
 out->b=*(src+t);
 out->g=*(src+t+1);
 out->r=*(src+t+2);
 }
}

CPPです。ちなみにレンジは Y’が 16–235。Cb/Crが 16–240。この辺は http://en.wikipedia.org/wiki/YUV を参考に書いてみたけど、ちょっと自信ない。clampしないとオーバーフローするし。なんとなくpreviewと色は合ってるのでまぁいいか。

ちなみに、このやり方だとUVチャンネルの読み取りに4倍コストかかるし、ポインタの連読性もないので、画像全体を描画するにはロスが大きい。スポイトのような用途で特定の1pxのBGR取得したい場合につかえます。そもそも画面全体を変換するならGPUに委譲すべきだね。