olrlobt

[Pose Estimation] Mediapipe Pose 3D grid에 새로운 축을 적용하고, 새로운 축으로 좌표 변환하기 본문

Spring/Project

[Pose Estimation] Mediapipe Pose 3D grid에 새로운 축을 적용하고, 새로운 축으로 좌표 변환하기

olrlobt 2023. 4. 4. 01:35

저번 포스팅에서 Mediapipe 분석 결과를 3D grid로 출력하는 것을 다루었다.

 

https://olrlobt.tistory.com/54

 

[Pose Estimation] Mediapipe Pose 분석 결과 3D grid로 렌더링하기

토이 프로젝트 진행 중, 3D 결과 값을 보여준다면 사용자에게 더 좋은 결과를 보여 줄 수 있다고 생각하여, 3D Utils의 Grid를 이용하여 사용자에게 결과를 보여주기로 하였다. Mediapipe Media pipe는 Googl

olrlobt.tistory.com

 

 


PoseWolrdLandmarks가 나타내는 값

poseWolrdLandmarks는 Mediapipe 공식 홈페이지에서 엉덩이 중심의 점으로부터 meter 단위의 3D 좌표라고 안내하고 있다. 나는 당연히 사람 기준이라고 생각을 했고, 이 값을 이용하여 사람의 포즈를 분석할 생각이었다.

 

하지만 PoseWorldLandmarks 값을 직접 확인한 결과 사람을 기준으로 좌표를 책정한 것은 맞지만, x, y, z 축의 원점이 엉덩이 중앙 점일 뿐, 공간상의 좌표는 카메라를 기준으로 하고 있었다.

 

무슨 말인지 이해를 돕기 위해 아래 사진을 보자. 

 

해당 그림은 요가의 전사자세로, 앞서 올린 영상의 대각선 정면 방향을 Mediapipe로 분석한 결과이다.

31번 점은 왼쪽 발을, 32번 점은 오른쪽 발을 의미하는데, 

좌표를 잘 보자.

 

전사 자세의 경우 왼발을 앞으로, 오른발을 뒤로 쭉 빼게 된다.

그 말은 즉, poseWorldLandmarks가 사람이 기준이었다면, x 좌표의 차이가 없어야 한다는 말이다.

하지만 결괏값에서 볼 수 있듯이 그렇지 않았고, 다른 좌표의 점들과 비교해서 분석해 본 결과 카메라가 기준이라는 것을 알 수 있었다.

 

즉, 엉덩이 중앙 점을 원점으로 카메라의 가로가 x축, 세로가 y축, 카메라에서 먼 쪽으로 z 축이 되는 것이다.

 

31번 점을 보면, 위 그림에서 엉덩이 중앙 점보다 왼쪽에 있기 때문에 x의 값이 음수(-),

y 값은 위로 갈수록 음수(-) 이기 때문에 양의 값이 나왔고,

z 값은 카메라에서 가까운 쪽이 음수(-)이다.

 

더 확연한 차이의 이해를 돕기 위해 같은 포즈를 다른 방면에서 찍은 영상을 분석해 봤다.

 

 

왼쪽 사진의 포즈를 보면, 이 전에 분석한 사진과 같은 자세인 것을 알 수 있다.

하지만, 그리드의 방향을 보면 알 수 있듯이, 원본 영상은 오른쪽 사진의 자세를 취하고 있는 영상으로, 자세는 같고 방향만 다른 영상이다.

 

이제, 분석 결과를 보자. 두 발(31,32)의 z좌표(카메라부터 얼마나 먼 지)가 비슷하고, 엉덩이를 기준으로 오른쪽에 위치한 31번 발의 x좌표가 양수임을 확인할 수 있었다.

 


데이터 변환

내가 필요로 하는 데이터는 한 자세를 여러 방향에서 찍어도 같은 값이 나오는 데이터이다.

하지만 poseWorldLandmarks는 위와 같이 좌표의 기준이 앞서 설명한 것과 같기 때문에, 그대로는 사용할 수 없었다. 

 

따라서 poseWorldLandmarks 결과를 변환하기로 했다.

 

아이디어

먼저, 내가 원하는 데이터가 무엇인지 생각을 해 보았다.

 

카메라가 어디서 사람을 찍던지, 같은 자세를 취하고 있으면 같은 값의 결과가 나오는 데이터가 필요했다.

다시 말해, 오른팔을 앞으로 뻗는 자세로 몸을 돌려 회전을 했다면, 이는 같은 값의 결과를 내야 한다.

 

오른팔을 앞으로 뻗은 것은 똑같으니까. 

 

따라서, 기존의 축 대신, 사람을 기준으로 하는 축을 써야 할 것 같다는 생각을 했다.

기존의 축은 앞서 말했듯, 카메라를 기준으로 x, y, z 축이 잡혀있다. 그렇게 되면 사람이 카메라에서 바라보는 방향에 따라 좌표가 다르게 책정되게 된다. 하지만, 사람을 기준으로 축을 다시 세운다면, 내가 앞으로 뻗은 손은, 항상 내 앞에 있으므로 같은 결과를 줄 것이라 생각했다.

 

그리고는 다음 고민에 빠졌다.

과연 사람을 기준으로 축을 세울 때, 어느 것을 기준으로 세울지가 문제였다.

 

처음엔 단순하게 어깨 선을 기준으로, 혹은 골반선을 기준으로 세우면 될 것이라 생각했다. 하지만 그리 간단한 문제가 아니었다. 사람은 고개나 어깨를 정면에 둔 상태로 하체를 돌리는 것이 가능하다. 또한, 고개와 하체를 정면에 둔 상태로 상체를 돌리는 것 또한 가능하다. 그렇다면 사람의 정면은 어디를 기준으로 하나?

 

여러 생각이 오갔고 내 결론은 배꼽을 기준으로 세우는 것이 가장 이상적일 것이라는 생각에 도달했다.

 

그 이후, X축, Y축, Z 축이 될 기준에 대해 정리를 했다.

 

Y축 : 어깨 중앙선과 골반 중앙 선을 이은 선

X축 : 옆구리를 이은 선

Z 축 : 배꼽이 보고 있는 선

 

구현

먼저, poseWorldLandmarks를 ajax로 서버로 보내준 이후에 처리를 진행하였다.

 

 // 어깨 중앙선과 엉덩이 중앙선을 구한다
        double[] shoulderCenter = {
                (data.get(11).get("x") + data.get(12).get("x")) / 2,
                (data.get(11).get("y") + data.get(12).get("y")) / 2,
                (data.get(11).get("z") + data.get(12).get("z")) / 2};
        double[] hipCenter = {
                (data.get(23).get("x") + data.get(24).get("x")) / 2,
                (data.get(23).get("y") + data.get(24).get("y")) / 2,
                (data.get(23).get("z") + data.get(24).get("z")) / 2};

double[] yAxis = {hipCenter[0] - shoulderCenter[0], hipCenter[1] - shoulderCenter[1],
                hipCenter[2] - shoulderCenter[2]};

 

좌 어깨 : 11 / 우 어깨 : 12 / 좌 엉덩이 : 23 / 우 엉덩이 : 24

 

 

가장 먼저 Y축을 구하기 위해, 어깨 중앙점과 엉덩이 중앙점을 구하고, 벡터의 차를 이용하여 Y축을 정의하였다.

 

 

  // 옆구리 중앙선을 구합니다.
        double[] leftSideCenter = {
                (data.get(11).get("x") + data.get(23).get("x")) / 2,
                (data.get(11).get("y") + data.get(23).get("y")) / 2,
                (data.get(11).get("z") + data.get(23).get("z")) / 2};
        double[] rightSideCenter = {
                (data.get(12).get("x") + data.get(24).get("x")) / 2,
                (data.get(12).get("y") + data.get(24).get("y")) / 2,
                (data.get(12).get("z") + data.get(24).get("z")) / 2};
                
                
double[] leftToRight = {rightSideCenter[0] - leftSideCenter[0], rightSideCenter[1] - leftSideCenter[1],
                rightSideCenter[2] - leftSideCenter[2]};

 

그다음, 똑같이 옆구리의 중앙 점들을 이어, X축을 구해주려고 했다.

 

하지만, 위 옆구리들을 이은 점은 X축이 될 수 없다.

왜냐하면, 옆구리의 중앙 점들을 이은 점이 항상 Y축가 수직인 것이 아니다.

 

또한 옆구리 운동을 해 보았다면 알겠지만, 옆구리는 되게 잘 늘어난다.

 

00:27:10 INFO 두 벡터 사이의 각 = 94.5

 

 

 

너무 간단하게만 생각했다.

 

 

위 사진을 보면 초록색 두 선분이 얼핏 보면 수직이 될 수 있지만, 그렇지 않은 것이다.

 

이를 두고 고민을 하다 보니, Z 축이 먼저 생각이 났다.

Z 축은 이 두 벡터와 수직이기만 하면 되니까 구할 수 있지 않을까?라는 생각이 들었다.

 


그렇게 되면 Z 축과 Y축을 정의할 수 있으므로 자연스럽게 X축도 구할 수 있겠다는 생각이 들었다.

 

 

double[] zAxis = crossProduct(leftToRight, yAxis);
/**
     * 벡터의 외적
     *
     * @return : 벡터 v1,v2와 수직인 벡터
     */
    public static double[] crossProduct(double[] v1, double[] v2) {
        double[] verticalVector = new double[3];
        verticalVector[0] = v1[1] * v2[2] - v1[2] * v2[1];
        verticalVector[1] = v1[2] * v2[0] - v1[0] * v2[2];
        verticalVector[2] = v1[0] * v2[1] - v1[1] * v2[0];
        return verticalVector;
    }

 

두 벡터의 수직인 벡터는 벡터의 외적을 통하여 구할 수 있다.

 

이렇게 옆구리를 이은 선과, Y축을 이용하여 Z 축을 정의해 주었다.

double[] xAxis = crossProduct(yAxis, zAxis);

그다음, 같은 방식으로 Z 축과 Y축의 외적을 이용하여 X축을 정의해 주었다.

 

그리고 새로운 축들의 크기가 다르기 때문에, 전부 단위 벡터로 변환해 주었다.

/**
     * 벡터를 단위 벡터로 정규화 하는 함수
     *
     * @param v : 벡터
     * @return : 정규화 벡터
     */
    public double[] normalize(double[] v) {
        double[] unitVector = new double[3];
        double magnitude = vectorSize(v);

        if (magnitude > 0) {
            unitVector[0] = v[0] / magnitude;
            unitVector[1] = v[1] / magnitude;
            unitVector[2] = v[2] / magnitude;
        }
        return unitVector;
    }

 

 

 

그런 다음 세 개의 축이 서로 수직임을 확인했다.

00:27:10 INFO x,y 사이의 각 = 90.0
00:27:10 INFO y,z 사이의 각 = 90.0
00:27:11 INFO z,x 사이의 각 = 90.0

 

 

 

이제 이 축에 맞게 각 벡터를 회전해 주는 작업을 해야 한다.

 

벡터는 회전 행렬을 이용하여 어떤 한 축을 기준으로 회전할 수 있다.

그리고 이 회전 행렬은 벡터의 내적을 통하여 계산되며, 다음과 같이 변환을 진행해 주었다.

 

for (Map<String, Double> keyPoint : data) {

    double[] point = {keyPoint.get("x"),
            keyPoint.get("y"), keyPoint.get("z")};

    keyPoint.put("x", dotProduct(xAxis, point));
    keyPoint.put("y", dotProduct(yAxis, point));
    keyPoint.put("z", dotProduct(zAxis, point));
}
/**
 * 벡터의 내적
 */
public static double dotProduct(double[] v1, double[] v2) {
    return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2];
}

 

 

결과

 

앞서 다른 결과를 보인 두 영상을 그대로 사용하였다.

 

결과를 보면 알 수 있듯이 추정이 힘든 부위를 제외하고는 비슷한 자세를 취하고 있고,

결과에 해당하는 값도 비슷한 값이 나오는 것을 알 수 있었다.

 

 

 

또한, 회전이 많은 영상에서도 몸의 정면이 중심이 되었기 때문에 회전하지 않는 것을 확인할 수 있었다.

 

현재 결과는 사람의 몸이 뒤를 보고 있는 방향으로 분석이 진행되고 있다.

만약 사람이 앞을 보길 바란다면, zAxis를 구할 때 외적 하는 벡터의 순서를 바꾸어 주도록 하자.

 

 

 

Comments