THETA Vの加速度センサを使って紅葉ポートレート撮ってみた話

# 自己紹介
皆様、はじめましてsmonoです。私は、JavaやらC#やらC/C++やら使って、電子工作や組み込み、Windowsアプリ開発などやってるエンジニアです。基本はエンジニアですが営業もやってます（~~やらされてまorg~~）。エンジニアは自己表現として楽しくやってます。自分の中に湧いてくるアイデアを、自分の手を動かして、ものづくりで表現するのが楽しいんですよね～。

趣味のカメラでは、主に一眼レフでスナップを撮ったり、映画のようなポートレートムービー（流行りのCINEMATICVLOGとも言う）を撮ったりしています。

# 私にとってTHETA Vは朗報なのか
私はエンジニアとしてだけではなく、カメラで自分の世界観を表現したい人です。カメラは機械としても好きですが、基本は表現力を拡張するための道具だと思ってます。
そんな中、アプリ開発もできて写真動画も撮れる「THETA V」を知りました。すごい勘違いかもしれませんが、THTEA Vは私のような人を想定してリコーさんが開発してくれたのではないでしょうか。つまり、私のような人にとって朗報だといえます。

# 今回はTHETA Vで何をやるのか
THETA Vを浮き輪に取り付けて、（安全に）投げ上げて、被写体さんと紅葉を空中から同時かつ自動的に撮影するガジェットを作ります。ポートレート撮影のひと時を楽しくするガジェットとして使えないか試してみたいのです。

写真は今回の被写体さんである澪さんです。いつも楽しく写活しています。今回はリアルの写活でTHETA Vのプラグインを使ってみました。

こちらのYouTubeを見れば一目瞭然です。撮影は11月です。
https://youtu.be/nz9xRODnDIY

![image_6483441 (2).JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922958/3a5f021d-098a-f9b7-76c4-40b3d6db5ebb.jpeg)
図1　浮き輪に具備されたTHETA V

なぜ、THETA＋浮き輪かを真面目に説明をすると、第１に空気が充填されており、柔らかく、もしもの落下や衝突の際の衝撃が吸収できるという点です。第２にドーナツ形状をしており、その穴の部分に具備すれば、THETAの視野角を広く活用できるという点であります。あと、浮き輪は誰でも知っていてなじみがある遊び道具であることです。

![image_6483441.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922958/2f26c9cc-8070-08e9-66eb-151f43092e23.jpeg)
図2　浮き輪を投げる瞬間（投げ上げ後、安全に捕獲しました）
※十分に周囲の安全を確認したうえで実施しております。

# ではどうやって投げ上げを検知するのか
THETA Vにはモーションセンサ（加速度センサ、ジャイロセンサ、地磁気センサ）が搭載されています。今回は加速度センサを使って投げ上げを検出しカメラのシャッターを切ります。

まずAndroid APIにはSensor.TYPE_LINEAR_ACCELERATIONというのがあります。このセンサータイプでは、デバイスの回転や姿勢を考慮し、ワールド座標系における加速度を推定して取得することができます。ワールド座標系での重力加速度は常に鉛直下向きに発生していますが、TEHTAが観測できるのはデバイス座標系での加速度（THETAの姿勢によって加速度の軸が変わってしまう）です。これをTHETAの姿勢を示す回転行列を使って逆算的に補正して、鉛直方向と水平方向の加速度を推定したものがSensor.TYPE_LINEAR_ACCELERATIONです。ただし、Sensor.TYPE_LINEAR_ACCELERATIONは、あくまでもAndroidのネイティブに組み込まれたドライバのモーションエンジンが推定した値であり、推定誤差（主に姿勢推定の誤差）が大きく載っているので、目安程度に考えて取り扱ったほうが良いです。

今回は下記のコードで、ワールド座標系での加速度の合成ベクトルのノルムを計算して、そのノルム値にIIRのローパスフィルタをかけました。IIRフィルタの時定数はk=0.3くらいにして、感覚的には1～5Hz程度が大きく通過するローパスになるよう工夫にしました。このフィルタが無いと、ちょっとしたノイズが誤って検知されてしまい、安定性に欠けたため、ローパスは必須だと思いました。IIRフィルタについてネット検索されてください。

■ ノルムの計算式
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922958/34c70a45-0e32-c810-886f-2a38e3cbbe34.png)

■ ノルムを計算してIIRフィルタかけているところ

    @Override
    public void onSensorChanged(SensorEvent event) {

        if(event.sensor.getType() ==Sensor.TYPE_LINEAR_ACCELERATION)
        {
            float accLinearPower = (float) Math.sqrt(event.values[0]*event.values[0]+event.values[1]*event.values[1]+event.values[2]*event.values[2]);
            mIIRLPF0.iir(accLinearPower,false);
            _accLinearPower = mIIRLPF0.getValue();
        }
    }

# 投げ上げ検知の処理

下記のコードで加速度のノルムから投げ上げを検知しました。閾値は現物合わせでいい塩梅に決めました。投げ上げる瞬間の加速度が閾値を超えたら投げ上げ開始判定、その後、匙加減の時間が経過したときに頂点に達していると判断して、THETAのレリーズをするコードです。

    int cnt_maxacclin_down_slope = 0;
    float cnt_detection_interval = 0;
    int state=0;
    float accLinMax=0;
    float predicted_reach_time = 0.0f;

    private void throwDetect(float accLinearPower)
    {

        if(state ==0) {
            if (accLinearPower > 10.0) {
                state = 1;
                accLinMax = accLinearPower;
            }
        }
        else if(state==1){
            if(accLinMax<accLinearPower)accLinMax=accLinearPower;
            else
            {
                cnt_maxacclin_down_slope++;
                if(cnt_maxacclin_down_slope>5){
                    state=2;
                    cnt_maxacclin_down_slope = 0;
                    mActionDetectListener.onThrowDetected(accLinMax);
                    Log.d("Event","onThrowDetected");

                }
            }
        }
        else if(state==2) {
            predicted_reach_time = accLinMax/9.8f;
            state = 3;
            cnt_detection_interval=0;

            Log.d("Event",String.valueOf(predicted_reach_time));
        }
        else if(state==3) {

            cnt_detection_interval++;
            if(cnt_detection_interval>((predicted_reach_time*100.0f)-1000.0f))
            {
                cnt_detection_interval=0;
                state=4;
                mActionDetectListener.onThrowTopDetected();
                Log.d("Event","onThrowDetectionStarted");

            }
        }
        else {
            cnt_detection_interval++;
            if(cnt_detection_interval>((predicted_reach_time*100.0f)*3.0f))
            {
                cnt_detection_interval=0;
                state=0;
                accLinMax = 0;
                mActionDetectListener.onThrowDetectionPrepared();
                Log.d("Event","onThrowDetectionStarted");

            }

        }
    }

#実際の動作結果
自動でレリーズがかかり、頂点でシャッターが切れました！被写体さんの笑顔と紅葉を同時におさめることができました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922958/32d11ab5-7427-60cd-c4b1-34d4572016b1.png)
図3　撮影されたTHETA写真(360°の一部をいい感じにキャプチャしました)

THETAの写真は、スマホに表示させで、指でグリグリする時にインスパイアされますよねー。
![image_6483441 (1).JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922958/ee50cdba-a611-460e-1b2b-096de1b55964.jpeg)
図4　初めてのTHETA写真に驚いている澪さん

#最後に

今回はTHETAの加速度センサを使ってみましたが、ジャイロセンサや地磁気センサ(これはアンオフィシャル？)の値も取れたので、これらを活用すればもっと色んなことができそうだと思いました。投げ上げ検知のアルゴリズムは超簡易的なので、意図しないタイミングでレリーズされることもありましたが、それが意外と良い味のショットになっていたりしたので、アルゴの精度を追い込むより、楽しい撮影の時間を増やすほうが良いなと思いました。