ゴーストタウンエフェクトでみなとみらいを撮る(THETAプラグイン開発例)

この記事は[RICOH THETA Advent Calendar 2020](https://qiita.com/advent-calendar/2020/theta) の18日目の記事です。
#はじめに
リコーの[Yuuki_S](https://qiita.com/Yuuki_S)です。
弊社では[RICOH THETA](https://theta360.com/ja/)という全周囲360度撮れるカメラを出しています。
RICOH THETA VやTHETA Z1は、OSにAndroidを採用しており、Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます。(詳細は本記事の末尾を参照）。
***

THETAに並んで有名な全天球カメラとしてInsta360のInsta360 ONE Xシリーズがあります。
先日、その新型Insta360 ONE X2が発売され、公式ページを眺めていました。
そこにはソフトウェアのアップデートにより充実した撮影機能の紹介が並んでいた訳ですが、その中で1つ面白い機能を見つけました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/12441091-5ac8-35a1-b642-3ea87f2fbac6.png)
※[insta360 onex2](https://www.insta360.com/jp/product/insta360-onex2)ページより引用

ゴーストタウンエフェクト、長時間露光と似ていて非なる表現、面白いですね。
自分もこんな写真撮ってみたいな、と思い、同様の機能をTHETAにプラグインで追加してみました。

![コンポ 3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/cf970199-6b77-d7fd-8d56-bbc6b6c9f3b4.gif)
どうでしょう、見事に人が消え、ゴーストタウンっぽくなりました。

それでは、この作り方を紹介していきます。
作り方ではなく結果画像が見たい方は最後に作例を貼ってありますので、どうぞ。
#openCVの導入
画像処理を効率的におこなうなら、openCVを導入するのが手っ取り早いです。
THETAプラグインでもopenCVは利用でき、過去の記事でも導入方法を取り上げています。
[THETAの中でOpenCVを動かす](https://qiita.com/roohii_3/items/63a546169981f41967df)
上記の記事では、openCVをC++で記述するNDKでの扱い方を紹介しています。
今回はこれとは別の方法、ライブラリを静的リンクしてJavaでopenCVを扱う方法を紹介します。

大きく分けて手順は3つ、ざっと5分ぐらいで導入できました。簡単です。
(1) OpenCV Android Pack の取得
(2) プロジェクトに OpenCV をインポートする
(3) ソースコード編集（OpenCV のinterfaceをimplementsする）
##(1)OpenCV Android Pack の取得
OpenCVの[Releasesページ](https://opencv.org/releases/)から、OpenCVバージョン3の最新版"Android pack"をダウンロードします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/6b09db4c-c89b-8a06-c028-c224c9924756.png)
なお、バージョン4は求められるAndroid APIレベルがTHETAと合っていないため、動作しない機能も多い可能性があります。今回は、3.4.12を利用しました。

ダウンロードしたZipファイルを展開し、フォルダを任意の場所に配置します。
後ほどAndroid Studioでこのフォルダにリンクを行うため、分かりやすい場所が良いと思います。
自分はCドライブ直下にopenCVというフォルダを作成し、その中に置きました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/94ad9323-4efb-2e09-edfb-0c60b4ea732f.png)
##(2)プロジェクトに OpenCV をインポートする
Android StudioでOpenCV をインポートしたいプロジェクトを開いたら、メニューから「File」→「New」→「IImport Module」を選択し、「Source directory」に"C:/(OpenCV を置いた場所)/sdk/java"と入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/e0006142-2684-f308-42b0-9b310b2149a3.png)
追加した後、プロジェクトファイル一式の整合性がチェックされます。
「openCV3412」の AndroidManifest.xml の以下行に問題があると通知されるので、この行は削除します。(7行目です、下図は既に削除した図になります)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/f0f6be18-f42b-8789-ad91-fd9784297b7d.png)
加えて、build.gradle(Module: openCVLibrary3412)の、「compileSdkVersion」と「targetSdkVersion」を「25」に修正してください。

```java:build.gradle(Module：openCVLibrary3412)
android {
   compileSdkVersion 25
   ～省略～
   defaultConfig {
   ～省略～
   targetSdkVersion 25
   }
   ～省略～
  }
```
openCVバージョン3の一部の関数が利用しているAndroid Camera2 APIには、THETAがサポートしていないモノがあります。
この作業は、その不整合を無視してビルドを通す方法になります。
基礎的な画像処理関数には影響がなく、不整合解消のためライブラリ側に手を加えるよりもシンプルなので今回はこのようにしました。

続いて、プロジェクト内所定の位置に「jniLibs」というフォルダを作成し、opnenCVのライブラリから、「arm64-v8a」配下のファイル（拡張子が so のライブラリ）をコピーし配置します。

コピー元: C:/(OpenCV を置いた場所)/sdk/native/libs/arm64-v8a
コピー先: C:/(プロジェクトファイルがある場所)/app/src/main/jniLibs/arm64-v8a
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/7ce6032d-ba80-0492-be78-25cdb4205dbe.png)

次に、このファイルを有効にする設定をします。
Android Studio のメニューから、「File」→「Project Structure」を開き、左から
「Dependencies」タブ、「app」タブを順に選択したあと、「Declared Dependencies」という文字の下にある「＋」をクリックします。
選択肢が表示されるので「Module Dependency」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/90684000-6765-8d0d-5d84-38233056dfb6.png)

表示されたダイアログで「openCVLibrary3412」をチェックしてOKを押します。
以上でプロジェクトへの OpenCV インポート作業は完了です。

##(3) ソースコード編集（OpenCV のinterfaceをimplementsする）
最後に、プラグイン内でOpenCVをロードする記述をします。
プラグイン起動時に読み込むよう、onResumeに以下を追加します。

```java:MainActivity.java
protected void onResume() {
 super.onResume();
 if (!OpenCVLoader.initDebug()) {
   Log.d(TAG, "Internal OpenCV library not found.");
 } else {
   Log.d(TAG, "OpenCV library found inside package.");
 }
 ～省略～
}
```
以上でopenCVをJava側から扱えるようになりました。
#ゴーストタウンエフェクトを実装する
今回は外で撮影時にその結果をリアルタイムで確認したいと思い、スマホからプレビューが可能な以下の記事のプロジェクトをベースにしました。
[THETAプラグインでライブプリビューを扱いやすくする](https://qiita.com/KA-2/items/cef05a4960663bd2ba2f)

このライブプレビューで取得した画像に対して処理を掛ける、という手法を取っています。
その場で確認がしやすく、実装が手軽という反面、プレビューを利用して画像処理するため解像度がフルHD程度(1920x960)というデメリットがあります。
今回はアドベントカレンダーに間に合わせるため、手軽さ優先でこの方法を取りましたが、本当はタイムラプス撮影をおこない処理する方が正統派な気がします。

とは言え、タイムラプスを利用してもエフェクト部分の画像処理は今回と同じになると思います。

そのエフェクトの実装ですが、実はopenCVの関数１つで出来てしまいます。
[Imgproc.accumulateWeighted](https://docs.opencv.org/3.4/javadoc/org/opencv/imgproc/Imgproc.html#accumulateWeighted(org.opencv.core.Mat,org.opencv.core.Mat,double,org.opencv.core.Mat))
この関数は、連続画像の移動平均を取得することができ、背景差分による動体検出なんかで背景画像の抽出に利用されます。
(当初この関数の存在をすっかり忘れて１から実装しようとしていました。気付いて良かった。)

これを利用したコードが以下になります。実際のエフェクト処理は上述の1行のみで、その前後は画像形式の変換だけです。

```java:MainActivity.java
    public void drawGTEffectThread() {;
        new Thread(new Runnable() {
            @Override
            public void run() {
                android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
                Mat averageImage = new Mat(960, 1920, CvType.CV_32FC(4));//背景画像
                Bitmap back_frame = Bitmap.createBitmap(1920,960, Bitmap.Config.ARGB_8888);
                while (mFinished == false)//ライブプレビュー状態
                {
                    byte[] jpegFrame = latestLvFrame;
                    if ( jpegFrame != null )
                    {
                        if(toggle_GTEffect == true)
                        {
                            Bitmap frame = BitmapFactory.decodeByteArray(jpegFrame, 0, jpegFrame.length);//プレビュー画像のBitmap化
                            matFrame =  new Mat(back_frame.getHeight(), back_frame.getWidth(), CvType.CV_8UC3);
                            matFrameBG = new Mat(back_frame.getHeight(), back_frame.getWidth(), CvType.CV_8UC3);

                            Utils.bitmapToMat(frame,matFrame);//プレビュー画像のMat化
                            Mat matFrame_f = new Mat(960, 1920, CvType.CV_32FC(3));//floatのMatを準備
                            matFrame.convertTo(matFrame_f,CV_32FC4, 1/255.0);//プレビュー画像をfloatに変換

                            Imgproc.accumulateWeighted(matFrame_f,averageImage,0.03);

                            averageImage.convertTo(matFrameBG,CvType.CV_8UC3,255);//背景画像をfloatからMatに変換
                            Utils.matToBitmap(matFrameBG,back_frame);//背景画像をMatからBitmapに変換
                            ByteArrayOutputStream baos = new ByteArrayOutputStream();
                            back_frame.compress(Bitmap.CompressFormat.JPEG, 100, baos);//プレビュー表示用にJpeg変換

                            latestFrame_Result = baos.toByteArray();

                            Map<TextArea, String> output = new HashMap<>();//Z1のOLEDにステータス表示
                            output.put(TextArea.MIDDLE, "");
                            output.put(TextArea.BOTTOM, "Processing...");
                            notificationOledTextShow(output);
                        }else{
                                Map<TextArea, String> output = new HashMap<>() ;
                                output.put(TextArea.MIDDLE, "");
                                output.put(TextArea.BOTTOM, "Stopping...");
                                notificationOledTextShow(output);
                                latestFrame_Result = latestLvFrame;
                            }
                    }else{
                        try {
                            Thread.sleep(125);
                        } catch (InterruptedException e) {e.printStackTrace();}
                    }}}}).start();
    }
```

あとは、このスレッドをonResumeで呼び、ボタンからtoggle_GTEffectのfalseとtrueを切り替えるようにすれば完成です。

1点注意としては、この実装ではライブプレビューの入力解像度を決め打ちしているため、異なる解像度が入るとエラーが発生します。
なので、以下の通りプレビューのフォーマットを指定する必要があります。

```java:MainActivity.java
 protected void onResume() {
       ～省略～
        //Start LivePreview
        previewFormatNo = GetLiveViewTask.FORMAT_NO_1920_8FPS;
　　　　～省略～
    }
```

加えてWeb表示側のJavaScriptも変更します。初期状態だと2(960x480)が選択されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/e651ad4d-180e-727c-7691-76510df4eafa.png)

```javascript:previw.js
var PREVIEW_FORMAT = 5;
```

これで、大丈夫です。

#動作確認&作例
屋外でTHETAをセッティングしてスマホと接続。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/b233ccd3-f997-dae4-a71b-22c00439e408.png)
スマホのブラウザから”192.168.1.1:8888”にアクセスすると以下のようにプレビューが表示されます。
![プレビュー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/d324ce6b-3540-53ba-4301-13df3d3070c1.png)
あとは、エフェクトをONにすると背景のみの写真が取得できます。ぼんやりと浮かび上がってくる様子が結構お気に入り。
![コンポ 1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/d1130a90-611e-7ec5-bc64-b896b37f0d99.gif)
画像の保存はブラウザ上からおこないます。良いと思ったタイミングで長押しして(iPhoneなら)”写真”に追加を選択するだけです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/e32a9229-1f52-73dc-bf45-e15fcd37204b.png)

動作確認がうまく行ったので、もう少し人の多い場所で撮った作例を載せておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/e72e063d-e41a-f674-d2f5-ec72044dd8fe.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/6fc37229-5b87-1ec1-5089-3f811c5b1440.png)
動いている人は綺麗に非表示されています。ただ仕組み上、長時間立ち止まってる人は映り込みます。

他の場所での撮影結果も載せておきます。
equirectangularでは味気ないので、[THETA＋アプリ](https://theta360.com/ja/about/application/edit.html)で編集しました。
<img width="640" alt="20201215_035406528_iOS.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/941d821d-cf19-ffb2-cf76-c3de409d142b.jpeg">
<img width="640" alt="20201216_040744476_iOS.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/fed68e1b-b276-1b55-9ae9-c93aec810deb.jpeg">
<img width="640" alt="20201216_044348111_iOS.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152249/bf468ab2-0ad9-f688-9ac0-6c164ff41975.jpeg">
横浜、みなとみらいのこの辺りは人が居ないことが無い場所で、通常ではこんな写真は撮れませんが今回のプラグインを利用することで手軽にその場でこんな写真が撮影できました。

#おわりに
今回は、ゴーストタウンエフェクトを手軽にかけられるプラグインを作ってみました。
自分の思いついたエフェクト(画像処理)を動かして、その場で結果が見れるのは楽しいです。
openCVもかなり手軽に導入できるので、ぜひ好みのエフェクトをかけれるプラグインを作ってみてください。


#RICOH THETAプラグインパートナープログラムについて 
THETAプラグインをご存じない方は[こちら](https://api.ricoh/products/theta-plugin/)をご覧ください。
パートナープログラムへの登録方法は[こちら](https://qiita.com/mShiiina/items/55d98f366e650ca42251)にもまとめてあります。
Qiitaの[RICOH THETAプラグイン開発者コミュニティ TOPページ「About」](https://qiita.com/organizations/theta-plugin)に便利な記事リンク集もあります。
興味を持たれた方は[Twitter](https://twitter.com/thetaplugin)のフォローと[THETAプラグイン開発コミュニティ(Slack)](https://join.slack.com/t/thetadeveloper/shared_invite/enQtNzcxODI2MTk2ODg0LWY5YmU2ZTIxNGYwNzlmNWZjMWMyMWFlMzY3ZmQ3NGU3ZjZiZjI0Njk4MGIxMDU5NDJiMzQ4Y2FkNDEzYmFlNGU)への参加もよろしくおねがいします｡