상세 컨텐츠

본문 제목

Unreal Engine 좌표변환 이해하기(2)

언리얼엔진

by zmo 2025. 10. 20. 17:42

본문

그렇다면 언리얼 엔진에서 어떻게 사용할 것인가?

언리얼 + 학습 파이프라인

  1. 데이터셋 생성 (SceneCapture 사용)
    • 언리얼에서 SceneCaptureComponent2D로 씬을 캡처하면 픽셀이 얻어진다.
    • 이 픽셀은 화면 좌표(Screen space)에 있고, 사실은 월드 좌표카메라 좌표화면 좌표 변환 과정을 거친 결과다.
    • 동차 좌표계와 좌표 변환을 이해함으로 라벨링 데이터(예: 객체 위치, 3D bounding box등)를 정확히 월드 기준으로 만들 수 있을 것이다.
    👉 예: AI 모델이 "화면에서 자동차 위치"를 예측하면, 우리는 이것을 다시 월드 좌표로 변환해야 실제로 사용할수 있을 것이다.(3D 구현이 가능한 언리얼 엔진이기에)
  2. 학습 데이터 라벨링
    • 객체 감지(Object Detection) 학습을 위해선 bounding box가 필요하다.
    • 언리얼은 AActor->GetActorLocation() 같은 API로 월드 좌표를 쉽게 얻을 수 있다.
    • 하지만 AI 모델은 보통 이미지 좌표(픽셀 좌표)가 필요하다.
    • 따라서 ProjectionMatrix * ViewMatrix * WorldCoord 과정을 거쳐 픽셀 좌표로 변환해야 한다.
    👉 여기서 바로 동차 좌표 변환이 적용할 수 있다.
  3. 학습 → 추론 → 피드백 루프
    • 학습된 모델이 "화면 좌표에서 박스 검출"(파악할 수 있게 되면)을 하면,
    • 언리얼 내부에서는 다시 그 좌표를 월드 좌표로 역변환하여 AI 캐릭터가 실제로 이동하거나 회피할 수 있게 한다.
    👉 예: 객체 감지를 학습한 AI가 "앞에 벽이 있다"를 인식 → 월드 좌표로 변환 → 경로 변경.
  4. Depth Estimation (깊이 추정) 활용
    • 우리가 이전에 다룬 monocular depth estimation도 결국 카메라 좌표계 개념을 써야 한다.
    • 모델이 예측한 depth(깊이) 값은 "카메라 중심 기준 z축" 값이다.
    • 이것을 다시 월드 좌표로 변환하면 언리얼 씬에서 실제 지형과 매칭할 수 있다.

정리

  • 동차 좌표계 등은 왜 필요한가?
    • 언리얼에서 수집한 데이터(월드 좌표) ↔ AI 학습 데이터(화면 좌표)를 잇는 다리 역할.
    • 객체 감지, 세그멘테이션, depth estimation 같은 비전 AI 학습에서 좌표 변환 수학이 필수.
  • 어떻게 쓰이는가?
    • SceneCapture로 얻은 이미지를 AI 학습에 쓰고,
    • 학습한 결과를 다시 좌표 변환을 통해 언리얼의 3D 씬에 반영.

언리얼에서 물체의 실제 위치(AActor::GetActorLocation())는 월드 좌표계에 존재한다.

하지만 화면에 표시하거나, 이미지로 라벨링하려면 2D 화면 좌표(Screen Space) 로 변환해야 한다.

이 변환은 수학적으로 다음 행렬 연산으로 표현된다.

 

ScreenCoord = ProjectionMatrix × ViewMatrix × WorldCoord

 

언리얼에서는 이 과정을 직접 행렬로 계산할 필요 없이, UGameplayStatics::ProjectWorldToScreen() 함수를 사용하면 된다.

함수로 행렬 계산하기

// WorldToScreenExample.cpp

#include "WorldToScreenExample.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/PlayerController.h"
#include "Engine/Engine.h"

void UWorldToScreenExample::ConvertActorToScreen(AActor* TargetActor)
{
    if (!TargetActor) return;

    // 현재 플레이어 컨트롤러 가져오기
    APlayerController* PlayerController = UGameplayStatics::GetPlayerController(GetWorld(), 0);
    if (!PlayerController) return;

    // 월드 좌표 얻기
    FVector WorldLocation = TargetActor->GetActorLocation();

    // 화면 좌표로 변환
    FVector2D ScreenPosition;
    bool bIsOnScreen = PlayerController->ProjectWorldLocationToScreen(WorldLocation, ScreenPosition);

    if (bIsOnScreen)
    {
        UE_LOG(LogTemp, Warning, TEXT("Actor %s is on screen at: X=%.1f, Y=%.1f"),
            *TargetActor->GetName(), ScreenPosition.X, ScreenPosition.Y);
    }
    else
    {
        UE_LOG(LogTemp, Warning, TEXT("Actor %s is off screen."), *TargetActor->GetName());
    }
}

역투영(Unprojection) 계산 과정

이미지의 특정 픽셀 위치 (u, v)와 그 지점의 뎁스 맵 값 D(u,v)를 알 때, 카메라 좌표계 기준 3D 포인트 (Xc, Yc, Zc)는 다음 공식으로 계산할 수 있다.

Z_c = D(u, v)

Xc=(u−cx)×Zc/fx

Yc=(v−cy)×Zc/fy

이 공식의 기하학적 의미는 '삼각형의 닮음' 원리이다. 이미지 평면에서 픽셀이 중심점(cx, cy)으로부터 떨어진 거리는, 실제 3D 공간에서 해당 포인트가 카메라 중심축으로부터 떨어진 거리와 비례한다. 초점 거리 fx, fy와 깊이 Zc가 바로 그 비례상수 역할을 한다.

 

최종 3D 복원 파이프라인

  1. 뎁스 맵 추론: RGB 이미지로부터 AI 모델이 뎁스 맵 D를 생성한다.
  2. 포인트 클라우드 생성 (카메라 좌표계): 이미지의 모든 픽셀 (u, v)에 대해 위의 역투영 공식을 적용, 카메라를 원점으로 하는 3D 포인트 클라우드 { (Xc, Yc, Zc) }를 만든다.
  3. 월드 좌표계로 변환: 이 포인트 클라우드는 아직 카메라에 종속되어 있다. 이것을 언리얼 월드에 배치하려면 카메라의 월드 위치와 회전 정보(FTransform)가 필요하다. 각 3D 포인트를 카메라의 월드 변환 행렬과 곱해주면, 최종적으로 우리가 원하는 월드 좌표계 기준 포인트 클라우드가 완성된다.

이 과정을 통해 우리는 2D 이미지 한 장으로부터 3D 공간 정보를 복원하고, 언리얼 엔진 안에서 실시간으로 시각화하거나 물리적으로 상호작용하는 객체를 만들어낼 수 있게 된다.

 

//'뎁스(Depth)'는 관찰자(보통 카메라)로부터 3D 공간에 있는 객체의 표면까지의 거리를 의미

 

심화과정: 2D Bounding Box를 3D 공간으로 되살리기

AI 객체 탐지 모델은 이미지 속에서 객체의 위치를 2D Bounding Box (사각형 좌표)로 알려준다. 하지만 이건 그냥 화면 위의 네모일 뿐, 언리얼 엔진의 3D 월드에서는 아무런 의미가 없을 것이다.

그러므로 이 2D 사각형에 깊이(Depth) 정보를 결합하여 실제 3D 공간의 Bounding Box로 "복원"하는 방법을 알아보자. 이 과정을 통해 AI를 3D 공간을 인지하고 상호작용할 수 있게 만들어줄 것이다.

 

핵심 아이디어: Frustum 단면 생성

카메라에서 뻗어 나가는 시야 영역을 절두체(Frustum)라고 한다. 2D Bounding Box의 네 꼭짓점을 3D 공간으로 역투영(Deproject)하면, 카메라에서 시작되는 4개의 광선(Ray)이 만들어진다.

여기에 특정 거리(Depth)에 가상의 평면을 놓으면, 4개의 광선이 그 평면과 만나는 4개의 3D 점이 생기고, 이 4개의 점이 바로 우리가 찾던 3D Bounding Box의 '앞면'이 된다.

 

구현 파이프라인 (Unreal C++)

입력:

  1. AI 모델이 예측한 2D Bounding Box의 네 꼭짓점 (FVector2D)
  2. Depth Estimation 모델이 예측한 해당 객체의 평균 깊이 값 (float Depth)

과정:

  1. 2D 좌표 → 3D 광선(Ray) 변환: 2D Bounding Box의 각 꼭짓점 (u, v)에 대해 APlayerController::DeprojectScreenPositionToWorld 함수를 호출한다. 이 함수는 카메라 위치에서 시작(WorldLocation)해서 해당 픽셀을 뚫고 나아가는 단위 방향 벡터(WorldDirection)를 반환한다.
  2. 깊이를 이용해 3D 위치 확정하기: 이제 WorldLocation에서 WorldDirection 방향으로 우리가 아는 Depth만큼 나아가면 최종 3D 좌표를 얻을 수 있다.주의: 위 공식은 근사치이다. 정확한 계산을 위해서는 카메라의 투영 방식을 고려해야 하지만, 대부분의 경우 이 방법으로 충분히 실용적인 결과를 얻을 수 있다고 한다. - by Gemini
  3. 3D 좌표 = WorldLocation + WorldDirection * Depth
  4. 3D Box 생성:
  • 1~2 단계를 4개의 꼭짓점에 대해 반복하여 Box의 앞면을 구성하는 4개의 3D 정점(Vertex)을 구한다.
  • Box에 두께를 주기 위해, Depth + BoxThickness 값을 사용해 1~2 단계를 다시 반복하여 Box의 뒷면을 구성하는 4개의 3D 정점을 추가로 구한다.
  • 이제 총 8개의 정점으로 완전한 3D Bounding Box를 정의할 수 있게 되었다.
void AMyActor::Create3DBoxFrom2DBox(const FBox2D& Box2D, float Depth, float BoxThickness)
{
    APlayerController* PC = GetWorld()->GetFirstPlayerController();
    if (!PC) return;

    // 2D Box의 네 꼭짓점 좌표 배열
    FVector2D Corners[4] = {
        FVector2D(Box2D.Min.X, Box2D.Min.Y), // Top-Left
        FVector2D(Box2D.Max.X, Box2D.Min.Y), // Top-Right
        FVector2D(Box2D.Min.X, Box2D.Max.Y), // Bottom-Left
        FVector2D(Box2D.Max.X, Box2D.Max.Y)  // Bottom-Right
    };

    FVector Vertices[8]; // 최종 3D Box의 8개 정점
    int32 VertexIndex = 0;

    // 앞면 (Depth) 과 뒷면 (Depth + Thickness) 계산
    for (float CurrentDepth : { Depth, Depth + BoxThickness })
    {
        for (const FVector2D& Corner : Corners)
        {
            FVector WorldLocation, WorldDirection;
            bool bSuccess = PC->DeprojectScreenPositionToWorld(
                Corner.X,
                Corner.Y,
                WorldLocation,
                WorldDirection
            );

            if (bSuccess)
            {
                // Ray 시작점(카메라 위치)에서 Depth 만큼 떨어진 지점을 계산
                Vertices[VertexIndex++] = WorldLocation + WorldDirection * CurrentDepth;
            }
        }
    }

    // 8개의 정점으로 3D Bounding Box의 중심과 범위를 계산
    FBox BoundingBox(Vertices, 8);
    
    // 디버그용으로 월드에 3D Box 그리기
    DrawDebugBox(
        GetWorld(),
        BoundingBox.GetCenter(),
        BoundingBox.GetExtent(),
        FColor::Green,
        false,
        5.0f,
        0,
        10.0f
    );
}

 

관련글 더보기