THETAの姿勢を利用し、スマホなし設定を可能にする

この記事は [RICOH THETA Advent Calendar 2020](https://qiita.com/advent-calendar/2020/theta) の初日記事です。
このアドベントカレンダーはどなたでも参加できます（コミュニティに入る必要はありません）
まだまだ隙間が開いています、THETAプラグインに限らずRICOH THETAにまつわる色々なことで参加して頂けると嬉しいです！

## はじめに

リコーの @KA-2 です。

弊社では[RICOH THETA](https://theta360.com/ja/)という全周囲360度撮れるカメラを出しています。
<B>RICOH THETA V</B>や<b>RICOH THETA Z1</b>は、OSに<B>Android</B>を採用しています。Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます（詳細は本記事の末尾を参照）。

今回は、カメラ本体の姿勢を利用してRICOH THETA Z1の操作系を拡張します。RICOH THETA Vでもある程度は類似のことができると思いますが、OLEDがついているほうがめいっぱい操作系を拡張できるので対象機種を絞りました。RICOH THETA Vへの応用やRICOH THETA Z1用でも「自分ならこうする」という異なったバリエーション作成にトライして頂けたら幸いです。

つくったものを動作させている様子はこんな感じです。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">◤Qiita記事公開◢<br>RICOH THETAアドベントカレンダー2020初日記事公開！<br>カメラ本体を「傾ける」「回す」「捻る」とボタン操作を組み合わせ、スマートフォンなしで殆どの撮影設定ができるプラグインを作ったよ！<a href="https://t.co/1Q7ZKAZd4y">https://t.co/1Q7ZKAZd4y</a><a href="https://twitter.com/hashtag/THETA?src=hash&amp;ref_src=twsrc%5Etfw">#THETA</a> <a href="https://twitter.com/hashtag/thetaplugin?src=hash&amp;ref_src=twsrc%5Etfw">#thetaplugin</a> <a href="https://twitter.com/hashtag/ThetaZ1?src=hash&amp;ref_src=twsrc%5Etfw">#ThetaZ1</a> <a href="https://t.co/UspTQc9lk5">pic.twitter.com/UspTQc9lk5</a></p>&mdash; THETAプラグイン開発者コミュニティ (@thetaplugin) <a href="https://twitter.com/thetaplugin/status/1333599444239990789?ref_src=twsrc%5Etfw">December 1, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

今回紹介するTHETAプラグインは、アドベントカレンダー2020の期間中にストア公開を目指しています。
便利なのでつかってほしい！~~公開になったら改めて記事更新をします。~~
<2020/12/23 更新>
<b>[「Change via Tilt」](https://pluginstore.theta360.com/plugins/skunkworks.tiltui/)という名称で RICOH THETA Plug-in STOREに公開になりました！</b>


## 作ったものの仕様

本プラグインでできることは以下のとおりです。
「スマホいらず」を目指したのでトコトン設定できます。

- 撮影モード(image/video)の切り替え
- 露出モード(Auto/Av/Tv/Iso/Manual)の切り替え
- 各露出プログラムにおける撮影設定<br>(絞り、シャッター速度、ISO感度、ホワイトバランス、静止画autoモードにおけるOption setting[NR/DR Comp/HDR/Hh HDR] )
- ホワイトバランス の指定方法（プリセット/色温度）の切り替え
- 静止画撮影時におけるファイル保存形式(JPEG/RAW+)の切り替え
- セルフタイマーのOn/Off切り替え
- 連続撮影（Off/Time Shift/Interval/Interval Composite）の切り替え

その他の細かな所として、以下の事項もケアしてあります。

- このプラグインで行った設定はプラグイン終了時に保存され、再びプラグインを起動したときに復帰します。「スーパーMYセッティング」的にも利用できます。
- 本プラグインはリモートコントローラー（ "Vol+"のキーコードを送出するHIDデバイス）に対応しています。


以降では、動画の解説になってしまいますが、このプラグインの振る舞いをひとつづつ説明していきます。基本的には、

- <b>ボタンを押さないとパラメータを変更しない</b><br>（「JPEG<->RAW+切り替え」や「ホワイトバランスの指定方式選択」はちょっと例外ですが、簡単には変わらなくしていたり、表示をみると一撃で確認できる配慮をしています）
- <b>頻繁に操作しない事項は「逆さま」にしないと変更できない</b>

ということに配慮して操作仕様を決めています。

"ワタシ仕様"なので、お好み操作系に合わない点もあるかもしれません。どうしても自分用に変更したい方は、ソースコードも公開していますので、ご自身でカスタマイズしてくださいませ。


### 傾け操作による変更可能項目の選択方法

THETAを直立させた姿勢を基準とし、「45度以上傾けたあと元の姿勢に戻す」という操作で、OLED右側 二重線枠 の中の表示項目が切り替わります。凝った仕掛けにしてない（姿勢判定にヒステリシスを持たせる等していない）ので45度付近でプルプルしてしまうと切り替わりますが実用上問題にならないレベルです。
「傾ける方向」によって、どの項目を表示するか選べます。露出プログラムによって、傾ける方向と表示項目の対応を以下のように変えています。OLEDに「傾けガイド」を表示してますので迷うことはないと思います。

![01.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/e8c76fe3-b99b-6dc4-02ef-61cac26e815a.jpeg)


### ボタン操作

逆さま以外の姿勢では、前述の「傾け操作で選んだ項目の値」を変更できます。ボタン長押しによる連続変更に対応したかったのでModeボタンは「露出プログラムの選択」に割り当てています（※Modeボタン長押しはプラグイン終了操作に固定されています）。

逆さま姿勢（直立を基準とすると 135度以上傾けていると逆さまとみなされます）のボタン操作は、プラグイン起動前の操作方法を踏襲しています。ModeとFnの振る舞いは同じです。これで迷わない。
WLANを使わないことがミソのプラグインなので、WLANボタンには「特殊撮影の切り替え」を割り当てました。

![02.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/3421a4f4-ad7b-46c2-dc2e-23c10b2d76d6.jpeg)


### ホワイトバランス指定方法の変更

前述の傾け操作で「ホワイトバランスを表示しているとき」だけ、ホワイトバランスの指定方法（プリセット／色温度）を切り替えられるようにしました。
頻繁に変更しないと思いますので「逆さま」を絡めて変わりにくくしてあります。（とはいえ、撮影前に目視確認することはお勧めしておきます）

![03_WB.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/a33afde3-a576-e876-5def-ecb716753b5a.jpeg)


### 静止画ファイル形式の変更

直立姿勢を基準として、右または左に、すばやく捻り操作をすると、ファイルフォーマットの選択をできるようにしました。「すばやく捻る」のあたりは、「200msecの間に 90度以上の回転」としてあります。
使わずに想像すると難しそうですが、やってみると簡単に変更できます。かといって簡単に変わりすぎない絶妙な仕上がりです。

![04_Format.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/420de792-730f-3021-09f1-7740ccd70f9d.jpeg)


### OLEDの表示

OLEDの表示を以下のようにしました。
キツキツです。もうこれ以上仕様を盛りたくないかんじです。
（RICOH THETA Z1の"個性"にはあっていると思います。普通のカメラっぽい表示です。）

![05.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/3a01d64e-af2b-3379-025f-43e648c09e76.jpeg)

「TimeShiftとセルフタイマーは独立してOn/Offできるのに、なんで表示を重ねているのだ？」という疑問を持った方は上級者ですね。
「どちらもOn設定となったときTimeShiftが優先される」という振る舞いでしたので、このような表示としています。

ちょっと凝っているのが以下の点です。

- タイムシフト撮影中とインターバル撮影中は、ダイアログに「in progress.」を表示します。
- インターバル合成中は、ダイアログに"経過時間"を表示します。
- 動画撮影中は、ダイアログに"記録時間"を表示します。
- 露出プログラムがマニュアルのとき、設定値に連動したOLEDの輝度調節をします。

ダイアログ表示は以下のとおり。

![00_daialog.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/43aa9a89-8b41-5330-c79b-40b52bef0e0f.gif)

最後のポイントは完全に私的仕様です。私は「暗いところでマニュアル撮影するときに、表示輝度が高いことがイヤ」なので、凝った作りにしました。
(絞り、シャッター速度、ISO感度のどれを操作しても、0.3段あたり1ステップ、OLEDの輝度を変化させています。カメラに多く光を取り込む設定であるほどOLEDを暗く、逆の場合はOLEDを明るく制御しています)

### 制約事項

- Option setting(_filter)の表示順番は、 RICOH THETA APIの応答により決定しています。
- インターバル撮影とインターバル合成の終了に時間がかかります。終了操作後、1回程度は撮影を続けますがしばらくお待ちください。
- インターバル撮影の設定は「撮影間隔=最短」「撮影期間=∞(=0)」固定です。
- インターバル合成の設定は「途中経過保存=なし」「撮影時間=24時間」固定です
- セルフタイマーの時間は、基本アプリから設定してください。


## 傾け操作の実現方法

過去の記事<b>「[NDKでEquirectangularの回転処理をして物体を全方位トラッキングする](https://qiita.com/KA-2/items/6f8395e4ea0dc378cc7a#%E4%B8%80%E8%88%AC%E7%9A%84%E3%81%AAandroid%E3%82%B9%E3%83%9E%E3%83%BC%E3%83%88%E3%83%95%E3%82%A9%E3%83%B3%E3%81%A8theta%E3%81%AE%E9%81%95%E3%81%84)」</b>でも説明していますが、今回必要なところだけ抜き出して再掲載しておきます。

Android標準ライブラリの姿勢情報を得るメソッド[SensorManager.getOrientation](https://developer.android.com/reference/android/hardware/SensorManager#getOrientation(float[],%20float[]))を利用しています。

一般的なAndroidスマートフォンにおけるローカル座標系と、THETAにおけるローカル座標系は、それぞれ以下のように定義されています。Googleのドキュメントを読むとき、これらの差異に注意してください。

![01_02_ローカル座標系.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/1c47f5fd-dd93-2763-e9f5-d35e8927bbb2.jpeg)

そして、ψ=方位角(Azimath/Yaw)、θ=勾配(Pitch)、φ=回転(Roll)の値域には、以下のような差異があります。

||ψの値域|θの値域|Φの値域|
|---|---|---|---|
|一般的なスマートフォン|－π～π|－π～π|－π/2～π/2|
|THETA|－π～π|－π/2～π/2|－π～π|

θとΦはいずれか一方の値域が－π/2～π/2、もう一方が－π～πになっていればよいです。
THETAは、Equirectangularという形式（地球儀と世界地図の関係における世界地図の形式）で映像が得られ、画像の縦軸がPitchを現しています。この地域が－π/2～π/2であるため、対応がとれるようにしてあります。直感的にもわかり易いです。

刻々と姿勢が3軸の角度で得られますので、それにあわせて、所望の動作をするコードを書くだけです。とても簡単です。

## ソースコード

[こちらのプロジェクト一式](https://github.com/theta-skunkworks/theta-plugin-tilt-ui)を参照してください。
ポイントを絞った詳細な説明を以下に記載します。

### ファイル構成

[RICOH THETA API](https://github.com/ricohapi/theta-api-specs/blob/main/theta-web-api-v2.1/README.md)を利用して各種撮影設定を行うことから、すでに殆どの撮影設定とOLED表示を網羅していた[こちらの記事のソースコード](https://qiita.com/KA-2/items/3de2e5a590c7768c6325#%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89)をベースに作成しました。

新規作成、または、変更を加えたファイルは以下のとおりです。

```
theta-plugin-switchbot-link\app\src\main
├assets		// OLED表示の部品が入っています。いくらか新規作成アイコンがありますが見栄えのことですので説明は割愛します。
└java\com\theta360
　├pluginapplication
　│　├model	// THETA Plug-in SDKのままです。
　│　├network	// 過去事例と同じようにHttpConnector.javaにメソッド追加したのみです。
　│　├oled	// Oled.java(OLED描画ライブラリ的なもの)のみです。
　│　└task	// ChangeFilterTask.javaとChangeFormatTask.javaが新規です。
　│			// GetCameraStatusTask.java, ShutterButtonTask.java, ChangeShutterSpeedTask.java, ChangeWbTask.java を修正しました。
　└tiltui	// Attitude.javaは、過去に作成したままです。便利クラスになってます。
			// MainActivity.java, DisplayInfo.java に本プラグイン独自事項が盛り込まれています。
```

### 傾き判定の方法

以下3つの基礎的事項は、リンク先を参照してください。

- [getOrientationについて](https://qiita.com/KA-2/items/6f8395e4ea0dc378cc7a#getorientation%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)
- [姿勢クラス Attitude.java](https://qiita.com/KA-2/items/6f8395e4ea0dc378cc7a#%E5%A7%BF%E5%8B%A2%E3%82%AF%E3%83%A9%E3%82%B9-attitudejava)
- [姿勢クラスの使い方](https://qiita.com/KA-2/items/6f8395e4ea0dc378cc7a#%E5%A7%BF%E5%8B%A2%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9)

#### プログラムの基本構造

MainActivity.javaの中に周期動作をするスレッドを作成し、以下のような処理を行っています。WebAPIで撮影タスクの状態を周期的に取得するので、内部の通信負荷が高くなりすぎないよう、200msecのお休みをしながら動作させるようにしています。

```MainActivity.java
    public void drawOledThread() {
        new Thread(new Runnable() {
            @Override
            public void run() {

                //ループ手前でやることがあるならば・・・

                //描画ループ
                while (mFinished == false) {
                    //ステータスチェックと描画
                    try {
                        //姿勢情報取得&姿勢関連情報更新
                        displayInfo.setTiltStatus(attitude.getDegAzimath(), attitude.getDegPitch(), attitude.getDegRoll());

                        //内部状態獲得
                        new GetCameraStatusTask(mGetCameraStatusCallback).execute();

                        // OLED表示
                        displayInfo.displayTiltUI();

                        // 逆さま戻しあり && WB変更可能 -> Preset <-> CT 切り替え
　　　　　　　　　　　　　～省略～

                        //捻り操作検出時の処理
　　　　　　　　　　　　　～省略～

                        //ボタン操作の実行
　　　　　　　　　　　　　～省略～

                        //もろもろが高頻度になりすぎないようスリープする
                        Thread.sleep(200);

                    } catch (InterruptedException e) {
                        // Deal with error.
                        e.printStackTrace();
                    } finally {
                        //
                    }

                }
            }
        }).start();
    }

```

#### 姿勢の判定

上記周期動作スレッドの先頭で呼び出している、DisplayinfoクラスのsetTiltStatusメソッドで行っています。
処理内容は以下のとおり。

```DisplayInfo.java
    void setTiltStatus(double inYaw, double inPitch, double inRoll) {
        beforDegAzimath = degAzimath;
        degAzimath = inYaw;
        degPitch = inPitch;
        degRoll = inRoll;

        beforTilt = curTilt;
        if ( (-45 <= degPitch) && (degPitch<45) ) {
            if ( (-45<=degRoll) && (degRoll<=45) ) {
                curTilt = TILT_BASE;
            } else if ( (45<=degRoll) && (degRoll<=135) ) {
                curTilt = TILT_RIGHT;
            } else if ( (-135<=degRoll) && (degRoll<=-45) ) {
                curTilt = TILT_LEFT;
            } else if ( ((135<=degRoll) && (degRoll<=180)) ||
                    ( (-180<=degRoll) && (degRoll<=-135) ) ) {
                curTilt = TILT_UPSIDE_DOWN;
                occurUpsideDown=true;
            } else {
                curTilt = TILT_ERROR;
            }
        } else if ( (45 <= degPitch) && (degPitch<=90) ) {
            curTilt = TILT_FRONT;
        } else if ( (-90 <= degPitch) && (degPitch<=-45) ) {
            curTilt = TILT_BACK;
        } else {
            curTilt = TILT_ERROR;
        }

        //捻りイベントチェック
        if ( curTilt == TILT_BASE ) {
            double diffAzimath = beforDegAzimath-degAzimath;
            if ( diffAzimath >= TWIST_THRESH ) {
                lastEvent = EVENT_TWIST_L;
            } else if ( diffAzimath <= -1.0*TWIST_THRESH) {
                lastEvent = EVENT_TWIST_R;
            } else {
                //捻りイベントはなし
            }
        }

        //傾きイベントチェック
        boolean occurChangeEvent = false;
        if ( (curTilt==TILT_BASE)&&(beforTilt!=TILT_BASE) ) {
            occurChangeEvent = true;

            if (occurUpsideDown) {
                occurUpsideDown=false;
                lastEvent =EVENT_UPSIDE_DOWN;
            } else {
                switch (beforTilt) {
                    case TILT_FRONT :
                        lastEvent =EVENT_FRONT;
                        break;
                    case TILT_BACK :
                        lastEvent =EVENT_BACK;
                        break;
                    case TILT_RIGHT :
                        lastEvent =EVENT_RIGHT;
                        break;
                    case TILT_LEFT :
                        lastEvent =EVENT_LEFT;
                        break;
                    default:
                        //無処理:起こらないはずが念のため
                        break;
                }
            }

            // 変更可能パラメータの遷移
            ～省略～


        }

    }
```
最初に、「現在の姿勢」を判定したあと、周期動作の中の前回姿勢からの変化によって「イベント」を設定しています。この処理のあとに続く処理では、イベントに対応する振る舞いをしているだけです。
書くのが面倒なだけで、難しくはありません。


### 撮影設定を行うタスクの共通構造

本プラグインでは、多種類の撮影設定ができ、設定項目ごとにタスクを11種類用意しています。各タスクの処理が終わった後には共通のCallback処理を行わせたかったので、AndroidのConsumerという仕組みを利用しています。コールバックだらけにならずスッキリ記述できています。


## まとめ

「RICOH THETAはスティック形状」ということも相まって、とてもよい操作拡張が行えたと思っています。
RICOH THETA Z1ユーザーは、「スマートフォンなしで撮影設定をしたい」という方がわりと多くいるのではないでしょうか？ストア公開を頑張りますので、ぜひとも使ってください。

類似の操作系をRICOH THETA Vで行う場合、CaptureModeと露出プログラムは決めうちしたapkとしたほうがよさそうですね。表示はLED3の色を駆使し、設定値お知らせは音声ファイル再生で読み上げることができますが、ボタンが少ない点がしんどそーです。
とはいえ、是非ともチャレンジしてみてください！

＜余談＞
このプラグインの超裏技的な使い方に気づいてしまいました。
このプラグインは設定値を記憶できるプラグインです。applicationIdとapp_name（プラグイン名称）を変えただけのapkを複数作ってインストールすると、その数だけマイセッティングが増やせることになります。
そして、「[Plug-in Launcher for Z1](https://pluginstore.theta360.com/plugins/skunkworks.launcher/)」を併用すると、名前違いのプラグインが沢山あっても直ぐに呼び出せるという、そんな使い方までできてしまいます。
（もしかして、名前違いのapkを5つくらいストア置いてもニーズあったりするのかしら・・・）
Twitterアカウントやslackなどを通して、このプラグインへのご意見ご要望なども頂けたら幸いです。


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

THETAプラグインをご存じない方は[こちら](https://github.com/ricohapi/theta-api-specs/blob/main/core/products/theta-plugin.md)をご覧ください。
パートナープログラムへの登録方法は[こちら](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)への参加もよろしくおねがいします｡