おもちゃラボ

Unityで遊びを作ってます

【Unity】Macで使えるNative Pluginを作る(OpenCV編)

UnityでOpenCVを使いたい場合は、OpenCV for Unityを使う方法と、自分で使いたい関数だけをNative Pluginとして用意する方法があります。ここでは、アセットを使わず、Native PluginからOpenCVの機能を使う方法を紹介します。

UnityでOpenCVを使ったNative Pluginを作る手順は次のようになります。

Homebrewをインストール

コマンドラインで次のコマンドを打ってHomebrewをインストールします

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

パッケージを最新にするため、Homebrewをアップデートします。

brew update

OpenCVをインストールする

brew install -v cmake
brew tap homebrew/science
brew install opencv3

これで/usr/local/Cellar/opencv3以下に、OpenCVのライブラリ一式がインストールされました。

XcodeでNative Pluginを作る

続いて、Unityから受け取った画像をNative Pluginでグレースケールに変換して、それをUnityに返すプログラムを作ります。

f:id:nn_hokuson:20170512201403j:plain:w500

Unityで使うMacのNative PluginはBundleとして作成します。Xcodeを起動してMacOS>Framewrok & Library>Bundleを選択してください。「Product Name」は「TestPlugin」にしておきます。

f:id:nn_hokuson:20170512192724p:plain:w400

XCodeからOpenCVが使えるようにパスを通しておきましょう。ナビゲーションビューでTestPluginを選択し、「Build Settings」→「Header Search Path」の欄に「/usr/local/Cellar/opencv3/3.2.0/include/」と入力します(バージョンは適宜書き換えてください)。

f:id:nn_hokuson:20170512200032j:plain

次に「Build Phasesを」選択し、Link Binary With Librariesの欄に使用するライブラリをドラッグ&ドロップします。とりあえず次のものをドラッグ&ドロップしておきましょう。

・libopencv_core.3.2.0.dlyb
・libopencv_highgui.3.2.0.dlyb
・libopencv_imageproc.3.2.0.dlyb

 続いてNative Pluginとして使うためのプログラム(Plugin.cpp)とヘッダファイル(Plugin.pch)を作成します。Unityから使えるようにヘッダファイルは「extern "C"」でくくっておいてください。

#include "Plugin.pch"
#include <opencv2/opencv.hpp>

void conv(unsigned char* arr, int w, int h)
{
    cv::Mat img(cv::Size(w, h), CV_8UC4, cv::Scalar(0, 0, 0));
    cv::Mat gray(cv::Size(w, h), CV_8UC1, cv::Scalar(0, 0, 0));

    // char* -> Mat
    int i = 0;
    for(int y = 0; y < img.rows; ++y){
        for(int x = 0; x < img.cols; ++x){
            for(int c = 0; c < img.channels(); ++c){
                img.data[ y * img.step + x * img.elemSize() + c ] = arr[i++];
            }
        }
    }
    
    // RGBA -> GRAY
    cv::cvtColor(img, gray, CV_RGBA2GRAY);
    cv::cvtColor(gray, img, CV_GRAY2RGBA);
    
    // Mat -> char*
    i = 0;
    for(int y = 0; y < img.rows; ++y){
        for(int x = 0; x < img.cols; ++x){
            for(int c = 0; c < img.channels(); ++c){
                arr[i++] = img.data[ y * img.step + x * img.elemSize() + c ];
            }
        }
    }
}
extern "C" {
    void conv(unsigned char* arr, int w, int h);
}

ここではグレースケールに変換するconv関数を作っています。最終的にはこのconv関数をUnity側から呼び出す事になります。引数にはピクセルの配列と画像の縦幅・横幅を受け取っています。Unity側ではcv::Mat型は使えないため、ここではunsigned charの配列として、画像のピクセルデータをやり取りしています。conv関数の内部ではOpenCVを使ってグレースケールに変換した後、再度ピクセル配列に書き戻しています。

メニューバーから「Product」→「Build」を選択し、プロジェクトをビルドするとProductsフォルダにTestPlugin.bundleが生成されます。

f:id:nn_hokuson:20170512193700p:plain:w250

UnityからNative Pluginを叩いて実行する

Unityでプロジェクトを作成し、Assetフォルダの下にPluginフォルダを作ってください。作成したPluginフォルダの中に先程の「TestPlugin.bundle」を入れます。このとき、XcodeのProductsフォルダからUnityに直接ドラッグ&ドロップすることが出来ます。

f:id:nn_hokuson:20170512194037p:plain:w500

最後にこのバンドルをNative Pluginとして使用するためのスクリプトを作成します。ImageProcessing.csという名前でスクリプトを作り、次のプログラムを入力して下さい。

public class ImageProcessing : MonoBehaviour {

    [DllImport ("TestPlugin0")]
    private static extern void conv(IntPtr img, int w, int h);

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

    void Start () {

        // ピクセルデータを取り出す
        Texture2D mainTexture = (Texture2D) GetComponent<Renderer> ().material.mainTexture;
        mainTexture.filterMode = FilterMode.Point;
        pixels_ = mainTexture.GetPixels32();
        pixels_handle_ = GCHandle.Alloc(pixels_, GCHandleType.Pinned);
        pixels_ptr_ = pixels_handle_.AddrOfPinnedObject();
    
        int w = mainTexture.width;
        int h = mainTexture.height;

        // Native Pluginを呼び出す
        conv (pixels_ptr_, w, h);

        // ピクセルデータからテクスチャを作る
        Texture2D change_texture = new Texture2D (w, h, TextureFormat.RGBA32, false);
        change_texture.filterMode = FilterMode.Point;
        change_texture.SetPixels32 (pixels_);
        change_texture.Apply ();

        GetComponent<Renderer> ().material.mainTexture = change_texture;
    }
}

このスクリプトではテクスチャからピクセルデータを取り出し、IntPtr型に変換してからNative Pluginにデータを渡しています。Native Pluginの中でグレースケール変換して、そのデータを再度Unity側でテクスチャにセットしています。

このスクリプトをPlaneオブジェクトにアタッチしてください。planeには適当な画像を配置しておきます。

f:id:nn_hokuson:20170512194449j:plain

実行すると、Planeに設定した画像が、グレースケールの画像に変換されました。なんか白黒にすると遺影みたいですが・・・(笑)

f:id:nn_hokuson:20170512194455p:plain:w400

プラグインのロードには注意が必要です!

Unityでは一度スクリプトからNative Pluginをロードすると、Unityを再起動するまでUnloadされない仕様になっています。

Once a native plugin is loaded from script, it's never unloaded. if you deselect a native plugin and it's already loaded, please restart Unity.

f:id:nn_hokuson:20170512194614p:plain:w350

つまり、Native Pluginのプログラムを更新して、古いものと置き換えたとしてもUnityを再起動するまでは古いプラグインが使われます・・・・ひどい

私は毎回、Pluginの名前を変えてからプロジェクトにドラッグ&ドロップしています。なぜUnityをだましだまし使わなくてはいけないのか・・・

まとめ

この記事では、Native PluginでOpenCVを使う方法を紹介しました。また、OpenCV for Unityは$95でAsset Storeで販売されています。Native Pluginなんて作るの面倒くさいよ、という方はこちらを使ってみて下さい。

f:id:nn_hokuson:20170512192332j:plain:w400

[asin:4877833986:detail]