第2回:ジャイロセンサーとStreet View連携、そしてデモ

前回の振り返り

第1回では、バーチャル観光アプリのコンセプトと、加速度センサーによる歩行検出の仕組みを解説しました。3軸の加速度データからノイズを除去し、スレッショルド判定とディレイ制御を組み合わせることで、足踏みの動きを正確にカウントできる仕組みを実装しました。

第2回では、残りの2つの技術的課題を解決します。

  1. ジャイロセンサーによる方向転換・見回しの検出
  2. Google Street Viewとの連携実装

さらに、Chromecastを使ったTVへの投影方法と、実際のデモの様子もご紹介します。


ジャイロセンサーによる方向転換の検出

加速度センサーだけでは、前方への歩行しか検出できません。左右への方向転換や、周囲を見回す動きを検出するには、ジャイロセンサーを使用します。ジャイロセンサーは、回転と向きを検出するセンサーです。

ジャイロセンサーを活用することで、スマートフォンを回転させて左右に方向転換したり、周囲を見回す動き(360度の回転)を検出できるようになります。


A. ジャイロセンサーの3軸回転データ

ジャイロセンサーは、3つの軸を中心とした回転を検出します。

X軸を中心とした回転(前後の傾き)

X軸回転

Y軸を中心とした回転(左右の回転)

Y軸回転

Z軸を中心とした回転(水平方向の回転)

Z軸回転

今回のアプリでは、スマートフォンを縦に持った状態で回転させたとき(Y軸を中心とした回転)を、左右の方向転換や見回しの動きとして判定します。


B. 3軸ベクトルの合計値計算

加速度センサーと同様に、ジャイロセンサーでも3軸の回転の合計値を計算します。3軸ベクトルの合計値は、以下の数式で求められます。

magnitude=x2+y2+z2\text{magnitude} = \sqrt{x^2 + y^2 + z^2}

C. ノイズ除去

加速度センサーと同じく、n回ごとの平均値(3軸ベクトルの平均値)がスレッショルドを超えているかどうかを判定することで、ノイズを除去します。

ノイズ除去の詳細な仕組みについては、第1回の解説をご参照ください。


D. ディレイ

加速度センサーと同様に、ディレイを考慮しています。1回の方向転換を検出した後、一定時間が経過するまでは次の回転に反応しないようにすることで、意図しない連続検出を防ぎます。


方向判定:左右の検出

ジャイロセンサーの重要なポイントは、回転の方向を判定できることです。

スマートフォンを縦に持った状態でY軸を中心に回転させたとき、以下のように判定します。

  • 反時計回り(Y軸の回転値 > 0)への方向転換
  • 時計回り(Y軸の回転値 < 0)への方向転換

同じ方向に回転を続けることで、周囲を見回す動きも実現できます。


コードの解説

ジャイロセンサーのソースコードは google/simple-pedometer には存在しないため、加速度センサーの実装を参考に独自に作成しました。基本的な考え方は加速度センサーと同じです。

SimplePedometerActivity.java(ジャイロセンサー追加版)

まず、SimplePedometerActivity.java にジャイロセンサーを追加します。onResume でジャイロセンサーを登録し、onSensorChanged でジャイロセンサーが反応した際に LotateDetector.updateGyro を呼び出して、方向転換の動きを検出します。

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  textView = new TextView(this);
  textView.setTextSize(30);
  setContentView(textView);
  // Get an instance of the SensorManager
  sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
  accel = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
  simpleStepDetector = new SimpleStepDetector();
  simpleStepDetector.registerListener(this);
  // Add gyro sensor
  gyro = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
  simpleLotateDetector = new SimpleLotateDetector();
  simpleLotateDetector .registerListener(this);
}
@Override
public void onResume() {
  super.onResume();
  numSteps = 0;
  textView.setText(TEXT_NUM_STEPS + numSteps);
  sensorManager.registerListener(this, accel, SensorManager.SENSOR_DELAY_FASTEST);
  // register gyro
  sensorManager.registerListener(this, gyro, SensorManager.SENSOR_DELAY_FASTEST);
}
@Override
public void onSensorChanged(SensorEvent event) {
  if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
    simpleStepDetector.updateAccel(
        event.timestamp, event.values[0], event.values[1], event.values[2]);
  }
  if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) {
    simpleLotateDetector.updateGyro(
        event.timestamp, event.values[0], event.values[1], event.values[2]);
  }

}

加速度センサーと同じ onSensorChanged メソッド内で、センサーのタイプに応じて処理を分岐させています。

LotateDetector.java

方向転換の検出を行うクラスです。加速度センサーの SimpleStepDetector.java と同じ構造ですが、左右の方向判定ロジックが追加されています。

package com.example.myapplication;
import android.util.Log;
public class LotateDetector {
    private static final int ACCEL_RING_SIZE = 50;
    private static final int VEL_RING_SIZE = 10;
    // change this threshold according to your sensitivity preferences
    private static final float STEP_THRESHOLD = 80f;
    // 1 sec
    private static final int STEP_DELAY_NS = 1000000000;
    private int accelRingCounter = 0;
    private float[] accelRingX = new float[ACCEL_RING_SIZE];
    private float[] accelRingY = new float[ACCEL_RING_SIZE];
    private float[] accelRingZ = new float[ACCEL_RING_SIZE];
    private int velRingCounter = 0;
    private float[] velRing = new float[VEL_RING_SIZE];
    private long lastStepTimeNs = 0;
    private float oldVelocityEstimate = 0;
    private StepListener listener;
    public void registerListener(StepListener listener) {
        this.listener = listener;
    }

    public static final String TYPE_RIGHT = "right";
    public static final String TYPE_LEFT = "left";
    // 1 sec
    private static final int LOTATE_DELAY_NS = 1000000000;
    // fire lotate power
    private static final float LOTATE_POWER = 2.0f;
    private static final float LOTATE_POWER_OTHER = 3.0f;
    private String type = TYPE_LEFT;
    public void updateGyro(long timeNs, float x, float y, float z) {
        float[] currentAccel = new float[3];
        currentAccel[0] = x;
        currentAccel[1] = y;
        currentAccel[2] = z;
        accelRingCounter++;
        accelRingX[accelRingCounter % ACCEL_RING_SIZE] = currentAccel[0];
        accelRingY[accelRingCounter % ACCEL_RING_SIZE] = currentAccel[1];
        accelRingZ[accelRingCounter % ACCEL_RING_SIZE] = currentAccel[2];
        float[] worldZ = new float[3];
        worldZ[0] = SensorFilter.sum(accelRingX) / Math.min(accelRingCounter, ACCEL_RING_SIZE);
        worldZ[1] = SensorFilter.sum(accelRingY) / Math.min(accelRingCounter, ACCEL_RING_SIZE);
        worldZ[2] = SensorFilter.sum(accelRingZ) / Math.min(accelRingCounter, ACCEL_RING_SIZE);
        float normalization_factor = SensorFilter.norm(worldZ);
        worldZ[0] = worldZ[0] / normalization_factor;
        worldZ[1] = worldZ[1] / normalization_factor;
        worldZ[2] = worldZ[2] / normalization_factor;
        float currentZ = SensorFilter.dot(worldZ, currentAccel) - normalization_factor;
        velRingCounter++;
        velRing[velRingCounter % VEL_RING_SIZE] = currentZ;
        float velocityEstimate = SensorFilter.sum(velRing);
        float absVelocityEstimate = Math.abs(velocityEstimate);
        if (absVelocityEstimate > STEP_THRESHOLD && oldVelocityEstimate <= STEP_THRESHOLD
                && (timeNs - lastStepTimeNs > STEP_DELAY_NS)) {
            // check if lotate is right or left
            type = (y > 0) ? TYPE_LEFT : TYPE_RIGHT ;
            listener.lotate(timeNs, type);
            lastStepTimeNs = timeNs;
        }
        oldVelocityEstimate = velocityEstimate;
    }

}

加速度センサーとの大きな違いは、方向判定のロジックです。velocityEstimate の絶対値(absVelocityEstimate)がスレッショルドを超えた場合に、Y軸の回転値(y)の正負を確認します。

  • y > 0 の場合 → TYPE_LEFT(左方向)
  • y <= 0 の場合 → TYPE_RIGHT(右方向)

これにより、スマートフォンを反時計回りに回転させると「左」、時計回りに回転させると「右」と判定されます。

方向転換が検出された際のコールバック処理は以下のとおりです。

@Override
public void lotate(long timeNs, String type) {
    // call lotating street view

}

この中で、後述するStreet Viewの回転処理を呼び出します。


Google Street Viewとの連携実装

ここからは、スマートフォンで検出した動きをGoogle Street Viewと連動させるコードについて解説します。

A. Maps SDK for Android APIの有効化

Google Street ViewをAndroid上で動作させるには、Google Cloud Console で Maps SDK for Android の API を有効にする必要があります。

B. Google Street Viewの呼び出し

アプリからGoogle Street Viewを呼び出します。サンプルソースが公開されていますので、それに沿って解説します。

まず、取得したAPIキーをプログラムに追加します。手順は android-samples のREADME に記載されています。

  1. ApiDemos/java ディレクトリに secure.properties ファイルを作成する(APIキーを保護するため、バージョン管理には含めない)
  2. secure.propertiesMAPS_API_KEY=YOUR_API_KEY の1行を追加する
  3. ビルドして実行する

その後、以下のコードでGoogle Street Viewを呼び出します。

@Override
protected void onCreate(final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.street_view_panorama_navigation_demo);
    SupportStreetViewPanoramaFragment streetViewPanoramaFragment =
            (SupportStreetViewPanoramaFragment)
                    getSupportFragmentManager().findFragmentById(R.id.streetviewpanorama);
    streetViewPanoramaFragment.getStreetViewPanoramaAsync(
            new OnStreetViewPanoramaReadyCallback() {
                @Override
                public void onStreetViewPanoramaReady(StreetViewPanorama panorama) {
                    mStreetViewPanorama = panorama;
                    // Only set the panorama to SYDNEY on startup (when no panoramas have been
                    // loaded which is when the savedInstanceState is null).
                    if (savedInstanceState == null) {
                        mStreetViewPanorama.setPosition(SYDNEY);
                    }
                }
            });
   mCustomDurationBar = (SeekBar) findViewById(R.id.duration_bar);

}

StreetViewPanoramaFragment を使って、指定した座標のStreet View画像を表示します。初期表示ではシドニーが設定されていますが、任意の座標に変更可能です。

C. 矢印方向に直進する処理

Google Street Viewの画面には矢印が表示されており、その方向に進むことができます。以下のコードは、カメラが向いている方向に最も近い矢印を探し、その方向に前進する処理です。

public void onMovePosition(View view) {
    StreetViewPanoramaLocation location = mStreetViewPanorama.getLocation();
    StreetViewPanoramaCamera camera = mStreetViewPanorama.getPanoramaCamera();
    if (location != null && location.links != null) {
        StreetViewPanoramaLink link = findClosestLinkToBearing(location.links, camera.bearing);
        mStreetViewPanorama.setPosition(link.panoId);
    }

}

歩行を検出した際にこのコードを歩数に応じて呼び出すことで、足踏みに連動してStreet View内を前方に移動させることができます。

Street View画面

D. 左右の回転(bearing調整)

Street Viewでは分岐点に複数の矢印が表示されることがあります。直進以外の方向に進みたい場合は、まず方向転換をする必要があります。

方向転換の際には、回転方向と回転量を検出し、その情報をStreet Viewの bearing(方位角)に反映させます。左を向くときはbearingをマイナス方向に、右を向くときはプラス方向に調整します。

public void onPanLeft(View view) {
    if (!checkReady()) {
        return;
    }
    mStreetViewPanorama.animateTo(
            new StreetViewPanoramaCamera.Builder().zoom(
                    mStreetViewPanorama.getPanoramaCamera().zoom)
                    .tilt(mStreetViewPanorama.getPanoramaCamera().tilt)
                    .bearing(mStreetViewPanorama.getPanoramaCamera().bearing - PAN_BY_DEG)
                    .build(), getDuration());

}
public void onPanRight(View view) {
    if (!checkReady()) {
        return;
    }
    mStreetViewPanorama.animateTo(
            new StreetViewPanoramaCamera.Builder().zoom(
                    mStreetViewPanorama.getPanoramaCamera().zoom)
                    .tilt(mStreetViewPanorama.getPanoramaCamera().tilt)
                    .bearing(mStreetViewPanorama.getPanoramaCamera().bearing + PAN_BY_DEG)
                    .build(), getDuration());

}

方向転換の後に歩行すると、向いている方向の矢印に向かって前進します。このプログラムをジャイロセンサーと組み合わせることで、スマートフォンをY軸中心に回転させるだけで左右の方向転換や360度の見回しが実現できます。


E. 横向き画面の固定

TVに投影する際に画面のアスペクト比を合わせるため、スマートフォンの画面を横向きに固定します。縦画面のままChromecastでTVにキャストすると、TV画面の比率と合わず表示が崩れてしまうためです。

AndroidManifest.xmlandroid:screenOrientation="landscape" を設定します。

<activity
   android:name=".MainActivity"
   android:label="maps"
   android:screenOrientation="landscape">
   <intent-filter>
       <action android:name="android.intent.action.MAIN" />
       <category android:name="android.intent.category.LAUNCHER" />
   </intent-filter>

</activity>

これでコードの実装は完了です。


ChromecastとGoogle HomeによるTV投影

ここまでで、足踏み運動とGoogle Street Viewをスマートフォンアプリ上で連動させることができました。

次に、Chromecast と Google Home アプリを使って、スマートフォンの画面をTVに投影します。Google Home アプリの「画面をキャスト」機能を使うことで、スマートフォンの画面をそのままTVに映し出すことができます。


デモ:実際にバーチャル観光を体験

すべての準備が整いましたので、実際に試してみましょう。

使い方

  1. Google Homeアプリで、スマートフォンの画面をTVにキャストする
  2. アプリを起動し、アプリ画面がTVに映し出されていることを確認する
  3. スマートフォンを手に持つか、ズボンのポケットに入れて、TVの前で「足踏み」をする

足踏みの際は、以下のポイントを意識してください。

太ももをゆっくりと高く上げる。手の指先をしっかり伸ばし、腕を大きく後ろまで引くように振る。

TVの前で足踏み運動

歩行連動

スマートフォンが歩行の動きを検出すると、TV画面のStreet Viewも歩行に連動して前進します。

歩行に連動するTV画面

方向転換

方向を変えたいときは、足踏みを止めてスマートフォンを回転させます。左に曲がりたい場合は反時計回りに、右に曲がりたい場合は時計回りに回転させます。

スマホを回転させて方向転換

TV画面も回転に連動して、向きが変わります。同じ方向に回転を続けると、周囲を見回すことができます。行きたい方向を向いたら、再び足踏みをして前進します。

回転に連動するTV画面


振り返りと学んだこと

このプロジェクトを通じて、いくつかの重要な学びがありました。

センサーデータの処理は奥が深い。 加速度センサーとジャイロセンサーの生データはノイズが多く、そのまま使うことはできません。リングバッファによる移動平均、スレッショルド判定、ディレイ制御など、複数のフィルタリング手法を組み合わせることで、初めて実用的な精度が得られました。

既存のOSSを活用する重要性。 Googleが公開している simple-pedometerandroid-samples を起点にすることで、基本的な仕組みを素早く理解し、独自の拡張(ジャイロセンサーの追加、Street Viewとの連携)に集中できました。

物理的な体験と組み合わせる価値。 画面を眺めるだけのアプリと比べて、実際に体を動かすことでStreet Viewが連動する体験は、没入感が格段に異なります。身体的な運動とデジタルコンテンツを組み合わせることの可能性を実感しました。

コロナ禍という制約の中で生まれたアプリですが、「制約が創造を生む」という経験は、その後のプロジェクトにも通じる大切な教訓になりました。

最後までお読みいただき、ありがとうございました。


バーチャル観光シリーズ:

この記事をシェア