OpenCV for Androidでカメラプレビューをクロッピングする

OpenCV4Androidのカメラプレビューをクロッピングします．

ちなみにOpenCV4Androidのカメラの使い方は
http://rest-term.com/archives/3010/
などが参考になります．
この記事でも，こんなコードを想定します．
JavaCameraViewを使います．

#問題
OpenCV4androidを使ったandroidアプリでカメラプレビューを正方形にクロッピングしたくなりました．
安直に`onCameraFrame()`で画像をクロップして返せばいいやと思ってやってみたところ，

```java
    @Override
    public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
        mRgba = inputFrame.rgba();
        return new Mat(mRgba, mROI);
    }
```

以下のようなエラーを吐き続けてプレビューが表示されなくなりました．

```
08-10 23:08:14.137  30863-31993/com.example E/cv::error()﹕ OpenCV Error: Assertion failed (src.dims == 2 && info.height == (uint32_t)src.rows && info.width == (uint32_t)src.cols) in void Java_org_opencv_android_Utils_nMatToBitmap2(JNIEnv*, jclass, jlong, jobject, jboolean), file /hdd2/buildbot/slaves/slave_ardbeg1/50-SDK/opencv/modules/java/generator/src/cpp/utils.cpp, line 97
08-10 23:08:14.137  30863-31993/com.example E/org.opencv.android.Utils﹕ nMatToBitmap catched cv::Exception: /hdd2/buildbot/slaves/slave_ardbeg1/50-SDK/opencv/modules/java/generator/src/cpp/utils.cpp:97: error: (-215) src.dims == 2 && info.height == (uint32_t)src.rows && info.width == (uint32_t)src.cols in function void Java_org_opencv_android_Utils_nMatToBitmap2(JNIEnv*, jclass, jlong, jobject, jboolean)
08-10 23:08:14.137  30863-31993/com.example E/CameraBridge﹕ Mat type: Mat [ 480*480*CV_8UC4, isCont=false, isSubmat=true, nativeObj=0x78cfb440, dataAddr=0x7a91a1f0 ]
08-10 23:08:14.137  30863-31993/com.example E/CameraBridge﹕ Bitmap type: 720*480
08-10 23:08:14.137  30863-31993/com.example E/CameraBridge﹕ Utils.matToBitmap() throws an exception: /hdd2/buildbot/slaves/slave_ardbeg1/50-SDK/opencv/modules/java/generator/src/cpp/utils.cpp:97: error: (-215) src.dims == 2 && info.height == (uint32_t)src.rows && info.width == (uint32_t)src.cols in function void Java_org_opencv_android_Utils_nMatToBitmap2(JNIEnv*, jclass, jlong, jobject, jboolean)
```

`JavaCameraView`の親クラス`CameraBridgeViewBase`は表示用バッファとして`Bitmap mCacheBitmap`を持っていて，初期化時にこれをカメラのフレームサイズで確保します．
そして`onCameraFrame()`から返る`Mat`を`Utils.matToBitmap()`で`mCacheBitmap`に入れてプレビュー表示処理に回します．
しかし，`onCameraFrame()`からサイズを変えた`Mat`を返すと，`Utils.matToBitmap()`で変換元と変換先のサイズが合わずに上記のエラーになります．

#対策
JavaCameraViewを継承したビュークラスを作り，クロッピングを行いました．

```java:CameraView.java
package com.example;

import android.content.Context;
import android.util.AttributeSet;

import org.opencv.android.JavaCameraView;
import org.opencv.core.Mat;
import org.opencv.core.Rect;

public class CameraView extends JavaCameraView {
    private Rect mROI;
    private int mSquareFrameWidth;

    public CameraView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void AllocateCache()
    {
        int frameWidth = mFrameWidth, frameHeight = mFrameHeight;
        mSquareFrameWidth = Math.min(mFrameWidth, mFrameHeight);
        mFrameWidth = mSquareFrameWidth;
        mFrameHeight = mSquareFrameWidth;
        super.AllocateCache();
        mFrameWidth = frameWidth;
        mFrameHeight = frameHeight;

        mROI = new Rect((mFrameWidth - mSquareFrameWidth)/2, (mFrameHeight - mSquareFrameWidth)/2, mSquareFrameWidth, mSquareFrameWidth);
    }


    @Override
    protected void deliverAndDrawFrame(CvCameraViewFrame frame) {
        super.deliverAndDrawFrame(new CroppedCameraViewFrame(frame, mROI));
    }

    private class CroppedCameraViewFrame implements CvCameraViewFrame {
        private Rect mROI;
        private CvCameraViewFrame mOriginalFrame;

        @Override
        public Mat gray() {
            return new Mat(mOriginalFrame.gray(), mROI);
        }

        @Override
        public Mat rgba() {
            return new Mat(mOriginalFrame.rgba(), mROI);
        }

        public CroppedCameraViewFrame(CvCameraViewFrame original, Rect ROI) {
            mOriginalFrame = original;
            mROI = ROI;
        }
    }
}
```

そして，レイアウトファイルで使うビューをこれに差し替えます

```xml:activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:opencv="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.CameraView
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:id="@+id/camera_preview"
        opencv:camera_id="any" />

</LinearLayout>
```

## 説明
上述したとおり，カメラのフレームサイズで`Bitmap mCacheBitmap`が初期化されますが，それを行っているのが`JavaCameraView.AllocateCache()`です．
継承したクラスでは，これを呼ぶ前に`mFrameWidth`,`mFrameHeight`を書き換えて初期化するサイズをごまかします．ここでは正方形にクロッピングするようにしました．
`AllocateCache()`を呼んだあとは元の値に戻します．

また，クロッピングに使う`Rect`オブジェクト`mROI`を作っておきます．

```java
    @Override
    protected void AllocateCache()
    {
        int frameWidth = mFrameWidth, frameHeight = mFrameHeight;
        mSquareFrameWidth = Math.min(mFrameWidth, mFrameHeight);
        mFrameWidth = mSquareFrameWidth;
        mFrameHeight = mSquareFrameWidth;
        super.AllocateCache();
        mFrameWidth = frameWidth;
        mFrameHeight = frameHeight;

        mROI = new Rect((mFrameWidth - mSquareFrameWidth)/2, (mFrameHeight - mSquareFrameWidth)/2, mSquareFrameWidth, mSquareFrameWidth);
    }
```

`JavaCameraView.deliverAndDrawFrame()`が`onCameraFrame()`を呼び，返ってきた`Mat`を`Utils.matToBitmap()`で`mCacheBitmap`に入れています．
そこで，子クラスで`deliverAndDrawFrame()`をオーバーライドし，`JavaCameraView.deliverAndDrawFrame()`に渡る`CvCameraViewFrame`を実装したオブジェクトを`CroppedCameraViewFrame`に差し替えています．
`CroppedCameraViewFrame`は元々の`CvCameraViewFrame`オブジェクトをラップし，`rgba()`と`gray()`で元のオブジェクトの`rgba()`,`gray()`の返り値をクロッピングして返すことでクロッピングを実現しています．

```java

    @Override
    protected void deliverAndDrawFrame(CvCameraViewFrame frame) {
        super.deliverAndDrawFrame(new CroppedCameraViewFrame(frame, mROI));
    }

    private class CroppedCameraViewFrame implements CvCameraViewFrame {
        private Rect mROI;
        private CvCameraViewFrame mOriginalFrame;

        @Override
        public Mat gray() {
            return new Mat(mOriginalFrame.gray(), mROI);
        }

        @Override
        public Mat rgba() {
            return new Mat(mOriginalFrame.rgba(), mROI);
        }

        public CroppedCameraViewFrame(CvCameraViewFrame original, Rect ROI) {
            mOriginalFrame = original;
            mROI = ROI;
        }
    }
```