오늘 공부해볼 내용은 Unreal + ONNX 통합 코드 (객체 감지 + 지형 파악)에 대해 알아볼 것이다. 저번 시간에 언리얼 엔진(UE)에서 그 모델을 불러와 실시간으로 추론하고 결과를 반영하기 위해 모델을 불러오고 그 결과를 추론을 통해 텍스처로 어떻게 불러오는지까지 알아보았다. 하지만 이는 본래 목적이던 언리얼 학습의 맛보기일 뿐이다.
우리는 학습 모델이 객체 감지 + 지형 파악을 추가로 할수 있도록 두 파트에 나누어서 공부를 진행해볼 예정이다. 언리얼 엔진에서 캡처하는 형식이 이미지 뿐만이 아닌 깊이데이터와 물체의 정보(일반 학습에서 태그를 붙인것과 유사하다) 또한 가져와 학습을 진행하는 코드를 Part1에서, 이 코드에 대해 알아보는 Part2로 알아볼 것이다.
C:\Users\godling\Documents\Unreal Projects\UnrealCpp
UnrealCpp/
ThirdParty/
onnxruntime/ <-- unzip onnxruntime-win-x64-gpu-1.22.1
include/
lib/
bin/ <-- onnxruntime.dll 등
opencv/
include/
x64/vc16/lib/
x64/vc16/bin/ <-- opencv_world*.dll
using UnrealBuildTool;
using System.IO;
public class UnrealCpp : ModuleRules
{
public UnrealCpp(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] {
"Core", "CoreUObject", "Engine", "RenderCore", "RHI", "UMG"
});
PrivateDependencyModuleNames.AddRange(new string[] { });
// 프로젝트 경로
string ProjectRoot = Path.GetFullPath(Path.Combine(ModuleDirectory, "../../"));
string ThirdPartyPath = Path.Combine(ProjectRoot, "ThirdParty");
// === OpenCV ===
string OpenCVPath = Path.Combine(ThirdPartyPath, "opencv");
PublicIncludePaths.Add(Path.Combine(OpenCVPath, "include"));
// lib 파일 실제 파일명으로 바꾸기.
PublicAdditionalLibraries.Add(Path.Combine(OpenCVPath, "x64", "vc16", "lib", "opencv_world412.lib"));
// Runtime DLL (패키징 시 포함)
RuntimeDependencies.Add(Path.Combine(OpenCVPath, "x64", "vc16", "bin", "opencv_world412.dll"));
// === ONNX Runtime ===
string OnnxPath = Path.Combine(ThirdPartyPath, "onnxruntime");
PublicIncludePaths.Add(Path.Combine(OnnxPath, "include"));
PublicAdditionalLibraries.Add(Path.Combine(OnnxPath, "lib", "onnxruntime.lib"));
RuntimeDependencies.Add(Path.Combine(OnnxPath, "bin", "onnxruntime.dll"));
}
}
주의: opencv_world412.lib / opencv_world412.dll 등 파일 이름은 OpenCV 버전(4.12 등)에 따라 달라진다. ThirdParty에 실제 파일명을 확인해서 Build.cs를 수정해야 한다.
// OnnxVisionComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Engine/TextureRenderTarget2D.h"
#include "Runtime/Engine/Classes/Components/SceneCaptureComponent2D.h"
#include <onnxruntime_cxx_api.h>
#include <opencv2/opencv.hpp>
#include "OnnxVisionComponent.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class UNREALCPP_API UOnnxVisionComponent : public UActorComponent
{
GENERATED_BODY()
public:
UOnnxVisionComponent();
protected:
virtual void BeginPlay() override;
public:
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
// 에디터에서 할당
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision")
USceneCaptureComponent2D* SceneCapture = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision")
UTextureRenderTarget2D* RenderTarget = nullptr;
// 모델 경로 (프로젝트 폴더 절대 경로 권장)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision")
FString DetectorOnnxPath;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision")
FString DepthOnnxPath;
// 디버그 옵션
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision")
bool bDrawDebugBoxes = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Vision")
bool bShowDepthOverlay = true;
private:
// ONNX Runtime
Ort::Env OrtEnv;
Ort::SessionOptions SessionOptions;
std::unique_ptr<Ort::Session> DetectorSession;
std::unique_ptr<Ort::Session> DepthSession;
Ort::MemoryInfo CpuMemoryInfo;
// helper
cv::Mat RenderTargetToCvMat(UTextureRenderTarget2D* RT);
std::vector<float> PreprocessForDetector(const cv::Mat& Img, int inW, int inH);
std::vector<float> PreprocessForDepth(const cv::Mat& Img, int inW, int inH);
UTexture2D* CreateTextureFromMat(const cv::Mat& Img); // for depth visualization
void RunDetector(const std::vector<float>& inputTensor, int inputW, int inputH);
void RunDepth(const std::vector<float>& inputTensor, int inputW, int inputH);
};
// OnnxVisionComponent.cpp
#include "OnnxVisionComponent.h"
#include "Engine/World.h"
#include "Kismet/KismetRenderingLibrary.h"
#include "Rendering/TextureRenderTargetResource.h"
#include "Engine/Texture2D.h"
#include "Async/Async.h"
#include "DrawDebugHelpers.h"
#include "Misc/Paths.h"
UOnnxVisionComponent::UOnnxVisionComponent()
: OrtEnv(ORT_LOGGING_LEVEL_WARNING, "UE_OnnxVision"),
CpuMemoryInfo(Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault))
{
PrimaryComponentTick.bCanEverTick = true;
SessionOptions.SetIntraOpNumThreads(1);
SessionOptions.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_EXTENDED);
}
void UOnnxVisionComponent::BeginPlay()
{
Super::BeginPlay();
// 모델 파일 있으면 세션 생성
if (!DetectorOnnxPath.IsEmpty())
{
std::string detPath = TCHAR_TO_UTF8(*DetectorOnnxPath);
DetectorSession = std::make_unique<Ort::Session>(OrtEnv, detPath.c_str(), SessionOptions);
UE_LOG(LogTemp, Warning, TEXT("Detector ONNX loaded: %s"), *DetectorOnnxPath);
}
if (!DepthOnnxPath.IsEmpty())
{
std::string dPath = TCHAR_TO_UTF8(*DepthOnnxPath);
DepthSession = std::make_unique<Ort::Session>(OrtEnv, dPath.c_str(), SessionOptions);
UE_LOG(LogTemp, Warning, TEXT("Depth ONNX loaded: %s"), *DepthOnnxPath);
}
}
void UOnnxVisionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (!RenderTarget || !SceneCapture) return;
// 1) 캡처
SceneCapture->TextureTarget = RenderTarget;
SceneCapture->CaptureScene();
// 2) RT -> cv::Mat
cv::Mat Img = RenderTargetToCvMat(RenderTarget);
if (Img.empty()) return;
// 기본 전처리 사이즈 (모델에 맞춰 조정하자)
const int DET_W = 640, DET_H = 640; // detector input
const int DEP_W = 256, DEP_H = 256; // depth input
// 3) Detector 전처리 및 비동기 추론
if (DetectorSession)
{
std::vector<float> detInput = PreprocessForDetector(Img, DET_W, DET_H);
// run async to avoid blocking game thread
Async(EAsyncExecution::ThreadPool, [this, detInput = MoveTemp(detInput), DET_W, DET_H]() mutable {
RunDetector(detInput, DET_W, DET_H);
});
}
// 4) Depth 전처리 및 비동기 추론
if (DepthSession)
{
std::vector<float> depthInput = PreprocessForDepth(Img, DEP_W, DEP_H);
Async(EAsyncExecution::ThreadPool, [this, depthInput = MoveTemp(depthInput), DEP_W, DEP_H]() mutable {
RunDepth(depthInput, DEP_W, DEP_H);
});
}
}
// -------------------- helpers --------------------
cv::Mat UOnnxVisionComponent::RenderTargetToCvMat(UTextureRenderTarget2D* RT)
{
if (!RT) return cv::Mat();
FTextureRenderTargetResource* RTResource = RT->GameThread_GetRenderTargetResource();
TArray<FColor> Bitmap;
RTResource->ReadPixels(Bitmap);
int Width = RT->SizeX;
int Height = RT->SizeY;
if (Bitmap.Num() != Width * Height) return cv::Mat();
// Create cv::Mat from FColor array (BGRA)
cv::Mat MatBGRA(Height, Width, CV_8UC4, Bitmap.GetData());
cv::Mat MatRGB;
cv::cvtColor(MatBGRA, MatRGB, cv::COLOR_BGRA2RGB);
return MatRGB.clone(); // clone to ensure continuous memory
}
std::vector<float> UOnnxVisionComponent::PreprocessForDetector(const cv::Mat& Img, int inW, int inH)
{
cv::Mat resized;
cv::resize(Img, resized, cv::Size(inW, inH));
cv::Mat floatImg;
resized.convertTo(floatImg, CV_32F, 1.0f/255.0f);
// YOLO 스타일: 1xCxHxW, channel order RGB, no normalization here or apply mean/std if needed
int C = 3;
std::vector<float> tensor(inW * inH * C);
// HWC -> CHW
for (int y=0; y<inH; ++y)
{
for (int x=0; x<inW; ++x)
{
cv::Vec3f pix = floatImg.at<cv::Vec3f>(y,x);
for (int c=0; c<3; ++c)
{
int idx = c * (inW*inH) + y * inW + x;
tensor[idx] = pix[c];
}
}
}
return tensor;
}
std::vector<float> UOnnxVisionComponent::PreprocessForDepth(const cv::Mat& Img, int inW, int inH)
{
cv::Mat resized;
cv::resize(Img, resized, cv::Size(inW, inH));
cv::Mat floatImg;
resized.convertTo(floatImg, CV_32F, 1.0f/255.0f);
// use mean/std if depth model needs (modify as per your model)
int C = 3;
std::vector<float> tensor(inW * inH * C);
for (int y=0; y<inH; ++y)
{
for (int x=0; x<inW; ++x)
{
cv::Vec3f pix = floatImg.at<cv::Vec3f>(y,x);
for (int c=0; c<3; ++c)
{
int idx = c * (inW*inH) + y * inW + x;
tensor[idx] = pix[c];
}
}
}
return tensor;
}
UTexture2D* UOnnxVisionComponent::CreateTextureFromMat(const cv::Mat& Img)
{
if (Img.empty()) return nullptr;
int W = Img.cols, H = Img.rows;
// Ensure 3-channel BGR -> convert to BGRA
cv::Mat BGRA;
if (Img.channels() == 1)
cv::cvtColor(Img, BGRA, cv::COLOR_GRAY2BGRA);
else if (Img.channels() == 3)
cv::cvtColor(Img, BGRA, cv::COLOR_BGR2BGRA);
else if (Img.channels() == 4)
BGRA = Img.clone();
else
return nullptr;
// Create transient texture
UTexture2D* Tex = UTexture2D::CreateTransient(W, H, PF_B8G8R8A8);
if (!Tex) return nullptr;
Tex->AddToRoot();
Tex->UpdateResource();
// Lock and copy
FTexture2DMipMap& Mip = Tex->PlatformData->Mips[0];
void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);
FMemory::Memcpy(Data, BGRA.data, W * H * 4);
Mip.BulkData.Unlock();
Tex->UpdateResource();
return Tex;
}
// -------------------- Inference runners --------------------
void UOnnxVisionComponent::RunDetector(const std::vector<float>& inputTensor, int inputW, int inputH)
{
if (!DetectorSession) return;
// Input shape: {1, C, H, W} — adjust if your model uses different layout
std::array<int64_t,4> inputShape = {1, 3, inputH, inputW};
size_t inputSize = inputTensor.size();
// Create input tensor
Ort::Value input = Ort::Value::CreateTensor<float>(CpuMemoryInfo, const_cast<float*>(inputTensor.data()), inputSize, inputShape.data(), inputShape.size());
// input/output names
Ort::AllocatorWithDefaultOptions allocator;
char* inName = DetectorSession->GetInputNameAllocated(0, allocator).get();
char* outName = DetectorSession->GetOutputNameAllocated(0, allocator).get();
const char* inputNames[] = { inName };
const char* outputNames[] = { outName };
// Run
auto outputs = DetectorSession->Run(Ort::RunOptions{nullptr}, inputNames, &input, 1, outputNames, 1);
// NOTE: parsing depends on model output format!
// Here we assume detector outputs Nx6 array: [x1, y1, x2, y2, score, class]
float* outData = outputs.front().GetTensorMutableData<float>();
auto outTypeInfo = outputs.front().GetTensorTypeAndShapeInfo();
std::vector<int64_t> outShape = outTypeInfo.GetShape();
size_t N = 1;
for (auto d : outShape) N *= d;
// If shape is (num,6) or (1, num, 6), handle accordingly. We attempt generic handling:
int64_t rows = (outShape.size() >= 2) ? outShape[outShape.size()-2] : (int64_t)(N/6);
int64_t cols = (outShape.size() >= 1) ? outShape.back() : 6;
// Collect detections
std::vector< TTuple<float,float,float,float,int,float> > dets; // placeholder: not compile-time type; we'll draw inline below
// For simplicity, interpret as flat (num x 6)
int numBoxes = N / cols;
struct Box { float x1,y1,x2,y2,score; int cls; };
std::vector<Box> boxes;
for (int i=0;i<numBoxes;i++)
{
int base = i * cols;
if (cols < 6) continue;
float x1 = outData[base + 0];
float y1 = outData[base + 1];
float x2 = outData[base + 2];
float y2 = outData[base + 3];
float score = outData[base + 4];
int cls = (int)outData[base + 5];
if (score < 0.3f) continue; // threshold
// Convert detector coords (assumed to be on resized input) to render target coords if needed
// Here we assume detector used same dims as the resized input; to map to world we need scale factors
boxes.push_back({x1,y1,x2,y2,score,cls});
}
// Draw boxes on game thread
AsyncTask(ENamedThreads::GameThread, [this, boxes]() {
UWorld* World = GetWorld();
if (!World) return;
for (const auto& b : boxes)
{
// Convert 2D screen coords to world debug boxes: approximate using camera location/direction, or draw 2D via UCanvas
// For simplicity, draw 2D rectangles via DrawDebugBox in world at actor location (visual approximation)
FVector Center = GetOwner()->GetActorLocation() + FVector(0,0,100); // offset upwards
FVector Extent = FVector(FMath::Abs(b.x2 - b.x1)*0.01f, FMath::Abs(b.y2 - b.y1)*0.01f, 50.0f);
DrawDebugBox(World, Center, Extent, FColor::Red, false, 0.5f);
// Show label as on-screen message
if (GEngine)
{
FString Msg = FString::Printf(TEXT("Det class %d score %.2f"), b.cls, b.score);
GEngine->AddOnScreenDebugMessage(-1, 1.5f, FColor::Yellow, Msg);
}
}
});
}
void UOnnxVisionComponent::RunDepth(const std::vector<float>& inputTensor, int inputW, int inputH)
{
if (!DepthSession) return;
std::array<int64_t,4> inputShape = {1, 3, inputH, inputW};
size_t inputSize = inputTensor.size();
Ort::Value input = Ort::Value::CreateTensor<float>(CpuMemoryInfo, const_cast<float*>(inputTensor.data()), inputSize, inputShape.data(), inputShape.size());
Ort::AllocatorWithDefaultOptions allocator;
char* inName = DepthSession->GetInputNameAllocated(0, allocator).get();
char* outName = DepthSession->GetOutputNameAllocated(0, allocator).get();
const char* inputNames[] = { inName };
const char* outputNames[] = { outName };
auto outputs = DepthSession->Run(Ort::RunOptions{nullptr}, inputNames, &input, 1, outputNames, 1);
// Assume output is [1,1,H,W] or [1,H,W], float depth values
float* outData = outputs.front().GetTensorMutableData<float>();
auto outInfo = outputs.front().GetTensorTypeAndShapeInfo();
std::vector<int64_t> shape = outInfo.GetShape();
int H = (int)shape.back();
int W = (int)shape[shape.size()-2];
if (shape.size() == 4) { H = (int)shape[2]; W = (int)shape[3]; }
// build cv::Mat grayscale float
cv::Mat depthMat(H, W, CV_32F);
memcpy(depthMat.data, outData, sizeof(float) * H * W);
// normalize for visualization
double minv, maxv;
cv::minMaxLoc(depthMat, &minv, &maxv);
cv::Mat vis;
depthMat.convertTo(vis, CV_8U, 255.0 / (maxv - minv + 1e-6), -minv * 255.0 / (maxv - minv + 1e-6));
cv::applyColorMap(vis, vis, cv::COLORMAP_JET);
// send to game thread to show as texture overlay
AsyncTask(ENamedThreads::GameThread, [this, visMat = vis.clone()]() mutable {
UTexture2D* Tex = CreateTextureFromMat(visMat);
if (!Tex) return;
// Option A: set as material parameter on an actor's mesh
// Option B: draw via UMG/Widget - here we use on-screen debug print and show that texture not trivial in code snippet
GEngine->AddOnScreenDebugMessage(-1, 1.5f, FColor::Cyan, TEXT("Depth visualized"));
// You can store Tex in a UPROPERTY to display in UI/Material
});
}
| Unreal Engine 좌표변환 이해하기(2) (0) | 2025.10.20 |
|---|---|
| Unreal Engine 좌표변환 이해하기(1) (0) | 2025.10.20 |
| Unreal Engine에서 학습한 ONNX 모델 불러와 실시간 추론하기 (0) | 2025.09.13 |
| Unreal Engine에서 CUDA와 OpenCV 사용 시행착오 정리하기 (5) | 2025.08.30 |
| Unreal Engine에서 CUDA와 OpenCV 사용하기 (2) | 2025.08.16 |