画像入力拡張の作成
開始之前
- カメラ、入力フレーム などの基本概念を理解する。
- 外部フレームデータソース を読み、外部フレームデータソースを作成するために必要な詳細なインターフェース仕様を確認する。
- 外部入力フレームデータ を読み、カメラフレームデータとレンダリングフレームデータを理解する。
外部フレームデータソースクラスの作成
継承 ExternalImageStreamFrameSource して画像入力拡張を作成します。これは MonoBehaviour のサブクラスであり、ファイル名はクラス名と同じである必要があります。
例えば:
public class MyFrameSource : ExternalImageStreamFrameSource
{
}
サンプル Workflow_FrameSource_ExternalImageStream は、スマートフォンで ARCore を使用して録画されたビデオを入力として使用する画像入力拡張の実装です。このビデオは、Pixel2 上の ARCore を使用してカメラコールバック方式で収集されました(スクリーン録画ではありません)。
Device definition
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 がセッションの組み立て時に判断できない場合、CheckAvailability() コルーチンをオーバーライドして、利用可能かどうかが確定するまで組み立てプロセスをブロックできます。
仮想カメラ
Camera を書き換えて仮想カメラを提供します。
例えば、時には Camera.main をセッションの仮想カメラとして使用できます:
protected override Camera Camera => Camera.main;
物理カメラ
DeviceCameras を上書きする際に FrameSourceCamera タイプを使用し、デバイスの物理カメラ情報を提供します。このデータは入力カメラフレームデータで使用されます。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;
セッションの開始と停止
OnSessionStart(ARSession) をオーバーライドし、AR固有の初期化処理を行います。必ず最初に base.OnSessionStart を呼び出してください。
例:
protected override void OnSessionStart(ARSession session)
{
base.OnSessionStart(session);
...
}
これは、デバイスカメラ(特に常時オンにするよう設計されていないもの)を起動するのに適した場所です。また、セッションライフサイクルを通じて変化しないキャリブレーションデータを取得するのにも適しています。デバイスの準備が整う、またはデータが更新されるのを待つ必要がある場合があります。
同時に、データ入力ループを開始するのにも適した場所です。このループは、データがUnityの実行順序における特定のタイミングで取得される必要がある場合、特に Update() や他のメソッド内に記述することもできます。セッションが準備完了(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 などのメカニズムによる Image、Buffer およびその他関連データの解放を忘れないでください。さもないと重大なメモリリークが発生し、バッファプールからのバッファ取得が失敗する可能性があります。