6 minute read

Amass Dataset

Amass에서 제공하는 point cloud로 smpl registration을 거쳐 나온 parameter들을 모아둔 dataset이다.
Amass Download에서 다양한 Dataset을 받을 수 있는데 SMPL+H G, SMPL-X G, SMPL-X N등이 있다. SMPL+H G는 SMPLH model을 거쳐서 나온 결과이며 G는 male or female의 정보를 가지고 있음을 보여준다. SMPL-X N은 SMPLX model을 거쳐 나온 parameter이며 N은 neutral로 나온 결과이다.
나는 여기서 SMPL+H G Dataset들을 2D Image projection 시켜보고자 한다.

requirements

기본적으로 T2M-GPT의 requirements상에서 진행했으며 다음과 같다.

torch==1.12.1+cu113 torchvision==0.13.1+cu113 torchaudio==0.12.1 --extra-index-url https://download.pytorch.org/whl/cu113
absl-py==0.13.0
backcall==0.2.0
cachetools==4.2.2
charset-normalizer==2.0.4
chumpy==0.70
cycler==0.10.0
decorator==5.0.9
google-auth==1.35.0
google-auth-oauthlib==0.4.5
grpcio==1.39.0
idna==3.2
imageio==2.9.0
ipdb==0.13.9
ipython==7.26.0
ipython-genutils==0.2.0
jedi==0.18.0
joblib==1.0.1
kiwisolver==1.3.1
markdown==3.3.4
matplotlib==3.4.3
matplotlib-inline==0.1.2
oauthlib==3.1.1
pandas==1.3.2
parso==0.8.2
pexpect==4.8.0
pickleshare==0.7.5
prompt-toolkit==3.0.20
protobuf==3.17.3
ptyprocess==0.7.0
pyasn1==0.4.8
pyasn1-modules==0.2.8
pygments==2.10.0
pyparsing==2.4.7
python-dateutil==2.8.2
pytz==2021.1
pyyaml==5.4.1
requests==2.26.0
requests-oauthlib==1.3.0
rsa==4.7.2
scikit-learn==0.24.2
scipy==1.7.1
sklearn==0.0
smplx==0.1.28
tensorboard==2.6.0
tensorboard-data-server==0.6.1
tensorboard-plugin-wit==1.8.0
threadpoolctl==2.2.0
toml==0.10.2
tqdm==4.62.2
traitlets==5.0.5
urllib3==1.26.6
wcwidth==0.2.5
werkzeug==2.0.1
git+https://github.com/openai/CLIP.git
git+https://github.com/nghorbani/human_body_prior
gdown
moviepy

너무 많은 requirements라면 아래만 사용해도 될 것으로 예상된다.

torch==1.12.1+cu113 torchvision==0.13.1+cu113 torchaudio==0.12.1 --extra-index-url https://download.pytorch.org/whl/cu113
matplotlib==3.4.3
matplotlib-inline==0.1.2
smplx==0.1.28
tqdm==4.62.2
git+https://github.com/nghorbani/human_body_prior

3D Pose preprocessing

HumanML3D에서 진행한 raw_pose_processing의 Code를 baseline으로 진행했다.
Amass dataset의 디렉토리 path는 다음과 같다.

|amass_data
| - ACCAD
| - - Female1General_c3d
| - - - A1 - Stand_poses.npz
| - - - A2 - Sway t2_poses.npz
| ...
| - - Female1Gestures_c3d
| ...
| - BioMotionLab_NTroje
| ...

가장 먼저 필요한 package들을 선언해준다.

%load_ext autoreload
%autoreload 2
%matplotlib notebook
%matplotlib inline

import sys, os
import torch
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from tqdm import tqdm



from human_body_prior.tools.omni_tools import copy2cpu as c2c

os.environ['PYOPENGL_PLATFORM'] = 'egl'

# Choose the device to run the body model on.
comp_device = torch.device("cuda:2" if torch.cuda.is_available() else "cpu")

그 후에 human_body_prior를 이용해 smpl model을 load해준다.

from human_body_prior.body_model.body_model import BodyModel

male_bm_path = './body_models/smplh/male/model.npz'
male_dmpl_path = './body_models/dmpls/male/model.npz'

female_bm_path = './body_models/smplh/female/model.npz'
female_dmpl_path = './body_models/dmpls/female/model.npz'

num_betas = 10 # number of body parameters
num_dmpls = 8 # number of DMPL parameters

male_bm = BodyModel(bm_fname=male_bm_path, num_betas=num_betas, num_dmpls=num_dmpls, dmpl_fname=male_dmpl_path).to(comp_device)
faces = c2c(male_bm.f)

female_bm = BodyModel(bm_fname=female_bm_path, num_betas=num_betas, num_dmpls=num_dmpls, dmpl_fname=female_dmpl_path).to(comp_device)

female, male model을 로드를 해준 후에 다운받았던 amass dataset들의 path를 저장해준다.

paths = []
folders = []
dataset_names = []
for root, dirs, files in os.walk('./amass_data'):
#     print(root, dirs, files)
#     for folder in dirs:
#         folders.append(os.path.join(root, folder))
    folders.append(root)
    for name in files:
        dataset_name = root.split('/')[2]
        if dataset_name not in dataset_names:
            dataset_names.append(dataset_name)
        paths.append(os.path.join(root, name))

그 후에 나는 amass_test라고 3D pose를 저장할 새로운 디렉토리를 만들어 줬다.

save_root = './amass_test'
save_folders = [folder.replace('./amass_data', './amass_test') for folder in folders]
for folder in save_folders:
    os.makedirs(folder, exist_ok=True)
group_path = [[path for path in paths if name in path] for name in dataset_names]

그다음으로 amass to pose에서 root_orient를 각 처음 motion의 orient를 빼주었다.
빼준 이유는 처음 motion의 orient를 빼주지 않으면 2d projection을 시키게 되면 위에서 보는 듯한 결과가 나오기 때문이다.
예시로 밑의 gif를 보면 확인할 수 있다.

만약 root_orient를 처음 motion에 대해서 뒤 모션을 빼주면 다음과 같은 결과를 얻을 수 있다.

ex_fps = 20
def amass_to_pose(src_path, save_path):
    bdata = np.load(src_path, allow_pickle=True)
    fps = 0
    try:
        fps = bdata['mocap_framerate']
        frame_number = bdata['trans'].shape[0]
    except:
#         print(list(bdata.keys()))
        return fps
    
    fId = 0 # frame id of the mocap sequence
    pose_seq = []
    if bdata['gender'] == 'male':
        bm = male_bm
    else:
        bm = female_bm
    down_sample = int(fps / ex_fps)
#     print(frame_number)
#     print(fps)
    root_data=0
    with torch.no_grad():
        for fId in range(0, frame_number, down_sample):
            root_orient = torch.Tensor(bdata['poses'][fId:fId+1, :3]).to(comp_device) # controls the global root orientation
            #tt_test=root_orient[0][2].detach().clone()
            if fId==0:
                root_data=root_orient
            root_orient=root_orient-root_data
            #root_orient[0][1]=tt_test
            pose_body = torch.Tensor(bdata['poses'][fId:fId+1, 3:66]).to(comp_device) # controls the body
            pose_hand = torch.Tensor(bdata['poses'][fId:fId+1, 66:]).to(comp_device) # controls the finger articulation
            betas = torch.Tensor(bdata['betas'][:10][np.newaxis]).to(comp_device) # controls the body shape

            #trans = torch.Tensor(bdata['trans'][fId:fId+1]).to(comp_device)    
            body = bm(pose_body=pose_body, pose_hand=pose_hand, betas=betas, root_orient=root_orient)
            joint_loc = body.Jtr[0] #+ trans
            pose_seq.append(joint_loc.unsqueeze(0))
    pose_seq = torch.cat(pose_seq, dim=0)
    
    pose_seq_np = pose_seq.detach().cpu().numpy()
    np.save(save_path, pose_seq_np)
    return fps

root_data에 처음 motion을 저장했다가 다음 frame마다 빼주는 식으로 code를 구성했다.
그 다음으로 3D Pose를 npy형태로 저장하게 끔 만들었다.

import time
for paths in group_path:
    dataset_name = paths[0].split('/')[2]
    pbar = tqdm(paths)
    pbar.set_description('Processing: %s'%dataset_name)
    fps = 0
    for path in pbar:
        #print(path)
        save_path = path.replace('./amass_data', './amass_test')
        save_path = save_path[:-3] + 'npy'
        #print(save_path)
        fps = amass_to_pose(path, save_path)
        
    cur_count += len(paths)
    print('Processed / All (fps %d): %d/%d'% (fps, cur_count, all_count) )
    time.sleep(0.5)

2D image Projection

이제부터 2D image에 projection을 하는 방법을 설명하고자 한다.
우선 npy 형식으로 저장한 하나의 data를 우선 얻고 skeleton을 확인한다.

import numpy as np
test=np.load("/home/qazw5741/T2M-GPT/amass_test/BMLhandball/S09_Novice/Trial_upper_right_020_poses.npy")
skeleton = ( (0,1), (1,4), (4,7), (7,10), (0,2), (2,5), (5,8), (8,11), (0,3), (3,6), (6,9), (9,14), (14,17), (17,19), (19, 21), (9,13), (13,16), (16,18), (18,20), (9,12))

skeleton은 SMPLH의 regressor 결과로 해당하는 joint는 다음과 같다.

     0: 'pelvis',
     1: 'left_hip',
     2: 'right_hip',
     3: 'spine1',
     4: 'left_knee',
     5: 'right_knee',
     6: 'spine2',
     7: 'left_ankle',
     8: 'right_ankle',
     9: 'spine3',
    10: 'left_foot',
    11: 'right_foot',
    12: 'neck',
    13: 'left_collar',
    14: 'right_collar',
    15: 'head',
    16: 'left_shoulder',
    17: 'right_shoulder',
    18: 'left_elbow',
    19: 'right_elbow',
    20: 'left_wrist',
    21: 'right_wrist',
    22: 'left_index1',
    23: 'left_index2',
    24: 'left_index3',
    25: 'left_middle1',
    26: 'left_middle2',
    27: 'left_middle3',
    28: 'left_pinky1',
    29: 'left_pinky2',
    30: 'left_pinky3',
    31: 'left_ring1',
    32: 'left_ring2',
    33: 'left_ring3',
    34: 'left_thumb1',
    35: 'left_thumb2',
    36: 'left_thumb3',
    37: 'right_index1',
    38: 'right_index2',
    39: 'right_index3',
    40: 'right_middle1',
    41: 'right_middle2',
    42: 'right_middle3',
    43: 'right_pinky1',
    44: 'right_pinky2',
    45: 'right_pinky3',
    46: 'right_ring1',
    47: 'right_ring2',
    48: 'right_ring3',
    49: 'right_thumb1',
    50: 'right_thumb2',
    51: 'right_thumb3'

우선 3D pose로 plotting 시켜보면 다음 코드를 작성해서 확인 가능하다.
(나는 보기 편하도록 hand에 대한 pose를 제외한 상태로 진행했다.)

import matplotlib.pyplot as plt

for num,frame in enumerate(test):
    fig = plt.figure(figsize=(4,4))
    ax = fig.add_subplot(111, projection='3d')
    ax.set_xlabel("X_axis")
    ax.set_ylabel("Y_axis")
    ax.set_zlabel("Z_axis")
    ax.view_init(90,-90) # default
    #ax.view_init(0,0) # default
    ax.set_xlim([-1, 1])
    ax.set_ylim([0, 2])
    ax.set_zlim([-1, 1])
    skeleton = ( (0,1), (1,4), (4,7), (7,10), (0,2), (2,5), (5,8), (8,11), (0,3), (3,6), (6,9), (9,14), (14,17), (17,19), (19, 21), (9,13), (13,16), (16,18), (18,20), (9,12))
    test_x=[]
    test_y=[]
    test_z=[]
    for i,j in skeleton:
        test_x.append(frame[i][0])
        test_x.append(frame[j][0])
        test_y.append(frame[i][1])
        test_y.append(frame[j][1])
        test_z.append(frame[i][2])
        test_z.append(frame[j][2])
        ax.plot(test_x,test_y,test_z)
        test_x=[]
        test_y=[]
        test_z=[]
    break

set_xlim,set_ylim,set_zlim은 3d plotting에서 보여줄 범위를 보여준다고 할 수 있고 view_init은 보는 각도를 의미한다. (90,-90)을 선택한 이유는 가장 보기 좋기 때문이다.

3d plot을 하게되면 위의 애니메이션 처럼 나오게 되고 root는 처음 시작할 때 [0,0,0]임을 확인할 수 있다.
이제 다음으로 2D Projection을 진행할 차례인데 projection에서 가장 중요한건 Camera Intrinsic, Extrinsic parameter이다.
카메라 외부 파라미터에서 pose에서 z에 해당하는 값에 +4를 더해준 후에 진행을 하였는데 이유는 이러한 작업 없이 바로 projection을 시키게 되면 카메라가 너무 가까워 적절한 pose를 뽑을 수 없기 때문이다.

camera_pose = np.eye(4)
camera_pose[:3, 3] = np.array([0, 0, 4])
joint_test=test[0]
res_joint=np.insert(joint_test,3,1,axis=1)
res_joint=res_joint.transpose()
res_joint=np.dot(camera_pose[:3],res_joint)

그 후에 대략적인 image shape으로 500,500을 선택했고 이에 따른 focal length estimation 함수를 적용해 focal length를 구했고 princpt는 image의 중앙으로 설정했다.
cam2pixel은 projection 시키는 함수이고 결과에 나오는 z는 버린다고 생각하면 된다.

img_shape=[500,500]
princpt=[int(img_shape[0]/2),int(img_shape[1]/2)]
def estimate_focal_length(img_h, img_w):
    return (img_w * img_w + img_h * img_h) ** 0.5
focal_xy=estimate_focal_length(img_shape[0],img_shape[1])
focal=[focal_xy,focal_xy]
def cam2pixel(cam_coord, f, c):
    x = cam_coord[:,0] / cam_coord[:,2] * f[0] + c[0]
    y = cam_coord[:,1] / cam_coord[:,2] * f[1] + c[1]
    z = cam_coord[:,2]
    return np.stack((x,y,z),1)
joint_img_xyz=cam2pixel(res_joint.transpose(),focal,princpt)

이렇게 코드를 거치면 projection된다.
joint_img_xyz를 plotting 시키면 다음과 같다.

만약 애니메이션을 보고싶다면 다음 코드를 작성하면 된다.

camera_pose = np.eye(4)
camera_pose[:3, 3] = np.array([0, 0, 4])
for num,i in enumerate(test):
    res_joint=np.insert(i,3,1,axis=1)
    res_joint=res_joint.transpose()
    res_joint=np.dot(camera_pose[:3],res_joint)

    img_shape=[500,500]
    princpt=[int(img_shape[0]/2),int(img_shape[1]/2)]
    def estimate_focal_length(img_h, img_w):
        return (img_w * img_w + img_h * img_h) ** 0.5
    focal_xy=estimate_focal_length(img_shape[0],img_shape[1])
    focal=[focal_xy,focal_xy]
    def cam2pixel(cam_coord, f, c):
        x = cam_coord[:,0] / cam_coord[:,2] * f[0] + c[0]
        y = cam_coord[:,1] / cam_coord[:,2] * f[1] + c[1]
        z = cam_coord[:,2]
        return np.stack((x,y,z),1)
    joint_img_h36m_xyz=cam2pixel(res_joint.transpose(),focal,princpt)
    plt.xlim([0, 500])
    plt.ylim([0, 500])
    skeleton = ( (0,1), (1,4), (4,7), (7,10), (0,2), (2,5), (5,8), (8,11), (0,3), (3,6), (6,9), (9,14), (14,17), (17,19), (19, 21), (9,13), (13,16), (16,18), (18,20), (9,12))
    test_x=[]
    test_y=[]
    for i,j in skeleton:
        test_x.append(joint_img_h36m_xyz[i][0])
        test_x.append(joint_img_h36m_xyz[j][0])
        test_y.append(joint_img_h36m_xyz[i][1])
        test_y.append(joint_img_h36m_xyz[j][1])
        plt.plot(test_x,test_y)
        test_x=[]
        test_y=[]
    plt.savefig('./amass_fig/savefig_{}.jpg'.format(str(num).zfill(3)))
    plt.cla()

Next

다음에 진행할 연구는 SMPLH의 vertex정보를 뽑아서 regressor를 돌려 eyes, nose, ear pose를 뽑아볼 계획이다.
여기에 다음 연구가 있다.

Categories:

Updated:

Leave a comment