Android でカメラオーバレイ
Android でカメラのプレビューの上に何か描きたい! AR やりたい! などと思い立ったので、やってみた。
AndroidManifest.xml 変更
カメラを使うので、 AndroidManifest.xml で Permission の設定を行わなければいけない。
とりあえずしたの 4 つを付けておけば良いみたい。
<uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" /> <uses-feature android:name="android.hardware.camera.flash" />
ついでに、 Android のカメラは水平でしか使えないらしいので、 AndroidManifest.xml の Activity の定義に
android:screenOrientation="landscape" android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
を追加してあげる。
SurfaceView への描画
画像を取得するところは良いとして、まずは描画だけやってみる。
上記サンプルでは、
holder.setType(view.SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
というような処理で SurfaceHolder のタイプを PUSH_BUFFERS としているが、この場合は push された画像を表示するだけで、こちらから描画命令を発行することができない。
なので、ここの部分はコメントアウトするか
holder.setType(view.SurfaceHolder.SURFACE_TYPE_NORMAL)
としてやる必要がある。
SurfaceView への描画は、
- SurfaceView::getHolder で SurfaceHolder を取得
- SurfaceHolder::lockCanvas で Canvas を取得
- Canvas にゴニョゴニョする
- SurfaceHolder::unlockCanvasAndPost で終了
という手順を踏むと、メインスレッド以外からでも描画できるそう。
ここは Scala っぽく
def draw(f: graphics.Canvas=>Unit) { val cv = this.holder.lockCanvas try { cv.save f(cv) cv.restore } finally { this.holder unlockCanvasAndPost cv } }
みたいな関数を用意するとなかなかいいかもしれない。
もっと Scala っぽくするなら SurfaceHolder から implicit conversion で上記メソッドを使えるようにしてしまうと良いのかも。
で、これを別スレッドでひたすら走らせ、描画を続ける。
これで Canvas::drawText でいくらでもデバッグプリントができるわけですよ。
Log でも良いんだけど、リアルタイムで FPS とか出したいときはこっちの方がいいね。
プレビューの画像を取得する
カメラでプレビューに使われている画像を取得するには、 Camera::setPreviewCallback で Camera::PreviewCallback クラスのインスタンスを渡してやればいい。
PreviewCallback は抽象クラスで、 onPreviewFrame(data: Array[Byte], cam: hardware.Camera) というメソッドを定義してやる必要がある。
onPreviewFrame メソッドの第一引数にカメラのプレビュー画像が入っている。
ただし、このバイト列は画像のヘッダ情報等がなく、生画像データでしかないので、 BitmapFactory::decodeByteArray しても Bitmap クラスのインスタンスは受け取れない。
それらに関する情報は以下で問題としてあげられている。
ログイン - Google アカウント
挙げられている問題点としては
- onPreviewFrame の第一引数が decodeByteArray できない
- バイト列のフォーマットが YUV422 である
- Camera::Parameters::setPreviewFormat で Bitmap::Config::RGB_565 などと指定しても反映されない
といったもので、このままでは onPreviewFormat で取得できるデータを描画に用いることはできない模様。
この問題が挙げられているのが 2008/08 なので、修正されることはないのかなーと思う。
YUV422 を変換する
上記問題に対する解決策として、 YUV_422 -> ARGB_8888 の変換を行う関数が掲載されているので、今回はこれを用いた。
変換関数は Java での実装であるが、ここはやはり Scala で再実装でしょう、ということで実装し直そうとしてみた。
してみたのだが、とりあえず輝度成分だけ取り出してグレースケール画像に変換する処理を書いただけで一回の変換に 200ms とかかかってしまい、さすがに遅すぎるので挫折した。
一応 nexus one でやっているんですけどね…。
def YUV422toARGB9999(src: Array[Byte], dest: Array[Int], w: Int, h: Int) { 0 until h foreach { y=> { 0 until w foreach { x=> { val i = y * w val s = src(i).asInstanceOf[Int] & 0xff dest(i) = 0xff000000 | s << 16 | s << 8 | s } } } } }
これを Java で書くと 20ms くらいで終わる。
RichInt::until で生成される Range と Range::foreach の実装がどのようになっているかわからないけど、 foreach での関数呼び出しが嵩んでしまっているのかな、という印象。
実に負けた気がするけれど、仕方ないので Java で実装してやった。
結局 YUV422 -> ARGB_8888 の変換処理は 100ms 前後で終わるっぽい。ここは NDK 使わないと遅すぎるかなーという感じ。
そもそも最初は副作用の存在すら許したくなかったので、 Array[Byte].map でやろうとしていたけど、そんなことすると一時変数がすごいことになって GC されまくりそうだったのでやめておいた。
描画する
あとはこの ARGB_8888 の Array[Int] を Bitmap::createBitmap で Bitmap 化して Canvas::drawBitmap で描画するだけ。
その他
SurfaceView::Callback::surfaceCreated の中で Camera::startPreview を呼んでプレビューを開始する際、一回目だと何故かプレビューが開始されないという問題が発生した。
どうやら初期化のタイミングの問題らしく、 Actor を使って startPreview を呼ぶタイミングをずらしたら動いた。
actors.Actor.actor{ Thread.sleep(1000) this.camera.startPreview }
Actor 便利だね!
カメラを使うアプリケーションを開発しているときに困ること
カメラを使うアプリケーションの開発中に、アプリケーションの反応がなくなって OS 側で強制終了を行なわないといけないような事態になって、アプリケーションを強制終了すると、アプリケーションがカメラを掴みっぱなしになってしまう。
こうなってしまうとすべてのカメラを使うアプリケーションがカメラの初期化時に失敗するようになってしまい、カメラが使えなくなってしまう。
こうなってしまうと、端末の再起動以外に打つ手がなくなるので注意!
これが写真を撮るようなときはちょっとミスるとすぐ発生したりして結構困る。