凹みTips

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

Unity で OpenCV で作成したテクスチャをネイティブプラグイン経由で利用してみた

はじめに

有償のプロ版限定ですが、Unity でネイティブプラグインと連携してテクスチャを操作できると、OpenCV で色々変更を加えたりと表現の幅が色々と広がります。調べてみましたのでメモします。

環境

先行技術

warapuri さんは OpenCvSharpOpenCVC# から使えるようにする wrapper)を利用して OpenCV 画を利用していました。今回は OpenCVC++ の領域で利用して、その結果だけ貰う形にします。

C++ 側で作ったテクスチャを Unity に持ってくる

プラグインの作り方は以下をご参照下さい。

32bit で bundle を作成する必要があります(Win なら dll)。まず適当に以下の様なコードを記述した bundle を Xcode でビルドします。

mesh.h

extern "C" {
    void createCheckTexture(unsigned char* arr, int w, int h, int ch);
}

mesh.cpp

#include "test.h"

void createCheckTexture(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;
            }
        }
    }
}

白黒の市松模様を与えられたポインタにセットするものです。ビルドして出来た .bundle ファイルを Unity のプロジェクトの Assets/Plugins 下に配置し、これを利用するコードを Unity で書いてみます。

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

public class Test : MonoBehaviour 
{
	[DllImport ("UnityNativePluginTest")]
	private static extern void createCheckTexture(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();
		// スクリプトがアタッチされたオブジェクトのテクスチャをコレにする
		renderer.material.mainTexture = texture_;
	}

	void Update() 
	{
		// ネイティブ側でテクスチャを生成
		createCheckTexture(pixels_ptr_, texture_.width, texture_.height, 4);
		// セットして反映させる
		texture_.SetPixels32(pixels_);
		texture_.Apply();
	}

	void OnApplicationQuit()
	{
		// GC 対象にする
		pixels_handle_.Free();
	}
}

これで以下のように表示されます。
f:id:hecomi:20140113144613p:plain
この例は固定のテクスチャでしたが、動的なテクスチャを作成すれば Unity だけだと面倒だったり難しい処理を代わりにネイティブ側にやってもらうことが可能になります。例えば以前記事を書いた PS Eye からカメラ画をもらってくるもの(PS Eye × 2個 を Unity 上で Oculus SDK と共に使ってみた - 凹みTips)のように特殊なデバイスから画を取得したり、動画ファイルを処理したりと色々出来ると思います。次はこれ相当の処理を OpenCV を利用してやってみます。

OpenCV で取得したカメラ画を持ってくる(+生画を別ウィンドウ表示)

OpenCV の 32 bit ビルド

brew だと --32-bit オプションでビルドに失敗するので、自前でビルドします。

OpenCV 2.4.8 をダウンロードしてきます。

$ unzip opencv-2.4.8.zip
$ cd opencv-2.4.8
$ export CMAKE_OSX_ARCHITECTURES="i386;x86_64"
$ cmake .
$ make -j8

これでユニバーサルバイナリが出来ます。なお、環境を汚すのが嫌な(全部 brew で管理したい)ので、install はしなくて構いません。

bundle の作成

Xcode の「Build Phases > Link Binary With Libraries」で、リンクするライブラリを選択します。今回のアプリでは以下の3つをリンクします。

  • libopencv_core.2.4.8.dylib
  • libopencv_highgui.2.4.8.dylib
  • libopencv_imgproc.2.4.8.dylib

いずれも「opencv-2.4.8/lib」下に生成されています。この上で以下のようなコードを書きます。

opencv_camera.h

extern "C" {
    void* getCamera();
    void releaseCamera(void* camera);
    void getCameraTexture(void* camera, unsigned char* data, int width, int height);
}

opencv_camera.cpp

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

void* getCamera()
{
    // ウィンドウを開く
    cv::namedWindow("hoge", CV_WINDOW_AUTOSIZE | CV_WINDOW_FREERATIO);
    return static_cast<void*>(new cv::VideoCapture(0));
}

void releaseCamera(void* camera)
{
    // ウィンドウを閉じる
    cv::destroyWindow("hoge");
    auto vc = static_cast<cv::VideoCapture*>(camera);
    delete vc;
}

void getCameraTexture(void* camera, unsigned char* data, int width, int height)
{
    auto vc = static_cast<cv::VideoCapture*>(camera);
    
    // カメラ画の取得
    cv::Mat img;
    *vc >> img;
    
    // リサイズ
    cv::Mat resized_img(height, width, img.type());
    cv::resize(img, resized_img, resized_img.size(), cv::INTER_CUBIC);
    
    // 別ウィンドウの画を更新
    cv::imshow("hoge", resized_img);
    
    // RGB --> ARGB 変換
    cv::Mat argb_img;
    cv::cvtColor(resized_img, argb_img, CV_RGB2BGRA);
    std::vector<cv::Mat> bgra;
    cv::split(argb_img, bgra);
    std::swap(bgra[0], bgra[3]);
    std::swap(bgra[1], bgra[2]);
    std::memcpy(data, argb_img.data, argb_img.total() * argb_img.elemSize());
}

これでビルドすれば bundle が出来ます。これを先ほど同様 Assets/Plugins 下に放り込んでおきます。
これを利用する C#スクリプトを次のように書きます。

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

public class OpenCV : MonoBehaviour 
{
	[DllImport ("OpenCVTest")]
	private static extern IntPtr getCamera(); 
	[DllImport ("OpenCVTest")]
	private static extern void releaseCamera(IntPtr camera); 
	[DllImport ("OpenCVTest")]
	private static extern void getCameraTexture(IntPtr camera, IntPtr data, int width, int height); 

	private IntPtr camera_;
	private Texture2D texture_;
	private Color32[] pixels_;
	private GCHandle pixels_handle_;
	private IntPtr pixels_ptr_;

	void Start() 
	{
		camera_ = getCamera();
		texture_ = new Texture2D(320, 180, TextureFormat.ARGB32, false);
		pixels_ = texture_.GetPixels32();
		pixels_handle_ = GCHandle.Alloc(pixels_, GCHandleType.Pinned);
		pixels_ptr_ = pixels_handle_.AddrOfPinnedObject();
		renderer.material.mainTexture = texture_;
	}

	void Update() 
	{
		getCameraTexture(camera_, pixels_ptr_, texture_.width, texture_.height);
		texture_.SetPixels32(pixels_);
		texture_.Apply();
	}

	void OnApplicationQuit()
	{
		pixels_handle_.Free();
		releaseCamera(camera_);
	}
}

これを適当なオブジェクトにアタッチします。

実行結果

f:id:hecomi:20140116011418p:plain
テクスチャとして反映されていますし、生のカメラ画のウィンドウも一緒に表示されました!便利です。

おわりに

次回は、Unity のカメラからテクスチャを貰ってきてそれを別ウィンドウに表示させるところをやりたいと思います。