じゅころぐAR

AR/VRメインのブログ。時々ドローン。

Augmented Imageで色々なモノにARコンテンツを重ねてみた

久しくブログを書けていませんでしたが、更新が滞っている間にAugmented Imageを使ったデモアプリをいくつか作ったので、まとめて記事にします。

Augmented Imageの概要

Augmented ImageはARCore v1.2.0で追加されたAPIで、任意の画像を実空間から検知してARコンテンツを重ねることができます。

developers.google.com

上記のツイートで試したサンプルでは、犬の画像が予めアプリに登録されていて、検知した画像のサイズに合わせて額縁が表示されています。
SNSやネットの記事でよく見かける事例としては、動く絵画とかが多いですね。

本記事では触れませんが、ARKitではARImageAnchorという機能があり、画像にARコンテンツを重ねる機能はマルチOSで実装可能です。

作ったもの

思いつきで色々なモノにARを重ねてました。

プリントTシャツ

Tシャツにプリントされているイラストを写真に撮って使っています。ARで着ぐるみになりたかった。
最初の頃に作ったのもあって検知精度がかなり低いです。

年賀状

年賀状をカメラで撮影してARコンテンツを乗せました。メッセージカードやポスター系でAugmented Imageを使うと、紙の情報+ARコンテンツで二度楽しめます。

スライドショー(スマホ画面)

スマートフォンで画像のスライドショーを行い、ARコンテンツの切り替えを行なっています。
後述しますが、この実装例は検知精度が非常に高いです。

スマートウォッチ

Android Wear対応のスマートウォッチを使っていてフェイスに自分の好きな画像を表示できるので、その画像を検知してARを表示しています。キャラクターだけではなく簡易のUIを表示させて、ラズパイと連携している点が他と違うところ。
また、ウォッチフェイスの画像は円形にくり抜かれている上(元画像は正方形)、時計の針が重ねて表示されていますが、画像の検知ができていることがわかります。

CDジャケット

ジャケット画像を検知して、キャラクターの表示と(ミュートしてるからわからないけど) 音源の再生を行なっています。

使い方

開発環境はUnity、MacBook Proで開発しています。
記事執筆時点の環境は以下。

  • Unity 2018.2.6f1
  • ARCore SDK for Unity v1.7.0

ARCore SDK for Unityを使う上での設定は割愛します。

developers.google.com

Augmented ImageDatabaseを作成する

公式のdeveloper guideに書いてある通りなのですが、正直ちょっとわかりにくいので図で示します。

f:id:jyuko49:20190315235725p:plain

ポイントとしては、

  • 画像を右クリックしないとAugmented ImageDatabaseが作れない
  • 作成時に選択した画像の数でAugmented ImageDatabaseが作成される

という点です。

Unityエディタ上でAugmented ImageDatabaseに登録した画像を差し替えたり、画像を削除する(減らす)ことはできますが、画像を追加する方法が不明です。そのため、最初に10個くらい画像を登録しておいた方が便利です。
なお、リファレンスを見るとAddImage()というメソッドが用意されているので、スクリプトからの追加はできそうです。

上記の手順で作成するとNameが連番のIDになっているため、必要に応じてわかりやすい値に変更します。

f:id:jyuko49:20190316000959p:plain

SessionConfigにAugmented ImageDatabaseをセットする

ARCore v1.2.0以上であれば、ARCoreSessionConfigにAugmented Image Databaseの項目があるはずです。ここに作成したImageDatabaseをセットします。
また、Camera Focus ModeAutoオートフォーカス)にしておいた方が画像の検知精度が上がります。

f:id:jyuko49:20190316001609p:plain

ARオブジェクトを表示する

AugmentedImageExampleController.csを参考にスクリプトでの実装になりますが、画像を検知したタイミングでARオブジェクトを表示するだけなら、然程難しくありません。

必要最小限のコードにコメントで解説を入れたものが以下になります。

[SerializeField]
private GameObject arObjectPrefab;

private GameObject arObject;
private List<AugmentedImage> m_TempAugmentedImages = new List<AugmentedImage>();

public void Update()
{
    // ここで検知した画像の情報がm_TempAugmentedImagesにセットされる
    Session.GetTrackables<AugmentedImage>(m_TempAugmentedImages, TrackableQueryFilter.Updated);

    // 検知した画像を1件ずつチェック
    foreach (var image in m_TempAugmentedImages)
    {
        // Trackingが停止していない場合のみ処理を実行
        if (image.TrackingState == TrackingState.Tracking)
        {
            // 画像の中心にAnchorを作成する
            Anchor anchor = image.CreateAnchor(image.CenterPose);

            // Nameが一致したらARオブジェクトの位置を追従する
            if (image.Name == "name") {

                // ARオブジェクトが未作成なら作成
                if (arObject == null) {
                    arObject = Instantiate(arObjectPrefab);
                }

                // ARオブジェクトの位置をAnchorに合わせる
                arObject.transform.position = anchor.transform.position;
                arObject.transform.rotation = anchor.transform.rotation;   
             }
        }
    }
}

上記のコードには、ARオブジェクトのサイズを画像の大きさに合わせる処理画像が検知できなくなったらARオブジェクトを非表示にする処理が入っていません。

前者はimage.ExtentXimage.ExtentZで画像の大きさが取得できるため、この値を使ってlocalScaleを調整できます。
後者に関しては、image.TrackingStateTrackingState. PausedになったタイミングでarObject.setActive(false)TrackingState. StoppedになったらDestroy(arObject)する等の実装ができます。

Tips

ImageDatabaseに使う画像の作り方

画像を用意する方法としては、大きく分けて2パターンあります。

検知対象を撮影して画像を作る

今回作ったアプリの中では、プリントTシャツ、年賀状(自分で印刷した物ではないため)がこちらのパターンです。

f:id:jyuko49:20190316015553p:plain

手軽ではあるものの、撮影時の角度や光の加減によっては検知がしにくくなります。この方法で画像を用意する場合は、十分に明るい場所で正面からの画像を撮った方がよいです。

画像を検知対象に表示・印刷する

元画像が先にあり、その画像から検知対象を作るパターンです。元画像を画面表示しているスライドショー、スマートウォッチが該当します。
検知対象を撮影する方法よりも、こちらの方が検知の精度を上げやすいと思います。

f:id:jyuko49:20190316015250p:plain

CDのジャケット写真もカメラで撮影したものではなく、ダウンロードしたジャケット写真を使った方が精度が上がります。
他のケースでも年賀状やプリントTシャツが自分で印刷・作成したモノであれば、任意の画像を入れることができます。

ARコンテンツの選び方

画像に拡張するコンテンツは、色々と考えられます。

  • 3Dオブジェクト
    • キャラクター
    • エフェクト
  • 画像
    • 見た目を一部変える
  • 動画
    • 静止画を動かす
    • 別の動画を再生する
  • テキスト
  • UI

ARコンテンツありきではなく、画像がプリントされている現実のモノに対して「何を拡張したら面白いか?」を考えた方がイメージが湧きやすいです。イラストなら描かれているキャラクターが飛び出す、CDであれば試聴ができるといった具合です。
日頃から頭の片隅に置いて生活していると、良いアイデアが閃くかもしれません。

今後

ARグラスでの利用

Augmented ImageはARCoreの機能のため、スマートフォンをかざす行為が必要になります。ARKitのARImageAnchorも同様です。
ARコンテンツを見るためとはいえ、やや不自然な動作と言えます。

ARCoreやARKitに対応したARグラスが発売されれば、画像を見るだけでARコンテンツが表示されるようになるため、利便性が大きく向上するはずです。

Webサーバの活用

現状ではローカルのImageDatabaseで画像とNameを紐付け、アプリ内でNameとARコンテンツを紐付けています。
リアルタイム性を考えると止むなしだとは思いますが、予め登録してある画像でないと検知ができないですし、検知のための画像やARコンテンツをアプリ内に大量に保持すると容量が大きくなります。

より汎用性の高い実装を考えていくと、"Webサーバで画像とARコンテンツの紐付けが保持されていて、必要に応じてダウンロードできる形"にならざるを得ません。実現できれば、全ての画像とコンテンツをアプリ内に持って事前に紐付けておく必要が無くなります。

まとめ

Augmented Imageは画像を媒体にしているので、どこに画像を乗せるか、画像にどのARコンテンツを乗せるかの組み合わせができ、現状のモバイルARでは最も汎用性があり、実用性も高いです。
実装はそこまで難しくなく、機能面でさらに使いやすくなる余地も残しているので、積極的に使っていい機能だと思います。

クレジット

本記事およびTwitter内のコンテンツには、クエリちゃんアセット、暮井 慧(プロ生ちゃん)3Dモデルを利用ガイドラインに基づいて利用しています。

DOWNLOAD | クエリちゃん公式サイト
利用ガイドライン(音声以外) | プロ生ちゃん(暮井 慧)

Crystal Signal Piで光る通知システムを作る

ARもAIも関係なく、AWSとラズパイの話。

AWSの監視したいけど、メール見落としたら意味ないよね。
ラズパイとかでパトランプ回してる人、いそうじゃない?

という訳で、ググってみた。

www.infiniteloop.co.jp

www.youtube.com

そうそう、こういうやつ・・・なっ!?
なにこれ!カッコいい!!
Amazonで売ってるの? 5,000円?

ポチりました。

お急ぎ便にしてないけど、2日で届いた。

組み立て

基板のはんだ付けが終わっている通常版を買ったので、ラズパイ本体(別売り)にネジ止めするだけです。

組み立ては5分くらいで終わりました。

f:id:jyuko49:20181119013514j:plain

試しにラズパイを電源につないでみると、クリスタルが緑色に!

思った以上にカッコいい!!
なんとなく部屋を暗くしてしまう・・・。

Crystal Signal Piの設定

公式のマニュアル(PDF)の手順の通りに実行しました。

設定が終わったら、ブラウザからラズパイのIPアドレスにアクセスして、Web UIを表示します。

f:id:jyuko49:20181119012112p:plain

アラート実行を試すと、色がRGB値で自由に変更できて点滅のパターンも指定できます。
API経由で叩く場合は、GETパラメータで渡す感じです。
http://192.168.0.8/ctrl/?color=200,0,248&mode=1&repeat=0&period=1000&json=1

リモートで叩けるようにする

AWSやSlackから通知を受けるには、グローバルでアクセス可能なホスト名かIPアドレスが必要になります。

方法は色々ありますが、今回はこちらの記事を参考にルーターの設定とNo-IPで実現しました。
raspberry.mirukome.com

ラズパイのサーバプロセスは既に立ち上がっているので、Apacheのインストールは不要です。

  • No-IPクライアントをラズパイにインストール
  • ラズパイのプライベートIP(192.168.0.XXX)を固定
  • ルーターでラズパイへのポート:80を開放

を行えば、No-IPで作成したホスト名→ルーターグローバルIP→ラズパイのプライベートIPでルーティングされます。

No-IPのインストールにはサインアップが必要で、こちらから作成しています。
無償のホスト名は何もしないと30日で期限切れになってしまうため、定期的にconfirmが必要です。嫌なら有償版に、ってことですね。

疎通テストはWi-Fi接続を切ったiPhoneからNo-IPで作成したホスト名へのリクエストで実施できます。インターネット(4G回線)越しでも確かにつながりました。

AWSからの通知を設定する

AWSで障害が起きた際の通知を想定して、設定&テストしてみました。

f:id:jyuko49:20181120220545p:plain

  • CloudWatchでアラームを作成してSNSに通知
  • LambdaファンクションでSNSトピックをサブスクライブ
  • LambdaファンクションからラズパイにGETでアラート送信

詳細は割愛しますが、CloudWatchのアラームをSlack通知する手順とほぼ同じです。ググるとたくさん出てきます。
Lambdaファンクションを変更して、AWSからラズパイにアラートを投げるようにしました。

'use strict';

const AWS = require('aws-sdk');
const url = require('url');
const http = require('http');

const hookUrl = 'No-IPで作ったホスト名';

function sendAlert(state, callback) {
   
    const options = url.parse(hookUrl);
    if (state === 'ALARM') {
        options.path = '/ctrl/?color=255,0,0&mode=1&repeat=20&period=500&json=1';
    } else if (state === 'OK'){
        options.path = '/ctrl/?color=0,255,0&mode=0&repeat=1&period=500&json=1';
    } else {
        options.path = '/ctrl/?ack=1';
    }
    
    
    const alertReq = http.get(options, (res) => {
        const chunks = [];
        res.setEncoding('utf8');
        res.on('data', (chunk) => chunks.push(chunk));
        res.on('end', () => {
            if (callback) {
                callback({
                    body: chunks.join(''),
                    statusCode: res.statusCode,
                    statusMessage: res.statusMessage,
                });
            }
        });
        return res;
    });

    alertReq.end();
}

function processEvent(event, callback) {
    const message = JSON.parse(event.Records[0].Sns.Message);
    const newState = message.NewStateValue;

    sendAlert(newState, (response) => {
        if (response.statusCode < 400) {
            console.info('Send Alert is successfully');
            callback(null);
        } else if (response.statusCode < 500) {
            console.error(`Error send Alert: ${response.statusCode} - ${response.statusMessage}`);
            callback(null);
        } else {
            // Let Lambda retry
            callback(`Server error when send Alert: ${response.statusCode} - ${response.statusMessage}`);
        }
    });
}


exports.handler = (event, context, callback) => {
    if (hookUrl) {
        processEvent(event, callback);
    } else {
        callback('Hook URL has not been set.');
    }
};
  • 通知された状態がアラームなら、クリスタルが赤く点滅
  • 通知された状態がOKなら、クリスタルを緑に点灯

通知をテストする

監視対象にしているEC2のApacheを停止して応答不可にします。
LBのTarget Groupがヘルスチェックに失敗して、CloudWatchがアラートを上げる仕組み。

まとめ

当初の目的だったアラートに気付きやすくという点では、目に入るところに置いておけば、ほぼ間違いなく気付きます。
少なくとも、メールやSlackよりは見落としにくいです。

あえて欠点を挙げるなら、アラート時じゃなくても光らせたくなってしまう点でしょうか。
頻繁に光らせたいなら、Slack通知をWebhookさせた方が楽しそうです。

DeepLabをMask R-CNNと比べてみた

前回の続きで、物体検知&セグメンテーションのライブラリ調査です。
最終的にはモバイルARで使う想定で評価しています。

↓前回
jyuko49.hatenablog.com

はじめに

Mask R-CNNをカメラでキャプチャしたリアルタイムの動画に適用したところ、すごくカクカクで思うように速度が出ませんでした。
GPU性能を活かせば速くなるらしいのですが、モバイルARで使うとなると、マシンスペックに頼るのは難しいです。

そこで代替のライブラリを探したところ、以下の動画を見つけました。

youtu.be

右下のDeepLabは、Mask R-CNNと同様に検知した物体のセグメンテーションができていて、リアルタイム動画にも追従できそうな雰囲気です。
人や車だけでなく、建物などの背景もマスクしている点も気になったので、試してみることにしました。

DeepLabの特徴

TensorFlowで実装・学習されており、オープンソースとしてGithubで公開されています。
また、Googleの紹介記事があります。

developers-jp.googleblog.com

記事内では、Semantic image segmentation(セマンティック・イメージ・セグメンテーション)と書かれており、画像を"ピクセル単位"で意味的に分割するアルゴリズムのようです。
Mask R-CNNは、画像内の物体を検知して、検知した物体をマスクしていたので、その点で若干の違いがありそうです。ここは、実際に結果を見てみるとイメージしやすいです。

DeepLabの環境設定

まず、Tensorflowのモデルが提供されているGithubからリポジトリごとcloneして、DeepLabのディレクトリに移動します。

github.com

$ git clone https://github.com/tensorflow/models.git
$ cd models/research/deeplab/

ライブラリのインストール

前回と同じで、Python3、Jupyter Notebookを使います。今回はMask R-CNNの実行時に構築した環境をそのまま使いました。

また、tensorflow、matplotlib、numpyなどのライブラリも必要になります。requirement.txtが見当たらないので、不足分はpip install [パッケージ名]で追加します。Mask R-CNNの環境を先に作ってあれば、大体インストールされていると思います。

サンプル実行

Mask R-CNNのときと同様に、Jupyter Notebookで可視化されたサンプルを動かします。

$ jupyter notebook

deeplab_demo.ipynbを選択して、"Run"をクリックしていくと処理の流れが見られます。
結果のサンプルとしては、以下のような画像が表示されます。

f:id:jyuko49:20181116083658p:plain

コードを読んでみる

重要な部分だけ抜き出して見ていきます。

コードの最後に書かれているrun_visualization()がメイン処理です。

def run_visualization(url):
  """Inferences DeepLab model and visualizes result."""
  try:
    f = urllib.request.urlopen(url)
    jpeg_str = f.read()
    original_im = Image.open(BytesIO(jpeg_str))
  except IOError:
    print('Cannot retrieve image. Please check url: ' + url)
    return

  print('running deeplab on image %s...' % url)
  resized_im, seg_map = MODEL.run(original_im)

  vis_segmentation(resized_im, seg_map)


image_url = IMAGE_URL or _SAMPLE_URL % SAMPLE_IMAGE
run_visualization(image_url)

image_urlから画像を読み込んでoriginal_imにセットし、MODEL.run()を実行すると、リサイズされた元画像resized_imとセグメンテーション結果seg_mapが得られます。
その結果をvis_segmentation()に渡して、画面に表示しています。

MODELはコードの前半で定義されているDeepLabModelクラスのインスタンスで、http://download.tensorflow.org/models/からダウンロードしたモデルを使ってTensorflowのSessionを実行しています。

MODEL = DeepLabModel(download_path)

seg_mapからマスク画像を生成する処理は、vis_segmentation()内に記述されています。

seg_image = label_to_color_image(seg_map).astype(np.uint8)
plt.imshow(seg_image)

具体的な処理はlabel_to_color_image()create_pascal_label_colormap()に書かれていますが、長くなりそうなのでここでは割愛。

テストスクリプトの作成

例によって、deeplab_demo.ipynbのコードを参考にして作りました。

静止画(ローカルファイル)

改変した箇所は以下です。

元画像を固定のURLから取得するようになっているので、ファイルパスに変更します。

#image_url = IMAGE_URL or _SAMPLE_URL % SAMPLE_IMAGE
image_path = '[ローカルのファイルパス]'
run_visualization(image_path)

Mask R-CNNのサンプルに倣って、ディレクトリ内の画像をランダムに表示する場合は以下のようにします。

import random

IMAGE_DIR = './test_image'
file_names = next(os.walk(IMAGE_DIR))[2]
image_path = os.path.join(IMAGE_DIR, random.choice(file_names))
run_visualization(image_path)

image_urlで使っていた定数は要らないので、コメントアウトします。

'''
SAMPLE_IMAGE = 'image1'  # @param ['image1', 'image2', 'image3']
IMAGE_URL = ''  #@param {type:"string"}

_SAMPLE_URL = ('https://github.com/tensorflow/models/blob/master/research/'
               'deeplab/g3doc/img/%s.jpg?raw=true')
'''

最後に、画像の読み込み部分を変更します。
ローカルファイルを読み込む場合、 Image.open()にパスをそのまま渡せばOKです。引数名はurlじゃなくてpathだけど直してません。

def run_visualization(url):
  """Inferences DeepLab model and visualizes result."""
  try:
    # f = urllib.request.urlopen(url)
    # jpeg_str = f.read()
    # original_im = Image.open(BytesIO(jpeg_str))
    original_im = Image.open(url)
  except IOError:
    print('Cannot retrieve image. Please check url: ' + url)
    return

完成版のコードサンプルです。

gist.github.com

リアルタイム動画

OpenCVVideoCapture()を使って、取得したフレーム画像にマスクをかける処理をループさせれば実現できます。

import cv2
import time

capure = cv2.VideoCapture(0)

def run_visualization():
  while(True):
    ret, frame = capure.read()
    original_im = cv2.resize(frame,(480,320))

    start_time = time.time()

    resized_im, seg_map = MODEL.run(original_im)
    vis_segmentation(resized_im, seg_map)

    elapsed_time = time.time() - start_time
    print(elapsed_time)

    if cv2.waitKey(1) == 27:
      break
  capure.release()
  cv2.destroyAllWindows()

run_visualization()

画像の表示部分もOpenCVに変更しています。かなりシンプルに書けます。

def vis_segmentation(image, seg_map):
  seg_image = label_to_color_image(seg_map).astype(np.uint8)

  result = cv2.add(image, seg_image)
  cv2.imshow("camera window", result)

動かすとこんな感じ。

完成版のサンプルコードはこちら。

gist.github.com

性能比較

実行環境

処理結果は4パターンの画像をそれぞれ比較。 処理時間の方はMacbookのカメラでキャプチャしたリアルタイムの映像を480 x 320にリサイズして連続処理した際の時間を計測。

Mask R-CNN

処理結果

f:id:jyuko49:20181116230323p:plain f:id:jyuko49:20181112154021p:plain f:id:jyuko49:20181112153856p:plain f:id:jyuko49:20181112154027p:plain

検知した物体をかなり正確にセグメンテーションできています。

処理時間(sec)

2.493619203567505
2.4120068550109863
2.4120633602142334
2.4788520336151123
2.5836117267608643
...

1回の検知に2.4-2.5 secかかっています。
ということは、フレームレートは0.4fps。1fpsを切っているため、リアルタイム動画ではラグが相当大きいです。

DeepLab

処理結果

f:id:jyuko49:20181116233707p:plain f:id:jyuko49:20181116234933p:plain f:id:jyuko49:20181116235210p:plain f:id:jyuko49:20181116235221p:plain

正面や横を向いている物体はキレイに検出できていますが、後ろ向きや座っている人の検出がイマイチです。この点は追加学習させれば改善する部分なのかもしれません。
明らかな差異としては物体の形状が途切れやすい点で、画像のピクセル単位でセグメンテーションを行なっている結果のようです。

処理時間(sec)

0.42249274253845215
0.45106005668640137
0.44458889961242676
0.42955899238586426
0.4366567134857178
...

0.40-0.45 sec前後なので、Mask R-CNNの1/5程度の処理時間です。決して速くはないですが、2.5fpsくらいは出ます。
冒頭で紹介したYoutubeの動画内でも平均2.5fpsと表示されているので、妥当な結果と思われます。

結果まとめ

  • リアルタイム性を重視するならDeepLab
  • リアルタイム性が重要でなく、精度重視ならMask R-CNN

という使い分けになりそうです。

今後

DeepLabの処理速度であれば、モバイルARでもなんとか使えそうという感触です。
実際に同様の研究はされていて、モバイル相当のスペックでもリアルタイム動画に追従はできる様子。

ai.googleblog.com

静止画のRGBに加えて前フレームで生成したマスクを使って、高速に処理しつつ精度を上げてるんですね。なるほど。

一旦は学習済みモデルをそのまま使うことにして、TensorFlowSharp in Unityを先に試そうかなと思っています。

Mask R-CNNを試してみた

AR x AIで使えそうなMask R-CNNというOSSを教えてもらったので動かしてみました。

github.com

Mask R-CNNでできること

Githubリポジトリに載っている画像がわかりやすいです。

https://github.com/matterport/Mask_RCNN/raw/master/assets/street.png

画像の中の物体(人とか車とか)を検知(Object Detection)して、検知した物体を背景と分割(Object Segmentation)する形でマスクしてくれます。

これとよく似た画像って、どこかで見たことがあるような?

japanese.engadget.com

Niantic Real World Platform」の記事で、現実と溶け込んだARの要素技術として物体検知が紹介されていました。
リアルタイムに物体検知を行いつつ、物体との距離も推定してマスクを行うことができれば、リアルなオクルージョンが実現できそうです。
AR以外の用途だと、写真投稿サイトで写り込んだ人物をぼかしたり、自動運転やナビゲーションで人を避けたり、とか。

環境構築

GithubのREADMEをざっくり読むと、Jupyter Notebookを使ってステップごとの結果が可視化できるらしい。

jupyter.org

また、物体検知用に提供されているMS COCOというデータセットを使うらしい。MSはMicrosoftの略。

COCO - Common Objects in Context

github.com

Jupyter Notebookのインストール

$ pip install jupyter

それでは早速入れてみ・・・エラーがいっぱい出ました!
メッセージを眺めるとライブラリのバージョンがどうのこうの言っているので、Pythonのバージョンを確認したところ2系でした。pyenv使っててglobalは古いままだった・・・。

$ python --version
Python 2.7.10

Mask_R_CNNはPython3で動作と明記されているので、3系に切り替えます。

pyenv versionsでインストール済みのバージョンを確認。3.6.6があったので、localのバージョンを切り替えて再度実行。

$ pyenv local 3.6.6
$ pip install jupyter

今度はインストールできたので、以下のコマンドで実行。

$ jupyter notebook

f:id:jyuko49:20181110224038p:plain

何か表示されました。

で、ここからどうすれば?

必要ライブラリのインストール

Getting Startedを見ると、とりあえずdemo.ipynbを動かしてみるのがよいらしい。
拡張子が.ipynbになっているファイルが、Jupyter Notebookの実行ファイルのようです。JSONPythonのコードを突っ込んだようなフォーマットになってます。

Jupyter Notebookを起動して"samples > demo.ipynb"をクリックすると、以下のような画面が表示されました。

f:id:jyuko49:20181111000428p:plain

Runボタンをクリックしていくと次の処理に進みますが、途中でエラーメッセージが出ます。

f:id:jyuko49:20181110234358p:plain

ModuleNotFoundError: No module named 'numpy'

どうやらライブラリが足りない様子です。

これらのインストール方法はREADMEの下の方に書いてありました(もうちょっと上の方に書いておいて欲しさ)

$ pip install -r requirements.txt
$ python setup.py install

requirements.txtにパッケージ名が列記されていて、上記コマンドを実行すれば必要なライブラリがインストールされます。

COCO APIのインストール

前述の手順でインストールしただけだと、以下のエラーが解消されません。

ModuleNotFoundError: No module named 'pycocotools'

上記はCOCO APIのインストールで入るようです。

github.com

Githubからcloneして、とりあえずプロジェクトフォルダ直下に配置。Python APIをインストールします。

$ git clone https://github.com/cocodataset/cocoapi.git
$ cd coco/PythonAPI
$ python setup.py install

これで環境は構築できたはず。
Runコマンドをポチポチしていくと、最終的に以下のような画像が表示されます。(画像は毎回変わる)

f:id:jyuko49:20181111000636p:plain

コードを読んでみる

サンプルスクリプトが何をやっているのか、ざっと見てみてみます。

In[1]

最初のブロックでは、ライブラリのインポートと定数を定義しているだけ。

import os
import sys
import random
import math
import numpy as np
import skimage.io
import matplotlib
import matplotlib.pyplot as plt

# Root directory of the project
ROOT_DIR = os.path.abspath("../")

# Import Mask RCNN
sys.path.append(ROOT_DIR)  # To find local version of the library
from mrcnn import utils
import mrcnn.model as modellib
from mrcnn import visualize
# Import COCO config
sys.path.append(os.path.join(ROOT_DIR, "samples/coco/"))  # To find local version
import coco

%matplotlib inline 

# Directory to save logs and trained model
MODEL_DIR = os.path.join(ROOT_DIR, "logs")

# Local path to trained weights file
COCO_MODEL_PATH = os.path.join(ROOT_DIR, "mask_rcnn_coco.h5")
# Download COCO trained weights from Releases if needed
if not os.path.exists(COCO_MODEL_PATH):
    utils.download_trained_weights(COCO_MODEL_PATH)

# Directory of images to run detection on
IMAGE_DIR = os.path.join(ROOT_DIR, "images")

mask_rcnn_coco.h5が学習済みモデルで、ローカルになければダウンロードしてくるようになっている。
IMAGE_DIRが物体検知を行う画像フォルダのパス。

In[2]: Configurations

CocoConfigクラスを継承して、InferenceConfigクラスで一部プロパティを上書きしている。

class InferenceConfig(coco.CocoConfig):
    # Set batch size to 1 since we'll be running inference on
    # one image at a time. Batch size = GPU_COUNT * IMAGES_PER_GPU
    GPU_COUNT = 1
    IMAGES_PER_GPU = 1

config = InferenceConfig()
config.display()

In[3]: Create Model and Load Trained Weights

モデルの生成部分です。先程作成したconfigを渡してます。

# Create model object in inference mode.
model = modellib.MaskRCNN(mode="inference", model_dir=MODEL_DIR, config=config)

# Load weights trained on MS-COCO
model.load_weights(COCO_MODEL_PATH, by_name=True)

コードの最初の方に、

import mrcnn.model as modellib

とあるので、'mrcnn/model.py'が本体のようです。

In[4]: Class Names

検知した物体のクラス名の定義。ラベルを付けるときに使う。

# COCO Class names
# Index of the class in the list is its ID. For example, to get ID of
# the teddy bear class, use: class_names.index('teddy bear')
class_names = ['BG', 'person', 'bicycle', 'car', 'motorcycle', 'airplane',
               'bus', 'train', 'truck', 'boat', 'traffic light',
               'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird',
               'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear',
               'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie',
               'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball',
               'kite', 'baseball bat', 'baseball glove', 'skateboard',
               'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup',
               'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple',
               'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza',
               'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed',
               'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote',
               'keyboard', 'cell phone', 'microwave', 'oven', 'toaster',
               'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors',
               'teddy bear', 'hair drier', 'toothbrush']

データセットが参照できれば以下のコードで置き換えられるらしい(試してないです)

# Load COCO dataset
dataset = coco.CocoDataset()
dataset.load_coco(COCO_DIR, "train")
dataset.prepare()

# Print class names
print(dataset.class_names)

In[5]: Run Object Detection

ここがメインの処理ですね。

# Load a random image from the images folder
file_names = next(os.walk(IMAGE_DIR))[2]
image = skimage.io.imread(os.path.join(IMAGE_DIR, random.choice(file_names)))

# Run detection
results = model.detect([image], verbose=1)

# Visualize results
r = results[0]
visualize.display_instances(image, r['rois'], r['masks'], r['class_ids'], 
                            class_names, r['scores'])

画像データをファイルから取得してmodel.detect()を行うと、resultsに結果のデータが入ってくるので、それをvisualize.display_instances()という処理に渡しています。
visualizeはコードの最初の方でimportしてあるライブラリで、mrcnn/visualize.pyに実際の処理が書いてあります。

from mrcnn import visualize

visualize.py内の処理はコード書きながら理解することにします。

使ってみる

サンプルと同じことをやるだけですが、自分でコードを書いてみます。

.ipynb形式はコーディングしづらいので、test.pyを作成して、In[1]〜[5]のコードをコピペしていきます。
ファイルができたら、とりあえず以下の3ヶ所だけ変更しておきます。

#%matplotlib inline  #よくわかんないけどエラーになるのでコメントアウト
ROOT_DIR = os.path.abspath("../")  #実行ファイルからルートへの相対パスに直す
# 画像を加工&表示する部分は自作するのでコメントアウト
#result = visualize.display_instances(image, r['rois'], r['masks'], r['class_ids'],
#                        class_names, r['scores'])

コマンドラインで実行します。

$ python test.py

バックエンドがMacOSだとなんちゃらみたいなmatplotlibのエラーが出た場合、以下の記事で解決しました。

qiita.com

結果の画像を表示する

skimage.ioとmatplotlib.pyplotがインポートされているので、これを使います。

result_image = image.copy()

skimage.io.imshow(result_image)
plt.show()

こんな感じで表示されるはず。

f:id:jyuko49:20181112142300p:plain

ウィンドウを閉じると、コマンドラインで実行したプログラムも終了します。

検知結果の中身を確認する

resultsに何が入っているか見たいので、printしてみます。

print(r['rois'])
print(r['masks'])
print(r['class_ids'])
print(r['scores'])

rois

[[ 38 265 493 533]
 [163 482 229 530]
 [250  31 587 782]]

検出した物体を囲う矩形(Bounding box)の頂点座標が入っています。配列の要素数が検知した物体の数になります。

masks

[[[False False False]
  [False False False]
  [False False False]
  ...
  [False False False]
  [False False False]
  [False False False]]

 [[False False False]
  [False False False]
  [False False False]
  ...

(長いので省略)

  [False False False]
  [False False False]
  [False False False]]]

rois内でマスクをかける範囲が配列で入っている。詳しいところはよくわかってない。

class_ids

[ 1 47 61]

検知した物体のクラスに対応するIDが入っています。
class_namesを使うことでラベル文字列を取得できます。

for id in r['class_ids']:
    print(class_names[id])
person
banana
dining table

scores

[0.99927574 0.9297102  0.91985023]

検知の精度がスコアとして入っています。数値が1に近いほど、検出の正確性が高いはず。

コードを書く

resultsの中身がわかったので、resultsを使って結果を描画していきます。

前提として、

for roi in r['rois']:
    # 矩形の描画

for mask in r['masks']:
    # マスクの描画

のようなコーディングだと、ループがたくさんになるので、

N = r['rois'].shape[0]  #配列の大きさ(要素数)
for i in range(N):
    # r['rois'][i]を描画
    # r['masks'][i]を描画

とした方がループが1回で速い(たぶん)

矩形の描画

visualize.draw_box(image, box, color)でimageにboxを重ねることができます。

result_image = visualize.draw_box(result_image, r['rois'][i], rgb)

もしくはOpenCVを使って、

import cv2

result_image = cv2.rectangle(result_image, 
    (r['rois'][i][1],  r['rois'][i][0]), ( r['rois'][i][3],  r['rois'][i][2]), rgb)

みたいな実装でもできます。

ラベルとスコアの描画

OpenCVを使いました。位置は対応する矩形(r['rois'][i])の左上の座標を使っています。

font = cv2.FONT_HERSHEY_SIMPLEX
text = class_names[r['class_ids'][i]] + ':' + str(r['scores'][i])
result_image = cv2.putText(result_image, text,
    (r['rois'][i][1],r['rois'][i][0]), font, 0.8, rgb, 2, cv2.LINE_AA)

マスクの描画

こはちょっと難しいので、visualize.apply_mask(image, mask, color)を使いました。

mask = r['masks'][:, :, i]
result_image = visualize.apply_mask(result_image, mask, color)

実行結果

f:id:jyuko49:20181112153856p:plain f:id:jyuko49:20181112153914p:plain f:id:jyuko49:20181112154021p:plain f:id:jyuko49:20181112154027p:plain

精巧なフィギュアやイラストは、人として認識されるんですね。

他にも、忍ちゃんの髪がバナナと判別されたり、パドックのOの字がフリスビーと判別されたりしていますが、ARで使う場合は、物体が何であるかの検知よりも物体がそこにあることを検知してくれた方が都合良さそうです。
検知(判別はともかく)の精度という意味では、パドックの写真で後ろ向きの人物をすべて検出していてすごい。

  • マスクあり版
  • マスクなし版

マスクなし版はGoogleVision APIをローカルで実行している感じですね。

複数のオブジェクトを検出する  |  Cloud Vision API ドキュメント  |  Google Cloud

完成版のコード

Gistにアップしました。
Mask_RCNN直下のファイルにコピペして、test_imageフォルダを作って画像を置けば動くはず。

今後

大体の動作はわかったので、AR開発で使ってみたいです。
というか、これからのARはAIありきになっていきそう。

リアルタイムの動画でテストする

各フレームに対して同様の処理を行えばよいので、実装自体は難しくないと思う。
フレームレートがどれくらい落ちるかの方が懸念。

Unityで使えるか調べる

ARアプリで使うには、学習済みモデルをUnityで動かしたい。
mask_rcnn_coco.h5はKeras用のファイルのようなので、TensorFlowで使えるようProtocol Bufferで保存して、TensorFlowSharpでごにょごにょする感じでしょうか。

オクルージョンに使う

マスクしている箇所をオクルージョンするシェーダを書けばいいはず。
書ければ。

他の学習モデルも試す

物体検知のモデルとしては、R-CNN、YOLO、SSD(Solid State Driveではない)が有名らしい。
以下の記事を見ると、R-CNNよりもYOLOの方がFPSが出てるので、同様のマスク処理ができるなら両方試した方がいいかもしれない。

qiita.com

追加学習させる

今回は学習済みモデルをそのまま使いましたが、自分で学習データを追加して用途に応じてチューニングできた方が便利です。が、教師データの準備とか大変そうなので優先度は低め。

3D対応を試す

READMEにて、3Dに対応したデータセットとしてMatterport3Dが紹介されています。

github.com

お問い合わせしないとダウンロードできないので試せませんでしたが、3Dでの物体検知は興味があります。

また、別のモデルで単眼カメラの画像からデプスを推定するvid2depthというのもあったので、これと物体検知を組み合わせて使うのも良さそう。

models/research/vid2depth at master · tensorflow/models · GitHub

↓続きです
jyuko49.hatenablog.com

three.jsでWebVRを行うための基本的なところ

久々の投稿デス。

WebXRやろうかなと思ったのですが、手持ちのARCore対応デバイスがAndroid7.xのため試せず・・・で気分転換にWebVRをやりました。

はじめに

three.jsに関する基礎知識を持っている前提で書いているので、three.jsをさわったことがない!という方は、他の記事やサイトも合わせて読んでください。

開発環境については、以下となっております。(WiFiルータ経由でローカル通信)

three.jsでVRMを扱う

VRMLoader.jsを使って、VRMを表示させることができます。
公式のexampleは以下。

VRMはglTF2.0ベースなので、GLTFLoader.jsでも読み込みは可能です。
上記のVRMLoader.jsも内部でGLTFLoader.jsをラップしています。

UniVRMでエクスポートする

まず、VRMモデルを用意します。
今回は公式の手順に従い、FBXファイルをUniVRMでエクスポートしてみました。

VRMファイルを作ってみたい - dwango on GitHub

モデルはいつもの(クエリちゃんSDモデル)です。

assetstore.unity.com

手順の通り、Humanoidリグが設定されているモデルをエクスポートするだけなので簡単。

f:id:jyuko49:20181008222525p:plain

こんな感じで、three.jsで使えるようになります。

f:id:jyuko49:20181008223306p:plain

ボーンアニメーションで動かす

FBXからエクスポートしたVRMにはアニメーションが付いていないので、何もしないとTポーズのままになります。
それだと面白くないので、ボーンを使って動きを付けます。

ボーンを確認する方法として、LoadしたVRMモデルの情報をconsoleに表示させました。

var loader = new THREE.VRMLoader();
loader.load( 'SD_QUERY_01.vrm', function (vrm) {
  console.log(vrm);
  ...
}

MacWebブラウザでページを開いてconsoleを見ると、VRMの構造が確認できます。

f:id:jyuko49:20181008230138p:plain

scene内にモデルが含まれるので、sceneのchildrenを辿っていくと、SkinnedMeshが見つかるはずです。SkinnedMeshはskeletonプロパティにbonesを持っていて、このbonesを回転させれば、モデルが動きます。

ボーンアニメーションについては、カヤックさんのブログがわかりやすかったです。

techblog.kayac.com

とりあえず、動かしてみました。

中央にいるSDクエリちゃんの回転と首振りがボーンアニメーションです。

一方、隣でキレキレのダンスを披露しているコロちゃん(コロコロ*1 )は、元のモデルがMMDMMDLoader.jsでVMDファイルをAnimationClipとしてLoadしています。
複雑なモーションの場合、ボーンをスクリプトで直接動かすのはつらいので、何らかの手段でAnimationClipを作った方がいいです。

three.jsのシーンをWebVRに対応させる

exampleを参考にthree.jsのシーンをVRで表示させます。

通常のスクリプトから以下の点を変更すればOKのよう。

// WebVR.jsを読み込む
<script src="examples/js/vr/WebVR.js"></script>

<script>
  renderer = new THREE.WebGLRenderer( { antialias: true } );

  // rendererでVRを有効にする
  renderer.vr.enabled = true;

  // createButtonで"Enter VR"ボタンを表示する
  document.body.appendChild(WEBVR.createButton(renderer));

  function animate() {
    // setAnimationFrameではなく、setAnimationLoopでループさせる
    renderer.setAnimationLoop(animate);
    render();
  };
</script>

Daydreamで表示させるとこんな感じになります。

Daydreamコントローラーを使う

Gamepad Extensionsを有効化する

three.jsのexamplesでは、renderer.vr.getController()でDaydreamコントローラーの情報が取れているのですが、ローカルで実行すると動きませんでした。

GoogleのDevelopers Guideによると、ChromeブラウザでGamepad API 拡張機能を有効にしないと動かないようです。 Adding Input to a WebVR Scene  |  Web Fundamentals  |  Google Developers

Chromeブラウザのアドレスバーにchrome://flagsと入力して、設定画面を開き、"Gamepad Extensions"、"WebXR Gamepad Support"をEnabled(有効)にすると動くようになります。

f:id:jyuko49:20181009083731j:plain

Ray Inputで代用する

開発者向けであれば、上記の設定を行えばいい話ですけど、一般のユーザに自身のデバイスでデモを体験させたいときなど、設定を変えてもらうのはちょっと手間です。

代案として、先程のGoogleの記事に記載されていたRay Inputであれば、拡張機能の設定なしに動きます。

github.com

Daydream(モバイルVR)の場合、オブジェクト選択に使うRayがコントローラーの向きに関わらず、視点の中心になりました。
click等はDaydreamコントローラーの操作がイベントとして取れます。

空間内の移動を実装する

three.jsで移動を実装する場合、cameraオブジェクトのposition、rotationを操作すれば視点が変わり、プレイヤーが移動しているように見えます。
ただ、WebVRの場合はcameraがVRバイスと連動するため、cameraのプロパティを直接操作しても視点が変わりません。

// three.jsでは移動するけど、WebVRだと動かない・・・
camera.position.z -= 0.1;

これの解決策として、cameraの親オブジェクトを作ってあげて、camera直接ではなく親オブジェクトの方を動かすと位置が変わります。

// Object3Dでplayerを作り、cameraを親子関係にする
var player;
player = new THREE.Object3D();
scene.add(player);
player.add(camera);

// playerを動かすとcameraも動いて視点が変わる
player.position.z -= 0.1;

playerの移動をコントローラー入力のイベントに連動させれば、3DoFデバイスでも空間上の前進、後退くらいは実現できます。

まとめ

今回の内容でthree.jsのシーンをWebVR対応させ、キャラクター(VRM)を表示させて、コントローラーで入力や移動を行うところまで実装できました。
若干の差異はあれど、通常のthree.jsとほぼ変わらないので、元々three.jsをやっていた人なら簡単にVR開発ができます。

three.jsの場合、シーン構成からアニメーションまですべてスクリプトで完結する点とライブラリ・サンプルが豊富という点で、UnityやUEにはない安心感がありますね。(圧倒的ホームグラウンド感!)

Webなので、ネットワークを介したコンテンツの共有が楽ですし、VR内でのWebブラウジングからスムーズにVRコンテンツに移行できるので、安価なVRバイスが普及していけば用途も広がりそうな感じはします。

*1:gdgd妖精sのBD特典モデル(Copyright (c) 2013 gdgd妖精s(ぐだぐだフェアリーーーズ), 2代目gdgd妖精s)、改造可・再配布・商用利用不可

ARKitのARObjectAnchorをUnityで使ってみる

ARKit2の新機能として追加されたARObjectAnchorをUnityで試してみます。

blogs.unity3d.com

ARObjectAnchorとは

現実の物体(オブジェクト)を検知して、Anchorの作成・トラッキングを行う機能です。
予め作成しておいたARReferenceObjectを元に検知を行います。

f:id:jyuko49:20180805172842p:plain

開発環境

ARKit2で追加された機能のため、iOS12およびXcode10が必要になります。
本記事執筆時点ではβ版のため、Apple Developerからダウンロードしました。

Unity ARKit Pluginのインストール

Unity ARKit PluginはBitbucketのから"arkit2.0_beta"ブランチをダウンロードします。

Unity-Technologies / Unity-ARKit-Plugin / Downloads — Bitbucket

ダウンロード後、Assets以下のUnityARKitPluginをUnityプロジェクトのAssetsにフォルダごとコピーして使います。

f:id:jyuko49:20180805171735p:plain

ARKitを使うのに必要なUnityの設定については割愛します。"Camera usage description"を忘れなければ、なんとかなると思います。

ARReferenceObjectを作成する

検知したいオブジェクトをスキャンして、ARReferenceObjectを作成します。
ここが上手くできないと検知の精度が落ちるので、重要な作業です。

UnityARKitPluginにExamplesとして付属しているUnityObjectScannerをビルドすると、スキャン用のアプリが起動します。

UnityARKitPlugin > Examples > ARKit2.0 > UnityObjectScanner

BoundingBoxを対象オブジェクトに合わせる

アプリ起動後の手順として、まずはBoundingBoxでスキャン対象のオブジェクトを囲み、Axisをオブジェクトの中心に合わせます。

f:id:jyuko49:20180805222935p:plain

基本的な操作方法は、以下の通り。

BoundingBoxの移動:オブジェクト付近の平面をタップ
BoundingBoxの伸縮:BoundingBoxの面をタップしてドラッグ
BoundingBoxの平行移動:Axisをタップしてドラッグ
BoundingBoxの回転:画面を二本指タップした状態で回転

対象オブジェクトをスキャンする

BoundingBoxの位置を合わせたら、その状態で対象オブジェクトを複数の方向からスキャンします。"Create Objects"、"Detect Objects"のボタンを押す必要はありません。カメラをかざすことで、オブジェクト上のPointCloudが特徴点として検出されていきます。

ARReferenceObjectを保存する

スキャンが完了したら、アプリ上のUIでARReferenceObjectを作成・保存します。

"Create Objects"でBoundingBox内の特徴点からARReferenceObjectが作られ、リストに追加されます。 この処理は多少時間がかかることがあり、画面中央のウィンドウにファイル名のリスト(objScan_0,objScan_1,...)が表示されたら、作成完了した合図です。
最後に、"Save Objects"でARReferenceObjectをファイルとしてデバイスに保存します。これを忘れると、せっかくスキャンした結果を利用できなくなります。

f:id:jyuko49:20180805225206p:plain

他のボタンは以下の用途に使えます。スキャンだけを行う場合、必須ではないです。

"Detect Object"は検知モードのON/OFFです。スキャン結果を用いた検知のテストに使えます。
"Clear Objects"は"Create Objects"で作成したARReferenceObjectを全て削除します。

ARReferenceObjectをコピーする

iTunesのファイル共有からコピーするのが簡単です。

スキャンを行ったデバイスMacに接続して、iTunesでファイル共有メニューを開きます。

f:id:jyuko49:20180805231521p:plain

アプリが作成したファイルに"ARReferenceObjects"という名称のフォルダがあり、ARReferenceObjectが保存されています。
フォルダごとMacにコピーしてFinderで開くと、拡張子が.arobjectのファイルがあるはずです。
このファイルをUnityにコピーして使います。

f:id:jyuko49:20180805232614p:plain

ARKitScannerを使ったARReferenceObjectの作成

スキャンを行う方法としてはもう一つ、Apple公式のサンプルコードをXcodeでビルドしたARKit Scannerが使えます。

Scanning and Detecting 3D Objects | Apple Developer Documentation

スキャンの流れはUnityのサンプルと同じですが、BoundingBoxの作成、Scan、Test、ファイルの保存・共有とステップを踏んでいくUIとなっており、UnityObjectScannerよりもわかりやすいです。

f:id:jyuko49:20180805221927p:plain

スキャンの進捗に応じてパーセンテージが表示され、スキャンを行った面はBoundingBoxが着色される機能もあるため、全方向から満遍なくスキャンを行えます。
100%になっていなくても"Finish" > "Cancel"(Continueのキャンセル)でスキャンを終了できます。

f:id:jyuko49:20180805221955p:plain

最終的に作成されるのは.arobjectファイルなので、Unityでも使えます。
ファイルの転送にAirDropが使える点も便利です。

ARObjectAnchorを使う

作成したARReferenceObjectを使って、ARObjectAnchorの作成、コンテンツの重ね合わせを行います。

Unity ARKit PluginでARObjectAnchorを使うには、UnityARCameraManagerの"Detection Objects"ARReferenceObjectsSetAssetをセットします。

f:id:jyuko49:20180805235927p:plain

ARReferenceObjectsSetAssetは、プロジェクトウィンドウで右クリックから"Create" > "UnityARKitPlugin" > "ARReferenceObjectsSetAsset"で作成できます。

f:id:jyuko49:20180805165517p:plain

ARReferenceObjectsSetAssetには、検知対象のARReferenceObjectAssetを複数セットできます。

f:id:jyuko49:20180806001817p:plain

ARReferenceObjectAssetもプロジェクトウィンドウで右クリックから"Create" > "UnityARKitPlugin" > "ARReferenceObjectAsset"で作成できます。

f:id:jyuko49:20180806002206p:plain

ARReferenceObjectsAssetには、スキャンで作成した.arobjectファイルをARReferenceObjectとしてセットします。

ここまでで、ARSessionに検知対象のARReferenceObjectが設定され、ARObjectAnchorが作成されるようになります。

作成されたARObjectAnchorを使ってARコンテンツを重ねるにはスクリプトを使います。UnityARObjectAnchorで利用されているGenerateObjectAnchor.csが参考になります。

まず、Start()でARObjectAnchor作成時、更新時に実行するメソッドを定義します。

void Start () {
    UnityARSessionNativeInterface.ARObjectAnchorAddedEvent += AddObjectAnchor;
    UnityARSessionNativeInterface.ARObjectAnchorUpdatedEvent += UpdateObjectAnchor;
}

ARSessionでオブジェクトが検知されると、定義したメソッドが呼ばれ、ARObjectAnchorが渡されます。
作成時に呼ばれるメソッド(AddObjectAnchor())では、ARObjectAnchorからposition、rotationを取得して、GameObjectを生成(Instantiate)しています。

void AddObjectAnchor(ARObjectAnchor arObjectAnchor)
{
    Debug.Log ("object anchor added");
    if (arObjectAnchor.referenceObjectName == referenceObjectAsset.objectName) {
        Vector3 position = UnityARMatrixOps.GetPosition (arObjectAnchor.transform);
        Quaternion rotation = UnityARMatrixOps.GetRotation (arObjectAnchor.transform);

        objectAnchorGO = Instantiate<GameObject> (prefabToGenerate, position, rotation);
    }
}

検知したオブジェクトの位置が更新されると、更新されたARObjectAnchorが渡されます。
更新時のメソッド(UpdateObjectAnchor())では、生成したGameObjectの位置をARObjectAnchorの更新に合わせて上書きしています。

void UpdateObjectAnchor(ARObjectAnchor arObjectAnchor)
{
    Debug.Log ("object anchor added");
    if (arObjectAnchor.referenceObjectName == referenceObjectAsset.objectName) {
        objectAnchorGO.transform.position = UnityARMatrixOps.GetPosition (arObjectAnchor.transform);
        objectAnchorGO.transform.rotation = UnityARMatrixOps.GetRotation (arObjectAnchor.transform);
    }
}

基本的には、上記のサンプルコードと同様にGameObjectの生成、位置の更新を行えばARコンテンツが現実のオブジェクトに重ねて表示され、対象が動いても追従するようになります。
複数のオブジェクトを検出しつつ、異なるARコンテンツを表示したい場合は、ARReferenceObjectAssetに設定したObjectNameがARObjectAnchorにセットされるため、何を検知したかを名称によって制御できます。

アプリで動かしてみる

まず、UnityARObjectAnchorと同様にAxisを表示してみます。

スキャンしたARReferenceObjectの精度にもよりますが、検知に数秒かかるようなこともなく、オブジェクトにカメラをかざすとすぐに検知してくれます。

続いて、ARコンテンツを3D Textに変更して、追従のテストを行ってみました。

検知対象のオブジェクトが移動すると、テキストも移動していることがわかります。

まとめ

ARKit2で追加されたARObjectAnchorは、現実の物体をARで拡張するのに適した機能です。
ARReferenceObjectのスキャンがやや面倒ではありますが、アプリの操作に慣れれば数分で作成できます。一度作成してしまえば、どのアプリでも使えますし、ファイルを共有することで同じ物を持っている人も使えます。

用途としてイメージしやすいのは、今回試したようにフィギュアや模型にエフェクトやUIを重ねる使い方でしょうか。市販されている製品であれば、ARReferenceObjectをインターネットで配布・共有できる点も魅力です。
また、物体の移動を検知できるので、チェスのような物の配置によって局面が変わるようなゲームにも使えそうです。

立体的な特徴が少なく検知が難しい場合、平面画像を検知・追従するARImageAnchorが代用できます。

『Unreal Engine 4 ゲーム開発入門』はこれからUEを始める人にオススメしたい

前回に引き続きUEを勉強中ですが、購入した書籍がよかったのでレビュー記事を書きました。

"リズちゃんが表紙のUnreal Engine入門書"、略して"リズ本"!!
UEを始めるなら、これは買うしかないと思っていました。
※書籍版とKindle版があります

「リズちゃん? 誰?」という方はこちら
弊ブログでも度々登場しているクエリちゃんの妹です。

オススメする理由

クエリちゃん推しだからだけではないよ。本当だよ。

著者が専門学校の先生

著者の荒川さんは普段からゲーム開発を教えている方だそうで、説明が非常にわかりやすいです。

UEだけではなく、Unityの入門書も執筆されていますね。

www.amazon.co.jp

画面キャプチャが多い

フルカラーになっており、画面キャプチャによる解説が多いです。
紙面の半分が画面キャプチャと言っても過言ではないです。

UEがブループリントで開発できる点もキャプチャでの説明に合っていて、コード(スクリプト)例などは全然出てこないですし、迷ったらキャプチャの通りに操作すればいいです。

開発に必要な要素が詰まっている

UEの操作方法に始まり、ブループリント、アニメーション、コリジョン、ナビメッシュ、UI作成、パッケージ化まで、初めてアプリを作るのに最低限必要な要素をしっかり押さえている感じです。

Webサイトで情報が更新されている

バージョン対応、正誤表、追加情報などが著者の運営するサイトで公開されています。

uebeginner.jp

サンプルプロジェクトの完成品やモデルデータもここからダウンロードできます。
また、書籍には収録されていなかった音関連(BGM、ボイス、SE)の情報も公開されています。

最新のバージョンには追従されていないこともありますが、そこは自分で検索して調べれば十分対応できました。例えば、被破壊性メッシュの作成が見当たらないケースでは"被破壊性メッシュ ue4"で検索できます。

非破壊性メッシュの作成について - UE4 AnswerHub

リズちゃん!

クエリちゃんではなくリズちゃんを表紙に起用したあたりにセンスを感じます。
なお、書籍内で開発するサンプルアプリの主役もリズちゃんです。

実際、動いてるリズちゃんは、なかなか可愛い。

クエリちゃんモデルは、ちょっぴりセクシーな感じに仕上がっております。

というか、UnityではSDクエリちゃんを使っていたので、セクシーさの概念はなかった。

f:id:jyuko49:20180224222021j:plain

感想

Unreal Engineの第一印象として、Unityよりもビューが豊富で機能も多そうなので、慣れるまで時間がかかりそうかなと思っていました。
でも、この本を読みながらUEの機能を色々さわっていると、なんとなくできた気分になってきます。

これって実は大事なことで、最初の一歩で躓くと、そこでもう諦めてしまうケースって多いと思うんですね。
初心者は情報量が多いと逆に混乱しますし、苦手意識が払拭されるだけで入門書としての役割は果たしていると思うので、対象読者とコンセプトがはっきりした良書という感じがしました。