16
5

More than 5 years have passed since last update.

Wikitudeを使用して特定の場所に近づいたタイミングでTheta自動撮影

Posted at

この記事は Tech-Circle Hands on Advent Calendar 2016 の 12/22の記事です。
昨日は、@enterprise1024さんによる The second step to become a Divine-Excelist でした。
That's EXCELlent!!

概要

タイトルの通り、ARライブラリであるWikitudeを使用し
リコーThetaで自動撮影を行います。

簡単に言うと
・アプリを起動する
・GPSを利用して目的地(今回は藤沢駅とした)との距離を計測
・一定の距離まで近づいたらThetaのAPI利用により「撮影せよ」のリクエストを送信
・撮影
になります。
Wikitude自体、リコーTheta自体の説明は下記参照でお願いします。

Wikitudeについて
Theta自体について
Theta APIについて

環境

端末:SONY Experia SO-04G
カメラ:リコーTheta S(アプリバージョン1.11.1)、Theta v2 SDK (Android v0.1.0)
IDE:Android Studio 2.2.2
ARライブラリ:Wikitude SDK 5.3.0

全体の流れ

大きく、下記のステップを踏みました。

1.Wikitudeサンプル動作確認
2.Wikitudeアプリ開発トライアル
3.Theta API確認
4.実装

1. Wikitudeサンプル動作確認

 下記のページからWikitudeアカウントを作成し、SDKをダウンロード。

 https://wikitude.grapecity.com/downloads/wikitudesdk

 \wikitude-sdk-android-530\Examples\SDKExamples\apk\WikitudeSDKSamples.apk 
 を自前のAndroidにデプロイし、一通りのサンプルを確認しました。


2. Wikitudeアプリ開発トライアル

 下記のページを参考に、WikitudeSDKの概要把握と
 修正のための勘所確認を行いました。

 位置情報と連動したAR! Wikitudeで「ロケーションベース型AR」Androidアプリを作ってみよう

 Wikitude以前に、私はUnityでのモバイル開発しかしてこなかったので
 Android Studioの扱い自体にかなり苦労しました・・・
 Gradleのバージョンなどによっては、何度もUPDATEが必要となります。気長にエラーを排除しましょう。

 ちなみにWikitude公式アナウンスに、「ロケーション型ARはUnityで開発できません」と載っています(マーカー型なら可能)。

 http://www.wikitude.com/developer/developer-forum/-/message_boards/message/894402

Hello Kaan,
Unfortunately you cannot have POIs with Unity 3D. Therefore, if you want to have POIs with your app I would suggest to try SDK JS.

 ※POIとは、Point Of Interestsのことであり、位置情報を使ったアプリ全般のことを言ってます。

 補足:
 「位置情報と連動したAR! Wikitudeで「ロケーションベース型AR」Androidアプリを作ってみよう」
 のサンプルを実装していて詰まった点などを列挙します。
 もし同事象が発生した場合の参考としてください。
  ・2ページ目に「レイアウトエディタの[Palette]から[CustomView]を選択し、「ArchtectView (com.wikitude.archtect)」を
   ダブルクリック」や「レイアウトエディタの[Canvas]に表示されている画面に」といった記載がありますが、
   私の環境ではこれらを見つけられず。。
   仕方なく手順をSKIPして進めましたが、結果的にうまく動いたので
   執筆時点の環境との違いなのかと思料。
  ・2ページ目 リスト4 activity_main.xmlの定義 において、tools:contextのパスは
   自分が作成したプロジェクトに合わせて変更が必要です。
  ・5ページ目で、特に説明は書かれていませんが、「ダウンロード サンプルファイル (14.0 MB)」から
   ダウンロードしたJavaを使用しないと、4ページまでに修正したコードでは記述が不足しており、コンパイルエラーとなるようです。
  ・さらに、MainActivity.javaでコンパイルエラーが発生しました。
   151行目の

            if ( accuracy < SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM && MainActivity.this != null && !MainActivity.this.isFinishing() && System.currentTimeMillis() - MainActivity.this.lastCalibrationToastShownTimeMillis > 5 * 1000) {
                Toast.makeText( MainActivity.this, R.string.compass_accuracy_low, Toast.LENGTH_LONG ).show();
                MainActivity.this.lastCalibrationToastShownTimeMillis = System.currentTimeMillis();
            }

   の部分で、R.string.compass_accuracy_lowのシンボルが解決できない、というエラーです。
   確かに、自動生成されたR.javaにはstring.compass_accuracy_lowという変数は定義されていませんでした。
   さんざん迷った挙句、適当なStringをリテラルで直接セットして回避できてしまいました・・


3. Theta API確認

 ThetaのAPIやリクエストの送信方法などを確認しました。

 ThetaにWifiで繋げてJSON飛ばすだけなので、使い勝手は良いです。

 参考記事としては下記がわかりやすかったです。すべて2015年のアドベントカレンダー RICOH THETA Advent Calendar 2015 からです。

 THETA Sをブラウザから操作し、無音で写真を撮る
 RICOH THETA S API in several languages.
 MESHをTHETA Sのリモートシャッターにしてみた

 私の場合、そもそものjqueryすら怪しいので相当苦労しましたが
 これを読んでいる皆さんはきっと大丈夫でしょう。


4. 実装

 2で動作確認したサンプルプロジェクトを拡張・修正していく形で実装を行いました。
 もし元のプロジェクトを残したい場合は、新規プロジェクトにWikitudeSDKの設定と
 各種Activityの設定が必要となります。

実装手順

上記「全体の流れ」1~3を実施済みの前提で記載します。

1.目的地の定義を変更する

サンプルプログラムでは、multiplepois.js内requestDataFromLocalの中で
今いる地点を中心に20箇所のPOIを生成し、アプリ上表示するようになっています。
ここの定義を書き換えて、目的地とする藤沢駅の座標をセットしました。
(目的地は別にどこでも好きな場所でいいです)。

藤沢駅の緯度、経度をググってlongitudeに経度、latitudeに緯度を設定します。
あとの文字列は自由でOKです。
altitudeは標高ですが、今回のアプリでは特に気にしません。

multiplepois.js
    requestDataFromLocal: function requestDataFromLocalFn(centerPointLatitude, centerPointLongitude) {

        var poiData = [];
        poiData.push({
            "id": (1),
            "longitude": (139.487222), //藤沢駅の経度
            "latitude": (35.338889), //藤沢駅の緯度
            "description": ("This is the description of POI#1"),
            "altitude": "100.0",
            "name": ("Fujisawa Station")
        });
        World.loadPoisFromJsonData(poiData);
    },

2.目的地までの距離を計算するロジックを追加する

今いる地点から目的地までの距離を計算するロジックが必要です。
これはサンプルアプリには無いので、別のところを参考にしましょう。
iPhone向け拡張現実アプリの開発に挑戦してみた(Wikitude活用)
記事はiOS向けですが、ロジック部分は参考になります。
ファンクションgetDistanceを下記のように定義します。

getDistance: function (targetLatitude, centerPointLatitude, targetLongtitude, centerPointLongitude) {
    // 参考:http://www.movable-type.co.uk/scripts/latlong.html
    var Δφ = (centerPointLatitude - targetLatitude) * Math.PI / 180;
    var Δλ = (centerPointLongitude - targetLongtitude) * Math.PI / 180;
    var a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + Math.cos(targetLatitude * Math.PI / 180) * Math.cos(centerPointLatitude * Math.PI / 180) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
    var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return 6371e3 * c
}

3.目的地の一定距離まで近づいたときに発動するロジックを追加する

2で定義したファンクションをこまめに呼び出し、一定距離まで近づいたら後続処理へ進むよう、
下記のメソッドを追加しました。

// 表示されているARメッセージの距離表示を更新します。
updateDistanceValues: function (centerPointLatitude, centerPointLongitude, centerPointAltitude, centerPointAccuracy) {

    var distance = World.getDistance(World.markerList[0].poiData.latitude, centerPointLatitude, World.markerList[0].poiData.longitude, centerPointLongitude);
    var distanceString = (distance > 999) ? ((distance / 1000).toFixed(2) + " km") : (Math.round(distance) + " m");

    //撮影済みの場合は何もしない
    if (World.didTakePicture) {
        return;
    }

    World.updateStatusMessage("あと"+distanceString);

    //目的地までの直線距離が50メートル超の場合は何もしない
    //目的地までの距離が50メートル以下の場合は後続の撮影処理を実行
    if (distance > 3000.0) {
        return;
    }
~~後略~~

追加した元は下記の部分です。

ファンクション:locationChanged内

    if (!World.initiallyLoadedData) {
        /* 
            requestDataFromLocal with the geo information as parameters (latitude, longitude) creates different poi data to a random location in the user's vicinity.
        */
        World.requestDataFromLocal(lat, lon);
        World.initiallyLoadedData = true;
    } else {
        // 対象地点からの距離情報を更新し、条件と満たしていたら撮影
        World.updateDistanceValues(lat, lon, alt, acc);
    }

locationChangedはActivityが場所の移動を通知する都度呼び出されるファンクションです。
その中に初回ロードか否かを判定するロジックがあったので、elseブロックにupdateDistanceValuesを追記しました。

4.Theta APIを呼び出すロジックを追加する

今回は藤沢駅まで50メートルの距離に近づいたらThetaで撮影するようにしました。
下記のように定義します。

// 表示されているARメッセージの距離表示を更新します。
updateDistanceValues: function (centerPointLatitude, centerPointLongitude, centerPointAltitude, centerPointAccuracy) {

    var distance = World.getDistance(World.markerList[0].poiData.latitude, centerPointLatitude, World.markerList[0].poiData.longitude, centerPointLongitude);
    var distanceString = (distance > 999) ? ((distance / 1000).toFixed(2) + " km") : (Math.round(distance) + " m");

    //撮影済みの場合は何もしない
    if (World.didTakePicture) {
        return;
    }

    World.updateStatusMessage("あと"+distanceString);

    //目的地までの直線距離が50メートル超の場合は何もしない
    //目的地までの距離が50メートル以下の場合は後続の撮影処理を実行
    if (distance > 50.0) {
        return;
    }

    World.didTakePicture = true; //一度撮影したら二回目を撮らないためのフラグ

    //Thetaのセッションを取得
    var JSONdata = {
        "name":"camera.startSession",
        "parameters":{}
    };
    $.ajax({
        type:"post",                // method = "POST"
        url:"http://192.168.1.1/osc/commands/execute",        // POST送信先のURL
        data:JSON.stringify(JSONdata),  // JSONデータ本体
        contentType: 'application/json', // リクエストの Content-Type
        dataType: "json",           // レスポンスをJSONとしてパースする
        success: function(json_data) {   // 200 OK時
            var id = json_data.results.sessionId;

            //無事にセッションを取得できたら撮影処理実行
            World.takePicture(id);
        },
        error: function() {         // HTTPエラー時
            alert("Server Error. Pleasy try again later.");
            World.updateStatusMessage("error");
        },
        complete: function() {      // 成功・失敗に関わらず通信が終了した際の処理
            World.closeThetaSession(id);
        }
    });
  },

// Theta撮影
takePicture: function (sessionId) {

    var JSONdata = {
        "name":"camera.takePicture",
        "parameters":{"sessionId":sessionId}
    };
    $.ajax({
        type:"post",
        url:"http://192.168.1.1/osc/commands/execute",
        data:JSON.stringify(JSONdata),  // JSONデータ本体
        contentType: 'application/json',
        dataType: "json",           // レスポンスをJSONとしてパースする
        success: function(json_data) {   // 200 OK時
            World.updateStatusMessage("Done!");
        },
        error: function() {         // HTTPエラー時
            alert("Server Error. Pleasy try again later.");
            World.updateStatusMessage("error");
        },
        complete: function() {      // 成功・失敗に関わらず通信が終了した際の処理
            World.closeThetaSession(id);
        }
    });
},

// Thetaセッションのクローズ
closeThetaSession: function (sessionId) {
    var JSONdata = {
        "name":"camera.closeSession",
        "parameters":{"sessionId":sessionId}
    };
    $.ajax({
        type:"post",
        url:"http://192.168.1.1/osc/commands/execute",
        data:JSON.stringify(JSONdata),  // JSONデータ本体
        contentType: 'application/json',
        dataType: "json",           // レスポンスをJSONとしてパースする
    });
},

これでdistanceが50以下になった場合に後続処理へ進み、
撮影用のJSONリクエスト送信処理を行うことになります。
50メートル以内にいる限りピュンピュン撮り続けてしまうので
一回撮影したら撮影済みフラグdidTakePicture を立てておくことにします。

5.動作確認

撮ってるところ


撮れた写真

1.png

感想

Wikitudeはビューの部分をHTML/JS/CSSで実装することになるので
多くのWebデザイナー、Web技術者にとって導入しやすいと考えます(私はどちらにも当てはまりませんが・・・)。
AR普及の波に乗るにはこうした技術も押さえておくとよさそうです。

Theta側、Wikitudeライブラリ側のログを参照できたらもっと開発がしやすくなるかな、と思いました。
エラー解析のためにChormeからJSON投げて反応見たりFiddler2で電文確認したりしたので・・・

明日の記事

明日の記事は、@kamujunさんによる、機械学習についての何かです、お楽しみに!

16
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
5