Table of Contents

画像入力拡張の作成

開始前

外部フレームデータソースクラスの作成

ExternalImageStreamFrameSource を継承して画像入力拡張を作成します。これは MonoBehaviour の子クラスで、ファイル名はクラス名と同じにします。

たとえば、次のようにします。

public class MyFrameSource : ExternalImageStreamFrameSource
{
}

サンプル Workflow_FrameSource_ExternalImageStream は、スマートフォン上で ARCore を使って録画した動画を入力に使う画像入力拡張の実装です。この動画は Pixel 2 上の ARCore で、画面録画ではなくカメラコールバック経由で取得されています。

デバイス定義

IsCameraUnderControl をオーバーライドして true を返します。

IsHMD をオーバーライドして、デバイスがヘッドマウントディスプレイかどうかを定義します。

たとえば、動画を入力として使う場合は false に設定します。

protected override bool IsHMD => false;

Display をオーバーライドして、デバイスの表示を定義します。

たとえば、モバイル端末でのみ動作する場合は、オペレーティングシステムの現在の表示状態に応じて回転値が自動的に変わる Display.DefaultSystemDisplay を使用できます。

protected override IDisplay Display => easyar.Display.DefaultSystemDisplay;

利用可能性

IsAvailable をオーバーライドして、デバイスが利用可能かどうかを定義します。

たとえば、動画を入力として使う場合は常に利用可能です。

protected override Optional<bool> IsAvailable => true;

IsAvailable を session の組み立て時に判定できない場合は、CheckAvailability() コルーチンをオーバーライドして、利用可能かどうかが確定するまで組み立て処理をブロックします。

仮想カメラ

Camera をオーバーライドして、仮想カメラを提供します。

たとえば、Camera.main を session の仮想カメラとして使うことがあります。

protected override Camera Camera => Camera.main;

物理カメラ

FrameSourceCamera 型を使って DeviceCameras をオーバーライドし、デバイスの物理カメラ情報を提供します。このデータはカメラフレームデータを入力する際に使用されます。CameraFrameStarted が true のときに必ず完成していなければなりません。

たとえば、サンプル Workflow_FrameSource_ExternalImageStream で使われている動画を入力する場合は次のようになります。

private FrameSourceCamera deviceCamera;
protected override List<FrameSourceCamera> DeviceCameras => new List<FrameSourceCamera> { deviceCamera };

{
    var size = new Vector2Int(640, 360);
    var cameraType = CameraDeviceType.Back;
    var cameraOrientation = 90;
    deviceCamera = new FrameSourceCamera(cameraType, cameraOrientation, size, new Vector2(30, 30));
    started = true;
}
注意

ここでのいくつかの入力パラメータは、実際に使用する動画に応じて設定する必要があります。上のコードのパラメータはサンプル動画にのみ適用されます。

CameraFrameStarted をオーバーライドして、カメラフレーム入力が開始されたことを示す識別子を返します。

たとえば、次のようにします。

protected override bool CameraFrameStarted => started;

session の開始と停止

OnSessionStart(ARSession) をオーバーライドして、AR 専用の初期化処理を行います。最初に base.OnSessionStart を呼び出すことを忘れないでください。

たとえば、次のようにします。

protected override void OnSessionStart(ARSession session)
{
    base.OnSessionStart(session);
    ...
}

ここはデバイスカメラを開くのに適した場所です。特に、これらのカメラが常時開いたままになるよう設計されていない場合に向いています。また、ライフサイクル全体を通して変わらないキャリブレーションデータを取得する場所としても適しています。こうしたデータを取得できるようになる前に、デバイスの準備やデータ更新を待つ必要がある場合もあります。

同時に、ここはデータ入力ループを開始するのに適した場所でもあります。Unity の実行順序の特定のタイミングでデータを取得する必要がある場合は、Update() や他のメソッドでこのループを書くこともできます。session が ready になるまではデータを入力しないでください。

必要であれば、起動処理を省略して、更新ごとにデータをチェックしてもかまいません。これは完全に要件次第です。

たとえば、動画を入力として使う場合は、ここで動画再生を開始し、データ入力ループも起動できます。

protected override void OnSessionStart(ARSession session)
{
    base.OnSessionStart(session);
    ...
    player.Play();
    StartCoroutine(VideoDataToInputFrames());
}

OnSessionStop() をオーバーライドしてリソースを解放します。base.OnSessionStop を呼び出すことを忘れないでください。

たとえば、動画を入力として使う場合は、ここで動画再生を停止し、関連リソースを解放できます。

protected override void OnSessionStop()
{
    base.OnSessionStop();

    StopAllCoroutines();
    player.Stop();
    if (renderTexture) { Destroy(renderTexture); }
    cameraParameters?.Dispose();
    cameraParameters = null;
    frameIndex = -1;
    started = false;
    deviceCamera?.Dispose();
    deviceCamera = null;
}

デバイスまたはファイルからカメラフレームデータを取得

画像は、システムカメラ、USB カメラ、動画ファイル、ネットワークなど、任意のソースから取得できます。データを Image に必要な形式へ変換できさえすれば構いません。これらのデバイスやファイルからデータを取得する方法はそれぞれ異なるため、関連するデバイスやファイルの使用方法を参照してください。

たとえば、動画を入力として使う場合は、Texture2D.ReadPixels(Rect, int, int, bool) を使って動画プレーヤーの RenderTexture からカメラフレームデータを取得し、Texture2D.GetRawTextureData() のデータを Buffer にコピーします。

void VideoDataToInputFrames()
{
    ...
    RenderTexture.active = renderTexture;
    var pixelSize = new Vector2Int((int)player.width, (int)player.height);
    var texture = new Texture2D(pixelSize.x, pixelSize.y, TextureFormat.RGB24, false);
    texture.ReadPixels(new Rect(0, 0, pixelSize.x, pixelSize.y), 0, 0);
    texture.Apply();
    RenderTexture.active = null;
    ...
    CopyRawTextureData(buffer, texture.GetRawTextureData<byte>(), pixelSize);
} 

static unsafe void CopyRawTextureData(Buffer buffer, Unity.Collections.NativeArray<byte> data, Vector2Int size)
{
    int oneLineLength = size.x * 3;
    int totalLength = oneLineLength * size.y;
    var ptr = new IntPtr(data.GetUnsafeReadOnlyPtr());
    for (int i = 0; i < size.y; i++)
    {
        buffer.tryCopyFrom(ptr, oneLineLength * i, totalLength - oneLineLength * (i + 1), oneLineLength);
    }
}
注意

上のコードのように、Texture2D のポインタからコピーしたデータは上下反転しているため、正常な画像のメモリ配置に戻す必要があります。

画像を取得すると同時に、カメラまたは同等のカメラのキャリブレーションデータも取得し、CameraParameters インスタンスを作成する必要があります。

データの元がスマートフォンのカメラコールバックで、データが人工的に切り抜かれていない場合は、端末のカメラキャリブレーションデータをそのまま使えます。ARCore や ARKit などのインターフェースでカメラコールバックデータを取得する場合は、関連ドキュメントを参照してカメラ内部パラメータを取得してください。使用したい AR 機能が画像トラッキングまたはオブジェクトトラッキングであれば、この場合は CameraParameters.createWithDefaultIntrinsics(Vec2I, CameraDeviceType, int) でカメラ内部パラメータを作成することもできます。このときアルゴリズムの効果はわずかに影響を受けますが、通常は大きな問題にはなりません。

データが USB カメラや、カメラコールバック以外で生成された動画ファイルなどの他のソースから来る場合は、正しい内部パラメータを得るためにカメラまたは動画フレームをキャリブレーションする必要があります。

注意

カメラコールバックのデータは切り抜いてはいけません。切り抜いた場合は内部パラメータを再計算する必要があります。画面録画などで取得した画像データでは、通常はスマートフォンのカメラキャリブレーションデータを使えません。この場合も、正しい内部パラメータを得るためにカメラまたは動画フレームのキャリブレーションが必要です。

内部パラメータが正しくないと AR 機能は正常に使えません。よくある症状として、仮想コンテンツと現実物体が一致しない、AR トラッキングが成功しにくい、すぐにロストする、などがあります。

たとえば、サンプル Workflow_FrameSource_ExternalImageStream で使われている動画では、対応するカメラ内部パラメータと CameraParameters の作成手順は次のようになります。

var size = new Vector2Int(640, 360);
var cameraType = CameraDeviceType.Back;
var cameraOrientation = 90;
cameraParameters = new CameraParameters(size.ToEasyARVector(), new Vec2F(506.085f, 505.3105f), new Vec2F(318.1032f, 177.6514f), cameraType, cameraOrientation);
注意

上のコードのパラメータはサンプル動画にのみ適用されます。このカメラ内部パラメータと動画は同じ時点で取得されています。別の動画やデバイスのデータを使う場合は、必ずデバイス内部パラメータも取得するか、手動でキャリブレーションしてください。

カメラフレームデータの入力

カメラフレームデータの更新を取得したら、HandleCameraFrameData(double, Image, CameraParameters) を呼び出してカメラフレームデータを入力します。

たとえば、動画を入力として使う場合の実装は次のようになります。

IEnumerator VideoDataToInputFrames()
{
    yield return new WaitUntil(() => player.isPrepared);
    var pixelSize = new Vector2Int((int)player.width, (int)player.height);
    ...
    yield return new WaitUntil(() => player.isPlaying && player.frame >= 0);
    while (true)
    {
        yield return null;
        if (frameIndex == player.frame) { continue; }
        frameIndex = player.frame;
        ...
        var pixelFormat = PixelFormat.RGB888;
        var bufferO = TryAcquireBuffer(pixelSize.x * pixelSize.y * 3);
        if (bufferO.OnNone) { continue; }

        var buffer = bufferO.Value;
        CopyRawTextureData(buffer, texture.GetRawTextureData<byte>(), pixelSize);

        using (buffer)
        using (var image = Image.create(buffer, pixelFormat, pixelSize.x, pixelSize.y, pixelSize.x, pixelSize.y))
        {
            HandleCameraFrameData(player.time, image, cameraParameters);
        }
    }
}
注意

使用後は Dispose() を実行するか、using などの仕組みで ImageBuffer、その他の関連データを解放することを忘れないでください。そうしないと、深刻なメモリリークが発生し、buffer pool から buffer を取得できなくなることがあります。

関連トピック