読者です 読者をやめる 読者になる 読者になる

凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity で Android 向けの OpenCV x ArUco を利用した AR アプリを作ってみる

AR Unity OpenCV C# C++ Android

はじめに

UnityAndroid 用のアプリを作る際、Unity だけで出来ないことをやろうとするとプラグインを利用することになります。Android 向けのプラグインは大きく分けて 2 種類あり、一つは Android の機能を利用する Java プラグイン、もう 1 つは C/C++ のコードを NDK でビルドして使うネイティブプラグインです。

今回はネイティブプラグインを利用して OpenCVArUco を使った AR な Android アプリを作る方法について解説します。まず、OpenCV を利用しない簡単なプラグインの作成方法を紹介し、その後 OpenCV を使ったサンプルを解説、最後に ArUco を利用して AR のサンプルを作ってみる、という流れで書いていこうと思います。

Unity で AR するよ、という趣旨ではなく、どちらかと言うとどういう風に作るかの解説になるので、単純に AR をする方法をお探しでしたら Vuforia の方が簡単だと思います。

デモ

Galaxy S6 上で動かしてます。ちょっと横着しているところがあって遅延が目立ちますがそこそこ動きます。

サンプルプロジェクト

以下に今回作成したサンプルプロジェクトを置いてあります。

環境

ネイティブプラグインの作成

サンプルとして以前のエントリ でも紹介したことのある市松模様を表示するものを書いてみます。

NDK

NDK は Android 用の以下のサイトから自分の環境に合わせて Android NDK をダウンロード、展開し、適当な場所に配置してパスを通しておきます。

ディレクトリ構造

Your Unity Project
├── Assets
│   │
│   ├── Plugins
│   │   └── Android
│   │        └── libcheck_texture.so
│   ├── SampleScene.unity
│   └──  Scripts
│        └── CheckTexture.cs
│
├── Plugins
│   └── check_texture
│       └── jni
│           ├── Android.mk
│           ├── Application.mk
│           └── check_texture.cpp
 :

こんな感じでプロジェクトを用意します。libcheck_texture.so を出力するところがゴールです。

プラグイン開発用ディレクトリ(ここでいう /Plugins)はどこでも良いのですが、Assets 以下に置くと .cpp ファイルがプラグインとして認識されたり、不要な .meta ファイルが作成されたりと不便です(.Plugins のように . プレフィックスをつければ回避は出来ます)。管理も楽だと思うので Assets の隣に置く構成にしてみました。

コード

市松模様を作成するコードを書いてみます。

extern "C"
{

void create_check_texture(unsigned char* arr, int w, int h, int ch)
{
    int n = 0;
    for (int i = 0; i < w; ++i) {
        for (int j = 0; j < h; ++j) {
            for (int k = 0; k < ch; ++k) {
                arr[n++] = ( (i + j) % 2 == 0 ) ? 255 : 0;
            }
        }
    }
}

}

与えられたメモリに 255/0 を順番に書き込むだけのコードです。

Android.mk / Application.mk の記述

NDK でビルドするためには Application.mkAndroid.mkjni ディレクトリに必要です。

Application.mk

APP_STL      := gnustl_static
APP_CPPFLAGS := -frtti -fexceptions
APP_ABI      := armeabi-v7a
APP_PLATFORM := android-9

Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

NDK_APP_DST_DIR := ../../../Assets/Plugins/Android
LOCAL_MODULE    := libcheck_texture
LOCAL_SRC_FILES := check_texture.cpp

include $(BUILD_SHARED_LIBRARY)

ビルドしたら Unity の Assets/Plugins/Android 以下にライブラリが配置されるようにしています。

ビルド

ndk-build コマンドを jni ディレクトリで実行します。

$ ndk-build
[armeabi-v7a] Compile++ thumb: check_texture <= check_texture.cpp
[armeabi-v7a] SharedLibrary  : libcheck_texture.so
[armeabi-v7a] Install        : libcheck_texture.so => ../../../Assets/Plugins/Android/libcheck_texture.so

これで Unity の Plugins ディレクトリ化に libcheck_texture.so が生成されます。Android のライブラリとして認識されていることを確認します。

f:id:hecomi:20150905170031p:plain

Unity 側からプラグインを利用する

次に Unity 上での作業に移ります。このプラグインを利用するコードを書いてみます。

using UnityEngine;
using System;
using System.Runtime.InteropServices;

public class CheckTexture : MonoBehaviour
{
    [DllImport("check_texture")]
    private static extern void create_check_texture(IntPtr data, int w, int h, int ch);

    private Texture2D texture_;
    private Color32[] pixels_;
    private GCHandle pixels_handle_;
    private IntPtr pixels_ptr_ = IntPtr.Zero;

    void Start()
    {
        // テクスチャを生成
        texture_ = new Texture2D(10, 10, TextureFormat.RGB24, false);
        // テクスチャの拡大方法をニアレストネイバーに変更
        texture_.filterMode = FilterMode.Point;
        // Color32 型の配列としてテクスチャの参照をもらう
        pixels_ = texture_.GetPixels32();
        // GC されないようにする
        pixels_handle_ = GCHandle.Alloc(pixels_, GCHandleType.Pinned);
        // そのテクスチャのアドレスをもらう
        pixels_ptr_ = pixels_handle_.AddrOfPinnedObject();
        // スクリプトがアタッチされたオブジェクトのテクスチャをコレにする
        GetComponent<Renderer>().material.mainTexture = texture_;
        // ネイティブ側でテクスチャを生成
        create_check_texture(pixels_ptr_, texture_.width, texture_.height, 4);
        // セットして反映させる
        texture_.SetPixels32(pixels_);
        texture_.Apply(false, true);
        // GC 対象にする
        pixels_handle_.Free();
    }
}

10x10 px、RGBA なメモリを確保してある pixels_ptr_ に直接市松模様を書き込んでいます。ネイティブから Unity 側への画像の渡し方は色々あるのですが、今回は C++ 側が環境依存せずに一番楽になる方法を利用しています(代わりに Unity 側のお作法が大変ですが...)。

ビルド、実機確認

最後に Android 用のプロジェクトの設定をしてビルドします。具体的には、Build Settings で Android へ Platform をスイッチし、Player Settings で適当な Identifier を指定します。

f:id:hecomi:20150905171816p:plain f:id:hecomi:20150905172210p:plain

これで Android 端末を USB で接続し、ビルド・実行すると以下のようになります。

f:id:hecomi:20150905172627p:plain

エディタ上でも確認できるようにする

このままだと毎回実機に送って作業しなければならずデバッグが大変です。そこで Editor 上でも確認できるように PC 向けにビルドをします。例えば Mac で作業しているのであれば Xcode で Bundle を出力するプロジェクトを作成し、同じソースコードを参照するようにします。

f:id:hecomi:20150905215837p:plain

ビルドした .bundle は Plugins/x86_64 へ出力するように設定しておきます。これで Editor 上でも市松模様を確認できます。

f:id:hecomi:20150905220049p:plain

ビルドスクリプトを書いて、ndk-buildxcodebuild が両方走るようにしても良いかもしれません。なお、Windows であれば IDEVisual Studio になるだけでやることは同じです。iOS 向けに同時にビルドするのも簡単です。

OpenCV for Android の利用

さて、次に OpenCV を使ってみましょう。Android.mk にちょっと記述するだけで OpenCV が使えるようになるので、基本的には前述の市松模様と同じです。

OpenCV for Android のダウンロード

まず公式より OpenCV for Android を入手します。

zip を展開したら適当な場所に配置しておきます。以下の解説では jni 以下に opencv とリネームして置いたことにします。

Android.mk

次に Android.mk に OpenCV for Android を include するように記述します。

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

OPENCV_LIB_TYPE  := STATIC
include opencv/jni/OpenCV.mk

NDK_APP_DST_DIR  := ../../../Assets/Plugins/Android
LOCAL_MODULE     := opencv_sample
LOCAL_SRC_FILES  := $(shell find . -name "*.cpp")
LOCAL_CFLAGS     += -std=c++11
LOCAL_LDLIBS     += -llog -lz

include $(BUILD_SHARED_LIBRARY)

途中で OpenCV.mk を include するようにしています。中を覗いていただくと分かるのですが、この中で OpenCV 用に LOCAL_C_INCLUDESLOCAL_LDLIBS の設定をしてくれているので、このまま ndk-build すればよしなにリンクしてくれるようになります。

コード

では OpenCV を利用するコードを書いてみます。

C++
#include <opencv2/opencv.hpp>

extern "C"
{

bool get_image_size(const char* path, int* width, int* height)
{
    const auto img = cv::imread(path);
    if (img.empty()) return false;
    *width  = img.rows;
    *height = img.cols;
    return true;
}

bool read_image(const char* path, unsigned char* dest)
{
    const auto rgb = cv::imread(path);
    if (rgb.empty()) return false;
    cv::Mat rgba;
    cv::cvtColor(rgb, rgba, cv::COLOR_BGR2BGRA);
    memcpy(dest, rgba.data, rgba.total() * rgba.elemSize());
    return true;
}

}

指定された画像の情報を取得する関数と、指定された画像を読み込んで与えられたメモリにコピーする関数を書きます。これを以下のように C# 側のコードから利用します。

C#
using UnityEngine;
using System;
using System.Runtime.InteropServices;

public class ReadTexture : MonoBehaviour
{
    [DllImport("opencv_sample")]
    private static extern bool get_image_size(string path, out int width, out int height);
    [DllImport("opencv_sample")]
    private static extern bool read_image(string path, IntPtr ptr);

    private Texture2D texture_;
    private Color32[] pixels_;
    private GCHandle pixels_handle_;
    private IntPtr pixels_ptr_ = IntPtr.Zero;

    public string imagePath = "zzz.png";
    
    string GetFilePath(string fileName)
    {
#if UNITY_EDITOR
        string path = Application.streamingAssetsPath + "/" + filePath;
#elif UNITY_ANDROID
        string path = "/sdcard/" + filePath;
#endif
        return path;
    }

    void Start()
    {
        string path = GetFilePath(imagePath);
        int width, height;
        if (!get_image_size(path, out width, out height)) {
            Debug.LogFormat("{0} was not found", path);
            return;
        }

        texture_ = new Texture2D(width, height, TextureFormat.RGB24, false);
        texture_.filterMode = FilterMode.Point;
        pixels_ = texture_.GetPixels32();
        pixels_handle_ = GCHandle.Alloc(pixels_, GCHandleType.Pinned);
        pixels_ptr_ = pixels_handle_.AddrOfPinnedObject();
        GetComponent<Renderer>().material.mainTexture = texture_;

        read_image(path, pixels_ptr_);

        texture_.SetPixels32(pixels_);
        texture_.Apply(false, true);
        pixels_handle_.Free();
    }
}

デバッグ用にエディタ上では StreamingAssets ディレクトリを参照するようにしています。

SD カードの中を見れるようにする

Android アプリでは AndroidManifest.xml で各種権限を管理しています。上記コードでは SD カード内のファイルの読み込みをしているので、この権限を追加しなければなりません。Unity では何もしなければ AndroidManifest.xml を書いたコードに応じて自動的に権限が追加されたものを用意してくれます。

手動で権限を追加するには、これを記述した AndroidManifest.xmlAssets/Plugins/Android 以下に用意すれば OK です。SD カードの読み込み / 書き込みの権限を追加したサンプルは以下になります。

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.unity3d.player"
    android:installLocation="preferExternal"
    android:versionCode="1"
    android:versionName="1.0">
    <application
        android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
        android:icon="@drawable/app_icon"
        android:label="@string/app_name"
        android:debuggable="true">
        <activity android:name="com.unity3d.player.UnityPlayerActivity"
            android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
        </activity>
    </application>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
</manifest>

ビルド

ビルドすると以下のように Android アプリで表示されます。SD カードから OpenCV を経由して画像を読み取りテクスチャとして貼り付けています。

f:id:hecomi:20150906180849p:plain

OpenCV を経由しているので適当に加工するのも簡単です。例えばコードを以下のようにして線画にしてみます。

bool read_image(const char* path, unsigned char* dest)
{
    const auto rgb = cv::imread(path);
    if (rgb.empty()) return false;
    cv::Mat canny;
    cv::Canny(rgb, canny, 50.f, 200.f);
    cv::Mat rgba;
    cv::cvtColor(canny, rgba, cv::COLOR_GRAY2BGRA);
    memcpy(dest, rgba.data, rgba.total() * rgba.elemSize());
    return true;
}

f:id:hecomi:20150906185136p:plain

ディレクトリ構造

ちなみに、今までのところのディレクトリの構造は以下のようになっています。

Unity

f:id:hecomi:20150906184103p:plain

Plugins
Plugins
├── check_texture
│   ├── jni
│   │   ├── Android.mk
│   │   ├── Application.mk
│   │   └── check_texture.cpp
│   └── mac
│       ├── check_texture
│       └── check_texture.xcodeproj
└── opencv_sample
    ├── jni
    │   ├── Android.mk
    │   ├── Application.mk
    │   ├── opencv
    │   └── read_image.cpp
    └── mac
        ├── opencv_sample
        └── opencv_sample.xcodeproj

カメラを利用する

ArUco を使う最後の準備としてカメラを利用してみます。

カメラは直接 OpenCVcv::VideoCapture から参照は出来ないようで、JNI 経由で呼び出さないと駄目なようです。が、Unity には便利な WebCamTexture が用意されているので、ここから画像をくすねてきます。

まずは貰った画像を線画にして返す C++ 側のコードを書いてみます。

#include <opencv2/opencv.hpp>

extern "C"
{

void to_canny(unsigned char* src, unsigned char* dest, int width, int height, int thresh1, int thresh2)
{
    cv::Mat src_img(height, width, CV_8UC4, src);
    cv::Mat dest_img;
    cv::Canny(src_img, dest_img, thresh1, thresh2);
    cv::cvtColor(dest_img, dest_img, CV_GRAY2BGRA);
    memcpy(dest, dest_img.data, dest_img.total() * dest_img.elemSize());
}

}

これを C# 側から利用します。

using UnityEngine;
using System;
using System.Runtime.InteropServices;

public class CannyCamera : MonoBehaviour
{
    [DllImport("opencv_sample")]
    private static extern int to_canny(IntPtr src, IntPtr dest, int width, int height, int thresh1, int thresh2);

    private WebCamTexture webcamTexture_;
    private Texture2D cannyTexture_;

    private bool isWebCamInitialized_ = false;
    private int width_;
    private int height_;

    [Range(0, 255)]
    public int thresh1 = 50;
    [Range(0, 255)]
    public int thresh2 = 200;

    void Start()
    {
        SetupWebCamTexture();
        SetupCannyTexture();
    }

    void SetupWebCamTexture()
    {
        var devices = WebCamTexture.devices;
        if (devices.Length > 0) {
            webcamTexture_ = new WebCamTexture(devices[0].name, 640, 480);
            webcamTexture_.Play();
            width_  = webcamTexture_.width;
            height_ = webcamTexture_.height;
            isWebCamInitialized_ = true;
        } else {
            Debug.Log("no camera");
        }
    }

    void SetupCannyTexture()
    {
        if (!isWebCamInitialized_) return;
        cannyTexture_ = new Texture2D(width_, height_);
    }

    void Update()
    {
        if (!isWebCamInitialized_) return;

        var pixels = webcamTexture_.GetPixels32();
        var handle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
        var ptr    = handle.AddrOfPinnedObject();
        to_canny(ptr, ptr, width_, height_, thresh1, thresh2);

        cannyTexture_.SetPixels32(pixels);
        cannyTexture_.Apply();
        GetComponent<Renderer>().material.mainTexture = cannyTexture_;
    }
}

WebCamTexture を使うと AndroidManifest.xml へ自動的にカメラへのアクセス権限が追加されるので後はこのままビルドすれば以下のように加工したカメラ画をテクスチャとして利用できます。

f:id:hecomi:20150906195333p:plain

ArUco を組み込む

いよいよ ArUco を使ったコードを見ていきます。

ArUco の準備とビルド

ArUco 1.3.0 をダウンロードして展開します。android ディレクトリが中にあり、Aruco.mk があるので一見これを include すれば良さそうですが、中を見ると別途 cmake でビルドしたものを読み込むようになっています。android.toolchain.cmake 及び、これを利用してビルドするスクリプトscripts/*.sh が用意されているのですが、環境依存なコードが多く、直すのが面倒なのでこれらは使いません。代わりに自分でビルドを行うようにします。

srcjni にリネームして以下の Aruco.mk をその中に置きます。

ARUCO_PATH       := $(call my-dir)
LOCAL_SRC_FILES  += $(shell ls -1 $(ARUCO_PATH)/*.cpp)
LOCAL_C_INCLUDES += $(ARUCO_PATH)

LOCAL_SRC_FILES に ArUco 関連の *.cpp を追加し、インクルードディレクトリも追加登録します。これを元々の Android.mk から以下のようにインクルードして使います。

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

OPENCV_LIB_TYPE  := STATIC
include opencv/jni/OpenCV.mk
include aruco/jni/Aruco.mk

NDK_APP_DST_DIR := ../../../Assets/Plugins/Android
LOCAL_MODULE    := opencv_sample
LOCAL_SRC_FILES += $(shell ls -1 *.cpp)
LOCAL_CFLAGS    += -std=c++11

include $(BUILD_SHARED_LIBRARY)

先にビルドしても良いと思うのですが、このようにまとめてビルドしてしまったほうが OpenCV のディレクトリ指定等が楽な点が多いと思います。なお、chromaticmask.cpp および highlyreliablemarkers.cpp はビルドに失敗するので削除しておきます。これでビルドが通ると思いますので、次にこれを利用したコードを書いてみます。

ArUco を利用したコード

C++
#include <opencv2/opencv.hpp>
#include <aruco.h>
#include <cstdio>


class aruco_manager
{
public:
    aruco_manager()
        : marker_size_(0.1f)
    {
    }

    void set_image_size(int width, int height)
    {
        width_ = width;
        height_ = height;
    }

    void set_marker_size(float marker_size)
    {
        marker_size_ = marker_size;
    }

    void read_params(const char* camera_params_file_path)
    {
        params_.readFromXMLFile(camera_params_file_path);
        params_.resize(cv::Size(width_, height_));
    }

    void detect(unsigned char* src, unsigned char* dest)
    {
        cv::Mat input(height_, width_, CV_8UC4, src);
        if (input.empty()) return;

        cv::Mat bgr;
        cv::cvtColor(input, bgr, cv::COLOR_BGRA2BGR);

        std::vector<aruco::Marker> markers;
        detector_.detect(bgr, markers, params_, marker_size_);

        for (auto&& marker : markers) {
            marker.draw(bgr, cv::Scalar(0, 0, 255), 2);
            aruco::CvDrawingUtils::draw3dCube(bgr, marker, params_);
        }

        if (dest != nullptr) {
            cv::Mat output;
            cv::cvtColor(bgr, output, cv::COLOR_BGR2BGRA);
            memcpy(dest, output.data, output.total() * output.elemSize());
        }
    }

private:
    aruco::CameraParameters params_;
    aruco::MarkerDetector detector_;
    int width_, height_;
    float marker_size_;
};


extern "C"
{

void* aruco_initialize(
    int width,
    int height,
    float marker_size,
    const char* camera_params_file_path)
{
    auto manager = new aruco_manager();
    manager->set_image_size(width, height);
    manager->set_marker_size(marker_size);
    manager->read_params(camera_params_file_path);
    return manager;
}

void aruco_finalize(void* instance)
{
    auto manager = static_cast<aruco_manager*>(instance);
    delete manager;
    instance = nullptr;
}

void aruco_detect(void* instance, unsigned char* src, unsigned char* dest)
{
    auto manager = static_cast<aruco_manager*>(instance);
    if (manager == nullptr) return;
    manager->detect(src, dest);
}

}

適当にラップしたクラスを作ってそれを利用する関数を用意しています。

C# 側のコード

using UnityEngine;
using System;
using System.Collections;
using System.Runtime.InteropServices;

public class DetectArucoMarker : MonoBehaviour
{
    [DllImport("opencv_sample")]
    private static extern IntPtr aruco_initialize(int width, int height, float markerSize, string cameraParamsFilePath);
    [DllImport("opencv_sample")]
    private static extern void aruco_finalize(IntPtr instance);
    [DllImport("opencv_sample")]
    private static extern void aruco_detect(IntPtr instance, IntPtr src, IntPtr dest);

    private WebCamTexture webcamTexture_;
    private Texture2D arucoTexture_;
    private bool isWebCamInitialized_ = false;
    private int width_;
    private int height_;
    private IntPtr aruco_ = IntPtr.Zero;

    public float markerSize = 0.04f;
    public string cameraParamsFileName = "intrinsics.yml";

    string GetFilePath(string fileName)
    {
#if UNITY_EDITOR
        return Application.streamingAssetsPath + "/" + fileName;
#elif UNITY_ANDROID
        return "/sdcard/AndroidOpenCvSample/" + fileName;
#endif
    }

    void Start()
    {
        SetupWebCamTexture();
        SetupAruco();
    }

    void SetupWebCamTexture()
    {
        var devices = WebCamTexture.devices;
        if (devices.Length > 0) {
            webcamTexture_ = new WebCamTexture(devices[0].name, 640, 480);
            webcamTexture_.Play();
            width_  = webcamTexture_.width;
            height_ = webcamTexture_.height;
            isWebCamInitialized_ = true;
        } else {
            Debug.Log("no camera");
        }
    }

    void SetupAruco()
    {
        if (!isWebCamInitialized_) return;
        var path = GetFilePath(cameraParamsFileName);
        aruco_ = aruco_initialize(width_, height_, markerSize, path);
        arucoTexture_ = new Texture2D(width_, height_, TextureFormat.ARGB32, false, true);
        GetComponent<Renderer>().material.mainTexture = arucoTexture_;
    }

    void Update()
    {
        if (!isWebCamInitialized_) return;

        var pixels = webcamTexture_.GetPixels32();
        var handle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
        var ptr    = handle.AddrOfPinnedObject();

        aruco_detect(aruco_, ptr, ptr);

        handle.Free();

        arucoTexture_.SetPixels32(pixels);
        arucoTexture_.Apply();

    }

    void OnDestroy()
    {
        aruco_finalize(aruco_);
    }
}

先ほどのカメラのサンプルの例で線画にしているところで認識をしている感じです。

結果

f:id:hecomi:20150912193845p:plain

ArUco が動いている様子が分かります。

AR にする

では最後に AR にしてみます。

高速化

先ほどのようなコードの書き方では速度が出ません。理由は大きく 2 つあって、1 つはカメラ画を WebCamTexture から取ってきているところで不要なテクスチャの転送が起きていること。もう一つは ArUco の処理をメインスレッドで行っている点です。前者については OpenCV では画が取ってこれなかったので、JNI で橋渡しして Java プラグインから引っ張ってくる必要があると思います(C++ だけから出来る方法があったら是非教えて下さい...)。ここでは前者は割愛して、後者の改善のために別スレッド化するところだけやったコードを書いています。

キャリブレーション

事前に ArUco 付属のキャリブレーションツールもしくは GMLツールを利用してキャリブレーションを行い、正しい intrinsics.yml を用意する必要があります。

C++

先ほどと同じように ArUco を管理するクラスを作って、そのポインタを経由して各機能へアクセスします。

#include <opencv2/opencv.hpp>
#include <aruco.h>
#include <cstdio>
#include "log.h"


class aruco_manager
{
public:
    struct marker_result
    {
        int id;
        double position[3];
        double orientation[4];
    };

    aruco_manager()
    {
    }

    void set_image_size(int width, int height)
    {
        width_ = width;
        height_ = height;
        input_ = cv::Mat(height, width, CV_8UC4);
    }

    void set_marker_size(float marker_size)
    {
        marker_size_ = marker_size;
    }

    void set_params(const char* camera_params_file_path)
    {
        params_.readFromXMLFile(camera_params_file_path);
        params_.resize(cv::Size(width_, height_));
    }

    void set_image(unsigned char* src)
    {
        if (src != nullptr) {
            input_ = cv::Mat(height_, width_, CV_8UC4, src).clone();
        }
    }

    void get_image(unsigned char* dest) const
    {
        if (dest != nullptr) {
            memcpy(dest, output_.data, output_.total() * output_.elemSize());
        }
    }

    size_t detect(bool drawOutputImage = true)
    {
        if (input_.empty()) return -1;

        cv::Mat bgr;
        cv::cvtColor(input_, bgr, cv::COLOR_BGRA2BGR);

        cv::Mat resized;
        cv::resize(bgr, resized, cv::Size(), 0.4f, 0.4f); // 適当に小さくする

        std::vector<aruco::Marker> markers;
        detector_.detect(bgr, markers, params_, marker_size_, true);

        markers_.clear();

        for (auto&& marker : markers) {
            marker.draw(bgr, cv::Scalar(0, 0, 255), 2);
            aruco::CvDrawingUtils::draw3dCube(bgr, marker, params_);

            marker_result result;
            result.id = marker.id;
            marker.OgreGetPoseParameters(result.position, result.orientation);

            markers_.push_back(result);
        }

        if (drawOutputImage) {
            cv::cvtColor(bgr, output_, cv::COLOR_BGR2BGRA);
        }

        input_.release();

        return markers_.size();
    }

    void* get_markers()
    {
        return &markers_[0];
    }

private:
    aruco::CameraParameters params_;
    aruco::MarkerDetector detector_;
    cv::Mat input_;
    cv::Mat output_;
    int width_  = 0;
    int height_ = 0;
    float marker_size_ = 0.1f;
    int marker_num_ = 10;
    std::vector<marker_result> markers_;
};


extern "C"
{

void* aruco_initialize(
    int width,
    int height,
    float marker_size,
    const char* camera_params_file_path)
{
    auto manager = new aruco_manager();
    manager->set_image_size(width, height);
    manager->set_marker_size(marker_size);
    manager->set_params(camera_params_file_path);
    return manager;
}

void aruco_finalize(void* instance)
{
    auto manager = static_cast<aruco_manager*>(instance);
    delete manager;
    instance = nullptr;
}

void aruco_set_image(void* instance, unsigned char* src)
{
    auto manager = static_cast<aruco_manager*>(instance);
    if (manager == nullptr) return;
    manager->set_image(src);
}

void aruco_get_image(void* instance, unsigned char* dest)
{
    auto manager = static_cast<aruco_manager*>(instance);
    if (manager == nullptr) return;
    manager->get_image(dest);
}

int aruco_detect(void* instance)
{
    auto manager = static_cast<aruco_manager*>(instance);
    if (manager == nullptr) return -1;
    return static_cast<int>(manager->detect());
}

void* aruco_get_markers(void* instance)
{
    auto manager = static_cast<aruco_manager*>(instance);
    if (manager == nullptr) return nullptr;
    return manager->get_markers();
}

}

ArUco で認識したマーカの位置姿勢については aruco::Marker::OgreGetPoseParameters() 経由で取ってくることが出来ます。detect() でこの結果を適当な構造体(aruco_result)に詰めて見つけたマーカの個数を返し、その先頭ポインタを get_markers() で取得できるようになっています。次に C# 側を見てみましょう。

C#

using UnityEngine;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;

public class ArPlane : MonoBehaviour
{
    [DllImport("opencv_sample")]
    private static extern IntPtr aruco_initialize(int width, int height, float markerSize, string cameraParamsFilePath);
    [DllImport("opencv_sample")]
    private static extern void aruco_finalize(IntPtr instance);
    [DllImport("opencv_sample")]
    private static extern void aruco_set_image(IntPtr instance, IntPtr src);
    [DllImport("opencv_sample")]
    private static extern void aruco_get_image(IntPtr instance, IntPtr dest);
    [DllImport("opencv_sample")]
    private static extern int aruco_detect(IntPtr instance, bool drawOutputImage);
    [DllImport("opencv_sample")]
    private static extern IntPtr aruco_get_markers(IntPtr instance);

    ...

    [StructLayout(LayoutKind.Sequential)]
    struct MarkerResult
    {
        [MarshalAs(UnmanagedType.I4)]
        public int id;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst=3)]
        public double[] position;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst=4)]
        public double[] orientation;
    }

    void Start()
    {
        ...
        InitializeAruco();

        mutex_ = new Mutex();
        thread_ = new Thread(() => {
            try {
                for (;;) {
                    Thread.Sleep(0);
                    if (!isArucoUpdated_) {
                        mutex_.WaitOne();
                        var num = aruco_detect(aruco_, false);
                        GetMarkers(num);
                        mutex_.ReleaseMutex();
                        isArucoUpdated_ = true;
                    }
                }
            } catch (Exception e) {
                if (!(e is ThreadAbortException)) {
                    Debug.LogError("Unexpected Death: " + e.ToString());
                }
            }
        });

        thread_.Start();
    }

    void OnDestroy()
    {
        FinalizeAruco();
        thread_.Abort();
    }

    void InitializeAruco()
    {
        if (!isWebCamInitialized_) return;

        var path = GetFilePath(cameraParamsFileName);
        aruco_ = aruco_initialize(width_, height_, markerSize, path);
    }

    void FinalizeAruco()
    {
        aruco_finalize(aruco_);
    }

    void GetMarkers(int num)
    {
        markers_.Clear();
        var ptr = aruco_get_markers(aruco_);
        var size = Marshal.SizeOf(typeof(MarkerResult));
        for (int i = 0; i < num; ++i) {
            var data = new IntPtr(ptr.ToInt64() + size * i);
            var marker = (MarkerResult)Marshal.PtrToStructure(data, typeof(MarkerResult));
            markers_.Add(marker);
        }
    }

    void OnMarkerDetected(int id, Vector3 pos, Quaternion rot)
    {
        if (arObjects_.ContainsKey(id)) {
            var obj = arObjects_[id].transform;
            obj.localPosition = pos;
            obj.localRotation = rot;
        } else {
            var obj = Instantiate(prefab) as GameObject;
            obj.transform.parent = Camera.main.transform;
            obj.transform.localPosition = pos;
            obj.transform.localRotation = rot;
            arObjects_.Add(id, obj);
        }
    }

    Vector3 GetMarkerPos(double[] p)
    {
        var unityFov = Camera.main.fieldOfView;
        var xyScaleFactor = Mathf.Tan(Mathf.Deg2Rad * unityFov) / Mathf.Tan(Mathf.Deg2Rad * cameraFov); // should be 1f
        var realToUnityScale = unityMarkerSize / markerSize;
        var x = -(float)p[0] * xyScaleFactor;
        var y = -(float)p[1] * xyScaleFactor;
        var z =  (float)p[2];
        return new Vector3(x, y, z) * realToUnityScale;
    }

    Quaternion GetMarkerRot(double[] q)
    {
        var x = -(float)q[2];
        var y =  (float)q[1];
        var z =  (float)q[0];
        var w = -(float)q[3];
        return new Quaternion(x, y, z, w);
    }

    void Update()
    {
        if (isWebCamInitialized_ && webcamTexture_.didUpdateThisFrame) return;

        while (!isArucoUpdated_) Thread.Sleep(1);

        var pixels = webcamTexture_.GetPixels32();
        var handle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
        var ptr    = handle.AddrOfPinnedObject();
        aruco_set_image(aruco_, ptr);
        handle.Free();

        mutex_.WaitOne();
        foreach (var marker in markers_) {
            var pos = GetMarkerPos(marker.position);
            var rot = GetMarkerRot(marker.orientation);
            OnMarkerDetected(marker.id, pos, rot);
        }
        isArucoUpdated_ = false;
        mutex_.ReleaseMutex();

        ...
    }
}

GetMarkers()Marshal.PtrToStructure() を使って C# 側で作成した同じ形の構造体(MarkerResult)に先ほど返した先頭ポインタをベースにそのまま詰めています。

そして Unity の世界に合うように位置姿勢を修正します。ArUco で認識した座標はカメラから見たローカル座標系になるので、Unity では単純に Camera の子として AR 用のオブジェクトを生成し、localPosition / localRotation に得た情報を入れればピッタリとマーカにくっついて表示されます。

f:id:hecomi:20150916013948p:plain

おわりに

色々と端折ってしまいましたが、やり方さえ分かれば Unity x Android で画像ベースのライブラリを動かすのもそう難しくはありません。是非色んなライブラリを組み込んで遊んでみてください。