Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FR] VMC protocol support #193

Closed
vivi90 opened this issue Apr 7, 2022 · 102 comments
Closed

[FR] VMC protocol support #193

vivi90 opened this issue Apr 7, 2022 · 102 comments

Comments

@vivi90
Copy link
Contributor

vivi90 commented Apr 7, 2022

Feature request: VMC protocol export support
https://protocol.vmc.info/english

Ready to help, implementing it.

@Arthur151
Copy link
Owner

I don't know what is VMC developed for ? But it seems interesting. What do we need to achieve this?

@vivi90
Copy link
Contributor Author

vivi90 commented Apr 22, 2022

@Arthur151 Sorry for my late answer.

I don't know what is VMC developed for ?

To keep it short:
The VMC protocol is based on the OSC protocol.
Established with the goal to create an realtime connection standard between different motion capture and 3D avatar rendering solutions (beside other features).
It's mainly used by vTubers.

What do we need to achieve this?

In short: Not much.
At least required are the skeleton bone positions according to the VRM specs.

Let's do it this way:
At first I will provide an python package for handling all required basic VMC communication.
After that, i answer you again with more concrete info about how to use this package in ROMP.
What do you think?

@Arthur151
Copy link
Owner

It seems interesting. Looking forward to your detailed introduction.

@vivi90
Copy link
Contributor Author

vivi90 commented May 18, 2022

@Arthur151 Hallo my friend 🙂

[..] Looking forward to your detailed introduction.

We have two possibilities to implement the VMC protocol.

a) Sending all skeleton bones with it's Position(x, y, z) and Quaternion(x, y, z, w)

Valid bones

  • Hips
  • LeftUpperLeg
  • RightUpperLeg
  • LeftLowerLeg
  • RightLowerLeg
  • LeftFoot
  • RightFoot
  • SpineRightFoot
  • Chest
  • Neck
  • Head
  • LeftShoulder
  • RightShoulder
  • LeftUpperArm
  • RightUpperArm
  • LeftLowerArm
  • RightLowerArm
  • LeftHand
  • RightHand
  • LeftToes
  • RightToes
  • LeftEye
  • RightEye
  • Jaw
  • LeftThumbProximal
  • LeftThumbIntermediate
  • LeftThumbDistal
  • LeftIndexProximal
  • LeftIndexIntermediate
  • LeftIndexDistal
  • LeftMiddleProximal
  • LeftMiddleIntermediate
  • LeftMiddleDistal
  • LeftRingProximal
  • LeftRingIntermediate
  • LeftRingDistal
  • LeftLittleProximal
  • LeftLittleIntermediate
  • LeftLittleDistal
  • RightThumbProximal
  • RightThumbIntermediate
  • RightThumbDistal
  • RightIndexProximal
  • RightIndexIntermediate
  • RightIndexDistal
  • RightMiddleProximal
  • RightMiddleIntermediate
  • RightMiddleDistal
  • RightRingProximal
  • RightRingIntermediate
  • RightRingDistal
  • RightLittleProximal
  • RightLittleIntermediate
  • RightLittleDistal
  • UpperChest

Pros
Exact pose.

Cons
The by ROMP estimated bones needs to match the bones of the used VRM model. The local positions of these model bones depends on the used model by the user. The good news: We are able to read these values from the users model file (vrm-c/vrm-specification#371).

b) Sending only some keypoints as virtual trackers with it's Position(x, y, z) and Quaternion(x, y, z, w).

Valid trackers

  • Head
  • Spine
  • Pelvis
  • LeftHand
  • RightHand
  • LeftFoot
  • RightFoot

Pros
Don't need to match the model bones.

Cons
No exact pose.

I have taken an quick look into the ROMP FBX export script.
But i don't know how diffcult it is to implement a) or b).
Solution a) implies maybe some sort of IK (inverse kinematics), if not yet implemented.
On the other hand might solution b) be good enough.

So what do you think?
How about testing both solutions?

The communication protocol itself is easy.

@Arthur151
Copy link
Owner

Arthur151 commented May 18, 2022

Hi, Vivian,

Glad to hear from you!

Yes, you can get all keypoints' 3D location and their quaternion from this function

def process_pose(current_frame, pose, trans, pelvis_position):

I think the challenge is about getting all joints we need from estimated SMPL mesh.
I think the default skeleton of each character might be different from corresponding joints of SMPL.
So it would be wonderful if we have a skeleton of VMC rigged to SMPL model. This could be the perfect solution. Some automatric rigging algorithm/software(Blender) could do this.

@vivi90
Copy link
Contributor Author

vivi90 commented May 18, 2022

@Arthur151

Yes, you can get all keypoints' 3D location and their quaternion from this function

def process_pose(current_frame, pose, trans, pelvis_position):

How to use it?

I think the challenge is about getting all joints we need from estimated SMPL mesh. I think the default skeleton of each character might be different from corresponding joints of SMPL.

i think the same way.

So it would be wonderful if we have a skeleton of VMC rigged to SMPL model. This could be the perfect solution. Some automatric rigging algorithm/software(Blender) could do this.

Unfortunately i am absolutely not familiar with rigging, SMPL or it's mathematics.
Have already taken a look at the SMPL docu but don't unterstand it.

@vivi90
Copy link
Contributor Author

vivi90 commented May 18, 2022

[..] a skeleton of VMC rigged to SMPL model [..]

You mean VRM.
VRM is the 3D Avatar model (extended binary glTF).
VMC is the transmission protocol (extended OSC) for position and quaterion transforms.

@Arthur151
Copy link
Owner

For the first step, I think you can just take the video predictions from simple-romp for animation.
You can take the 3D keypoints ('joints') and convert 'smpl_thetas' to quaterion.

@vivi90
Copy link
Contributor Author

vivi90 commented May 18, 2022

For the first step, I think you can just take the video predictions from simple-romp for animation. You can take the 3D keypoints ('joints') and convert 'smpl_thetas' to quaterion.

Thank you but i don't understand how your code for SMPL works.
So i am not able to implement it by myself.
Maybe some more comments in the code may help. 🙂

@vivi90
Copy link
Contributor Author

vivi90 commented May 18, 2022

So it would be wonderful if we have a skeleton of VMC rigged to SMPL model. This could be the perfect solution. Some automatric rigging algorithm/software(Blender) could do this.

Unfortunately the VMC protocol specification is fairly unclear about this and calibration.
I asked it's author and he pointed me to the reference implementation: https://github.com/sh-akira/VirtualMotionCapture/blob/master/Assets/Scripts/ExternalSender/ExternalSender.cs

So for me there is no information at all about it.

@Arthur151
Copy link
Owner

Then we can start with solution 2 then.

  1. Firstly please run simple-romp on demo videos to get motion sequence saved in video_results.npz.
  2. Please refer to
    def process_pose(current_frame, pose, trans, pelvis_position):

    to convert the results to the format you want.

@vivi90
Copy link
Contributor Author

vivi90 commented May 18, 2022

Then we can start with solution 2 then.

Okay.

  1. Firstly please run simple-romp on demo videos to get motion sequence saved in video_results.npz.

Using the realtime webcam tracking would be nice 🙂

  1. Please refer to

    def process_pose(current_frame, pose, trans, pelvis_position):

    to convert the results to the format you want.

I need the bone name, position (x, y, z) and the quaternion (x, y, z, w).
Euler rotation angles are also okay, then i only need to calculate the quaternion.

@Arthur151
Copy link
Owner

Sure, you can firstly using video API for testing.

Bone name
https://github.com/Arthur151/ROMP/tree/master/simple_romp#joints-in-output-npz-file

position is the key joints in outputs / saved .npz

The vector-angle 3D rotation of 24 SMPL joints are smpl_thetas
You can convert to any format you like via

def transform_rot_representation(rot, input_type='mat',out_type='vec'):

That's it, all the things you need. My job is done. Let's see your part.

@vivi90
Copy link
Contributor Author

vivi90 commented Jun 3, 2022

@Arthur151 After taking a look at this line:

bpy.ops.export_scene.gltf(filepath=output_path, export_format='GLB', export_selected=True, export_morph=False)

I had an spontaneous idea (not tested):
The VMC protocol is mainly used with *.fbx and *.vrm (binary glTF) models.
For example by using the following snippet, it's easy to extract bones and it's positions of the user's VRM model:

#!/usr/bin/env python3

from gltflib import GLTF

gltf = GLTF.load_glb(
    "test.vrm",
    load_file_resources = False
)
print(gltf.model.nodes[101]) # leftUpperLeg

(more details: vrm-c/vrm-specification#371)

So how about to:

  1. add an option to load the users model at the beginning (by using bpy.ops.import_scene.gltf()).
  2. combining the extracted bone positions from the user's model with the estimated bone quaternions.
  3. just sending the results 'as it is' via VMC protocol.

I will test it soon, just wanted to share my thoughts. 🙂

@vivi90
Copy link
Contributor Author

vivi90 commented Jun 3, 2022

Oh important:
It seems i missed to mention, that the VMC protocol only supports single-person applications.
Note for users: This requires to use Simple ROMP with the --show_largest option:

parser.add_argument('--show_largest', action='store_true', help = 'Whether to show the largest person only')

@Arthur151
Copy link
Owner

Hi @vivi90 ,
Happy to hear from you~
Thanks for your efforts on developing such a great extension.
Looking forward to see it.
Best,
Yu

@vivi90
Copy link
Contributor Author

vivi90 commented Jun 8, 2022

@Arthur151 I have taken an closer look at the rigs (from left: VRM, SMPL v1.0.0, Mixamo):
rig comparison
It seems you are ignoring the ..root joint (f_avg_root or m_avg_root) from the SMPL v1.0.0 FBX model (for Unity)?


bones[bone_name_from_index[0]].location = root_location

rootbone = bones[0]

And then looking for it by the wrong name:
if "avg_root" in bone.name:

Is this a bug? 🙂

@vivi90
Copy link
Contributor Author

vivi90 commented Jun 9, 2022

@Arthur151 It seems, we did not require Blender or bpy just for the VMC protocol.
If i understand convert2fbx.py the right way, then it doesn't do any vertex estimation from the *.npz file.
Only bone rotations and the root bone translation.
If thats the point, then everything that is there done by bpy can also achieved by gltflib (of course not for *.fbx).

Note:
I see in np.load(input_path, allow_pickle=True)['results'][()][frame_name]['smpl_thetas'][0] the following:

{
  'smpl_thetas': array(
    [
      [
        2.99169707e+00, -1.14872180e-01, 2.52191164e-02,
        -5.85280538e-01, -2.17623878e-02, 1.04836568e-01,
        -5.94840944e-01, 2.05031112e-02, -4.04515043e-02,
        4.90758419e-01, -1.56615092e-03, -1.22337053e-02,
        1.18982077e+00, 6.25830796e-03, -1.33860081e-01,
        1.03421855e+00, -4.33490239e-02, 9.75086764e-02,
        8.08686297e-03, -1.64354779e-02, -1.56924091e-02,
        -5.29559441e-02, 1.50208578e-01, -8.31045508e-02,
        -6.23615980e-02, -1.55124173e-01, 8.85685012e-02,
        3.80534641e-02, -1.49868811e-02, -1.34220216e-02,
        -2.74092108e-01, 9.23865065e-02, 1.74044952e-01,
        -2.23217666e-01, -2.57145371e-02, -1.95539817e-01,
        -1.50648043e-01, 2.01505367e-02, -2.51643322e-02,
        3.70577164e-02, -2.34452099e-01, -3.42204958e-01,
        2.55778432e-02, 2.65333027e-01, 2.82382011e-01,
        8.90259165e-03, 5.71487285e-03, 1.26959458e-02,
        9.14356261e-02, -3.04939717e-01, -9.25578892e-01,
        1.75735921e-01, 3.51879328e-01, 9.20472264e-01,
        6.75733909e-02, -5.05667686e-01, 6.31040707e-02,
        2.48549446e-01, 8.01332653e-01, -9.67753083e-02,
        7.34215826e-02, -6.02314919e-02, 1.28485382e-01,
        5.53916991e-02, 8.01604688e-02, -1.09875299e-01,
        0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
        0.00000000e+00, 0.00000000e+00, 0.00000000e+00
      ]
    ], 
    dtype = float32
  )
}

It's not Quaternions or Euler degrees.
What exactly is it?
I see it's processed by the following lines but still unsure:

if pose.shape[0] == 72:

What means 72?
mat_rots = [Rodrigues(rod_rot) for rod_rot in rod_rots]

Okay, after that it's an rotation matrix.
And here:
bone_rotation = Matrix(mat_rot).to_quaternion()

It becomes finally an Quaternion.

@vivi90
Copy link
Contributor Author

vivi90 commented Jun 9, 2022

@Arthur151 I need a little bit help.
I wanted to use *.npz files, created by an video (see: #267):

romp -t -sc=3 --onnx --show_largest --mode=video -i "E:\Projekte\romp-test\test.mp4" --calc_smpl --render_mesh --save_video -o "E:\Projekte\romp-test\test_result.mp4"

According to:

frame_results = np.load(input_path, allow_pickle=True)['results'][()]

and:
sequence_results = np.load(input_path, allow_pickle=True)['sequence_results'][()]

I get in the variable frame_results for the frame files (for example 00000215.npz) valid results and sequence_results is invalid (KeyError: 'sequence_results is not a file in the archive').
Seems okay to me.

But if i am testing it with the final *.npz file (for example video_results.npz), then i get for both (frame_results and sequence_results) invalid results: _pickle.UnpicklingError: Failed to interpret file 'video_results.npz' as a pickle

What's my mistake? 🙂

@vivi90
Copy link
Contributor Author

vivi90 commented Jun 9, 2022

@Arthur151 Sorry, my mistake.
Seems to load properly, if i edit the script a little bit and open it inside Blender:

# -*- coding: utf-8 -*-

# Max-Planck-Gesellschaft zur Förderung der Wissenschaften e.V. (MPG) is
# holder of all proprietary rights on this computer program.
# You can only use this computer program if you have closed
# a license agreement with MPG or you get the right to use the computer
# program from someone who is authorized to grant you that right.
# Any use of the computer program without a valid license is prohibited and
# liable to prosecution.
#
# Copyright©2019 Max-Planck-Gesellschaft zur Förderung
# der Wissenschaften e.V. (MPG). acting on behalf of its Max Planck Institute
# for Intelligent Systems. All rights reserved.
#
# Contact: ps-license@tuebingen.mpg.de
#
# Author: Joachim Tesch, Max Planck Institute for Intelligent Systems, Perceiving Systems
#
# Create keyframed animated skinned SMPL mesh from .pkl pose description
#
# Generated mesh will be exported in FBX or glTF format
#
# Notes:
#  + Male and female gender models only
#  + Script can be run from command line or in Blender Editor (Text Editor>Run Script)
#  + Command line: Install mathutils module in your bpy virtualenv with 'pip install mathutils==2.81.2'

import os
import sys
import time
import argparse
import numpy as np
from math import radians

try:
    import bpy
except:
    print('Missing bpy, install via pip, please install bpy by yourself if failed.')
    os.system('pip install future-fstrings')
    os.system('pip install tools/bpy-2.82.1 && post_install')
    import bpy
try:
    from mathutils import Matrix, Vector, Quaternion, Euler
except:
    os.system('pip install mathutils==2.81.2')
    from mathutils import Matrix, Vector, Quaternion, Euler


# Globals
# Add your UNIX paths here!
male_model_path = '/home/yusun/ROMP/model_data/SMPL_unity_v.1.0.0/smpl/Models/SMPL_m_unityDoubleBlends_lbs_10_scale5_207_v1.0.0.fbx'
female_model_path = 'E:/Projekte/romp-test3/SMPL_f_unityDoubleBlends_lbs_10_scale5_207_v1.0.0.fbx'
character_model_path = None

'''
python tools/convert2fbx.py --input=/home/yusun/BEV_results/video_results.npz --output=/home/yusun/BEV_results/dance.fbx --gender=female
'''

fps_source = 24
fps_target = 24

gender = 'female' #female

support_formats = ['.fbx', '.glb', '.bvh']

vrm_bone_names = (
    0 : "Hips",
    1 : "LeftUpperLeg",
    2 : "RightUpperLeg",
    3 : "Spine",
    4 : "LeftLowerLeg",
    5 : "RightLowerLeg",
    6 : "Chest",
    7 : "LeftFoot",
    8 : "RightFoot",
    9 : "UpperChest"
    10 : "LeftToes",
    11 : "RightToes",
    12 : "Neck",
    13 : "LeftIndexProximal",
    14 : "RightIndexProximal",
    15 : "Head",
    16 : "LeftShoulder",
    17 : "RightShoulder",
    18 : "LeftUpperArm",
    19 : "RightUpperArm",
    20 : "LeftLowerArm",
    21 : "RightLowerArm",
    22 : "LeftHand",
    23 : "RightHand"
)

bone_name_from_index = {
    0 : 'Pelvis',
    1 : 'L_Hip',
    2 : 'R_Hip',
    3 : 'Spine1',
    4 : 'L_Knee',
    5 : 'R_Knee',
    6 : 'Spine2',
    7 : 'L_Ankle',
    8: 'R_Ankle',
    9: 'Spine3',
    10: 'L_Foot',
    11: 'R_Foot',
    12: 'Neck',
    13: 'L_Collar',
    14: 'R_Collar',
    15: 'Head',
    16: 'L_Shoulder',
    17: 'R_Shoulder',
    18: 'L_Elbow',
    19: 'R_Elbow',
    20: 'L_Wrist',
    21: 'R_Wrist',
    22: 'L_Hand',
    23: 'R_Hand'
}

# To use other avatar for animation, please define the corresponding 3D skeleton like this.
bone_name_from_index_character = {
    0 : 'Hips',
    1 : 'RightUpLeg',
    2 : 'LeftUpLeg',
    3 : 'Spine',
    4 : 'RightLeg',
    5 : 'LeftLeg',
    6 : 'Spine1',
    7 : 'RightFoot',
    8: 'LeftFoot',
    9: 'Spine2',
    10: 'LeftToeBase',
    11: 'RightToeBase',
    12: 'Neck',
    13: 'LeftHandIndex1',
    14: 'RightHandIndex1',
    15: 'Head',
    16: 'LeftShoulder',
    17: 'RightShoulder',
    18: 'LeftArm',
    19: 'RightArm',
    20: 'LeftForeArm',
    21: 'RightForeArm',
    22: 'LeftHand',
    23: 'RightHand'
}

# Helper functions

# Computes rotation matrix through Rodrigues formula as in cv2.Rodrigues
# Source: smpl/plugins/blender/corrective_bpy_sh.py
def Rodrigues(rotvec):
    theta = np.linalg.norm(rotvec)
    r = (rotvec/theta).reshape(3, 1) if theta > 0. else rotvec
    cost = np.cos(theta)
    mat = np.asarray([[0, -r[2], r[1]],
                      [r[2], 0, -r[0]],
                      [-r[1], r[0], 0]])
    return(cost*np.eye(3) + (1-cost)*r.dot(r.T) + np.sin(theta)*mat)


# Setup scene
def setup_scene(model_path, fps_target):
    scene = bpy.data.scenes['Scene']

    ###########################
    # Engine independent setup
    ###########################

    scene.render.fps = fps_target

    # Remove default cube
    if 'Cube' in bpy.data.objects:
        bpy.data.objects['Cube'].select_set(True)
        bpy.ops.object.delete()

    # Import gender specific .fbx template file
    bpy.ops.import_scene.fbx(filepath=model_path)


# Process single pose into keyframed bone orientations
def process_pose(current_frame, pose, trans, pelvis_position):

    if pose.shape[0] == 72:
        rod_rots = pose.reshape(24, 3)
    else:
        rod_rots = pose.reshape(26, 3)

    mat_rots = [Rodrigues(rod_rot) for rod_rot in rod_rots]

    # Set the location of the Pelvis bone to the translation parameter
    armature = bpy.data.objects['Armature']
    bones = armature.pose.bones

    # Pelvis: X-Right, Y-Up, Z-Forward (Blender -Y)
    root_location = Vector(
        (100*trans[1], 100*trans[2], 100*trans[0])) - pelvis_position
    # Set absolute pelvis location relative to Pelvis bone head
    bones[bone_name_from_index[0]].location = root_location
            
    # bones['Root'].location = Vector(trans)
    bones[bone_name_from_index[0]].keyframe_insert('location', frame=current_frame)

    for index, mat_rot in enumerate(mat_rots, 0):
        if index >= 24:
            continue

        bone = bones[bone_name_from_index[index]]

        bone_rotation = Matrix(mat_rot).to_quaternion()
        quat_x_90_cw = Quaternion((1.0, 0.0, 0.0), radians(-90))
        #quat_x_n135_cw = Quaternion((1.0, 0.0, 0.0), radians(-135))
        #quat_x_p45_cw = Quaternion((1.0, 0.0, 0.0), radians(45))
        #quat_y_90_cw = Quaternion((0.0, 1.0, 0.0), radians(-90))
        quat_z_90_cw = Quaternion((0.0, 0.0, 1.0), radians(-90))

        if index == 0:
            # Rotate pelvis so that avatar stands upright and looks along negative Y avis
            bone.rotation_quaternion = (quat_x_90_cw @ quat_z_90_cw) @ bone_rotation
        else:
           bone.rotation_quaternion = bone_rotation

        bone.keyframe_insert('rotation_quaternion', frame=current_frame)

    return


# Process all the poses from the pose file
def process_poses(
        input_path,
        gender,
        fps_source,
        fps_target,
        subject_id=-1):

    print('Processing: ' + input_path)

    frame_results = np.load(input_path, allow_pickle=True)['results'][()]
    sequence_results = np.load(input_path, allow_pickle=True)['sequence_results'][()] 

    poses, trans = [], []

    if len(sequence_results)>0:
        subject_ids = list(sequence_results.keys())
        if subject_id == -1 or subject_id not in subject_ids:
            print('Get motion sequence with subject IDs:', subject_ids)
            subject_id = int(input('Please select one subject ID (int):'))
        poses = np.array(sequence_results[subject_id]['smpl_thetas'])
        trans = np.array(sequence_results[subject_id]['cam_trans'])
    else:
        print('Missing tracking IDs in results. Using the first pose results for animation.')
        print('To get the tracking IDs, please use temporal optimization during inference.')
        frame_names = sorted(list(frame_results.keys()))
        poses, trans = np.zeros((len(frame_names), 72)), np.zeros((len(frame_names), 3))
        for inds, frame_name in enumerate(frame_names):
            poses[inds] = frame_results[frame_name]['smpl_thetas'][0]
            trans[inds] = frame_results[frame_name]['cam_trans'][0]

    if gender == 'female':
        model_path = female_model_path
        for k,v in bone_name_from_index.items():
            bone_name_from_index[k] = 'f_avg_' + v
    elif gender == 'male':
        model_path = male_model_path
        for k,v in bone_name_from_index.items():
            bone_name_from_index[k] = 'm_avg_' + v
    elif gender == 'character':
        model_path = character_model_path
        for k, v in bone_name_from_index_character.items():
            bone_name_from_index[k] = 'mixamorig1:' + v
    else:
        print('ERROR: Unsupported gender: ' + gender)
        sys.exit(1)

    # Limit target fps to source fps
    if fps_target > fps_source:
        fps_target = fps_source

    print('Gender:',gender)
    print('Number of source poses: ',poses.shape[0])
    print('Source frames-per-second: ', fps_source)
    print('Target frames-per-second: ', fps_target)
    print('--------------------------------------------------')

    setup_scene(model_path, fps_target)

    scene = bpy.data.scenes['Scene']
    sample_rate = int(fps_source/fps_target)
    scene.frame_end = (int)(poses.shape[0]/sample_rate)

    # Retrieve pelvis world position.
    # Unit is [cm] due to Armature scaling.
    # Need to make copy since reference will change when bone location is modified.
    armaturee = bpy.data.armatures[0]
    ob = bpy.data.objects['Armature']
    armature = ob.data

    bpy.ops.object.mode_set(mode='EDIT')
    # get specific bone name 'Bone'
    pelvis_bone = armature.edit_bones[bone_name_from_index[0]]
    # pelvis_bone = armature.edit_bones['f_avg_Pelvis']
    pelvis_position = Vector(pelvis_bone.head)
    bpy.ops.object.mode_set(mode='OBJECT')

    source_index = 0
    frame = 1

    offset = np.array([0.0, 0.0, 0.0])

    while source_index < poses.shape[0]:
        print('Adding pose: ' + str(source_index))

        # Go to new frame
        scene.frame_set(frame)

        process_pose(frame, poses[source_index], (trans[source_index] - offset), pelvis_position)
        source_index += sample_rate
        frame += 1

    return frame

def rotate_armature(use):
    if use == True:
        # Switch to Pose Mode
        bpy.ops.object.posemode_toggle()
        
        # Find the Armature & Bones
        ob = bpy.data.objects['Armature']
        armature = ob.data
        bones = armature.bones
        rootbone = bones[0]
        
        # Find the Root bone
        for bone in bones:
            if "f_avg_root" in bone.name:
                rootbone = bone
                
        rootbone.select = True
        
        # Rotate the Root bone by 90 euler degrees on the Y axis. Set --rotate_Y=False if the rotation is not needed.
        bpy.ops.transform.rotate(value=1.5708, orient_axis='Y', orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(False, True, False), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False, release_confirm=True)
        # Revert back to Object Mode
        bpy.ops.object.posemode_toggle()
        


def export_animated_mesh(output_path, rotate_y):
    # Create output directory if needed
    output_dir = os.path.dirname(output_path)
    if not os.path.isdir(output_dir):
        os.makedirs(output_dir, exist_ok=True)

    # Fix Rotation
    rotate_armature(rotate_y)

    # Select only skinned mesh and rig
    bpy.ops.object.select_all(action='DESELECT')
    bpy.data.objects['Armature'].select_set(True)
    bpy.data.objects['Armature'].children[0].select_set(True)

    if output_path.endswith('.glb'):
        print('Exporting to glTF binary (.glb)')
        # Currently exporting without shape/pose shapes for smaller file sizes
        bpy.ops.export_scene.gltf(filepath=output_path, export_format='GLB', export_selected=True, export_morph=False)
    elif output_path.endswith('.fbx'):
        print('Exporting to FBX binary (.fbx)')
        bpy.ops.export_scene.fbx(filepath=output_path, use_selection=True, add_leaf_bones=False)
    elif output_path.endswith('.bvh'):
        bpy.ops.export_anim.bvh(filepath=output_path, root_transform_only=False)
    else:
        print('ERROR: Unsupported export format: ' + output_path)
        sys.exit(1)

    return

 
input_path = 'E:/Projekte/romp-test3/video_results.npz'
output_path = 'E:/Projekte/romp-test3/result.fbx'
subject_id = 0 # -1
rotate_y = True
        
print('Input path: ' + input_path)
print('Output path: ' + output_path)

startTime = time.perf_counter()

# Process pose file
poses_processed = process_poses(
    input_path=input_path,
    gender=gender,
    fps_source=fps_source,
    fps_target=fps_target,
    subject_id=subject_id
    )
export_animated_mesh(output_path, rotate_y)

print('--------------------------------------------------')
print('Animation export finished, save to ', output_path)
print('Poses processed: ', poses_processed)
print('Processing time : ', time.perf_counter() - startTime)
print('--------------------------------------------------')

```

Btw, now i can proof, that this line is indeed a bug:
https://github.com/Arthur151/ROMP/blob/1458ba0b05a63d64b7fff7898cd917d9fda378b5/simple_romp/tools/convert2fbx.py#L308

@vivi90
Copy link
Contributor Author

vivi90 commented Jun 10, 2022

@Arthur151 Okay i have run some tests with the latest full ROMP version (not simple-romp) and the Blender Addon.

In ROMP the romp.predict.webcam is sending it to blender 'as it is':

sender.send([0,poses[0].tolist(),trans[0].tolist(),frame_id])

data_send = json.dumps(data_list).encode('utf-8')

In Blender the plugin does all necessary calculations for the rig:
https://github.com/yanch2116/CharacterDriven-BlenderAddon/blob/6c288acf8540f900202de10adc2463ab2ca75f04/src/characterDriven.py#L51

It's no problem to use an VRM rig instead the mixamo rig in Blender for example.
For VMC i could implement it the easy way inside this blender script.

But how about moving the most of these calculations from Blender back to ROMP?
There is another point: an VMC to Blender plugin still exists.
So how about also replacing the visulize_platform of blender by vmc completely.
Implementing VMC directly into ROMP makes it more flexible instead of using an proprietary blender connection.

@Arthur151
Copy link
Owner

Happy to see thing going so well with your efforts! Excellent work!
I just finished another deadline.
I don't know VMC well. Is there any support tools to implement VMC directly into ROMP?

@vivi90
Copy link
Contributor Author

vivi90 commented Jun 10, 2022

I just finished another deadline.

great! 🙂

[..] Is there any support tools to implement VMC directly into ROMP?

Currently there is no VMC protocol package for python.
But since it's based on OSC, thats my drafts:

osc.py:

#!/usr/bin/env python3

from socket import gaierror
from osc4py3.as_eventloop import *
from osc4py3 import oscmethod as oscm
from osc4py3 import oscbuildparse as oscbp
import time

class Client:
    """Based on: https://github.com/barlaensdoonn/osc_basics/blob/master/osc_basics.py"""
    def __init__(self, host: str, port: int, name: str) -> None:
        self.host = host
        self.port = port
        self.name = name
        self.start()

    def __enter__(self) -> 'Client':
        return self

    def start(self):
        osc_startup()
        try:
            osc_udp_client(self.host, self.port, self.name)
            print("OSC client ready.")
        except gaierror:
            print(
                "Host '{}' unavailable, failed to create client '{}'.".format(
                    self.host, 
                    self.name
                )
            )
            raise

    def send(self, address: str, data_types: str, data: str) -> None:
        message = oscbp.OSCMessage(
            address,
            data_types,
            data
        )
        osc_send(
            message,
            self.name
        )
        osc_process()
        print(str(message))

    def send_bundle(self, address: str, data_types: str, data: str) -> None:
        message_bundle = []
        for data_tuple in data:
            message_bundle.append(
                oscbp.OSCMessage(
                    address,
                    data_types,
                    [
                        str(data_tuple[0]),
                        data_tuple[1].x,
                        data_tuple[1].y,
                        data_tuple[1].z,
                        data_tuple[2].x,
                        data_tuple[2].y,
                        data_tuple[2].z,
                        data_tuple[2].w
                    ]
                )
            )
        message_bundle = oscbp.OSCBundle(
            oscbp.unixtime2timetag(time.time()),
            message_bundle
        )
        osc_send(
            message_bundle,
            self.name
        )
        osc_process()
        print(str(message_bundle))

    def __exit__(self, type, value, traceback) -> None:
        self.__del__()

    def __del__(self) -> None:
        osc_terminate()
        print("OSC client down.")

vmc.py:

#!/usr/bin/env python3

from osc import Client as OSCClient
import time
from math import radians, degrees, cos, sin, atan2, asin, pow, floor

class Bone:
    valid_names = (
        "Hips",
        "LeftUpperLeg",
        "RightUpperLeg",
        "LeftLowerLeg",
        "RightLowerLeg",
        "LeftFoot",
        "RightFoot",
        "Spine",
        "Chest",
        "Neck",
        "Head",
        "LeftShoulder",
        "RightShoulder",
        "LeftUpperArm",
        "RightUpperArm",
        "LeftLowerArm",
        "RightLowerArm",
        "LeftHand",
        "RightHand",
        "LeftToes",
        "RightToes",
        "LeftEye",
        "RightEye",
        "Jaw",
        "LeftThumbProximal",
        "LeftThumbIntermediate",
        "LeftThumbDistal",
        "LeftIndexProximal",
        "LeftIndexIntermediate",
        "LeftIndexDistal",
        "LeftMiddleProximal",
        "LeftMiddleIntermediate",
        "LeftMiddleDistal",
        "LeftRingProximal",
        "LeftRingIntermediate",
        "LeftRingDistal",
        "LeftLittleProximal",
        "LeftLittleIntermediate",
        "LeftLittleDistal",
        "RightThumbProximal",
        "RightThumbIntermediate",
        "RightThumbDistal",
        "RightIndexProximal",
        "RightIndexIntermediate",
        "RightIndexDistal",
        "RightMiddleProximal",
        "RightMiddleIntermediate",
        "RightMiddleDistal",
        "RightRingProximal",
        "RightRingIntermediate",
        "RightRingDistal",
        "RightLittleProximal",
        "RightLittleIntermediate",
        "RightLittleDistal",
        "UpperChest"
    )

    def __init__(self, name: str) -> None:
        if name in self.valid_names:
            self.name = name
        else:
            raise ValueError("Invalid bone name.")
  
    def __str__(self) -> str:
        return self.name

class Position:
    def __init__(self, x: float, y: float, z: float) -> None:
        self.x = x
        self.y = y
        self.z = z
    
    def __str__(self) -> str:
        return ", ".join(
            (
                str(self.x), 
                str(self.y), 
                str(self.z)
            )
        )

class Quaternion:
    def __init__(self, x: float, y: float, z: float, w: float) -> None:
        if round(pow(x, 2) + pow(y, 2) + pow(z, 2) + pow(w, 2), 1) == 1:
            self.x = x
            self.y = y
            self.z = z
            self.w = w
        else:
            raise ValueError(
                "Invalid quaternion values: x={}, y={}, z={}, w={}".format(
                    x,
                    y,
                    z,
                    w
                )
            )
    
    @classmethod
    def from_euler(cls, 
                   phi: float, theta: float, psi: float,
                   precision: int) -> 'Quaternion':
        """Creates an quaternion by euler angles and returns it

        Args:
            phi (float): Rotation angle around the X axis
            theta (float): Rotation angle around the Y axis
            psi (float): Rotation angle around the Y axis
            precision (int): Round the results to 'precision' digits after the decimal point

        Returns:
            Quaternion: Created quaternion (x, y, z, w)

        >>> str(Quaternion.from_euler(-90, -180, 90, 12))
        '(0.5, -0.5, -0.5, 0.5)'

        .. _Source:
            https://www.meccanismocomplesso.org/en/hamiltons-quaternions-and-3d-rotation-with-python

        """
        # x axis rotation angle
        cos_phi_half = cos(radians(phi) / 2)
        sin_phi_half = sin(radians(phi) / 2)
        # y axis rotation angle
        cos_theta_half = cos(radians(theta) / 2)
        sin_theta_half = sin(radians(theta) / 2)
        # z axis rotation angle
        cos_psi_half = cos(radians(psi) / 2)
        sin_psi_half = sin(radians(psi) / 2)
        # Calculation
        return cls(
            x = float(round(sin_phi_half * cos_theta_half * cos_psi_half - cos_phi_half * sin_theta_half * sin_psi_half, precision)),
            y = float(round(cos_phi_half * sin_theta_half * cos_psi_half + sin_phi_half * cos_theta_half * sin_psi_half, precision)),
            z = float(round(cos_phi_half * cos_theta_half * sin_psi_half - sin_phi_half * sin_theta_half * cos_psi_half, precision)),
            w = float(round(cos_phi_half * cos_theta_half * cos_psi_half + sin_phi_half * sin_theta_half * sin_psi_half, precision))
        )

    def to_euler(self) -> tuple[float, float, float]:
        """Exports an quaternion as an euler angle tuple

        Returns:
            tuple[float, float, float]: (x, y, z)

        >>> str(Quaternion.from_euler(-90, -180, 90, 12).to_euler())
        (90.0, 0.0, -90.0)

        .. _Source:
            https://www.meccanismocomplesso.org/en/hamiltons-quaternions-and-3d-rotation-with-python

        """
        # x axis rotation angle
        t0 = 2 * (self.w * self.x + self.y * self.z)
        t1 = 1 - 2 * (self.x * self.x + self.y * self.y)
        x = atan2(t0, t1)
        # y axis rotation angle
        t2 = 2 * (self.w * self.y - self.z * self.x)
        t2 = 1 if t2 > 1 else t2
        t2 = -1 if t2 < -1 else t2
        y = asin(t2)
        # y axis rotation angle
        t3 = 2 * (self.w * self.z + self.x * self.y)
        t4 = 1 - 2 * (self.y * self.y + self.z * self.z)
        z = atan2(t3, t4)
        return degrees(x), degrees(y), degrees(z)

    def __str__(self) -> str:
        return "(" + ", ".join(
            (
                str(self.x), 
                str(self.y), 
                str(self.z), 
                str(self.w)
            )
        ) + ")"

class Assistant(OSCClient):
    def __init__(self, host: str, port: int, name: str) -> None:
        self.started_at = time.time()
        super().__init__(host, port, name)

    def send_root_transform(self, position: Position, 
                            quaternion: Quaternion) -> None:
        # since VMC v2.0.0
        self.send(
            "/VMC/Ext/Root/Pos",
            None,
            [
                "root",
                position.x,
                position.y,
                position.z, 
                quaternion.x,
                quaternion.y,
                quaternion.z,
                quaternion.w
            ]
        )

    def send_bones_transform(self,
                             transform: list[list[Bone, Position, Quaternion]]
                            )-> None:
        self.send_bundle(
            "/VMC/Ext/Bone/Pos",
            None,
            transform
        )

    def send_available_states(self, loaded: int, 
                              calibration_state: int = None,
                              calibration_mode: int = None,
                              tracking_status: int = None) -> None:
        if tracking_status == None:
            if calibration_state == None or calibration_mode == None:
                self.send(
                    "/VMC/Ext/OK",
                    None,
                    [
                        loaded
                    ]
                )
            else:
                # since VMC v2.5
                self.send(
                    "/VMC/Ext/OK",
                    None,
                    [
                        loaded,
                        calibration_state,
                        calibration_mode
                    ]
                )
        else:
            # since VMC v2.7
            self.send(
                "/VMC/Ext/OK",
                None,
                [
                    loaded,
                    calibration_state,
                    calibration_mode,
                    tracking_status
                ]
            )

    def send_relative_time(self) -> None:
        self.send(
            "/VMC/Ext/T",
            None,
            [
                float(time.time() - self.started_at)
            ]
        )

vrm.py:

#!/usr/bin/env python3

from gltflib import GLTF

class VRM(GLTF):
    def __init__(self, filename: str) -> None:
        gltf = super().load_glb(filename)
        super().__init__(gltf.model, gltf.resources)

    def export_to_glb(self, filename: str) -> None:
        super().export(filename)
    
    def export_to_gltf(self, filename: str) -> None:
        super().convert_to_file_resource(
            super().get_glb_resource(), 
            filename + ".resources.bin"
        )
        super().export(filename)

    def get_bones(self) -> list:
        return self.model.nodes

test.py:

#!/usr/bin/env python3

from log import Log
import sys
from vrm import VRM as Model
from vmc import Assistant as VMCAssistant, Quaternion
from gui import Window
import numpy as np
import romp, cv2

# Configuration
connection = {
    "host" : "localhost",
    "port" : 39539,
    "name" : "example"
}

# Logging
sys.stdout = Log(filename = "vmc.log", is_error = False)
sys.stderr = Log(filename = "vmc.log", is_error = True)

# ROMP
settings = romp.main.default_settings
# settings.mode='video'
#print(settings)
romp_model = romp.ROMP(settings)
outputs = romp_model(cv2.imread('test.jpg'))
print(outputs["smpl_thetas"][0].reshape(24, 3))

"""
# NPZ data
input_path = "E:/Projekte/romp-test/00000215.npz"
input_path = "video_results.npz"
frame_results = np.load(input_path, allow_pickle=True)['results'][()]
sequence_results = np.load(input_path, allow_pickle=True)['sequence_results'][()]
print(frame_results)
print(sequence_results)

# Avatar model
model = Model("test.vrm")
print(model.get_bones()[101]) # leftUpperLeg (see: https://github.com/vrm-c/vrm-specification/issues/371)
model_root = []
model_t_pose = []

# Quaternions example
q = Quaternion.from_euler(-90, -180, 90, precision = 12)
print(q)
e = q.to_euler()
print(e)


# GUI
Window(
    VMCAssistant(
        connection['host'],
        connection['port'],
        connection['name']
    ),
    model_root,
    model_t_pose
).MainLoop()
"""

gui.py:

#!/usr/bin/env python3

import wx
from vmc import Assistant as VMCAssistant, Bone, Position, Quaternion

class Window(wx.App):
    def __init__(self, vmc: VMCAssistant, 
                 model_root: list[Position, Quaternion],
                 model_t_pose: list[Bone, Position, Quaternion]) -> None:
        self.vmc = vmc
        self.model_root = model_root
        self.model_t_pose = model_t_pose
        super().__init__()

    def OnInit(self) -> bool:
        self.frame = self.Frame(
            vmc = self.vmc,
            parent = None, 
            title = 'VMC Assistant Demo', 
            size = wx.Size(400, 300)
        )
        self.frame.Show(True)
        return True
  
    class Frame(wx.Frame):
        def __init__(self, vmc: VMCAssistant, 
                     parent: wx.App, title: str, size: wx.Size) -> None:
            super().__init__(
                parent, 
                title = title, 
                size = size
            )
            self.panel = self.Panel(self, vmc)

        class Panel(wx.Panel):
                     
            def __init__(self, parent: wx.Frame, vmc: VMCAssistant) -> None:
                self.vmc = vmc
                super().__init__(parent)
                # Controls
                self.test_caption = wx.StaticText(
                    self, label = 'Test', 
                    style = wx.ALIGN_LEFT
                )
                self.test_slider = wx.Slider(
                    self, 
                    value = 0, 
                    minValue = -180, 
                    maxValue = 180, 
                    style = wx.SL_HORIZONTAL | wx.SL_LABELS
                )
                self.Bind(wx.EVT_SLIDER, self.change_test)
                # Log
                self.log = wx.TextCtrl(
                    self, 
                    style = wx.TE_MULTILINE | wx.TE_READONLY
                )
                # Layout
                self.layout = wx.BoxSizer(wx.VERTICAL)
                self.layout.AddMany([
                    (self.test_caption, 0, wx.EXPAND | wx.ALIGN_LEFT | wx.ALL, 5),
                    (self.test_slider, 0, wx.EXPAND | wx.ALL, 5),
                    (self.log, 1, wx.EXPAND | wx.ALIGN_LEFT | wx.ALL, 5)
                ])
                self.SetSizer(self.layout)
                self.Center()

            def change_test(self, event: wx.Event) -> None:
                value = float(self.test_slider.GetValue())
                quat = Quaternion.from_euler(0.0, 0.0, value, 12)
                self.vmc.send_bones_transform(
                    [
                        [
                            Bone("LeftUpperLeg"),
                            Position(0.0, 0.0, 0.0),
                            quat
                        ]
                    ]
                )
                self.vmc.send_available_states(1)
                self.vmc.send_relative_time()
                self.log.AppendText(
                    str(quat) + "\r\n"
                )

@Arthur151
Copy link
Owner

Great job. It seems interesting~
We need to install some additional packages, instead of blender to drive other virtual avatars. Looks great.
I'd like to join your project.

@Arthur151
Copy link
Owner

Please let me know if you want me to add / check BEV/ROMP features to it.
I think that people may love it for its convenience.

@vivi90
Copy link
Contributor Author

vivi90 commented Jun 10, 2022

[..] I'd like to join your project.

Already invited you: https://github.com/vivi90/python-vmc/invitations

Please let me know if you want me to add / check BEV/ROMP features to it. [..]

What do you mean by features?

Currently i can only find the output for the 24 SMPL joints.
In the README of simple-romp are also another joints for face and hands mentioned:

...
SMPL_EXTRA_30 = {
'Nose':24, 'R_Eye':25, 'L_Eye':26, 'R_Ear': 27, 'L_Ear':28,
'L_BigToe':29, 'L_SmallToe': 30, 'L_Heel':31, 'R_BigToe':32,'R_SmallToe':33, 'R_Heel':34,
'L_Hand_thumb':35, 'L_Hand_index': 36, 'L_Hand_middle':37, 'L_Hand_ring':38, 'L_Hand_pinky':39,
'R_Hand_thumb':40, 'R_Hand_index':41,'R_Hand_middle':42, 'R_Hand_ring':43, 'R_Hand_pinky': 44,
'R_Hip': 45, 'L_Hip':46, 'Neck_LSP':47, 'Head_top':48, 'Pelvis':49, 'Thorax_MPII':50,
'Spine_H36M':51, 'Jaw_H36M':52, 'Head':53}

Would be great to have also these joints available.

@Arthur151
Copy link
Owner

Hi, all 54(24+30) joints are saved as
'j3d_all54' in output .npz file of originial ROMP, see this
'joints' in output .npz file of simple-romp.

@Altai01
Copy link

Altai01 commented Jul 11, 2022

@Altai01

[..] difference between mediapipe's location[x,y,z] and ROMP's location[x,y,z]?
[..] turn to quaternion by the same way?

Sorry, not familiar with MediaPipe. But also interested in it. So your welcome to share your knowledge 🙂

I am not familiar with MediaPipe,just want to have a try.like this
`
from mathutils import Matrix, Vector, Quaternion
from math import radians
import numpy as np
import bpy
import cv2
import mediapipe as mp

bl_info = {
"name": "Character Driven: Live or Offline",
"author": "yanch2116",
"blender": (2, 80, 0),
"version": (1, 0, 0),
}

class CharacterDriven(bpy.types.Operator):
bl_idname = 'yanch.characterdriven'
bl_label = 'characterdriven'

def execute(self, ctx):
    self.mp_drawing = mp.solutions.drawing_utils
    self.mp_hands = mp.solutions.hands
    self.factor = 10
    self.fram=0
    fram = 0
    self.cap = cv2.VideoCapture(0)
    self.thumbl_Importer_ = SMPL_Importer(ctx)

    ctx.window_manager.modal_handler_add(self)
    mocap_timer = ctx.window_manager.event_timer_add(
        1 / 120, window=ctx.window)

    return {'RUNNING_MODAL'}

# @time_fun
def modal(self, ctx, evt):

    if evt.type == 'TIMER':
        with self.mp_hands.Hands(
                static_image_mode=True,
                max_num_hands=1,
                min_detection_confidence=0.6) as hands:
            flag, image = self.cap.read()
            results = hands.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
            if results.multi_handedness==None:
                return {'RUNNING_MODAL'}
            for classfid in results.multi_handedness:
                for j,name in enumerate(classfid.classification):
                    if name.label!="Right":
                        return {'RUNNING_MODAL'}
            if results.multi_hand_world_landmarks:
                for hand_landmarks in results.multi_hand_world_landmarks:
                    for i, lm in enumerate(hand_landmarks.landmark):
                        matrix = [lm.x*10, lm.y*10, lm.z*10]
                        print(i)
                        if i == 1:
                            continue
                        self.thumbl_Importer_.process_poses(self.fram, i, matrix)
            self.fram=self.fram+1
        if cv2.waitKey(5):
            cv2.imshow("nihao", image)
        print("begain")

    if evt.type == 'A':
        self.cap.release()
        return {'FINISHED'}

    return {'RUNNING_MODAL'}

class SMPL_Importer:

def __init__(self, context):
    self.bone_name_from_index = {
        0: 'hand.L',
        1: 'L_Hip',
        2: 'thumb.01.L',
        3: 'thumb.02.L',
        4: 'thumb.03.L',
        5: 'palm.01.L',
        6: 'f_index.01.L',
        7: 'f_index.02.L',
        8: 'f_index.03.L',
        9: 'palm.02.L',
        10: 'f_middle.01.L',
        11: 'f_middle.02.L',
        12: 'f_middle.03.L',
        13: 'palm.03.L',
        14: 'f_ring.01.L',
        15: 'f_ring.02.L',
        16: 'f_ring.03.L',
        17: 'palm.04.L',
        18: 'f_pinky.01.L',
        19: 'f_pinky.02.L',
        20: 'f_pinky.03.L',
    }

def Rodrigues(self, rotvec):
    theta = np.linalg.norm(rotvec)
    r = (rotvec / theta).reshape(3, 1) if theta > 0. else rotvec
    cost = np.cos(theta)
    mat = np.asarray([[0, -r[2], r[1]],
                      [r[2], 0, -r[0]],
                      [-r[1], r[0], 0]])
    return (cost * np.eye(3) + (1 - cost) * r.dot(r.T) + np.sin(theta) * mat)

def process_poses(self, framid, indx, matr):
    matrix = self.Rodrigues(matr)
    armature = bpy.data.objects['metarig']
    bones = armature.pose.bones
    bone = bones[self.bone_name_from_index[indx]]
    bone_rotation = Matrix(matrix).to_quaternion()
    #print("bone_rotation", bone_rotation)
    #print("matr",matr)
    #bone.location=Vector(matr)
    bone.rotation_quaternion = bone_rotation
    bpy.context.scene.frame_end = framid
    return

addon_keymaps = []

def register():
bpy.utils.register_class(CharacterDriven)
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
if kc:
km = wm.keyconfigs.addon.keymaps.new(
name='3D View', space_type='VIEW_3D')
kmi = km.keymap_items.new(
CharacterDriven.bl_idname, type='E', value='PRESS', ctrl=True)
addon_keymaps.append((km, kmi))

def unregister():
bpy.utils.unregister_class(CharacterDriven)
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()

if name == "main":
register()`
but this does not work well,do you want to have a try?

@vivi90
Copy link
Contributor Author

vivi90 commented Jul 11, 2022

@Altai01

[..] this does not work well [..]

What exactly do you mean by 'not well'?

@Altai01
Copy link

Altai01 commented Jul 11, 2022

@Altai01

[..] this does not work well [..]

What exactly do you mean by 'not well'?
it can not drive model correct

@vivi90
Copy link
Contributor Author

vivi90 commented Jul 11, 2022

@Altai01

it can not drive model correct

okay may you please provide:

  • a screenshot (containg, 'what you want' and 'what you get')
  • an repository with your complete code (makes testing way more easier)

Btw, how about moving this topic to another issue in your own repo.
Seems not related to ROMP.
You may leave an link here.

@Altai01
Copy link

Altai01 commented Jul 12, 2022

@Altai01

it can not drive model correct

okay may you please provide:

  • a screenshot (containg, 'what you want' and 'what you get')
  • an repository with your complete code (makes testing way more easier)

Btw, how about moving this topic to another issue in your own repo. Seems not related to ROMP. You may leave an link here.

here:https://github.com/Altai01/mediapipe_hand_test

@vivi90
Copy link
Contributor Author

vivi90 commented Jul 18, 2022

@Arthur151 Sorry, for asking again in this already long issue.. 🙂
Do you have an idea, how i might get an simple body scaling factor?
Maybe from smpl_betas?

@Arthur151
Copy link
Owner

@vivi90 .
Hi, Vivian, the first dimension of the smpl_betas describe the body scale to some extend.

@vivi90
Copy link
Contributor Author

vivi90 commented Aug 12, 2022

@Arthur151 My VMC protocol python library is now published at PyPi and should work as expected 🙂

My new script for ROMP follows later (using it for my bachelor thesis and so i need to keep it unpublished until it's finished).
But with the provided examples in combination with the old code it might be kinda obvious, how to implement it 😉

@Arthur151
Copy link
Owner

Arthur151 commented Aug 13, 2022 via email

@vivi90
Copy link
Contributor Author

vivi90 commented Aug 14, 2022

@Arthur151 Is cam_trans really the root position of the person or is it the position of pelvis?
If it is the position of pelvis, is there a way to get the root position?

//EDIT: Ah i see, cam_trans is the center of the person?

@Arthur151
Copy link
Owner

Arthur151 commented Aug 15, 2022 via email

@vivi90
Copy link
Contributor Author

vivi90 commented Aug 15, 2022

@Arthur151

Technically, it is the position of the pelvis [..]

I see, thank you 🙂
Is there a way to get the difference from the pelvis height to the persons ground position?

@Arthur151
Copy link
Owner

I think you can use the mean of the two ankles.

@vivi90
Copy link
Contributor Author

vivi90 commented Aug 15, 2022

I think you can use the mean of the two ankles.

Great idea 👍

@vivi90
Copy link
Contributor Author

vivi90 commented Aug 15, 2022

I think you can use the mean of the two ankles.

Sorry, does not work in certain situations.
For example if one leg is in the air.
Using the most buttom point might work.
But using the scaling with an offset for now.

@vivi90
Copy link
Contributor Author

vivi90 commented Aug 17, 2022

@Arthur151 Sorry, for making this issue longer and longer 😄

But i have still an related issue..

Using simple-romp at the command line

romp --mode=webcam --show --temporal_optimize -sc 1

Works as expected. 🙂
I mean only minimal 'shaking' (see: #285 and #321)

Using from romp import ROMP in my Python script

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""VMCP support for ROMP."""

...
from romp import ROMP
from romp.main import default_settings as romp_default_settings
...
config = romp_default_settings
config.GPU = self._configuration["gpu"]
config.onnx = self._configuration["onnx"]
config.temporal_optimize = self._configuration["temporal-optimize"]
config.smooth_coeff = self._configuration["smooth-coeff"]
print(config)
self._romp = ROMP(config)
...
result = self._romp(data)
if result is not None:  # At least 1 person detected.
    root_position = CoordinateVector(
        -result["cam_trans"][self._configuration["subject"]][0],
        -result["cam_trans"][self._configuration["subject"]][1],
        -result["cam_trans"][self._configuration["subject"]][2]
    )
    root_rotation = Quaternion.from_euler(radians(-180), 0, 0)
...
mode='image',
input=None,
save_path='C:\\Users\\Vivien\\ROMP_results',
GPU=0,
onnx=False,
temporal_optimize=True,
center_thresh=0.25,
show_largest=False,
smooth_coeff=1,
calc_smpl=True,
render_mesh=False,
renderer='sim3dr',
show=False,
show_items='mesh',
save_video=False,
frame_rate=24,
smpl_path='C:\\Users\\Vivien\\.romp\\smpl_packed_info.pth',
model_path='C:\\Users\\Vivien\\.romp\\ROMP.pkl',
model_onnx_path='C:\\Users\\Vivien\\.romp\\ROMP.onnx',
root_align=False

I have really huge shaking. 🙁
Not sure, what's my mistake there.
Do you have any ideas?

System

  • OS: Windows 10 Enterprise
  • Python: 3.10.4
  • simple-romp: 1.0.5

@Arthur151
Copy link
Owner

Arthur151 commented Aug 19, 2022

Hi, Vivian, @vivi90
To active the temporal optimization, we have to set the mode to 'video', instead of current 'image'.

@vivi90
Copy link
Contributor Author

vivi90 commented Aug 19, 2022

@Arthur151

[..] To active the temporal optimization, we have to set the mode to 'video', instead of current 'image'.

Changed, but no difference. Still shaking a lot.

@vivi90
Copy link
Contributor Author

vivi90 commented Aug 21, 2022

ONNX seems at least working, because if i comment the line config.onnx = self._configuration["onnx"] out in my code and comparing it's results, then it shakes even more.
There is definitely an bug somewhere in my script: #193 (comment)
I will look deeper in the rendering part of ROMP to find it hopefully.

@vivi90
Copy link
Contributor Author

vivi90 commented Aug 21, 2022

@Arthur151 I have taken a look at:

def rendering_romp_bev_results(renderer, outputs, image, rendering_cfgs, alpha=1):

But i am not sure, if i properly understand, how --show renders the images.
I mean it seems you are rendering multiple perspectives and then merging it together?

In my script i am just using the output of ROMP as it is in an 3D environment.
Maybe that's my mistake?

@lucasjinreal
Copy link

@vivi90 Hi, am also interesting at this. Did u managed converting SMPL output to VMC format?

@vivi90
Copy link
Contributor Author

vivi90 commented Nov 2, 2022

@jinfagang

@vivi90 [..] Did u managed converting SMPL output to VMC format?

Yes, i have 🙂
Will publish my code draft on 7 th of November 2022 at this link:
https://codeberg.org/vivi90/vmcps

@lucasjinreal
Copy link

@vivi90 thank u. I have followed u, also, my ask a very Tortured soul question?

if we can convert smpl to vmc protocal, how can we using it to driven metahuman? Through vmc4ue? How to streaming data

@vivi90
Copy link
Contributor Author

vivi90 commented Nov 2, 2022

@jinfagang

[..] how can we using it to driven metahuman? Through vmc4ue? How to streaming data

Seems like you have an very similiar question to this one:

@lucasjinreal
Copy link

@vivi90 Hi, i found a workable solution is sending VMC, then using MopUE4 to driven.

But now the question is, I can not get your code right to driven in standard VMC protocol:

image

as you can see, normally, the data from ROMP convert to VMC with yourcode should drive VMCtoMOP, but the model is collapsed.

@vivi90
Copy link
Contributor Author

vivi90 commented Nov 3, 2022

@jinfagang

as you can see, normally, the data from ROMP convert to VMC with yourcode should drive VMCtoMOP, but the model is collapsed.

See: https://codeberg.org/vivi90/python-vmcp/issues/3#issuecomment-664578

@lucasjinreal
Copy link

@vivi90 hello, could be provide a basic workable script to convert SMPL data to VMC format? you said will opensouce at 11.7, will it possiable to opensource earlier?

@vivi90
Copy link
Contributor Author

vivi90 commented Nov 5, 2022

@Arthur151 @jinfagang @Altai01 @gngdb My VMC protocol implementation draft for ROMP is now public available:
https://codeberg.org/vivi90/vmcps
Tested and hopefully it's useful for you 🙂

My next goals will be:

@Arthur151
Copy link
Owner

@vivi90 ,
Thanks for creating such a great project!
I make the recommandation at here. Welcome to rectify the description about your project.
I will try it later (after the cvpr deadline).
Best,
Yu

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants