はじめに
Unity で Android 用のアプリを作る際、Unity だけで出来ないことをやろうとするとプラグインを利用することになります。Android 向けのプラグインは大きく分けて 2 種類あり、一つは Android の機能を利用する Java プラグイン、もう 1 つは C/C++ のコードを NDK でビルドして使うネイティブプラグインです。
今回はネイティブプラグインを利用して OpenCV と ArUco を使った AR な Android アプリを作る方法について解説します。まず、OpenCV を利用しない簡単なプラグインの作成方法を紹介し、その後 OpenCV を使ったサンプルを解説、最後に ArUco を利用して AR のサンプルを作ってみる、という流れで書いていこうと思います。
Unity で AR するよ、という趣旨ではなく、どちらかと言うとどういう風に作るかの解説になるので、単純に AR をする方法をお探しでしたら Vuforia の方が簡単だと思います。
デモ
Galaxy S6 上で動かしてます。ちょっと横着しているところがあって遅延が目立ちますがそこそこ動きます。
サンプルプロジェクト
以下に今回作成したサンプルプロジェクトを置いてあります。
環境
- Mac OS X Yosemite 10.10.5
- Unity 5.1.2f1
- android-ndk-r10e (64-bit)
- OpenCV 2.4.11 (Mac) / 3.0.0-rc1 (Android)
- Aruco 1.3.0
ネイティブプラグインの作成
サンプルとして以前のエントリ でも紹介したことのある市松模様を表示するものを書いてみます。
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.mk
と Android.mk
が jni
ディレクトリに必要です。
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 のライブラリとして認識されていることを確認します。
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 側のお作法が大変ですが...)。
- その他の方法: Unityでテクスチャを読む7つの方法 - テラシュールブログ
ビルド、実機確認
最後に Android 用のプロジェクトの設定をしてビルドします。具体的には、Build Settings で Android へ Platform をスイッチし、Player Settings で適当な Identifier を指定します。
これで Android 端末を USB で接続し、ビルド・実行すると以下のようになります。
エディタ上でも確認できるようにする
このままだと毎回実機に送って作業しなければならずデバッグが大変です。そこで Editor 上でも確認できるように PC 向けにビルドをします。例えば Mac で作業しているのであれば Xcode で Bundle を出力するプロジェクトを作成し、同じソースコードを参照するようにします。
ビルドした .bundle は Plugins/x86_64
へ出力するように設定しておきます。これで Editor 上でも市松模様を確認できます。
ビルドスクリプトを書いて、ndk-build
と xcodebuild
が両方走るようにしても良いかもしれません。なお、Windows であれば IDE が Visual 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_INCLUDES
や LOCAL_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
を書いたコードに応じて自動的に権限が追加されたものを用意してくれます。
- Unity - マニュアル: Android 用のプラグインをビルド
- Unity3D - INTERNETパーミッションが勝手につく等,UnityとAndroidManifestのよくわからないところ - Qiita
手動で権限を追加するには、これを記述した AndroidManifest.xml
を Assets/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 を経由して画像を読み取りテクスチャとして貼り付けています。
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; }
ディレクトリ構造
ちなみに、今までのところのディレクトリの構造は以下のようになっています。
Unity
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 を使う最後の準備としてカメラを利用してみます。
カメラは直接 OpenCV の cv::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
へ自動的にカメラへのアクセス権限が追加されるので後はこのままビルドすれば以下のように加工したカメラ画をテクスチャとして利用できます。
ArUco を組み込む
いよいよ ArUco を使ったコードを見ていきます。
ArUco の準備とビルド
- ArUco: a minimal library for Augmented Reality applications based on OpenCv | Aplicaciones de la Visión Artificial
- ArUco - Browse Files at SourceForge.net
ArUco 1.3.0 をダウンロードして展開します。android
ディレクトリが中にあり、Aruco.mk
があるので一見これを include すれば良さそうですが、中を見ると別途 cmake でビルドしたものを読み込むようになっています。android.toolchain.cmake
及び、これを利用してビルドするスクリプトが scripts/*.sh
が用意されているのですが、環境依存なコードが多く、直すのが面倒なのでこれらは使いません。代わりに自分でビルドを行うようにします。
src
を jni
にリネームして以下の 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_); } }
先ほどのカメラのサンプルの例で線画にしているところで認識をしている感じです。
結果
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
)に先ほど返した先頭ポインタをベースにそのまま詰めています。
- C#のためのC++の配列、構造体、ポインタの変換処理 | TomoSoft
- .net - Marshalling an array of structures from C++ to C#? - Stack Overflow
そして Unity の世界に合うように位置姿勢を修正します。ArUco で認識した座標はカメラから見たローカル座標系になるので、Unity では単純に Camera の子として AR 用のオブジェクトを生成し、localPosition
/ localRotation
に得た情報を入れればピッタリとマーカにくっついて表示されます。
おわりに
色々と端折ってしまいましたが、やり方さえ分かれば Unity x Android で画像ベースのライブラリを動かすのもそう難しくはありません。是非色んなライブラリを組み込んで遊んでみてください。