simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

Unity:OpenCVを直接使う4

今回は Android 端末で OpenCV を直接使う Unity 製アプリを作ってみます。

先に Unity 側のコードを書くと次の通り
前回の WebCam 画像の取得の枠組みを用意して、そこの Update 関数にて次のようにカメラ画像の pixel 配列の先頭アドレスを固定して渡します。

	void Update () {
        if (webCamHelper.webCamTexture && webCamHelper.webCamTexture.didUpdateThisFrame)
        {
            Color32[] colors = webCamHelper.webCamTexture.GetPixels32();
            GCHandle handle = GCHandle.Alloc(colors, GCHandleType.Pinned);
            System.IntPtr pinnedPointer = handle.AddrOfPinnedObject();

            cv_work(pinnedPointer, webCamHelper.webCamTexture.width, webCamHelper.webCamTexture.height);

            handle.Free();
        }	
	}

では cv_work 関数をどうやって呼べるようにするか、手順を記録しておきますね。

OpenCVAndroid 用のパッケージを次の公式URLより取得します
opencv.org

今回は Assets フォルダの外に AndroidAssets フォルダを作って、そこに jni フォルダを作り、その下に opencv-3.4.2-android-sdk フォルダとして配置しました。
ここからの手順は Android 用の ndk build を参考に行っていきます。
プロジェクトのビルド  |  Android NDK  |  Android Developers
書式の意味が知りたくなったら、こちらのドキュメントを参照してください。

手順だけ示すと、まず jni フォルダ直下に Android.mk ファイルと Application.mk ファイルを作って置きます。
Application.mk ファイルを作ったら、以下のようにサポートが必要なAndroid端末の CPU アーキテクチャを指定します。

APP_STL      := gnustl_static
APP_CPPFLAGS := -frtti -fexceptions
APP_ABI      := armeabi-v7a arm64-v8a
APP_PLATFORM := android-14

アーキテクチャの指定子はここから選ぶみたいですね。
https://developer.android.com/ndk/guides/abis?hl=ja

Android.mk ファイルにはプロジェクト構成を書きます。
今回は OpenCV の静的ライブラリをインクルードして、適当なモジュール名、適当なソースファイルを指定しています。

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

# for OpenCV
OPENCV_CAMERA_MODULES:=on
OPENCV_INSTALL_MODULES:=on
OPENCV_LIB_TYPE:=STATIC
include opencv-3.4.2-android-sdk/OpenCV-android-sdk/sdk/native/jni/OpenCV.mk

# for Module
LOCAL_MODULE := unitydeopencv
LOCAL_SRC_FILES := unity_opencv.cpp
LOCAL_CFLAGS += -std=c++11

include $(BUILD_SHARED_LIBRARY)

では適当なソースファイルとして指定した unity_opencv.cpp の中身を確認してみましょう。

#include <opencv2/opencv.hpp>

extern "C"
{
    void cv_work(unsigned char* imgptr, int width, int height)
    {
        cv::Mat img(height, width, CV_8UC4, imgptr);
        cv::Mat gray_img;
        cv::cvtColor(img, gray_img, CV_BGRA2GRAY);
        return;
    }
}

jni フォルダをカレントにコマンドプロンプトから ndk-build コマンドを実行します。
ndk が古い(11とかだ)と警告が出るので、新しい ndk をこちらから入手しました。
NDK のダウンロード  |  Android NDK  |  Android Developers

ndk-build コマンドに成功すると lib~.so ファイルが libs フォルダに作られます。
あとはこれらを Unity の Assets/Plugins/Android フォルダの下へ libs フォルダごと配置します。
これで dllimport が可能になるはずです。

Unity 側のコードで次の宣言をすることで Android での実行時に関数実装を呼び出せるようになります。
デバッグするなら Windows の DLL も同じコードから作れるといいですね。

    [DllImport("unitydeopencv")]
    private static extern void cv_work(System.IntPtr imgptr, int width, int height);

Unity 側で OpenCV の計算結果を画像として表示する方法を示して「UnityでOpneCVを直接使う」シリーズを締めくくりたいと思います。

C++ コード側は引数を一つ増やして、確認用に4ch画像に戻し、メモリコピー

#include <opencv2/opencv.hpp>

extern "C"
{
    void cv_work(unsigned char* imgptr, int width, int height, unsigned char* resultptr)
    {
        cv::Mat img(height, width, CV_8UC4, imgptr);
        cv::Mat gray_img;
        cv::cvtColor(img, gray_img, CV_BGRA2GRAY);
        cv::cvtColor(gray_img, img, CV_GRAY2BGRA);
        memcpy(resultptr, img.data, img.total() * img.elemSize());
        return;
    }
}

利用側では、受け取った Color32 配列をテクスチャに適用します。

using System.Runtime.InteropServices;
using UnityEngine;

[RequireComponent(typeof(WebCamHelper))]
public class UnityDeOpenCV : MonoBehaviour {

    [DllImport("unitydeopencv")]
    private static extern void cv_work(System.IntPtr imgptr, int width, int height, System.IntPtr resultptr);

    public GameObject quad;

	void Start () {
        webCamHelper = GetComponent<WebCamHelper>();
        webCamHelper.onInitialized.AddListener(OnInitalizedCamera);
        webCamHelper.InitializeCamera();
    }
	
	void Update () {
        if (webCamHelper.webCamTexture && webCamHelper.webCamTexture.didUpdateThisFrame)
        {
            Color32[] colors = webCamHelper.webCamTexture.GetPixels32();
            GCHandle handle = GCHandle.Alloc(colors, GCHandleType.Pinned);
            System.IntPtr pinnedPointer = handle.AddrOfPinnedObject();

            Color32[] colorsR = resultImage.GetPixels32();
            GCHandle handleR = GCHandle.Alloc(colorsR, GCHandleType.Pinned);
            System.IntPtr pinnedPointerR = handleR.AddrOfPinnedObject();

            cv_work(pinnedPointer, webCamHelper.webCamTexture.width, webCamHelper.webCamTexture.height, pinnedPointerR);

            resultImage.SetPixels32(colorsR);
            resultImage.Apply(false);

            handleR.Free();
            handle.Free();
        }	
	}

    void OnInitalizedCamera()
    {
        int width = webCamHelper.webCamTexture.width;
        int height = webCamHelper.webCamTexture.height;

        resultImage = new Texture2D(width, height, TextureFormat.ARGB32, false);
        if (quad)
        {
            quad.GetComponent<Renderer>().material.mainTexture = resultImage;
        }
        
    }

    Texture2D resultImage;
    WebCamHelper webCamHelper;
}

あとは Unity の Build Setting ダイアログから Switch Platform にて Android を選択して、Other Settings の Package Name に適当なパッケージ名を書き込んで
開発者モードを有効化した Android 端末を PC に接続して、Build and Run ボタンを押します。

Android SDK バージョンが足りないとか言ってくる場合は、なんとかして SDK Manager を見つけ出して、求められた API をインストールします。
JDKのパスの設定先も 9.0 系だと Gradle 作成エラーが起きるので 8.0 系をインストールしてこれを与えてエラーを解決しました。

期待通り、Quad にカメラのグレースケール化された画像が表示されることを確認できました。
直接 OpenCV に Unity からカメラ画像を渡すことができているので、あとは C++ コード側を色々頑張れば OpenCV パワー全開の Android アプリが作れますね。

コンピュータビジョンに強い学者と、適当にプログラム書けるエンジニアがタッグを組めばきっと面白い世の中になると思います。