この記事は 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は標高ですが、今回のアプリでは特に気にしません。
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.動作確認
撮ってるところ
wikitudeとリコーシータAPIで撮影自動実行の実験 pic.twitter.com/2NJQpme9Tr
— morio (@morio36) 2016年12月18日
撮れた写真
#感想
Wikitudeはビューの部分をHTML/JS/CSSで実装することになるので
多くのWebデザイナー、Web技術者にとって導入しやすいと考えます(私はどちらにも当てはまりませんが・・・)。
AR普及の波に乗るにはこうした技術も押さえておくとよさそうです。
Theta側、Wikitudeライブラリ側のログを参照できたらもっと開発がしやすくなるかな、と思いました。
エラー解析のためにChormeからJSON投げて反応見たりFiddler2で電文確認したりしたので・・・
#明日の記事
明日の記事は、@kamujunさんによる、機械学習についての何かです、お楽しみに!