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를 판단할 수 없다면 CheckAvailability() 코루틴을 재정의하여, 사용 가능 여부가 확인될 때까지 어셈블리 과정을 차단할 수 있습니다.

가상 카메라

Camera를 재정의하여 가상 카메라를 제공합니다.

예를 들어, 때로는 Camera.main를 세션의 가상 카메라로 사용할 수 있습니다.

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;

세션 시작과 중지

AR 전용 초기화 작업을 수행하기 위해 OnSessionStart(ARSession)를 재정의합니다. 먼저 base.OnSessionStart를 호출해야 합니다.

예를 들어:

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

이 위치는 디바이스 카메라를 여는 데 적합하며, 특히 이런 카메라가 항상 열려 있도록 설계되지 않은 경우에 그렇습니다. 또한 수명 전체에서 변하지 않는 보정 데이터를 가져오기에 적합한 위치이기도 합니다. 때로는 이 데이터가 사용 가능해지기 전에 디바이스가 준비되거나 데이터가 갱신되기를 기다려야 할 수 있습니다.

이곳은 데이터 입력 루프를 시작하기에도 적합한 위치입니다. Unity 실행 순서의 특정 시점에 데이터를 가져와야 하는 경우에는 Update()나 다른 메서드에 루프를 작성할 수도 있습니다. 세션이 준비되기 전에는 데이터를 입력하지 마세요.

필요하다면 시작 과정을 건너뛰고 매 업데이트마다 데이터 검사를 수행할 수도 있으며, 이는 전적으로 구체적인 요구 사항에 달려 있습니다.

예를 들어, 비디오를 입력으로 사용할 때는 여기서 비디오 재생을 시작하고 데이터 입력 루프를 시작할 수 있습니다.

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 및 기타 관련 데이터를 해제하는 것을 잊지 마세요. 그렇지 않으면 심각한 메모리 누수가 발생할 수 있고, buffer pool에서 buffer를 가져오지 못할 수도 있습니다.

관련 주제