一覧に戻る

Android ネイティブAPIのプラグインAPI

2017.09.22AndroidネイティブAPI

プラグインAPI

このトピックは複数のセクションで構成されています。Wikitude SDKプラグインの概念、プラットフォーム固有の事項とプラグインをWikitude SDKに登録する方法について説明してから、サンプルに付属するサードパーティ製プラグインの実装例を紹介します。

Wikitude SDK プラグインAPIについて

技術的に見ると、プラグインは基底クラスであるPluginクラスから派生したC++またはJavaクラスです。Pluginクラスには、ライフサイクル処理と、プラグインを有効/無効にするオプションに加えて、cameraFrameAvailable、update、startRender、endRenderの4つのオーバーライド可能なメソッドがあります。cameraFrameAvailableは、カメラが新しいフレームを作成するたびに呼び出されます。updateは、ターゲット認識の後に呼び出されます。startRenderとendRenderはそれぞれ、Wikitude SDKがレンダリングを実行する前および実行した後に呼び出されます。

プラグインを操作するときに覚えておくべき最も重要なことは、プラグインには一意の識別子が必要であるということです。Wikitude SDKにとって既知の識別子を持つプラグインを登録しようとした場合、登録メソッドはfalseを返します。

Pluginクラス

class Plugin {
   public:
      Plugin(std::string identifier_);
      ~Plugin();
      string getIdentifier() const; // returns a unique plugin identifier
      bool processesColorCameraFrames(); // returns true if the plugins wants to process color frames instead of bw

      void setEnabled(bool enabled_);
      bool isEnabled();

      string callJavaScript(string javaScriptSnippet); // evaluates the given JavaScript snippet in the currently loaded ARchitect World context.

   protected:
      void initialize(); // called when the plugin is initially added to the Wikitude SDK
      void pause(); // called when the Wikitude SDK is paused e.g. the application state changes from active to background
      void resume(uint pausedTime_); // called when the Wikitude SDK resumes e.g. from background to active state. pausedTime represents the time in milliseconds that the plugin was not updated.
      void destroy(); // called when the plugin is removed from the Wikitude SDK

      void cameraFrameAvailable(const Frame&; cameraFrame_); // called each time the camera has a new frame
      void update(const vector<RecognizedTarget> recognizedTargets_); // called each time the Wikitude SDK renders a new frame

      void startRender(); // called before any Wikitude SDK internal rendering is done
      void endRender(); // called right after any Wikitude SDK internal rendering is done

   protected:
      string      _identifier;
      bool        _enabled;
};

これらのメソッドを適切に実装することで、プラグインから任意の目的のためにカメラフレームを読み取ることができ、そのYUV画像はWikitude SDKのコンピュータビジョンエンジンでも処理されます。

ターゲットに関する情報

Wikitude SDKで画像認識を実行している場合、ターゲットが認識されると、認識されたターゲットがupdateメソッドのRecognizedTargetに格納されます。これにより、プラグインからRecognizedTargetクラス(カメラビュー内のターゲットの詳細をラップするクラス)を操作し、ターゲットの情報を読み出して任意の目的に使用できます。また、ターゲットまでの距離も取得できます。

class RecognizedTarget {
   public:
      const string&    getIdentifier() const; // the identifier of the target. The identifier is defined when the target is added to a target collection
      const Mat4&      getModelViewMatrix() const; // the model view matrix that defines the transformation of the target in the camera frame (translation, rotation, scale)
      const Mat4&      getProjectionMatrix() const;
      const float      getDistanceToCamera() const; // represents the distance from the target to the camera in millimeter
};

プラットフォーム固有の事項

C++で記述したプラグインをAndroidで使用できるようにするには、C++コードから、サポートされているCPUアーキテクチャごとにバイナリを作成する必要があります。このプロセスをできるだけ簡単にするため、Android NDKメイクファイルと、プラグインをWikitude SDKに渡すテンプレートコードが用意されています。次のセクションで、これらのテンプレートを対象のプラグインに合わせて変更する方法を説明します。

複数のC++プラグインをアプリケーションで使用する場合は、すべてのプラグインを1つの共有ライブラリにパッケージ化する必要があります。その理由は、C++プラグインをWikitude SDKに登録する際にJNIが使用されており、これを実行するためのシンボルを一意にしなければならないためです。

Android C++ プラグインライブラリの作成

必要なすべてのファイルは、Wikitude SDK AndroidパッケージのSDKPluginTemplateフォルダにあります。 Android NDKをまだセットアップしていない場合は、公式ガイドに従ってください。

SDKPackageRoot/SDKPluginTemplate/jniフォルダにあるAndroid.mkファイルを見てみましょう。まず、メイクファイルの場所を基準としたソースファイルの相対パスを含む変数を宣言し、このパスをLOCAL_PATHに設定します。すべてのインクルードファイルが存在する場所と、コンパイルするファイルを定義します。samplePluginではAndroidログを使用しているので、Androidネイティブのログをリンクします。

LOCAL_PATH := $(call my-dir)/..
SRC_DIR := $(LOCAL_PATH)/src

include $(CLEAR_VARS)

LOCAL_PATH := $(SRC_DIR)
include $(CLEAR_VARS)

LOCAL_MODULE := samplePlugin

LOCAL_C_INCLUDES := $(SRC_DIR)
LOCAL_SRC_FILES := __YOUR_PLUGIN__.cpp JniRegistration.cpp Plugin.cpp

LOCAL_LDLIBS += -llog

include $(BUILD_SHARED_LIBRARY)

PluginLoader/srcフォルダに、プラグインを正しくコンパイルしてリンクするために必要な各種ソースファイルがあります。これらのうち、__YOUR_PLUGIN__.hと__YOUR_PLUGIN__.cpp以外は変更しないでください。ただし、JniRegistration.cppは少し変更を加える必要があります。その例を以下に示します。includeディレクティブとコンストラクター呼び出しを対象のプラグイン名に変更します。複数のC++プラグインを使用する場合は、必要なプラグインをcPluginsArray配列に追加し、numberOfPluginsの値をプラグインの数と同じにします。

複数のプラグインを1つの共有ライブラリにパッケージ化し、それらのプラグインのサブセットのみをインスタンス化する場合は、Javaからライブラリをロードするときにこのメソッドに識別子を渡すことができます。これにより、jPluginNameの値によってどのプラグインを作成するかを指定できます。

#include <jni.h>

#include "Plugin.h"
#include "__YOUR_PLUGIN__.h"

extern "C" JNIEXPORT jlongArray JNICALL Java_com_wikitude_common_plugins_internal_PluginManagerInternal_createNativePlugins(JNIEnv *env, jobject thisObj, jstring jPluginName) {

    int numberOfPlugins = 1;

    jlong cPluginsArray[numberOfPlugins];
    cPluginsArray[0] = (jlong) new __YOUR_PLUGIN__("com.example.plugin");

    jlongArray jPluginsArray = env->NewLongArray(numberOfPlugins);
    if (jPluginsArray != nullptr) {
        env->SetLongArrayRegion(jPluginsArray, 0, numberOfPlugins, cPluginsArray);
    }

    return jPluginsArray;
}

プラグインのバイナリファイルをビルドするには、jniフォルダに移動してndk-buildを呼び出します。arm7、arm64、およびintel用のバイナリを含むlibsフォルダが作成されます。このlibsフォルダの内容をYourProjectRoot/app/src/main/jniLibsにコピーします。

Wikitudeのサンプルアプリケーションで提供されたプラグインをリビルドするにはプロジェクトにOpenCVを追加する必要があります。OpenCVはMarker Trackingプラグインと顔検出プラグインで使用されます。OpenCVを追加するにはOpenCV公式ページからAndroid版OpenCV3.0.0をダウンロードして、src、lib、jni、includeフォルダ以下のSDKExamplePluginsフォルダに展開したフォルダの内容を配置します。

プラグインの登録

Androidでは、プラグインをC++またはJavaで記述できます。C++とJavaの登録プロセスは少し異なります。以降のセクションで、まずC++プラグインの登録方法を示し、次にJavaプラグインの登録方法を示します。

C++プラグインの登録

C++プラグインをWikitude SDK ネイティブAPIに登録するには、WikitudeSDKインスタンスからPluginManagerを取得し、registerNativePluginを呼び出してライブラリの名前を渡します。名前の前にlibは付けず、.so拡張子も付けないでください。アクティビティのonCreateメソッドでPluginを登録する場合は、必ず先にWikitudeSDKのonCreateメソッドを呼び出してください。以下のサンプルコードは、バンドルされているサンプルのBarcodePluginActivityから抜粋したものです。

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    mWikitudeSDK.onCreate(getApplicationContext(), startupConfiguration);
    ...
    mWikitudeSDK.getPluginManager().registerNativePlugins("barcodePlugin");
}

JAVAプラグインの登録

JavaプラグインをWikitude SDK ネイティブAPIに登録するには、WikitudeSDKインスタンスからPluginManagerを取得し、registerPluginを呼び出してPluginのインスタンスを渡します。

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    mWikitudeSDK.onCreate(getApplicationContext(), startupConfiguration);
    ...
    mWikitudeSDK.getPluginManager().registerPlugin(new MyPlugin());
}

バーコードおよびQRコードリーダー

このサンプルは、人気のあるバーコードライブラリのZBarをWikitude SDKに実装する例を示します。ZBarはLGPL2.1の下でライセンスされているため、このサンプルは他のプロジェクトにも使用できます。

ZBarは、ビデオストリーム、画像ファイル、強度センサーなどのさまざまなソースからバーコードを読み取るオープンソースのソフトウェアスイートです。EAN-13/UPC-A、UPC-E、EAN-8、Code 128、Code 39、Interleaved 2 of 5、QRコードといった数多くの一般的なシンボル体系(バーコードの種類)をサポートしています。

BarcodePluginActivity.onCreateメソッドで、バーコードC++プラグインを登録します。そのために、WikitudeSDKからPluginManagerを取得し、registerNativePluginを呼び出してC++プラグインを含むネイティブライブラリの名前を渡します。その直後にinitNative()を呼び出し(これはネイティブメソッドとして宣言し、C++プラグインで実装しています)、ネイティブプラグインが保持するJavaVMポインタを初期化します。また、スキャンしたバーコードの内容を表示するonBarcodeDetectedメソッドも実装しています。このメソッドは、後でバーコードプラグインから呼び出します。

public class BarcodePluginActivity extends Activity implements ImageTrackerListener, ExternalRendering {

    private static String mCodeContent;
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mWikitudeSDK.getPluginManager().registerNativePlugins("barcodePlugin");
        initNative();
    }

    ...

    public void onBarcodeDetected(final String codeContent_) {
        mCodeContent = codeContent;
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                EditText targetInformationTextField = (EditText) findViewById(R.id.barcode_plugin_info_field);
                targetInformationTextField.setText("Scan result: " + codeContent, TextView.BufferType.NORMAL);
            }
        });
    }

    private native void initNative();
}

次に、プラグインのC++コードに進みます。まず、BarcodePlugin.hファイルを見てみましょう。バーコードプラグインを作成するため、wikitude::sdk::PluginからBarcodePluginクラスを派生させ、initialize、destroy、cameraFrameAvailable、updateをオーバーライドします。また、メンバ変数として、_worldNeedsUpdate、_image、_imageScanner、_methodIdを宣言しています。_worldNeedsUpdate変数は、ArchitectViewの更新が必要かどうかを示すインジケーターとして使用されます。_imageと_imageScannerは、バーコードのスキャンに使用するzBarのクラスです。_methodIdは、onBarcodeDetected JavaメソッドのメソッドIDを保持します。

extern JavaVM* pluginJavaVM;

class BarcodePlugin : public wikitude::sdk::Plugin {
public:
    BarcodePlugin(int cameraFrameWidth, int cameraFrameHeight);
    virtual ~BarcodePlugin();

    virtual void initialize();
    virtual void destroy();

    virtual void cameraFrameAvailable(const wikitude::sdk::Frame& cameraFrame_);
    virtual void update(const std::list<wikitude::sdk::RecognizedTarget>& recognizedTargets_);


protected:
    int                             _worldNeedsUpdate;

    zbar::Image                     _image;
    zbar::ImageScanner              _imageScanner;

private:
    jmethodID                       _methodId;
};

グローバルnamespaceで2つの変数を宣言します。一方はJavaVMへのポインタを保持し、もう一方はアクティビティへの参照を保持します。これら2つの変数を初期化するため、BarcodeActivityでinitNativeネイティブメソッドを宣言し、これを以下のように実装しました。ここでは、JNIEnvからJavaVMへのポインタを取得し、呼び出し元アクティビティインスタンスへの新しいグローバル参照を作成しています。

JavaVM* pluginJavaVM;
jobject activityObj;

extern "C" JNIEXPORT void JNICALL Java_com_wikitude_samples_plugins_BarcodePluginActivity_initNative(JNIEnv* env, jobject obj) {
    env->GetJavaVM(&pluginJavaVM);
    activityObj = env->NewGlobalRef(obj);
}

コンストラクターでは、_worldNeedsUpdateを更新が不要であることを示すゼロに設定します。さらに、zBar::Imageメンバ変数を初期化するため、そのコンストラクターにカメラフレームの幅と高さ、Y800の画像形式、nullのデータポインタ、ゼロのデータ長を渡しています。また、JavaVMを使用して、JavaVMResource(JavaVMを管理するヘルパークラス)のインスタンスを作成します。このクラスはjniHelper.cppファイルに含まれています。次に、JavaVMResourceからJava環境を取得し、_methodIdメンバを初期化します。デストラクターでは、アクティビティオブジェクトへのグローバル参照を削除します。

BarcodePlugin::BarcodePlugin(int cameraFrameWidth, int cameraFrameHeight) :
Plugin("com.wikitude.ios.barcodePluign"),
_worldNeedsUpdate(0),
_image(cameraFrameWidth, cameraFrameHeight, "Y800", nullptr, 0)
{
    JavaVMResource vm(pluginJavaVM);
    jclass clazz = vm.env->FindClass("com/wikitude/samples/plugins/BarcodePluginActivity");
    _methodId = vm.env->GetMethodID(clazz, "onBarcodeDetected", "(Ljava/lang/String;)V");
}

BarcodePlugin::~BarcodePlugin()
{
    JavaVMResource vm(pluginJavaVM);
    vm.env->DeleteGlobalRef(activityObj);
}

initializeメソッドでは、zbar::ImageScannerのsetConfigを呼び出して、サポートされているすべてのバーコードを有効にします。特定の種類のバーコードのみを読み取る場合は、まずすべてのバーコードの種類を無効にしてから、読み取るバーコードの種類を手動で1つずつ有効にするのが得策です。こうすることで、パフォーマンスが大幅に向上します。

void BarcodePlugin::initialize() {    
    _imageScanner.set_config(zbar::ZBAR_NONE, zbar::ZBAR_CFG_ENABLE, 1);
}

destroyイベントが発生したときは、zbar::Imageメンバの現在のデータポインタをnullに設定し、長さをゼロにします。

void BarcodePlugin::destroy() {
    _image.set_data(nullptr, 0);
}

最も興味深いメソッドはcameraFrameAvailableとupdateです。cameraFrameAvailableメソッドでは、set_dataを呼び出して、すでに初期化したzbar::Imageメンバ変数のデータを今受け取ったフレームデータに設定し、データの長さをフレームの幅×フレームの高さにします。次に、zBar::ImageScannerのscanメソッドを呼び出してzBar::Imageメンバインスタンスを渡し、スキャンプロセスを開始します。zBar::ImageScanner::scanメソッドは画像フレームで検出されたバーコードの数を返すので、その数をローカル変数のnに格納します。nが_worldNeedsUpdateメンバ変数に格納されている前回のフレームの結果と等しくない場合は、新しいバーコードが検出されたか、または前回のフレームにバーコードがあって今回はなかったことを示します。この場合はさらに、今回のフレームでバーコードが検出されたかどうかをチェックし、検出された場合はonBarcodeDetected Javaメソッドを呼び出してコードの内容を渡します。

void BarcodePlugin::cameraFrameAvailable(const wikitude::sdk::Frame& cameraFrame_) {
    int frameWidth = cameraFrame_.getSize().width;
    int frameHeight = cameraFrame_.getSize().height;

    _image.set_data(cameraFrame_.getLuminanceData(), frameWidth * frameHeight);

    int n = _imageScanner.scan(_image);

    if ( n != _worldNeedsUpdate ) {
        if ( n ) {

            zbar::Image::SymbolIterator symbol = _image.symbol_begin();
            JavaVMResource vm(pluginJavaVM);
            jstring codeContent = vm.env->NewStringUTF(symbol->get_data().c_str());
            vm.env->CallVoidMethod(activityObj, _methodId, codeContent);

        }
    }

    _worldNeedsUpdate = n;
}

顔検出

このサンプルは、OpenCVを使用して顔検出をWikitudeのAR体験に追加する方法を示します。

顔検出プラグインサンプルは、C++クラスのFaceDetectionPluginおよびFaceDetectionPluginConnectorと、JavaクラスのFaceDetectionPluginActivityで構成されています。OpenCVを使用して現在のカメラフレーム内で顔を検出し、JavaでOpenGLを呼び出して検出された顔の周りに枠をレンダリングします。

FaceDetectionPluginConnectorはネイティブコードとJavaの間のインタフェースとして機能し、JNIコードが含まれます。JNIはこのサンプルの焦点ではないので、その実装については詳しく説明しません。完全なコードが見たい場合は、Wikitude SDKリリースパッケージに含まれるソースコードを自由に閲覧してください。

initNativeとsetFlipFlagの2つのJavaネイティブメソッドを実装しています。前者はプラグインをOpenCVデータベースのパスで初期化し、後者はデバイスの向きの変更をPluginに知らせます。その他のfaceDetected、faceLost、projectionMatrixChanged、renderDetectedFaceAugmentationの各メソッドは、レンダリングを制御するJava Androidアクティビティを更新するためにPluginによって呼び出されます。

extern "C" JNIEXPORT void JNICALL Java_com_wikitude_samples_plugins_FaceDetectionPluginActivity_initNative(JNIEnv* env, jobject obj, jstring databasePath_) {
    ...
}

extern "C" JNIEXPORT void JNICALL Java_com_wikitude_samples_plugins_FaceDetectionPluginActivity_setFlipFlag(JNIEnv* env, jobject obj, jint flag) {
    ...
}

... ctor / dtor ...

void FaceDetectionPluginConnector::faceDetected(const float *modelViewMatrix)
{
...
}

void FaceDetectionPluginConnector::faceLost()
{
...
}

void FaceDetectionPluginConnector::projectionMatrixChanged(const float *projectionMatrix)
{
...
}

void FaceDetectionPluginConnector::renderDetectedFaceAugmentation() {
...
}

次に、FaceDetectionPluginクラスを確認します。今度も実装の詳細は省略し、プラグイン自体の使用方法に焦点を合わせます。cameraFrameAvailableメソッドでOpenCVを使用して、Wikitude SDKからプラグインに渡された現在のカメラフレームで顔を検出します。FaceDetectionPluginConnectorのインスタンスであるオブザーバーを呼び出して、結果をJavaアクティビティに通知します。基底クラスであるPluginクラスにはstartRenderとendRenderが定義されており、Wikitude SDKが実行するすべてのレンダリングの上にレンダリングするか下にレンダリングするかに応じて、どちらか一方または両方を選んでオーバーライドします。このサンプルではWikitudeのすべてのレンダリングの下にレンダリングするためstartRenderを選び、再びFaceDetectionPluginConnectorインスタンスからAndroidアクティビティを呼び出します。このサンプルではWikitude SDK画像認識の結果は使用しないので、updateは空のままにします。

... ctor/dtor ...

void FaceDetectionPlugin::cameraFrameAvailable(const wikitude::sdk::Frame& cameraFrame_) {

    ... Control Open CV ...

    if ( _result.size() ) {
        convertFaceRectToModelViewMatrix(croppedImg, _result.at(0));
        _observer->faceDetected(_modelViewMatrix);
    } else {
        _observer->faceLost();
    }
}

void FaceDetectionPlugin::startRender() {
    _observer->renderDetectedFaceAugmentation();
}

void FaceDetectionPlugin::update(const std::list<wikitude::sdk::RecognizedTarget> &recognizedTargets_) {
}

... other internally used methods ...

FaceDetectionPluginActivityでonCreateをオーバーライドし、initNativeネイティブメソッドにデータベースファイルのパスを渡してPluginを初期化します。また、onConfigurationChangedをオーバーライドしてデバイスの向きが変更されたときに通知を受け、setFlipFlagネイティブメソッドを呼び出して向きの変更をPluginに知らせます。検出された顔の周りに枠をレンダリングするため、顔の周りの矩形のレンダリングとアクティブなImageTrackerのすべてのターゲットのレンダリングを処理するGLRendererFaceDetectionPluginクラスのインスタンスを作成しています。プラグインが顔を検出するか、見失うか、または射影行列が再計算されると、適切なJavaメソッドが呼び出されてRendererインスタンスが更新されます。検出された顔の周りに枠をレンダリングする場合、PluginはrenderDetectedFaceAugmentationを呼び出します。このメソッドはstartRenderメソッド内で呼び出されるだけなので、現在のスレッドはOpenGLスレッドであり、OpenGL呼び出しをディスパッチできます。

... imports ...

public class FaceDetectionPluginActivity extends Activity implements ImageTrackerListener, ExternalRendering {

    private WikitudeSDK mWikitudeSDK;
    private GLRendererFaceDetectionPlugin mGLRenderer;
    private File mCascadeFile;
    private FaceTarget mFaceTarget = new FaceTarget();

    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        ... init native sdk ...

        ... copy database file ...

        initNative(mCascadeFile.getAbsolutePath());

        ...

        setInterfaceOrientationInPlugin();
    }

    ... other lifecycle events ...

    private void setInterfaceOrientationInPlugin() {
        ...
        setFlipFlag(x);
        ...
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        setInterfaceOrientationInPlugin();
    }

    public void onFaceDetected(float[] modelViewMatrix) {
        mFaceTarget.viewMatrix = modelViewMatrix;
        mGLRenderer.setCurrentlyRecognizedFace(mFaceTarget);
    }

    public void onFaceLost() {
        mGLRenderer.setCurrentlyRecognizedFace(null);
    }

    public void onProjectionMatrixChanged(float[] projectionMatrix) {
        mFaceTarget.projectionMatrix = projectionMatrix;
        mGLRenderer.setCurrentlyRecognizedFace(mFaceTarget);
    }

    ... other Wikitude callbacks ...

    private native void initNative(String casecadeFilePath);
    private native void setFlipFlag(int flag);
}

FaceDetectionPluginActivityまたはStrokedRectangleクラスの実装の詳細に関心がある場合は、サンプルに含まれる各クラスのソースコードを確認してください。