코드모음/C#

C# RTMPose Model 사용방법 정리, AI Model 실행 방법!

John-Co 2023. 8. 8. 18:54

Onnx format 을 따르는 model 파일을 이미 가지고 있다라는 가정하에 진행을 하겠습니다. 아래 설명을 그대로 따라 하시고자 한다면, 아래 파일을 다운로드해서 실습 진행해주세요.

rtmpose_t_coco.onnx
12.75MB

 

 

먼저, RTMpose model 은 Conputer vision 분야의 AI Model 입니다. 첨부된 파일은 사진속에 사람을 대상으로 17개의 포인트를 감지 해내는 AI 알고리즘 입니다.

 

여기서 17개 포인트란, 코, 눈, 귀, 어깨, 팔꿈치 등등의 포인트를 말합니다. 이해를 돕기위해 아래 그림을 첨부해 드렸습니다.

 

이제 위와 같이 AI Model 을 가지고 C# 에서 실행하는 방법에 대해 알아보겠습니다.


1. Nuget Package 설치

여기서 필요한 패키지는 총 2종류 입니다. "onnxruntime" 과 "opencvsharp" 입니다. 이미지 전/후처리를 위한 C# Opencv 패키지와, AI Model 실행을 위한 C# OnnxRuntime 패키지를 설치합니다.

 

① Nuget Package 화면에서, "OpenCvSharp" 을 검색하면 아래와 같이 두 가지 패키지가 나옵니다.

이 두가지 모두를 설치 진행해주시면 되구요.

 

② Nuget Package 화면에서, "onnxruntime" 을 검색하면 아래와 같이 패키지  한개를 찾아볼 수 있습니다.

똑같이 설치를 진행합니다.


2. Pre-Processing 코드 작성

먼저 해당 모델의 Input Shape 을 알아보도록 하겠습니다. Netron Tool 을 사용해서 모델의 Input Spec 을 확인해보면 다음과 같은데요, 보통 Computer Vision 관련 분야에서는 "BCHW" 형태를 따릅니다. Batch, Channel, Height, Width 를 뜻하죠.

일단 batch 란 이미지의 갯수를 뜻하는데요, 우리는 1개만 넣기때문에 무시하셔도 됩니다. 위의 의미를 그림으로 나타내면 아래와 같은 그림으로 표현될 수 있습니다. 총 3개의 채널인 R,G,B 각각 192 * 256 사이즈를 갖는 Array 구조임을 확인할 수 있습니다.

Model Input Shape 에 맞도록 Image 파일은 전처리 과정을 진행하게 됩니다. 이때 OpenCV 패키지를 활용해서, 손쉽게 Resizing 작업을 진행 할 수 있으며, 여기에서는 비율이 무너지더라도 강제로 Resize 작업을 하도록 했습니다.

(본래는 이미지 비율에 맞춰 Crop을 하거나, Border 부분에 덧붙이는 등의 작업을 통해 기존 이미지가 손실되지 않도록합니다.)

 

📃 Image Load 및 Resize

public string imgPath = "./pose.jpg";

private const int IN_W = 192;
private const int IN_H = 256;
        
Mat img = Cv2.ImRead(imgPath);
Mat inImg = null;

Cv2.Resize(img, inImg, new OpenCvSharp.Size(IN_W, IN_H));

 


3. Inference 코드 작성

이제 전처리된 이미지 데이터를 Tensor 로 변환하여 AI Model 에 Input 으로 실행할 차례입니다. 일단 OpenCvSharp 에서 사용되는 Mat Type 의 이미지 데이터는 Tensor 형태의 Array로 변환이 필요한데요, 이때  CvDnn.BlobFromImage() 라는 함수를 사용하게 되는데 아래 링크를 통해 조금 더 자세한 정보를 얻을 수 있습니다.

 

OpenCvSharp - Mat, Blob 기반 Array구조

1. OpenCvSharp.Mat Open CV의 기본데이터 타입 matrix(행렬) 의 약어로, 영상을 matrix 형태로 표현한 데이터 타입입니다. 보통 Cv2.imread() 함수로 이미지를 로드하면 Mat type 의 변수로 로드되어 지며, 행과

johnconomics.tistory.com

일단 blob Data 형태로 변환을 하고, Model Spec 에 맞도록 Normalize 하는 코드를 작성하도록 하겠습니다. 학습된 데이터의 표준편차와 평균값을 이용해 Normalization 을 진행합니다. (해당 표준편차와 평균값은 모델 배포하는 Github 사이트에서 보통 제공하곤 합니다.)

 

📃 Blob 변환 및 Normalization

private double[] std = new double[] { 58.395, 57.12, 57.375 };
private Scalar mean = new Scalar(123.675, 116.28, 103.53);
 
// 평균치 기반 Normalization
Mat blob = CvDnn.BlobFromImage(inImg, 1, new OpenCvSharp.Size(inImg.Width, inImg.Height), mean);

unsafe
{
    float* ptr = (float*)blob.DataPointer;
	
    // 표준편차 기준 Normalization
    for (int i = 0; i < blob.Size(1); i++)
    {
        for (int j = 0; j < (blob.Size(2) * blob.Size(3)); j++)
        {
            ptr[i * blob.Size(2) * blob.Size(3) + j] = (float)(ptr[i * blob.Size(2) * blob.Size(3) + j] / std[i]);
        }
    }
	
    // Address Pointer (*) 을 Array 형태로 변환
    float[] memArr = new float[blob.Size(0) * blob.Size(1) * blob.Size(2)];
    for (int i = 0; i < memArr.Length; i++)
    {
        memArr[i] = ptr[i];
    }
}

Tensor 를 만들기 위한 Blob 형태의 데이터로 변환이 완료되었습니다. 이제, Tensor 형태의 데이터로 가공해서 Model Inference 진행을 합니다.

 

📃 Inference Code

public string path = "./rtmpose_t_coco.onnx";
private InferenceSession sess = null;

// Load Onnx File
sess = new InferenceSession(path);

// Create Tensor
Tensor<float> input = new DenseTensor<float>(memArr, new[] { 1, 3, inImg.Height, inImg.Width });

// Merge Tensor to Batch
var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor(sess.InputNames[0], input) };

// Inference
var result = sess.Run(inputs).ToArray();

 


4. Post-Processing 코드 작성

Output 으로 나온 데이터를 후처리해서 이미지에 적용을 해볼텐데요, 일단 Netron 으로 모델 Output Spec 을 살펴보겠습니다.

위를 보시면, Output 으로 "simcc_x", "simcc_y" 두 종류의 데이터가 [batch, MatMulSimcc_x_dim_1, 384], [512] 각각 나오게 됩니다. 이를 유추해 보자면, 우리가 Input 으로 192 * 256 이미지를 넣었는데 X 좌표를 기준으로 384개, Y좌표를 기준으로 512개가 나왔다라고 볼 수 있고, Image Scale 이 X2 가 되어 결과가 나옵니다.

 

"MatMulSimcc_x_dim_1" 의 의미는 KeyPoint 갯수를 뜻합니다. 여기서는 17의 숫자를 의미합니다. 그 이야기는 곧,

simcc_x : [17, 384], simcc_y : [17, 512] 형태의 2차원 배열을 뜻하게 됩니다. 아래와 같이 요런 이미지가 연상 될 수 있겠네요.

이제 각 KeyPoint 별, 최고의 Score 를 갖는 X, Y 좌표를 얻어내고 이를 이미지에 표현하는 코드를 작성해보겠습니다.

 

📃 Post-Processing Code

var result = sess.Run(inputs).ToArray();

int max_w = (IN_W * 2), max_h = (IN_H * 2);
// Output Shape 의 경우 기존 Resolution 의 X2.
float ratio = 2;
// Input Image Resolution 기준 현 이미지 비율
float x_ratio = (float)img.Width / inImg.Width;
float y_ratio = (float)img.Height / inImg.Height;
// Key Point 갯수
int k_num = 17;

// max location, max score
float[,] result_x = new float[k_num, 2];
float[,] result_y = new float[k_num, 2];

for (int key = 0; key < k_num; key++)
{
    result_x[key, 0] = 0;
    result_x[key, 1] = 0;
    result_y[key, 0] = 0;
    result_y[key, 1] = 0;

    for (int x = 0; x < max_w; x++)
    {
        float score = result[0].AsEnumerable<float>().ToArray()[key * max_w + x];
        if (result_x[key, 1] < score)
        {
            result_x[key, 1] = score;
            result_x[key, 0] = x / ratio;
        }
    }

    for (int y = 0; y < max_h; y++)
    {
        float score = result[1].AsEnumerable<float>().ToArray()[key * max_h + y];
        if (result_y[key, 1] < score)
        {
            result_y[key, 1] = score;
            result_y[key, 0] = y / ratio;
        }
    }

    Console.WriteLine($"[{key}] : loc({result_x[key, 0]}, {result_y[key, 0]})");
}

for (int key = 0; key < k_num; key++)
{
    float x = result_x[key, 0] * x_ratio, y = result_y[key, 0] * y_ratio;
    Cv2.Circle(img, new OpenCvSharp.Point(x, y), 1, new Scalar(0, 255, 255), 2);
}

Cv2.ImShow("Result", img);
Cv2.WaitKey();
Cv2.DestroyAllWindows();

5. 실행결과

 

 

반응형