카테고리 없음

Unreal Engine에서 실시간 3D Scene Reconstruction 구현하기 (ONNX 변환 공부하기) [기록용]

zmo 2025. 6. 28. 23:01

 

 

 

1. Unreal Engine은 PyTorch를 직접 실행할 수 없다

  • PyTorch는 Python 기반 프레임워크
  • Unreal Engine은 C++ 기반 게임/AR/VR 엔진으로, Python 모델을 직접 로드하거나 실행할 수 없다
  • 따라서 PyTorch에서 훈련한 모델을 Unreal이 이해할 수 있는 중간 표현(ONNX) 으로 변환

2. ONNX는 범용 모델 포맷이다

  • ONNX(Open Neural Network Exchange)는 PyTorch, TensorFlow 등에서 공통으로 모델을 표현하기 위한 표준 포맷.
  • 다양한 플랫폼에서 추론 가능:
    • Unreal (C++에서 ONNX Runtime 사용)
    • Web (ONNX.js)
    • 모바일 (ONNX → CoreML or TFLite)
    • NVIDIA GPU (ONNX → TensorRT)
  • 한 번 변환하면 어디든 쓸 수 있는 "중립 포맷"으로써 사용할수 있게 세팅하기

흐름

[PyTorch에서 모델 훈련 or 불러오기] ↓ [ONNX로 변환] ↓ [Unreal Engine 내 ONNX Runtime 연동 (C++ 또는 Python via Plugin)] ↓ [실시간 입력 이미지 → Depth Map 추정 → 결과 사용]

 

 

PyTorch → ONNX 변환

import torch
from midas.model_loader import load_model
import torch.onnx

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model, transform = load_model("DPT_Large", device)
model.eval()

# Dummy 입력 생성 (B, C, H, W)
dummy_input = torch.randn(1, 3, 384, 384).to(device)

# ONNX 변환
torch.onnx.export(model, dummy_input, "midas_depth.onnx",
                  input_names=["input"], output_names=["output"],
                  opset_version=11)

opset_version은 Unreal 호환성을 위해 11~15 정도로 설정

 

 

#include <onnxruntime_cxx_api.h>

Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "DepthApp");
Ort::SessionOptions session_options;
Ort::Session session(env, "midas_depth.onnx", session_options);

// 입력 및 출력 이름 확인
Ort::AllocatorWithDefaultOptions allocator;
auto input_name = session.GetInputName(0, allocator);
auto output_name = session.GetOutputName(0, allocator);

// 입력 텐서 생성 및 실행
std::vector<float> input_tensor_values = ... // 입력 이미지 텐서로 변환
Ort::Value input_tensor = Ort::Value::CreateTensor<float>(...);

auto output_tensors = session.Run(...);

Unreal 내에서 실시간 카메라 이미지 → Tensor 변환이 필요하며, [OpenCV + UE 연동] 또는 [MediaTexture 추출] 방식으로 구현 가능

실시간 최적화

  • ONNX 모델을 TensorRT로 변환하여 속도 향상 (NVIDIA GPU 사용 시)
  • 깊이 맵 해상도 축소 → 보간 → 실시간성 확보
  • 카메라 프레임 중 일부만 추론하고 나머지는 추정 보간 처리

파일 구조는?

/DepthApp

├── midas_depth.onnx

├── DepthRuntime.cpp (ONNX 호출 코드)

├── PythonServer/ (선택?)

│ └── flask_onnx_server.py

├── UEProject/

│ ├── Plugins/

│ │ └── ONNXRuntime/

│ ├── Blueprints/

│ │ └── DepthReceiverBP.uasset

 

 

Unreal Engine의 C++ 클래스에서 ONNX Runtime을 사용해 midas_depth.onnx 모델을 로딩하고 추론하기

 

ONNXDepthComponent.h

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "onnxruntime_cxx_api.h" // ONNX Runtime C++ 헤더
#include "ONNXDepthComponent.generated.h"

// UE5에서 사용할 수 있는 Actor 컴포넌트 클래스 정의
// 이 컴포넌트는 AI 추론 기능만 전담함

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class YOURPROJECT_API UONNXDepthComponent : public UActorComponent
{
    GENERATED_BODY()

public:
    // 생성자
    UONNXDepthComponent();

    // 게임 시작 시 자동 호출되는 함수
    virtual void BeginPlay() override;

    // 실제로 모델을 실행해서 Depth를 추론하는 함수
    void RunDepthInference(const std::vector<float>& InputData, int Width, int Height);

private:
    // ONNX Runtime 관련 객체들
    Ort::Env* Env;                // 환경 객체: 로그 설정 등 초기화
    Ort::Session* Session;        // 모델 세션: 모델 로딩 및 실행 담당
    Ort::SessionOptions* SessionOptions; // 세션 설정: 멀티스레드 등 옵션 조정
};

 

 

ONNXDepthComponent.cpp

#include "ONNXDepthComponent.h"

UONNXDepthComponent::UONNXDepthComponent()
{
    // 컴포넌트는 기본적으로 매 프레임 Tick을 수행하지 않음
    PrimaryComponentTick.bCanEverTick = false;

    // ONNX Runtime 환경 생성 (로깅 레벨, 애플리케이션 이름 지정)
    Env = new Ort::Env(ORT_LOGGING_LEVEL_WARNING, "DepthInference");

    // 세션 옵션 초기화 (추론 설정)
    SessionOptions = new Ort::SessionOptions();
    SessionOptions->SetIntraOpNumThreads(1);  // 스레드 수 설정 (성능 조정)

    // ONNX 모델 파일 경로를 주고 세션 생성
    Session = new Ort::Session(*Env, TEXT("midas_depth.onnx"), *SessionOptions);
}

void UONNXDepthComponent::BeginPlay()
{
    Super::BeginPlay();

    // BeginPlay에서 추론 로직을 바로 실행해도 되고,
    // 외부에서 함수 호출로 처리해도 됨
}

void UONNXDepthComponent::RunDepthInference(const std::vector<float>& InputData, int Width, int Height)
{
    // ONNX Runtime에서 메모리 할당을 쉽게 하기 위한 기본 할당자
    Ort::AllocatorWithDefaultOptions Allocator;

    // 입력 텐서의 차원 정의: 1x3xHxW (배치 1, RGB, 높이, 너비)
    std::vector<int64_t> InputDims = {1, 3, Height, Width};

    // 전체 입력 텐서의 원소 개수 계산
    size_t InputTensorSize = 1 * 3 * Height * Width;

    // 입력 데이터로부터 ONNX Runtime 텐서 생성
    Ort::Value InputTensor = Ort::Value::CreateTensor<float>(
        Allocator,                            // 메모리 관리 도구
        const_cast<float*>(InputData.data()), // 입력 데이터 포인터 (const 제거)
        InputTensorSize,                      // 총 원소 개수
        InputDims.data(),                     // 차원 정보 포인터
        InputDims.size()                      // 차원 수
    );

    // ONNX 모델의 입력 및 출력 이름 설정
    const char* InputNames[] = {"input"};     // 모델 입력 이름
    const char* OutputNames[] = {"output"};   // 모델 출력 이름

    // 실제 추론 수행 (Run)
    auto OutputTensors = Session->Run(
        Ort::RunOptions{nullptr},             // 실행 옵션 (기본값)
        InputNames, &InputTensor, 1,          // 입력 이름 및 텐서
        OutputNames, 1                        // 출력 이름
    );

    // 출력 텐서에서 결과 데이터 가져오기 (float* 형태)
    float* OutputData = OutputTensors.front().GetTensorMutableData<float>();

    // 출력은 1x1xHxW로 나오므로 원소 수는 Height * Width
    int OutputSize = 1 * 1 * Height * Width;

    // 예시: 첫 번째 Depth 값을 UE 로그로 출력
    UE_LOG(LogTemp, Warning, TEXT("First depth value: %f"), OutputData[0]);

    // 이후: Depth 결과를 시각화하거나 저장할 수 있음
}