画像処理を含むTHETAプラグインの実装方法【THETAプラグイン開発】

こんにちは、リコーの @3215 です。

先日[RICOH THETA プラグインストア](https://pluginstore.theta360.com/)がオープンし、開発したTHETAプラグインをこのストアから発信できるようになりました。更に、いくつかのプラグインは[ソースコードも公開](https://qiita.com/mrtakada/items/9cbcad0059ab0a03a186#%E9%96%8B%E7%99%BA%E8%80%85%E3%81%AE%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E5%88%A9%E7%94%A8%E6%96%B9%E6%B3%95)されています。

今回はその中の[Automatic Face Blur BETA](https://pluginstore.theta360.com/plugins/com.theta360.automaticfaceblur/) (以降、自動顔ぼかしβ版)の[ソースコード](https://github.com/ricohapi/theta-automatic-face-blur-plugin)から、撮影して何かしらの画像処理をかけて保存する、というプラグインを開発する場合に必要な作法を読み解いてみます。

自動顔ぼかしβ版の顔認識精度を改善したい・ぼかし方を変えたいという場合はもちろんですが、顔ぼかしに限らず画像処理を含むプラグインを作成したいという場合は、このプラグインをベースにするのが作りやすいと思います。

THETAプラグインをご存じない方は[こちら](https://api.ricoh/products/theta-plugin/)をご覧ください。
プラグインを開発してみたいという方はまずは以下の記事をご覧ください。

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

THETAプラグインSDKの使い方は以下の記事を参考にしてください。

- [【THETAプラグイン開発】THETAでRICOH THETA Plug-in SDKを動かす方法](https://qiita.com/yokazuya/items/cda20420ee0b1b58e03f)
- [【THETAプラグイン開発】THETAプラグインでHello World](https://qiita.com/meronpan/items/fde7d66bed7f31f9e193)

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

## プラグインの構成

まずはプラグインがどのような構成になっているか確認します。
自動顔ぼかしβ版のプロジェクトをAndroid Studioにインポートしてみると、プロジェクトは[`app`](https://github.com/ricohapi/theta-automatic-face-blur-plugin/tree/v1.0.13/app)と[`pluginlibrary`](https://github.com/ricohapi/theta-automatic-face-blur-plugin/tree/v1.0.13/pluginlibrary)の2つで構成されています。

プラグインとしての処理は`app`で実装しています。
`pluginlibrary`は[THETAプラグインのSDK](https://github.com/ricohapi/theta-plugin-sdk)で、基本的には開発者が手を入れる必要はないものです。

![project_contents.png](https://qiita-image-store.s3.amazonaws.com/0/265699/d6cf79bc-6587-2b3b-120e-ba9a194d4f96.png)

以下、`app`の方を見ていきます。

一般的なAndroidアプリと同じく、[`MainActivity`](https://github.com/ricohapi/theta-automatic-face-blur-plugin/blob/v1.0.13/app/src/main/java/com/theta360/automaticfaceblur/MainActivity.java)が入り口です。
ただし、`AppCompatActivity`ではなくTHETAプラグインのSDKで提供されている`PluginActivity`を継承しています。

`MainActivity`ではプラグインのライフサイクルやTHETAのボタン操作に対応する処理の呼び出しをして、実際の処理はサブパッケージのクラスで行っています。
撮影から保存までの部分に関してはこの`MainActivity`と、`task`パッケージの[`TakePictureTask`](https://github.com/ricohapi/theta-automatic-face-blur-plugin/blob/v1.0.13/app/src/main/java/com/theta360/automaticfaceblur/task/TakePictureTask.java)、[`ImageProcessorTask`](https://github.com/ricohapi/theta-automatic-face-blur-plugin/blob/v1.0.13/app/src/main/java/com/theta360/automaticfaceblur/task/ImageProcessorTask.java)あたりを見ればほぼ把握できそうです。

その他の大半はWebUIに関係するソースコードですが、今回は解説の対象外とします。
WebUIとはブラウザからアクセスしてプラグインの設定などができる機能で、自動顔ぼかしβ版では以下のようにプレビュー、撮影、撮影モード設定などができます。

![web_ui.png](https://qiita-image-store.s3.amazonaws.com/0/265699/73aa4be5-b442-ced8-6b7e-97cf349bd0e2.png)


## 撮影の実行

単に撮影を実行するだけであれば、既に[SDKの`TakePictureTask`](https://github.com/ricohapi/theta-plugin-sdk/blob/v1.0.0/app/src/main/java/com/theta360/pluginapplication/task/TakePictureTask.java)で最低限の実装がされています。
[`MainActivity`の`onKeyDown()`](https://github.com/ricohapi/theta-automatic-face-blur-plugin/blob/v1.0.13/app/src/main/java/com/theta360/automaticfaceblur/MainActivity.java#L82)で、シャッターボタンが押された時の処理が書かれています。

```java
public void onKeyDown(int keyCode, KeyEvent keyEvent) {
    if (keyCode == KeyReceiver.KEYCODE_CAMERA) {
        if (mTakePictureTask == null && mImageProcessorTask == null) { // 撮影中や画像処理中でないことを確認
            if (mUpdatePreviewTask != null) {
                mUpdatePreviewTask.cancel(false);   // WebUIのプレビュー更新をcancel
            }
            mTakePictureTask = new TakePictureTask(mTakePictureTaskCallback, null,
                    null);
            mTakePictureTask.execute(); // 撮影タスクを実行
        }
    }
}
```

自動顔ぼかしβ版では`TakePictureTask`のコンストラクタに引数が追加されていますが、これはWebUIからの撮影もサポートするためです。
ここではシャッターボタン押下の場合なので追加の引数はnullとし、コールバックには`MainActivity`内に定義している`mTakePictureTaskCallback`を指定しています。


## 撮影した画像ファイルへのアクセス

`TakePictureTask`のコールバックで呼ばれる[`mTakePictureTaskCallback`の`onPictureGenerated()`](https://github.com/ricohapi/theta-automatic-face-blur-plugin/blob/v1.0.13/app/src/main/java/com/theta360/automaticfaceblur/MainActivity.java#L150)で、撮影後に画像処理を行う`ImageProcesssorTask`を呼び出しています。

```java
public void onPictureGenerated(String fileUrl) {
    if (!TextUtils.isEmpty(fileUrl)) {
        notificationAudioOpen();
        notificationLedBlink(LedTarget.LED4, LedColor.BLUE, 1000);
        mImageProcessorTask = new ImageProcessorTask(mImageProcessorTaskCallback);
        mImageProcessorTask.execute(fileUrl);   // 画像処理タスクを実行
    } else {
        notificationError(getResources().getString(R.string.take_picture_error));
    }
    mTakePictureTask = null;
}
```

この引数の`fileUrl`には、[WebAPIの撮影コマンド](https://developers.theta360.com/ja/docs/v2.1/api_reference/commands/camera.take_picture.html)を叩いた時のresponseに入っているURLが入っています。
このURLはHTTPアクセス用のもの(http\://...)になっているので、[`ImageProcessorTask`の`doInBackground()`](https://github.com/ricohapi/theta-automatic-face-blur-plugin/blob/v1.0.13/app/src/main/java/com/theta360/automaticfaceblur/task/ImageProcessorTask.java#L83)の中でローカルファイルのパスに変換しています。

```java
protected Map<String, String> doInBackground(String... params) {
    Matcher matcher = Pattern.compile("/\\d{3}RICOH.*").matcher(params[0]);
    if (matcher.find()) {
        String fileUrl = DCIM + matcher.group();
        .....
```

一般的なデジタルカメラの画像ファイルは「DCIM」という名前のディレクトリの下の、数字3桁＋アルファベット5文字のサブディレクトリの下に保存されます。
DCIMへのパスは[Androidの`Environment`クラス](https://developer.android.com/reference/android/os/Environment#getExternalStorageDirectory())を使って取得できます。自動顔ぼかしβ版では[`MainActivity`の冒頭](https://github.com/ricohapi/theta-automatic-face-blur-plugin/blob/v1.0.13/app/src/main/java/com/theta360/automaticfaceblur/MainActivity.java#L55)で定義しています。

```java
public static final String DCIM = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getPath();
```

デバッグしてみたところ、具体的には以下のように文字列が変換されていました。
- 変換前：`http://127.0.0.1:8080/files/150100525831424d42075b6ace1cc300/100RICOH/R0010063.JPG`
- 変換後：`/storage/emulated/0/DCIM/100RICOH/R0010063.JPG`


## 顔ぼかし処理

ファイルパスを取得した後は、[`ImageProcessorTask`の`blurInputFile()`以下](https://github.com/ricohapi/theta-automatic-face-blur-plugin/blob/v1.0.13/app/src/main/java/com/theta360/automaticfaceblur/task/ImageProcessorTask.java#L141)のシーケンスで顔ぼかし処理を行っています。

```java
private Bitmap blurInputFile(@NonNull String fileUrl) throws IOException {
    long start = System.currentTimeMillis();
    inputFile(fileUrl);
    long now = System.currentTimeMillis();
    Timber.d("inputFile : %d", now - start);
    blurFaces();
    now = System.currentTimeMillis();
    Timber.d("blurFaceInEqui : %d", now - start);
    blurFacesOnSides();
    now = System.currentTimeMillis();
    Timber.d("blurFaceEquiTwoEdges : %d", now - start);
    return mBitmapToBlur;
}
```

ここでのポイントは、ファイルがEquirectangular形式の全天球画像なので、左右の両端部分はつなぎ合わせて処理を行う必要があるということです。[`blurFaceOnSides()`](https://github.com/ricohapi/theta-automatic-face-blur-plugin/blob/v1.0.13/app/src/main/java/com/theta360/automaticfaceblur/task/ImageProcessorTask.java#L216)が画像両端部分に対する処理です。

顔認識は[`blurFaces()`](https://github.com/ricohapi/theta-automatic-face-blur-plugin/blob/v1.0.13/app/src/main/java/com/theta360/automaticfaceblur/task/ImageProcessorTask.java#L183)及び[`calculateCoordinateOfEyes()`](https://github.com/ricohapi/theta-automatic-face-blur-plugin/blob/v1.0.13/app/src/main/java/com/theta360/automaticfaceblur/task/ImageProcessorTask.java#L279)で行っていて、顔認識ライブラリにはAndroid標準のFaceDetectorを使っています。
また、顔認識できた領域に対するぼかし処理は[`blur()`](https://github.com/ricohapi/theta-automatic-face-blur-plugin/blob/v1.0.13/app/src/main/java/com/theta360/automaticfaceblur/task/ImageProcessorTask.java#L348)で行っています。

これらのメソッドを修正することで、顔認識の精度を改善したり、ぼかし方を変えることができます。
このプラグインは最も簡易的な実装になっていますので、これをベースにライブラリを差し替えたりロジックを工夫したりして、自分好みの顔ぼかしにチューニングしてみてください。


## Exifのコピー

このままでは顔ぼかし後の画像ファイルの[Exif](https://ja.wikipedia.org/wiki/Exchangeable_image_file_format)が空になってしまうので、元画像のExifをコピーしてやる必要があります。
`ImageProcessorTask`の[`doInBackground()`](https://github.com/ricohapi/theta-automatic-face-blur-plugin/blob/v1.0.13/app/src/main/java/com/theta360/automaticfaceblur/task/ImageProcessorTask.java#L103)で[`Exif.copyMetadata()`](https://github.com/ricohapi/theta-automatic-face-blur-plugin/blob/v1.0.13/app/src/main/java/com/theta360/automaticfaceblur/exif/Exif.java#L37)を使って、元画像のExifを顔ぼかし後の画像にコピーしています。

```java
File blurredFile = new File(blurredFileUrl);
File file = new File(fileUrl);
if (Exif.copyMetadata(fileUrl, blurredFileUrl)) {
.....
```


## データベースの更新

[API ReferenceのUpdating the Databaseの説明](https://api.ricoh/docs/theta-plugin-reference/broadcast-intent/#updating-the-database)にあるように、新しく画像ファイルを作成したらTHETA内で画像を管理しているデータベースに反映させる必要があります。
`MainActivity`の`ImageProcessorTask`のコールバックで`notificationDatabaseUpdate()`を呼んでいます。

```java
Matcher blurredMatcher = Pattern.compile("/DCIM.*")
        .matcher(fileUrlMap.get(ImageProcessorTask.BLURRED_FILE_KEY));
if (blurredMatcher.find()) {
    String formattedFileUrl = blurredMatcher.group();
    Timber.d(formattedFileUrl);
    Timber.d(fileUrl);
    String[] fileUrls = new String[]{formattedFileUrl, fileUrl};
    notificationDatabaseUpdate(fileUrls);   // データベースを更新
}
```

これで顔ぼかし処理をした画像がTHETAの画像一覧に登録されました。


## まとめ

自動顔ぼかしβ版のソースコードから、画像処理を含むTHETAプラグインの実装方法を読み解いてみました。
ぜひ公開されているソースコードを参考にして、自分なりのプラグインを作ってみてください。


THETAプラグイン開発に興味を持たれた方はぜひパートナープログラムにご登録ください！
なお、登録時に申請したシリアルナンバーのTHETAについてはメーカーサポート対象外になりますので、ご注意ください。

* パートナープログラムの詳細は[こちら](https://api.ricoh/products/theta-plugin/)
* 登録フォームは[こちら](https://www8.webcas.net/db/pub/ricoh/thetaplugin/create/input)