地磁気で屋内測位をやってみた(サービスとして使えるようにしたとは言っていない)

この記事はAkerun Advent Calendar 7日目の記事です。

こんにちわ、はじめまして、11月にフォトシンスに入社した nbs です。 Webシステムの開発を担当しております。まだ、入社したばかりですが、ゆくゆくは機械学習を用いた新サービスとか作りたいです。

Akerunは入退室が正確に管理できるサービスですので、入室しているのか、退室しているのかをきっちり、かっきり確認できます。

ただ、室内のどこにいるのかが知りたい場合もありますよねということで、 本アドベントカレンダーではスマホアプリでの 屋内測位 にチャレンジしたいと考えています。

特別な端末を必要とせず、スマホで人の屋内の位置がわかったら素敵やん。

ということで、後で理由は記載しますが、採用した手法は地磁気での屋内測位です。地磁気のデータを元に位置情報をベイズ推定したのですが、精度がでなかったため、 逐次モンテカルロ法 を用いて精度向上を図りました。

目次:

思ったよりも長い記事になってしまったので、先結(=先に結論を書きましょうの略)でいきます。それなりの結果になりました!

  • 精度は頑張ればそれなりに出る(概ね1m)
  • 導入時は費用と時間はそれなりにかかる
  • 精度を出すためにアプリを起動し続ける必要がある
  • 精度を出すために使用者ごとの学習が必要(涙目)
  • 基準点があると更に良い感じになる

地磁気測位、君に決めた!

屋内測位の手法はかなりの数があります、詳しく知りたい方はGoogleで検索していただくと、色々な比較記事風の広告が出てくると思います。

今回、地磁気測位を選んだ理由として以下2点を上げさせていただきます。

  • 他方式に比べて、特別なデバイスが不要であること
  • 記事にした際に、そこそこ盛り上がる精度は出るのではないかという期待感(PDR: 歩行者自立航法測位も興味あるが、精度出すのシンドイ)

注)駅や倉庫の屋内測位で地磁気測位を用いようとした際に、鉄道車両や大型車両が近くを運行すると地磁気が乱れるという弱点があります、そのため、駅や物流拠点の測位には向きません。他の測位手法にもそれぞれ弱点がありますので、実際に導入する際には、しっかりとした検討をお勧めします。

iPhone地磁気と回転と

地磁気とはWikipediaによると

地磁気(ちじき、英: geomagnetism、Earth's magnetic field)は、地球が持つ磁性(磁気)である。及び、地磁気は、地球により生じる磁場(磁界)である。磁場は、空間の各点で向きと大きさを持つ物理量(ベクトル場)である。

そう、地磁気とは地球の磁場です。地磁気測位は、磁場をセンシングしてるんですね。磁場って、建物の鉄骨に影響うけるんです。詳しくは参考サイトをご覧ください。簡単にいうと、 磁場によって、微量ですが鉄骨が磁化し、その磁化した鉄骨で磁場が発生するっていう。電場と同じ現象が起こっています。ちなみに先述の電車やトラックが近くを通ると磁場が乱れる理由もわかりやすく解説されています。

参考サイト:地磁気観測所|基礎知識|Q&A

さて、地磁気による屋内測位いけるやんと思った方。 そもそも、iPhoneに、何故、地磁気センサが搭載されているのでしょうか。 そう、iPhoneの向きを把握するためですね。iPhoneの向きが変わると、センサの値が変わります。

おっと、全く回転させずに歩いて移動できるのかと思った方。 正解です、無理です。意識して歩いても無理。 ではどうするのかということですが、 1つの方法は、ジャイロセンサからどう回転しているのかを計算して、そこから地磁気を計算するという方法。 今回やってないですが、恐らく、こちらでも頑張れば何とかなるでしょう。

ただ、今回はお手軽に実装したいと考えたので、iPhone側の機能に依存しようと思います。

さて、iPhoneで取得可能な地磁気の値に3つの種類があるのはご存知でしょうか。

参考サイト:In iOS, what is the difference between the Magnetic Field values from the Core Location and Core Motion frameworks? - Stack Overflow

上記サイトの引用の訳ですが

  1. CMMagnetometer 磁力計からの生の読み取り
  2. CMDeviceMotion(CMCalibratedMagneticField *)magneticField デバイスバイアス(オンボード磁場)に対して補正された磁力計の読み取り値
  3. CLHeading [x | y | z] 装置のバイアスに対して補正され、局所的な外部磁場を除去するためにフィルタリングされた磁力計の読み取り値(装置の動きによって検出されるように – フィールドが装置と共に移動する場合はそれを無視し、そうでなければそれを測定する)

地磁気のデータに関して、iOS側で様々な処理をかけていることがわかります。2はiPhone内部の磁場の影響の除去、3の局所的な外部磁場というのに、建物の鉄骨による影響の除去も含まれますね。2と3の差って、局所的な外部磁場の影響の除外ですから、この差って、回転によらないはずですよね。

地磁気測位をするために、BLE端末を買った orz

屋内測位で地磁気を用いる方式を簡単に説明しますと、

  1. 位置(x, y)と地磁気(gx, gy, gz)を結びつけて記録する
  2. 地磁気(gx, gy, gz)から位置(x, y)をベイズ推定する

こうやって、書くとめっちゃ簡単! ですが、実際には、最初に位置と地磁気を正確に紐づけることがとてもとても重要です。それを怠ると、そもそも望んでいる結果が得られません。

簡単に位置を正確に測位するという方法を考えると、BLEビーコン測位が頭に浮かびました。

地磁気による屋内測位のための地磁気マップを作成するために、BLEビーコン測位を行う。訳がわからないよ、初めからBLE測位で良いじゃんと思った方、私も一瞬そう思いました。ですが、これは最初だけの苦しみなのです。 私が正確な地磁気マップを作成すれば、後続の方が使うときにはBLE端末を取り除いても良いのです。

つまり、最初、地磁気マップを作るのにBLE端末がいるが、その後、そのBLE端末は別の場所の地磁気マップを作るのにも使えるということですね。買いきりしてもらう必要はなく、導入時に貸し出すだけで良い。お金の匂いがしてきましたね!

前置きが長い、コードを寄越せ

ええ、私もそう思います。ですが、コードがすっごい長くなったので、気が向いたらGitで公開します。

以下、抜粋

    
    var trackingCLLocationManager:CLLocationManager!
    var beaconRegion:CLBeaconRegion!
    var cmManager:CMMotionManager!

    // 設置したビーコンのUUID。iOSで検出可能な上限数は20個程度のため、
    // 測位エリアをいくつかに分割し、計測を起こっていく必要がある
    let UUIDList = [...]

    override func viewDidLoad() {
        super.viewDidLoad()

        // 表示部分の処理、省略

        // ロケーションマネージャを作成し、delegateを自身に設定
        trackingCLLocationManager = CLLocationManager()
        trackingCLLocationManager.delegate = self

        // 取得設定の初期化(精度最大、頻度設定1mごと)
        trackingCLLocationManager.desiredAccuracy = kCLLocationAccuracyBest
        trackingCLLocationManager.distanceFilter = 1

        // セキュリティ認証のステータスを取得
        let status = CLLocationManager.authorizationStatus()
        // まだ認証が得られていない場合は、認証ダイアログを表示
        if(status == CLAuthorizationStatus.notDetermined) {
            trackLocationManager.requestWhenInUseAuthorization()
        }

        // CLLocationManagerの地磁気測位開始
        trackingCLLocationManager.startUpdatingHeading()
    }

    /*
     iBeaconの検出を開始する.
     */
    private func startMonitoring() {
        // UUIDListから対象設定
        for i in 0 ..< UUIDList.count {
            // BeaconのUUIDを設定.
            let uuid: NSUUID! = NSUUID(uuidString: "\(UUIDList[i].lowercased())")
            let identifierStr: String = "ps_\(i)"

            // リージョンを作成.
            beaconRegion = CLBeaconRegion(proximityUUID: uuid as UUID, identifier: identifierStr)

            // リージョンへの入場, 退場通知の設定.debug時はtrueオススメ
            beaconRegion.notifyOnEntry = false
            beaconRegion.notifyOnExit = false

            // モニタリング開始
            trackingCLLocationManager.startMonitoring(for: beaconRegion)
        }
    }

    /*
     認証ダイアログで許可された場合、モニタリングを開始、拒否された場合、何もしない
     */
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        switch (status) {
        case .authorizedAlways:
            startMonitoring()
        case .authorizedWhenInUse:
            startMonitoring()
        }
    }

    /*
     モニタリング開始時にリージョンに入っているか確認する
     リージョンに入った出たの変化をイベントとして感知するので、モニタリング開始時の状態を確認する必要がある
     */
    func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) {
        // リージョン内かどうかの確認
        manager.requestState(for: region);
    }

    /*
     リージョン内にiBeaconが存在するかどうか
     */
    func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) {
        switch (state) {
        case .inside:
            // リージョン内にiBeaconが存在いるのでRangingを開始
            // 他のステータスでは何もしない
            manager.startRangingBeacons(in: region as! CLBeaconRegion)
        }
    }

    /*
     現在取得しているiBeacon情報一覧を取得、処理
     */
    func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
        if(beacons.count > 0){
            // 位置計算に回す分のビーコンの連想配列
            var targetBeacons: [String: Double] = [:]

            for_beacon: for i in 0 ..< beacons.count {
                let beacon = beacons[i]

                // Proximity unknowのデータは使用しない
                switch (beacon.proximity) {
                case CLProximity.unknown :
                    continue for_beacon
                }

                // 距離計算
                let distanse = pow(10.0, (beacon.power - beacon.rssi) / 20.0)

                targetBeacons[beacon.uuid] = distanse
            }

            // targetBeaconsの要素数に応じて、位置計算処理に回す
            // 位置計算処理は省略

            // 地磁気の取得
        }
    }

    /*
     ビーコン検出時、Ranging開始
     */
    func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
        manager.startRangingBeacons(in: region as! CLBeaconRegion)
    }

    /*
     ビーコンを見失って時間が経ったとき、Ranging停止
     */
    func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
        manager.stopRangingBeacons(in: region as! CLBeaconRegion)
    }

    /*
     地磁気の角度更新時
     */
    func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
        // 今回はビーコン検出をトリガーとして、組み合わせデータを作成した
        print(-newHeading.magneticHeading)
    }
 
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

上記のように、ビーコン側の検知をトリガーに地磁気を取得して、ビーコン側から計算できる位置と地磁気の差分を記録しました。

地磁気マップを保持して、ベイズ推定をしたのですが、この時点でそこそこ精度が出ていました。ですが、ときたま、大きく位置がずれることがありました。

ふと気づいたのですが、地磁気の値はユニークとは限らないんですよね。 (x, y) => (gx, gy, gz) を記録して、逆引きした際、(gx, gy, gz) => (x1, y1) or (x2, y2)になることもあるわけです。

そこで考えました。精度を上げるために、逐次モンテカルロ法を用いようと。

逐次モンテカルロ法を使おう、ただし、時系列ではない

さて、逐次モンテカルロ法、知ってる人は知っているし、知らない人は知らないですよね。詳細はGoogle先生にお願いするとして、簡単に説明します。 当初、用いていたベイズ推定は、ある特定の状態のみを切り取って値を推定するのに対して、逐次モンテカルロ法は、連続する状態の推定を過去の観測データも用いながら推定します。

ちょっぴり内容が難しくなってきましたね。なんで、そんなことをしたいのかというと、実際に人が動くときは、連続した状態で変化をしていきますよね。入り口にいた人が急に反対の壁に瞬間移動することはなく、入り口から、徐々に反対の壁まで移動していき、その状態も観測できる訳です。今回でいうと、観測するのは地磁気ですから、入り口の地磁気から、内側の地磁気、反対の壁の地磁気と観測されるのが正しい訳です。現状態から起こりえない突然の事象を除外できるっていうことですね。

元々は、[A - Z]のどの地点にいるかを地磁気から推定していたわけですが、元々Aにいたとしたら、全ての地点に行ける訳ではなくBに行くか、Cに行くか、Aに留まるかしかないとイメージしてみてください。その状況なら地磁気の観測値からAにいるのかBにいるのかCにいるのかを推定すれば良い訳です。分かりやすいように具体例をあげたはずなのに、分かりづらくなるとはこれいかに。わからなかったよっていう人はお父さんに聞いてみてください。きっと困ると思います。

さて、ここで問題なのが、よくあるパターンだと、状態の推移に関して、時間軸でどう変化するのかを用います、時系列データですね。ですが、実際の人の動きの場合、停止もある訳です。オフィスなんかだと、長時間座りっぱもありますよね。私もよくその状態にあります。

今回は、移動の歩数による連続状態を考えてみました。iPhoneだと歩数が簡単に取得できます。歩数ごとの地磁気の変化から歩数ごとの位置の変化を推定したわけです。

逐次モンテカルロ法の辺り、Swiftで実装してもよかったのですが、ありものを使おうと言うことで、逐次モンテカルロ法を用いた推定部分はサーバサイドのPythonで行いました。そのため、全てのコードを載せるのもしんどく少し地磁気を観測してから位置を推定されるまで、時間差があります。約4秒ほどね。

実際にサービスとして提供するためには、エッジ側での処理を検討した方が良さそうですね。

まとめ

先に書いた通り、個人的にはそれなりの結果だなと思います。勉強になることも多く、精度も当初予定通りに強引に納めたので満足です。ただ、即サービス化するのは色々難しいですし。割とハードル高いですね。

  • 精度は頑張ればそれなりに出る(概ね1m) → これ以上の精度を求めるには、地磁気マップ作成時の位置測位をより正確に行える方法が必要。

  • 導入時は費用と時間はそれなりにかかる → iBeaconを20個買って、4,000円くらいでした。後ほど、これらiBeaconはスタッフが別用途で美味しく食べました(使いました)。また、地磁気マップ作成に思ったより時間がかかりました。今回、自分の家で試してオフィスにトライする予定だったのですが、観測エリアの広さに、地磁気マップの作成、学習にかかる時間が比例するとすると、20日以上かかることになりますね。(自宅で地磁気マップ作成に4日も要しました)

  • 精度を出すためにアプリを起動し続ける必要がある → 逐次モンテカルロ法を用いて精度を上げたので、連続したデータが必要でございます。

  • 精度を出すために使用者ごとの学習が必要(涙目) → 逐次モンテカルロ法における状態の推移を歩数軸にしたので、個人差が出ますね。ただ、自分で実験した限り、時間より歩数の方が安定して推定できました。

  • 基準点があると更に良い感じになる → 実は、今回の記事の出発点は、基準点にAkerunって最適じゃないかしら?!です。大真面目に、NFCで扉を開けるときは位置が決まっていて、ほぼ速度0ですから基準点としては最適ですね。もうちょい導入時のハードルが低かったら嬉々として提案したのに。。

また、先に書かなかったところですと、オフィスでの利用を考えた場合、地磁気測位では垂直方向が推定できないので、今、どこの階にいるのかを別の方法で推定する必要があります。フロアの区切りが階によって異なる場合(多くの場合そうだと思いますが)どの階にいるかわからないと、逐次モンテカルロ法によって、余計誤差が出るようになりますね(笑)

時間があれば、加速度センサから今何階か判別したかったのですが、階段で上がった場合の加速度の波形を見て今回は諦めました。ちなみにエレベーターで上昇するパターンは、事前学習は当然必要ですが、どうにかなりそうです。

ということで、引き続き、開発していきたいと思います。機会があれば、第2弾ということで。ニーズの範囲にコストを納めるのって、本当に大変。

・・・

株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 recruit.jobcan.jp

Akerun Proの購入はこちらから akerun.com