LoginSignup
7

More than 1 year has passed since last update.

Organization

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

この記事はRICOH THETA Advent Calendar 2020 の18日目の記事です。

はじめに

リコーのYuuki_Sです。
弊社ではRICOH THETAという全周囲360度撮れるカメラを出しています。
RICOH THETA VやTHETA Z1は、OSにAndroidを採用しており、Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます。(詳細は本記事の末尾を参照)。


THETAに並んで有名な全天球カメラとしてInsta360のInsta360 ONE Xシリーズがあります。
先日、その新型Insta360 ONE X2が発売され、公式ページを眺めていました。
そこにはソフトウェアのアップデートにより充実した撮影機能の紹介が並んでいた訳ですが、その中で1つ面白い機能を見つけました。
image.png
insta360 onex2ページより引用

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

コンポ 3.gif
どうでしょう、見事に人が消え、ゴーストタウンっぽくなりました。

それでは、この作り方を紹介していきます。
作り方ではなく結果画像が見たい方は最後に作例を貼ってありますので、どうぞ。

openCVの導入

画像処理を効率的におこなうなら、openCVを導入するのが手っ取り早いです。
THETAプラグインでもopenCVは利用でき、過去の記事でも導入方法を取り上げています。
THETAの中でOpenCVを動かす
上記の記事では、openCVをC++で記述するNDKでの扱い方を紹介しています。
今回はこれとは別の方法、ライブラリを静的リンクしてJavaでopenCVを扱う方法を紹介します。

大きく分けて手順は3つ、ざっと5分ぐらいで導入できました。簡単です。
(1) OpenCV Android Pack の取得
(2) プロジェクトに OpenCV をインポートする
(3) ソースコード編集(OpenCV のinterfaceをimplementsする)

(1)OpenCV Android Pack の取得

OpenCVのReleasesページから、OpenCVバージョン3の最新版"Android pack"をダウンロードします。
image.png
なお、バージョン4は求められるAndroid APIレベルがTHETAと合っていないため、動作しない機能も多い可能性があります。今回は、3.4.12を利用しました。

ダウンロードしたZipファイルを展開し、フォルダを任意の場所に配置します。
後ほどAndroid Studioでこのフォルダにリンクを行うため、分かりやすい場所が良いと思います。
自分はCドライブ直下にopenCVというフォルダを作成し、その中に置きました。
image.png

(2)プロジェクトに OpenCV をインポートする

Android StudioでOpenCV をインポートしたいプロジェクトを開いたら、メニューから「File」→「New」→「IImport Module」を選択し、「Source directory」に"C:/(OpenCV を置いた場所)/sdk/java"と入力します。
image.png
追加した後、プロジェクトファイル一式の整合性がチェックされます。
「openCV3412」の AndroidManifest.xml の以下行に問題があると通知されるので、この行は削除します。(7行目です、下図は既に削除した図になります)
image.png
加えて、build.gradle(Module: openCVLibrary3412)の、「compileSdkVersion」と「targetSdkVersion」を「25」に修正してください。

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

次に、このファイルを有効にする設定をします。
Android Studio のメニューから、「File」→「Project Structure」を開き、左から
「Dependencies」タブ、「app」タブを順に選択したあと、「Declared Dependencies」という文字の下にある「+」をクリックします。
選択肢が表示されるので「Module Dependency」をクリックします。
image.png

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

(3) ソースコード編集(OpenCV のinterfaceをimplementsする)

最後に、プラグイン内でOpenCVをロードする記述をします。
プラグイン起動時に読み込むよう、onResumeに以下を追加します。

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プラグインでライブプリビューを扱いやすくする

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

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

そのエフェクトの実装ですが、実はopenCVの関数1つで出来てしまいます。
Imgproc.accumulateWeighted
この関数は、連続画像の移動平均を取得することができ、背景差分による動体検出なんかで背景画像の抽出に利用されます。
(当初この関数の存在をすっかり忘れて1から実装しようとしていました。気付いて良かった。)

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

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

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

加えてWeb表示側のJavaScriptも変更します。初期状態だと2(960x480)が選択されています。
image.png

previw.js
var PREVIEW_FORMAT = 5;

これで、大丈夫です。

動作確認&作例

屋外でTHETAをセッティングしてスマホと接続。
image.png
スマホのブラウザから”192.168.1.1:8888”にアクセスすると以下のようにプレビューが表示されます。
プレビュー.png
あとは、エフェクトをONにすると背景のみの写真が取得できます。ぼんやりと浮かび上がってくる様子が結構お気に入り。
コンポ 1.gif
画像の保存はブラウザ上からおこないます。良いと思ったタイミングで長押しして(iPhoneなら)”写真”に追加を選択するだけです。
image.png

動作確認がうまく行ったので、もう少し人の多い場所で撮った作例を載せておきます。
image.png
image.png
動いている人は綺麗に非表示されています。ただ仕組み上、長時間立ち止まってる人は映り込みます。

他の場所での撮影結果も載せておきます。
equirectangularでは味気ないので、THETA+アプリで編集しました。
20201215_035406528_iOS.jpg
20201216_040744476_iOS.jpg
20201216_044348111_iOS.jpg
横浜、みなとみらいのこの辺りは人が居ないことが無い場所で、通常ではこんな写真は撮れませんが今回のプラグインを利用することで手軽にその場でこんな写真が撮影できました。

おわりに

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

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

THETAプラグインをご存じない方はこちらをご覧ください。
パートナープログラムへの登録方法はこちらにもまとめてあります。
QiitaのRICOH THETAプラグイン開発者コミュニティ TOPページ「About」に便利な記事リンク集もあります。
興味を持たれた方はTwitterのフォローとTHETAプラグイン開発コミュニティ(Slack)への参加もよろしくおねがいします。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
7