딥러닝 기반 포즈 인식(skeleton 탐지)

2025. 4. 12. 00:50·Study/Computer Vision

- 이전에는 YOLO 모델을 이용한 컴퓨터 비전 객체 탐지 기능을 구현해 보았습니다.

Skeleton 탐지는, 객체 중 인체에서 머리, 어깨, 가슴, 팔꿈치, 손, 무릎 등의 각 파트를 구분하여 탐지하는 기능으로,

인체의 각 파츠를 탐지함으로써 인체 포즈를 구분해낼 수 있습니다.

 

아래에 실습해볼 내용은 현 시점 그대로 사용하기엔 낡은 기술일지도 모르지만(본 포스팅을 작성한 시점은 2022년도 입니다.),

실시간으로 영상에서 인체 부위를 구분하여 탐지하는 모델로서는 성능상으로 상당히 유용한 방법이며,

인체 탐지를 경험해보기 좋은 예시입니다.

(아래에 소개하는 방식을 조금 운용하면, 양 눈과 코, 입으로 구성된 안면 파츠를 탐지해서 표정이나 시선 방향 탐지를 할 수도 있습니다.)

 

- 먼저, 영상 데이터에서 객체를 탐지하는 것은, 여러가지가 있는데, 딥러닝의 객체 탐지 모델을 사용하면 됩니다.

당장 darknet yolo를 사용해서 객체에 바운딩 박스를 그려도 되고, pre training 된 모델에서, 특징을 추려내는 CNN 부분을 떼어내고, 전이학습을 시켜서 객체 탐지 모델을 스스로 만들수도 있습니다.(이 경우엔 이젠 거의 대부분 오픈된 상태로, 문제는 보다 빠르고 효율적으로 탐지하는게 관건)

분류와 회귀 정도만 배워둔 딥러닝으로, 갑작스레 조금 실용적인 부분을 파고들려 하는데,
human skeleton detection이라는게 있더군요.
human pose estimation의 한 방식으로,

 

사람을 인식하고, 사람의 위에, 움직이는 관절부위를 탐지하여 미리 정해진 skeleton을 분할해서 투영해주는 것을 말합니다.

(분할된 부분은 모두 skeleton 학습과 추출이 가능하며, 고로 손과 같은 부분에도 이를 추출하여 제스쳐, 손동작 파악으로 사용할수 있습니다.)

0부터 17까지의 18개의 Key Point가 존재하고, 각각의 점을, 미리 훈련된 딥러닝 모델로 씌워주는 것입니다.

이에 대해서는 과정이 존재합니다.
1. 사람이라는 객체를 탐지해서 바운딩 박스를 그림
2. 추출된 바운딩 박스의 좌표들을 통해, RoI를 선정하여, 사람 부분만 따로 떼어냄
3. 추출된 사람 사진에서 미리 훈련된 모델로, 키 포인트를 생성함(포인트를 생성하면 저절로 각 ID별로 이어진 곳에 직선을 그릴수 있음)

그리고, 키 포인트를 생성하는 부분의 학습의 경우는,
미리 사람마다 키 포인트를 찍어놓은 데이터를 사용하는 것입니다.

모델에 대해서는 솔직히 모릅니다. 아래에 구현할 것은, 미리 학습되어 제공되는 모델을 OpenCV로 가져와서 사용할 것이기 때문이죠.

모델링은 신뢰도 추정방식의 분류모델입니다.
각각 cnn으로 특징을 추출하고, 해당 부분이 목이다 팔꿈치다 어깨다라고 픽셀별로 판단한 신뢰도 맵을 출력해줍니다.

모델을 학습시키는 데이터셋은 일단
http://cocodataset.org/#keypoints-2018
http://www.robots.ox.ac.uk/~vgg/data/pose_evaluation/
같은 것이 있다고 합니다.

그러니까, 여기까지는 학습 모델의 기능이 2개 필요한 것입니다.
객체 인식으로 사람 부분을 인식해서 바운딩 박스를 그리고,
그 RoI 내에서, 사람에게 키포인트를 찍는 방식입니다.

더 응용하자면, 여기서 나온 키포인트 데이터를 가지고, LSTM을 돌려서, 시간의 흐름에 따른 움직임을 판별할수도 있겠죠.

솔직히 키포인트 찍는것만 해도, 이렇게 주어진 딥러닝 모델을 사용하는게 아니라, 사람이 일일이 수학적 모델링으로 수식을 세우려고 하면... 일단 제 수준으론 불가능합니다.

- 스켈레톤 생성 모델
https://arxiv.org/pdf/1611.08050.pdf
를 기반으로 만들어졌다는 모델을 제공받아 사용할 것입니다.

모델 구조도

구조를 전부 설명할수는 없고,
기본적인 골격을 말하자면,

가장 먼저 VGG-19라는 CNN 계층을 통해, 이미지 특징을 추려올 것입니다.

사전 훈련된 컨볼루션 특징 추출 모델로, 보통 이미지 인식 모델을 만들려면 이러한 모델의 CNN 계층을 떼어와서, 뒤에 원하는 분류 모델을 만들어 전이학습시키는 방식을 사용하죠.

전이학습에 대한 것은 검색해 보시고(텐서플로로 된 실습 자료도 많습니다.), VGG는, 
https://datascienceschool.net/view-notebook/47c57f9446224af08f13e1f3b00a774e/

그리고 VGG에서 이미지 판별에 필요한 특징들을 추출했으면, 그것을 사용하는 2개의 병렬 레이어를 만듭니다.

나뉘어지는 첫번째 branch는, 신체 각 부위의 2D 신뢰도 맵을 판별하는 CNN 레이어의 결합으로, 
각 구역별로, 해당 부위가 몇퍼센트 신뢰도로 무릎이다, 어깨다, 목이다 라고, 회귀분류하는 레이어입니다.(신뢰도 구간은 0에서 1 사이의 실수값)

두번째 branch는, part affinity를 예측하는 부분이라고 하는데,
정확히는 모르겠습니다.

아마, 첫번째 branch에서는 파츠의 신뢰도를 구하고, 이곳에서는, 각 파츠가 올바른 위치에 위치했는지를 판단하는 데이터를 출력하는 것 같습니다.

위에서 제가 설명한 객체 추정 및 RoI 분리는 이 모델에서 따로 할 필요 없이 한번에 처리가 가능하다는데,
stage2부분에서, 신뢰도와 선호도 맵을 받아들여서, 각 사람별로 키포인트를 찍어준다고 하네요.

이것이 2016 COCO keypoints challenge에서 우승한 모델로,
정확한 모델 분석은 다음에 하겠습니다.

- 위 논문의 저자는 2가지 타입의 모델을 제공합니다.
하나는 MPII (Multi-Person Dataset)를 사용하여 학습한 모델로, 15개의 포인트를 반환하고,
나머지는 COCO dataset을 사용하여 학습한 모델로, 18개의 포인트를 반환합니다.

각각 데이터 셋 마다 키 포인트의 숫자에 의미가 있으며,

COCO Output Format Nose – 0, Neck – 1, Right Shoulder – 2, Right Elbow – 3, Right Wrist – 4, Left Shoulder – 5, Left Elbow – 6, Left Wrist – 7, Right Hip – 8, Right Knee – 9, Right Ankle – 10, Left Hip – 11, Left Knee – 12, LAnkle – 13, Right Eye – 14, Left Eye – 15, Right Ear – 16, Left Ear – 17, Background – 18 

MPII Output Format Head – 0, Neck – 1, Right Shoulder – 2, Right Elbow – 3, Right Wrist – 4, Left Shoulder – 5, Left Elbow – 6, Left Wrist – 7, Right Hip – 8, Right Knee – 9, Right Ankle – 10, Left Hip – 11, Left Knee – 12, Left Ankle – 13, Chest – 14, Background – 15

위와 같습니다.

- 학습된 모델 구조에 대해서는,
https://github.com/CMU-Perceptual-Computing-Lab/openpose/tree/master/models
에서 받을수 있습니다.

- 이제 본격적으로 키 포인트 추출 및 출력 프로그램을 만들어 볼것입니다.

OpenCV를 이용할 것이며, 위 출처 링크에는 C++와 python 두가지 버전에 대해서 모두 코드가 존재하는데,

저는 그냥 C++를 사용한 코드만 리뷰할 것입니다.(코드는 위 블로그의 본문에서 download code를 하시면 쉽게 받을수 있습니다. 이메일만 입력하면, 코드 다운 깃허브 경로를 얻을수 있는데, 이외에도 다양한 OpenCV 영상 프로그램 코드가 있습니다.)

OpenCV로 딥러닝 모델을 사용하는 방법,
외부의 가중치를 가져와서 사용하는 방식을 위주로 볼 것이고,
그 데이터를 간단하게 영상에 씌워서, 출력합시다.

-1. 전체코드

#include <opencv2/dnn.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>

using namespace std;
using namespace cv;
using namespace cv::dnn;

#define MPI

#ifdef MPI
const int POSE_PAIRS[14][2] = 
{   
    {0,1}, {1,2}, {2,3},
    {3,4}, {1,5}, {5,6},
    {6,7}, {1,14}, {14,8}, {8,9},
    {9,10}, {14,11}, {11,12}, {12,13}
};

string protoFile = "pose/mpi/pose_deploy_linevec_faster_4_stages.prototxt";
string weightsFile = "pose/mpi/pose_iter_160000.caffemodel";

int nPoints = 15;
#endif

#ifdef COCO
const int POSE_PAIRS[17][2] = 
{   
    {1,2}, {1,5}, {2,3},
    {3,4}, {5,6}, {6,7},
    {1,8}, {8,9}, {9,10},
    {1,11}, {11,12}, {12,13},
    {1,0}, {0,14},
    {14,16}, {0,15}, {15,17}
};

string protoFile = "pose/coco/pose_deploy_linevec.prototxt";
string weightsFile = "pose/coco/pose_iter_440000.caffemodel";

int nPoints = 18;
#endif

int main(int argc, char **argv)
{

    cout << "USAGE : ./openpose <imageFile> " << endl;
    
    string imageFile = "single.jpeg";
    // Take arguments from commmand line
    if (argc == 2)
    {   
      imageFile = argv[1];
    }

    int inWidth = 368;
    int inHeight = 368;
    float thresh = 0.1;    

    Mat frame = imread(imageFile);
    Mat frameCopy = frame.clone();
    int frameWidth = frame.cols;
    int frameHeight = frame.rows;

    double t = (double) cv::getTickCount();
    Net net = readNetFromCaffe(protoFile, weightsFile);

    Mat inpBlob = blobFromImage(frame, 1.0 / 255, Size(inWidth, inHeight), Scalar(0, 0, 0), false, false);

    net.setInput(inpBlob);

    Mat output = net.forward();

    int H = output.size[2];
    int W = output.size[3];

    // find the position of the body parts
    vector<Point> points(nPoints);
    for (int n=0; n < nPoints; n++)
    {
        // Probability map of corresponding body's part.
        Mat probMap(H, W, CV_32F, output.ptr(0,n));

        Point2f p(-1,-1);
        Point maxLoc;
        double prob;
        minMaxLoc(probMap, 0, &prob, 0, &maxLoc);
        if (prob > thresh)
        {
            p = maxLoc;
            p.x *= (float)frameWidth / W ;
            p.y *= (float)frameHeight / H ;

            circle(frameCopy, cv::Point((int)p.x, (int)p.y), 8, Scalar(0,255,255), -1);
            cv::putText(frameCopy, cv::format("%d", n), cv::Point((int)p.x, (int)p.y), cv::FONT_HERSHEY_COMPLEX, 1, cv::Scalar(0, 0, 255), 2);

        }
        points[n] = p;
    }

    int nPairs = sizeof(POSE_PAIRS)/sizeof(POSE_PAIRS[0]);

    for (int n = 0; n < nPairs; n++)
    {
        // lookup 2 connected body/hand parts
        Point2f partA = points[POSE_PAIRS[n][0]];
        Point2f partB = points[POSE_PAIRS[n][1]];

        if (partA.x<=0 || partA.y<=0 || partB.x<=0 || partB.y<=0)
            continue;

        line(frame, partA, partB, Scalar(0,255,255), 8);
        circle(frame, partA, 8, Scalar(0,0,255), -1);
        circle(frame, partB, 8, Scalar(0,0,255), -1);
    }
    
    t = ((double)cv::getTickCount() - t)/cv::getTickFrequency();
    cout << "Time Taken = " << t << endl;
    imshow("Output-Keypoints", frameCopy);
    imshow("Output-Skeleton", frame);
    imwrite("Output-Skeleton.jpg", frame);

    waitKey();

    return 0;
}

 

0. 준비
C++를 기준으로 설명하므로,
C++가 설치되어야 하고,
OpenCV를 설치하세요.

C++ 설치는 검색해보시면 되고,
OpenCV는 제 블로그 컴퓨터 비전 카테고리에서 설치법을 검색하시거나, 혹은 구글링을 하시면 나와있습니다.
OS의 경우에는 윈도우를 하시건 리눅스를 하시건 상관 없습니다.

1. 모델 가중치 다운로드

sudo chmod a+x getModels.sh
./getModels.sh
위의 깃허브에 모델 제공자가 제공해준 getModels 쉘 파일로 모델을 받아올수 있습니다.

윈도우의 경우는 bat 파일로 받아오면 되겠죠.

수동으로 받으려면,
MP모델은 posefs1.perception.cs.cmu.edu/OpenPose/models/pose/mpi/pose_iter_160000.caffemodel
COCO모델은 posefs1.perception.cs.cmu.edu/OpenPose/models/pose/coco/pose_iter_440000.caffemodel
에서 받으시면 됩니다.

모델은 caffe로 학습시키신 것 같은데, .caffe 파일이 잘 다운받아졌는지를 확인하세요.
해당 파일 내에, 모델의 구조 정보와 가중치 정보가 들어있어서 복원이 가능합니다.

2. 신경망 로드
caffe는 텐서플로와 같은 딥러닝 학습 프레임워크인데,
caffe 모델의 저장 구성요소는 신경망 아키텍쳐를 저장하는 .prototxt 부분과,
학습된 가중치를 저장하는 .caffemodel파일로 나뉘어져 있습니다.

// Specify the paths for the 2 files
string protoFile = "pose/mpi/pose_deploy_linevec_faster_4_stages.prototxt";
string weightsFile = "pose/mpi/pose_iter_160000.caffemodel";
 
// Read the network into Memory
Net net = readNetFromCaffe(protoFile, weightsFile);

 

OpenCV에서 Caffe 모델을 사용할수 있도록 제공되는 readNetFromCaffe로, 아키텍쳐와 가중치를 인자로 넣어주면, 신경망이 메모리에 로드된 것입니다.

3. 이미지 데이터 준비

//
Mat frame = imread("single.jpg");
// Specify the input image dimensions
int inWidth = 368;
int inHeight = 368;
 
// Prepare the frame to be fed to the network
Mat inpBlob = blobFromImage(frame, 1.0 / 255, Size(inWidth, inHeight), Scalar(0, 0, 0), false, false);

// Set the prepared object as the input blob of the network
net.setInput(inpBlob);

 

모델의 동작을 확인할 이미지를 준비합니다.

추후 캠으로 이미지를 받아와서 실시간으로 스켈레톤을 찍을수도 있는데, 여기선 그냥 사람 전신이 들어있는 사진 한장을 준비하면 됩니다.

이것을 그냥 그대로 집어넣으면 안되는데,
신경망이 존재한다면, 그 입력층에 맞게 변형을 해주어야 합니다.

이미지 blob을 추출해야 하며,
보시다시피 함수가 제공됩니다.

추출하려는 원본 이미지를 넣고, 이미지의 데이터 스케일을 0~1 사이의 실수값으로, 그리고 이미지 사이즈는 368x368로 해줍니다. 평균값을 (0,0,0)으로 두고, 채널 교체는 할 필요 없으니 뒤에는 false로 둡니다.

채널이라는 것은, 컬러를 표현하는 순서를 말하는데,

보통 RGB라고 말하지만, OpenCV에서는 BGR 순서입니다.
모델과 OpenCV가 색을 판단하는 순서가 다르면 문제가 일어나니, 이를 맞춰줘야 하는데,
OpenCV나 Caffe 둘다 색을 BGR로 사용하니 바꿀 필요가 없습니다.

이렇게 준비한 이미지를, 신경망 객체에 setInput 메소드로 넣어주면, 입력층에 순전파하기 전의 단계로 준비가 되는 것입니다.

4. 예측 실행

Mat output = net.forward()

 

만들어진 신경망 모델을 사용하는게 가장 쉽죠.
OpenCV의 딥러닝 api는, 순전파시 predict가 아닌 forward를 사용하면 됩니다.

그러면 결과값이 output으로 들어옵니다.
이는 4D matrix형태입니다.

첫번째 차원은, 이미지의 ID로, 네트워크에 하나 이상의 이미지를 넣었을 때, 각 이미지별 결과를 분별하기 위한 것입니다.
두번째 차원은, 키포인트의 인덱스를 의미합니다.


Confidence Maps와 Part Affinity maps를 반환한다는데, 
COCO 모델의 경우는, 키포인트 신뢰도 맵 18개  + 배경 1개 + Part Affinity maps 19*2개로 해서, 총 57개의 데이터를 반환한다고 합니다.

솔직히 Part Affinity maps이라는게 뭔지 정확히 모르겠습니다. 두 점 사이의 연관성을 말하는 것 같기는 한데...
그냥 앞의 18개의 포인트 신뢰도 맵을 사용합시다.
세번째 차원은 output map의 높이
네번째 차원은 output map의 너비를 의미합니다.

요약하자면, 각 픽셀별로 해당 픽셀에 대한 맵이 나옵니다.

0번 맵에서는 위에서 언급한대로 목의 키 포인트인지에 대한 신뢰도 결과를 나타내고, 1번, 2번... 이렇게 해서, 마지막까지 신뢰도를 조회하면 됩니다.((0, n)으로 조회를 하며 키포인트 신뢰도 구간 결과만 가져옵시다.)

키포인트 신뢰도 맵이란, 해당 부분이 어느정도 확률로 해당 클래스에 속할지를 나타내주는 것으로, 가장 높은 값을 사용하면 되고, 오류를 없애기 위해, 신뢰도가 일정 이하인 키들은 날려주면 됩니다.

5. 출력 키포인트 출력
출력된 키포인트를 원본에 합성해서, 얼마나 잘 나왔는지를 확인해봅시다.

    int H = output.size[2];
    int W = output.size[3];
 
    // find the position of the body parts
    vector<point> points(nPoints);
    for (int n=0; n < nPoints; n++)
    {
        // Probability map of corresponding body's part.
        Mat probMap(H, W, CV_32F, output.ptr(0,n));
 
        Point2f p(-1,-1);
        Point maxLoc;
        double prob;
        minMaxLoc(probMap, 0, &prob, 0, &maxLoc);
        if (prob > thresh)
        {
            p = maxLoc;
            p.x *= (float)frameWidth / W ;
            p.y *= (float)frameHeight / H ;
 
            circle(frameCopy, cv::Point((int)p.x, (int)p.y), 8, Scalar(0,255,255), -1);
            cv::putText(frameCopy, cv::format("%d", n), cv::Point((int)p.x, (int)p.y), cv::FONT_HERSHEY_COMPLEX, 1, cv::Scalar(0, 0, 255), 2);

        }
        points[n] = p;
    }

 

스켈레톤 탐지 실행 결과

 

코드와 결과는 위와 같은데,

nPoints는, 전처리기에 따라, 학습한 모델 종류에 따라 18개인지 15개인지를 원본 코드에 적혀있는데,
각각 사이즈에 맞는 points 벡터를 생성하고, 여기에 신뢰도 높은 점들을 추려낼 것입니다.

각각 파츠별로 신뢰도가 높은 것들만 추려올 것인데,
probMap으로 output의 높이와 너비만큼, (0, n)만을 추려옵니다.

OpenCV의 minMaxLoc으로 probMap의 최대값과 최대값의 포인터를 얻어오고,
최대값이 임계점을 넘는 것들만 p에 담아서, 좌표를 구해주면 됩니다.

p.x와 p.y를 계산하는 이유는 각자 생각해보세요.

map에서의 W와 프레임의 width가 비율이 다르니, 제대로된 좌표를 구하려면, 둘 사이의 비율을 곱해줘야 합니다.

그렇게 구해진 좌표를 가지고, OpenCV로 해당 점에 원을 그려주고, 각각의 인덱스를 표기해주면, 결과를 얻어낼수 있습니다.


6. 선 긋기
각 점을 이어, 선을 만들려면,
각각의 번호가 어디에 연결된지를 알아야 합니다.
원본 코드에 각 모델별 pairs를 적어두었습니다.

for (int n = 0; n < nPairs; n++)
{
    // lookup 2 connected body/hand parts
    Point2f partA = points[POSE_PAIRS[n][0]];
    Point2f partB = points[POSE_PAIRS[n][1]];

    if (partA.x<=0 || partA.y<=0 || partB.x<=0 || partB.y<=0)
        continue;
 
    line(frame, partA, partB, Scalar(0,255,255), 8);
    circle(frame, partA, 8, Scalar(0,0,255), -1);
    circle(frame, partB, 8, Scalar(0,0,255), -1);
}

 

pairs 별로 선을 그어주면 되는데,
각 페어만큼 선회하며, 먼저 페어의 n번째 출발점의 인덱스와 일치하는 points를 만들어 partA라고 하고,
도착점의 인덱스와 일치하는 것은 partB라고 합니다.

신뢰도에 따라 키포인트가 검출이 안될수 있으니, 각각 x,y별로 값이 0이라면, 이어질 곳이 없기 때문에 continue를 하고,
이상이 없다면, 각 점마다 선을 연결해주면 됩니다.

 

탐지점 연결

 

- 이상입니다.

저작자표시 비영리 변경금지 (새창열림)

'Study > Computer Vision' 카테고리의 다른 글

[딥러닝] FCN(Fully Convolutional Networks) 정리 (Image Segmentation, DeConvolution, Skip Connections)  (0) 2025.06.07
[딥러닝] SigLIP(Sigmoid Loss for Image-Text Pretraining) 모델 정리 (ViT 이미지 인코더 백본)  (0) 2025.06.06
Convolutional Neural Networks 개념 설명 및 실습 (Pytorch CNN 이미지 분류기 구축 + CAM(Class Activation Map))  (0) 2025.04.19
OpenCV, Dlib Python 얼굴인식기 제작  (0) 2025.04.10
Colab을 사용한 YOLOv4 커스텀 객체 탐지 모델 학습 (컴퓨터에 눈을 달아보자!)  (1) 2025.03.31
'Study/Computer Vision' 카테고리의 다른 글
  • [딥러닝] SigLIP(Sigmoid Loss for Image-Text Pretraining) 모델 정리 (ViT 이미지 인코더 백본)
  • Convolutional Neural Networks 개념 설명 및 실습 (Pytorch CNN 이미지 분류기 구축 + CAM(Class Activation Map))
  • OpenCV, Dlib Python 얼굴인식기 제작
  • Colab을 사용한 YOLOv4 커스텀 객체 탐지 모델 학습 (컴퓨터에 눈을 달아보자!)
Railly Linker
Railly Linker
IT 지식 정리 및 공유 블로그
  • Railly Linker
    Railly`s IT 정리노트
    Railly Linker
  • 전체
    오늘
    어제
  • 공지사항

    • 분류 전체보기 (106)
      • Programming (33)
        • BackEnd (18)
        • FrontEnd (2)
        • DBMS (1)
        • ETC (12)
      • Study (72)
        • Computer Science (20)
        • Data Science (17)
        • Computer Vision (16)
        • NLP (15)
        • ETC (4)
      • Error Note (1)
      • ETC (0)
  • 인기 글

  • 최근 글

  • 최근 댓글

  • 태그

    kotlin linkedlist
    kotlin mutablelist
    MacOS
    jvm 메모리 누수
    list
    논리적 삭제
    springboot 배포
    kotlin arraylist
    localhost
    docker 배포
    지리 정보
    network_mode: "host"
    데이터베이스 제약
    Kotlin
    단축키
    docker compose
    unique
  • 링크

    • RaillyLinker Github
  • hELLO· Designed By정상우.v4.10.0
Railly Linker
딥러닝 기반 포즈 인식(skeleton 탐지)
상단으로

티스토리툴바