第1回:自宅の足踏みをバーチャル観光に — コンセプトと歩行検出の実装

はじめに

2020年、新型コロナウイルスの影響で多くの方が自宅での生活を余儀なくされました。外出自粛が長期化するなかで、特に深刻だったのが運動不足外出できないストレスの2つではないでしょうか。

慢性的な疲労感に悩まされたり、観光・旅行・登山・散歩といったレジャーを楽しめなくなったり。日常の中で体を動かす機会と、外の世界を楽しむ機会が同時に失われていきました。

そこで今回、この2つのストレスを同時に解消するためのAndroidアプリを開発しました。コンセプトは、自宅で足踏み運動をしながら、Google Street Viewで世界中を歩く「バーチャル観光」 です。

本シリーズでは、このアプリの仕組みと実装を2回に分けて解説します。第1回では、コンセプトの紹介と、加速度センサーによる歩行検出の実装について詳しく説明します。


バーチャル観光とは

仕組みはシンプルです。

  1. スマートフォンを手に持つか、ポケットに入れた状態で、その場で足踏みをする
  2. スマートフォンのアプリが歩行の動きと歩数を検出する
  3. 検出した動きに連動して、TVに観光地のパノラマ映像(Google Street View)が投影される

システム概要:足踏みでStreet Viewが連動

TVに観光地の映像を映しながら足踏みをすると、まるで実際に観光地を歩いているような感覚を味わうことができます。運動不足の解消と、疑似的な外出体験を同時に実現する仕組みです。


必要なデバイスとアプリ

このアプリを使ったバーチャル観光には、以下のデバイスとアプリが必要です。

  • Androidスマートフォン — 加速度センサーとジャイロセンサーを搭載したもの
  • Google Chromecast — スマートフォンの画面をTVにミラーリングするデバイス
  • Google Home(アプリ) — Chromecastの画面キャストを制御するためのアプリ
  • TV — 家庭用の一般的なテレビ

アプリのコードは、google/simple-pedometergooglemaps/android-samples を参考にしています。


足踏み運動の効果

自宅でできる運動として、足踏み運動は非常に効果的な有酸素運動です。松本大学大学院の根本賢一教授が紹介している方法では、以下のポイントが重視されています。

太ももをゆっくりと高く上げる。

手の指先をしっかり伸ばし、腕を大きく後ろまで引くように振る。

実際にこの方法で5分ほど足踏みをしてみると、じわじわと汗がにじんでくるのを感じます。代謝が上がり、有酸素運動として十分な効果が得られます。

この動きをスマートフォンのセンサーで検出すれば、運動不足解消の効果をさらに高めることができるのではないか。そう考えたことが、このアプリの出発点でした。


バーチャル観光のツール:Google Street View

バーチャル観光のツールとして、Google Street Viewを利用します。360度のパノラマ画像が使われているため、画面越しでもリアルな観光体験が得られます。

例えば、富士山の登山道をStreet Viewで見ると、こちらのように臨場感のある景色を楽しむことができます。

ただし、通常のStreet Viewはマウスやタッチ操作(スワイプ、スクロール)が必要です。そのままでは足踏み運動と連動させることができません。歩行の動きや歩数に応じて画面が自動的に移動する仕組みを、新たに構築する必要があります。


アプリ開発の3つのポイント

このアプリを実現するために、以下の3つの技術的課題を解決する必要がありました。

  1. スマートフォンで歩行の動きと歩数を検出する — 加速度センサーを活用
  2. 左右への方向転換や見回す動きを検出する — ジャイロセンサーを活用
  3. 検出した動きに応じてGoogle Street Viewを操作する — Maps SDK for Androidとの連携

第1回では、このうち最初のポイントである歩行検出の仕組みを詳しく解説します。


加速度センサーによる歩行検出の実装

スマートフォンには、さまざまな動きを検出できるセンサーが搭載されています。その中でも歩行検出に使用するのが加速度センサーです。

一般的な歩数計アプリは、単純な振動ではなく「歩行の動き」だけを検出するように設計されています。軽い振動が歩行としてカウントされることはありません。

では、どのようにして歩行の動きだけを正確に検出するのでしょうか。以下の4つのステップに分けて解説します。

開発環境

  • Android Studio 3.6.3
  • Android 9.0
  • API Level 28
  • Latest Android Build Tools
  • Google Play services

A. 加速度センサーの3軸データ取得

加速度センサーは、以下の3つの軸の動きを検出します。

  • X軸 — 左右方向の動き
  • Y軸 — 上下方向の動き
  • Z軸 — 前後方向(奥行き)の動き

加速度センサーの3軸

歩行時のスマートフォンには、上下・左右・前後からさまざまな振動が伝わります。1つの軸だけの動きに限定されることはありません。この3軸の動きを組み合わせることで、歩行に近い動きを検出・カウントします。


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

歩行の動きを検出するには、X軸・Y軸・Z軸の動きを総合的に捉える必要があります。

実際にスマートフォンをポケットに入れて歩いてみると、上下・左右・前後のすべての方向から振動が伝わることがわかります。どれか1つの軸だけが反応するわけではありません。

そこで、XYZ各軸の加速度(以下「3軸ベクトル」)のどの軸がどの程度反応しているかを判定するために、3軸ベクトルの合計値を計算します。合計値は以下の数式で求められます。

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

この値が大きいほど、スマートフォンに伝わる振動が大きいことを意味します。


C. ノイズ除去

加速度センサーは非常に敏感で、わずかな振動にも反応します。すべての微小な振動を検出してしまうと、歩数を正確に計測することができません。

このような不要な振動をノイズと呼び、除去する処理が必要になります。ノイズ除去には、主に以下の2つの方法があります。

ローパスフィルタ

以下の数式を用いてノイズを除去する方法です。

Y(t)=aY(t1)+(1a)x(t)Y(t) = a \cdot Y(t-1) + (1 - a) \cdot x(t)

ここで、YY はフィルタ後の値、xx は生のセンサーデータ、tt は時間軸、aa は定数です。一般的によく使われる手法ですが、今回は次に紹介する方法を採用しました。

n回ごとの平均値によるスレッショルド判定

こちらが今回採用した方法です。3軸ベクトルの合計値の平均をn回ごとに算出し、その平均値が一定のスレッショルド(閾値)を超えない場合はノイズとして除去します。

nの値(加速度センサーが反応する回数)が大きいほど、ノイズを拾いにくくなります。算出された平均値がスレッショルドを超えた場合に、歩行の1歩としてカウントされます。

今回のアプリでは、運動不足解消に効果的な「しっかりとした動き」を検出したいため、スレッショルドを高めに設定しています。軽い振動ではカウントされず、太ももを上げてしっかり足踏みした場合にのみ歩数が加算される仕組みです。


D. ディレイ(間隔制御)

最後に、ディレイについて説明します。

歩行の動作には、1歩をカウントしてから次の1歩をカウントするまでに、一定の時間間隔があります。この間隔を考慮しないと、1回の足踏みで複数歩がカウントされてしまう可能性があります。

ディレイのロジックでは、1歩を検出した後、一定時間が経過するまでは振動に反応しないように実装されています。これにより、歩行のリズムに合った正確な歩数カウントが実現します。


コードの解説

それでは、実際のコードを見ていきましょう。歩数計のプログラムは、google/simple-pedometer を参考にしています。

SimplePedometerActivity.java

まず、加速度センサーのインスタンスを取得する部分です。

@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);

次に、アプリのライフサイクルに応じたセンサーの登録・解除と、センサーデータの処理です。

  • onPause — アプリがバックグラウンドに移行した際の処理です。バッテリーの消耗を防ぐため、加速度センサーの登録を解除します。
  • onResume — アプリがバックグラウンドから復帰した際の処理です。加速度センサーを再登録します。
  • onSensorChanged — 加速度センサーが反応した際の処理です。simpleStepDetector.updateAccel を呼び出して、歩行の動きと歩数を判定します。
@Override
public void onPause() {
  super.onPause();
  sensorManager.unregisterListener(this);
}
@Override
public void onResume() {
  super.onResume();
  numSteps = 0;
  textView.setText(TEXT_NUM_STEPS + numSteps);
  sensorManager.registerListener(this, accel, 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]);
  }
}

SimpleStepDetector.java

歩行検出のコアロジックです。先ほど解説したA〜Dのステップが、このコード内に実装されています。

public void updateAccel(long timeNs, float x, float y, float z) {
    float[] currentAccel = new float[3];
    currentAccel[0] = x;
    currentAccel[1] = y;
    currentAccel[2] = z;
    // First step is to update our guess of where the global z vector is.
    accelRingCounter++;
    accelRingX[accelRingCounter % ACCEL_RING_SIZE] = currentAccel[0];
    accelRingY[accelRingCounter % ACCEL_RING_SIZE] = currentAccel[1];
    accelRingZ[accelRingCounter % ACCEL_RING_SIZE] = currentAccel[2];
    // point A
    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);
    // point B
    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);
    // point C
    if (velocityEstimate > STEP_THRESHOLD && oldVelocityEstimate <= STEP_THRESHOLD
            && (timeNs - lastStepTimeNs > STEP_DELAY_NS)) {
        listener.step(timeNs);
        lastStepTimeNs = timeNs;
    }
    oldVelocityEstimate = velocityEstimate;

}

コード内の各ポイントを解説します。

Point A — ここがノイズ除去のためのn回ごとの平均値算出です。3軸ベクトルの合計ではなく、各軸ごとの加速度を個別に平均化しています。リングバッファ(ACCEL_RING_SIZE)を使うことで、直近n回分のデータのみを対象にした移動平均を実現しています。

Point B — ここで3軸ベクトルの合計値を計算しています。正規化した重力方向ベクトルと現在の加速度データの内積を取り、重力成分を差し引くことで、歩行による加速度成分だけを抽出しています。

Point C — ここでベクトルの合計値がスレッショルドを超えているかどうかを判定しています。ディレイも考慮されており、STEP_DELAY_NS の時間が経過するまでは次の歩数をカウントしません。STEP_THRESHOLD は、運動不足解消に効果的な大きめの動きを検出するために高く設定されています。

このコードでは、加速度データを正確に処理するためのさまざまな手法が組み合わされており、歩行の動きと歩数を精度よく検出できるようになっています。

歩数カウント

最後に、歩行の動きを検出した際に歩数をカウントする処理です。

@Override
public void step(long timeNs) {
    // count walking step
    numSteps++;

}

歩行が検出されるたびに numSteps がインクリメントされ、画面に表示される歩数が更新されます。


まとめと次回の予告

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

次回の第2回では、ジャイロセンサーによる方向転換の検出と、Google Street Viewとの連携実装について解説します。さらに、Chromecastを使ったTVへの投影方法と、実際のデモの様子もご紹介します。


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

この記事をシェア