NDKでEquirectangularの回転処理をして物体を全方位トラッキングする

## はじめに
リコーの @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をカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます（詳細は本記事の末尾を参照）。

今回は、以下２つの記事の続編です。

- [<b>THETAプラグインでライブプリビューを扱いやすくする</b>](https://qiita.com/KA-2/items/cef05a4960663bd2ba2f)
- [<b>THETAプラグインで連続フレームにTensorFlow Liteの物体認識をかける</b>](https://qiita.com/KA-2/items/8c150975a998b0ad0f16)

前回の記事に以下２つの技術要素を加えてバナナを全方位トラッキングします。

- <b>THETAの姿勢情報の利用</b>
- <b>Equirectangularの回転処理</b>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">◤Qiita記事公開◢<br>THETAの姿勢情報利用とNDKのEquirectangular回転処理を詳しく解説。物体認識と連動し全方位追尾！<br>応用として、常に下方の人数カウントも可能。記事末にTHETAを簡易3密緩和お助けツール化するアイディアも。ご意見求ム！<a href="https://t.co/mlEPaJ8ivH">https://t.co/mlEPaJ8ivH</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/3%E5%AF%86%E3%82%92%E9%81%BF%E3%81%91%E3%82%8B?src=hash&amp;ref_src=twsrc%5Etfw">#3密を避ける</a> <a href="https://t.co/VFQvn72Woa">pic.twitter.com/VFQvn72Woa</a></p>&mdash; THETAプラグイン開発者コミュニティ (@thetaplugin) <a href="https://twitter.com/thetaplugin/status/1253505477851242496?ref_src=twsrc%5Etfw">April 24, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

【2020/5/1 Maker Faire Kyoto Online向け動画も貼りました】

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">TMMF2020でデモした横方向の物体追尾を全方向追尾に改善しました！<br>他にもM5Stackリモコン化など、色々な事例アリマス。GWのstayhomeを利用してTHETAプラグインを作ってみませんか？<a href="https://t.co/mlEPaIQHE9">https://t.co/mlEPaIQHE9</a><a href="https://twitter.com/hashtag/MFKyoto2020?src=hash&amp;ref_src=twsrc%5Etfw">#MFKyoto2020</a> <a href="https://twitter.com/hashtag/%E4%BD%9C%E5%93%81%E7%99%BA%E8%A1%A8?src=hash&amp;ref_src=twsrc%5Etfw">#作品発表</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/%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92?src=hash&amp;ref_src=twsrc%5Etfw">#機械学習</a> <a href="https://twitter.com/hashtag/TensorFlowLite?src=hash&amp;ref_src=twsrc%5Etfw">#TensorFlowLite</a> <a href="https://twitter.com/hashtag/objectdetection?src=hash&amp;ref_src=twsrc%5Etfw">#objectdetection</a> <a href="https://t.co/3yd0zlwnoM">pic.twitter.com/3yd0zlwnoM</a></p>&mdash; THETAプラグイン開発者コミュニティ (@thetaplugin) <a href="https://twitter.com/thetaplugin/status/1256456551512104961?ref_src=twsrc%5Etfw">May 2, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


特に「Equirectangularの回転処理」についてはAndroid NDKで実装しつつ、行列演算を楽に実装するためにOpenCVのMat型を利用します。<b>[THETAの中でOpenCVを動かす【プレビューフレーム取得編】](https://qiita.com/roohii_3/items/f96918cdb52fd84e1cea)</b>の記事の派生記事でもあります。さらに、処理時間高速化のためにオプティマイズをしたりマルチコア動作をさせるOpenMPも利用します。

とっても盛り沢山ですが、順番に紐解いていくと理解できますのでお付き合いください。
ここを抑えておくと、色々なTHETAプラグインへと応用ができると思います。


## 姿勢とEquirectangularの回転


### グローバル座標系、ローカル座標系、姿勢の表現

空間の物事を考えるにあたり、まず座標系を明確にしておきます。
Googleのドキュメントにも[図](https://developer.android.com/guide/topics/sensors/sensors_motion?hl=ja#sensors-motion-rotate)や[説明](https://developer.android.com/guide/topics/sensors/sensors_position)があるのですが、少々わかりにくいと思われるので、まとめ直してみました。わかる方は読み飛ばしてください。

![01_01_座標系.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/8a9b2161-e4e9-3758-e58f-f0b1c1988620.jpeg)


透明な球状の器に半分まで水を満たして、それを俯瞰でみていることを思い浮かべてみてください。そして、水面の真北に「十字のしるし」をつけます。これを基準の姿勢とします。

任意の姿勢というのは、透明の器を回したあとの「十字のしるし」が、基準の姿勢から「どの方向に」「どのように傾いて」いるのかを表現することになります。
このとき、いいかげんに球をまわしてしまうとわけがわからなくなるので、ルールを決めます。
球の中心から北（青色）、球の中心から東（緑色）、球の中心から真上（赤色）に動かない仮想の３つの軸（Yaw軸、Pitch軸、Roll軸）をさだめ、各軸まわりの回転操作をすることとします。

とある１つの姿勢にたどり着く道のりは、軸をまわす順番の組み合わせだけ存在してしまいますので、回転の順番を決めます。「順番」とても大切です。
こうすることで、各軸周りの回転角度（ψ=方位角、θ=勾配、φ=回転）で、任意の姿勢を一意に定めることができます。

このとき、動かない３つの軸の座標系を「グローバル座標系（または、世界座標系や地球座標系とも呼ばれます）」といいます。とある姿勢を客観視している座標系です。
一方、動いたあとの姿勢を基準としている座標系（主観の座標系）を「ローカル座標系（または、センサー座標系）」といいます。


### 一般的なAndroidスマートフォンとTHETAの違い。

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

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


そして、ψ=方位角、θ=勾配、φ=回転は[SensorManager.getOrientation](https://developer.android.com/reference/android/hardware/SensorManager#getOrientation(float[],%20float[]))というメソッドで得ることができ、値域には以下のような差異があります。

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

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

### Equirectangularの見え方

ローカル座標において、Equirectangularがどのような映像になるかを示した図が以下です。

![01_03_姿勢とEqui.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/43b1715f-af28-504d-aa84-1edf85d49ab0.jpeg)

少々余談となりますが、この画像のψ、θ、Φをすべてグローバル座標系にあわせるEquirectangularの回転処理を「姿勢補正（Orientation correction）」と呼びます。θ、Φだけをグローバル座標系にあわせるEquirectangularの回転処理を「天頂補正（Zenith correction）」と呼びます。


### 姿勢回転の数式

ある基準となる座標系を定め、その座標系で表現された 姿勢m=ベクトルm を 姿勢n=ベクトルn に回転するとき、以下の数式が成り立ちます。結論だけをエイヤっと書いていておきます。

![02姿勢回転.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/a7440b44-f4d5-1141-2a64-d2e07a0195d0.jpeg)

詳しい説明は、ご自身にあった図入りの説明を探すとよいと思います。たとえば、ちょっとTHETAと事情が異なりますが、[こちらのような解説](https://watako-lab.com/2019/02/15/3axis_acc/)も参考になります。

式は、行列によってシンプルに表現されています。
図はグローバル座標系を基準にしていますが、ローカル座標系を基準にしても成り立ちます。
座標系と回転順さえ揃っていれば、複数回の回転も、Yaw、Pitch、Rollそれぞれの加減算をした後、1つの回転行列を求めれよいですし、回転順が異なる複数回の回転をする場合でも、最終的には１つの回転行列を求めればよいです。


### Equirectangularの回転

Equirectangularはベクトルの集合体とみなせます。Equirectangularの回転は、画素の座標と姿勢（Yaw, Pitch, Roll）の幾何学と、前述の「姿勢回転の数式」を組み合わせた以下の演算を、画素数の数だけ繰り返せばよいです。

![03Equi回転.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/e981a647-08ae-3f60-f5ff-c4265cde81b1.jpeg)


(p, q)は小数で得られますが、正確なRGB値がわかるのは整数の画素位置です。
今回は、処理高速化を優先して、演算結果に最も近い整数の画素位置からRGB値を取得することにしました。このため、回転後画像にシャギのような荒れがでます。認識できればよいので、意図的に映像の滑らかさを切り捨てました。
以下は1024x512pixelのEquirectangularを回転した後、物体認識をかけている300×300pixelのエリアを切り出した例です。画素数が少ないほど目立ち、多いほど目立ちません。

![04Equi回転の補完なし.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/c3d33361-9b0d-e57a-756e-ce182a5aed66.jpeg)

演算時間が増えたとしても、より滑らかな映像が欲しい場合、「演算結果（p, q）に近しい四点のRGB値から、尤もらしいRGB（安直には線形補完など）を算出する」など、ご自身で工夫を行ってください。


### なぜNDKのOpenCVを使う？

[こちらの記事](https://qiita.com/KA-2/items/cef05a4960663bd2ba2f#%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E5%88%86%E5%89%B2%E5%BE%8C%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AB%E7%8B%AC%E8%87%AA%E3%81%AE%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86%E3%82%92%E8%A1%8C%E3%81%86%E3%81%A8%E3%81%8D%E3%81%AE%E6%8E%A8%E5%A5%A8%E7%AE%87%E6%89%80)で、「[エッジ抽出もどき](https://github.com/theta-skunkworks/theta-plugin-extendedpreview/blob/master/app/src/main/java/com/theta360/extendedpreview/MainActivity.java#L453-L485)」や「[コマ間差分](https://github.com/theta-skunkworks/theta-plugin-extendedpreview/blob/master/app/src/main/java/com/theta360/extendedpreview/MainActivity.java#L420-L451)」をJavaで記述して動作させた例を紹介しました。このような簡単な繰り返し演算でさえ、VMで動作すると処理時間が増えてしまいます。そこで、NDKを利用することにしました。

さらに、今回の演算は行列演算が多用されます。行列演算を一から組むこともできますが、デバッグ量が増えることが容易に想像できるので、「[NDKでOpenCVを利用している記事](https://qiita.com/roohii_3/items/63a546169981f41967df)」を参考にして、OpenCVのMat型（各種行列が簡単に扱えます）を使うことにしました。
※OpenCVの画像処理ライブラリは使っていません。

これまでに解説した事項がわかっていれば、他の手段でも実装できると思います。お好みに応じて色々試してください。


## ソースコードの説明

今回作成したプラグインのファイル一式を<b>[こちら](https://github.com/theta-skunkworks/theta-plugin-tfl-od-tracking)</b>においておきます。

★2020/10/23:TensorFlow LiteのObject Detectionサンプルコード変更に対応しました。（サンプルコード側にある問題の対策もしてあります）

<b>このプロジェクトファイル一式をビルドするには、[こちらの記事の「事前準備」の章](https://qiita.com/roohii_3/items/63a546169981f41967df#%E4%BA%8B%E5%89%8D%E6%BA%96%E5%82%99)と本記事「NDK版OpenCVの環境構築」の項に記されている、以下4つの事項を各自で行う必要があります。ご注意ください。</b>

 - [OpenCV Android pack 3.4.9](https://sourceforge.net/projects/opencvlibrary/files/3.4.9/opencv-3.4.9-android-sdk.zip/download)のダウンロードと配置<br>（Android.mkに記載するOpenCV.mkのパスを、ご自身の環境に合わせて書き換える作業も行ってください）
 - Android Studioの設定（NDKをビルドできるようにする設定）
 - ”Java SE Development Kit 8”をのダウンロードとインストール
 - OpenCVライブラリ（.soファイル）のコピー<br> ※ここだけ本記事の「NDK版OpenCVの環境構築」の項を参照。


このプロジェクトは、[前回の記事](https://qiita.com/KA-2/items/8c150975a998b0ad0f16)で説明した内容に、以下の追加作業を行って作成しました。（ただし、パッケージ名称は「com.theta360.extendedpreview」から「com.theta360.tracking」に変更してあります。フォルダ名もこれにあわせて変更されていますので適宜読み替えてください。）
「THETAの姿勢情報利用」や「Equirectangularの回転処理」以外の事項については前回の記事を参照してください。

- NDK版OpenCVの環境構築
- NDKでEquirectangular回転処理(rotation_equi.cpp)を追加する
- NDKの高速化（オプティマイズ、OpenMP）
- 姿勢を求めるクラス(Attitude.java)の追加と利用方法
- TensorFlow Lite 物体認識と組み合わせ全方位追尾をする<br>（MainActivity.javaをちょっと修正）

前回の記事に対して2つソースコードが増えて1つソースコードに手を加えただけです。

以降でそれぞれのパートについて解説します。
例えば、別のプラグインを作るときに「Equirectangularの回転処理だけをしたい」「姿勢情報だけを利用したい」というときにも該当する作業だけを抜き出せるように記述しておきますね。

### NDK版OpenCVの環境構築

基本は、[こちらの記事の「プロジェクトファイルndkビルド環境の準備」の章](https://qiita.com/roohii_3/items/63a546169981f41967df#%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%83%95%E3%82%A1%E3%82%A4%E3%83%ABndk%E3%83%93%E3%83%AB%E3%83%89%E7%92%B0%E5%A2%83%E3%81%AE%E6%BA%96%E5%82%99)に従って作業しますが、一部変更しなければならない点があるので差異を以下にまとめます。

- プロジェクトは新規作成ではなくTHETA Plug-in SDKやこちらの記事のプロジェクトでOKです。
- 今回使用したOpenCV SDK は「opencv-3.4.9-android-sdk」です。
- ビルドオプションのCPUの指定は「armeabi-v7a」→「arm64-v8a」になります。<br>32bit互換モードがあるCPUなので元のままでも動きますが、64bit CPUを指定するほうが正しいです。具体的な修正ポイントは以下のとおり。

<b>2. ビルドファイルの作成 → 【Application.mk】</b>

```Application.mk
～他の行は省略しています～
APP_ABI := arm64-v8a
```

<b>3. OpenCVライブラリ（.soファイル）のコピー<b>

```
コピー元: C:/(OpenCV Android packがある場所)/sdk/native/libs/arm64-v8a
コピー先: C:/(プロジェクトファイルがある場所)/app/jniLibs/arm64-v8a
```

<b>4. build.gradleの編集</b>

```build.gradle
android {
    ...
    defaultConfig {
        ...
        ndk {
            moduleName "libopencvsample"
            abiFilters 'arm64-v8a'
        }
    }
}
```

### NDKでEquirectangular回転処理(rotation_equi.cpp)を追加する

[こちらの記事の「nativeコード」の項](https://qiita.com/roohii_3/items/63a546169981f41967df#native%E3%82%B3%E3%83%BC%E3%83%89)で説明されている以下事項は抑えておいてください。

- Nativeコードの置き場所
- Java ↔ Native(C/C++)のやり取りのルール

今回作成したNativeコードのファイル名は「rotation_equi.cpp」です。
Android Studioでこのソースコードを表示すると、以下のメッセージが表示されます。

![10_AndroidStudioのメッセージ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/932e7f04-c2ab-e45d-2a54-61a4287dd8ea.png)

これはAndroid Studioの既知の問題だそうです。
無視してもOKです。ビルドも通ります。いずれ修正して頂けることでしょう。

「rotation_equi.cpp」には、以下の関数が記述されています。

|No|関数名|処理概要|
|---|---|---|
|1|Java_com_theta360_tracking_MainActivity_version<br>※Javaからは「version」という名称で呼び出します|OpenCVのバージョン文字列を取得します。<br>回転処理には関係ありません。|
|2|eular2rot<br>※Javaからは呼び出せません|引数から指定された回転順とYaw,Pitch,Rollを元に回転行列を生成します。|
|3|rotate_pixel<br>※Javaからは呼び出せません|引数から指定された位置の画素が、元画像のどの位置の画素に対応するのかを計算します。|
|4|rotate_rgb<br>※Javaからは呼び出せません|画素数の数だけrotate_pixelを行い、RGB値を求め、回転後画像を生成します。|
|5|Java_com_theta360_tracking_MainActivity_rotateEqui<br>※Javaからは「rotateEqui」という名称で呼び出します|Javaから呼び出される回転処理です。1種類の回転が行えます。|
|6|Java_com_theta360_tracking_MainActivity_rotateEqui2<br>※Javaからは「rotateEqui2」という名称で呼び出します|Javaから呼び出される回転処理です。2種類の回転を一括して行えます。|

2が、前章「姿勢回転の数式」の項で説明した回転行列 R(ψθφ) を求める部分に対応しています。
3が、前章「Equirectangularの回転」の項で説明した（p, q）を求める計算式に対応しています。
前章の説明とともにコードをみると、行っていることが理解できると思います。

余談となりますが、別のプラットフォーム向けに作成された[こちらのソースコード](https://github.com/whdlgp/Equirectangular_rotate/blob/master/Equirectangular_rotate.cpp)をベースにカスタマイズしました。ベースにしたソースコードでは回転順番が固定ですし順番も異なっていたりします。さらには今回使っていない関数も含まれています。比較するときにはご注意ください。ご参考まで。

### NDKの高速化（オプティマイズ、OpenMP）

なにも指定せずにビルドをしてもJavaよりかなり高速な演算が行えるのですが、「オプティマイズ」と「OpenMP（マルチコア利用）」の指定をして、より高速な演算が行えるようにしてみました。

<b>オプティマイズ</b>

Application.mkに「APP_CFLAGS」を追記することでオプティマイズや浮動小数点演算器の使い方をコンパイラに指定できます。ひとまず、オプティマイズのレベルを最高レベルの「-O3」とするだけでも大きな効果が得られました。それ以外のオプションは浮動小数点演算器の使い方を指定しています。[こちらのARMのドキュメント](http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0774dj/chr1392305424052_00039.html)などを見ながら指定してみるとよいと思います。（以下の浮動小数点演算に関する所はちょっと間違いがあるかもしれません）

```Application.mk
APP_CFLAGS := -O3 -mcpu=cortex-a53 -mfpu=neon -mfloat-abi=softfp -fPIC -march=armv8-a
#APP_CFLAGS := -O3
```

<b>OpenMPによるマルチコア利用</b>

OpenMPはマルチコアを利用するためのAPIです。今回のように「順番を問わないループ処理」は、処理を各コアに振り分けて並列処理をすると、演算時間が短縮できます。

Application.mkファイルに以下のオプションをつけると、コード中に「#paragma」で記載されているOpenMPの指示が有効になります。

```Android.mk
LOCAL_CFLAGS := -fopenmp -static-openmp
LOCAL_LDFLAGS := -fopenmp -static-openmp
```

今回は、以下の二重ループの先頭に「#pragma omp parallel for」を記載しました。これで、自動的に処理を複数のコアに振り分けてくれます。
こちらも、今回の演算で大きな効果がみられました。

```rotation_equi.cpp
    void rotate_rgb(Mat inRotMat, int im_width, int im_height, Vec3b* im_data, Vec3b* im_out_data )
    {
        #pragma omp parallel for    //Use OpenMP : Specify "-fopenmp -static-openmp" for the build option.
        for(int i = 0; i < static_cast<int>(im_height); i++) {
            for (int j = 0; j < static_cast<int>(im_width); j++) {
                ～省略～
            }
        }

        return;
    }
```

今回は深入りしませんが、実際にいくつのコアに処理が振り分けられたかを確認する「omp_get_thread_num()」というAPIがあったり、他にも色々なAPIが用意されています。
（今回、このAPIも試してはみたのですが、あまりリソースが空いていないようで、2コアまでしか使用されていませんでした。使いこなしがなかなか難しい。。。）


### 姿勢を求めるクラス(Attitude.java)の追加と利用方法

#### getOrientationについて

前章にて、「姿勢を表す方位角、勾配、回転をgetOrientationというメソッドで取得する」と記載していました。このgetOrientationというメソッドは、所謂「センサーフュージョン」で結果を求めるメソッドです。元となる物理センサーの組み合わせをどうするか、以下2通りの選択肢があります。

- 地磁気センサーと加速度センサー（6軸の中の加速度3軸）から求める
- 6軸センサー（加速度3軸と角速度3軸）から求める

前者は、地磁気による「北」を検出できますが、地磁気は微弱なため、機体にちょっとした金属が近づいたり、離れたりしただけで影響をうけます。その時々で基準がふらふらして値が安定しません。電源On時に必ず北が得られるかどうかもわかりません。
後者は、北の代わりに「センサーを起動したときの方向」を基準とします。センサー次第の基準です。あとは角速度の累積から基準の方向との角度差を返します。このため、あまり長時間動かすと誤差の累積があるものの、前者よりも安定した方向が得られます。

今回は北が判る必要がないので、後者の方法を使います。前者を使いたい場合は[こちら](https://techbooster.org/android/device/443/)等を参考にすると良いでしょう。（試してはみましたが、映像の補正に使うと映像が揺れてしまいますのでご注意を）

#### 姿勢クラス Attitude.java

大枠は[加速度センサーを使ったこちらの記事](https://qiita.com/yokazuya/items/f36e5a2252bf32b0c18b#%E5%8A%A0%E9%80%9F%E5%BA%A6%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E6%9B%B8%E3%81%84%E3%81%A6%E3%81%BF%E3%81%9F)と同じです。

コンストラクタにて、センサー値の取得レートと元センサーの種類を指定しています。
レートは以下4種類が指定できます。アプリケーション全体の負荷バランスをみて「SENSOR_DELAY_UI」を使うことにしました。

|設定名称|説明|
|---|---|
|SENSOR_DELAY_FASTEST|最も高頻度になるがCPU負荷が高すぎる|
|SENSOR_DELAY_GAME|スマートフォンにおいて、画面を傾けるゲームを作る時に最適|
|SENSOR_DELAY_UI|ゲーム用より低頻度だが、姿勢を使うUIに最適|
|SENSOR_DELAY_NORMAL|最も低頻度|

「Sensor.TYPE_GAME_ROTATION_VECTOR」というのが6軸センサーを使う指定です。


```Attitude.java
    public Attitude(SensorManager sensorManager){
        int rate = SensorManager.SENSOR_DELAY_UI;
        sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR), rate);
    }

```

センサー値に変化があると呼び出されるonSensorChangedを以下のように記述しておきます。
値を更新する箇所ではsynchronized を使い排他をかけています。

```Attitude.java
    @Override
    public void onSensorChanged(SensorEvent event) {
        if ( event.sensor.getType() == Sensor.TYPE_GAME_ROTATION_VECTOR ) {
            SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values);
            synchronized (this) {
                SensorManager.getOrientation(
                        rotationMatrix,
                        curAttitudeVal);
            }
        }
    }
```
あとは、RadianとDegreeのゲッターがあるだけです。

#### 姿勢クラスの使い方

使い方は簡単です。
MainActivityクラスの中で以下のインスタンスを宣言し、

```MainActivity.java
public class MainActivity extends PluginActivity {
    ～省略～

    //Attitude
    private SensorManager sensorManager;
    private Attitude attitude;

```
onCreateメソッドでインスタンスを生成すると、定期的に姿勢情報が更新されるので

```MainActivity.java
    protected void onCreate(Bundle savedInstanceState) {
        ～省略～

        //init Attitude
        sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
        attitude = new Attitude(sensorManager);
```

あとは、使いたいところでゲッターを呼んで最新の値を参照するだけです。

```MainActivity.java
                        double corrAzimath = attitude.getDegAzimath();
                        double corrPitch = attitude.getDegPitch();
                        double corrRoll = attitude.getDegRoll();
```

今回はEquirectangularの回転（映像の姿勢補正）に利用しましたが、例えば「姿勢によってボタンの役割を変える」というような使い方をすると、操作ボタンが少ないTHETAのUIを改善することもできます。


### TensorFlow Lite 物体認識と合わせて全方位追尾する

前回記事の「[bitmapクラスで-equirectangularの横方向回転をする](https://qiita.com/KA-2/items/8c150975a998b0ad0f16#bitmap%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%A7-equirectangular%E3%81%AE%E6%A8%AA%E6%96%B9%E5%90%91%E5%9B%9E%E8%BB%A2%E3%82%92%E3%81%99%E3%82%8B)」で説明した「rotationYawメソッド」を、今回NDKで記述した回転処理に置き換える。というのが基本です。

以下のコメント部分は、回転処理を2回行って追尾した場合のコードです。
一度グローバル座標系にしてから、累積している位置が映像の中心になるよう回転しています。
このような、回転の処理時間を短縮するため、2種類の異なる回転を一括して行える「rotateEqui2」を作成しています。

```MainActivity.java
                        //--- 姿勢補正&追尾 回転処理2回（遅い、ほぼ2fps）---
                        //byte[] dst1 = rotateEqui(false, corrAzimath, corrPitch, corrRoll, bitmap.getWidth(), bitmap.getHeight(), byteBuffer.array());
                        //byte[] dst = rotateEqui(true, -rotYaw, -rotPitch, 0, bitmap.getWidth(), bitmap.getHeight(), dst1);
                        //--- 姿勢補正&追尾 回転処理1回まとめ（ほぼ3fps）---
                        byte[] dst = rotateEqui2(
                                false, corrAzimath, corrPitch, corrRoll,
                                true, -rotYaw, -rotPitch, 0,
                                bitmap.getWidth(), bitmap.getHeight(), byteBuffer.array() );

```

さらに、「updateDetectInfoメソッド」はYaw方向（横方向）の追尾角だけを累積していたのでPitch方向の追尾角も累積するようコードを書き足しました。コードの解説は割愛します。
これで、映像の姿勢補正をしつつ全方位のバナナトラッキングも行えるようになります。

![00_全方位trackingショート.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/49bc37ea-9a9d-0312-a330-398a53559ea5.gif)


少々余談となりますが、姿勢情報と回転のさせ方によって、多様な振る舞いができます。
コメントとしてコードも残していますが、軽くふれておきますね。

[姿勢補正]

上記の「rotateEqui2メソッドの呼び出し」を以下に置き換えると、姿勢補正だけが行えます。

```
                        //--- Orientation correction ---
                        //byte[] dst = rotateEqui(false, corrAzimath, corrPitch, corrRoll, bitmap.getWidth(), bitmap.getHeight(), byteBuffer.array());
```

追尾をしないので物体認識もコメントアウトすると、概ね5fpsで姿勢補正が行えました。
ソフトウェア演算による回転処理、なかなか速い。

![03_姿勢補正400.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/ee5d8a81-fe25-9762-92f0-a55c422dc058.gif)


[天頂補正]

上記の「rotateEqui2メソッドの呼び出し」を以下に置き換えると、天頂補正だけが行えます。
姿勢補正から、Yawを0にしてYaw方向だけ補正しないようにしています。

```
                        //--- Zenith correction ---
                        //byte[] dst = rotateEqui(false, 0, corrPitch, corrRoll, bitmap.getWidth(), bitmap.getHeight(), byteBuffer.array());
```

追尾をしないので物体認識もコメントアウトすると、概ね5fpsで天頂補正が行えました。
当然ながら、姿勢補正と演算時間は変わりません。

![04_天頂補正400.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/55c342af-e6ba-6a76-8b4f-89bdae092990.gif)


[ローカル座標系の視点で追尾]


上記の「rotateEqui2メソッドの呼び出し」を以下に置き換えると、ローカル座標系での追尾が行えます。

```
                        //--- Local Tracking ---（ローカル座標系で追尾）
                        //byte[] dst = rotateEqui(true, -rotYaw, -rotPitch, 0, bitmap.getWidth(), bitmap.getHeight(), byteBuffer.array());
```

概ね3fpsで動作します。rotateEqui2で2種類の回転を一括して行うときと処理時間が変わりません。

![02_LocalTracking400.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/01545d02-b55a-0184-5545-60a09ea5903a.gif)


これくらい例を挙げると、回転処理の使い方が判ってきたのではないでしょうか？
ご自身の目的に合わせて、いろいろ試してみてください。


### 補足：WebUIが与える負荷をさらに下げる

解説したかった内容にあまり関係がない事項なので「補足」にしました。
前回記事の[こちら](https://qiita.com/KA-2/items/8c150975a998b0ad0f16#%E3%83%A9%E3%82%A4%E3%83%96%E3%83%97%E3%83%AA%E3%83%93%E3%83%A5%E3%83%BC%E8%A1%A8%E7%A4%BA%E3%81%AE%E6%9B%B4%E6%96%B0%E9%A0%BB%E5%BA%A6%E3%82%92%E4%B8%8B%E3%81%92%E3%82%8B)にしたがって、WebUIの表示更新頻度を下げてはいるのですが、たまにスマートフォンブラウザ側の映像更新だけがとまるような動作が見受けられました。

そこで、露出補正値の定期取得をコメントアウトしました。

```preview.js
function repeat() {
  const d1 = new Date();
  while (true) {
    const d2 = new Date();
    if (d2 - d1 >125) {
      break;
    }
  }
  updatePreviwFrame();
  updatePreviewStat();
  //updateEv();
}
```

定期更新をしないので、ブラウザ表示もコメントアウト

```index.html
    <!--
    <input name="ev" type="radio" value=-2.0 onclick="setEv(value);"> -2.0
    <input name="ev" type="radio" value=-1.7 onclick="setEv(value);"> -1.7
    <input name="ev" type="radio" value=-1.3 onclick="setEv(value);"> -1.3
    <input name="ev" type="radio" value=-1.0 onclick="setEv(value);"> -1.0
    <input name="ev" type="radio" value=-0.7 onclick="setEv(value);"> -0.7
    <input name="ev" type="radio" value=-0.3 onclick="setEv(value);"> -0.3
    <input name="ev" type="radio" value=0.0 onclick="setEv(value);"> 0.0
    <input name="ev" type="radio" value=0.3 onclick="setEv(value);"> 0.3
    <input name="ev" type="radio" value=0.7 onclick="setEv(value);"> 0.7
    <input name="ev" type="radio" value=1.0 onclick="setEv(value);"> 1.0
    <input name="ev" type="radio" value=1.3 onclick="setEv(value);"> 1.3
    <input name="ev" type="radio" value=1.7 onclick="setEv(value);"> 1.7
    <input name="ev" type="radio" value=2.0 onclick="setEv(value);"> 2.0
    -->
```

これで、たまにスマートフォン側の映像更新がされない現象はおさまりました。


## 応用事例（たとえばこんなことに・・・）

今回の事例を少しアレンジすると、THETAは、比較的簡単かつ廉価な（仰々しいシステムを組むことなくTHETAだけで成立する）「3密緩和対策」のお役立ちツールになるかもしれません。

![応用事例.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/ae7eb324-52d1-5348-9f55-edffe3041f15.jpeg)

- TensorFlow Liteのモデルだと認識数が10個までなので、静止画の複数領域に対して認識をかけるようにする解決手段がありそうです(9エリアくらい可能かな)。他の認識数が多いモデルを動かすことも可能です。
- 長時間の連続稼動は、認識の頻度を下げる（数分に１回程度）ことで可能になりそうです。
- SNS、もしくは、クラウド連携などはいろいろと可能です。THETAの中身はAndroidスマホと同じです。
- 電源On/Offがちょっと厄介そうです。電源OnはUSB端子に外部電源を投入すると行われます。電源Offは、プラグイン終了時に[こちらのAPI](https://github.com/ricohapi/theta-api-specs/blob/main/theta-web-api-v2.1/options/off_delay.md)で無操作から電源Offまでの時間を短くしておくとどうにかなりそうです。プラグインの起動は基本アプリからも可能ですしWeb APIをつかった別アプリを作ってもよいです。プラグインの諸設定や終了はWebUIを実装すればよいです。


## まとめ

「THETAの姿勢情報利用」と「Equirectangularの回転処理」によって、全天球映像の使いこなしバリエーションが広がったと思います。
ソフトウェア処理ですので、ぼちぼち処理時間がかかるものの、実用できる事例は結構あるのではないかと思います。

是非とも、いろいろなTHETAプラグインの作成に役立てていただきたいです。


## 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)への参加もよろしくおねがいします｡