THETAプラグインで画像処理【FastCV画像処理編】

こんにちは、リコーの@roohii_3です。
本記事は「[THETAプラグインで画像処理【FastCV導入編】][prev-article]」の続きです。

<del>
<br>※【11/28(水) もくもく会@銀座】
11/28(水)にRICOH THETAプラグイン開発のもくもく会を企画しています。
ご興味ある方はぜひご参加ください。
</del>
→ご参加頂いた皆様ありがとうございました！

## はじめに
2018年11月現在の[RICOH THETA](https://theta360.com/ja/)の最新機種、**RICOH THETA V**では「**プラグイン**」機能を使ってTHETAをカスタマイズできます。
さらに、「**RICOH THETAプラグインパートナープログラム**」に登録するとプラグインを作ることもできます。
パートナープログラムについては、[記事の一番最後](#ricoh-thetaプラグインパートナープログラムについて)をご覧ください。

本記事では前回に引き続き、THETAプラグインで[FastCV](https://developer.qualcomm.com/software/fastcv-sdk)を動かす方法をまとめます。
前回は **FastCVの導入まで** でしたが、今回はいよいよ **画像処理をする手順** をご紹介したいと思います。

また、本記事のソースコードは下記で公開されているので参考にしてみてください。

[ricohapi/theta-plugin-fastcv-sample](https://github.com/ricohapi/theta-plugin-fastcv-sample)

先にお見せすると、このプラグインを使うと下のような画像が撮れます。
普通に静止画を撮影するようにシャッターを押せば、THETA内部で自動的に画像処理してくれます。
![canny_sample_thum.png](https://qiita-image-store.s3.amazonaws.com/0/304709/8738d6bb-2d58-5c90-e888-128dfefea1fe.png)



## 事前準備
### AndroidStudioプロジェクト
本記事のプラグインではカメラの制御も必要になりますが、その部分は本記事の範疇を超えているので、サンプルコードを流用することにします。
下記リポジトリにて公開されている、**CameraAPI**[^1][^2] を使ったサンプルコードをベースにします。

[ricohapi/theta-plugin-camera-api-sample](https://github.com/ricohapi/theta-plugin-camera-api-sample)

ダウンロードしたものをAndroidStudioで読み込み、プラグインの名称やアプリケーションID等を変更してから使います。
各名称の変更方法は[前回記事「プロジェクトファイルの準備 > 2.名称変更」項][prev-article]を参考にしてください。

**※メタデータ(Exif、XMP等)について**<br>
CameraAPIを使う場合は、撮影データにExifやXMPなどのメタデータが付与されません。
WebAPI経由で撮影した場合はメタデータが付くようになっているため、メタデータが必要な場合はWebAPIベースでの作成を検討してみてください。
[ricohapi/theta-automatic-face-blur-plugin](https://github.com/ricohapi/theta-automatic-face-blur-plugin) の [Exifクラス](https://github.com/ricohapi/theta-automatic-face-blur-plugin/blob/master/app/src/main/java/com/theta360/automaticfaceblur/exif/Exif.java) が参考になると思います。

### パーミッションの設定
このプラグインを使用するには、 **カメラ、マイク、ストレージ** の **パーミッション許可** が必要です。
パーミッションに許可がなければ、プラグインを起動してもすぐに落ちてしまいます。

なお、プラグインストア経由でインストールしたプラグインについては、マニフェストの記載に従い自動的にパーミッションに許可が入るようになっています。
今回のように開発時には手動で設定を行ってください。

開発時には、[Vysor](https://www.vysor.io/)を利用して事前にパーミッション設定をしておくと楽です。
Vysorについては下記記事が参考になります。

[THETAプラグイン開発におけるVysorの使い方【THETAプラグイン開発】](https://qiita.com/KA-2/items/9dce1a165fcf0203b1cb)

#### パーミッションの設定方法
以下の手順で、パーミッション設定を行います。

* AndroidStudioで、用意したプロジェクトを開く
* Vysorを立ち上げておく
* AndroidStudioから **"▶"** または **"Build > Run"** を選択し、THETA上でプラグインを実行<br>(パーミッション設定をしていないため、実行したプラグインはすぐ落ちてしまうはず)
* Vysorにて、ホーム画面からアプリ一覧を表示
* **Settings** アプリを開き、**"(Plugin名) > Permissions"** を選択
* 各種項目をONにする<br>
![permissions.png](https://qiita-image-store.s3.amazonaws.com/0/304709/00886bfe-fd8e-3efe-e306-f7b175f6eb22.png)


## 処理の方法
今回作るプラグインでは、[サンプルコード](https://github.com/ricohapi/theta-plugin-camera-api-sample)の **静止画撮影** の部分を改造します。
静止画撮影では、大まかには以下のような処理を行っています。

1. シャッターボタンが押されたことを認識
1. 撮影パラメータ（露出やホワイトバランス、画像形式など）の設定
1. カメラ制御による撮影
1. 撮影画像データの取得
1. 画像データの保存

本プラグインでは「4. 撮影画像データの取得」と「5. 画像データの保存」の間で画像処理を行います。
本項では、画像処理前後に必要な処理についてまとめます。

### 撮影画像データの取得方法
カメラにまつわる処理は`CameraFragment.java`で実装されており、撮影画像のデータは`Camera.PictureCallback` の `onPictureTaken` メソッドから取得できます。
第一引数の`byte[] data`に、JPEG形式で画像データが入っています。
この`data`を画像処理します。

```CameraFragment.java
private Camera.PictureCallback onJpegPictureCallback = new Camera.PictureCallback() {
    @Override
    public void onPictureTaken(byte[] data, Camera camera) {
        
        // ...

        byte[] dst = mImageProcessor.process(data);

        // ...
    }
};
```

上の例では、`ImageProcessor`クラスの`process`メソッドに撮影画像データ`data`を渡すと、画像処理されたデータ`dst`が返ってくることを想定しています。



### 画像データの取り扱い方
#### 画像処理の前処理
上で触れたように、`onPictureTaken` で取得できる画像は **JPEG形式** になっています。
一方で、FastCVではJPEG形式を扱うことを想定していません[^3]。
そのため、FastCVに入力する画像は **RGBA8888** などのピクセルフォーマットに変換しなければなりません。

##### RGBA8888形式について
RGBA8888のデータの中身は下図のようになっています。
画素単位でRed, Green, Blue, Alpha(不透明度)の値が順に並び、各チャンネルには0～255(8bit)の値が入ります。
JPEGは画像圧縮形式であることに対し、このようにRGBA8888では全画素の情報を持ちます。
※ R,G,B,Aの並び方は、システムやライブラリによって異なります。それぞれのドキュメントを参照し、並び順を確認してください。

![rgba8888.png](https://qiita-image-store.s3.amazonaws.com/0/304709/f41ba9a9-ca1d-0b89-f869-e79a3465e9f9.png)


上でも述べましたが、FastCVへ入力するための前処理としては、**JPEG** から **RGBA8888** への変換を行います。
**RGBA8888** は **Bitmapクラス** として扱えます。
さらに注意すべきところとして、NativeコードへはBitmapクラスをそのまま渡せないので、Bitmapクラスから **byte配列** を取り出して渡します。

##### 撮影画像データを受け取ってからFastCV（Nativeコード）に渡すまで
前処理を図にすると以下のような流れです。

![before-proc.png](https://qiita-image-store.s3.amazonaws.com/0/304709/5278e91e-7602-495a-17ef-84ea31d1df1f.png)


#### 画像処理の後処理
FastCV画像処理からは、RGBA8888形式の画像データがbyte配列で出力されます。
画像処理の後処理では、画像処理後のデータを **JPEG** や **PNG** などに変換（画像圧縮）し、保存するための形式にします。
画像データを圧縮する際には、Bitmapクラスのメソッドを使います。そのために、画像処理後のデータをbyte配列からBitmap型に変換します。

##### FastCV（Nativeコード）から画像処理後データを受け取ってから保存するまで
後処理を図にすると以下のような流れです。

![after-proc.png](https://qiita-image-store.s3.amazonaws.com/0/304709/fc1ab816-9557-6c50-8cd7-9e988726a0c9.png)


#### 各フォーマットの相互変換の方法
Android SDKの **Bitmap** や **BitmapFactory** を使えば、上に述べた画像の形式や型の相互変換ができます。
これらの相互変換については、下記記事などを参考にしてください。
[AndroidでのBitmap/JPEG/byte配列の相互変換](https://qiita.com/aymikmts/items/7139fa6c4da3b57cb4fc)


### FastCVでの画像処理
画像処理の流れは、以下のような簡単なものです。

1. RGB888化
1. グレースケール化
1. Cannyフィルタ
1. 色反転
1. RGBA8888化

基本的にFastCV APIを用います。
しかし[APIリファレンス](https://developer.qualcomm.com/docs/fastcv/api/index.html)によると **RGBA8888 → グレースケール** のAPIはなく、代わりに **RGB888 → グレースケール** はあったので、「1. RGB888化」の処理を加えています。
また、**グレースケール → RGBA8888** もないようだったので、「5. RGBA8888化」は自作しました。

#### 関数
1.～4.については、それぞれ以下のAPIを使います。

1. `fcvColorRGBA8888ToRGB888u8()`
1. `fcvColorRGB888ToGrayu8()`
1. `fcvFilterCanny3x3u8()`
1. `fcvBitwiseNotu8()`

#### FastCVでの画像データの扱い
FastCVの関数の引数として画像データを使うものに関しては、アライメント指定がされているものもありました。
たとえば`fcvColorRGB888ToGrayu8()`では、入力画像`src`と出力画像`dst`に128bitでアライメントされた配列を指定されています。

![alignment.png](https://qiita-image-store.s3.amazonaws.com/0/304709/e040e591-4c33-0d19-4d72-325359a1cf19.png)


そういった場合、`fcvMemAlloc()`を使うと、メモリの動的確保と同時にアライメント指定もできるようです。
解放時には`fcvMemFree()`を使います。

```cpp
void* dst = fcvMemAlloc( width*height, 16);     // <- 128bitアライメントでメモリを確保

fcvColorRGBA8888ToRGB888u8((uint8_t*)src, width, height, 0, (uint8_t*)dst, 0);

fcvMemFree(dst);    // <- 解放
```


## 実装
本項で実装例を紹介します。
以下では **画像取得～画像処理～保存** の部分を抜粋しており、実際はFastCVの **初期化・終了処理** も実装する必要があります。
FastCVを扱う部分はNativeコード（C/C++）で実装します。
詳しくは[前回記事][prev-article]をご参照ください。
また、下記実装例ではエラー処理が不十分なので流用される際はご注意ください。


### Java側
**画像取得～FastCV（Nativeコード）に渡すまで** と、**Nativeから画像処理後データを取得～保存まで** を実装します。

ここでは、Java ↔ Nativeコードのやりとりを担う`ImageProcessor`クラスを定義しました。
`CameraFragment`の`onPictureTaken`で撮影画像データを`ImageProcessor`に渡し、諸々処理するようにしています。
`ImageProcessor`からはJPEG圧縮された画像データが返るようにしており、そのまま保存しています。

```CameraFragment.java
/**
 * CameraFragment
 */
public class CameraFragment extends Fragment {

    private Camera.PictureCallback onJpegPictureCallback = new Camera.PictureCallback() {
        @Override
        public void onPictureTaken(byte[] data, Camera camera) {

            // ...

            byte[] dst = mImageProcessor.process(data);     // <- 画像データを渡すと画像処理結果が返る

            if (dst != null) {
                // 画像保存
                String fileName = String.format("fastcv_%s.jpg", getDateTime());
                String fileUrl = DCIM + "/" + fileName;
                try (FileOutputStream fileOutputStream = new FileOutputStream(fileUrl)) {
                    fileOutputStream.write(dst);            // <- 画像保存
                    registerDatabase(fileName, fileUrl);    // <- Databaseへ登録
                    Log.d("CameraFragment", "save: " + fileUrl);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            // ...

        }
    };

    // ...

    private void registerDatabase(String fileName, String filePath){
        ContentValues values = new ContentValues();
        ContentResolver contentResolver = this.getContext().getContentResolver();
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
        values.put(MediaStore.Images.Media.TITLE, fileName);
        values.put("_data", filePath);
        contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
    }   
}
```

`ImageProcessor`の`process`メソッドでは、[画像データの取り扱い方](#画像データの取り扱い方)で紹介した前処理とNativeコードへの値受け渡し、後処理を行っています。

```ImageProcessor.java
public class ImageProcessor {

    // ...

    public byte[] process(byte[] data) {
        Log.d(TAG, "ImageProcess");

        /**
        * 前処理
        * JPEG形式からRGBA8888形式に変換する
        */
        // BitmapFactory.decodeByteArray()で、byte配列→Bitmap型の変換と同時にJPEG形式→RGBA8888形式への変換が行われる
        Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length);
        // byte配列を取り出すために、ByteBufferにBitmapの中身をコピー
        ByteBuffer byteBuffer = ByteBuffer.allocate(bmp.getByteCount());
        bmp.copyPixelsToBuffer(byteBuffer);

        int width = bmp.getWidth();
        int height = bmp.getHeight();


        /**
        * FastCV（nativeコード）で画像処理
        */
        // byteBuffer.array()でbyte配列を取り出し、nativeコードへ渡す
        byte[] dstBmpArr = cannyFilter(byteBuffer.array(), width, height);
        if (dstBmpArr == null) {
            Log.e(TAG, "Failed to cannyFilter(). dstBmpArr is null.");
            return null;
        }


        /**
        * 後処理
        * 画像処理後データをJPEG形式に圧縮する
        */
        // byte配列をBitmap型変数の中身にコピー
        Bitmap dstBmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        dstBmp.copyPixelsFromBuffer(ByteBuffer.wrap(dstBmpArr));

        // JPEGに圧縮
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        dstBmp.compress(Bitmap.CompressFormat.JPEG,100, baos);
        byte[] dst = baos.toByteArray();

        return dst;
    }

    // Native Functions
    private native byte[] cannyFilter(byte[] data, int width, int height);

    // ...

}

```

画像データの処理にメモリを多く使うため、`AndroidManifest.xml`でLargeHeapを有効化しておきます。

```AndroidManifest.xml
    ...

    <application
        ...
        android:largeHeap="true">
        ...
    </application>
    ...

```


### FastCV（Nativeコード）
**Java側から渡ってきた画像をFastCVで画像処理し、処理後データをJavaに戻す**処理をNativeコード（C/C++）に実装します。
また、グレースケールからRGBA8888に変換する関数も実装します。

```FastCVSample.cpp

// ...

void colorGrayToRGBA8888(const uint8_t* src, unsigned int width, unsigned int height, uint8_t* dst){

    if (src == NULL) {
        DPRINTF("colorGrayToRGBA8888() src is NULL.");
        return;
    }
    if (dst == NULL) {
        DPRINTF("colorGrayToRGBA8888() dst is NULL.");
        return;
    }

    // グレースケールからRGBA8888へ変換
    // R,G,Bに同じ値をコピーする
    for (unsigned int i = 0; i < width*height; i++) {
        dst[4*i]   = src[i];
        dst[4*i+1] = src[i];
        dst[4*i+2] = src[i];
        dst[4*i+3] = 0xFF;     // アルファチャンネル
    }
}

// ...

JNIEXPORT jbyteArray
JNICALL Java_com_theta360_fastcvsample_ImageProcessor_cannyFilter
        (
                JNIEnv* env,
                jobject obj,
                jbyteArray img,
                jint w,
                jint h
        )
{
    DPRINTF("cannyFilter()");

    /**
     * convert input data to jbyte object
     */
    jbyte* jimgData = NULL;
    jboolean isCopy = 0;
    jimgData = env->GetByteArrayElements( img, &isCopy);
    if (jimgData == NULL) {
        DPRINTF("jimgData is NULL");
        return NULL;
    }

    /**
     * process
     */
    // RGBA8888 -> RGB888
    void* rgb888 = fcvMemAlloc( w*h*4, 16);
    fcvColorRGBA8888ToRGB888u8((uint8_t*) jimgData, w, h, 0, (uint8_t*)rgb888, 0);

    // グレースケール化
    void* gray = fcvMemAlloc( w*h, 16);
    fcvColorRGB888ToGrayu8((uint8_t*)rgb888, w, h, 0, (uint8_t*)gray, 0);

    // Cannyフィルタ適用
    void* canny = fcvMemAlloc(w*h, 16);
    // Cannyフィルタの閾値(第5, 6引数)は適当な値
    fcvFilterCanny3x3u8((uint8_t*)gray,w,h,(uint8_t*)canny, 10, 30);

    // 色反転
    void* bitwise_not = fcvMemAlloc( w*h, 16);
    fcvBitwiseNotu8((uint8_t*)canny, w, h, 0, (uint8_t*)bitwise_not, 0);

    // グレースケール -> 3ch + アルファch
    void* dst_rgba8888 = fcvMemAlloc( w*h*4, 16);
    colorGrayToRGBA8888((uint8_t*)bitwise_not, w, h, (uint8_t*)dst_rgba8888);

    /**
     * copy to destination jbyte object
     */
    jbyteArray dst = env->NewByteArray(w*h*4);
    if (dst == NULL){
        DPRINTF("dst is NULL");
        // release
        fcvMemFree(dst_rgba8888);
        fcvMemFree(bitwise_not);
        fcvMemFree(canny);
        fcvMemFree(gray);
        fcvMemFree(rgb888);
        env->ReleaseByteArrayElements(img, jimgData, 0);
        return NULL;
    }
    env->SetByteArrayRegion(dst,0,w*h*4,(jbyte*)dst_rgba8888);
    DPRINTF("copy");

    // release
    fcvMemFree(dst_rgba8888);
    fcvMemFree(bitwise_not);
    fcvMemFree(canny);
    fcvMemFree(gray);
    fcvMemFree(rgb888);
    env->ReleaseByteArrayElements(img,jimgData,0);

    DPRINTF("processImage end");
    return dst;
}

// ...

```



## おわりに
FastCVがらみのデータの持ち方や関数の使い方など、慣れるのに時間は掛かりそうです…。
今回は簡単に、FastCVの既存フィルタを掛けてみるものでしたが、工夫をしたら絵画調などもできそうですね。
また、動画撮影中やモニタリング時に画像処理・認識処理ができないか試してみたいところです。

THETAプラグイン ＋ Computer Visionに興味がある方も、ぜひやってみてください！


### RICOH THETAプラグインパートナープログラムについて

RICOH THETAプラグインをご存じない方は[こちら](https://api.ricoh/products/theta-plugin/)をご覧ください。
[RICOH THETAプラグイン開発者コミュニティ](https://qiita.com/organizations/theta-plugin)の以下の記事もぜひご覧ください。

* [THETAプラグインパートナープログラムはじめました](https://qiita.com/mrtakada/items/8f3bbf8d283c26899473)
* [【THETAプラグイン開発】プラグインパートナープログラムに登録してみよう](https://qiita.com/mShiiina/items/55d98f366e650ca42251)
* [【THETAプラグイン開発】THETAを開発者モードにする手順](https://qiita.com/ueue/items/7c4027db3af463956e26)

興味を持たれた方は[twitter](https://twitter.com/thetaplugin)のフォローと[THETAプラグイン開発コミュニティ(slack)](https://join.slack.com/t/thetadeveloper/shared_invite/enQtNzcxODI2MTk2ODg0LWY5YmU2ZTIxNGYwNzlmNWZjMWMyMWFlMzY3ZmQ3NGU3ZjZiZjI0Njk4MGIxMDU5NDJiMzQ4Y2FkNDEzYmFlNGU)への参加もよろしくおねがいします｡


[^1]: [Android Developers | Camera API]( https://developer.android.com/guide/topics/media/camera)
[^2]: [RICOH Developer Connection | Docs/THETA Plugin > API Reference > Camera API](https://api.ricoh/docs/theta-plugin-reference/camera-api/)
[^3]: [Forums - Read image(JPG, ...) using FastCV](https://developer.qualcomm.com/forum/qdn-forums/software/fastcv-computer-vision-sdk/32906)

[prev-article]:https://qiita.com/roohii_3/items/5fbb3cb5645c9300ce69