一覧に戻る

iOS ネイティブAPIの入力プラグインAPI

2017.09.22iOSネイティブAPI

入力プラグインAPI

このトピックではWikitude SDKネイティブの入力プラグインAPIの概念と制約について説明します。サンプルアプリケーションのソースコードは長くて複雑になるためコード全体は説明せずに、関連するソースコードを抜粋して説明します。入力プラグインAPIは、プラグインAPIを拡張したものなのでプラグインAPIを理解しておくことをお勧めします。

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

入力プラグインAPIはWikitude SDKネイティブの入力と出力を変更するために使用されます。入力の場合、任意のソースのカスタムフレームデータはWikitude SDK ネイティブAPIの入力処理として供給されます。一方、出力の場合は、Wikitude SDK ネイティブAPIの初期レンダリングの代わりにより高度な実装が可能です。

入力プラグインは、SDKの起動前、またはSDKの実行中に登録できます。Wikitude SDKが起動する前に登録されている場合、内部Wikitudeカメラの実装はまったく開始されず、Wikitude SDKは最初から入力プラグインを使用し始めます。実行時に入力プラグインが登録されている場合、内部Wikitude SDKカメラが最初に停止され、続いて新しく登録された入力プラグインに移行されます。

入力Pluginクラス

class InputPlugin: public Plugin {
public:
    using InputFrameAvailableNotifier = std::function<int(long frameId, std::shared_ptr<unsigned char> frameData)>;

public:
    InputPlugin(std::string identifier_);
    virtual ~InputPlugin();

    virtual bool requestsInputFrameRendering();
    virtual bool requestsInputFrameProcessing();

    void notifyNewInputFrame(long frameId_, std::shared_ptr<unsigned char> inputFrame_, bool managedFromOutside_ = false);

    InputRenderSettings& getRenderSettings();
    InputFrameSettings& getFrameSettings();
    virtual void prepareRenderingOfInputFrame(long frameId_);

    virtual std::shared_ptr<unsigned char> getPresentableInputFrameData();

    virtual void internalError(const std::string& errorMessage);
    void setInputFrameAvailableNotifier(InputFrameAvailableNotifier newInputFrameAvailableNotifier);

private:
    InputFrameAvailableNotifier                     _newInputFrameAvailableNotifier;

    InputFrameRenderSettings                        _renderSettings;
    InputFrameSettings                              _frameSettings;
    std::unique_ptr<InputFrameBufferController>     _inputFrameBufferController;
};

InputPluginクラスはPluginクラスから派生されます。したがって、InputPluginは通常のプラグインと同様に処理ができます。そのため、プラグインのインスタンス化と登録は同じであるため、ここでは説明を省略します。詳細については、プラグインAPIを参照してください。このトピックでは、新規機能の使用方法のみを紹介します。

カラースペース

getFrameSettings.setInputFrameColorSpace(wikitude::sdk::FrameColorSpace::YUV_420_NV21);

Wikitude SDKネイティブでは、FrameColorSpace::RGB値に対してRGBフレームデータを受け取り、FrameColorSpace::YUV_420_NV21に対して4:2:0 NV21形式のYUVデータを受け取ります。前者では、フレームデータのサイズがframeWidth * frameHeight * 3バイトで、後者ではフレームデータのサイズがframeWidth * frameHeight * 3/2バイトです。

視野

getFrameSettings.setFrameFieldOfView([_camera fieldOfView]);

setFrameFieldOfView関数のパラメータは、カメラ撮影したフレームの水平視野角を示すfloat型である必要があります。この値により、Wikitudeコンピュータビジョンエンジンはフレーム内にターゲットを正常に認識およびトラッキングできます。視野の値はデバイスによって異なるので、代表値を確保するためにこの値をフレームのソースから直接クエリすることをお勧めします。入力画像や入力動画の場合、この値は対応するメタデータから認識できる必要があります。入力カメラストリームのために、この値は対応するカメラのAPIからアクセスしなければなりません。

フレームのサイズ

getFrameSettings.setInputFrameSize({[_camera videoDimensions].width, [_camera videoDimensions].height});

setInputFrameSize関数のパラメータは、入力画像の幅と高さ(ピクセル単位)を含むwikitude::sdk::Size型である必要があります。この値は多くの場合で定数になるため、適切な値にハードコーディングしておくことができます。この値を入力画像/動画ファイルまたは入力カメラのいずれかからクエリすることをお勧めします。

デフォルトのフレームレンダリング

bool YUVFrameInputPlugin::requestsInputFrameRendering() {
    return false;
}

Wikitude SDKネイティブAPIによって入力フレームデータを処理するかどうかを示すブール値を提供するためにrequestsInputFrameRendering関数をオーバーライドすることができます。デフォルトで実装する場合はtrueを返し、フレームはWikitude SDKネイティブAPIの内部レンダリングを使用してレンダリングされます。falseを返すように関数をオーバーライドした場合は、フレームがInputPluginによってレンダリングします。

デフォルトのフレーム処理

bool YUVFrameInputPlugin::requestsInputFrameProcessing() {
    return true;
}

画像データを渡す

void notifyNewInputFrame(long frameId_, std::shared_ptr<unsigned char> inputFrame_, bool managedFromOutside_ = false);

実際の入力フレームデータをWikitude SDKネイティブAPIに渡すためにnotifyNewInputFrame関数を呼び出す必要があります。この処理では、long型のユニークなフレーム識別子とstd::shared_ptrにラップされたフレームデータが必要となり、デフォルトのフレームキャッシングを行うかどうかを示すブール値を受け入れます。パラメータのデフォルト値はfalseで、Wikitude SDKネイティブAPIのデフォルトのキャッシングを使用することを示します。独自のフレームのキャッシュ メカニズムを渡すには、この値をtrueに設定します。デフォルトのキャッシュ メカニズムは、スムーズな処理性能のためにメモリで最近の5つのフレームを保持します。ファイルリソースと入力ストリームデバイスだけがネイティブコードにアクセスできるので、このメソッドをネイティブコードから呼び出す必要があります。詳細については、Wikitude SDKネイティブの「Custom Camera」サンプルのコードを参照してください。

レンダリングの設定

InputFrameRenderSettings& getRenderSettings();

InputFrameRenderSettingsのパラメータ化インスタンスを提供するためにgetRenderSettings関数を変更することができます。デフォルトで実装する場合は_renderSettingsのデフォルトメンバーを返します。Wikitude SDK ネイティブAPIにデフォルト値と異なるレンダリング設定を提供するには、_renderSettingsを返す前に変更します。

フレームのキャッシング

virtual void prepareRenderingOfInputFrame(long frameId_);

識別子のためにフレームを処理したときにprepareRenderingOfInputFrame関数が呼び出されます。この関数は、requestsInputFrameRendering関数がfalseを返し、requestsInputFrameProcessing関数がtrueを返すようにオーバーライドされた場合のみ呼び出されます。デフォルトで実装する場合は受信した識別子とフレーム・キャッシュからの古いフレームが示すフレームを解放します。デフォルト以外のフレームのキャッシュ メカニズムの場合は、このメソッドをオーバーライドします。-1の入力パラメータは、最新のフレームを識別します。

処理されたフレームのデータの受信

virtual std::shared_ptr<unsigned char> getPresentableInputFrameData();

デフォルトのフレーム・キャッシュから最後に処理されたフレームのデータを受信するためにgetPresentableInputFrameData関数を呼び出すことができます。レンダリングする現在のフレームデータを取得するようにfalseを返すためにrequestsInputFrameRendering関数がオーバーライドされる場合はこのメソッドを使用します。カスタムのフレームのキャッシュ メカニズムを使用する場合は、この関数は廃止されます。

エラー処理

virtual void internalError(const std::string& errorMessage);

入力プラグインに直接関連しないエラーがWikitude SDKネイティブで発生したときはinternalError関数が呼び出されます。発生したエラーの詳細は入力パラメータによって提供します。

内部使用のみ

void setInputFrameAvailableNotifier(InputFrameAvailableNotifier newInputFrameAvailableNotifier);

setInputFrameAvailableNotifier関数は内部的にしか呼び出されません。

簡単な入力プラグイン

このサンプルは、フレームがWikitude SDKによってレンダリングされるカスタムカメラの実装を示しています。
Simple Input Pluginの例は、C++クラスのSimpleYUVInputPluginとObjective-CクラスのWTSimpleDeviceCameraとWTSimpleYUVInputCameraで構成されています。WTSimpleDeviceCameraクラスには、AVFoundationカメラAPIの簡単な実装が含まれています。WTSimpleYUVInputCameraクラスは、WTSimpleDeviceCameraクラスとSimpleYUVInputPluginクラスのインスタンスを作成します。さらに両方のインスタンスを接続することで、iOS SDKカメラを起動/停止し、フレームをC++入力プラグインの実装に渡すことができます。

self.simpleYUVInputCamera = [[WTSimpleYUVInputCamera alloc] init];

入力プラグインはWikitude SDK ネイティブAPIが起動する前に登録されます。

NSError *error = nil;
    BOOL pluginRegistered = [self.wikitudeSDK registerPlugin:[_simpleYUVInputCamera cameraInputPlugin] error:&error];
    if ( !pluginRegistered )
    {
        NSLog(@"Unable to register plugin '%@'. Error: %@", [NSString stringWithUTF8String:[_simpleYUVInputCamera cameraInputPlugin]->getIdentifier().c_str()], [error localizedDescription]);
    }

    [self.wikitudeSDK start:nil completion:^(BOOL isRunning, NSError * _Nonnull error) {
        // ...
    }];

Wikitude SDK ネイティブAPIが起動する前に入力プラグインを登録すると、Wikitude SDK ネイティブAPIは内部カメラの実装が不要になり、起動もしません。これにより、最初のフレームでカメラの切り替えが発生せずに、SDKのクリーンで高速な起動が可能になります。

SimpleInputPluginクラスはInputPluginから派生し、Wikitude SDKの入力プラグインの最小限の実装を含みます。
カメラのライフサイクルは入力プラグインライフサイクルメソッドに結合されています。次のコードスニペットはこれを示しています。

void SimpleYUVInputPlugin::initialize() {
    /* The initialize method is used to initialize the iOS SDK camera */
    _initialized = [_camera initialize];

    /* Once the camera was initialzed correctly, input frame related settings are adjusted */
    if ( _initialized )
    {
        this->getFrameSettings().setInputFrameColorSpace(wikitude::sdk::FrameColorSpace::YUV_420_NV21);
        this->getFrameSettings().setInputFrameSize({640, 480});
        this->getFrameSettings().setFrameFieldOfView( [_camera fieldOfView] );
    }
}

void SimpleYUVInputPlugin::pause() {

    /* In case the SDK pauses (because the hosting application resignes active), also the iOS SDK camera is paused */
    [_camera stopRunning];
    _running = false;
}

void SimpleYUVInputPlugin::resume(unsigned int pausedTime_) {

    /* Like in `pause()`, `resume()` is used to resume the iOS SDK camera */
    if ( _initialized ) {
        _running = [_camera startRunning];
    }
}

void SimpleYUVInputPlugin::destroy() {

    /* In case the plugin is destroyed (Which is called as soon as the plugin is unregistered and the hosting application destroyes the shared_ptr used to register the plugin), also the iOS SDK camera is released */
    [_camera shutdown];
}

カメラフレームをObjective-CからC++に移動するために、AVCaptureVideoDataOutputSampleBufferDelegateオブジェクトは、AVCaptureVideoDataOutput-setSampleBufferDelegate:queueメソッドに内部的に渡されるWTSimpleDeviceCamera初期化子に渡されます。

WTSimpleYUVInputCamera内のAVCaptureVideoDataOutputSampleBufferDelegateの実装は、ImageBufferDataからユーザー定義メソッドvoid notifyNewImageBufferData(std::shared_ptr SimpleInputPlugin)を呼び出し、iOS SDK カメラフレームの表現を含むstd::shared_ptrを渡します。このstd::shared_ptrは、次のようなWTSimpleYUVInputCameraクラスで作成されます。

if ( kCVReturnSuccess == CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly) )
{
    size_t frameDataSize = CVPixelBufferGetDataSize(imageBuffer);
    std::shared_ptr<unsigned char> frameData = std::shared_ptr<unsigned char>(new unsigned char[frameDataSize], std::default_delete<unsigned char[]>());
    std::memcpy(frameData.get(), CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0), frameDataSize);           
    _cameraInputPlugin->notifyNewImageBufferData(frameData);
    CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
}

新しいステートメントで作成されたメモリを解放するために、カスタムディレクタがstd ::shared_ptrコンストラクタに渡されることに注意してください。
notifyNewImageBufferDataの実装は、入力プラグイン用に定義されたnotifyNewInputFrameメソッドを呼び出します。 新しいフレームIDを生成し、Objective-Cで生成されたフレームデータを含むstd ::shared_ptrを渡します。

void SimpleYUVInputPlugin::notifyNewImageBufferData(std::shared_ptr<unsigned char> imageBufferData) {
    notifyNewInputFrame(++_frameId, imageBufferData);
}

ここで使用されている実装は、デモンストレーション用であることに注意してください。プロダクションアプリケーションは、カメラアクセスとカメラフレームの配布を完全に違った方法で自由に実装できます。

カスタムカメラ

カスタムカメラの例は、統一されたユースケースの両方の原則を示しています。カスタムカメラストリームが入力として提供され、カスタムレンダリングエフェクトがレンダリングされた出力を補強するために使用されます。

並行性

InputPluginを実装する場合、プラグインのコールバック関数はWikitude SDKネイティブAPIによって同時に呼び出されるので、競合状態が発生しないようにする必要があります。これに対して、このサンプルでは、アトミック操作と排他制御の2つの方法が提供されています。
入力プラグインAPIの機能を十分に利用するためには、さまざまな非同期で呼び出されるメンバ関数からデータを収集し、メンバ変数として格納し、一括して使用します。
以下のコードは、「Custom Camera」サンプルから抽出したコードの例です。

void YUVFrameInputPlugin::surfaceChanged(wikitude::sdk::Size<int> renderSurfaceSize_, wikitude::sdk::Size<float> cameraSurfaceScaling_, wikitude::sdk::DeviceOrientation deviceOrientation_) {
    // some orientation handling code here
    _surfaceInitialized.store(true);
}
void YUVFrameInputPlugin::startRender() {
    // some early exit code here
    render();
}
void YUVFrameInputPlugin::render() {
    // some early exit code here
    if (!_surfaceInitialized.load()) {
        return;
    }
    // lots of OpenGL code here
}
#include <atomic>
std::atomic_bool _surfaceInitialized;

surfaceChanged関数とstartRender関数が同時に呼び出されます。これはsurfaceChanged関数から設定されたRender関数内のブール値に依存し、ブール値の読み取りと書き込みが非アトミックである場合は競合状態が発生します。この場合は、アトミック操作がC++における標準Cライブラリによって提供されるように組み込みデータ型の使用をお勧めします。これらのstd::atomicsは対応する操作やLoadとStore関数によって設定や読み取りができます。
以下のコードは、アトミック操作が使用できないコードの例です。

void YUVFrameInputPlugin::update(const std::list<wikitude::sdk::RecognizedTarget>& recognizedTargets_) {
    // platform specific intialization code here
    { // mutex auto release scope
        std::lock_guard<std::mutex> lock(_currentlyRecognizedTargetsMutex);
        _currentlyRecognizedTargets = std::list<wikitude::sdk::RecognizedTarget>(recognizedTargets_);
    }
}
void YUVFrameInputPlugin::startRender() {
    // some early exit code here
    render();
}
void YUVFrameInputPlugin::render() {
    // early returns and lots of OpenGL code here
    { // mutex auto release scope
    std::unique_lock<std::mutex> lock(_currentlyRecognizedTargetsMutex);
        if (!_currentlyRecognizedTargets.empty()) {
            const wikitude::sdk::RecognizedTarget targetToDraw = _currentlyRecognizedTargets.front();
            // early unlock to minimize locking duration
            lock.unlock();
            // lots of OpenGL code here
        }
    }
}

Update関数とstartRender関数が同時に呼び出されます。これは、Render関数内で非同期型関数によって設定されたデータに依存します。std::listのオブジェクトはstd::atomicsを使用してアトミックに設定できないので、std::mutexを使用します。コードに示したように、ミューテックスを正常に解放するためにstd::lock_guardおよびstd::unique_lockのようなRAIIスタイルのミューテックロックを使用します。

OpenGLのコンテキスト

プラグインの実行時に有効なOpenGLのコンテキストが使用できます。startRender、endRender、pause、resume関数を実行するときに有効なコンテキストが利用可能です。startRenderおよびendRender関数では、レンダリングに関連するすべての関数呼び出しが含まれます。OpenGLコンテキストは、アプリケーションを一時停止するときに破壊され、アプリケーションを再開するときに再作成されるので、pauseおよびresume関数はOpenGL関連のリソースを解放または取得するために使用されます。したがって、以前に取得したすべてのOpenGLのハンドルは無効になり、再取得する必要があります。
以下のコードは、「Custom Camera」から抽出したコードの例です。

void YUVFrameInputPlugin::pause() {
    releaseFramebufferObject();
    releaseFrameTextures();
    releaseVertexBuffers();
    releaseShaderProgram();
    _renderingInitialized.store(false);
    // some additional code here
}
void YUVFrameInputPlugin::startRender() {
    if (!_renderingInitialized.load()) {
        _renderingInitialized.store(setupRendering());
    }
    render();
}

以前に作成したすべてのOpenGLのリソースを解放し、_renderingInitializedフラグをfalseに設定すると、レンダリング環境はレンダリングループを次回実行するときに再初期化されます。

デバイスの向き

デバイスの向きを検討しながらOpenGLを使用してInputPluginに入力フレームのレンダリングを示します。正しい向きのフレームレンダリングを実現するためにいくつかの方法がありますが、カスタム頂点シェーダ内に必要な変換をマトリックスとして適用することをお勧めします。
以下のコードは、「Custom Camera」から抽出したもので、フルスクリーンクワッドに適用するマトリックスを構成する方法を示します。

void YUVFrameInputPlugin::surfaceChanged(wikitude::sdk::Size<int> renderSurfaceSize_, wikitude::sdk::Size<float> cameraSurfaceScaling_, wikitude::sdk::DeviceOrientation deviceOrientation_) {
    wikitude::sdk::Matrix4 scaleMatrix;
    scaleMatrix.scale(cameraSurfaceScaling_.width, cameraSurfaceScaling_.height, 1.0f);
    switch (deviceOrientation_)
    {
        case wikitude::sdk::DeviceOrientation::DeviceOrientationPortrait:
        {
            wikitude::sdk::Matrix4 rotationToPortrait;
            rotationToPortrait.rotateZ(270.0f);
            _orientationMatrix = rotationToPortrait;
            break;
        }
        case wikitude::sdk::DeviceOrientation::DeviceOrientationPortraitUpsideDown:
        {
            wikitude::sdk::Matrix4 rotationToUpsideDown;
            rotationToUpsideDown.rotateZ(90.0f);
            _orientationMatrix = rotationToUpsideDown;
            break;
        }
        case wikitude::sdk::DeviceOrientation::DeviceOrientationLandscapeLeft:
        {
            wikitude::sdk::Matrix4 rotationToLandscapeLeft;
            rotationToLandscapeLeft.rotateZ(180.0f);
            _orientationMatrix = rotationToLandscapeLeft;
            break;
        }
        case wikitude::sdk::DeviceOrientation::DeviceOrientationLandscapeRight:
        {
            _orientationMatrix.identity();
            break;
        }
    }
    _modelMatrix = scaleMatrix * _orientationMatrix;
    // some synchronization code here
}
attribute vec3 vPosition;
attribute vec2 vTexCoords;
varying mediump vec2 fTexCoords;
uniform mat4 uModelMatrix;
void main(void)
{
    gl_Position = uModelMatrix * vec4(vPosition, 1.0);
    fTexCoords = vTexCoords;
}";
struct Vertex
{
    GLfloat position[3];
    GLfloat texCoord[2];
};
Vertex _vertices[4];
_vertices[0] = (Vertex){{1.0f, -1.0f, 0}, {1.0f, 0.0f}};
_vertices[1] = (Vertex){{1.0f, 1.0f, 0}, {1.0f, 1.0f}};
_vertices[2] = (Vertex){{-1.0f, 1.0f, 0}, {0.0f, 1.0f}};
_vertices[3] = (Vertex){{-1.0f, -1.0f, 0}, {0.0f, 0.0f}};

surfaceChanged関数に構成したマトリックスは均一パラメータとして頂点シェーダに供給され、入力頂点を変換するために使用されます。FBOにレンダリングしたものに応じて追加のマトリックスが必要になる場合があります。この場合は、以下のコードのようにsurfaceChanged関数を変更することで、Y軸が修正できます。

wikitude::sdk::Matrix4 scaleMatrix;
_fboCorrectionMatrix.scale(1.0f, -1.0f, 1.0f);
// same device orientation code here as depicted above
_modelMatrix = scaleMatrix * _orientationMatrix * _fboCorrectionMatrix;

特定の高度なユースケースの入力プラグインを実装するには、カスタムカメラのサンプルアプリケーションのソースコードを確認してください。カスタムカメラのサンプルソースコードは、独自の実装をするのに参考になります。