Androidでリアルタイム画像処理の限界について(メモリ)
画像処理は大きなデータをそれぞれ数回ずつ計算するという処理が多く,そこでボトルネックになるのが,CPUメモリ間の速度です.当然,解像度が大きくなればなるほどそれは顕著になります.例えば1920x1080xrgbを60fpsで転送するだけで,およそ0.4GB/secが消費さます.スマートフォンやPCのメインメモリ帯域は理論値5GB/sec+なので10回も転送すればフレーム落ちは避けられない計算,480pでもこの7倍の70回程度が理論的な上限です.
このような計算が実機でどのようになるのか調べてみました.
機種 | arrows M02 |
SoC | Snapdragon 410 |
CPU | Cortex-A53 |
コア数 | 4 |
クロック | 1.2 GHz |
メモリ | 32bitバス LPDDR2/3-1066 533MHz x1ch |
メモリ量 | 2GB |
メモリ速度 | 4.2GB/s |
解像度に対する実験と考察
Mat.zeros()は指定サイズの行列を確保し0埋めするメソッドなのでメモリに対するwriteのみが画素数*チャンネル分発生します.(Allocateなどのオーバーヘッドは無視する)
1080p
Mat frame1080 = Mat.zeros(1920,1080, CvType.CV_8UC3); Imgproc.resize(inputFrame,frame1080,new Size(),1,1,Imgproc.INTER_NEAREST); Imgproc.cvtColor(frame1080,frame1080,Imgproc.COLOR_RGB2GRAY); Imgproc.threshold(frame1080, frame1080, 0.0, 255.0, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
10秒間の計測で,zeros()に1.4secを費やしていてこれで平均15fpsなので1.4/(10*15)≒0.01secが一枚の転送に掛かった時間になります.すなわち60fpsで転送できるのは1,2回までです.理論値の半分にも満たりません.少なくともアラインメントの影響で1要素4Byteに(使ってるのは3Byte)なり,全体で1.3倍のサイズになっていると思われます.
480p
Mat frame480 = Mat.zeros(480,640, CvType.CV_8UC3); Imgproc.resize(inputFrame,frame480 ,new Size(),1,1,Imgproc.INTER_NEAREST); Imgproc.cvtColor(frame480 ,frame480 ,Imgproc.COLOR_RGB2GRAY); Imgproc.threshold(frame480 , frame480 , 0.0, 255.0, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);
同じく10秒間で,zeros()に0.5secと費やし30fpsなので0.5/(10*30)≒0.002secが一枚の転送に掛かった時間で,1080pの5倍+ですから相対的には大体理論通りになります.
実用的な処理をした場合のパフォーマンス
画像内の明るい物体の数を数えます.
M02のcamera APIの最大解像度の480pです.camera2 APIを使えば720pも取れますがOpenCVによるラッパーであるJavaCameraViewは対応していません.
輪郭(contour)を追っていくアルゴリズム
Mat frame = inputFrame.clone(); // グレースケール Imgproc.cvtColor(frame,frame,Imgproc.COLOR_RGB2GRAY); // 二値化手法を切り替え Core.MinMaxLocResult minMax = Core.minMaxLoc(frame); double ave = Core.mean(frame).val[0]; if(minMax.maxVal < 23){ // 固定閾値 Imgproc.threshold(frame,frame,10.0, 255.0, Imgproc.THRESH_BINARY); }else { // 大津の二値化 Imgproc.threshold(frame, frame, 0.0, 255.0, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU); } // ノイズ処理 final Mat kernel = Mat.ones(3, 3, CvType.CV_8UC1); final Point anchor = new Point(-1,-1); Imgproc.morphologyEx(frame, frame, Imgproc.MORPH_CLOSE, kernel,anchor ,1); Imgproc.morphologyEx(frame, frame, Imgproc.MORPH_OPEN, kernel,anchor ,1); // 輪郭でラベリング List<MatOfPoint> contours = new ArrayList<MatOfPoint>(); Mat hierarchy = new Mat(); Imgproc.findContours(frame, contours,hierarchy, Imgproc.RETR_EXTERNAL,Imgproc.CHAIN_APPROX_SIMPLE); // 出力にオーバーレイ Imgproc.cvtColor(frame,frame,Imgproc.COLOR_GRAY2BGR); for(int i=0; i<contours.size(); i++){ Moments moments = Imgproc.moments(contours.get(i)); int x = (int)(moments.m10/moments.m00); int y = (int)(moments.m01/moments.m00); Imgproc.circle(frame,new Point(x,y),3,new Scalar(50,50,200),5); }
Imgproc.morphologyEx()は1メソッドにつき2回(引数iterater=1のとき)ずつreadとwriteが発生しオープニングとクロージングで2回あるので合計8回のメモリアクセスが発生するため時間を要しています.1.2/(10*15)≒0.008secなのでzeros()の4倍なので,膨張と収縮を並行してキャッシュを利かせているのかもしれません.
ハフ変換によるアルゴリズム
Mat frame = inputFrame.clone(); // グレースケール ... // 二値化手法を切り替え ... // ノイズ処理 ... // ハフ変換でエン検出 Mat circles = new Mat();// 検出した円の情報格納する変数 Imgproc.HoughCircles(frame, circles, Imgproc.CV_HOUGH_GRADIENT, 2, 10, 160, 30, 7, 20); Point pt = new Point(); // 検出した直線上を緑線で塗る Imgproc.cvtColor(frame,frame,Imgproc.COLOR_GRAY2BGR); for (int i = 0; i < circles.cols(); i++){ double data[] = circles.get(0, i); pt.x = data[0]; pt.y = data[1]; double rho = data[2]; Imgproc.circle(frame, pt, (int)rho, new Scalar(0, 200, 0), 5); }
やはりハフ変換は非常に重たいですが,パラメータ次第で実用できそうです.
一応,処理結果の画像も載せておきます.(左:エッジ,右:ハフ変換)
限界を超える方法について
画像をキャッシュに収まるぐらいの細かいブロックに区切って処理することで,キャッシュがよく利くようになります.モバイルの3DCGレンダリングではGPUにメモリが乗っていないのでこの方法でメモリの使用量とアクセスを抑えています.デメリットとして関数コールが増えるのでC++で書かないと余計に遅くなると思われます.加えて,つなぎ目をどうするかも考える必要があります.
また,スマートフォンのGPUにはメモリがないのでビデオカードがある場合の話ですが,GPUのメモリはメインメモリの帯域より一桁上かつ,キャッシュもトータルで大きいのでCUDAやOpenGLで処理すれば全く同じフローでも速くなる可能性があります.(スマホでも前者の理由で少しだけ速くなるかもしれない)