crouton on chromebook でポート開放方法

crouton on chromebookのポート開放で悩んだのでメモしておく。


cronton上にhttpsサーバ立てて、動作確認することが多いのですが、ポート開放のやり方がよくわからず今まではchromebookスマホをUSBケーブルで接続してポートフォワードしていた。
いちいちケーブル接続して、ポートフォワード設定してが面倒になってきたのと、正月休みが重なったので、重い腰を上げて調べてみた。

最初はubuntuなどでポート開放する要領でファイルに記載するのかと思って、/etc/sysconfig/iptablesとか探したけどファイル自体がない。なんでないかよくわからないけどserviceコマンドすらない。さてどうしようかとググってたらchromiumOSでポート開放している記載がある記事を発見

qiita.com

iptablesで直接追加できるようだ。
でもこれって、動的だから毎回入力しないと行けないんだろうなぁ・・・

追加方法

INBOUND TCP:8080を追加してみた

追加する前の一覧

chronos@localhost /etc $ sudo iptables -nL
Chain INPUT (policy DROP)
target     prot opt source               destination         
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:34191
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:44369
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:40089
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:39177
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:36233
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:37658
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:43905
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:45392
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            state RELATED,ESTABLISHED
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           
ACCEPT     icmp --  0.0.0.0/0            0.0.0.0/0           
ACCEPT     udp  --  0.0.0.0/0            224.0.0.251          udp dpt:5353
ACCEPT     udp  --  0.0.0.0/0            239.255.255.250      udp dpt:1900

Chain FORWARD (policy DROP)
target     prot opt source               destination         
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            mark match 0x1
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            state RELATED,ESTABLISHED
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           

Chain OUTPUT (policy DROP)
target     prot opt source               destination         
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            state NEW,RELATED,ESTABLISHED
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           

追加コマンド

sudo iptables -I INPUT -p tcp --dport 8080 -j ACCEPT

追加したあとの一覧

chronos@localhost /etc $ sudo iptables -nL
Chain INPUT (policy DROP)
target     prot opt source               destination         
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:8080
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:34191
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:44369
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:40089
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:39177
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:36233
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:37658
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:43905
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp dpt:45392
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            state RELATED,ESTABLISHED
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           
ACCEPT     icmp --  0.0.0.0/0            0.0.0.0/0           
ACCEPT     udp  --  0.0.0.0/0            224.0.0.251          udp dpt:5353
ACCEPT     udp  --  0.0.0.0/0            239.255.255.250      udp dpt:1900

Chain FORWARD (policy DROP)
target     prot opt source               destination         
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            mark match 0x1
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            state RELATED,ESTABLISHED
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           

Chain OUTPUT (policy DROP)
target     prot opt source               destination         
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            state NEW,RELATED,ESTABLISHED
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           

Chain INPUT に「tcp dpt:8080」 が追加されていますね。

よしよし

8thwallweb アカウント作成方法

今年は8thWallWebを使ってWebARの作品をいくつか作ってみましたが、来年は8thWallWebのWebARハンズオンをする予定なので、初学者向けはじめの一歩的な情報をまとめとこうと思い、ブログを書いています。

8thWallWebとは?

iOS/Androidどちらでも動作するクロスプラットフォームでWebARを提供する、Javascriptライブラリです。

8th Wall - Products

上のリンクから動画および実際に動作するWebアプリがありますので、一度お試しください。

作ったもの

昔からつくりたかったのですが、WebARのマイクラ的なものを作ってみました。8thwallにもtweetしていただき、ちょっと盛り上がりました。

スマートフォンで下記URLにアクセスすると遊べます。

CraftBlocks by @sgidon: 8th Wall Web: A-FRAME

アカウント作成

8thwallwebを利用するには、8thwallの無料アカウントが必要となります。上で紹介した8thwallwebのページからアクセスできます。

f:id:sgi-don:20181231174625p:plain

アクセスすると以下のような画面となります。各項目へ入力していきます。
Initial Workspace Typeは "Web Developer" を選択しておきましょう。

f:id:sgi-don:20181231175206p:plain

"Get Started"を選択後、一番下まで移動して"I Agree"を選択します。

f:id:sgi-don:20181231183159p:plain

"I Agree" を押したあとは電話番号認証です。"Japan(日本)"を選択して携帯電話番号を入力します。0から始まる11桁を入力し、"Verify via SMS"を選択します。
先頭に+81をつけたり、0を外さなくても良さそうです。

f:id:sgi-don:20181231194744p:plain

SMSで通知された6桁の番号を入力して"Verify"を選択します。

f:id:sgi-don:20181231194536p:plain

おめでとうございます。これでアカウント作成されました。
コンソール画面からAPI KEYの発行などが可能となります。

f:id:sgi-don:20181231194554p:plain

登録したメールアドレスに、チュートリアルなどの情報が送られます。
もう一度コンソール画面へアクセスするには、以下のURLからアクセスしましょう。

8th Wall Console

以上

deeplearn.jsのplaygroundで遊んでみる。SqueezeNetのソース理解

最近、おやつをガムに変えてみたところ、1か月で4キロほど痩せてしまった。
いままでどんだけおやつ食べてたんだと反省。

deeplearn.jsの勉強にと、とりあえず簡単そうなところから入ってみる。

playground

環境設定とかで躓きたくないので、とりあえずデモのPlayGroundで遊んでみる。
ここだと、サーバとか用意せず書いたコードをそのまま動かすことができるようだ。

deeplearnjs.org

SqueezeNet

playgroupndのリンクに「SqueezeNet - basic usage」とあったので、これ元にいじくって遊んでみよう。 その前に、SqueezeNetってなんだ?

https://www.semiconportal.com/archive/contribution/applications/170418-neurochip5-2.html

うーん、わからん。
わかったのは、ネットワークモデルの一種だということと、正確性より処理応答性を重視していることくらい。 使う分にはわからなくてもよいので、とりあえず表面をサラッと理解してみる。

ソースの理解

javascriptのソースを少しづつ理解していこう。

画像情報の読み込み

const catImage = document.getElementById('cat');

HTMLのIMGタグの画像情報を取り込んでいる。右下の猫の画像がそれ。
なんか細長い猫だなと思ったら、元画像は 240x217 で、IMGタグ内で 227x227 に変更している。 なんでやねんって、IMGタグのwidth x heightを240x217に変更して[RUN]を実行したらエラーになった。

Error: The output # of columns (119.5) must be an integer. Change the stride and/or zero pad parameters

どうやらSqueezeNetに読み込ませる画像は227x227出ないといけないようだ。

SqueezeNetのロード処理

const math = new dl.NDArrayMathGPU();
// squeezenet is loaded from https://unpkg.com/deeplearn-squeezenet
const squeezeNet = new squeezenet.SqueezeNet(math);
await squeezeNet.load();

SqueezeNetの何かを読み込んでいるらしい。
いきなり「dl」と出てきているが、これはplaygroundがデフォルトでdeeplean.jsを読み込んでいるため使えている。(だと思う)。 次の「NDArrayMathGPU()」は多次元配列をGPUで処理するものという理解(であっているのか?)

以下にdeeplearn.jsのチュートリアルを日本語訳されたサイトがあった。とても参考になる。

http://tensorflow.classcat.com/2017/08/20/deeplearn-js-tutorials-introduction/

イメージデータの変換

// Load the image into an NDArray from the HTMLImageElement.
const image = dl.Array3D.fromPixels(catImage);

何か変換しているのはわかるが、なんだろうこれ?よくわからないので「console.log()」で吐き出す。

console.log(catImage);
 ↓
[object HTMLImageElement]
console.log(image)
 ↓
Tensor dtype: int32 rank: 3 shape: [227,227,3] values: [[[255, 255, 255], [255, 255, 255], [255, 255, 255], ..., [255, 255, 255], [255, 255, 255], [255, 255, 255]], [[255, 255, 255], [255, 255, 255], [255, 255, 255], ..., [255, 255, 255], [255, 255, 255], [255, 255, 255]], [[255, 255, 255], [255, 255, 255], [255, 255, 255], ..., [255, 255, 255], [255, 255, 255], [255, 255, 255]], ... [[169, 193, 180], [165, 193, 178], [161, 196, 176], ..., [40 , 39 , 35 ], [55 , 51 , 48 ], [37 , 34 , 29 ]], [[150, 188, 173], [152, 190, 175], [158, 192, 175], ..., [27 , 26 , 22 ], [33 , 29 , 26 ], [30 , 27 , 22 ]], [[157, 194, 187], [164, 197, 186], [165, 198, 181], ..., [35 , 34 , 30 ], [49 , 44 , 40 ], [38 , 30 , 27 ]]]

IMGタグの画像情報をint32の3次元配列[227,227,3]に変換しているようだ。 画像が227x227なので、3はRGB情報だろう。

推論

// Predict through SqueezeNet.
const logits = squeezeNet.predict(image);

[Predict:予測] なので、たぶん推論しているんだろうな。 logitsって何だろう。

console.log(logits)
 ↓
Tensor dtype: float32 rank: 1 shape: [1000] values: [14.1952286, 12.3713379, 13.3236885, ..., 1.7310563, 17.3527832, 17.3972549]

float32の1次元配列[1000]が入っていた。よくわからない。

結果表示

// Convert the logits to a map of class to probability of the class.
const topClassesToProbs = await squeezeNet.getTopKClasses(logits, 10);
for (const className in topClassesToProbs) {
  console.log(
      `${topClassesToProbs[className].toFixed(5)}: ${className}`);
}

推論結果を出力。
squeezeNet.getTopKClassesの前にawaitが入っている。非同期処理を想定しているってことはそれなりに重い処理なのか?
想像するに、SqueezeNetには1000の答えがあり、上で求めた「logits」には、各答えの正解率が入っているのではないか?
「squeezeNet.getTopKClasses(logits, 10)」でそのうち上位10件を切り出している。そんなところだろう。

よくわからないこと

HTML部に

<script src='https://unpkg.com/deeplearn-squeezenet'></script>

とあるが、これはなんだ?削っても問題なく動くし。
アクセスするとよくわからないJavascriptソースが下りてきた。

まとめ

一番簡単そうなソースで遊んでみましたが、DeepLearningの知識がほとんどない自分には突っかかりまくりで大変でした。このソースを使って応用する場合、画像サイズは227x227にそろえることを覚えておこう。

以上

GoogleTango + DeepLearnJS + WebARで物体認識

寒くてバイクに3か月乗っていなかったら、案の定バッテリーがあがってた。
近くのバイク屋さんまで汗だくになりながら押して持っていき、見事復活!
今年はいろいろなところに行って写真を撮りまくろうと思っています。

今回作ったのは、スマホで物体認識した結果をARで表示するWebアプリです。一度作ってたのですが、AI部分(物体認識部)をクラウドを利用していたため、レスポンスが遅いなぁと感じていました。

こんなのです。

youtu.be

この時は、GCE(n1-highmem-8[vCPU x 8、メモリ 52 GB])上にTensorFlowを配置してぶん回したのですがこのレスポンス。遅い要因としては、

  • 解析画像リサイズしていない
  • ネットワーク転送
  • GCEでGPUを使っていない。

これらを改善した版を今回作ってみました。

youtu.be

だいぶ早くなっていますね。
大きく変わったところは2点。

  • 取得した画像をリサイズして解析している。
  • 物体検知をクラウド上ではなくブラウザ上で行っている。

使っている技術

多種多様な技術を使っています。
(Web業界って使う技術が多すぎてついていけない・・・)

GoogleTango

モーショントラッキング、エリアラーニング、Depthの取得をスマホでできるという画期的なデバイス
広角レンズと赤外線カメラで前方4メーター四方の物体の位置を認識することができます。
バイスASUS ZenfoneARをつかっています。

developers.google.com

残念ながら今の時代に早すぎたようで、ARCoreに置き換わるべく2018年03月でプロジェクト終了・・・

DeeplearnJS

Googleが提供しているJavascriptで動作するDeeplearning環境。WebGL経由でGPGPUを使用しており思った以上に高速に動作します。

deeplearnjs.org

WebAR

Web上でARを実現する手段。

github.com

A-Frame

WebでVR/ARを実装する際、タグだけで実装できちゃうライブラリ。

aframe.io

加えて、WebARを簡単に実装できるA-frameコンポーネントを使用しています。

github.com

使い方

以下に公開しています。残念ながらWebAR対応機種でかつWebAR専用ブラウザでないと動作しないです。

whispering-salesman.glitch.me

真ん中のカーソルが緑となっている状態で検知可能です。ARCore/ARKitの場合はまずは床面検知しないと緑にならないかと思います。
検知可能状態(緑)で画面タッチすると物体検知を実行し、解析結果をARで表示します。
端末をシェイクすると、AR表示を全削除します。

実装説明

ソースはGithubに公開しています。

github.com

index.html

HEADER部、BODY部、SCRIPT部に分けてポイントを説明します。

HEADER

A-Frameライブラリを一部修正しています。そのままではcanvasから画像取得ができなかったためです。
以下に詳細を記載したページがあります。

mementoo.info

BODY

a-assets内のmixinはAR表示Entityのテンプレートです。
AR表示EntityはtextGroupのEntity配下に追加登録しています。
raycasterとmarkについては、上で紹介したAFrame-ARを参考にしてください。
inference-canvasはよくわかりません。物体検知の実装をDeepLearnJSのWebCam Imagenetから取得した際の名残です。多分BODY句になくても動作する。

SCRIPT

「./dist/bundle.js」を読み込みます。ここに他のJS(DeeplearnJSとか、main.js)が入っています。
raycasterについては、上で紹介したAFrame-ARを参考にしてください。
shakeについては、Shake.jsを参考にしてください。

main.js

物体検知実装はDeepLearnJSのimagenetを持ってきただけですので、そちらを参考にするのがよいかと思います。

async function ready()

各種データ準備。

window.addEventListener('click', (env) => ...

画面表示しているCanvasとは別のCanvasを用意してリサイズ処理を行っています。
squeezeNetに食わせる画像のサイズがなぜか227x227固定だったので(それ以外だとエラーとなる)ため、227x227のResizeCanvasになっています。

function detection()

  1. リサイズ後画像の物体検知
  2. 解析結果から物体名の取得
  3. AR表示

の流れです。正直物体検知のところはサンプルそのまま持ってきているのでよくわかりません。
AR表示部は、A-FrameのTextコンポーネントを使っています。それにグルグル回るアニメーションをつけたうえで、index.htmlのBODY句にあったtextGroupエンティティに追加していきます。

まとめ

「[センシング/IoT → AI解析 → AR/VR表示] on WEB on モバイル」という流れは今後主流になると勝手に思っていますので、今回この流れで実装できたのでかなりテンション上がっています。
AIについてはまだまだ勉強が必要ですが、学習済みモデルを利用するだけなら難しくないですね。DeepLearnJSはTensorFlowの学習済みモデルをインポートできるようですので、どんどん活用していこうと思います。

以上です。

 

 

OpenCV.jsでFace Detectionを試してみた。

こんにちは。
腰を痛めて家で療養中です。あまりにも暇なので、以前から気になっていたOpenCVについて調べてみました。 んで調べてみると、OpenCV.jsなるものがあるんですね。
「ブラウザで動くじゃん、モバイルでも動くかな?」
OpenCVの勉強をするつもりが、主目的がOpenCV.jsをモバイルブラウザで動かすことに代わってしまいました。あるあるですね。
というわけで、そんな勉強がてらOpenCVのFace Detectionをブラウザで動かしてみました。

作ったもの

こんなのです。
youtu.be

もう、どうしようもないくらいカックカクですね。
ASUS ZenfoneAR(Snapdragon 821)なのでそんなにスペック悪くないですけどねー。
PC(Lenovo X260)側だとぬるぬるなので、現時点でのスマホの限界なのでしょうね。
Web Assembly化するともうちょとましになるのかな。暇になったら対応してみよう。

あと、画像がぼやけているせいか、小さな顔は認識しませんでした。寄ると認識します。

作業手順

手順というか、参考にしたサイトの羅列です。

まずここ

qiita.com

そしてそこで紹介されているここ

OpenCV: Build OpenCV.js

emsdkのインストールはここがわかりやすかった

Developer’s Guide - WebAssembly

「./emsdk install」実行後、ものすごく時間がかかるので覚悟することw

そのあと手順通りに実行していくと、
python ./platforms/js/build_js.py build_js」
を実行した後で、opencv.jsが出来上がる。
このコマンドもものすごく時間がかかります。

できたOpencv.jsを使って、チュートリアルで一番簡単そうなのを実装してみた。

OpenCV: Changing Colorspaces

問題なく動いたので、次に本題のFace Detectionです。

OpenCV: Face Detection in Video Capture

これも問題なく動いたので、あとはHTMLとかそこいらへんをいじって全画面化しています。

まとめ

Javascriptは遅い、スマホのは特にって言われているのは知っていましたが、VRとかARとか普通に動くし問題ないんじゃ?ってどこかで思っていましたが、OpenCVを動かしてみたら、カックカクだし「あー、これはだめだぁ」って思いました。
asm.jsやWebAssemblyはこれを解決しようとがんばっているのですね。一人で納得

明らかに重い処理でなければ、ブラウザ上でOpenCVが動くということは様々な使い道があるのではと感じています。
今まで画像を扱う場合、クラウド上にファイルをそのまま送信し下処理の上AIに食わせるという流れでしたが、スマホ上で下処理および軽量化したうえで送信できるので、サーバ負荷が少なくなるでしょう。サーバ側はAI処理に集中できるので良いことだと思いました。

以上

A-FrameでPositionを設定するときの書き方について

新年があけまして、今年こそは少しスリムになろうと運動を始めました。
去年も、一昨年もそうでした。果たしていつまで続くやら。

A-Frameで実装する際、PositionやらRotationやらを設定することが多いのですが、毎回書き方がバラバラで、好き勝手に書いているのが気になっています。
いい機会なので、どういうときにどういう書きかたになるのか整理してみようかと。

Positionの設定方法

entityにposition設定をする際は、大きく分けて

  1. HTML中での設定
  2. Javascript中での設定

に分かれます。

HTML中での設定

HTMLでの設定する際は考える余地もなく

<a-entity position="x y z"></a-entity>

です。
HTMLでは静的に一意に設定します。なので設定方法もこのやり方だけになります。

Javascript中での設定

こっちが面倒です。設定の方法がいくつもあります。

  1. DOMの要素として属性に設定する。
  2. Three.jsのメソッドを使って設定する。
  3. A-FrameのUtilsを使って設定する。

他にもあるかな。基本的に「DOMの要素として設定」することが多いです。
というかほかの方法はよくわかっていないので・・・
なので、これを機にそれ以外の設定方法をまとめておきます。

DOMの要素として設定

setAttributeを使います。
スペース区切りでXYZの順で文字列を作成して設定します。

var box = document.getElementById("box");

// 文字列で設定
box.setAttribute("position", "x y z")

// 連想配列形式で設定
box.setAttribute("position", {x:-1, y:-1, z:-3});

// Vector3形式データも設定できる。
var pos = new THREE.Vector3(-1, 1.5, -3);
box.setAttribute("position", pos);

単純にXYZの値を変更する際はこれが一番楽です。
ですが、例えばYだけを変更する場合、一度文字列加工したうえで設定しなければならず、とても面倒で煩雑なソースになってしまいます。

ということで、知りたいのはこれから下のThree.jsとA-Frameの部分について。
ブログを書きながら調べながら、実装して確認しながらまとめていきます。

Three.jsのメソッドを使って設定

Three.jsでpositionは3つの数値項目を保有するVector3クラスを使って表します。
Vector3の説明は以下を参照。

three.js / documentation

したがって、あるエンティティのpositionを変更したい場合、エンティティからVector3のposition情報を取得し、そのposition情報が保有するVector3のメソッドを使用して変更します。
A-Frameエンティティのpositionは、entity.object3D.positionで取得できます。

var box = document.getElementById("box");
box.object3D.position.set(-1, 2.5, -3);

設定する値がすでにVector3の場合は、こんな書きかたもできます。

var box = document.getElementById("box");
var pos = new THREE.Vector3(-1, 1.5, -3);
box.object3D.position.copy(pos);

それから、XYZのうち一部だけ変更したい場合はこんな感じ

var box = document.getElementById("box");
// メソッドを使って設定
box.object3D.position.setY(2);
// もしくはプロパティ直指定
box.object3D.position.y = 2;

A-FrameのUtilsを使って設定

A-Frameには便利なUtilityがいくつか用意されています。

Utils – A-Frame

そのうち、position設定で使えるのは、AFRAME.utils.entity.setComponentProperty です。

var box = document.getElementById("box");
AFRAME.utils.entity.setComponentProperty(box, 'position', "-1 -2 -3");
// 設定値としては上記文字列以外に、連想配列とVector3も設定可能です。

長いですね。こんな長いの打ちたくないですね。
なのでposition設定で普通はこのUtilityは使わないでしょう。
この関数は他の設定において有用なのですが、それは別の機会に。

まとめ

今までなんとなく使っていたのですが、今回まとめることによりThree.js側で使うべきか、DOM設定として使うべきかなど自分の中で使用用途に応じて切り替えることができそうです。
今まで毎回悩んでいたのが馬鹿らしくなりました。

こういうノウハウや経験則で培ってきたものってDocumentには載っていないものです。自分の知識を整理する意味でも、できるだけブログにアップしていこうと思います。

以上

MercariARハッカソンに参加してきた

こんにちは。最近週末は東京、福岡、岡山と飛び回っていることが多く、部屋がちーとも片付かないで困っています。乾燥機じゃなくて天日で布団干したいです。

MercariAR ハッカソン

さてさて、先週「MercariARハッカソン」に参加してきました。

vrtokyo.connpass.com

ハッカソン自体あまり経験がない上に、東京で、しかも最新技術のARで、たった一日で。。。
我ながら無謀なチャレンジだったなぁと。

一人では心細いし、その場で即興チームを組んでみました。そしたらなんとWebARやらA-Frameやらを知っている人(レア)。しかも同じ福岡の人!! とても心強い。

ハッカソンの様子とかは相方さんのブログがよく書かれているのでそちらをどうぞー

bibinbaleo.hatenablog.com

なので僕の方は、作ったアプリの紹介と実装について書いてみようと思います。
10時間突貫でつくりっぱソースなので、汚いのはご容赦願います。。。

作ったもの

まずはアイデア出しから始めたのですが、どうにも苦手でして。相方さんとうーんうーんと悩んでひねり出して、早一時間。ふと、
「メルカリってことで、商品紹介文をARで表示したら面白そうだね。」
「録画していろいろな角度から見せられたら説明の説得力が上がるね。」
ってことで、商品に吹き出し的なものをARで表示してみようとなりました。

作ったもの  → https://daffodil-bell.glitch.me/

f:id:sgi-don:20171219232551p:plain

専用ブラウザが必要なので、お使いになる際は以下を参考にセットアップ願います。

ソースコード

github.com

使った技術

相方さんには申し訳なかったですけど、私はUnityが使えない人間なのでWebARで頑張らせてもらいました。

WebARonARCore/ARKit/Tango をベースとして、フレームワークにA-Frameを使用。開発環境及び動作環境にglitch.comを使いました。A-Frameのコンポーネントとして、

を使っています。

環境設定からaframe-arの使い方については、吉永さん(師匠(非公認(多分怒られる)))のブログが参考になります。

tks-yoshinaga.hatenablog.com

WebARが使える環境になったところで、具体的に実装したところの説明をしていきます。

実装箇所

やってることは大したことなく

  1. 床面を認識してマーカーを表示する。
  2. 始点を選択する。
  3. 終点を選択する。
  4. コメントを入力して表示する。

順に説明していきます。

床面を認識してマーカーを表示する

index.htmlのL149~L173が実装部分です。

    // リングの位置を更新する。
    ar_raycaster.addEventListener('raycaster-intersection', function(evt) {
      var pos = evt.detail.intersections[0].point; 
      mark.setAttribute('color', 'green');
      if (line) {
        // 線描画中
        var box = document.querySelector("#sample");
        var boxPos = box.object3D.getWorldPosition();
        mark.setAttribute("position", boxPos.x+" "+boxPos.y+" "+boxPos.z);
        AFRAME.utils.entity.setComponentProperty(line, 'line.end', boxPos.x+" "+boxPos.y+" "+boxPos.z);        

      } else {
        // 線描画中以外
        mark.setAttribute('position', pos); 
      }
      
    });

    // トラッキングが外れたらマーカーを赤くする。
    ar_raycaster.addEventListener('raycaster-intersection-cleared', function (evt) {
      if (!line) {
        // Turn the mark red.  FIXME: lerp position
        mark.setAttribute('color', 'red');
      }
    });

2つのイベント内にマーカーの処理を書いていきます。

raycaster-intersection

認識した床面とraycasterが交わる座標をとってきて、マーカーの位置としています。これで床面を這うようにマーカーが移動します。線描画中は、スマホの位置・向きを基に、マーカーの位置を取得しています。

今気が付いたけど、これってraycaster-intersection内で実装しちゃだめじゃん。
床面取れなくなったら(raycaster外れたら)位置取れなくなる。
カメラにregisterComponent付けて追随させるのがよいかな。

raycaster-intersection-cleared

raycasterが外れてしまった場合、マーカーの表示を赤色にして外れたことがわかるようにします。

上の実装はコメツケAR用に線描画とかいろいろと書いてあるので、シンプルな実装を確認したい人は、aframe-arのGithubにあるReadme.mdに記載のサンプルを参考にするとよいと思います。

github.com

始点を選択する

index.htmlのL89~L110が実装部分です。

    window.addEventListener('click', function(evt) {
      if (!line && !endPos) {
        // 線描画スタート
        startPos = mark.getAttribute("position").clone();
        startCameraPos = document.querySelector('[camera]').object3D.position.clone();
        line = document.createElement("a-entity");
        line.setAttribute("mixin","lineObj");
        AFRAME.utils.entity.setComponentProperty(line, 'line.start', startPos);
        AFRAME.utils.entity.setComponentProperty(line, 'line.end', startPos);
        scene.appendChild(line);
        
        if (!box) {
          var camera = document.querySelector("[camera]");
          box = document.createElement("a-box");
          box.setAttribute("id","sample");
          box.setAttribute("position","0 0 " +startPos.distanceTo(startCameraPos)*-1);
          box.setAttribute("scale", "0.01 0.01 0.01 ");
          box.object3D.visible = false;
          camera.appendChild(box);          
        }

        mark.object3D.visible = false;

線描画は THREE.Line を使って書いています。始点、終点、色を設定するとサッと線を描いてくれる便利な子。

line – A-Frame

線描画中がわかるようにマーカーを非表示にして、変わりに適当なBOXを配置しています。
このBOXは線描画終了後に削除する予定だったけど、消す実装忘れてた。。。

線描画中は変数の「line」にエンティティが入っているので、上で説明したマーカー位置特定の際に以下のロジックに入ります。

      if (line) {
        // 線描画中
        var box = document.querySelector("#sample");
        var boxPos = box.object3D.getWorldPosition();
        mark.setAttribute("position", boxPos.x+" "+boxPos.y+" "+boxPos.z);
        AFRAME.utils.entity.setComponentProperty(line, 'line.end', boxPos.x+" "+boxPos.y+" "+boxPos.z);        

線描画中のマーカー位置情報は、床面に沿ってではなく、BOXの位置(カメラから見て常に真ん中の一定距離)に配置しています。LINEの終点も同様にBOX位置を指定します。

終点を選択する

index.htmlのL112-L116です。

      } else if (line && !endPos) {
        // 日本語入力中
        line = null;
        endPos = mark.object3D.position.clone();
        document.querySelector("#commentText").focus();

状態判定用にライン変数(line)をクリア、および終点位置を変数(endPos)に格納しています。そのままコメント入力できるよう、テキストボックスにフォーカスを合わせています。

コメントを入力して表示する

A-Frameには<a-text>~</a-text>という文字表示エンティティがあるのですが、マルチバイト文字非対応という残念無念。
さてどうしようと思ったら、相方さんがサクッと参考になるサイトを見つけてくれました。すごいね!!

vr-lab.voyagegroup.com

まずは、描画用のHTMLソース。

index.htmlのL74-77

    <!-- HTML2Canvas -->
    <div style="background: rgba(255,255,255,0.01);color: #333; width: 100%; height: 100%; position: fixed; left: 0; top: 0; z-index: -1; overflow: hidden">
      <center><div id="htmlElement" style="background: rgba(255,255,255,0.01); color: #333; font-size: 48px; width:100%"></div></center>
    </div>

このHTML表示情報をAR空間上のPLANEに張り付けています。

次は、コメントの設定とPLANEの生成

index.htmlのL133-143

    // コメント入力したら描画した線の終端に反映する。
    function inputComment() {
      var htmlElement = document.querySelector("#htmlElement");
      htmlElement.innerHTML = document.querySelector("#commentText").value;

      plane = document.createElement("a-plane");
      plane.setAttribute("position", endPos);
      plane.setAttribute("scale", "0.2 0.1 0.1");
      plane.setAttribute("material","shader: html; target: #htmlElement; opacity:0.99");
      plane.setAttribute("look-at", "[camera]");
      scene.appendChild(plane);
    }

テキストボックスのコメントを上記描画用のHTMLソースに追加。html-shaderでPLANEに張り付けています。加えて、look-atで常にカメラの方向を向くようにしています。

まとめ

細かいところは飛ばしていますが、「どんなことをしたくて、どんな実装をしているか」はなんとなくわかっていただけたのではないでしょうか。
動画をとってアップロードする機能まではできませんでしたが、アイデア出しから始まって、初めて触るARCoreの挙動に戸惑いながら、約10時間ほどでここまで作れたのは自分でもびっくりしています。

MercariARハッカソンの参加者方々の作品を拝見して回り感じたこととして、音声認識と位置共有は今後ARを使っていく上で必須機能かなぁと感じました。あと、実装をUnityに乗り換えようかと本気で迷いました。実装楽そうだし、アセットとか、サードパーティライブラリとかがそろってて、成果物の完成度が段違いですもん。超嫉妬w
ARCore機が手元にないのでまたWebVRに戻りますが、機会があればWebARでアイデア練って一週間くらい本気で作りこんで見たいです。

終わり