Table of Contents

創建圖像輸入擴展

開始之前

創建外部幀數據源類

繼承 ExternalImageStreamFrameSource 來創建圖像輸入擴展。它是 MonoBehaviour 的子類,文件名應與類名相同。

例如:

public class MyFrameSource : ExternalImageStreamFrameSource
{
}

示例 Workflow_FrameSource_ExternalImageStream 就是一個基於手機上使用 ARCore 錄製的視頻作爲輸入的圖像輸入擴展實現。該視頻是使用 Pixel2 上的 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);
    ...
}

這裏是適合打開設備相機的位置,尤其是如果這些相機沒有被設計成要一直打開時。同時這裏也是適合獲取整個生命週期內不會變化的標定數據的位置。有時在這些數據可以被獲取前可能需要等待設備準備好或等待數據更新。

同時,這裏也是一個適合啓動數據輸入循環的位置。也可以在 Update() 或其它方法中寫這個循環,尤其是當數據需要在 Unity 執行順序的某個特殊時間點獲取的時候。在 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 也可能會失敗。

相關主題