Pose estimation 관련 프로젝트를 진행하던 도중 여러가지 아이디어가 나왔는데 사용자의 비디오로부터 3D keypoints를 추출하여 이를 3D 캐릭터가 움직이는 기능을 추가하면 어떨까?라고 생각했다.
MHFormer는 간단하게 말하면 input video(2D)로부터 keypoints(3D)를 얻을 수 있는 모델이다.
MHFormer: Multi-Hypothesis Transformer for 3D Human Pose Estimation [CVPR 2022]
현재 프로젝트를 시작할 때 선행 기술들을 조사해본 결과 temporal based model 중 가장 결과물에서 자연스러운 모션을 보여준 MHFormer를 base model로 선정하였다. 그런데 이를 애니메이션으로 구현하려면 mediapipe를 이용해서 구현하는 경우가 많았다.
[Mediapipe]
[Mediapipe] 위 사진들을 보면 각 모델의 keypoints 구조가 조금씩 다른 것을 볼 수 있다. 쉽게 말하자면 규격이 맞지 않는 것이다.
[3D keypoints of character(출처 : Mixamo)]
Character rigging 분야는 잘 모르지만 어렴풋이 떠오르는 것은 모델링을 한 후 애니메이션을 구현하기 위해서는 뼈나 관절들을 붙이는 과정인데, Mediapipe based로 작성된 오픈소스를 사용하기 보다 rigging된 3D character를 가져와서 필요한 joints만 사용하면 된다고 생각했다.
이를 위해서 MHFormer에서 keypoints가 신체의 어떤 부분인지 알아내야 했는데 논문이나 tutorial 중 어느 부분을 찾아봐도 각 joints가 어느 부분을 가르키는 지 알 수 없었다. 코드 파일들을 하나씩 뜯어보던 중 h36m_dataset.py에서 수상한 부분을 발견했다.
Pose estimation 관련 모델들은 보통 Human 3.6M 데이터셋을 사용하는데, 위에서 살펴보았던 Mediapipe의 Pose Landmarks가 떠올랐다. 왜냐하면 위 코드에서 33개의 landmarks 중 MHFormer 모델에서 불필요한 부분을 제거하는 것으로 추측할 수 있었기 때문이다. vis.py의 get_pose3D 함수에서 모든 처리과정을 마치고 난 후의 결과값인 post_out을 출력해봤을 때 아래와 같았다.
[[ 0.0000000e+00 0.0000000e+00 8.4420329e-01]
[ 8.8207878e-02 9.2296645e-02 8.4085423e-01]
[ 2.8526276e-02 1.7012623e-01 4.4420582e-01]
[ 8.2822859e-02 1.1527586e-01 1.3722479e-02]
[-8.8199146e-02 -9.2290625e-02 8.4754664e-01]
[-1.1647248e-01 -4.5239925e-05 4.3729496e-01]
[-9.4206184e-02 -6.0740948e-02 0.0000000e+00]
[-8.1383362e-02 8.0201551e-02 1.0322781e+00]
[-2.8163904e-01 1.6704074e-01 1.2137067e+00]
[-3.7558842e-01 2.0953813e-01 1.2314460e+00]
[-3.8651535e-01 2.0851630e-01 1.3184435e+00]
[-3.1916845e-01 4.9338400e-02 1.1647184e+00]
[-2.7403679e-01 2.6366934e-03 8.8778472e-01]
[-2.8321713e-01 1.4157784e-01 7.0229799e-01]
[-1.8924257e-01 2.7290934e-01 1.1895345e+00]
[-1.9097099e-01 3.1332725e-01 9.2133105e-01]
[-3.0335081e-01 2.5122309e-01 7.0681131e-01]]
예상대로 17(33-15-1..?)개의 joints(x, y, z 좌표)를 가지고 있는 것을 알 수 있었다.
이제 공간상의 x, y, z의 좌표를 알아내는 것까지는 완료했는데, 아직까지 이 좌표들이 도대체 어느 신체부위에 위치한 joint인지 알 수 없었다.
그러던 도중 vis.py에서 좌표들을 선으로 이어서 사람과 비슷한 모양으로 시각화할텐데 이 부분에 힌트가 있을 것이라고 생각했다. 예상했던 대로 있었고 I와 J의 값들을 하나씩 불러와서 index를 하나씩 매치시키고 하나의 선분을 만드는 것이다. 예를 들면 0(hip) - 1(left hip)으로 연결하고 0(hip) - 4(right hip)으로 연결하고.... 이런 식으로 선분을 하나씩 이어보면 하나의 뼈대를 완성시킬 수 있었다.
이제 본격적으로 애니메이션을 만들기 위한 준비를 해야했다. 위에서 얻은 3차원 좌표로 애니메이션으로 구현하려면 각 joints의 rotation(roll, pitch yaw)값이 필요했다. 여기서 또 다른 하나의 과제가 생긴 것이다.
좌표와 rotation값이 함께 나오는 모델을 새로 찾아보려고 했지만 MHFormer에 비해 부자연스러운 결과물을 보여주는 경우가 많았다. 그래서 굳이 모델을 사용하기 보다 MHFormer에서 얻은 x, y, z 좌표로부터 rotation 값을 알아낼 수 있다면 가장 좋은 방법일 것 같다고 생각했다.
[Github] Calculating Joint Angles
구글링한 결과 감사하게도 소스코드가 존재했다.
이 코드에서는 어느 keypoints를 input으로 사용하는지 알아내기 위해서 calculate_joint_angles.py를 살펴보다가 위와 같은 딕셔너리를 발견했다.
그리고 이 알 수 없는 .dat 파일을 input으로 사용하는데 침착하게 read_keypoints 함수를 뜯어본 결과 reshape(num_keypoints, -1)를 보자마자 x, y, z 좌표 12개가 필요하겠구나라고 생각했다. -1은 아마 3 대신 사용하지 않았을까 생각했고 .dat 파일의 한줄에 있는 숫자의 개수를 세본 결과 36개가 맞았다.
그래서 파일입출력을 이용해서 .dat 파일에 12개의 joints의 좌표를 저장하면 되겠다고 생각했고 이를 위해서 필요한 joints를 추출하는 작업을 또 해야했다.
def joint2target(kpts):
return [
kpts[11], # leftshoulder
kpts[14], # rightshoulder
kpts[12], # leftelbow
kpts[15], # rightelbow
kpts[13], # leftwrist
kpts[16], # rightwrist
kpts[1], # lefthip
kpts[4], # righthip
kpts[2], # leftknee
kpts[5], # rightknee
kpts[3], # leftfoot
kpts[6] # rightfoot
]
여기서 순서를 맞출 때 대단한 알고리즘이 필요한 것이 아니고 오히려 더 불편함을 야기할 수 있기 때문에..... 인덱스를 일일이 넣는 하드코딩(12개 정도니까...)으로 함수 하나를 구현했다. 이 리스트를 .dat 파일에 한줄씩 저장하였다. 한줄은 동영상에서 보았을 때 하나의 프레임이라고 생각하면 편하다.
그런데 이 과정을 거쳐서 얻은 .dat 파일로 rotation 값을 얻고 시각화를 하였는데 다리로 골프를 치는 듯한 모션을 보여주었다....
왜인지 이유를 계속 생각해보다가 joint 순서를 잘못 적었나 싶어 다시 체크하고 순서도 바꿔보고 하였지만 똑같은 결과를 보여주었다. 그러던 도중 get_rotation_chain 함수에서 이상한 부분을 발견했다.
일반적인 상식으로는 당연히 x, y, z 순서로 좌표를 넣는 것이 맞다고 생각했으나 아니었다. 무지했던 탓이었다. 무작정 달려들어 결과를 내고 싶은 마음에 상세한 내용을 모른 채 좌표를 넣어 계속 실패했던 것이었다. 이를 확인하고 z, x, y 순으로 좌표를 넣었더니 성공할 수 있었다.
그리고 rotation 값을 출력해보았다.
lefthip_angles
[ 0.13728371 0.04323627 -0.00297295]
leftknee_angles
[ 0.00980843 -0.20820338 0.00102479]
leftfoot_angles
[0. 0. 0.]
righthip_angles
[ 0.19645255 -0.09465981 0.009335 ]
rightknee_angles
[-0.09364591 -0.19211892 -0.00902989]
rightfoot_angles
[0. 0. 0.]
leftshoulder_angles
[-0.65673912 0.07645581 -0.22357862]
leftelbow_angles
[-0.1142758 0.14113038 -1.78087583]
leftwrist_angles
[0. 0. 0.]
rightshoulder_angles
[1.11304404 0.79378048 1.18574193]
rightelbow_angles
[0.06174672 0.03824638 1.10895982]
rightwrist_angles
[0. 0. 0.]
hips_angles
[-0.06824357 0.14371572 -0.11844533]
neck_angles
[-0.09503102 0.84745133 -2.93152086]
12개의 joints의 rotation을 알아낼 수 있었고 mixamo에서 가져온 캐릭터는 더 디테일하게 rigging되어있기 때문에 필요한 joints만 사용하고 나머지 joints를 움직일 필요는 없다고 생각했다.
이를 실험해보기 위해서 Blender를 실행하고 Mixamo에서 무료 모델을 하나 불러왔다.
Blender에서도 rotation mode가 다양하게 있었고 ZXY Euler를 선택하였다.(나만 몰랐음)
rotation 값을 넣어본 결과..... 아래와 같았다.... 조금 더 꼼꼼히 내용을 살펴보고 다시 시도해봐야할 것 같다. Blender에서 어떻게 rotation을 다루는지와 keypoints에서 rotation값으로 변환하는 과정을 다시 살펴봐야할 것 같다.