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;

若在 session 組裝時無法透過 IsAvailable 判斷可用性,可覆寫 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 也可能會失敗。

相關主題