이미지 입력 확장 프로그램 만들기
시작하기 전에
- 카메라, 입력 프레임 등의 기본 개념을 이해하세요.
- 외부 프레임 데이터 소스를 생성하는 데 필요한 상세 인터페이스 설명은 외부 프레임 데이터 소스를 참고하세요.
- 카메라 프레임 데이터와 렌더링 프레임 데이터에 대해 알아보려면 외부 입력 프레임 데이터를 읽어보세요.
생성 외부 프레임 데이터 소스 클래스
ExternalImageStreamFrameSource를 상속하여 이미지 입력 확장을 생성합니다. 이는 MonoBehaviour의 하위 클래스이며, 파일 이름은 클래스명과 동일해야 합니다.
예시:
public class MyFrameSource : ExternalImageStreamFrameSource
{
}
예제 Workflow_FrameSource_ExternalImageStream은 휴대폰에서 ARCore로 녹화된 동영상을 입력 소스로 사용하는 이미지 입력 확장 구현입니다. 해당 동영상은 Pixel2 기기에서 ARCore를 통해 카메라 콜백 방식으로 수집되었습니다(화면 녹화 아님).
장치 정의
IsCameraUnderControl을 재정의하고 true를 반환합니다.
IsHMD를 재정의하여 장치가 헤드 마운티드 디스플레이(HMD)인지 정의합니다.
예를 들어, 비디오를 입력으로 사용할 때는 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;
세션 시작 및 중지
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 및 기타 관련 데이터를 해제하는 것을 잊지 마세요. 그렇지 않으면 심각한 메모리 누수가 발생할 수 있으며, 버퍼 풀에서 버퍼를 가져오지 못할 수 있습니다.