From 85d773bd22157a5b626de13ab63b7628cdece16d Mon Sep 17 00:00:00 2001 From: endomorphosis Date: Tue, 16 Jan 2024 23:51:56 -0800 Subject: [PATCH] Update python_requires in setup.py --- anim_utils.egg-info/PKG-INFO | 65 + anim_utils.egg-info/SOURCES.txt | 56 + anim_utils.egg-info/dependency_links.txt | 1 + anim_utils.egg-info/requires.txt | 4 + anim_utils.egg-info/top_level.txt | 1 + build/lib/anim_utils/__init__.py | 0 .../lib/anim_utils/animation_data/__init__.py | 9 + .../lib/anim_utils/animation_data/acclaim.py | 291 ++++ build/lib/anim_utils/animation_data/bvh.py | 478 ++++++ .../anim_utils/animation_data/constants.py | 30 + .../anim_utils/animation_data/fbx/__init__.py | 58 + .../animation_data/fbx/fbx_export.py | 219 +++ .../animation_data/fbx/fbx_import.py | 198 +++ .../animation_data/joint_constraints.py | 487 ++++++ .../animation_data/motion_blending.py | 615 ++++++++ .../animation_data/motion_concatenation.py | 843 +++++++++++ .../animation_data/motion_distance.py | 99 ++ .../anim_utils/animation_data/motion_state.py | 352 +++++ .../animation_data/motion_vector.py | 383 +++++ .../animation_data/quaternion_frame.py | 181 +++ .../lib/anim_utils/animation_data/skeleton.py | 600 ++++++++ .../animation_data/skeleton_builder.py | 529 +++++++ .../animation_data/skeleton_models.py | 1314 +++++++++++++++++ .../animation_data/skeleton_node.py | 303 ++++ build/lib/anim_utils/animation_data/utils.py | 947 ++++++++++++ .../lib/anim_utils/motion_editing/__init__.py | 4 + .../analytical_inverse_kinematics.py | 286 ++++ .../coordinate_cyclic_descent.py | 353 +++++ .../motion_editing/cubic_motion_spline.py | 117 ++ .../anim_utils/motion_editing/fabrik_chain.py | 551 +++++++ .../anim_utils/motion_editing/fabrik_node.py | 316 ++++ .../footplant_constraint_generator.py | 692 +++++++++ .../anim_utils/motion_editing/hybrit_ik.py | 106 ++ .../motion_editing/ik_constraints.py | 167 +++ .../motion_editing/ik_constraints_builder.py | 233 +++ .../motion_editing/motion_editing.py | 900 +++++++++++ .../motion_editing/motion_filtering.py | 128 ++ .../motion_editing/motion_grounding.py | 483 ++++++ .../motion_editing/numerical_ik_exp.py | 96 ++ .../motion_editing/numerical_ik_quat.py | 131 ++ .../motion_editing/skeleton_pose_model.py | 291 ++++ build/lib/anim_utils/motion_editing/utils.py | 597 ++++++++ build/lib/anim_utils/retargeting/__init__.py | 5 + .../lib/anim_utils/retargeting/analytical.py | 408 +++++ build/lib/anim_utils/retargeting/constants.py | 67 + .../retargeting/constrained_retargeting.py | 124 ++ .../fast_point_cloud_retargeting.py | 331 +++++ .../retargeting/point_cloud_retargeting.py | 523 +++++++ build/lib/anim_utils/retargeting/utils.py | 189 +++ build/lib/anim_utils/utilities/__init__.py | 0 build/lib/anim_utils/utilities/custom_math.py | 651 ++++++++ .../utilities/io_helper_functions.py | 111 ++ build/lib/anim_utils/utilities/log.py | 70 + dist/anim_utils-0.1-py3.10.egg | Bin 0 -> 388594 bytes setup.py | 2 +- 55 files changed, 15994 insertions(+), 1 deletion(-) create mode 100644 anim_utils.egg-info/PKG-INFO create mode 100644 anim_utils.egg-info/SOURCES.txt create mode 100644 anim_utils.egg-info/dependency_links.txt create mode 100644 anim_utils.egg-info/requires.txt create mode 100644 anim_utils.egg-info/top_level.txt create mode 100644 build/lib/anim_utils/__init__.py create mode 100644 build/lib/anim_utils/animation_data/__init__.py create mode 100644 build/lib/anim_utils/animation_data/acclaim.py create mode 100644 build/lib/anim_utils/animation_data/bvh.py create mode 100644 build/lib/anim_utils/animation_data/constants.py create mode 100644 build/lib/anim_utils/animation_data/fbx/__init__.py create mode 100644 build/lib/anim_utils/animation_data/fbx/fbx_export.py create mode 100644 build/lib/anim_utils/animation_data/fbx/fbx_import.py create mode 100644 build/lib/anim_utils/animation_data/joint_constraints.py create mode 100644 build/lib/anim_utils/animation_data/motion_blending.py create mode 100644 build/lib/anim_utils/animation_data/motion_concatenation.py create mode 100644 build/lib/anim_utils/animation_data/motion_distance.py create mode 100644 build/lib/anim_utils/animation_data/motion_state.py create mode 100644 build/lib/anim_utils/animation_data/motion_vector.py create mode 100644 build/lib/anim_utils/animation_data/quaternion_frame.py create mode 100644 build/lib/anim_utils/animation_data/skeleton.py create mode 100644 build/lib/anim_utils/animation_data/skeleton_builder.py create mode 100644 build/lib/anim_utils/animation_data/skeleton_models.py create mode 100644 build/lib/anim_utils/animation_data/skeleton_node.py create mode 100644 build/lib/anim_utils/animation_data/utils.py create mode 100644 build/lib/anim_utils/motion_editing/__init__.py create mode 100644 build/lib/anim_utils/motion_editing/analytical_inverse_kinematics.py create mode 100644 build/lib/anim_utils/motion_editing/coordinate_cyclic_descent.py create mode 100644 build/lib/anim_utils/motion_editing/cubic_motion_spline.py create mode 100644 build/lib/anim_utils/motion_editing/fabrik_chain.py create mode 100644 build/lib/anim_utils/motion_editing/fabrik_node.py create mode 100644 build/lib/anim_utils/motion_editing/footplant_constraint_generator.py create mode 100644 build/lib/anim_utils/motion_editing/hybrit_ik.py create mode 100644 build/lib/anim_utils/motion_editing/ik_constraints.py create mode 100644 build/lib/anim_utils/motion_editing/ik_constraints_builder.py create mode 100644 build/lib/anim_utils/motion_editing/motion_editing.py create mode 100644 build/lib/anim_utils/motion_editing/motion_filtering.py create mode 100644 build/lib/anim_utils/motion_editing/motion_grounding.py create mode 100644 build/lib/anim_utils/motion_editing/numerical_ik_exp.py create mode 100644 build/lib/anim_utils/motion_editing/numerical_ik_quat.py create mode 100644 build/lib/anim_utils/motion_editing/skeleton_pose_model.py create mode 100644 build/lib/anim_utils/motion_editing/utils.py create mode 100644 build/lib/anim_utils/retargeting/__init__.py create mode 100644 build/lib/anim_utils/retargeting/analytical.py create mode 100644 build/lib/anim_utils/retargeting/constants.py create mode 100644 build/lib/anim_utils/retargeting/constrained_retargeting.py create mode 100644 build/lib/anim_utils/retargeting/fast_point_cloud_retargeting.py create mode 100644 build/lib/anim_utils/retargeting/point_cloud_retargeting.py create mode 100644 build/lib/anim_utils/retargeting/utils.py create mode 100644 build/lib/anim_utils/utilities/__init__.py create mode 100644 build/lib/anim_utils/utilities/custom_math.py create mode 100644 build/lib/anim_utils/utilities/io_helper_functions.py create mode 100644 build/lib/anim_utils/utilities/log.py create mode 100644 dist/anim_utils-0.1-py3.10.egg diff --git a/anim_utils.egg-info/PKG-INFO b/anim_utils.egg-info/PKG-INFO new file mode 100644 index 0000000..476c4e6 --- /dev/null +++ b/anim_utils.egg-info/PKG-INFO @@ -0,0 +1,65 @@ +Metadata-Version: 2.1 +Name: anim-utils +Version: 0.1 +Summary: Skeleton Animation Utilities. +Home-page: https://github.com/eherr/anim_utils +Author: DFKI GmbH +License: MIT +Keywords: skeleton animation data retargeting +Platform: UNKNOWN +Requires-Python: >=3.5, <4 +Description-Content-Type: text/markdown +License-File: LICENSE + +# Skeleton Animation Utilities + +Utility functions and data structures for skeleton animations loaded from BVH and ASF/AMC files. The library provides functions for inverse kinematics and retargeting. The main dependency, in addition to NumPy, SciPy and Matplotlib, is the transformations library by Christoph Gohlke https://www.lfd.uci.edu/~gohlke/. + +## Installation + +Clone the repository and install the package with editable flag or use the follwing command: +```bat +pip install git+https://github.com/eherr/anim_utils +``` +The optional FBX IO requires the [Python FBX SDK](https://www.autodesk.com/developer-network/platform-technologies/fbx-sdk-2020-3) + +## Example + +```python +from anim_utils.animation_data import BVHReader, MotionVector, SkeletonBuilder + +bvh = BVHReader("example.bvh") +mv = MotionVector() +mv.from_bvh_reader(bvh) +skeleton = SkeletonBuilder().load_from_bvh(bvh) +point_clouds = [] +for frame in mv.frames: + point_cloud = [] + for j in skeleton.animated_joints: + p = skeleton.nodes[j].get_global_position(frame) + point_cloud.append(p) + point_clouds.append(point_cloud) + +``` + +A retargeting script can be found in the example directory. + +## Developers + +Erik Herrmann1, Han Du1, Martin Manns2, Markus Mauer2 + +1DFKI GmbH +2Daimler AG + + +## License +Copyright (c) 2019 DFKI GmbH. +MIT License, see the LICENSE file. + +Contributions by Daimler AG in the following files are also licensed under terms of the MIT license: +anim_utils/animation_data/bvh.py +anim_utils/animation_data/utils.py + +Each file contains a copyright notice. + + diff --git a/anim_utils.egg-info/SOURCES.txt b/anim_utils.egg-info/SOURCES.txt new file mode 100644 index 0000000..ed7640d --- /dev/null +++ b/anim_utils.egg-info/SOURCES.txt @@ -0,0 +1,56 @@ +LICENSE +README.md +setup.py +anim_utils/__init__.py +anim_utils.egg-info/PKG-INFO +anim_utils.egg-info/SOURCES.txt +anim_utils.egg-info/dependency_links.txt +anim_utils.egg-info/requires.txt +anim_utils.egg-info/top_level.txt +anim_utils/animation_data/__init__.py +anim_utils/animation_data/acclaim.py +anim_utils/animation_data/bvh.py +anim_utils/animation_data/constants.py +anim_utils/animation_data/joint_constraints.py +anim_utils/animation_data/motion_blending.py +anim_utils/animation_data/motion_concatenation.py +anim_utils/animation_data/motion_distance.py +anim_utils/animation_data/motion_state.py +anim_utils/animation_data/motion_vector.py +anim_utils/animation_data/quaternion_frame.py +anim_utils/animation_data/skeleton.py +anim_utils/animation_data/skeleton_builder.py +anim_utils/animation_data/skeleton_models.py +anim_utils/animation_data/skeleton_node.py +anim_utils/animation_data/utils.py +anim_utils/animation_data/fbx/__init__.py +anim_utils/animation_data/fbx/fbx_export.py +anim_utils/animation_data/fbx/fbx_import.py +anim_utils/motion_editing/__init__.py +anim_utils/motion_editing/analytical_inverse_kinematics.py +anim_utils/motion_editing/coordinate_cyclic_descent.py +anim_utils/motion_editing/cubic_motion_spline.py +anim_utils/motion_editing/fabrik_chain.py +anim_utils/motion_editing/fabrik_node.py +anim_utils/motion_editing/footplant_constraint_generator.py +anim_utils/motion_editing/hybrit_ik.py +anim_utils/motion_editing/ik_constraints.py +anim_utils/motion_editing/ik_constraints_builder.py +anim_utils/motion_editing/motion_editing.py +anim_utils/motion_editing/motion_filtering.py +anim_utils/motion_editing/motion_grounding.py +anim_utils/motion_editing/numerical_ik_exp.py +anim_utils/motion_editing/numerical_ik_quat.py +anim_utils/motion_editing/skeleton_pose_model.py +anim_utils/motion_editing/utils.py +anim_utils/retargeting/__init__.py +anim_utils/retargeting/analytical.py +anim_utils/retargeting/constants.py +anim_utils/retargeting/constrained_retargeting.py +anim_utils/retargeting/fast_point_cloud_retargeting.py +anim_utils/retargeting/point_cloud_retargeting.py +anim_utils/retargeting/utils.py +anim_utils/utilities/__init__.py +anim_utils/utilities/custom_math.py +anim_utils/utilities/io_helper_functions.py +anim_utils/utilities/log.py \ No newline at end of file diff --git a/anim_utils.egg-info/dependency_links.txt b/anim_utils.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/anim_utils.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/anim_utils.egg-info/requires.txt b/anim_utils.egg-info/requires.txt new file mode 100644 index 0000000..3f85c64 --- /dev/null +++ b/anim_utils.egg-info/requires.txt @@ -0,0 +1,4 @@ +matplotlib +numpy +scipy +transformations diff --git a/anim_utils.egg-info/top_level.txt b/anim_utils.egg-info/top_level.txt new file mode 100644 index 0000000..8dc8fff --- /dev/null +++ b/anim_utils.egg-info/top_level.txt @@ -0,0 +1 @@ +anim_utils diff --git a/build/lib/anim_utils/__init__.py b/build/lib/anim_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/anim_utils/animation_data/__init__.py b/build/lib/anim_utils/animation_data/__init__.py new file mode 100644 index 0000000..e05760a --- /dev/null +++ b/build/lib/anim_utils/animation_data/__init__.py @@ -0,0 +1,9 @@ +from .motion_vector import MotionVector +from .motion_concatenation import align_quaternion_frames, transform_euler_frames, transform_quaternion_frames +from .bvh import BVHReader, BVHWriter +from .acclaim import parse_asf_file, parse_amc_file +from .skeleton import Skeleton +from .skeleton_builder import SkeletonBuilder +from .skeleton_node import SKELETON_NODE_TYPE_ROOT, SKELETON_NODE_TYPE_JOINT, SKELETON_NODE_TYPE_END_SITE, SkeletonRootNode, SkeletonJointNode, SkeletonEndSiteNode +from .skeleton_models import SKELETON_MODELS +from .constants import * diff --git a/build/lib/anim_utils/animation_data/acclaim.py b/build/lib/anim_utils/animation_data/acclaim.py new file mode 100644 index 0000000..1b5c9cd --- /dev/null +++ b/build/lib/anim_utils/animation_data/acclaim.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +""" parser of asf and amc formats + https://research.cs.wisc.edu/graphics/Courses/cs-838-1999/Jeff/ASF-AMC.html + """ +import sys +import math +import numpy as np +from transformations import euler_matrix + +DEFAULT_FRAME_TIME = 1.0/120 + +def asf_to_bvh_channels(dof): + channels = [] + for d in dof: + type_str = "" + if d[0].lower() == "r": + type_str = "rotation" + elif d[0].lower() == "t": + type_str = "translation" + axis_str = d[-1].upper() + channels.append(axis_str + type_str) + return channels + +def rotate_around_x(alpha): + #Note vectors represent columns + cx = math.cos(alpha) + sx = math.sin(alpha) + m = np.array([[1.0, 0.0, 0.0, 0.0], + [0.0, cx , sx,0.0], + [0.0, -sx, cx,0.0], + [0.0,0.0,0.0,1.0]],np.float32) + return m.T + + +def rotate_around_y(beta): + #Note vectors represent columns + cy = math.cos(beta) + sy = math.sin(beta) + m = np.array([[cy,0.0,-sy ,0.0], + [0.0,1.0,0.0,0.0], + [sy, 0.0, cy,0.0], + [0.0,0.0,0.0,1.0]],np.float32) + return m.T + + +def rotate_around_z(gamma): + #Note vectors represent columns + cz = math.cos(gamma) + sz = math.sin(gamma) + m = np.array([[cz, sz,0.0,0.0], + [-sz, cz,0.0,0.0], + [0.0,0.0,1.0,0.0], + [0.0,0.0,0.0,1.0]],np.float32) + return m.T + + +AXES = "rxyz" +def create_euler_matrix(angles, order): + m = np.eye(4, dtype=np.float32) + for idx, d in enumerate(order): + a = np.radians(angles[idx]) + d = d[0].lower() + local_rot = np.eye(4) + if d =="x": + local_rot = euler_matrix(a,0,0, AXES) + elif d =="y": + local_rot = euler_matrix(0,a,0, AXES) + elif d =="z": + local_rot = euler_matrix(0,0,a, AXES) + m = np.dot(local_rot, m) + return m + +def create_euler_matrix2(angles, order): + m = np.eye(4, dtype=np.float32) + for idx, d in enumerate(order): + a = np.radians(angles[idx]) + d = d[0].lower() + local_rot = np.eye(4) + if d =="x": + local_rot = rotate_around_x(a) + elif d =="y": + local_rot = rotate_around_y(a) + elif d =="z": + local_rot = rotate_around_z(a) + m = np.dot(local_rot, m) + return m + +def create_coordinate_transform_matrices(data): + """ FROM https://research.cs.wisc.edu/graphics/Courses/cs-838-1999/Jeff/ASF-AMC.html + Precalculate some the transformation for each segment from the axis + """ + angles = data["root"]["orientation"] + order = data["root"]["axis"] + order = asf_to_bvh_channels(order) + c = create_euler_matrix(angles, order) + data["root"]["coordinate_transform"] = c + data["root"]["inv_coordinate_transform"] = np.linalg.inv(c) + + for key in data["bones"]: + if "axis" in data["bones"][key]: + angles, order = data["bones"][key]["axis"] + order = asf_to_bvh_channels(order) + c = create_euler_matrix(angles, order) + data["bones"][key]["coordinate_transform"] = c + data["bones"][key]["inv_coordinate_transform"] = np.linalg.inv(c) + return data + +def set_parents(data): + for parent in data["children"]: + for c in data["children"][parent]: + data["bones"][c]["parent"] = parent + return data + +def convert_bones_to_joints(data): + """ ASF stores bones that need to be converted into joints the internal skeleton class + based on the BVH format + additionally helper bones for the root need to be removed + """ + data = set_parents(data) + for b in data["bones"]: + if b == "root": + continue + # add offset to parent + parent = data["bones"][b]["parent"] + if parent in data["bones"] and "direction" in data["bones"][parent]: + offset = np.array(data["bones"][parent]["direction"]) + offset *= float(data["bones"][parent]["length"]) + data["bones"][b]["offset"] = offset + + # remove helper bones for hips without dofs + new_root_children = [] + old_root_children = [] + for c in data["children"]["root"]: + if "dof" in data["bones"][c] and len(data["bones"][c]["dof"])>0: + new_root_children.append(c) + else: # replace bones without dofs with their children + old_root_children.append(c) + for cc in data["children"][c]: + data["bones"][cc]["parent"] = "root" + new_root_children.append(cc) + data["children"]["root"] = new_root_children + for c in old_root_children: + del data["bones"][c] + return data + +def parse_asf_file(filepath): + with open(filepath, "rt") as in_file: + lines = in_file.readlines() + data = dict() + data["bones"] = dict() + idx = 0 + while idx < len(lines): + next_line = lines[idx].lstrip() + if next_line.startswith(":root"): + idx += 1 + data["root"], idx = read_root_data(lines, idx) + idx -=1 + if next_line.startswith(":name"): + data["name"] = next_line.split(" ")[1] + idx+=1 + elif next_line.startswith(":bonedata"): + idx+=1 + next_line = lines[idx].lstrip() + while not next_line.startswith(":hierarchy") and idx+1 < len(lines): + node, idx = read_bone_data(lines, idx) + if "name" in node: + name = node["name"] + data["bones"][name] = node + if idx < len(lines): + next_line = lines[idx].lstrip() + elif next_line.startswith(":hierarchy"): + data["children"], idx = read_hierarchy(lines, idx) + else: + idx+=1 + data = create_coordinate_transform_matrices(data) + data = convert_bones_to_joints(data) + return data + +def read_root_data(lines, idx): + data = dict() + #print("start root", idx) + next_line = lines[idx].strip() + while not next_line.startswith(":bonedata") and idx+1 < len(lines): + values = next_line.split(" ") + if len(values) > 0: + key = values[0] + if key == "position": + data["position"] = [values[1], values[2], values[3]] + elif key == "orientation": + data["orientation"] = [float(values[1]),float(values[2]), float(values[3])] + elif key == "axis": + data["axis"] = values[1] + elif key == "order": + data["order"] = [v for v in values if v != "order"] + #elif key == "limits": + # data[key] = [values[1], values[2], values[3]] + if idx+1 < len(lines): + idx+=1 + next_line = lines[idx].strip() # remove empty lines + + #print("end root", idx, next_line) + return data, idx + +def read_bone_data(lines, idx): + idx +=1 #skip begin + data = dict() + #print("start bone", idx) + next_line = lines[idx].strip() + while not next_line.startswith("end") and idx+1 < len(lines): + values = next_line.split(" ") + values = [v for v in values if v != ""] + if len(values) > 0: + key = values[0] + if key == "id": + data["id"] = values[1] + elif key == "name": + data["name"] = values[1] + elif key == "direction": + direction = np.array([float(v) for v in values if v != "direction"]) + direction /= np.linalg.norm(direction) + data["direction"] = direction.tolist() + elif key == "length": + data["length"] = float(values[1]) + elif key == "axis": + data["axis"] = [float(values[1]), float(values[2]), float(values[3])], values[4] + elif key == "dof": + data["dof"] = [v for v in values if v != "dof"] + #elif key == "limits": + # data[key] = [values[1], values[2], values[3]] + if idx+1 < len(lines): + idx+=1 + next_line = lines[idx].strip() # remove empty lines + idx +=1 #skip end + #print("end", idx, lines[idx]) + return data, idx + + +def read_hierarchy(lines, idx): + print("found hierarchy") + idx +=1 #skip begin + child_dict = dict() + next_line = lines[idx].strip() + while not next_line.startswith("end"): + values = next_line.split(" ") + if len(values) > 1: + child_dict[values[0]] = values[1:] + idx+=1 + next_line = lines[idx].strip() # remove empty lines + idx +=1 #skip end + return child_dict, idx + +def parse_amc_file(filepath): + with open(filepath, "rt") as in_file: + lines = in_file.readlines() + frames = [] + current_frame = None + idx = 0 + while idx < len(lines): + next_line = lines[idx].strip() + values = next_line.split(" ") + if len(values) == 1: + if values[0].isdigit(): + if current_frame is not None: + frames.append(current_frame) + current_frame = dict() + elif len(values) > 1 and current_frame is not None: + key = values[0] + current_frame[key] = [float(v) for v in values if v != key] + idx+=1 + return frames diff --git a/build/lib/anim_utils/animation_data/bvh.py b/build/lib/anim_utils/animation_data/bvh.py new file mode 100644 index 0000000..aebcc06 --- /dev/null +++ b/build/lib/anim_utils/animation_data/bvh.py @@ -0,0 +1,478 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH, Daimler AG. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +""" + +BVH +=== + +Biovision file format classes for reading and writing. +BVH Reader by Martin Manns +BVH Writer by Erik Herrmann + +""" + +import os +from collections import OrderedDict +import numpy as np +from transformations import quaternion_matrix, euler_from_matrix, euler_matrix +from .utils import rotation_order_to_string +from .quaternion_frame import quaternion_to_euler + +EULER_LEN = 3 +QUAT_LEN = 4 +TRANSLATION_LEN = 3 +TOE_NODES = ["Bip01_R_Toe0", "Bip01_L_Toe0"] +DEFAULT_FRAME_TIME = 0.013889 +DEFAULT_ROTATION_ORDER = ['Xrotation','Yrotation','Zrotation'] + + +class BVHData: + """Biovision data format class + """ + def __init__(self): + self.node_names = OrderedDict() + self.node_channels = [] + self.parent_dict = {} + self.root = None + self.frame_time = DEFAULT_FRAME_TIME + self.frames = None + self.animated_joints = None + self.filename = "" + + + def get_node_angles(self, node_name, frame): + """Returns the rotation for one node at one frame of an animation + Parameters + ---------- + * node_name: String + \tName of node + * frame: np.ndarray + \t animation keyframe frame + + """ + channels = self.node_names[node_name]["channels"] + euler_angles = [] + rotation_order = [] + for ch in channels: + if ch.lower().endswith("rotation"): + idx = self.node_channels.index((node_name, ch)) + rotation_order.append(ch) + euler_angles.append(frame[idx]) + return euler_angles, rotation_order + + def get_angles(self, node_channels): + """Returns numpy array of angles in all frames for specified channels + + Parameters + ---------- + * node_channels: 2-tuples of strings + \tEach tuple contains joint name and channel name + \te.g. ("hip", "Xposition") + + """ + indices = self.get_channel_indices(node_channels) + return self.frames[:, indices] + + def get_animated_joints(self): + """Returns an ordered list of joints which have animation channels""" + if self.animated_joints is not None: + for joint in self.animated_joints: + yield joint + else: + for name, node in self.node_names.items(): + if "channels" in node.keys() and len(node["channels"]) > 0: + yield name + + def set_animated_joints(self, animated_joints): + self.animated_joints = animated_joints + + def get_animated_frames(self): + channel_indices = [] + for joint in self.get_animated_joints(): + channel_indices += self.node_names[joint]["channel_indices"] + return self.frames[:, channel_indices] + + def convert_rotation_order(self, rotation_order): + self.convert_skeleton_rotation_order(rotation_order) + self.convert_motion_rotation_order(rotation_order_to_string(rotation_order)) + + def convert_skeleton_rotation_order(self, rotation_order): + # update channel indices + rotation_list = self.node_names[self.root]["channels"][3:] + #new_indices = sorted(range(len(rotation_list)), key=lambda k : rotation_list[k]) + for node_name, node in self.node_names.items(): + if 'End' not in node_name: + if len(node['channels']) == 6: + rotation_list = node['channels'][3:] + node['channels'][3:] = rotation_order + node['rotation_order'] = rotation_order_to_string(rotation_list) + else: + rotation_list = node['channels'] + node['channels'] = rotation_order + node['rotation_order'] = rotation_order_to_string(rotation_list) + + + def convert_motion_rotation_order(self, rotation_order_str): + new_frames = np.zeros(self.frames.shape) + for i in range(len(new_frames)): + for node_name, node in self.node_names.items(): + if 'End' not in node_name: + if len(node['channels']) == 6: + rot_mat = euler_matrix(*np.deg2rad(self.frames[i, node['channel_indices'][3:]]), + axes=node['rotation_order']) + new_frames[i, node['channel_indices'][:3]] = self.frames[i, node['channel_indices'][:3]] + new_frames[i, node['channel_indices'][3:]] = np.rad2deg(euler_from_matrix(rot_mat, rotation_order_str)) + else: + rot_mat = euler_matrix(*np.deg2rad(self.frames[i, node['channel_indices']]), + axes=node['rotation_order']) + new_frames[i, node['channel_indices']] = np.rad2deg(euler_from_matrix(rot_mat, rotation_order_str)) + self.frames = new_frames + + def scale(self, scale): + for node in self.node_names: + self.node_names[node]["offset"] = [scale * o for o in self.node_names[node]["offset"]] + if "channels" not in self.node_names[node]: + continue + ch = [(node, c) for c in self.node_names[node]["channels"] if "position" in c] + if len(ch) > 0: + ch_indices = self.get_channel_indices(ch) + scaled_params = [scale * o for o in self.frames[:, ch_indices]] + self.frames[:, ch_indices] = scaled_params + + def get_channel_indices(self, node_channels): + """Returns indices for specified channels + + Parameters + ---------- + * node_channels: 2-tuples of strings + \tEach tuple contains joint name and channel name + \te.g. ("hip", "Xposition") + + """ + return [self.node_channels.index(nc) for nc in node_channels] + + def get_node_channels(self, node_name): + channels = None + if node_name in self.node_names and "channels" in self.node_names[node_name]: + channels = self.node_names[node_name]["channels"] + return channels + +class BVHReader(BVHData): + """Biovision file format class + + Parameters + ---------- + * filename: string + \t path to BVH file that is loaded initially + """ + def __init__(self, filename=""): + super().__init__() + if filename != "": + self.filename = os.path.split(filename)[-1] + with open(filename, "r") as file: + lines = file.readlines() + self.process_lines(lines) + + @classmethod + def init_from_string(cls, skeleton_string): + bvh_reader = cls(infilename="") + lines = skeleton_string.split("\n") + bvh_reader.process_lines(lines) + return bvh_reader + + def process_lines(self, lines): + """Reads lines from a BVH file + + Parameters + ---------- + * lines: list of strings + + """ + lines = [l for l in lines if l.strip() != ""] + line_index = 0 + n_lines = len(lines) + while line_index < n_lines: + if lines[line_index].startswith("HIERARCHY"): + line_index = self._read_skeleton(lines, line_index, n_lines) + if lines[line_index].startswith("MOTION") and n_lines > line_index+3: + self._read_frametime(lines, line_index+2) + line_index = self._read_frames(lines, line_index+3, n_lines) + else: + line_index += 1 + + def _read_skeleton(self, lines, line_index, n_lines): + """Reads the skeleton part of a BVH file""" + line_index = line_index + parents = [] + node_name = None + parent_name = None + self.node_names = OrderedDict() + self.node_channels = [] + while line_index < n_lines and not lines[line_index].startswith("MOTION"): + split_line = lines[line_index].strip().split() + if not split_line: + continue + if "{" in split_line: + parents.append(node_name) + elif "}" in split_line: + node_name = parents.pop(-1) + level = len(parents) + if level > 0: + parent_name = parents[-1] + node_name = self._read_split_joint_line(split_line, level, node_name, parent_name) + if self.root is None: + self.root = node_name + line_index += 1 + return line_index + + + def _read_split_joint_line(self, split_line, level, node_name, parent): + if split_line[0] == "ROOT" or split_line[0] == "JOINT": + node_name = split_line[1] + node_data = {"children": [], "level": level, "channels": [], "channel_indices": []} + self.node_names[node_name] = node_data + if parent is not None: + self.node_names[parent]["children"].append(node_name) + + elif split_line[0] == "CHANNELS": + for channel in split_line[2:]: + self.node_channels.append((node_name, channel)) + self.node_names[node_name]["channels"].append(channel) + self.node_names[node_name]["channel_indices"].append(len(self.node_channels) - 1) + + elif split_line == ["End", "Site"]: + node_name = node_name + "_" + "".join(split_line) + self.node_names[node_name] = {"level": level} + # also the end sites need to be adde as children + self.node_names[parent]["children"].append(node_name) + + elif split_line[0] == "OFFSET" and node_name is not None: + self.node_names[node_name]["offset"] = list(map(float, split_line[1:])) + return node_name + + def _read_frametime(self, lines, line_index): + """Reads the frametime part of a BVH file""" + if lines[line_index].startswith("Frame Time:"): + self.frame_time = float(lines[line_index].split(":")[-1].strip()) + + def _read_frames(self, lines, line_index, n_lines=-1): + """Reads the frames part of a BVH file""" + line_index = line_index + if n_lines == -1: + n_lines = len(lines) + frames = [] + while line_index < n_lines: + line_split = lines[line_index].strip().split() + frames.append(np.array(list(map(float, line_split)))) + line_index += 1 + + self.frames = np.array(frames) + return line_index + + + +def write_euler_frames_to_bvh_file(filename, skeleton, euler_frames, frame_time): + """ Write the hierarchy string and the frame parameter string to a file + Parameters + ---------- + * filename: String + Name of the created bvh file. + * skeleton: Skeleton + Skeleton structure needed to copy the hierarchy + * euler_frames: np.ndarray + array of pose rotation parameters in euler angles + * frame_time: float + time in seconds for the display of each keyframe + """ + bvh_string = generate_bvh_string(skeleton, euler_frames, frame_time) + if filename[-4:] == '.bvh': + filename = filename + else: + filename = filename + '.bvh' + with open(filename, 'w') as outfile: + outfile.write(bvh_string) + + +def generate_bvh_string(skeleton, euler_frames, frame_time): + """ Write the hierarchy string and the frame parameter string to a file + Parameters + ---------- + * skeleton: Skeleton + Skeleton structure needed to copy the hierarchy + * euler_frames: np.ndarray + array of pose rotation parameters in euler angles + * frame_time: float + time in seconds for the display of each keyframe + """ + bvh_string = _generate_hierarchy_string(skeleton) + "\n" + bvh_string += _generate_bvh_frame_string(euler_frames, frame_time) + return bvh_string + +def _generate_hierarchy_string(skeleton): + """ Initiates the recursive generation of the skeleton structure string + by calling _generate_joint_string with the root joint + """ + hierarchy_string = "HIERARCHY\n" + hierarchy_string += _generate_joint_string(skeleton.root, skeleton, 0) + return hierarchy_string + +def _generate_joint_string(joint, skeleton, joint_level): + """ Recursive traversing of the joint hierarchy to create a + skeleton structure string in the BVH format + """ + joint_string = "" + temp_level = 0 + tab_string = "" + while temp_level < joint_level: + tab_string += "\t" + temp_level += 1 + + # determine joint type + if joint_level == 0: + joint_string += tab_string + "ROOT " + joint + "\n" + else: + if len(skeleton.nodes[joint].children) > 0: + joint_string += tab_string + "JOINT " + joint + "\n" + else: + joint_string += tab_string + "End Site" + "\n" + # open bracket add offset + joint_string += tab_string + "{" + "\n" + offset = skeleton.nodes[joint].offset + joint_string += tab_string + "\t" + "OFFSET " + "\t " + \ + str(offset[0]) + "\t " + str(offset[1]) + "\t " + str(offset[2]) + "\n" + + if len(skeleton.nodes[joint].children) > 0: + # channel information + channels = skeleton.nodes[joint].channels + joint_string += tab_string + "\t" + \ + "CHANNELS " + str(len(channels)) + " " + for tok in channels: + joint_string += tok + " " + joint_string += "\n" + + joint_level += 1 + # recursive call for all children + for child in skeleton.nodes[joint].children: + joint_string += _generate_joint_string(child.node_name, skeleton, joint_level) + + # close the bracket + joint_string += tab_string + "}" + "\n" + return joint_string + +def _generate_bvh_frame_string(euler_frames, frame_time): + """ Converts a list of euler frames into the BVH file representation. + Parameters + ---------- + * euler_frames: np.ndarray + array of pose rotation parameters in euler angles + * frame_time: float + time in seconds for the display of each keyframe + """ + frame_parameter_string = "MOTION\n" + frame_parameter_string += "Frames: " + str(len(euler_frames)) + "\n" + frame_parameter_string += "Frame Time: " + str(frame_time) + "\n" + for frame in euler_frames: + frame_parameter_string += ' '.join([str(f) for f in frame]) + frame_parameter_string += '\n' + return frame_parameter_string + + +def convert_quaternion_to_euler_frames(skeleton, quat_frames): + """ Converts the joint rotations from quaternion to euler rotations + Parameters + ---------- + * skeleton: Skeleton + Skeleton structure needed to copy the hierarchy + * quat_frames: np.ndarray + array of pose rotation parameters as quaternion + """ + joint_order = [] + rotation_info = dict() + def get_joint_meta_info(joint_name, skeleton): + if len(skeleton.nodes[joint_name].children) > 0: + joint_order.append(joint_name) + rot_order = [] + offset = 0 + for idx, ch in enumerate(skeleton.nodes[joint_name].channels): + if ch.lower().endswith("rotation"): + rot_order.append(ch) + else: + offset += 1 + rotation_info[joint_name] = rot_order, offset + for child in skeleton.nodes[joint_name].children: + get_joint_meta_info(child.node_name, skeleton) + + get_joint_meta_info(skeleton.root, skeleton) + n_frames = len(quat_frames) + n_params = sum([len(skeleton.nodes[j].channels) for j in joint_order]) + euler_frames = np.zeros((n_frames, n_params)) + for frame_idx, quat_frame in enumerate(quat_frames): + euler_frames[frame_idx,:TRANSLATION_LEN] = quat_frame[:TRANSLATION_LEN] + dst = 0 # the translation offset will be added + for joint_name in joint_order: + channels = skeleton.nodes[joint_name].channels + n_channels = len(channels) + rotation_order = rotation_info[joint_name][0] + rotation_offset = rotation_info[joint_name][1] + src = skeleton.nodes[joint_name].index * QUAT_LEN + TRANSLATION_LEN + q = quat_frame[src:src+QUAT_LEN] + e = quaternion_to_euler(q, rotation_order) + params_start = dst + rotation_offset + params_end = params_start + EULER_LEN + euler_frames[frame_idx, params_start:params_end] = e + dst += n_channels + return euler_frames + + +class BVHWriter(object): + """Saves an input motion defined either as an array of euler or quaternion + frame vectors to a BVH file. + Legacy interface that calls write_euler_frames_to_bvh_file + + Parameters + ---------- + * filename: String or None + Name of the created bvh file. Can be None. + * skeleton: Skeleton + Skeleton structure needed to copy the hierarchy + * frame_data: np.ndarray + array of motion vectors, either with euler or quaternion as rotation parameters + * frame_time: float + time in seconds for the display of each keyframe + * is_quaternion: Boolean + Defines wether the frame_data is quaternion data or euler data + """ + def __init__(self, filename, skeleton, frame_data, frame_time, is_quaternion=False): + self.skeleton = skeleton + self.frame_data = frame_data + self.frame_time = frame_time + if is_quaternion: + self.frame_data = convert_quaternion_to_euler_frames(self.skeleton, self.frame_data) + if filename is not None: + self.write(filename) + + def write(self, filename): + write_euler_frames_to_bvh_file(filename, self.skeleton, self.frame_data, self.frame_time) + + def generate_bvh_string(self): + return generate_bvh_string(self.skeleton, self.frame_data, self.frame_time) diff --git a/build/lib/anim_utils/animation_data/constants.py b/build/lib/anim_utils/animation_data/constants.py new file mode 100644 index 0000000..3321aae --- /dev/null +++ b/build/lib/anim_utils/animation_data/constants.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +DEFAULT_ROTATION_ORDER = ['Xrotation','Yrotation','Zrotation'] +DEFAULT_SMOOTHING_WINDOW_SIZE = 20 +LEN_QUAT = 4 +LEN_ROOT_POS = 3 +LEN_EULER = 3 +ROTATION_TYPE_EULER = 0 +ROTATION_TYPE_QUATERNION = 1 +LEN_ROOT = 3 \ No newline at end of file diff --git a/build/lib/anim_utils/animation_data/fbx/__init__.py b/build/lib/anim_utils/animation_data/fbx/__init__.py new file mode 100644 index 0000000..688b3ca --- /dev/null +++ b/build/lib/anim_utils/animation_data/fbx/__init__.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +from anim_utils.animation_data.motion_vector import MotionVector +from anim_utils.animation_data.skeleton_builder import SkeletonBuilder + +has_fbx = True +try: + import FbxCommon +except ImportError as e: + has_fbx = False + print("Info: Disable FBX IO due to missing library") + pass + + +if has_fbx: + from .fbx_import import load_fbx_file + from .fbx_export import export_motion_vector_to_fbx_file +else: + def load_fbx_file(file_path): + raise NotImplementedError + + def export_motion_vector_to_fbx_file(skeleton, motion_vector, out_file_name): + raise NotImplementedError + + + +def load_skeleton_and_animations_from_fbx(file_path): + skeleton_def, animations = load_fbx_file(file_path) + skeleton = SkeletonBuilder().load_from_fbx_data(skeleton_def) + anim_names = list(animations.keys()) + motion_vectors = dict() + if len(anim_names) > 0: + anim_name = anim_names[0] + mv = MotionVector() + mv.from_fbx(animations[anim_name], skeleton.animated_joints) + motion_vectors[anim_name] = mv + return skeleton, motion_vectors + diff --git a/build/lib/anim_utils/animation_data/fbx/fbx_export.py b/build/lib/anim_utils/animation_data/fbx/fbx_export.py new file mode 100644 index 0000000..2f23a08 --- /dev/null +++ b/build/lib/anim_utils/animation_data/fbx/fbx_export.py @@ -0,0 +1,219 @@ +""" +Code to export an animated skeleton to an FBX file based on a skeleton and a motion vector. +The code is based on Example01 of the FBX SDK samples. +""" + +from transformations import euler_from_quaternion +import numpy as np +import FbxCommon +from fbx import * + + +def create_scene(sdk_manager, scene, skeleton, frames, frame_time): + info = FbxDocumentInfo.Create(sdk_manager, "SceneInfo") + info.mTitle = "MotionExportScene" + scene.SetSceneInfo(info) + root_node = create_skeleton(sdk_manager, "", skeleton) + scene.GetRootNode().LclTranslation.Set(FbxDouble3(0, 0, 0)) + scene.GetRootNode().AddChild(root_node) + set_rest_pose(sdk_manager, scene, root_node, skeleton) + set_animation_curves(scene, root_node, skeleton, frames, frame_time) + return root_node + + +def create_skeleton(sdk_manager, name, skeleton): + root_node = create_root_node(sdk_manager, name, skeleton) + return root_node + +def create_root_node(sdk_manager, name, skeleton): + skeleton_root = create_skeleton_nodes_recursively(sdk_manager, name, skeleton, skeleton.root) + node_type = FbxSkeleton.eRoot + node_attribute = FbxSkeleton.Create(sdk_manager, name) + node_attribute.SetSkeletonType(node_type) + skeleton_root.SetNodeAttribute(node_attribute) + return skeleton_root + +def create_root_node2(sdk_manager, name, skeleton): + node_type = FbxSkeleton.eRoot + node_attribute = FbxSkeleton.Create(sdk_manager, name) + node_attribute.SetSkeletonType(node_type) + extra_root_node = FbxNode.Create(sdk_manager, "Root") + extra_root_node.SetNodeAttribute(node_attribute) + skeleton_root = create_skeleton_nodes_recursively(sdk_manager, name, skeleton, skeleton.root) + extra_root_node.AddChild(skeleton_root) + return extra_root_node + +def create_skeleton_nodes_recursively(sdk_manager, skeleton_name, skeleton, node_name): + node = skeleton.nodes[node_name] + name = skeleton_name + node_name + skeleton_node_attribute = FbxSkeleton.Create(sdk_manager, skeleton_name) + node_type = FbxSkeleton.eLimbNode + skeleton_node_attribute.SetSkeletonType(node_type) + skeleton_node = FbxNode.Create(sdk_manager, name) + skeleton_node.SetNodeAttribute(skeleton_node_attribute) + t = FbxDouble3(node.offset[0], node.offset[1], node.offset[2]) + skeleton_node.LclTranslation.Set(t) + for c_node in node.children: + c_name = c_node.node_name + c_node = create_skeleton_nodes_recursively(sdk_manager, skeleton_name, skeleton, c_name) + skeleton_node.AddChild(c_node) + return skeleton_node + + +def set_rest_pose_recursively(pose, fbx_node, skeleton): + name = fbx_node.GetName() + if name in skeleton.nodes.keys(): + node = skeleton.nodes[name] + t = node.offset + + l_t = FbxVector4(t[0], t[1], t[2]) + l_t = FbxVector4(0,0,0) + l_r = FbxVector4(0.0, 0.0, 0.0) + l_s = FbxVector4(1.0, 1.0, 1.0) + + transform = FbxMatrix() + transform.SetTRS(l_t, l_r, l_s) + pose.Add(fbx_node, transform, True)# maybe set this to false + n_children = fbx_node.GetChildCount() + for idx in range(n_children): + c_node = fbx_node.GetChild(idx) + set_rest_pose_recursively(pose, c_node, skeleton) + + +def set_rest_pose(sdk_manager, scene, root_node, skeleton): + pose = FbxPose.Create(sdk_manager, "RestPose") + set_rest_pose_recursively(pose, root_node, skeleton) + scene.AddPose(pose) + + +def create_translation_curve(fbx_node, anim_layer, euler_frames, frame_time, dimension, dim_idx): + t = FbxTime() + curve = fbx_node.LclTranslation.GetCurve(anim_layer, dimension, True) + curve.KeyModifyBegin() + for idx, frame in enumerate(euler_frames): + t.SetSecondDouble(idx * frame_time) + key_index = curve.KeyAdd(t)[0] + curve.KeySetValue(key_index, frame[dim_idx]) + curve.KeySetInterpolation(key_index, FbxAnimCurveDef.eInterpolationConstant) + curve.KeyModifyEnd() + + +def create_euler_curve(fbx_node, anim_layer, euler_frames, frame_time, dimension, dim_idx): + t = FbxTime() + curve = fbx_node.LclRotation.GetCurve(anim_layer, dimension, True) + curve.KeyModifyBegin() + for idx, frame in enumerate(euler_frames): + t.SetSecondDouble(idx * frame_time) + key_index = curve.KeyAdd(t)[0] + curve.KeySetValue(key_index, frame[dim_idx]) + curve.KeySetInterpolation(key_index, FbxAnimCurveDef.eInterpolationConstant) + curve.KeyModifyEnd() + + +def create_translation_curves(fbx_node, anim_layer, euler_frames, frame_time): + create_translation_curve(fbx_node, anim_layer, euler_frames, frame_time, "X", 0) + create_translation_curve(fbx_node, anim_layer, euler_frames, frame_time, "Y", 1) + create_translation_curve(fbx_node, anim_layer, euler_frames, frame_time, "Z", 2) + + +def create_rotation_curves(fbx_node, anim_layer, skeleton, euler_frames, frame_time): + node_name = fbx_node.GetName() + if node_name not in skeleton.animated_joints: + return + node_idx = skeleton.animated_joints.index(node_name) + offset = node_idx * 3 + 3 + create_euler_curve(fbx_node, anim_layer, euler_frames, frame_time, "X", offset) + create_euler_curve(fbx_node, anim_layer, euler_frames, frame_time, "Y", offset+1) + create_euler_curve(fbx_node, anim_layer, euler_frames, frame_time, "Z", offset+2) + + +def add_rotation_curves_recursively(fbx_node, anim_layer, skeleton, euler_frames, frame_time): + create_rotation_curves(fbx_node, anim_layer, skeleton, euler_frames, frame_time) + n_children = fbx_node.GetChildCount() + for idx in range(n_children): + c_node = fbx_node.GetChild(idx) + add_rotation_curves_recursively(c_node, anim_layer, skeleton, euler_frames, frame_time) + + +def convert_quaternion_to_euler_frame(skeleton, frame): + n_dims = len(skeleton.animated_joints) * 3 + 3 + euler_frame = np.zeros(n_dims) + euler_frame[:3] = frame[:3] + target_offset = 3 + src_offset = 3 + for idx, node in enumerate(skeleton.animated_joints): + q = frame[src_offset:src_offset + 4] + e = euler_from_quaternion(q) + euler_frame[target_offset:target_offset + 3] = np.degrees(e) + target_offset += 3 + src_offset += 4 + return euler_frame + +def set_animation_curves(scene, root_node, skeleton, frames, frame_time): + + # convert frames from quaternion to euler + euler_frames = [] + for frame in frames: + euler_frame = convert_quaternion_to_euler_frame(skeleton, frame) + euler_frames.append(euler_frame) + + anim_stack_name = "default" + anim_stack = FbxAnimStack.Create(scene, anim_stack_name) + anim_layer = FbxAnimLayer.Create(scene, "Base Layer") + anim_stack.AddMember(anim_layer) + + create_translation_curves(root_node, anim_layer, euler_frames, frame_time) + add_rotation_curves_recursively(root_node, anim_layer, skeleton, euler_frames, frame_time) + + +def export_motion_vector_to_fbx_file(skeleton, motion_vector, out_file_name, fbx_axis_system=FbxAxisSystem.MayaZUp): + """ Save a scene with a single skeleton and animation curves for the root and joints + """ + sdk_manager, scene = FbxCommon.InitializeSdkObjects() + root_node = create_scene(sdk_manager, scene, skeleton, motion_vector.frames, motion_vector.frame_time) + fbx_axis_system.MayaZUp.ConvertScene(scene) + #sdk_manager.GetIOSettings().SetBoolProp(EXP_FBX_EMBEDDED, True) + save_scene(sdk_manager, scene, out_file_name, pEmbedMedia=False) + print("finished") + + sdk_manager.Destroy() + + +def save_scene(pSdkManager, pScene, pFilename, pFileFormat = -1, pEmbedMedia = False): + """ copied from fbx common + changed settings to a Blender compatible version according to + http://stackoverflow.com/questions/29833986/fbx-sdk-exporting-into-older-fbx-file-format + """ + + lExporter = FbxExporter.Create(pSdkManager, "") + lExporter.SetFileExportVersion("FBX201200", FbxSceneRenamer.eFBX_TO_FBX) + + if pFileFormat < 0 or pFileFormat >= pSdkManager.GetIOPluginRegistry().GetWriterFormatCount(): + pFileFormat = pSdkManager.GetIOPluginRegistry().GetNativeWriterFormat() + if not pEmbedMedia: + lFormatCount = pSdkManager.GetIOPluginRegistry().GetWriterFormatCount() + for lFormatIndex in range(lFormatCount): + if pSdkManager.GetIOPluginRegistry().WriterIsFBX(lFormatIndex): + lDesc = pSdkManager.GetIOPluginRegistry().GetWriterFormatDescription(lFormatIndex) + if "binary" in lDesc: + pFileFormat = lFormatIndex + break + + if not pSdkManager.GetIOSettings(): + ios = FbxIOSettings.Create(pSdkManager, IOSROOT) + pSdkManager.SetIOSettings(ios) + + pSdkManager.GetIOSettings().SetBoolProp(EXP_FBX_MATERIAL, True) + pSdkManager.GetIOSettings().SetBoolProp(EXP_FBX_TEXTURE, True) + pSdkManager.GetIOSettings().SetBoolProp(EXP_FBX_EMBEDDED, pEmbedMedia) + pSdkManager.GetIOSettings().SetBoolProp(EXP_FBX_SHAPE, True) + pSdkManager.GetIOSettings().SetBoolProp(EXP_FBX_GOBO, True) + pSdkManager.GetIOSettings().SetBoolProp(EXP_FBX_ANIMATION, True) + pSdkManager.GetIOSettings().SetBoolProp(EXP_FBX_GLOBAL_SETTINGS, True) + + result = lExporter.Initialize(pFilename, pFileFormat, pSdkManager.GetIOSettings()) + if result == True: + result = lExporter.Export(pScene) + + lExporter.Destroy() + return result \ No newline at end of file diff --git a/build/lib/anim_utils/animation_data/fbx/fbx_import.py b/build/lib/anim_utils/animation_data/fbx/fbx_import.py new file mode 100644 index 0000000..5226024 --- /dev/null +++ b/build/lib/anim_utils/animation_data/fbx/fbx_import.py @@ -0,0 +1,198 @@ +import FbxCommon +from fbx import * +import numpy as np +import collections +from transformations import quaternion_matrix, euler_from_quaternion, quaternion_from_euler +from anim_utils.animation_data.skeleton_node import SKELETON_NODE_TYPE_ROOT,SKELETON_NODE_TYPE_JOINT, SKELETON_NODE_TYPE_END_SITE + +def load_fbx_file(file_path): + importer = FBXImporter(file_path) + importer.load() + importer.destroy() + return importer.skeleton, importer.animations + + +class FBXImporter(object): + def __init__(self, file_path): + self.skeleton = None + self.skeleton_root_node = None + self.skinning_data = dict() + self.animations = dict() + self.mesh_list = [] + + (self.sdk_manager, self.fbx_scene) = FbxCommon.InitializeSdkObjects() + FbxCommon.LoadScene(self.sdk_manager, self.fbx_scene, file_path) + FbxAxisSystem.OpenGL.ConvertScene(self.fbx_scene) + + def destroy(self): + self.sdk_manager.Destroy() + + def load(self): + self.skeleton = None + self.skinning_data = dict() + self.animations = dict() + self.mesh_list = [] + root_node = self.fbx_scene.GetRootNode() + self.parseFBXNodeHierarchy(root_node, 0) + if self.skeleton_root_node is not None: + self.animations = self.read_animations(self.skeleton_root_node) + + + def parseFBXNodeHierarchy(self, fbx_node, depth): + + n_attributes = fbx_node.GetNodeAttributeCount() + for idx in range(n_attributes): + attribute = fbx_node.GetNodeAttributeByIndex(idx) + if self.skeleton is None and attribute.GetAttributeType() == FbxNodeAttribute.eSkeleton: + self.skeleton_root_node = fbx_node + self.skeleton = self.create_skeleton(fbx_node) + + for idx in range(fbx_node.GetChildCount()): + self.parseFBXNodeHierarchy(fbx_node.GetChild(idx), depth + 1) + + + def create_skeleton(self, node): + current_time = FbxTime() + current_time.SetFrame(0, FbxTime.eFrames24) + scale = node.EvaluateGlobalTransform().GetS()[0] + def add_child_node_recursively(skeleton, fbx_node): + node_name = fbx_node.GetName() + node_idx = len(skeleton["animated_joints"]) + localTransform = fbx_node.EvaluateLocalTransform(current_time) + #lT = localTransform.GetT() + o = fbx_node.LclTranslation.Get() + offset = scale*np.array([o[0], o[1], o[2]]) + q = localTransform.GetQ() + rotation = np.array([q[3],q[0], q[1], q[2]]) + + node = {"name": node_name, + "children": [], + "channels": [], + "offset": offset, + "rotation": rotation, + "fixed": True, + "index": -1, + "quaternion_frame_index": -1, + "node_type": SKELETON_NODE_TYPE_JOINT} + n_children = fbx_node.GetChildCount() + if n_children > 0: + node["channels"] = ["Xrotation", "Yrotation", "Zrotation"] + node["index"] = node_idx + node["quaternion_frame_index"] = node_idx + node["fixed"] = False + skeleton["animated_joints"].append(node_name) + for idx in range(n_children): + c_node = add_child_node_recursively(skeleton, fbx_node.GetChild(idx)) + node["children"].append(c_node["name"]) + skeleton["nodes"][node_name] = node + return node + + o = node.LclTranslation.Get() + offset = scale*np.array([o[0], o[1], o[2]]) + e = node.LclRotation.Get() + rotation = quaternion_from_euler(*e, axes='sxyz') + root_name = node.GetName() + skeleton = dict() + skeleton["animated_joints"] = [root_name] + skeleton["node_names"] = dict() + skeleton["nodes"] = collections.OrderedDict() + skeleton["frame_time"] = 0.013889 + root_node = {"name": root_name, + "children": [], + "channels": ["Xposition", "Yposition", "Zposition", + "Xrotation", "Yrotation", "Zrotation"], + "offset": offset, + "rotation": rotation, + "fixed": False, + "index": 0, + "quaternion_frame_index": 0, + "inv_bind_pose": np.eye(4), + "node_type": SKELETON_NODE_TYPE_ROOT} + + skeleton["nodes"][root_name] = root_node + for idx in range(node.GetChildCount()): + c_node = add_child_node_recursively(skeleton, node.GetChild(idx)) + root_node["children"].append(c_node["name"]) + + skeleton["root"] = root_name + print('animated_joints', len(skeleton["animated_joints"])) + return skeleton + + + def read_animations(self, temp_node): + """src: http://gamedev.stackexchange.com/questions/59419/c-fbx-animation-importer-using-the-fbx-sdk + """ + anims = dict() + count = self.fbx_scene.GetSrcObjectCount(FbxCriteria.ObjectType(FbxAnimStack.ClassId)) + for idx in range(count): + anim = dict() + anim_stack = self.fbx_scene.GetSrcObject(FbxCriteria.ObjectType(FbxAnimStack.ClassId), idx) + self.fbx_scene.SetCurrentAnimationStack(anim_stack) + anim_name = anim_stack.GetName() + anim_layer = anim_stack.GetMember(FbxCriteria.ObjectType(FbxAnimLayer.ClassId), 0) + mLocalTimeSpan = anim_stack.GetLocalTimeSpan() + start = mLocalTimeSpan.GetStart() + end = mLocalTimeSpan.GetStop() + anim["n_frames"] = end.GetFrameCount(FbxTime.eFrames24) - start.GetFrameCount(FbxTime.eFrames24) + 1 + anim["duration"] = end.GetSecondCount() - start.GetSecondCount() + anim["frame_time"] = anim["duration"]/anim["n_frames"] # 0.013889 + anim["curves"] = collections.OrderedDict() + print("found animation", anim_name, anim["n_frames"], anim["duration"]) + is_root = True + nodes = [] + while temp_node is not None: + name = temp_node.GetName() + if has_curve(anim_layer, temp_node): + if is_root: + anim["curves"][name] = get_global_anim_curve(temp_node, start, end) + is_root = False + else: + anim["curves"][name] = get_local_anim_curve(temp_node, start, end) + for i in range(temp_node.GetChildCount()): + nodes.append(temp_node.GetChild(i)) + if len(nodes) > 0: + temp_node = nodes.pop(0) + else: + temp_node = None + anims[anim_name] = anim + return anims + +def get_local_anim_curve(node, start, end): + curve= [] + current_t = FbxTime() + for frame_idx in range(start.GetFrameCount(FbxTime.eFrames24), end.GetFrameCount(FbxTime.eFrames24)): + current_t.SetFrame(frame_idx, FbxTime.eFrames24) + local_transform = node.EvaluateLocalTransform(current_t) + q = local_transform.GetQ() + t = local_transform.GetT() + transform = {"rotation": [q[3], q[0], q[1], q[2]], + "translation": [t[0], t[1], t[2]]} + curve.append(transform) + return curve + +def get_global_anim_curve(node, start, end): + curve= [] + current_t = FbxTime() + for frame_idx in range(start.GetFrameCount(FbxTime.eFrames24), end.GetFrameCount(FbxTime.eFrames24)): + current_t.SetFrame(frame_idx, FbxTime.eFrames24) + local_transform = node.EvaluateGlobalTransform(current_t) + q = local_transform.GetQ() + t = local_transform.GetT() + transform = {"rotation": [q[3], q[0], q[1], q[2]], + "translation": [t[0], t[1], t[2]]} + curve.append(transform) + return curve + +def has_curve(anim_layer, node): + translation = node.LclTranslation.GetCurve(anim_layer, "X") + rotation = node.LclRotation.GetCurve(anim_layer, "X") + return rotation is not None or translation is not None + + +def FBXMatrixToNumpy(m): + q = m.GetQ() + t = m.GetT() + m = quaternion_matrix([q[0], q[1], q[2], q[3]]) + m[:3,3] = t[0], t[1],t[2] + return m + diff --git a/build/lib/anim_utils/animation_data/joint_constraints.py b/build/lib/anim_utils/animation_data/joint_constraints.py new file mode 100644 index 0000000..aafe6d3 --- /dev/null +++ b/build/lib/anim_utils/animation_data/joint_constraints.py @@ -0,0 +1,487 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import numpy as np +import math +from math import acos, cos, sin, atan +from transformations import quaternion_about_axis, quaternion_inverse, quaternion_multiply, euler_from_quaternion + + +def normalize(v): + return v/np.linalg.norm(v) + +def quaternion_to_axis_angle(q): + """http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToAngle/ + + """ + a = 2* math.acos(q[0]) + s = math.sqrt(1- q[0]*q[0]) + if s < 0.001: + x = q[1] + y = q[2] + z = q[3] + else: + x = q[1] / s + y = q[2] / s + z = q[3] / s + v = np.array([x,y,z]) + if np.sum(v)> 0: + return normalize(v),a + else: + return v, a + + + +def swing_twist_decomposition(q, twist_axis): + """ code by janis sprenger based on + Dobrowsolski 2015 Swing-twist decomposition in Clifford algebra. https://arxiv.org/abs/1506.05481 + """ + q = normalize(q) + #twist_axis = np.array((q * offset))[0] + projection = np.dot(twist_axis, np.array([q[1], q[2], q[3]])) * twist_axis + twist_q = np.array([q[0], projection[0], projection[1],projection[2]]) + if np.linalg.norm(twist_q) == 0: + twist_q = np.array([1,0,0,0]) + twist_q = normalize(twist_q) + swing_q = quaternion_multiply(q, quaternion_inverse(twist_q))#q * quaternion_inverse(twist) + return swing_q, twist_q + +OPENGL_UP = np.array([0,1,0]) + + + +def exponential(q): + """ https://math.stackexchange.com/questions/1030737/exponential-function-of-quaternion-derivation + chapter 2 of lee 2000 + """ + v = np.array([q[1], q[2], q[3]]) + theta = np.linalg.norm(v) + return (cos(theta) + v/ theta * sin(theta))# * np.exp(q[0]) + +def log(q): + """ https://math.stackexchange.com/questions/1030737/exponential-function-of-quaternion-derivation + chapter 2 of lee 2000 + """ + v = np.array([q[1], q[2], q[3]]) + w = q[0] + if w == 0: + return np.pi/2 * v + elif 0 < abs(w) < 1: + theta = np.linalg.norm(v) + return v / theta * atan(theta/w) + else: # w == 1 + return 0 + +def dist(q1, q2): + """ chapter 2 of lee 2000""" + return np.linalg.norm(log(quaternion_multiply(quaternion_inverse(q1), q2))) + +def rotate_vector_by_quaternion(v, q): + vq = [0, v[0], v[1], v[2]] + v_prime = quaternion_multiply(q, quaternion_multiply(vq , quaternion_inverse(q)))[1:] + v_prime /= np.linalg.norm(v_prime) + return v_prime + + +def apply_conic_constraint(q, ref_q, axis, k): + """ lee 2000 p. 48""" + q0 = ref_q + w = axis + #rel_q = quaternion_multiply(quaternion_inverse(q0), q) + #rel_q /np.linalg.norm(rel_q) + w_prime = rotate_vector_by_quaternion(w, q) + w_prime = normalize(w_prime) + phi = acos(np.dot(w, w_prime)) + if 0 < phi > k: # apply + delta_phi = k - phi + #print(delta_phi) + v = np.cross(w, w_prime) + delta_q = quaternion_about_axis(delta_phi, v) + q = quaternion_multiply(delta_q, q) + return q + + +def apply_axial_constraint(q, ref_q, axis, k1, k2): + """ lee 2000 p. 48""" + q0 = ref_q + w = axis + #rel_q = quaternion_multiply(quaternion_inverse(q0), q) + #rel_q = normalize(rel_q) + w_prime = rotate_vector_by_quaternion(w, q) + v = np.cross(w, w_prime) + phi = acos(np.dot(w, w_prime)) + + #remove rotation around v + # psi_q = exp(-phi*v) + inv_phi_q = quaternion_about_axis(-phi, v) + psi_q = quaternion_multiply(inv_phi_q, ref_q) + + # get angle around w from psi_q + w_prime2 = rotate_vector_by_quaternion(w, psi_q) + w_prime2 = normalize(w_prime2) + psi = acos(np.dot(w, w_prime2)) + if k1 < psi > k2: # apply + delta_psi1 = k1 - psi + delta_psi2 = k2 - psi + delta_psi = min(delta_psi1, delta_psi2) + delta_q = quaternion_about_axis(delta_psi, w) + q = quaternion_multiply(delta_q, q) + print("apply", psi) + return q + +def apply_axial_constraint2(q, ref_q, axis, min_angle, max_angle): + """ get delta orientation + do axis decomposition + restrict angle between min and max + """ + q = normalize(q) + inv_ref_q = quaternion_inverse(ref_q) + inv_ref_q = normalize(inv_ref_q) + delta_q = quaternion_multiply(inv_ref_q, q) + delta_q = normalize(delta_q) + swing_q, twist_q = swing_twist_decomposition(delta_q, axis) + swing_q = normalize(swing_q) + twist_q = normalize(twist_q) + v, a = quaternion_to_axis_angle(twist_q) + print("apply axial", q) + v2, a2 = quaternion_to_axis_angle(swing_q) + sign = 1 + if np.sum(v) < 0: + sign = -1 + a *= sign + print("twist swing", v, np.degrees(a), v2, np.degrees(a2)) + print("boundary", min_angle, max_angle) + a = max(a, min_angle) + a = min(a, max_angle) + new_twist_q = quaternion_about_axis(a, axis) + new_twist_q = normalize(new_twist_q) + delta_q = quaternion_multiply(swing_q, new_twist_q) + delta_q = normalize(delta_q) + q = quaternion_multiply(ref_q, delta_q) + q = normalize(q) + return q + + +def apply_head_constraint(q, ref_q, axis, t_min_angle, t_max_angle, s_min_angle, s_max_angle): + """ get delta orientation + do axis decomposition + restrict angle between min and max + """ + q = normalize(q) + inv_ref_q = quaternion_inverse(ref_q) + inv_ref_q = normalize(inv_ref_q) + delta_q = quaternion_multiply(inv_ref_q, q) + delta_q = normalize(delta_q) + swing_q, twist_q = swing_twist_decomposition(delta_q, axis) + swing_q = normalize(swing_q) + twist_q = normalize(twist_q) + v, a = quaternion_to_axis_angle(twist_q) + sign = 1 + if np.sum(v) < 0: + sign = -1 + a *= sign + v*=sign + #print("apply axial", q) + v2, a2 = quaternion_to_axis_angle(swing_q) + sign = 1 + if np.sum(v2) < 0: + sign = -1 + a2 *= sign + v2*= sign + #print("twist swing", v, np.degrees(a), v2, np.degrees(a2)) + #print("boundary", min_angle, max_angle) + a = max(a, t_min_angle) + a = min(a, t_max_angle) + new_twist_q = quaternion_about_axis(a, axis) + new_twist_q = normalize(new_twist_q) + a2 = max(a2, s_min_angle) + a2 = min(a2, s_max_angle) + new_swing_q = quaternion_about_axis(a2, v2) + new_swing_q = normalize(new_swing_q) + delta_q = quaternion_multiply(new_swing_q, new_twist_q) + delta_q = normalize(delta_q) + q = quaternion_multiply(ref_q, delta_q) + q = normalize(q) + return q + + +def apply_spherical_constraint(q, ref_q, axis, k): + """ lee 2000 p. 48""" + q0 = ref_q + w = axis + rel_q = quaternion_multiply(quaternion_inverse(q0), q) + rel_q /np.linalg.norm(rel_q) + w_prime = rotate_vector_by_quaternion(w, rel_q) + v = np.cross(w, w_prime) + v = normalize(v) + log_rel_q = log(rel_q) + phi = acos(np.dot(w, w_prime)) + if 2*np.linalg.norm(log_rel_q) > k: # apply + delta_phi = k - phi + delta_q = quaternion_about_axis(delta_phi, v) + delta_q = normalize(delta_q) + q = quaternion_multiply(delta_q, q) + return normalize(q) + +REFERENCE_QUATERNION = [1,0,0,0] + + +class HingeConstraint(object): + def __init__(self, axis, deg_angle_range): + self.axis = axis + self.angle_range = np.radians(deg_angle_range) + + def apply(self, q, parent_q): + #axis = rotate_vector_by_quaternion(self.axis, parent_q) + return apply_axial_constraint(q, parent_q, self.axis, self.angle_range[0], self.angle_range[1]) + + +class JointConstraint(object): + def __init__(self): + self.is_static = False + + @classmethod + def from_dict(cls, joint_name, data): + return JointConstraint() + + def get_axis(self): + return OPENGL_UP + + def apply(self, q): + return q + +class StaticConstraint(JointConstraint): + def __init__(self): + JointConstraint.__init__(self) + self.is_static = True + + @classmethod + def from_dict(cls, joint_name, data): + return StaticConstraint() + +class BallSocketConstraint(JointConstraint): + def __init__(self, axis, k): + JointConstraint.__init__(self) + self.axis = axis + self.k = k + + @classmethod + def from_dict(cls, joint_name, data): + axis = np.array(data["axis"]) + k = np.radians(data["k"]) + c = BallSocketConstraint(axis, k) + return c + + def apply(self, q): + ref_q = [1,0,0,0] + return apply_spherical_constraint(q, ref_q, self.axis, self.k) + + + def get_axis(self): + return self.axis + + +class ConeConstraint(JointConstraint): + def __init__(self, axis, k): + JointConstraint.__init__(self) + self.axis = axis + self.k = k + + @classmethod + def from_dict(cls, joint_name, data): + axis = np.array(data["axis"]) + k = np.radians(data["k"]) + c = ConeConstraint(axis, k) + return c + + def apply(self, q): + ref_q = [1, 0, 0, 0] + return apply_conic_constraint(q, ref_q, self.axis, self.k) + + def get_axis(self): + return self.axis + +class SpineConstraint(JointConstraint): + def __init__(self, ref_q, axis, twist_min, twist_max, swing_min, swing_max): + JointConstraint.__init__(self) + self.ref_q = ref_q + self.axis = axis + self.twist_min = twist_min + self.twist_max = twist_max + self.swing_min = swing_min + self.swing_max = swing_max + self.joint_name = "" + + @classmethod + def from_dict(cls, joint_name, data): + axis = np.array(data["axis"]) + tk1 = np.radians(data["tk1"]) + tk2 = np.radians(data["tk2"]) + sk1 = np.radians(data["sk1"]) + sk2 = np.radians(data["sk2"]) + ref_q = [1,0,0,0] + c = SpineConstraint(ref_q, axis, tk1, tk2, sk1, sk2) + c.joint_name = joint_name + return c + + def apply(self, q): + #print("apply spine constraint",self.joint_name, q) + #q = apply_spine_constraint(q, self.ref_q, self.axis, self.twist_min, self.twist_max, self.swing_min, self.swing_max) + #print() + return q + + def get_axis(self): + return self.axis + +class HeadConstraint(JointConstraint): + def __init__(self, ref_q, axis, twist_min, twist_max, swing_min, swing_max): + JointConstraint.__init__(self) + self.ref_q = ref_q + self.axis = axis + self.twist_min = twist_min + self.twist_max = twist_max + self.swing_min = swing_min + self.swing_max = swing_max + self.joint_name = "" + + @classmethod + def from_dict(cls, joint_name, data): + axis = np.array(data["axis"]) + tk1 = np.radians(data["tk1"]) + tk2 = np.radians(data["tk2"]) + sk1 = np.radians(data["sk1"]) + sk2 = np.radians(data["sk2"]) + ref_q = [1,0,0,0] + c = HeadConstraint(ref_q, axis, tk1, tk2, sk1, sk2) + c.joint_name = joint_name + return c + + def apply(self, q): + q = apply_head_constraint(q, self.ref_q, self.axis, self.twist_min, self.twist_max, self.swing_min, self.swing_max) + return q + + def get_axis(self): + return self.axis + +class HingeConstraint2(JointConstraint): + def __init__(self, swing_axis, twist_axis, deg_angle_range=None, verbose=False): + JointConstraint.__init__(self) + self.swing_axis = swing_axis + self.twist_axis = twist_axis + if deg_angle_range is not None: + self.angle_range = np.radians(deg_angle_range) + else: + self.angle_range = None + self.verbose = False + + @classmethod + def from_dict(cls, joint_name, data): + swing_axis = np.array(data["swing_axis"]) + twist_axis = np.array(data["twist_axis"]) + deg_angle_range = None + if "k1" in data and "k2" in data: + deg_angle_range = [data["k1"], data["k2"]] + c = HingeConstraint2(swing_axis, twist_axis, deg_angle_range) + return c + + def apply(self, q): + sq, tq = swing_twist_decomposition(q, self.swing_axis) + tv, ta = quaternion_to_axis_angle(tq) + if self.verbose: + print("before", np.degrees(ta), tv) + if self.angle_range is not None: + ta = max(ta, self.angle_range[0]) + ta = min(ta, self.angle_range[1]) + + if self.verbose: + print("after", np.degrees(ta), tv) + return quaternion_about_axis(ta, self.swing_axis) + + def apply_global(self, pm, q): + axis = np.dot(pm, self.axis) + axis = normalize(axis) + sq, tq = swing_twist_decomposition(q, axis) + tv, ta = quaternion_to_axis_angle(tq) + if self.verbose: + print("before", np.degrees(ta), tv) + if self.angle_range is not None: + ta = max(ta, self.angle_range[0]) + ta = min(ta, self.angle_range[1]) + + if self.verbose: + print("after", np.degrees(ta), tv) + return quaternion_about_axis(ta, self.swing_axis) + + def split(self,q): + axis = normalize(self.swing_axis) + sq, tq = swing_twist_decomposition(q, axis) + return sq, tq + + def split_correct(self,q): + axis = normalize(self.twist_axis) + sq, tq = swing_twist_decomposition(q, axis) + return sq, tq + + def get_axis(self): + return self.swing_axis + + +class ShoulderConstraint(JointConstraint): + """ combines conic and axial""" + def __init__(self, axis, k1,k2, k): + JointConstraint.__init__(self) + self.axis = axis + self.k1 = k1 + self.k2 = k2 + self.k = k + + @classmethod + def from_dict(cls, joint_name, data): + axis = np.array(data["axis"]) + k = np.radians(data["k"]) + k1 = np.radians(data["k1"]) + k2 = np.radians(data["k2"]) + #print("add shoulder socket constraint to", joint_name) + c = ShoulderConstraint(axis, k, k1, k2) + return c + + def apply(self, q): + ref_q = [1, 0, 0, 0] + q = apply_conic_constraint(q, ref_q, self.axis, self.k) + q = normalize(q) + #q = apply_axial_constraint(q, ref_q, self.axis, self.k1, self.k2) + return q + + def get_axis(self): + return self.axis + + + +CONSTRAINT_MAP= dict() +CONSTRAINT_MAP["static"] = StaticConstraint +CONSTRAINT_MAP["hinge"] = HingeConstraint2 +CONSTRAINT_MAP["ball"] = BallSocketConstraint +CONSTRAINT_MAP["cone"] = ConeConstraint +CONSTRAINT_MAP["shoulder"] = ShoulderConstraint +CONSTRAINT_MAP["head"] = HeadConstraint +CONSTRAINT_MAP["spine"] = SpineConstraint + diff --git a/build/lib/anim_utils/animation_data/motion_blending.py b/build/lib/anim_utils/animation_data/motion_blending.py new file mode 100644 index 0000000..ddcd8de --- /dev/null +++ b/build/lib/anim_utils/animation_data/motion_blending.py @@ -0,0 +1,615 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +from transformations import quaternion_slerp +import numpy as np +from copy import deepcopy +from .constants import LEN_QUAT, LEN_ROOT_POS + + +BLEND_DIRECTION_FORWARD = 0 +BLEND_DIRECTION_BACKWARD = 1 + + +def smooth_root_positions(positions, window): + h_window = int(window/2) + smoothed_positions = [] + n_pos = len(positions) + for idx, p in enumerate(positions): + start = max(idx-h_window, 0) + end = min(idx + h_window, n_pos) + #print start, end, positions[start:end] + avg_p = np.average(positions[start:end], axis=0) + smoothed_positions.append(avg_p) + return smoothed_positions + + +def blend_quaternion(a, b, w): + return quaternion_slerp(a, b, w, spin=0, shortestpath=True) + + +def smooth_joints_around_transition_using_slerp(quat_frames, joint_param_indices, discontinuity, window): + h_window = int(window/2) + start_frame = max(discontinuity-h_window, 0) + end_frame = min(discontinuity+h_window, len(quat_frames)-1) + start_window = discontinuity-start_frame + end_window = end_frame-discontinuity + if start_window > 0: + create_transition_for_joints_using_slerp(quat_frames, joint_param_indices, start_frame, discontinuity, start_window, BLEND_DIRECTION_FORWARD) + if end_window > 0: + create_transition_for_joints_using_slerp(quat_frames, joint_param_indices, discontinuity, end_frame, end_window, BLEND_DIRECTION_BACKWARD) + + + +def create_transition_for_joints_using_slerp(quat_frames, joint_param_indices, start_frame, end_frame, steps, direction=BLEND_DIRECTION_FORWARD): + new_quats = create_frames_using_slerp(quat_frames, start_frame, end_frame, steps, joint_param_indices) + for i in range(steps): + if direction == BLEND_DIRECTION_FORWARD: + t = float(i)/steps + else: + t = 1.0 - (i / steps) + old_quat = quat_frames[start_frame+i, joint_param_indices] + blended_quat = blend_quaternion(old_quat, new_quats[i], t) + quat_frames[start_frame + i, joint_param_indices] = blended_quat + return quat_frames + + +def smooth_quaternion_frames_using_slerp_(quat_frames, joint_parameter_indices, event_frame, window): + start_frame = event_frame-window/2 + end_frame = event_frame+window/2 + start_q = quat_frames[start_frame, joint_parameter_indices] + end_q = quat_frames[end_frame, joint_parameter_indices] + for i in range(window): + t = float(i)/window + #nlerp_q = self.nlerp(start_q, end_q, t) + slerp_q = quaternion_slerp(start_q, end_q, t, spin=0, shortestpath=True) + #print "slerp",start_q, end_q, t, nlerp_q, slerp_q + quat_frames[start_frame+i, joint_parameter_indices] = slerp_q + + +def smooth_quaternion_frames_using_slerp_old(quat_frames, joint_param_indices, event_frame, window): + h_window = window/2 + start_frame = max(event_frame-h_window, 0) + end_frame = min(event_frame+h_window, quat_frames.shape[0]-1) + # create transition frames + from_start_to_event = create_frames_using_slerp(quat_frames, start_frame, event_frame, h_window, joint_param_indices) + from_event_to_end = create_frames_using_slerp(quat_frames, event_frame, end_frame, h_window, joint_param_indices) + + #blend transition frames with original frames + steps = event_frame-start_frame + for i in range(steps): + t = float(i)/steps + quat_frames[start_frame+i, joint_param_indices] = blend_quaternion(quat_frames[start_frame+i, joint_param_indices], from_start_to_event[i], t) + + steps = end_frame-event_frame + for i in range(steps): + t = 1.0-(i/steps) + quat_frames[event_frame+i, joint_param_indices] = blend_quaternion(quat_frames[start_frame+i, joint_param_indices], from_event_to_end[i], t) + + +def smooth_quaternion_frames_using_slerp_overwrite_frames(quat_frames, joint_param_indices, event_frame, window): + h_window = window/2 + start_frame = max(event_frame-h_window, 0) + end_frame = min(event_frame+h_window, quat_frames.shape[0]-1) + create_transition_using_slerp(quat_frames, start_frame, event_frame, joint_param_indices) + create_transition_using_slerp(quat_frames, event_frame, end_frame, joint_param_indices) + + +def blend_frames(quat_frames, start, end, new_frames, joint_parameter_indices): + steps = end-start + for i in range(steps): + t = i/steps + quat_frames[start+i, joint_parameter_indices] = blend_quaternion(quat_frames[start+i, joint_parameter_indices], new_frames[i], t) + + +def create_frames_using_slerp(quat_frames, start_frame, end_frame, steps, joint_parameter_indices): + start_q = quat_frames[start_frame, joint_parameter_indices] + end_q = quat_frames[end_frame, joint_parameter_indices] + frames = [] + for i in range(steps): + t = float(i)/steps + slerp_q = quaternion_slerp(start_q, end_q, t, spin=0, shortestpath=True) + frames.append(slerp_q) + return frames + + +def create_transition_using_slerp(quat_frames, start_frame, end_frame, q_indices): + start_q = quat_frames[start_frame, q_indices] + end_q = quat_frames[end_frame, q_indices] + steps = end_frame-start_frame + for i in range(steps): + t = float(i)/steps + slerp_q = quaternion_slerp(start_q, end_q, t, spin=0, shortestpath=True) + quat_frames[start_frame+i, q_indices] = slerp_q + +def create_transition_using_slerp_forward(quat_frames, start_frame, end_frame, q_indices): + end_q = quat_frames[end_frame, q_indices] + steps = end_frame - start_frame + for i in range(steps): + t = float(i) / steps + start_q = quat_frames[i, q_indices] + slerp_q = quaternion_slerp(start_q, end_q, t, spin=0, shortestpath=True) + quat_frames[start_frame + i, q_indices] = slerp_q + + +def create_transition_using_slerp_backward(quat_frames, start_frame, end_frame, q_indices): + start_q = quat_frames[start_frame, q_indices] + steps = end_frame - start_frame + for i in range(steps): + t = float(i) / steps + end_q = quat_frames[i, q_indices] + slerp_q = quaternion_slerp(start_q, end_q, t, spin=0, shortestpath=True) + quat_frames[start_frame + i, q_indices] = slerp_q + +def smooth_quaternion_frames(frames, discontinuity, window=20, include_root=True): + """ Smooth quaternion frames given discontinuity frame + + Parameters + ---------- + frames: list + \tA list of quaternion frames + discontinuity : int + The frame where the discontinuity is. (e.g. the transitionframe) + window : (optional) int, default is 20 + The smoothing window + include_root: (optional) bool, default is False + \tSets whether or not smoothing is applied on the x and z dimensions of the root translation + Returns + ------- + None. + """ + n_joints = int((len(frames[0]) - 3) / 4) + # smooth quaternion + n_frames = len(frames) + for i in range(n_joints): + for j in range(n_frames - 1): + start = 3 + i * 4 + end = 3 + (i + 1) * 4 + q1 = np.array(frames[j][start: end]) + q2 = np.array(frames[j + 1][start:end]) + if np.dot(q1, q2) < 0: + frames[j + 1][start:end] = -frames[j + 1][start:end] + + smoothing_factors = generate_smoothing_factors(discontinuity, window, n_frames) + d = int(discontinuity) + dofs = list(range(len(frames[0])))[3:] + if include_root: + dofs = [0,1,2] + dofs + else: + dofs = [1] + dofs + new_frames = np.array(frames) + for dof_idx in dofs: + curve = np.array(frames[:, dof_idx]) # extract dof curve + magnitude = curve[d] - curve[d - 1] + #print(dof_idx, magnitude, d) + new_frames[:, dof_idx] = curve + (magnitude * smoothing_factors) + return new_frames + + + +def slerp_quaternion_frame(frame_a, frame_b, weight): + frame_a = np.asarray(frame_a) + frame_b = np.asarray(frame_b) + assert len(frame_a) == len(frame_b) + n_joints = int((len(frame_a) - 3) / 4) + new_frame = np.zeros(len(frame_a)) + # linear interpolate root translation + new_frame[:3] = (1 - weight) * frame_a[:3] + weight * frame_b[:3] + for i in range(n_joints): + new_frame[3+ i*4 : 3 + (i+1) * 4] = quaternion_slerp(frame_a[3 + i*4 : 3 + (i+1) * 4], + frame_b[3 + i*4 : 3 + (i+1) * 4], + weight) + return new_frame + + +def smooth_quaternion_frames_with_slerp(frames, discontinuity, window=20): + n_frames = len(frames) + d = float(discontinuity) + ref_pose = slerp_quaternion_frame(frames[int(d)-1], frames[int(d)], 0.5) + w = float(window) + new_quaternion_frames = [] + for f in range(n_frames): + if f < d - w: + new_quaternion_frames.append(frames[f]) + elif d - w <= f < d: + tmp = (f - d + w) / w + weight = 2 * (0.5 * tmp ** 2) + new_quaternion_frames.append(slerp_quaternion_frame(frames[f], ref_pose, weight)) + elif d <= f <= d + w: + tmp = (f - d + w) / w + weight = 2 * (0.5 * tmp ** 2 - 2 * tmp + 2) + new_quaternion_frames.append(slerp_quaternion_frame(frames[f], ref_pose, weight)) + else: + new_quaternion_frames.append(frames[f]) + return np.asarray(new_quaternion_frames) + + +def smooth_quaternion_frames_with_slerp2(skeleton, frames, d, smoothing_window): + ''' + + :param new_frames (numpy.array): n_frames * n_dims + :param prev_frames (numpy.array): n_frames * n_dims + :param smoothing_window: + :return: + ''' + smooth_translation_in_quat_frames(frames, d, smoothing_window) + for joint_idx, joint_name in enumerate(skeleton.animated_joints): + start = joint_idx*4+3 + joint_indices = list(range(start, start+4)) + smooth_joints_around_transition_using_slerp(frames, joint_indices, d, smoothing_window) + return frames + + +def generate_smoothing_factors(discontinuity, window, n_frames): + """ Generate curve of smoothing factors + """ + d = float(discontinuity) + w = float(window) + smoothing_factors = [] + for f in range(n_frames): + value = 0.0 + if d - w <= f < d: + tmp = (f - d + w) / w + value = 0.5 * tmp ** 2 + elif d <= f <= d + w: + tmp = (f - d + w) / w + value = -0.5 * tmp ** 2 + 2 * tmp - 2 + smoothing_factors.append(value) + return np.array(smoothing_factors) + + +def smooth_quaternion_frames_joint_filter(skeleton, frames, discontinuity, joints, window=20): + """ Smooth quaternion frames given discontinuity frame + + Parameters + ---------- + skeleton: Skeleton + frames: list + \tA list of quaternion frames + discontinuity : int + The frame where the discontinuity is. (e.g. the transitionframe) + joints: list + \tA list of strings + window : (optional) int, default is 20 + The smoothing window + Returns + ------- + None. + """ + + n_frames = len(frames) + # generate curve of smoothing factors + smoothing_factors = generate_smoothing_factors(discontinuity, window, n_frames) + + # align quaternions and extract dofs + dof_filter_list = [] + if skeleton.root in joints: + dof_filter_list += [0,1,2] + for idx, j in enumerate(joints): + j_idx = skeleton.animated_joints.index(j) + q_start_idx = 3 + j_idx * 4 + q_end_idx = q_start_idx + 4 + dof_filter_list += [q_start_idx, q_start_idx + 1, q_start_idx + 2, q_start_idx + 3] + for f in range(n_frames - 1): + q1 = np.array(frames[f][q_start_idx: q_end_idx]) + q2 = np.array(frames[f + 1][q_start_idx:q_end_idx]) + if np.dot(q1, q2) < 0: + frames[f + 1][q_start_idx:q_end_idx] = -q2 + d = int(discontinuity) + new_frames = np.array(frames) + for dof_idx in dof_filter_list: + current_curve = np.array(frames[:, dof_idx]) # extract dof curve + magnitude = current_curve[d] - current_curve[d - 1] + new_curve = current_curve + (magnitude * smoothing_factors) + new_frames[:, dof_idx] = new_curve + return new_frames + +def smooth_quaternion_frames2(skeleton, frames, discontinuity, window=20): + """ Smooth quaternion frames given discontinuity frame + + Parameters + ---------- + skeleton: Skeleton + frames: list + \tA list of quaternion frames + discontinuity : int + The frame where the discontinuity is. (e.g. the transitionframe) + window : (optional) int, default is 20 + The smoothing window + Returns + ------- + None. + """ + + n_frames = len(frames) + smoothing_factors = generate_smoothing_factors(discontinuity, window, n_frames) + d = int(discontinuity) + new_frames = np.array(frames) + for idx in range(len(skeleton.animated_joints)): + o = idx *4 + 3 + dofs = [o, o+1, o+2, o+3] + q1 = frames[d, dofs] + q2 = frames[d+1, dofs] + if np.dot(q1, q2) < 0: + q2 = -q2 + magnitude = q1 - q2 + new_frames[:, dofs] += magnitude * smoothing_factors + return new_frames + +def smooth_translation_in_quat_frames(frames, discontinuity, window=20, only_height=False): + """ Smooth translation in quaternion frames given discontinuity frame + + Parameters + ---------- + frames: list + \tA list of quaternion frames + discontinuity : int + The frame where the discontinuity is. (e.g. the transitionframe) + window : (optional) int, default is 20 + The smoothing window + Returns + ------- + None. + """ + + n_frames = len(frames) + # generate curve of smoothing factors + d = int(discontinuity) + smoothing_factors = generate_smoothing_factors(d, window, n_frames) + if only_height: + dofs = [1] + else: + dofs = [0, 1, 2] + for dof_idx in dofs: + current_curve = np.array(frames[:, dof_idx]) # extract dof curve + magnitude = current_curve[d] - current_curve[d - 1] + new_curve = current_curve + (magnitude * smoothing_factors) + frames[:, dof_idx] = new_curve + return frames + + +def smooth_root_translation_around_transition(frames, d, window): + hwindow = int(window/2.0) + root_pos1 = frames[d-1, :3] + root_pos2 = frames[d, :3] + root_pos = (root_pos1 + root_pos2)/2 + start_idx = d-hwindow + end_idx = d + hwindow + start = frames[start_idx, :3] + end = root_pos + for i in range(hwindow): + t = float(i) / hwindow + frames[start_idx + i, :3] = start * (1 - t) + end * t + start = root_pos + end = frames[end_idx, :3] + for i in range(hwindow): + t = float(i) / hwindow + frames[d + i, :3] = start * (1 - t) + end * t + + +def linear_blending(ref_pose, quat_frames, skeleton, weights, joint_list=None): + ''' + Apply linear blending on quaternion motion data + :param ref_pose (numpy.array) + :param quat_frames: + :param skeleton (morphablegraphs.animation_data.Skeleton): + :param weights (numpy.array): weights used for slerp + :param joint_list (list): animated joint to be blended + :return: + ''' + if joint_list is None: + joint_list = skeleton.animated_joints + new_frames = deepcopy(quat_frames) + for i in range(len(quat_frames)): + for joint in joint_list: + joint_index = skeleton.nodes[joint].quaternion_frame_index + start_index = LEN_ROOT_POS + LEN_QUAT * joint_index + end_index = LEN_ROOT_POS + LEN_QUAT * (joint_index + 1) + ref_q = ref_pose[start_index: end_index] + motion_q = quat_frames[i, start_index: end_index] + new_frames[i, start_index: end_index] = quaternion_slerp(ref_q, motion_q, weights[i]) + return new_frames + + +def blend_quaternion_frames_linearly(new_frames, prev_frames, skeleton, smoothing_window=None): + ''' + Blend new frames linearly based on the last pose of previous frames + :param new_frames (Quaternion Frames): + :param prev_frames (Quaternion Frames): + :param skeleton (morphablegraphs.animation_data.Skeleton): + :param smoothing_window (int): smoothing window decides how many frames will be blended, if is None, then blend all + :return: + ''' + if smoothing_window is not None and smoothing_window != 0: + slerp_weights = np.linspace(0, 1, smoothing_window) + else: + slerp_weights = np.linspace(0, 1, len(new_frames)) + + return linear_blending(prev_frames[-1], new_frames, skeleton, slerp_weights) + + +def blend_between_frames(skeleton, frames, transition_start, transition_end, joint_list, window): + for c_joint in joint_list: + idx = skeleton.animated_joints.index(c_joint) * 4 + 3 + j_indices = [idx, idx + 1, idx + 2, idx + 3] + start_q = frames[transition_start][j_indices] + end_q = frames[transition_end][j_indices] + for i in range(window): + t = float(i) / window + t = (float(t) / window) + slerp_q = quaternion_slerp(start_q, end_q, t, spin=0, shortestpath=True) + frames[transition_start + i][j_indices] = slerp_q + + +def generated_blend(start_q, end_q, window): + blend = np.zeros((window, 4)) + for i in range(window): + t = float(i) / window + slerp_q = quaternion_slerp(start_q, end_q, t, spin=0, shortestpath=True) + blend[i] = slerp_q + return blend + + +def generate_blended_frames(skeleton, frames, start, end, joint_list, window): + blended_frames = deepcopy(frames[:]) + for c_joint in joint_list: + idx = skeleton.animated_joints.index(c_joint) * 4 + 3 + j_indices = [idx, idx + 1, idx + 2, idx + 3] + start_q = frames[start][j_indices] + end_q = frames[end][j_indices] + blended_qs = generated_blend(start_q, end_q, window) + for fi, q in enumerate(blended_qs): + blended_frames[start+fi][j_indices] = q + return blended_frames + + +def blend_quaternions_to_next_step(skeleton, frames, frame_idx, plant_joint, swing_joint, ik_chains, window): + start = frame_idx - window # end of blending range + end = frame_idx # modified frame + plant_ik_chain = ik_chains[plant_joint] + swing_ik_chain = ik_chains[swing_joint] + joint_list = [skeleton.root, "pelvis", plant_ik_chain["root"], plant_ik_chain["joint"], plant_joint, swing_ik_chain["root"], swing_ik_chain["joint"], swing_joint] + blend_between_frames(skeleton, frames, start, end, joint_list, window) + + +def interpolate_frames(skeleton, frames_a, frames_b, joint_list, start, end): + blended_frames = deepcopy(frames_a[:]) + window = end - start + for joint in joint_list: + idx = skeleton.animated_joints.index(joint) * 4 + 3 + j_indices = [idx, idx + 1, idx + 2, idx + 3] + for f in range(window): + t = (float(f) / window) + q_a = frames_a[start + f][j_indices] + q_b = frames_b[start + f][j_indices] + blended_frames[start + f][j_indices] = quaternion_slerp(q_a, q_b, t, spin=0, shortestpath=True) + return blended_frames + + +def blend_towards_next_step_linear_with_original(skeleton, frames, start, end, joint_list): + new_frames = generate_blended_frames(skeleton, frames, start, end, joint_list, end-start) + new_frames2 = interpolate_frames(skeleton, frames, new_frames, joint_list, start, end) + return new_frames2 + + +def generate_frame_using_iterative_slerp(skeleton, motions, frame_idx, weights): + """src: https://gamedev.stackexchange.com/questions/62354/method-for-interpolation-between-3-quaternions + """ + frame = None + w_sum = 0.0 + for n, w in weights.items(): + if frame is None: + frame = np.zeros(len(motions[n][0])) + frame[:] = motions[n][frame_idx][:] + w_sum += w + else: + new_w_sum = w_sum + w + if new_w_sum > 0: + w_a = w_sum / new_w_sum + w_b = w / new_w_sum + frame_b = motions[n][frame_idx] + frame[:3] = w_a * frame[:3] + w_b * frame_b[:3] + for idx, j in enumerate(skeleton.animated_joints): + q_start_idx = (idx * 4) + 3 + q_end_idx = q_start_idx + 4 + q_a = np.array(frame[q_start_idx:q_end_idx]) + q_b = frame_b[q_start_idx:q_end_idx] + new_q = quaternion_slerp(q_a, q_b, w_b) + new_q /= np.linalg.norm(new_q) + frame[q_start_idx:q_end_idx] = new_q + w_sum = new_w_sum + return frame + + +def generate_frame_using_iterative_slerp2(skeleton, frames, weights): + """src: https://gamedev.stackexchange.com/questions/62354/method-for-interpolation-between-3-quaternions + """ + frame = None + w_sum = 0.0 + for frame_idx, w in enumerate(weights): + if frame is None: + frame = frames[frame_idx][:] + w_sum += w + else: + new_w_sum = w_sum + w + if new_w_sum > 0: + w_a = w_sum / new_w_sum + w_b = w / new_w_sum + frame_b = frames[frame_idx][:] + frame[:3] = w_a * frame[:3] + w_b * frame_b[:3] + for idx, j in enumerate(skeleton.animated_joints): + q_start_idx = (idx * 4) + 3 + q_end_idx = q_start_idx + 4 + q_a = np.array(frame[q_start_idx:q_end_idx]) + q_b = frame_b[q_start_idx:q_end_idx] + new_q = quaternion_slerp(q_a, q_b, w_b) + new_q /= np.linalg.norm(new_q) + frame[q_start_idx:q_end_idx] = new_q + w_sum = new_w_sum + return frame + + + +def smooth_euler_frames(euler_frames, discontinuity, window=20): + """ Smooth a function around the given discontinuity frame + + Parameters + ---------- + motion : AnimationController + The motion to be smoothed + ATTENTION: This function changes the values of the motion + discontinuity : int + The frame where the discontinuity is. (e.g. the transitionframe) + window : (optional) int, default is 20 + The smoothing window + Returns + ------- + None. + """ + d = float(discontinuity) + s = float(window) + + smoothing_faktors = [] + for f in range(len(euler_frames)): + value = 0.0 + if d - s <= f < d: + tmp = ((f - d + s) / s) + value = 0.5 * tmp ** 2 + elif d <= f <= d + s: + tmp = ((f - d + s) / s) + value = -0.5 * tmp ** 2 + 2 * tmp - 2 + + smoothing_faktors.append(value) + + smoothing_faktors = np.array(smoothing_faktors) + new_euler_frames = [] + for i in range(len(euler_frames[0])): + current_value = euler_frames[:, i] + magnitude = current_value[int(d)] - current_value[int(d) - 1] + if magnitude > 180: + magnitude -= 360 + elif magnitude < -180: + magnitude += 360 + new_value = current_value + (magnitude * smoothing_faktors) + new_euler_frames.append(new_value) + new_euler_frames = np.array(new_euler_frames).T + return new_euler_frames diff --git a/build/lib/anim_utils/animation_data/motion_concatenation.py b/build/lib/anim_utils/animation_data/motion_concatenation.py new file mode 100644 index 0000000..13a3476 --- /dev/null +++ b/build/lib/anim_utils/animation_data/motion_concatenation.py @@ -0,0 +1,843 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import numpy as np +from copy import deepcopy +from transformations import euler_from_matrix, quaternion_matrix, quaternion_about_axis, quaternion_multiply, quaternion_from_matrix, quaternion_from_euler, quaternion_slerp, euler_matrix, quaternion_inverse +from .utils import pose_orientation_general, transform_point, rotation_order_to_string, euler_substraction, point_to_euler_angle, euler_angles_to_rotation_matrix, get_rotation_angle, point_rotation_by_quaternion, LEN_QUAT, LEN_EULER, LEN_ROOT_POS, pose_orientation_euler, pose_up_vector_euler +from .constants import DEFAULT_SMOOTHING_WINDOW_SIZE, DEFAULT_ROTATION_ORDER +from .quaternion_frame import euler_to_quaternion, quaternion_to_euler +from .motion_blending import smooth_quaternion_frames_with_slerp, smooth_quaternion_frames, blend_quaternion_frames_linearly, smooth_quaternion_frames_with_slerp2, smooth_euler_frames +from ..motion_editing.motion_grounding import create_grounding_constraint_from_frame, generate_ankle_constraint_from_toe, interpolate_constraints +from ..motion_editing.analytical_inverse_kinematics import AnalyticalLimbIK +from ..motion_editing.utils import normalize, generate_root_constraint_for_two_feet, smooth_root_translation_at_start, smooth_root_translation_at_end, project_on_intersection_circle, global_position_to_root_translation +from .motion_blending import blend_between_frames, smooth_translation_in_quat_frames, generate_blended_frames, interpolate_frames, smooth_root_translation_around_transition, blend_quaternions_to_next_step, smooth_quaternion_frames_joint_filter, blend_towards_next_step_linear_with_original, smooth_root_positions + +ALIGNMENT_MODE_FAST = 0 +ALIGNMENT_MODE_PCL = 1 + +def concatenate_frames(prev_frames, new_frames): + frames = prev_frames.tolist() + for idx in range(1, len(new_frames)): # skip first frame + frames.append(new_frames[idx]) + frames = np.array(frames) + return frames + + +def convert_quat_frame_to_point_cloud(skeleton, frame, joints=None): + points = list() + if joints is None: + joints = [k for k, n in list(skeleton.nodes.items()) if len(n.children) > 0 and "Bip" not in n.node_name] + for j in joints: + p = skeleton.nodes[j].get_global_position(frame) + points.append(p) + return points + + +def _align_point_clouds_2D(a, b, weights): + ''' + Finds aligning 2d transformation of two equally sized point clouds. + from Motion Graphs paper by Kovar et al. + Parameters + --------- + *a: list + \t point cloud + *b: list + \t point cloud + *weights: list + \t weights of correspondences + Returns + ------- + *theta: float + \t angle around y axis in radians + *offset_x: float + \t + *offset_z: float + + ''' + if len(a) != len(b): + raise ValueError("two point cloud should have the same number points: "+str(len(a))+","+str(len(b))) + n_points = len(a) + numerator_left = 0 + denominator_left = 0 + weighted_sum_a_x = 0 + weighted_sum_b_x = 0 + weighted_sum_a_z = 0 + weighted_sum_b_z = 0 + sum_of_weights = 0.0 + # if not weights: + # weight = 1.0/n_points # todo set weight base on joint level + for index in range(n_points): + numerator_left += weights[index] * (a[index][0] * b[index][2] - + b[index][0] * a[index][2]) + denominator_left += weights[index] * (a[index][0] * b[index][0] + + a[index][2] * b[index][2]) + sum_of_weights += weights[index] + weighted_sum_a_x += weights[index] * a[index][0] + weighted_sum_b_x += weights[index] * b[index][0] + weighted_sum_a_z += weights[index] * a[index][2] + weighted_sum_b_z += weights[index] * b[index][2] + numerator_right = 1.0 / sum_of_weights * \ + (weighted_sum_a_x * weighted_sum_b_z - + weighted_sum_b_x * weighted_sum_a_z) + numerator = numerator_left - numerator_right + denominator_right = 1.0 / sum_of_weights * \ + (weighted_sum_a_x * weighted_sum_b_x + + weighted_sum_a_z * weighted_sum_b_z) + denominator = denominator_left - denominator_right + theta = np.arctan2(numerator, denominator) + offset_x = (weighted_sum_a_x - weighted_sum_b_x * + np.cos(theta) - weighted_sum_b_z * np.sin(theta)) / sum_of_weights + offset_z = (weighted_sum_a_z + weighted_sum_b_x * + np.sin(theta) - weighted_sum_b_z * np.cos(theta)) / sum_of_weights + + return theta, offset_x, offset_z + + +def align_euler_frames(euler_frames, + frame_idx, + ref_orientation_euler): + print("Deprecation Warning: Function marked as Deprecated!") + pass + new_euler_frames = deepcopy(euler_frames) + ref_quat = euler_to_quaternion(ref_orientation_euler) + root_rot_angles = euler_frames[frame_idx][3:6] + root_rot_quat = euler_to_quaternion(root_rot_angles) + quat_diff = quaternion_multiply(ref_quat, quaternion_inverse(root_rot_quat)) + for euler_frame in new_euler_frames: + root_trans = euler_frame[:3] + new_root_trans = point_rotation_by_quaternion(root_trans, quat_diff) + euler_frame[:3] = new_root_trans + root_rot_angles = euler_frame[3:6] + root_rot_quat = euler_to_quaternion(root_rot_angles) + + new_root_quat = quaternion_multiply(quat_diff, root_rot_quat) + new_root_euler = quaternion_to_euler(new_root_quat) + euler_frame[3:6] = new_root_euler + return new_euler_frames + + +def translate_euler_frames(euler_frames, frame_idx, ref_position, root_joint, skeleton): + """ + translate euler frames to ref_position, tralsation is 2D on the ground + :param euler_frames: + :param frame_idx: + :param ref_position: + :return: + """ + root_pos = skeleton.nodes[root_joint].get_global_position_from_euler(euler_frames[frame_idx]) + if len(ref_position) == 2: + offset = np.array([ref_position[0] - root_pos[0], 0, ref_position[1] - root_pos[2]]) + elif len(ref_position) == 3: + offset = ref_position - root_pos + offset[1] = 0 + else: + raise ValueError('the length of reference position is not correct! ') + rotation = [0, 0, 0] + return transform_euler_frames(euler_frames, rotation, offset) + + +def rotate_euler_frame(euler_frame, ref_orientation, body_plane_joints, skeleton): + forward = pose_orientation_general(euler_frame, body_plane_joints, skeleton) + rot_angle = get_rotation_angle(ref_orientation, forward) + translation = np.array([0, 0, 0]) + return transform_euler_frame(euler_frame, [0, rot_angle, 0], translation) + + +def rotate_euler_frames(euler_frames, frame_idx, ref_orientation, body_plane_joints, skeleton, rotation_order=None): + """ + Rotate a list of euler frames using the same rotation angle + :param euler_frames: a list of euler frames to be rotated + :param frame_idx: frame which uses to calculate rotation anlge + :param ref_orientation: reference orientation for alignment + :param rotation_order: define the rotation order for euler angles to calculate rotation matrix + :global_rotation: if true, the rotation is also applied to the global position. If false, the rotation will not be applied to global position + :return rotated_frames: a list of rotated euler frames + """ + forward = pose_orientation_general(euler_frames[frame_idx], + body_plane_joints, + skeleton) + rot_angle = get_rotation_angle(ref_orientation, forward) + translation = np.array([0, 0, 0]) + rotated_frames = transform_euler_frames(euler_frames, + [0, rot_angle, 0], + translation, + rotation_order) + return rotated_frames + + +def rotate_euler_frames_about_x_axis(euler_frames, frame_idx, ref_upvector): + sample_upvector = pose_up_vector_euler(euler_frames[frame_idx]) + rot_angle = get_rotation_angle(ref_upvector, sample_upvector) + translation = np.array([0, 0, 0]) + rotated_frames = transform_euler_frames(euler_frames, + [rot_angle, 0, 0], + translation) + return rotated_frames + + + +def smoothly_concatenate(euler_frames_a, euler_frames_b, window_size=20): + euler_frames = np.concatenate((euler_frames_a, euler_frames_b), axis=0) + euler_frames = smooth_euler_frames(euler_frames, len(euler_frames_a), window_size) + return euler_frames + +def fast_euler_frames_transformation(euler_frames_a, + euler_frames_b): + dir_vec_a = pose_orientation_euler(euler_frames_a[-1]) + dir_vec_b = pose_orientation_euler(euler_frames_b[0]) + angle = get_rotation_angle(dir_vec_a, dir_vec_b) + offset_x = euler_frames_a[-1][0] - euler_frames_b[0][0] + offset_z = euler_frames_a[-1][2] - euler_frames_b[0][2] + offset = [offset_x, 0.0, offset_z] + return angle, offset + + + +def fast_euler_frames_alignment(euler_frames_a, + euler_frames_b, + smooth=True, + smoothing_window=DEFAULT_SMOOTHING_WINDOW_SIZE): + angle, offset = fast_euler_frames_transformation(euler_frames_a, euler_frames_b) + transformed_frames = transform_euler_frames(euler_frames_b, [0, angle, 0],offset) + # concatenate the quaternion_frames_a and transformed_framess + if smooth: + euler_frames = smoothly_concatenate(euler_frames_a, transformed_frames, window_size=smoothing_window) + else: + euler_frames = np.concatenate((euler_frames_a, + transformed_frames)) + return euler_frames + +def transform_quaternion_frames_legacy(quat_frames, angles, offset, rotation_order=None): + """ Applies a transformation on the root joint of a list quaternion frames. + Parameters + ---------- + *quat_frames: np.ndarray + \tList of frames where the rotation is represented as euler angles in degrees. + *angles: list of floats + \tRotation angles in degrees + *offset: np.ndarray + \tTranslation + """ + if rotation_order is None: + rotation_order = DEFAULT_ROTATION_ORDER + offset = np.array(offset) + if round(angles[0], 3) == 0 and round(angles[2], 3) == 0: + rotation_q = quaternion_about_axis(np.deg2rad(angles[1]), [0, 1, 0]) + else: + rotation_q = euler_to_quaternion(angles, rotation_order) + rotation_matrix = euler_angles_to_rotation_matrix(angles, rotation_order)[:3, :3] + for frame in quat_frames: + ot = frame[:3] + oq = frame[3:7] + frame[:3] = np.dot(rotation_matrix, ot) + offset + frame[3:7] = quaternion_multiply(rotation_q, oq) + return quat_frames + +def pose_orientation_quat(quaternion_frame): + """Estimate pose orientation from root orientation + """ + ref_offset = np.array([0, 0, 1, 1]) + rotmat = quaternion_matrix(quaternion_frame[3:7]) + rotated_point = np.dot(rotmat, ref_offset) + dir_vec = np.array([rotated_point[0], rotated_point[2]]) + dir_vec /= np.linalg.norm(dir_vec) + return dir_vec + +def fast_quat_frames_transformation(quaternion_frames_a, + quaternion_frames_b): + dir_vec_a = pose_orientation_quat(quaternion_frames_a[-1]) + dir_vec_b = pose_orientation_quat(quaternion_frames_b[0]) + angle = get_rotation_angle(dir_vec_a, dir_vec_b) + offset_x = quaternion_frames_a[-1][0] - quaternion_frames_b[0][0] + offset_z = quaternion_frames_a[-1][2] - quaternion_frames_b[0][2] + offset = [offset_x, 0.0, offset_z] + return angle, offset + + + +def get_orientation_vector_from_matrix(m, v=[0, 0, 1]): + p = np.dot(m, v) + dir_vec = np.array([p[0], p[2]]) + dir_vec /= np.linalg.norm(dir_vec) + return dir_vec + + +def get_global_node_orientation_vector(skeleton, node_name, frame, v=[0, 0, 1]): + v = np.array(v) + m = skeleton.nodes[node_name].get_global_matrix(frame)[:3, :3] + p = np.dot(m, v) + dir_vec = np.array([p[0], p[2]]) + dir_vec /= np.linalg.norm(dir_vec) + return dir_vec + + +REF_VECTOR = np.array([0,0,1]) +def get_node_aligning_2d_transform(skeleton, node_name, prev_frames, new_frames, ref_vector=REF_VECTOR): + """from last of prev frames to first of new frames""" + + m_a = skeleton.nodes[node_name].get_global_matrix(prev_frames[-1]) + m_b = skeleton.nodes[node_name].get_global_matrix(new_frames[0]) + dir_vec_a = get_orientation_vector_from_matrix(m_a[:3, :3], ref_vector) + dir_vec_b = get_orientation_vector_from_matrix(m_b[:3, :3], ref_vector) + angle = get_rotation_angle(dir_vec_a, dir_vec_b) + q = quaternion_about_axis(np.deg2rad(angle), [0, 1, 0]) + m = quaternion_matrix(q) + + first_frame_pos = [new_frames[0][0], new_frames[0][1], new_frames[0][2],1.0] + rotated_first_frame_pos = np.dot(m, first_frame_pos)[:3] + delta = prev_frames[-1][:3] - rotated_first_frame_pos[:3] + m[0, 3] = delta[0] + #m[1, 3] = delta[1] + m[2, 3] = delta[2] + return m + +def get_transform_from_point_cloud_alignment(skeleton, prev_frames, new_frames): + weights = skeleton.get_joint_weights() + p_a = convert_quat_frame_to_point_cloud(skeleton, prev_frames[-1]) + p_b = convert_quat_frame_to_point_cloud(skeleton, new_frames[0]) + theta, offset_x, offset_z = _align_point_clouds_2D(p_a, p_b, weights) + euler = [0, np.radians(theta), 0] + m = np.eye(4) + m[:3,:3] = euler_matrix(*euler)[:3,:3] + m[0,3] = offset_x + m[2,3] = offset_z + print("run point cloud alignment", theta, offset_x, offset_z, m) + return m + +def transform_quaternion_frames(frames, m, + translation_param_range=(0, 3), + orientation_param_range=(3, 7)): + """ possibly broken because not 3,7 is the root orientation but 7,11 + """ + q = quaternion_from_matrix(m) + for frame in frames: + ot = frame[translation_param_range[0]:translation_param_range[1]].tolist() + [1] + oq = frame[orientation_param_range[0]:orientation_param_range[1]] + transformed_t = np.dot(m, ot)[:3] + frame[translation_param_range[0]:translation_param_range[1]] = transformed_t + frame[orientation_param_range[0]:orientation_param_range[1]] = quaternion_multiply(q, oq) + return frames + + +def concatenate_frames_smoothly(new_frames, prev_frames, smoothing_window=0): + d = len(prev_frames) + frames = concatenate_frames(prev_frames, new_frames) + if smoothing_window > 0: + frames = smooth_quaternion_frames(frames, d, smoothing_window) + return frames + + +def concatenate_frames_with_slerp(new_frames, prev_frames, smoothing_window=0): + ''' + + :param new_frames (numpy.array): n_frames * n_dims + :param prev_frames (numpy.array): n_frames * n_dims + :param smoothing_window: + :return: + ''' + d = len(prev_frames) + frames = concatenate_frames(prev_frames, new_frames) + if smoothing_window > 0: + frames = smooth_quaternion_frames_with_slerp(frames, d, smoothing_window) + return frames + + +def align_quaternion_frames_automatically(skeleton, node_name, new_frames, prev_frames, alignment_mode=ALIGNMENT_MODE_FAST): + if alignment_mode == ALIGNMENT_MODE_FAST: + m = get_node_aligning_2d_transform(skeleton, node_name, prev_frames, new_frames) + else: + m = get_transform_from_point_cloud_alignment(skeleton, prev_frames, new_frames) + new_frames = transform_quaternion_frames(new_frames, m) + return new_frames + + +def align_quaternion_frames_automatically2(skeleton, node_name, new_frames, prev_frames): + angle, offset = fast_quat_frames_transformation(prev_frames, new_frames) + new_frames = transform_quaternion_frames_legacy(new_frames, [0, angle, 0], offset) + return new_frames + + +def align_quaternion_frames(skeleton, node_name, new_frames, prev_frames=None, start_pose=None): + """align quaternion frames based on previous frames or + given transformation + + Parameters + ---------- + * new_frames: list + A list of quaternion frames + * prev_frames: list + A list of quaternion frames + * transformation: dict + Contains translation and orientation in Euler angles + Returns: + -------- + * transformed_frames: np.ndarray + Quaternion frames resulting from the back projection of s, + transformed to fit to prev_frames. + + """ + if prev_frames is not None: + return align_quaternion_frames_automatically(skeleton, node_name, new_frames, prev_frames) + elif start_pose is not None: + m = get_transform_from_start_pose(start_pose) + first_frame_pos = new_frames[0][:3].tolist() + [1] + t_pos = np.dot(m, first_frame_pos)[:3] + delta = start_pose["position"] + # FIXME this assumes the y translation is the up axis and can be ignored + delta[0] -= t_pos[0] + delta[2] -= t_pos[2] + m[:3, 3] = delta + transformed_frames = transform_quaternion_frames(new_frames, m) + return transformed_frames + else: + return new_frames + + +def get_transform_from_start_pose(start_pose): + e = np.deg2rad(start_pose["orientation"]) + p = start_pose["position"] + if None not in e: + q = quaternion_from_euler(*e) + m = quaternion_matrix(q) + else: + m = np.eye(4) + if None not in p: + m[:3,3] = p + return m + + +def align_and_concatenate_frames(skeleton, joint_name, new_frames, prev_frames=None, start_pose=None, smoothing_window=0, + blending_method='smoothing'): + new_frames = align_quaternion_frames(skeleton, joint_name, new_frames, prev_frames, start_pose) + + if prev_frames is not None: + d = len(prev_frames) + new_frames = concatenate_frames(prev_frames, new_frames) + if smoothing_window > 0: + if blending_method == 'smoothing': + new_frames = smooth_quaternion_frames(new_frames, d, smoothing_window) + elif blending_method == 'slerp': + new_frames = smooth_quaternion_frames_with_slerp(new_frames, d, smoothing_window) + elif blending_method == 'slerp2': + new_frames = smooth_quaternion_frames_with_slerp2(skeleton, new_frames, d, smoothing_window) + elif blending_method == 'linear': + new_frames = blend_quaternion_frames_linearly(new_frames, prev_frames, skeleton, smoothing_window) + else: + raise KeyError('Unknown method!') + return new_frames + + +blend = lambda x: 2 * x * x * x - 3 * x * x + 1 + + +def align_joint(skeleton, frames, frame_idx, foot_joint, ik_chain, ik_window=7): + transition_start = frame_idx + 1 + transition_end = frame_idx + ik_window + c = create_grounding_constraint_from_frame(skeleton, frames, frame_idx, foot_joint) + ik = AnalyticalLimbIK.init_from_dict(skeleton, c.joint_name, ik_chain) + frames[transition_start] = ik.apply(frames[transition_start], c.position, c.orientation) + + chain_joints = [ik_chain["root"], ik_chain["joint"], foot_joint] + for c_joint in chain_joints: + idx = skeleton.animated_joints.index(c_joint) * 4 + 3 + j_indices = [idx, idx + 1, idx + 2, idx + 3] + start_q = frames[transition_start][j_indices] + end_q = frames[transition_end][j_indices] + for i in range(ik_window): + t = float(i) / ik_window + frames[transition_start + 1 + i][j_indices] = quaternion_slerp(start_q, end_q, t, spin=0, shortestpath=True) + return frames + + +def align_frames_and_fix_foot_to_prev(skeleton, aligning_joint, new_frames, prev_frames, start_pose, foot_joint, ik_chain, ik_window=7, smoothing_window=0): + new_frames = align_quaternion_frames(skeleton, aligning_joint, new_frames, prev_frames, start_pose) + if prev_frames is not None: + offset = prev_frames[-1][:3] - new_frames[0][:3] + + d = len(prev_frames) + frames = prev_frames.tolist() + for idx in range(1, len(new_frames)): # skip first frame + frames.append(new_frames[idx]) + frames = np.array(frames) + + transition_start = d + c = create_grounding_constraint_from_frame(skeleton, frames, d-1, foot_joint) + ik = AnalyticalLimbIK.init_from_dict(skeleton, c.joint_name, ik_chain) + before = skeleton.nodes[foot_joint].get_global_position(frames[transition_start]) + + frames[transition_start] = ik.apply(frames[transition_start], c.position, c.orientation) + + transition_end = d+ik_window + print("allign frames", c.position, foot_joint, d-1, transition_end, before, skeleton.nodes[foot_joint].get_global_position(frames[transition_start])) + print(skeleton.nodes[foot_joint].get_global_position(frames[d])) + + chain_joints = [ik_chain["root"], ik_chain["joint"], foot_joint] + for c_joint in chain_joints: + idx = skeleton.animated_joints.index(c_joint) * 4 + 3 + j_indices = [idx, idx + 1, idx + 2, idx + 3] + start_q = frames[transition_start][j_indices] + end_q = frames[transition_end][j_indices] + print(c_joint, start_q, end_q,j_indices) + for i in range(ik_window): + t = float(i) / ik_window + # nlerp_q = self.nlerp(start_q, end_q, t) + slerp_q = quaternion_slerp(start_q, end_q, t, spin=0, shortestpath=True) + print(transition_start+i+1, frames[transition_start + 1 + i][j_indices], slerp_q) + frames[transition_start + 1 + i][j_indices] = slerp_q + if smoothing_window > 0 and False: + frames = smooth_quaternion_frames(frames, d, smoothing_window) + return frames + else: + return new_frames + +def get_limb_length(skeleton, joint_name): + limb_length = np.linalg.norm(skeleton.nodes[joint_name].offset) + limb_length += np.linalg.norm(skeleton.nodes[joint_name].parent.offset) + return limb_length + +def generate_root_constraint_for_one_foot(skeleton, frame, root, c): + root_pos = skeleton.nodes[root].get_global_position(frame) + target_length = np.linalg.norm(c.position - root_pos) + limb_length = get_limb_length(skeleton, c.joint_name) + if target_length >= limb_length: + new_root_pos = (c.position + normalize(root_pos - c.position) * limb_length) + print("one constraint on ", c.joint_name, "- before", root_pos, "after", new_root_pos) + return new_root_pos + #frame[:3] = new_root_pos + + else: + print("no change") + + +def translate_root(skeleton, frames, target_frame_idx, plant_heel, ground_height=0): + """ translate the next frames closer to the previous frames root translation""" + n_frames = len(frames) + foot_pos = skeleton.nodes[plant_heel].get_global_position(frames[target_frame_idx-1]) + print("foot pos before", foot_pos) + delta = ground_height - foot_pos[1] + n_frames = len(frames) + for f in range(target_frame_idx, n_frames): + frames[f][1] += delta + print("after", skeleton.nodes[plant_heel].get_global_position(frames[target_frame_idx])) + for f in range(target_frame_idx, n_frames): + frames[f,:3] += delta/2 + + +def apply_constraint(skeleton, frames, c, ik_chain, frame_idx, start, end, window): + print("apply swing foot constraint on frame", frame_idx, start, end) + ik = AnalyticalLimbIK.init_from_dict(skeleton, c.joint_name, ik_chain) + frames[frame_idx] = ik.apply(frames[frame_idx], c.position, c.orientation) + joint_list = [ik_chain["root"], ik_chain["joint"], c.joint_name] + blend_between_frames(skeleton, frames, start, end, joint_list, window) + + +def apply_constraint_on_window_prev(skeleton, frames, c, ik_chain, start, end, window): + ik = AnalyticalLimbIK.init_from_dict(skeleton, c.joint_name, ik_chain) + indices = list(range(start, end + 1)) + print("apply on frames", indices) + for f in indices: + frames[f] = ik.apply(frames[f], c.position, c.orientation) + joint_list = [ik_chain["root"], ik_chain["joint"], c.joint_name] + blend_between_frames(skeleton, frames, end, end+window, joint_list, window) + + +def apply_constraint_on_window_next(skeleton, frames, c, ik_chain, start, end, window): + ik = AnalyticalLimbIK.init_from_dict(skeleton, c.joint_name, ik_chain) + indices = list(range(start, end + 1)) + print("apply on frames", indices) + for f in indices: + frames[f] = ik.apply(frames[f], c.position, c.orientation) + joint_list = [ik_chain["root"], ik_chain["joint"], c.joint_name] + print("blend between frames",start-window, start) + blend_between_frames(skeleton, frames, start-window, start, joint_list, window) + + +def align_foot_to_next_step(skeleton, frames, foot_joint, ik_chain, target_frame_idx, window): + start = target_frame_idx - window # start of blending range + end = target_frame_idx - 1 # modified frame + + c = create_grounding_constraint_from_frame(skeleton, frames, target_frame_idx, foot_joint) + apply_constraint(skeleton, frames, c, ik_chain, target_frame_idx, start, end, window) + + +def align_foot_to_prev_step(skeleton, frames, foot_joint, ik_chain, target_frame_idx, window): + start = target_frame_idx # modified frame + end = target_frame_idx + window # end of blending range + c = create_grounding_constraint_from_frame(skeleton, frames, target_frame_idx-1, foot_joint) + apply_constraint(skeleton, frames, c, ik_chain, target_frame_idx, start, end, window) + + + + +def generate_feet_constraints(skeleton, frames, frame_idx, plant_side, swing_side, target_ground_height): + plant_foot_joint = skeleton.skeleton_model["joints"][plant_side + "_ankle"] + plant_toe_joint = skeleton.skeleton_model["joints"][plant_side + "_toe"] + plant_heel_joint = skeleton.skeleton_model["joints"][plant_side + "_heel"] + swing_foot_joint = skeleton.skeleton_model["joints"][swing_side + "_ankle"] + swing_toe_joint = skeleton.skeleton_model["joints"][swing_side + "_toe"] + swing_heel_joint = skeleton.skeleton_model["joints"][swing_side + "_heel"] + plant_constraint = generate_ankle_constraint_from_toe(skeleton, frames, frame_idx, plant_foot_joint, + plant_heel_joint, plant_toe_joint, target_ground_height) + swing_constraint = generate_ankle_constraint_from_toe(skeleton, frames, frame_idx, swing_foot_joint, + swing_heel_joint, swing_toe_joint, target_ground_height) + return plant_constraint, swing_constraint + + +def generate_feet_constraints2(skeleton, frames, frame_idx, plant_side, swing_side): + plant_foot_joint = skeleton.skeleton_model["joints"][plant_side + "_ankle"] + swing_foot_joint = skeleton.skeleton_model["joints"][swing_side + "_ankle"] + plant_constraint = create_grounding_constraint_from_frame(skeleton, frames, frame_idx - 1, plant_foot_joint) + swing_constraint = create_grounding_constraint_from_frame(skeleton, frames, frame_idx - 1, swing_foot_joint) + return plant_constraint, swing_constraint + + +def align_feet_to_prev_step(skeleton, frames, frame_idx, plant_constraint, swing_constraint, ik_chains, window): + start = frame_idx # modified frame + end = frame_idx + window # end of blending range + apply_constraint_on_window_prev(skeleton, frames, plant_constraint, ik_chains[plant_constraint.joint_name], start, end, window) + apply_constraint(skeleton, frames, swing_constraint, ik_chains[swing_constraint.joint_name], frame_idx, start, end, window) + + +def align_feet_to_next_step(skeleton, frames, frame_idx, plant_constraint, swing_constraint, ik_chains, plant_window, window): + start = frame_idx - window # end of blending range + end = frame_idx # modified frame + apply_constraint_on_window_next(skeleton, frames, plant_constraint, ik_chains[plant_constraint.joint_name], start, end, plant_window) + apply_constraint(skeleton, frames, swing_constraint, ik_chains[swing_constraint.joint_name], frame_idx, start, end, window) + + +def align_feet_to_next_step2(skeleton, frames, frame_idx, plant_constraint, swing_constraint, ik_chains, plant_window, window): + start = frame_idx - window # end of blending range + end = frame_idx # modified frame + ik_chain = ik_chains[plant_constraint.joint_name] + joint_list = [ik_chain["root"], ik_chain["joint"], plant_constraint.joint_name] + blend_between_frames(skeleton, frames, start, end, joint_list, window) + ik_chain = ik_chains[swing_constraint.joint_name] + joint_list = [ik_chain["root"], ik_chain["joint"], plant_constraint.joint_name] + blend_between_frames(skeleton, frames, start, end, joint_list, window) + + +def fix_feet_at_transition(skeleton, frames, d, plant_side, swing_side, ik_chains, ik_window=8, plant_window=20): + target_ground_height = 0 + smooth_root_translation_around_transition(frames, d, 2 * ik_window) + + plant_constraint, swing_constraint = generate_feet_constraints(skeleton, frames, d, plant_side, swing_side, target_ground_height) + root_pos = generate_root_constraint_for_two_feet(skeleton, frames[d-1], plant_constraint, swing_constraint) + if root_pos is not None: + frames[d - 1][:3] = root_pos + smooth_root_translation_at_end(frames, d - 1, ik_window) + smooth_root_translation_at_start(frames, d, ik_window) + + align_feet_to_next_step(skeleton, frames, d-1, plant_constraint, swing_constraint, ik_chains, plant_window, ik_window) + align_feet_to_prev_step(skeleton, frames, d, plant_constraint, swing_constraint, ik_chains, ik_window) + #swing_foot = skeleton.skeleton_model["joints"][swing_side + "_ankle"] + #align_foot_to_prev_step(skeleton, frames, swing_foot, ik_chains[swing_foot], d, ik_window) + + +def align_frames_using_forward_blending(skeleton, aligning_joint, new_frames, prev_frames, prev_start, start_pose, ik_chains, smoothing_window=0): + """ applies foot ik constraint to fit the prev motion primitive to the next motion primitive + """ + + new_frames = align_quaternion_frames(skeleton, aligning_joint, new_frames, prev_frames, start_pose) + if prev_frames is not None: + d = len(prev_frames) + frames = prev_frames.tolist() + for idx in range(1, len(new_frames)): # skip first frame + frames.append(new_frames[idx]) + frames = np.array(frames) + + blend_end = d + blend_start = int(prev_start + (blend_end-prev_start)/2) # start blending from the middle of the step + + left_joint = skeleton.skeleton_model["joints"]["left_ankle"] + right_joint = skeleton.skeleton_model["joints"]["right_ankle"] + pelvis = skeleton.skeleton_model["joints"]["pelvis"] + left_ik_chain = ik_chains[left_joint] + right_ik_chain = ik_chains[right_joint] + leg_joint_list = [skeleton.root, left_ik_chain["root"], left_ik_chain["joint"], left_joint, + right_ik_chain["root"], right_ik_chain["joint"], right_joint] + if pelvis != skeleton.root: + leg_joint_list.append(pelvis) + + frames = blend_towards_next_step_linear_with_original(skeleton, frames, blend_start, blend_end, leg_joint_list) + joint_list = [j for j in skeleton.animated_joints if j not in leg_joint_list] + + if smoothing_window > 0: + frames = smooth_quaternion_frames_joint_filter(skeleton, frames, d, joint_list, smoothing_window) + return frames + else: + return new_frames + +def blend_towards_next_step_linear(skeleton, frames, d, plant_side, swing_side, ik_chains, window=8): + target_ground_height = 0 + smooth_root_translation_around_transition(frames, d, 2 * window) + plant_constraint, swing_constraint = generate_feet_constraints(skeleton, frames, d, plant_side, swing_side, target_ground_height) + root_pos = generate_root_constraint_for_two_feet(skeleton, frames[d-1], plant_constraint, swing_constraint) + if root_pos is not None: + frames[d - 1][:3] = root_pos + smooth_root_translation_at_end(frames, d - 1, window) + smooth_root_translation_at_start(frames, d, window) + blend_quaternions_to_next_step(skeleton, frames, d, plant_constraint.joint_name, swing_constraint.joint_name, ik_chains, window) + + + +def blend_towards_next_step3(skeleton, frames, start, end, plant_side, swing_side, ik_chains, window=8): + plant_joint = skeleton.skeleton_model["joints"][plant_side + "_ankle"] + swing_joint = skeleton.skeleton_model["joints"][swing_side + "_ankle"] + plant_ik_chain = ik_chains[plant_joint] + swing_ik_chain = ik_chains[swing_joint] + joint_list = [skeleton.root, "pelvis", plant_ik_chain["root"], plant_ik_chain["joint"], plant_joint, + swing_ik_chain["root"], swing_ik_chain["joint"], swing_joint] + middle = int(start + (end-start)/2) + new_frames = generate_blended_frames(skeleton, frames, middle, end, joint_list, end-middle) + frames = interpolate_frames(skeleton, frames, new_frames, joint_list, middle, end) + return frames + + + +def transform_euler_frame(euler_frame, angles, offset, rotation_order=None, global_rotation=True): + """ + Calls transform_point for the root parameters and adds theta to the y rotation + channel of the frame. + + The offset of root is transformed by transform_point + The orientation of root is rotated by Rotation matrix + + Parameters + --------- + *euler_frame: np.ndarray + \t the parameters of a single frame + *angles: list of floats + \tRotation angles in degrees + *offset: np.ndarray + \tTranslation + """ + if rotation_order is None: + rotation_order = DEFAULT_ROTATION_ORDER + transformed_frame = deepcopy(euler_frame) + if global_rotation: + transformed_frame[:3] = transform_point(euler_frame[:3], angles, offset, rotation_order=rotation_order) + else: + transformed_frame[:3] = transform_point(euler_frame[:3], np.zeros(3), offset, rotation_order=rotation_order) + R = euler_matrix(np.deg2rad(angles[0]), np.deg2rad(angles[1]), np.deg2rad(angles[2]), axes='rxyz') + + src_e = np.deg2rad(euler_frame[3:6]) + rot_string = rotation_order_to_string(rotation_order) + OR = euler_matrix(*src_e, axes=rot_string) + rotmat = np.dot(R, OR) + eul_angles = np.rad2deg(euler_from_matrix(rotmat, rot_string)) + transformed_frame[3:6] = eul_angles + return transformed_frame + + +def transform_euler_frames(euler_frames, angles, offset, rotation_order=None): + """ Applies a transformation on the root joint of a list euler frames. + Parameters + ---------- + *euler_frames: np.ndarray + \tList of frames where the rotation is represented as euler angles in degrees. + *angles: list of floats + \tRotation angles in degrees + *offset: np.ndarray + \tTranslation + """ + transformed_euler_frames = [] + for frame in euler_frames: + transformed_euler_frames.append( + transform_euler_frame(frame, angles, offset, rotation_order)) + return np.array(transformed_euler_frames) + + +def shift_euler_frames_to_ground(euler_frames, ground_contact_joints, skeleton, align_index=0): + """ + shift all euler frames of motion to ground, which means the y-axis for + gound contact joint should be 0 + Step 1: apply forward kinematic to compute global position for ground + contact joint for each frame + Setp 2: find the offset from ground contact joint to ground, and shift + corresponding frame based on offset + """ + foot_contact_heights = [] + for joint in ground_contact_joints: + foot_contact_heights.append(skeleton.nodes[joint].get_global_position_from_euler(euler_frames[align_index])[1]) + avg_foot_height =np.array([0, -np.average(foot_contact_heights), 0]) + return transform_euler_frames(euler_frames, [0.0, 0.0, 0.0], avg_foot_height) + + +def shift_quat_frames_to_ground(quat_frames, ground_contact_joints, skeleton, align_index=0): + foot_contact_heights = [] + for joint in ground_contact_joints: + foot_contact_heights.append(skeleton.nodes[joint].get_global_position(quat_frames[align_index])[1]) + return transform_quaternion_frames(quat_frames, + [0.0, 0.0, 0.0], + np.array([0, -np.average(foot_contact_heights), 0])) + + + +def find_aligning_transformation(skeleton, euler_frames_a, euler_frames_b): + """ + performs alignment of the point clouds based on the poses at the end of + euler_frames_a and the start of euler_frames_b + Returns the rotation around y axis in radians, x offset and z offset + """ + point_cloud_a = convert_euler_frame_to_cartesian_frame(skeleton, euler_frames_a[-1]) + point_cloud_b = convert_euler_frame_to_cartesian_frame(skeleton, euler_frames_b[0]) + weights = skeleton.get_joint_weights() + theta, offset_x, offset_z = _align_point_clouds_2D(point_cloud_a, point_cloud_b, weights) + return theta, offset_x, offset_z + + +def align_frames(skeleton, euler_frames_a, euler_frames_b, smooth=True): + """ + calls find_aligning_transformation and concatenates the frames based on the + resulting transformation + Parameters + ---------- + *skeleton: Skeleton + \tUsed to extract hierarchy information. + *euler_frames_a: np.ndarray + \List of frames where the rotation is represented as euler angles in degrees. + *euler_frames_b: np.ndarray + \List of frames where the rotation is represented as euler angles in degrees. + *smooth: bool + \t Sets whether or not smoothing is supposed to be applied on the at the transition. + Returns + ------- + *aligned_frames : np.ndarray + \tAligned and optionally smoothed motion + """ + theta, offset_x, offset_z = find_aligning_transformation(skeleton, euler_frames_a, euler_frames_b) + + # apply 2d transformation + offset = np.array([offset_x, 0, offset_z]) + angles = [0, np.degrees(theta), 0] + euler_frames_b = transform_euler_frames(euler_frames_b, angles, + offset) + + # concatenate frames and optionally apply smoothing + if smooth: + euler_frames = smoothly_concatenate(euler_frames_a, euler_frames_b) + else: + euler_frames = np.concatenate((euler_frames_a, euler_frames_b), axis=0) + return euler_frames + + diff --git a/build/lib/anim_utils/animation_data/motion_distance.py b/build/lib/anim_utils/animation_data/motion_distance.py new file mode 100644 index 0000000..df4e13c --- /dev/null +++ b/build/lib/anim_utils/animation_data/motion_distance.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import numpy as np +from math import sqrt +from transformations import euler_matrix +from .motion_concatenation import _align_point_clouds_2D + + +def _point_cloud_distance(a, b): + """ Calculates the distance between two point clouds with equal length and + corresponding indices + """ + assert len(a) == len(b) + distance = 0 + n_points = len(a) + for i in range(n_points): + d = [a[i][0] - b[i][0], a[i][1] - b[i][1], a[i][2] - b[i][2]] + distance += sqrt(d[0] ** 2 + d[1] ** 2 + d[2] ** 2) + return distance / n_points + + +def convert_quat_frame_to_point_cloud(skeleton, frame, joints=None): + points = [] + if joints is None: + joints = [k for k, n in list(skeleton.nodes.items()) if len(n.children) > 0] + for j in joints: + p = skeleton.nodes[j].get_global_position(frame) + points.append(p) + return points + + +def _transform_point_cloud(point_cloud, theta, offset_x, offset_z): + """ Transforms points in a point cloud by a rotation around the y axis and a translation + along x and z + """ + transformed_point_cloud = [] + for p in point_cloud: + if p is not None: + m = euler_matrix(0,np.radians(theta), 0) + m[0, 3] = offset_x + m[2, 3] = offset_z + p = p.tolist() + new_point = np.dot(m, p + [1])[:3] + transformed_point_cloud.append(new_point) + return transformed_point_cloud + + +def _transform_invariant_point_cloud_distance(pa,pb, weights=None): + if weights is None: + weights = [1.0] * len(pa) + theta, offset_x, offset_z = _align_point_clouds_2D(pa, pb, weights) + t_b = _transform_point_cloud(pb, theta, offset_x, offset_z) + distance = _point_cloud_distance(pa, t_b) + return distance + + +def frame_distance_measure(skeleton, a, b, weights=None): + """ Converts frames into point clouds and calculates + the distance between the aligned point clouds. + + Parameters + --------- + *skeleton: Skeleton + \t skeleton used for forward kinematics + *a: np.ndarray + \t the parameters of a single frame representing a skeleton pose. + *b: np.ndarray + \t the parameters of a single frame representing a skeleton pose. + *weights: list of floats + \t weights giving the importance of points during the alignment and distance + + Returns + ------- + Distance representing similarity of the poses. + """ + assert len(a) == len(b) + pa = convert_quat_frame_to_point_cloud(skeleton, a) + pb = convert_quat_frame_to_point_cloud(skeleton, b) + return _transform_invariant_point_cloud_distance(pa,pb, weights) diff --git a/build/lib/anim_utils/animation_data/motion_state.py b/build/lib/anim_utils/animation_data/motion_state.py new file mode 100644 index 0000000..b9d7eb5 --- /dev/null +++ b/build/lib/anim_utils/animation_data/motion_state.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import collections +import numpy as np +from transformations import quaternion_inverse, quaternion_conjugate, quaternion_multiply, quaternion_matrix + +def get_random_color(): + random_color = np.random.rand(3, ) + if np.sum(random_color) < 0.5: + random_color += np.array([0, 0, 1]) + return random_color.tolist() + + +class MotionStateInterface(object): + def update(self, dt): + pass + + def get_pose(self, frame_idx=None): + pass + + def reset(self): + pass + + def get_frames(self): + pass + + def get_n_frames(self): + pass + + def get_frame_time(self): + pass + + def set_frame_idx(self, frame_idx): + pass + + def set_color_annotation_from_labels(self, labels, colors): + pass + + def replace_current_frame(self, frame): + pass + + def replace_frames(self, new_frames): + pass + + def set_color_annotation(self, semantic_annotation, color_map): + pass + + def set_time_function(self, time_function): + pass + + def set_color_annotation_legacy(self, annotation, color_map): + pass + + def set_random_color_annotation(self): + pass + + def get_current_frame_idx(self): + pass + + def get_current_annotation(self): + return None + + def get_n_annotations(self): + return 0 + + def get_semantic_annotation(self): + return list() + + def set_time(self, t): + return + + +class MotionState(MotionStateInterface): + """ Wrapper of a MotionVector to add functions for keeping track of the current time and frame index as well as semantic information. + """ + def __init__(self, mv): + self.mv = mv + self.time = 0 + self.frame_idx = 0 + self.play = False + self._frame_annotation = None + self.label_color_map = dict() + self._semantic_annotation = collections.OrderedDict() + self.events = dict() + self.n_annotations = 0 + self.hold_frames = list() # sorted list + self.paused = False + self.interpolate = False + self.tick = None + self._time_function = None + + def update(self, dt): + if self.play and not self.paused: + self.time += dt + self.frame_idx = int(self.time / self.mv.frame_time) + if len(self.hold_frames) > 0: + #print("hold frame", self.frame_idx,self.hold_frame, self.mv.n_frames) + if self.frame_idx >= self.hold_frames[0]: + self.paused = True + self.frame_idx = self.hold_frames[0] + self.hold_frames = self.hold_frames[1:] # remove entry + if self.tick is not None: + self.tick.update(dt) + if self.frame_idx >= self.mv.n_frames: + self.reset() + return True + return False + + + def get_pose(self, frame_idx=None): + if self.interpolate: + return self.get_pose_interpolate(frame_idx) + else: + if frame_idx is None: + return self.mv.frames[self.frame_idx] + else: + return self.mv.frames[frame_idx] + + def get_pose_interpolate(self, frame_idx=None): + #FIXME interpolation causes jump in translation at start and end + if frame_idx is None: + start_frame_idx = int(self.time / self.mv.frame_time) + end_frame_idx = start_frame_idx+1 + t = (self.time % self.mv.frame_time)/ self.mv.frame_time + if end_frame_idx >= self.mv.n_frames: + end_frame_idx = start_frame_idx + t = 0 + #print("get pose", self.time, start_frame_idx, t, end_frame_idx) + return self.mv.interpolate(start_frame_idx, end_frame_idx, t) + #return self.mv.frames[self.frame_idx] + else: + return self.mv.frames[frame_idx] + + def reset(self): + self.time = 0 + self.frame_idx = 0 + self.paused = False + if self.tick is not None: + self.tick.reset() + + def get_frames(self): + return self.mv.frames + + def get_n_frames(self): + return self.mv.n_frames + + def get_frame_time(self): + return self.mv.frame_time + + def get_current_annotation(self): + return self._frame_annotation[self.frame_idx] + + + def get_current_annotation_label(self): + if self._frame_annotation is not None and 0 <= self.frame_idx < len(self._frame_annotation): + return self._frame_annotation[self.frame_idx]["label"] + else: + return "" + + def set_frame_idx(self, frame_idx): + self.frame_idx = frame_idx + self.time = self.get_frame_time() * self.frame_idx + + def set_color_annotation_from_labels(self, labels, colors=None): + + if colors is None: + def getRGBfromI(RGBint): + """ https://stackoverflow.com/questions/33124347/convert-integers-to-rgb-values-and-back-with-python + """ + blue = RGBint & 255 + green = (RGBint >> 8) & 255 + red = (RGBint >> 16) & 255 + return red, green, blue + n_classes = int(max(labels)+1) + step_size = 255255255 / n_classes + self.label_color_map = dict() + for n in range(n_classes): + l = int(n*step_size) + self.label_color_map[str(n)] = np.array(getRGBfromI(l), dtype=float)/255 + else: + n_classes = len(colors) + for n in range(n_classes): + self.label_color_map[str(n)] = colors[n] + + self._semantic_annotation = collections.OrderedDict() + for n in range(n_classes): + self._semantic_annotation[str(n)] = [] + self.n_annotations = len(labels) + self._frame_annotation = [] + for idx, label in enumerate(labels): + label = str(int(label)) + self._frame_annotation.append({"label": label, "color": self.label_color_map[label]}) + self._semantic_annotation[label].append(idx) + + def replace_current_frame(self, frame): + if 0 <= self.frame_idx < self.mv.n_frames: + self.mv.frames[self.frame_idx] = frame + + def replace_frames(self, new_frames): + self.time = 0 + self.frame_idx = 0 + self.mv.frames = new_frames + self.mv.n_frames = len(new_frames) + + def set_color_annotation(self, semantic_annotation, color_map): + self._semantic_annotation = semantic_annotation + self.label_color_map = color_map + self.n_annotations = self.mv.n_frames + self._frame_annotation = [] + for idx in range(self.n_annotations): + self._frame_annotation.append({"label": "none", "color": [1,1,1]}) + for label in semantic_annotation.keys(): + if label in color_map.keys(): + entry = {"label": label, "color": color_map[label]} + if len(semantic_annotation[label]) > 0 and type(semantic_annotation[label][0]) == list: + for indices in semantic_annotation[label]: + for i in indices: + if i < self.n_annotations: + self._frame_annotation[i] = entry + else: + for idx in semantic_annotation[label]: + if idx < self.n_annotations: + self._frame_annotation[idx] = entry + else: + print("warning", idx, self.n_annotations) + + + def set_time_function(self, time_function): + self._time_function = time_function + + def set_color_annotation_legacy(self, annotation, color_map): + self._semantic_annotation = collections.OrderedDict() + self.label_color_map = color_map + self.n_annotations = len(annotation) + self._frame_annotation = [] + for idx, label in enumerate(annotation): + + self._frame_annotation.append({"label": label, + "color": color_map[label]}) + + if label not in list(self._semantic_annotation.keys()): + self._semantic_annotation[label] = [] + self._semantic_annotation[label].append(idx) + + def set_random_color_annotation(self): + self._frame_annotation = [] + for idx in range(self.mv.n_frames): + self._frame_annotation.append({"color": get_random_color()}) + self.n_annotations = self.mv.n_frames + + def get_current_frame_idx(self): + return self.frame_idx + + def get_n_annotations(self): + return self.n_annotations + + def get_semantic_annotation(self): + return list(self._semantic_annotation.items()) + + def get_label_color_map(self): + return self.label_color_map + + def apply_delta_frame(self, skeleton, delta_frame): + self.mv.apply_delta_frame(skeleton, delta_frame) + + def set_time(self, t): + self.frame_idx = int(t / self.get_frame_time()) + self.frame_idx = min(self.frame_idx, self.mv.n_frames-1) + #self.time = self.frame_idx*self.get_frame_time() + self.time = t + + def set_position(self, position): + delta = position - self.mv.frames[self.frame_idx, :3] + self.mv.frames[:, :3] += delta + + def set_orientation(self, orientation): + print("rotate frames") + current_q = self.mv.frames[self.frame_idx, 3:7] + delta_q = quaternion_multiply(orientation, quaternion_inverse(current_q)) + delta_q /= np.linalg.norm(delta_q) + delta_m = quaternion_matrix(delta_q)[:3, :3] + for idx in range(self.mv.n_frames): + old_t = self.mv.frames[idx, :3] + old_q = self.mv.frames[idx, 3:7] + self.mv.frames[idx, :3] = np.dot(delta_m, old_t)[:3] + self.mv.frames[idx, 3:7] = quaternion_multiply(delta_q, old_q) + #self.mv.frames[i, 3:7] = quaternion_multiply(delta_q, old_q) + + def set_ticker(self, tick): + self.tick = tick + +class MotionSplineState(MotionStateInterface): + def __init__(self, spline, frame_time): + self.spline = spline + self.time = 0.0 + self.frame_idx = 0.0 + self.frame_time = frame_time + self.events = dict() + + def update(self, dt): + self.time += dt + self.frame_idx = self.time / self.frame_time + if self.frame_idx >= self.spline.get_domain()[-1]: + self.reset() + return True + else: + return False + + def get_pose(self, frame_idx=None): + if frame_idx is None: + return self.spline.evaluate(self.frame_idx) + else: + return self.spline.evaluate(frame_idx) + + def reset(self): + self.time = 0 + + def get_frames(self): + return self.spline.get_motion_vector() + + def get_last_frame(self): + return self.spline.evaluate(self.spline.get_domain()[-1]) + + def get_n_frames(self): + return self.spline.get_domain()[-1] + + def get_frame_time(self): + return self.frame_time + + def set_time(self, t): + self.time = t + diff --git a/build/lib/anim_utils/animation_data/motion_vector.py b/build/lib/anim_utils/animation_data/motion_vector.py new file mode 100644 index 0000000..94ae946 --- /dev/null +++ b/build/lib/anim_utils/animation_data/motion_vector.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +from datetime import datetime +import os +import numpy as np +import imp +from transformations import quaternion_inverse, quaternion_multiply, quaternion_slerp, quaternion_from_euler, euler_matrix, quaternion_from_matrix +from .quaternion_frame import convert_euler_frames_to_quaternion_frames +from .motion_concatenation import align_and_concatenate_frames, smooth_root_positions +from .constants import ROTATION_TYPE_QUATERNION, ROTATION_TYPE_EULER +from .bvh import BVHWriter +from .acclaim import create_euler_matrix, DEFAULT_FRAME_TIME + + +def get_quaternion_delta(a, b): + return quaternion_multiply(quaternion_inverse(b), a) + + +def add_frames(skeleton, a, b): + """ returns c = a + b""" + c = np.zeros(len(a)) + c[:3] = a[:3] + b[:3] + for idx, j in enumerate(skeleton.animated_joints): + o = idx * 4 + 3 + q_a = a[o:o + 4] + q_b = b[o:o + 4] + q_prod = quaternion_multiply(q_a, q_b) + c[o:o + 4] = q_prod / np.linalg.norm(q_prod) + return c + + +def substract_frames(skeleton, a, b): + """ returns c = a - b""" + c = np.zeros(len(a)) + c[:3] = a[:3] - b[:3] + for idx, j in enumerate(skeleton.animated_joints): + o = idx*4 + 3 + q_a = a[o:o+4] + q_b = b[o:o+4] + q_delta = get_quaternion_delta(q_a, q_b) + c[o:o+4] = q_delta / np.linalg.norm(q_delta) + return c + +def amc_euler_to_quaternion(angles, dofs, c, c_inv, align_quat=True): + """ https://research.cs.wisc.edu/graphics/Courses/cs-838-1999/Jeff/ASF-AMC.html + """ + m = create_euler_matrix(angles, dofs) + #apply joint coordinate system defined by asf skeleton: C_inv x M x C + m = np.dot(c, np.dot(m, c_inv)) + q = quaternion_from_matrix(m) + if align_quat: + dot = np.sum(q) + if dot < 0: + q = -q + return q.tolist() + +class MotionVector(object): + """ Contains a list of skeleton animation frames. Each frame represents a list of parameters for the degrees of freedom of a skeleton. + """ + def __init__(self, skeleton=None, **kwargs): + self.n_frames = 0 + self._prev_n_frames = 0 + self.frames = None + self.start_pose = None + self.rotation_type = kwargs.get("rotation_type",ROTATION_TYPE_QUATERNION) + self.apply_spatial_smoothing = kwargs.get("apply_spatial_smoothing",False) + self.apply_foot_alignment = kwargs.get("apply_foot_alignment",False) + self.smoothing_window = kwargs.get("smoothing_window",0) + self.spatial_smoothing_method = kwargs.get("spatial_smoothing_method","smoothing") + self.frame_time = 1.0/30.0 + self.skeleton = skeleton + + + def from_bvh_reader(self, bvh_reader, filter_joints=True, animated_joints=None): + if self.rotation_type == ROTATION_TYPE_QUATERNION: + quat_frame = convert_euler_frames_to_quaternion_frames(bvh_reader, bvh_reader.frames, filter_joints, animated_joints) + self.frames = np.array(quat_frame) + elif self.rotation_type == ROTATION_TYPE_EULER: + self.frames = bvh_reader.frames + self.n_frames = len(self.frames) + self._prev_n_frames = 0 + self.frame_time = bvh_reader.frame_time + + def get_relative_frames(self): + relative_frames = [] + for idx in range(1, len(self.frames)): + delta_frame = np.zeros(len(self.frames[0])) + delta_frame[7:] = self.frames[idx][7:] + delta_frame[:3] = self.frames[idx][:3] - self.frames[idx-1][:3] + currentq = self.frames[idx][3:7] / np.linalg.norm(self.frames[idx][3:7]) + prevq = self.frames[idx-1][3:7] / np.linalg.norm(self.frames[idx-1][3:7]) + delta_q = quaternion_multiply(quaternion_inverse(prevq), currentq) + delta_frame[3:7] = delta_q + #print(idx, self.frames[idx][:3], self.frames[idx - 1][:3], delta_frame[:3], delta_frame[3:7]) + + relative_frames.append(delta_frame) + return relative_frames + + def append_frames_generic(self, new_frames): + """Align quaternion frames to previous frames + + Parameters + ---------- + * new_frames: list + A list of frames with the same rotation format type as the motion vector + """ + if self.apply_spatial_smoothing: + smoothing_window = self.smoothing_window + else: + smoothing_window = 0 + self.frames = align_and_concatenate_frames(self.skeleton, self.skeleton.aligning_root_node, new_frames, self.frames, self.start_pose, + smoothing_window=smoothing_window, blending_method=self.spatial_smoothing_method) + + self.n_frames = len(self.frames) + self._prev_n_frames = self.n_frames + + + def append_frames_using_forward_blending(self, new_frames): + if self.apply_spatial_smoothing: + smoothing_window = self.smoothing_window + else: + smoothing_window = 0 + from . import motion_concatenation + imp.reload(motion_concatenation) + ik_chains = self.skeleton.skeleton_model["ik_chains"] + self.frames = motion_concatenation.align_frames_using_forward_blending(self.skeleton, self.skeleton.aligning_root_node, new_frames, + self.frames, self._prev_n_frames, self.start_pose, + ik_chains, smoothing_window) + self._prev_n_frames = self.n_frames + self.n_frames = len(self.frames) + + def append_frames(self, new_frames, plant_foot=None): + if self.apply_foot_alignment and self.skeleton.skeleton_model is not None: + self.append_frames_using_forward_blending(new_frames) + else: + self.append_frames_generic(new_frames) + + def export(self, skeleton, output_filename, add_time_stamp=False): + bvh_writer = BVHWriter(None, skeleton, self.frames, self.frame_time, True) + if add_time_stamp: + output_filename = output_filename + "_" + \ + str(datetime.now().strftime("%d%m%y_%H%M%S")) + ".bvh" + elif output_filename != "": + if not output_filename.endswith("bvh"): + output_filename = output_filename + ".bvh" + else: + output_filename = "output.bvh" + bvh_writer.write(output_filename) + + def reduce_frames(self, n_frames): + if n_frames == 0: + self.frames = None + self.n_frames = 0 + self._prev_n_frames = self.n_frames + else: + self.frames = self.frames[:n_frames] + self.n_frames = len(self.frames) + self._prev_n_frames = 0 + + def has_frames(self): + return self.frames is not None + + def clear(self, end_frame=0): + if end_frame == 0: + self.frames = None + self.n_frames = 0 + self._prev_n_frames = 0 + else: + self.frames = self.frames[:end_frame] + self.n_frames = len(self.frames) + self._prev_n_frames = 0 + + def translate_root(self, offset): + for idx in range(self.n_frames): + self.frames[idx][:3] += offset + + def scale_root(self, scale_factor): + for idx in range(self.n_frames): + self.frames[idx][:3] *= scale_factor + + def from_fbx(self, animation, animated_joints): + self.frame_time = animation["frame_time"] + root_joint = animated_joints[0] + self.n_frames = len(animation["curves"][root_joint]) + self.frames = [] + for idx in range(self.n_frames): + frame = self._create_frame_from_fbx(animation, animated_joints, idx) + self.frames.append(frame) + + def _create_frame_from_fbx(self, animation, animated_joints, idx): + n_dims = len(animated_joints) * 4 + 3 + frame = np.zeros(n_dims) + offset = 3 + root_name = animated_joints[0] + frame[:3] = animation["curves"][root_name][idx]["translation"] + for node_name in animated_joints: + if node_name in animation["curves"].keys(): + rotation = animation["curves"][node_name][idx]["rotation"] + frame[offset:offset+4] = rotation + offset += 4 + + return frame + + def to_unity_format(self, scale=1.0): + """ Converts the frames into a custom json format for use in a Unity client""" + animated_joints = [j for j, n in list(self.skeleton.nodes.items()) if + "EndSite" not in j and len(n.children) > 0] # self.animated_joints + unity_frames = [] + + for node in list(self.skeleton.nodes.values()): + node.quaternion_index = node.index + + for frame in self.frames: + unity_frame = self._convert_frame_to_unity_format(frame, animated_joints, scale) + unity_frames.append(unity_frame) + + result_object = dict() + result_object["frames"] = unity_frames + result_object["frameTime"] = self.frame_time + result_object["jointSequence"] = animated_joints + return result_object + + def to_db_format(self, scale=1.0, animated_joints=None): + """ Converts the frames into a custom json format for use in a Unity client""" + if animated_joints is None: + animated_joints = [j for j, n in self.skeleton.nodes.items() if + n.children is not None and len(n.children) > 0] # self.animated_joints + poses = [] + for frame in self.frames: + pose = self._convert_frame_to_db_format(frame, animated_joints, scale) + poses.append(pose) + + result_object = dict() + result_object["poses"] = poses + result_object["frame_time"] = self.frame_time + result_object["joint_sequence"] = animated_joints + return result_object + + def _convert_frame_to_db_format(self, frame, animated_joints, scale=1.0): + """ Converts the frame into a custom json format and converts the transformations + to the left-handed coordinate system of Unity. + src: http://answers.unity3d.com/questions/503407/need-to-convert-to-right-handed-coordinates.html + """ + + t = frame[:3] * scale + pose = [-t[0], t[1], t[2]] + for node_name in self.skeleton.nodes.keys(): + if node_name in animated_joints: + node = self.skeleton.nodes[node_name] + if node_name in self.skeleton.animated_joints: # use rotation from frame + # TODO fix: the animated_joints is ordered differently than the nodes list for the latest model + index = self.skeleton.animated_joints.index(node_name) + offset = index * 4 + 3 + r = frame[offset:offset + 4] + pose += [-r[0], -r[1], r[2], r[3]] + else: # use fixed joint rotation + r = node.rotation + pose += [-float(r[0]), -float(r[1]), float(r[2]), float(r[3])] + return pose + + + def _convert_frame_to_unity_format(self, frame, animated_joints, scale=1.0): + """ Converts the frame into a custom json format and converts the transformations + to the left-handed coordinate system of Unity. + src: http://answers.unity3d.com/questions/503407/need-to-convert-to-right-handed-coordinates.html + """ + unity_frame = {"rotations": [], "rootTranslation": None} + for node_name in self.skeleton.nodes.keys(): + if node_name in animated_joints: + node = self.skeleton.nodes[node_name] + if node_name == self.skeleton.root: + t = frame[:3] * scale + unity_frame["rootTranslation"] = {"x": -t[0], "y": t[1], "z": t[2]} + + if node_name in self.skeleton.animated_joints: # use rotation from frame + # TODO fix: the animated_joints is ordered differently than the nodes list for the latest model + index = self.skeleton.animated_joints.index(node_name) + offset = index * 4 + 3 + r = frame[offset:offset + 4] + unity_frame["rotations"].append({"x": -r[1], "y": r[2], "z": r[3], "w": -r[0]}) + else: # use fixed joint rotation + r = node.rotation + unity_frame["rotations"].append( + {"x": -float(r[1]), "y": float(r[2]), "z": float(r[3]), "w": -float(r[0])}) + return unity_frame + + def from_custom_db_format(self, data): + self.frames = [] + for f in data["poses"]: + t = f[:3] + o = 3 + new_f = [-t[0], t[1], t[2]] + for i in data["joint_sequence"]: + new_f.append(-f[o]) + new_f.append(-f[o+1]) + new_f.append(f[o+2]) + new_f.append(f[o+3]) + o+=4 + + self.frames.append(new_f) + self.frames = np.array(self.frames) + self.n_frames = len(self.frames) + self.frame_time = data["frame_time"] + + def from_custom_unity_format(self, data): + self.frames = [] + for f in data["frames"]: + t = f["rootTranslation"] + new_f = [-t["x"], t["y"], t["z"]] + for q in f["rotations"]: + new_f.append(-q["w"]) + new_f.append(-q["x"]) + new_f.append(q["y"]) + new_f.append(q["z"]) + + self.frames.append(new_f) + self.frames = np.array(self.frames) + self.n_frames = len(self.frames) + self.frame_time = data["frameTime"] + + def apply_low_pass_filter_on_root(self, window): + self.frames[:, :3] = smooth_root_positions(self.frames[:, :3], window) + + def apply_delta_frame(self, skeleton, delta_frame): + for f in range(self.n_frames): + self.frames[f] = add_frames(skeleton, self.frames[f], delta_frame) + + def interpolate(self, start_idx, end_idx, t): + new_frame = np.zeros(self.frames[0].shape) + new_frame[:3] = (1-t) * self.frames[start_idx][:3] + t * self.frames[end_idx][:3] + for i in range(3, new_frame.shape[0], 4): + start_q = self.frames[start_idx][i:i+4] + end_q = self.frames[end_idx][i:i+4] + new_frame[i:i+4] = quaternion_slerp(start_q, end_q, t, spin=0, shortestpath=True) + return new_frame + + def from_amc_data(self, skeleton, amc_frames, frame_time=DEFAULT_FRAME_TIME): + identity = np.eye(4, dtype=np.float32) + self.frames = [] + for f in amc_frames: + new_f = [] + for key in skeleton.animated_joints: + b_data = skeleton.nodes[key].format_meta_data + if "coordinate_transform" in b_data and "inv_coordinate_transform" in b_data: + c = b_data["coordinate_transform"] + c_inv = b_data["inv_coordinate_transform"] + else: + c = identity + c_inv = identity + if key == "root": + new_f += f["root"][:3] + dof = b_data["asf_channels"] + values = amc_euler_to_quaternion(f[key][3:], dof, c, c_inv) + new_f += values + elif key in f: + dof = b_data["asf_channels"] + new_f += amc_euler_to_quaternion(f[key], dof, c, c_inv) + else: + new_f += [1,0,0,0] + self.frames.append(new_f) + self.frames = np.array(self.frames) + self.n_frames = len(self.frames) + self.frame_time = frame_time diff --git a/build/lib/anim_utils/animation_data/quaternion_frame.py b/build/lib/anim_utils/animation_data/quaternion_frame.py new file mode 100644 index 0000000..0f13f0e --- /dev/null +++ b/build/lib/anim_utils/animation_data/quaternion_frame.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +""" +Created on Fri Nov 24 14:10:21 2014 + +@author: Erik Herrmann, Han Du + +""" +import collections +import numpy as np +from transformations import euler_matrix, quaternion_from_matrix, quaternion_matrix, euler_from_matrix +from .utils import rotation_order_to_string +from .constants import DEFAULT_ROTATION_ORDER, LEN_EULER, LEN_QUAT, LEN_ROOT_POS + + + + +def check_quat(test_quat, ref_quat): + """check locomotion_synthesis_test quat needs to be filpped or not + """ + test_quat = np.asarray(test_quat) + ref_quat = np.asarray(ref_quat) + dot = np.dot(test_quat, ref_quat) + if dot < 0: + test_quat = - test_quat + return test_quat.tolist() + +def euler_to_quaternion(euler_angles, rotation_order=DEFAULT_ROTATION_ORDER,filter_values=True): + """Convert euler angles to quaternion vector [qw, qx, qy, qz] + Parameters + ---------- + * euler_angles: list of floats + \tA list of ordered euler angles in degress + * rotation_order: Iteratable + \t a list that specifies the rotation axis corresponding to the values in euler_angles + * filter_values: Bool + \t enforce a unique rotation representation + + """ + assert len(euler_angles) == 3, ('The length of euler angles should be 3!') + euler_angles = np.deg2rad(euler_angles) + rotmat = euler_matrix(*euler_angles, rotation_order_to_string(rotation_order)) + # convert rotation matrix R into quaternion vector (qw, qx, qy, qz) + quat = quaternion_from_matrix(rotmat) + # filter the quaternion see + # http://physicsforgames.blogspot.de/2010/02/quaternions.html + if filter_values: + dot = np.sum(quat) + if dot < 0: + quat = -quat + return [quat[0], quat[1], quat[2], quat[3]] + + +def quaternion_to_euler(quat, rotation_order=DEFAULT_ROTATION_ORDER): + """ + Parameters + ---------- + * q: list of floats + \tQuaternion vector with form: [qw, qx, qy, qz] + + Return + ------ + * euler_angles: list + \tEuler angles in degree with specified order + """ + quat = np.asarray(quat) + quat = quat / np.linalg.norm(quat) + rotmat_quat = quaternion_matrix(quat) + rotation_order_str = rotation_order_to_string(rotation_order) + euler_angles = np.rad2deg(euler_from_matrix(rotmat_quat, rotation_order_str)) + return euler_angles + + +def convert_euler_to_quaternion_frame(bvh_data, e_frame, filter_values=True, animated_joints=None): + """Convert a BVH frame into an ordered dict of quaternions for each skeleton node + Parameters + ---------- + * bvh_data: BVHData + \t Contains skeleton information + * frame_vector: np.ndarray + \t animation keyframe frame represented by Euler angles + * filter_values: Bool + \t enforce a unique rotation representation + + Returns: + ---------- + * quat_frame: OrderedDict that contains a quaternion for each joint + """ + if animated_joints is None: + animated_joints = list(bvh_data.node_names.keys()) + n_dims = len(animated_joints)*4 + quat_frame = np.zeros((n_dims)) + offset = 0 + for node_name in animated_joints: + if bvh_data.get_node_channels(node_name) is not None: + angles, order = bvh_data.get_node_angles(node_name, e_frame) + q = euler_to_quaternion(angles, order, filter_values) + quat_frame[offset:offset+4] = q + offset +=4 + return quat_frame + + +def convert_euler_frames_to_quaternion_frames(bvh_data, euler_frames, filter_values=True, animated_joints=None): + """ + Parameters + ---------- + * bvh_data: BVHData + \t Contains skeleton information + * euler_frames: np.ndarray + \t list of euler frames + Returns: + ---------- + * quater_frames: a list of quaternion frames + """ + if animated_joints is None: + animated_joints = list(bvh_data.node_names.keys()) + quat_frames = [] + prev_frame = None + for e_frame in euler_frames: + q_frame = convert_euler_to_quaternion_frame(bvh_data, e_frame, filter_values, animated_joints=animated_joints) + o = 0 + if prev_frame is not None and filter_values: + for joint_name in animated_joints: + q = check_quat(q_frame[o:o+4], prev_frame[o:o+4]) + q_frame[o:o+4] = q + o+=4 + prev_frame = q_frame + q_frame = np.concatenate([e_frame[:3],q_frame]) + quat_frames.append(q_frame) + return quat_frames + + + +def convert_quaternion_frames_to_euler_frames(quat_frames): + """Returns an nparray of Euler frames + + Parameters + ---------- + * quat_frames: List of quaternion frames + \tQuaternion frames that shall be converted to Euler frames + + Returns + ------- + * euler_frames: numpy array + \tEuler frames + """ + n_frames = len(quat_frames) + assert n_frames > 0 + n_joints = int((len(quat_frames[0])-LEN_ROOT_POS) /LEN_QUAT) + n_params = n_joints*LEN_EULER + LEN_ROOT_POS + euler_frames = np.zeros((n_frames, n_params)) + for frame_idx, quat_frame in enumerate(quat_frames): + euler_frames[frame_idx,:LEN_ROOT_POS] = quat_frame[:LEN_ROOT_POS] + src = LEN_ROOT_POS + dst = LEN_ROOT_POS + for i in range(n_joints): + q = quat_frame[src:src+LEN_QUAT] + euler_frames[frame_idx, dst:dst+LEN_EULER] = quaternion_to_euler(q) + dst += LEN_EULER + src += LEN_QUAT + return euler_frames diff --git a/build/lib/anim_utils/animation_data/skeleton.py b/build/lib/anim_utils/animation_data/skeleton.py new file mode 100644 index 0000000..ad31388 --- /dev/null +++ b/build/lib/anim_utils/animation_data/skeleton.py @@ -0,0 +1,600 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +""" +Created on Tue Jul 14 14:18:37 2015 + +@author: Erik Herrmann, Martin Manns +""" + +import collections +from copy import copy +import json +import numpy as np +from transformations import quaternion_matrix, quaternion_from_matrix +from .constants import ROTATION_TYPE_QUATERNION, ROTATION_TYPE_EULER, LEN_EULER, LEN_ROOT_POS, LEN_QUAT +from ..motion_editing.coordinate_cyclic_descent import run_ccd, normalize, set_global_orientation, run_ccd_look_at, orient_node_to_target_look_at, LOOK_AT_DIR, SPINE_LOOK_AT_DIR, orient_node_to_target_look_at_projected +from .joint_constraints import CONSTRAINT_MAP + + +class Skeleton(object): + """ Data structure that stores the skeleton hierarchy information + extracted from a BVH file with additional meta information. + """ + def __init__(self): + self.animated_joints = [] + self.free_joints_map = dict() + self.skeleton_model = None + self.frame_time = None + self.root = None + self.aligning_root_node = None # Node that defines body orientation. Can be different from the root node. + self.aligning_root_dir = None + self.reference_frame = None + self.reference_frame_length = None + self.nodes = collections.OrderedDict() + self.tool_nodes = [] + self.max_level = -1 + self.parent_dict = dict() + self._chain_names = [] + self.identity_frame = None + self.annotation = None + + def _get_node_desc(self, name): + node_desc = dict() + node = self.nodes[name] + node_desc["name"] = node.node_name + if node.parent is not None: + node_desc["parent"] = node.parent.node_name + else: + node_desc["parent"] = None + node_desc["quaternion_frame_index"] = node.quaternion_frame_index + node_desc["index"] = node.index + if type(node.offset) == np.ndarray: + offset = node.offset.tolist() + else: + offset = node.offset + node_desc["offset"] = offset + node_desc["channels"] = node.channels + node_desc["rotation"] = node.rotation.tolist() + node_desc["fixed"] = node.fixed + node_desc["level"] = node.level + node_desc["node_type"] = node.node_type + node_desc["children"] = [] + for c in node.children: + c_desc = self._get_node_desc(c.node_name) + node_desc["children"].append(c_desc) + return node_desc + + def to_json(self): + data = dict() + data["animated_joints"] = self.animated_joints + data["free_joints_map"] = self.free_joints_map + data["skeleton_model"] = self.skeleton_model + data["frame_time"] = self.frame_time + data["root"] = self._get_node_desc(self.root) + data["reference_frame"] = self.reference_frame.tolist() + data["tool_nodes"] = self.tool_nodes + data["aligning_root_node"] = self.aligning_root_node + return data + + def save_to_json(self, file_name): + with open(file_name, 'w') as outfile: + tmp = json.dumps(self.to_json(), indent=4) + outfile.write(tmp) + outfile.close() + + def is_motion_vector_complete(self, frames, is_quaternion): + if is_quaternion: + rotation_type = ROTATION_TYPE_QUATERNION + else: + rotation_type = ROTATION_TYPE_EULER + return len(frames[0]) == self.get_number_of_frame_parameters(rotation_type) + + def get_number_of_frame_parameters(self, rotation_type): + n_parameters = 0 + for node_name in self.nodes.keys(): + local_parameters = self.nodes[node_name].get_number_of_frame_parameters(rotation_type) + n_parameters += local_parameters + return n_parameters + + def add_fixed_joint_parameters_to_motion(self, reduced_quat_frames): + new_quat_frames = np.zeros((len(reduced_quat_frames), self.reference_frame_length)) + for idx, reduced_frame in enumerate(reduced_quat_frames): + new_quat_frames[idx] = self.add_fixed_joint_parameters_to_frame(reduced_frame) + return new_quat_frames + + def add_fixed_joint_parameters_to_frame(self, reduced_frame): + """ + Takes parameters from the reduced frame for each joint of the complete skeleton found in the reduced skeleton + otherwise it takes parameters from the reference frame + Parameters: + reduced_frame (np.array): pose parameters ignoring fixed joints. + Returns: + new_frame (np.array): pose parameters including fixed joints. + """ + new_frame = np.zeros(self.reference_frame_length) + joint_index = 0 + for joint_name in self.nodes.keys(): + if len(self.nodes[joint_name].children) > 0 and "EndSite" not in joint_name: + if joint_name == self.root: + new_frame[:7] = reduced_frame[:7] + else: + dest_start = joint_index * 4 + 3 + if self.nodes[joint_name].fixed: + new_frame[dest_start: dest_start+4] = self.nodes[joint_name].rotation + else: + src_start = self.nodes[joint_name].quaternion_frame_index * 4 + 3 + new_frame[dest_start: dest_start+4] = reduced_frame[src_start: src_start + 4] + + joint_index += 1 + return new_frame + + + def add_fixed_joint_parameters_to_other_frame(self, reduced_frame, other_animated_joints): + """ + Takes parameters from the reduced frame for each joint of the complete skeleton found in the reduced skeleton + otherwise it takes parameters from the reference frame + Parameters + reduced_frame (np.array): frame ignoring parameters of joints in other_animated_joints. + other_animated_joints (List): names of animated joints. + Returns + new_frame (np.array): frame including fixed joints. + """ + new_frame = np.zeros(self.reference_frame_length) + src_joint_index = 0 + dest_joint_index = 0 + for joint_name in list(self.nodes.keys()): + if len(self.nodes[joint_name].children) > 0 and "EndSite" not in joint_name: + if joint_name == self.root: + new_frame[:7] = reduced_frame[:7] + src_joint_index += 1 + else: + dest_start = dest_joint_index * 4 + 3 + if joint_name in other_animated_joints: + src_start = other_animated_joints.index(joint_name) * 4 + 3 + new_frame[dest_start: dest_start + 4] = reduced_frame[src_start: src_start + 4] + src_joint_index+=1 + else: + new_frame[dest_start: dest_start + 4] = self.nodes[joint_name].rotation + + dest_joint_index += 1 + return new_frame + + def _get_max_level(self): + levels = [node.level for node in list(self.nodes.values())] + if len(levels) > 0: + return max(levels) + else: + return 0 + + def _get_parent_dict(self): + """Returns a dict of node names to their parent node's name""" + + parent_dict = {} + for node_name in list(self.nodes.keys()): + for child_node in self.nodes[node_name].children: + parent_dict[child_node.node_name] = node_name + + return parent_dict + + def gen_all_parents(self, node_name): + """Generator of all parents' node names of node with node_name""" + + while node_name in self.parent_dict: + parent_name = self.parent_dict[node_name] + yield parent_name + node_name = parent_name + + def _set_joint_weights(self): + """ Gives joints weights according to their distance in the joint hierarchy + to the root joint. The further away the smaller the weight. + """ + self.joint_weights = [1.0/(self.nodes[n].level + 1.0) for n in list(self.nodes.keys())] + self.joint_weight_map = collections.OrderedDict() + weight_index = 0 + for node_name in list(self.nodes.keys()): + self.joint_weight_map[node_name] = self.joint_weights[weight_index] + weight_index += 1 + self.joint_weight_map["RightHand"] = 2.0 + self.joint_weight_map["LeftHand"] = 2.0 + self.joint_weights = list(self.joint_weight_map.values()) + + def get_joint_weights(self): + return list(self.joint_weight_map.values()) + + def _generate_chain_names(self): + chain_names = dict() + for node_name in list(self.nodes.keys()): + chain_names[node_name] = list(self.gen_all_parents(node_name)) + # Names are generated bottom to up --> reverse + chain_names[node_name].reverse() + chain_names[node_name] += [node_name] # Node is not in its parent list + return chain_names + + def get_cartesian_coordinates_from_quaternion(self, target_node_name, quaternion_frame, return_global_matrix=False): + """Returns cartesian coordinates for one node at one frame. Modified to + handle frames with omitted values for joints starting with "Bip" + + Parameters + ---------- + + * node_name: String + \tName of node + * skeleton: Skeleton + \tBVH data structure read from a file + + """ + if self.nodes[target_node_name].level == 0: + root_frame_position = quaternion_frame[:3] + root_node_offset = self.nodes[target_node_name].offset + return [t + o for t, o in + zip(root_frame_position, root_node_offset)] + else: + offsets = [list(self.nodes[node_name].offset) + for node_name in self._chain_names[target_node_name]] + root_position = quaternion_frame[:3].flatten() + offsets[0] = [r + o for r, o in zip(root_position, offsets[0])] + j_matrices = [] + count = 0 + for node_name in self._chain_names[target_node_name]: + if len(self.nodes[target_node_name].children) > 0: # check if it is a joint or an end site + index = self.animated_joints.index(node_name) * 4 + 3 + j_matrix = quaternion_matrix(quaternion_frame[index: index + 4]) + j_matrix[:, 3] = offsets[count] + [1] + else: + j_matrix = np.identity(4) + j_matrix[:, 3] = offsets[count] + [1] + j_matrices.append(j_matrix) + count += 1 + + global_matrix = np.identity(4) + for j_matrix in j_matrices: + global_matrix = np.dot(global_matrix, j_matrix) + if return_global_matrix: + return global_matrix + else: + point = np.array([0, 0, 0, 1]) + point = np.dot(global_matrix, point) + return point[:3].tolist() + + def convert_quaternion_frame_to_cartesian_frame(self, quat_frame, node_names=None): + """ + Converts quaternion frames to cartesian frames by calling get_cartesian_coordinates_from_quaternion for each joint + """ + if node_names is None: + node_names = self.nodes.keys() + cartesian_frame = [] + for node_name in node_names: + if node_name in self.nodes: + position = self.nodes[node_name].get_global_position(quat_frame) + cartesian_frame.append(position) + return cartesian_frame + + def clear_cached_global_matrices(self): + for joint in self.nodes.values(): + joint.clear_cached_global_matrix() + + def get_root_reference_orientation(self): + # reference orientation from BVH: 179.477078182 3.34148613293 -87.6482840381 x y z euler angles + return self.reference_frame[3:7] + + def get_joint_indices(self, joint_names): + indices = [] + for name in joint_names: + if name in list(self.nodes.keys()): + node = self.nodes[name] + indices.append(node.index) + return indices + + def get_n_joints(self): + return len([node for node in list(self.nodes.values()) if len(node.channels) > 0]) + + def get_joint_names(self): + return [k for k, n in list(self.nodes.items()) if len(n.children) > 0] + + def scale(self, scale_factor): + for node in list(self.nodes.values()): + node.offset = np.array(node.offset) * scale_factor + + def get_channels(self, euler=False): + channels = collections.OrderedDict() + for node in list(self.nodes.values()): + if node.node_name in self.animated_joints: + node_channels = copy(node.channels) + if not euler: + if np.all([ch in node_channels for ch in ["Xrotation", "Yrotation", "Zrotation"]]): + node_channels += ["Wrotation"] # TODO fix order + channels[node.node_name] = node_channels + return channels + + + def to_unity_format(self, joint_name_map=None, animated_joints=None, scale=1): + """ Converts the skeleton into a custom json format and applies a coordinate transform to the left-handed coordinate system of Unity. + src: http://answers.unity3d.com/questions/503407/need-to-convert-to-right-handed-coordinates.html + """ + if animated_joints is None: + animated_joints = [j for j, n in self.nodes.items() if "EndSite" not in j and len(n.children) > 0] # self.animated_joints + + #print("animated joints", len(animated_joints)) + joint_descs = [] + self.nodes[self.root].to_unity_format(joint_descs, scale, joint_name_map=joint_name_map) + + data = dict() + data["root"] = self.aligning_root_node + data["jointDescs"] = joint_descs + data["jointSequence"] = animated_joints + default_pose = dict() + default_pose["rotations"] = [] + default_pose["translations"] = [] + for node_name in animated_joints: + node = self.nodes[node_name] + q = node.rotation + if len(q) == 4: + r = {"x": -float(q[1]), "y": float(q[2]), "z": float(q[3]), "w": -float(q[0])} + else: + r = {"x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0} + default_pose["rotations"].append(r) + o = node.offset + if node_name == self.root: + o = self.reference_frame[:3] + t = {"x": -float(scale * o[0]), "y": float(scale * o[1]), "z": float(scale * o[2])} + default_pose["translations"].append(t) + + print("new reference", default_pose["rotations"][0]) + data["referencePose"] = default_pose + return data + + def get_reduced_reference_frame(self): + frame = list(self.reference_frame[:3]) + for joint_name in self.animated_joints: + frame += list(self.nodes[joint_name].rotation) + return frame + + def set_reference_frame(self, frame): + self.reference_frame = frame + o = 3 + for joint_name in self.animated_joints: + self.nodes[joint_name].rotation = frame[o:o+4] + o += 4 + + def apply_joint_constraints(self, frame): + for n in self.animated_joints: + if self.nodes[n].joint_constraint is not None: + idx = self.nodes[n].quaternion_frame_index * 4 + 3 + q = frame[idx:idx + 4] + frame[idx:idx + 4] = self.nodes[n].joint_constraint.apply(q) + return frame + + def reach_target_position(self, frame, constraint, eps=0.01, max_iter=50, verbose=False, chain_end_joint=None): + frame, error = run_ccd(self, frame, constraint.joint_name, constraint, eps, max_iter, chain_end_joint, verbose) + print("reached with error", error) + return frame + + def reach_target_positions(self, frame, constraints, chain_end_joints=None, eps=0.0001, n_max_iter=500, ccd_iters=20, verbose=False): + error = np.inf + prev_error = error + n_iters = 0 + is_stuck = False + if chain_end_joints is None: + chain_end_joints = dict() + for c in constraints: + chain_end_joints[c.joint_name] = self.root + while n_iters < n_max_iter and error > eps and not is_stuck: + error = self._reach_target_positions_iteration(frame, constraints, chain_end_joints, eps, n_max_iter, ccd_iters, verbose) + if abs(prev_error - error) < eps: + is_stuck = True + #print("iter", is_stuck, error,eps, prev_error, len(constraints)) + prev_error = error + n_iters += 1 + print("reached with error", error, n_iters) + + # run a last correction step for each constraint separatley + for c in constraints: + root = self.nodes[c.joint_name].parent + d_iter = 1 + while root is not None and root.parent is not None and d_iter < 2: + root = root.parent + if root is not None: + chain_end_joints[c.joint_name] = root.node_name + error = self._reach_target_positions_iteration(frame, [c], chain_end_joints, eps, n_max_iter, ccd_iters, verbose) + n_iters += 1 + print("error in last correction", c.joint_name, error) + return frame + + def _reach_target_positions_iteration(self, frame, constraints, chain_end_joints=None, eps=0.0001, n_max_iter=500, ccd_iters=20, verbose=False): + error = 0 + for c in constraints: + joint_error = 0 + if c.look_at: + pos = c.look_at_pos + if pos is None: + pos = c.position + joint_name = self.skeleton_model["joints"]["head"] + if joint_name is not None: + local_dir = LOOK_AT_DIR + if "look_at_dir" in self.skeleton_model: + local_dir = self.skeleton_model["look_at_dir"] + parent_node = self.nodes[joint_name].parent + if parent_node is not None: + frame = orient_node_to_target_look_at(self, frame, parent_node.node_name, joint_name, pos, local_dir=local_dir) + if c.position is not None and c.relative_parent_joint_name is None: + frame, _joint_error = run_ccd(self, frame, c.joint_name, c, eps, ccd_iters, chain_end_joints[c.joint_name], verbose) + joint_error += _joint_error + elif c.relative_parent_joint_name is not None: # run ccd on relative constraint + #turn relative constraint into a normal constraint + _c = c.instantiate_relative_constraint(self, frame) + #print("create relative constraint", _c.joint_name, c.relative_offset) + frame, _joint_error = run_ccd(self, frame, _c.joint_name, _c, eps, ccd_iters, chain_end_joints[c.joint_name], verbose) + joint_error += _joint_error + elif c.orientation is not None: + frame = set_global_orientation(self, frame, c.joint_name, c.orientation) + else: + print("ignore constraint on", c.joint_name, c.position) + error += joint_error + return error + + def set_joint_orientation(self, frame, joint_name, orientation): + m = quaternion_matrix(orientation) + parent = self.nodes[joint_name].parent + if parent is not None: + parent_m = parent.get_global_matrix(frame, use_cache=False) + local_m = np.dot(np.linalg.inv(parent_m), m) + q = quaternion_from_matrix(local_m) + offset = self.nodes[joint_name].quaternion_frame_index*4+3 + frame[offset:offset+4] = normalize(q) + return frame + + def look_at(self, frame, joint_name, position, eps=0.0001, n_max_iter=1, local_dir=LOOK_AT_DIR, chain_end_joint=None): + #self.clear_cached_global_matrices() + frame, error = run_ccd_look_at(self, frame, joint_name, position, eps, n_max_iter, local_dir, chain_end_joint) + #frame = orient_node_to_target_look_at(self,frame,joint_name, joint_name, position) + return frame + + def look_at_projected(self, frame, joint_name, position, local_dir=LOOK_AT_DIR): + """ apply a 2d rotation around global y """ + twist_axis = None + max_angle = None + p_name = self.nodes[joint_name].parent.node_name + if self.nodes[p_name ].joint_constraint is not None: + twist_axis = self.nodes[p_name ].joint_constraint.axis + max_angle = np.rad2deg(self.nodes[p_name ].joint_constraint.twist_max) + frame = orient_node_to_target_look_at_projected(self, frame, self.nodes[joint_name].parent.node_name, joint_name, position, local_dir, twist_axis, max_angle) + return frame + + def get_reduced_euler_frames(self, euler_frames, has_root=True): + ''' + create reduced size of euler frames based on animated joints + :param euler_frames: n_frames * n_dims + :return: + ''' + nonEndSite_joints = self.get_joints_without_EndSite() + euler_frames = np.asarray(euler_frames) + if has_root: + assert euler_frames.shape[1] == LEN_ROOT_POS + LEN_EULER * len(nonEndSite_joints) + new_euler_frames = np.zeros([euler_frames.shape[0], LEN_ROOT_POS + LEN_EULER * len(self.animated_joints)]) + else: + assert euler_frames.shape[1] == LEN_EULER * len(nonEndSite_joints) + new_euler_frames = np.zeros([euler_frames.shape[0], LEN_EULER * len(self.animated_joints)]) + for i in range(euler_frames.shape[0]): + new_euler_frames[i] = self.get_reduced_euler_frame(euler_frames[i], has_root=has_root) + return new_euler_frames + + def get_reduced_euler_frame(self, euler_frame, has_root=True): + ''' + create a reduced size of euler frame based on animated joints + :param euler_frames: n_frames * n_dims + :return: + ''' + if has_root: + new_euler_frame = np.zeros(LEN_ROOT_POS + LEN_EULER * len(self.animated_joints)) + new_euler_frame[:LEN_ROOT_POS] = euler_frame[:LEN_ROOT_POS] + i = 0 + for joint in self.animated_joints: + if joint == self.root: + new_euler_frame[LEN_ROOT_POS+i*LEN_EULER: LEN_ROOT_POS+(i+1)*LEN_EULER] = euler_frame[self.nodes[joint].channel_indices[LEN_ROOT_POS:]] + else: + new_euler_frame[LEN_ROOT_POS+i*LEN_EULER: LEN_ROOT_POS+(i+1)*LEN_EULER] = euler_frame[self.nodes[joint].channel_indices] + i += 1 + else: + new_euler_frame = np.zeros(LEN_EULER * len(self.animated_joints)) + i = 0 + for joint in self.animated_joints: + if joint == self.root: + new_euler_frame[LEN_ROOT_POS + i * LEN_EULER: LEN_ROOT_POS + (i + 1) * LEN_EULER] = euler_frame[ + self.nodes[joint].channel_indices[LEN_ROOT_POS:]] + else: + new_euler_frame[LEN_ROOT_POS + i * LEN_EULER: LEN_ROOT_POS + (i + 1) * LEN_EULER] = euler_frame[ + self.nodes[joint].channel_indices] + i += 1 + return new_euler_frame + + def get_joints_without_EndSite(self): + joints = [] + for joint in self.nodes.keys(): + if 'EndSite' not in joint: + joints.append(joint) + return joints + + def get_body_hip2foot_height(self): + """ helper function for autoscale by gemlongman""" + if self.skeleton_model is None: + print("skeleton.skeleton_model is None") + return None + joint_pelvis = self.skeleton_model["joints"]["pelvis"] # upperleg maybe better + joint_right_toe = self.skeleton_model["joints"]["right_ankle"]# ["right_toe"] + ident_f = self.identity_frame + joint_pelvis_p = self.nodes[joint_pelvis].get_global_position(ident_f) + joint_right_toe_p = self.nodes[joint_right_toe].get_global_position(ident_f) + delta = joint_pelvis_p - joint_right_toe_p + #it should be same dircetion as body up,there is a simple way to get length + if delta[1] < 0.01: + return max(abs(delta.max()), abs(delta.min())) + return delta[1] + + def add_constraints(self, joint_constraints): + joint_map = self.skeleton_model["joints"] + for j in joint_constraints: + if j in joint_map: + skel_j = joint_map[j] + else: + continue + if skel_j not in self.nodes: + continue + c_desc = joint_constraints[j] + if "stiffness" in c_desc: + self.nodes[skel_j].stiffness = c_desc["stiffness"] + c_type = c_desc["type"] + c = None + if c_type in CONSTRAINT_MAP: + c = CONSTRAINT_MAP[c_type].from_dict(skel_j, c_desc) + print("add"+c_desc["type"] +"constraint to", skel_j) + self.nodes[skel_j].joint_constraint = c + + + def get_bounding_box(self, frame=None): + """ Calculate axis aligned bounding box. + Parameters: + frame (np.array): pose parameters for the skeleton. + Returns: + tuple(min_p, max_p): minimum and maximum points. + """ + if frame is None and self.reference_frame is not None: + frame = self.reference_frame + else: + raise Exception("No frame given and reference_frame is None") + min_p = np.ones((3))*np.inf + max_p = np.ones((3))*-np.inf + for k, n in self.nodes.items(): + p = n.get_global_position(frame) + if p[0] >= max_p[0]: + max_p[0] = p[0] + elif p[0] < min_p[0]: + min_p[0] = p[0] + if p[1] >= max_p[1]: + max_p[1] = p[1] + elif p[1] < min_p[1]: + min_p[1] = p[1] + if p[2] >= max_p[2]: + max_p[2] = p[2] + elif p[2] < min_p[2]: + min_p[2] = p[2] + return min_p, max_p diff --git a/build/lib/anim_utils/animation_data/skeleton_builder.py b/build/lib/anim_utils/animation_data/skeleton_builder.py new file mode 100644 index 0000000..a1fc7e8 --- /dev/null +++ b/build/lib/anim_utils/animation_data/skeleton_builder.py @@ -0,0 +1,529 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import collections +from copy import deepcopy +import json +import numpy as np + +from .skeleton import Skeleton +from .skeleton_node import SkeletonRootNode, SkeletonJointNode, SkeletonEndSiteNode, SKELETON_NODE_TYPE_JOINT, SKELETON_NODE_TYPE_END_SITE +from .quaternion_frame import convert_euler_to_quaternion_frame +from .joint_constraints import HingeConstraint2 +from .acclaim import asf_to_bvh_channels + + +DEFAULT_ROOT_DIR = [0, 0, 1] + + +def create_identity_frame(skeleton): + skeleton.identity_frame = np.zeros(skeleton.reference_frame_length) + offset = 3 + for j in skeleton.animated_joints: + skeleton.identity_frame[offset:offset + 4] = [1, 0, 0, 0] + offset += 4 + + +def create_euler_frame_indices(skeleton): + nodes_without_endsite = [node for node in list(skeleton.nodes.values()) if node.node_type != SKELETON_NODE_TYPE_END_SITE] + for node in nodes_without_endsite: + node.euler_frame_index = nodes_without_endsite.index(node) + + +def read_reference_frame_from_bvh_reader(bvh_reader, frame_index=0): + quat_frame = convert_euler_to_quaternion_frame(bvh_reader, bvh_reader.frames[frame_index], False, animated_joints=None) + return np.array(bvh_reader.frames[0][:3].tolist() + quat_frame.tolist()) + + +def add_tool_nodes(skeleton, node_names, new_tool_bones): + for b in new_tool_bones: + add_new_end_site(skeleton, node_names, b["parent_node_name"], b["new_node_offset"]) + skeleton.tool_nodes.append(b["new_node_name"]) + + +def add_new_end_site(skeleton, node_names, parent_node_name, offset): + if parent_node_name in list(node_names.keys()): + level = node_names[parent_node_name]["level"] + 1 + node_desc = dict() + node_desc["level"] = level + node_desc["offset"] = offset + +def reference_frame_from_unity(data): + n_j = len(data["rotations"]) + q_frame = np.zeros(n_j*4+3) + q_frame[:3] = arr_from_unity_t(data["translations"][0]) + o = 3 + for q in data["rotations"]: + q_frame[o:o+4] = arr_from_unity_q(q) + o+=4 + return q_frame + + +def generate_reference_frame(skeleton, animated_joints): + identity_frame = [0,0,0] + frame = [0, 0, 0] + joint_idx = 0 + node_list = [(skeleton.nodes[n].index, n) for n in skeleton.nodes.keys() if skeleton.nodes[n].index >= 0] + node_list.sort() + for idx, node in node_list: + frame += list(skeleton.nodes[node].rotation) + if node in animated_joints: + identity_frame += [1.0,0.0,0.0,0.0] + skeleton.nodes[node].quaternion_frame_index = joint_idx + joint_idx += 1 + else: + skeleton.nodes[node].quaternion_frame_index = -1 + skeleton.reference_frame = np.array(frame) + skeleton.reference_frame_length = len(frame) + skeleton.identity_frame = np.array(identity_frame) + + +def arr_from_unity_q(_q): + q = np.zeros(4) + q[0] = - _q["w"] + q[1] = - _q["x"] + q[2] = _q["y"] + q[3] = _q["z"] + return q + + +def arr_from_unity_t(_t): + t = np.zeros(3) + t[0] = - _t["x"] + t[1] = _t["y"] + t[2] = _t["z"] + return t + +class SkeletonBuilder(object): + + def load_from_bvh(self, bvh_reader, animated_joints=None, reference_frame=None, skeleton_model=None, tool_bones=None): + skeleton = Skeleton() + if animated_joints is None: + animated_joints = list(bvh_reader.get_animated_joints()) + skeleton.animated_joints = animated_joints + skeleton.frame_time = bvh_reader.frame_time + skeleton.root = bvh_reader.root + skeleton.aligning_root_dir = DEFAULT_ROOT_DIR + if reference_frame is None: + skeleton.reference_frame = read_reference_frame_from_bvh_reader(bvh_reader) + else: + skeleton.reference_frame = reference_frame + skeleton.reference_frame_length = len(skeleton.reference_frame) + skeleton.tool_nodes = [] + if tool_bones is not None: + add_tool_nodes(skeleton, bvh_reader.node_names, tool_bones) + skeleton.nodes = collections.OrderedDict() + joint_list = list(bvh_reader.get_animated_joints()) + self.construct_hierarchy_from_bvh(skeleton, joint_list, bvh_reader.node_names, skeleton.root, 0) + + create_euler_frame_indices(skeleton) + SkeletonBuilder.set_meta_info(skeleton) + + if skeleton_model is not None: + skeleton.skeleton_model = skeleton_model + #skeleton.add_heels(skeleton_model) + return skeleton + + def construct_hierarchy_from_bvh(self, skeleton, joint_list, node_info, node_name, level): + + channels = [] + if "channels" in node_info[node_name]: + channels = node_info[node_name]["channels"] + is_fixed = True + quaternion_frame_index = -1 + if node_name in skeleton.animated_joints: + is_fixed = False + quaternion_frame_index = skeleton.animated_joints.index(node_name) + joint_index = -1 + if node_name in joint_list: + joint_index = joint_list.index(node_name) + if node_name == skeleton.root: + node = SkeletonRootNode(node_name, channels, None, level) + elif "children" in list(node_info[node_name].keys()) and len(node_info[node_name]["children"]) > 0: + node = SkeletonJointNode(node_name, channels, None, level) + offset = joint_index * 4 + 3 + node.rotation = skeleton.reference_frame[offset: offset + 4] + else: + node = SkeletonEndSiteNode(node_name, channels, None, level) + node.fixed = is_fixed + node.quaternion_frame_index = quaternion_frame_index + node.index = joint_index + node.offset = node_info[node_name]["offset"] + skeleton.nodes[node_name] = node + if "children" in node_info[node_name]: + for c in node_info[node_name]["children"]: + c_node = self.construct_hierarchy_from_bvh(skeleton, joint_list, node_info, c, level+1) + c_node.parent = node + node.children.append(c_node) + return node + + def load_from_json_file(self, filename): + with open(filename) as infile: + data = json.load(infile) + skeleton = self.load_from_custom_unity_format(data) + return skeleton + + def load_from_custom_unity_format(self, data, frame_time=1.0/30, add_extra_end_site=False): + skeleton = Skeleton() + animated_joints = data["jointSequence"] + if len(animated_joints) == 0: + print("Error no joints defined") + return skeleton + + print("load from json", len(animated_joints)) + skeleton.animated_joints = animated_joints + + skeleton.skeleton_model = collections.OrderedDict() + skeleton.skeleton_model["joints"] = dict() + if "head_joint" in data: + skeleton.skeleton_model["joints"]["head"] = data["head_joint"] + if "neck_joint" in data: + skeleton.skeleton_model["joints"]["neck"] = data["neck_joint"] + + skeleton.frame_time = frame_time + skeleton.nodes = collections.OrderedDict() + skeleton.root = animated_joints[0] + root = self._create_node_from_unity_desc(skeleton, skeleton.root, data, None, 0, add_extra_end_site) + + SkeletonBuilder.set_meta_info(skeleton) + create_euler_frame_indices(skeleton) + if "root" in data: + skeleton.aligning_root_node = data["root"] + else: + skeleton.aligning_root_node = skeleton.root + skeleton.aligning_root_dir = DEFAULT_ROOT_DIR + + skeleton.reference_frame = reference_frame_from_unity(data["referencePose"]) + print("reference", skeleton.reference_frame[3:7],data["referencePose"]["rotations"][0]) + skeleton.reference_frame_length = len(skeleton.reference_frame) + return skeleton + + def load_from_json_data(self, data, animated_joints=None, use_all_joints=False): + def extract_animated_joints(node, animated_joints): + animated_joints.append(node["name"]) + for c in node["children"]: + if c["index"] >= 0 and len(c["children"]) > 0: + extract_animated_joints(c, animated_joints) + + skeleton = Skeleton() + if animated_joints is not None: + skeleton.animated_joints = animated_joints + elif "animated_joints" in data: + skeleton.animated_joints = data["animated_joints"] + else: + animated_joints = list() + extract_animated_joints(data["root"], animated_joints) + skeleton.animated_joints = animated_joints + + skeleton.free_joints_map = data.get("free_joints_map", dict()) + if "skeleton_model" in data: + skeleton.skeleton_model = data["skeleton_model"] + else: + skeleton.skeleton_model = collections.OrderedDict() + skeleton.skeleton_model["joints"] = dict() + if "head_joint" in data: + skeleton.skeleton_model["joints"]["head"] = data["head_joint"] + if "neck_joint" in data: + skeleton.skeleton_model["joints"]["neck"] = data["neck_joint"] + + skeleton.frame_time = data["frame_time"] + skeleton.nodes = collections.OrderedDict() + root = self._create_node_from_desc(skeleton, data["root"], None, 0) + skeleton.root = root.node_name + if "tool_nodes" in data: + skeleton.tool_nodes = data["tool_nodes"] + create_euler_frame_indices(skeleton) + skeleton.aligning_root_node = data.get("aligning_root_node",skeleton.root) + skeleton.aligning_root_dir = data.get("aligning_root_dir",DEFAULT_ROOT_DIR) + if "reference_frame" in data: + skeleton.reference_frame = data["reference_frame"] + skeleton.reference_frame_length = len(skeleton.reference_frame) + if skeleton.reference_frame is None or use_all_joints: + generate_reference_frame(skeleton, skeleton.animated_joints) + SkeletonBuilder.set_meta_info(skeleton) + return skeleton + + def load_from_fbx_data(self, data): + skeleton = Skeleton() + skeleton.nodes = collections.OrderedDict() + skeleton.animated_joints = data["animated_joints"] + skeleton.root = data["root"] + self._create_node_from_fbx_node_desc(skeleton, data, skeleton.root, None, 0) + skeleton.frame_time = data["frame_time"] + SkeletonBuilder.set_meta_info(skeleton) + return skeleton + + def _create_node_from_fbx_node_desc(self, skeleton, data, node_name, parent, level=0): + node_data = data["nodes"][node_name] + channels = node_data["channels"] + if parent is None: + node = SkeletonRootNode(node_name, channels, parent, level) + elif node_data["node_type"] == SKELETON_NODE_TYPE_JOINT: + node = SkeletonJointNode(node_name, channels, parent, level) + else: + node = SkeletonEndSiteNode(node_name, channels, parent, level) + node.fixed = node_data["fixed"] + node.index = node_data["index"] + node.offset = np.array(node_data["offset"]) + node.rotation = np.array(node_data["rotation"]) + if node_name in skeleton.animated_joints: + node.quaternion_frame_index = skeleton.animated_joints.index(node_name) + node.fixed = False + else: + node.quaternion_frame_index = -1 + node.fixed = True + node.children = [] + skeleton.nodes[node_name] = node + for c_name in node_data["children"]: + c_node = self._create_node_from_fbx_node_desc(skeleton, data, c_name, node, level+1) + node.children.append(c_node) + + return node + + def _create_node_from_desc(self, skeleton, data, parent, level): + node_name = data["name"] + channels = data["channels"] + if parent is None: + node = SkeletonRootNode(node_name, channels, parent, level) + elif data["node_type"] == SKELETON_NODE_TYPE_JOINT: + node = SkeletonJointNode(node_name, channels, parent, level) + else: + node = SkeletonEndSiteNode(node_name, channels, parent, level) + # node.fixed = data["fixed"] + node.index = data["index"] + node.offset = np.array(data["offset"]) + node.rotation = np.array(data["rotation"]) + if node_name in skeleton.animated_joints: + node.quaternion_frame_index = skeleton.animated_joints.index(node_name) + node.fixed = False + else: + node.quaternion_frame_index = -1 + node.fixed = True + + skeleton.nodes[node_name] = node + skeleton.nodes[node_name].children = [] + for c_desc in data["children"]: + skeleton.nodes[node_name].children.append(self._create_node_from_desc(skeleton, c_desc, node, level+1)) + return node + + def get_joint_desc(self, data, name): + for idx in range(len(data["jointDescs"])): + if data["jointDescs"][idx]["name"] == name: + return data["jointDescs"][idx] + return None + + def _create_node_from_unity_desc(self, skeleton, node_name, data, parent, level, add_extra_end_site=False): + node_data = self.get_joint_desc(data, node_name) + if node_data is None: + return + + if parent is None: + channels = ["Xposition","Yposition","Zposition", "Xrotation","Yrotation","Zrotation"] + else:# len(node_data["children"]) > 0: + channels = ["Xrotation","Yrotation","Zrotation"] + + if parent is None: + node = SkeletonRootNode(node_name, channels, parent, level) + else:# len(node_data["children"]) > 0: + node = SkeletonJointNode(node_name, channels, parent, level) + node.offset = np.array(node_data["offset"]) + node.offset[0] *= -1 + node.rotation = np.array(node_data["rotation"]) + node.rotation[0] *= -1 + node.rotation[1] *= -1 + + if node_name in skeleton.animated_joints: + node.quaternion_frame_index = skeleton.animated_joints.index(node_name) + node.fixed = False + else: + node.quaternion_frame_index = -1 + node.fixed = True + + skeleton.nodes[node_name] = node + skeleton.nodes[node_name].children = [] + if len(node_data["children"]) > 0: + node.index = node.quaternion_frame_index + for c_name in node_data["children"]: + c_node = self._create_node_from_unity_desc(skeleton, c_name, data, node, level+1) + if c_node is not None: + skeleton.nodes[node_name].children.append(c_node) + if add_extra_end_site: + print("add extra end site") + node.index = -1 + channels = [] + c_name = node_name+"_EndSite" + c_node = SkeletonEndSiteNode(c_name, channels, node, level+1) + skeleton.nodes[node_name].children.append(c_node) + skeleton.nodes[c_name] = c_node + return node + + + @classmethod + def construct_arm_with_constraints(cls, n_joints, length): + skeleton = Skeleton() + skeleton.frame_time = 1 / 30 + animated_joints = [] + animated_joints.append("root") + skeleton.root = "root" + channels = ["rotationX", "rotationY", "rotationZ", "rotationW"] + node = SkeletonRootNode("root", channels, None, 0) + node.fixed = False + node.index = 0 + node.offset = [0, 0, 0] + node.rotation = [1, 0, 0, 0] + node.quaternion_frame_index = 0 + skeleton.nodes["root"] = node + parent = node + swing_axis = np.array([0,0,1]) + twist_axis = np.array([0, 1, 0]) + + angle_range = [0,90] + for n in range(1, n_joints + 1): # start after the root joint and add one endsite + if n + 1 < n_joints + 1: + node_name = "joint" + str(n) + node = SkeletonJointNode(node_name, channels, parent, n) + animated_joints.append(node_name) + node.fixed = False + node.quaternion_frame_index = n + node.index = n + node.offset = np.array([0, length, 0], dtype=np.float) + print("create", node_name, node.offset) + if n in [1]: + node.joint_constraint = HingeConstraint2(swing_axis, twist_axis) + else: + node_name = "joint" + str(n - 1) + "_EndSite" + node = SkeletonEndSiteNode(node_name, channels, parent, n) + node.fixed = True + node.quaternion_frame_index = -1 + node.index = -1 + node.offset = np.array([0, 0, 0], dtype=np.float) + print("create", node_name, node.offset) + parent.children.append(node) + + node.rotation = [1, 0, 0, 0] + skeleton.nodes[node_name] = node + parent = node + + skeleton.animated_joints = animated_joints + SkeletonBuilder.set_meta_info(skeleton) + return skeleton + + + @classmethod + def generate_reference_frame(cls, skeleton): + n_animated_joints = len(skeleton.animated_joints) + reference_frame = np.zeros(n_animated_joints * 4 + 3) + o = 3 + for n in skeleton.animated_joints: + reference_frame[o:o + 4] = skeleton.nodes[n].rotation + o += 4 + return reference_frame + + + + def load_from_asf_data(self, data, frame_time=1.0/30): + skeleton = Skeleton() + animated_joints = ["root"]+list(data["bones"].keys()) + + print("load from asf", len(animated_joints)) + skeleton.animated_joints =animated_joints + skeleton.skeleton_model = collections.OrderedDict() + skeleton.skeleton_model["joints"] = dict() + + skeleton.frame_time = frame_time + skeleton.nodes = collections.OrderedDict() + skeleton.root = "root" + if skeleton.root is None: + skeleton.root = animated_joints[0] + root = self._create_node_from_asf_data(skeleton, skeleton.root, data, None, 0) + + SkeletonBuilder.set_meta_info(skeleton) + return skeleton + + def _create_node_from_asf_data(self, skeleton, node_name, data, parent, level): + if parent is None: + channels = asf_to_bvh_channels(data["root"]["order"]) + node = SkeletonRootNode(node_name, channels, parent, level) + node.format_meta_data = data["root"] + node.quaternion_frame_index = 0 + node.fixed = False + node.format_meta_data["asf_channels"] = deepcopy(node.rotation_order) + elif "dof" in data["bones"][node_name]: + channels = asf_to_bvh_channels(data["bones"][node_name]["dof"]) + asf_channels = deepcopy(channels) + if len(channels) < 3: + channels = ['Xrotation','Yrotation','Zrotation'] + node = SkeletonJointNode(node_name, channels, parent, level) + node.format_meta_data = data["bones"][node_name] + node.format_meta_data["asf_channels"] = asf_channels + if "offset" in data["bones"][node_name]: + node.offset = data["bones"][node_name]["offset"] + #if parent.node_name is not "root": + # node.offset = np.array(data["bones"][parent.node_name]["direction"]) + # node.offset *= data["bones"][parent.node_name]["length"] + if node_name in skeleton.animated_joints: + node.quaternion_frame_index = skeleton.animated_joints.index(node_name) + node.fixed = False + else: + channels = [] + node = SkeletonJointNode(node_name, channels, parent, level) + node.format_meta_data = data["bones"][node_name] + node.quaternion_frame_index = -1 + node.fixed = True + node.format_meta_data["asf_channels"] = channels + + n_children = 0 + if node_name in data["children"]: + n_children = len(data["children"][node_name]) + + skeleton.nodes[node_name] = node + skeleton.nodes[node_name].children = [] + if n_children > 0: + node.index = node.quaternion_frame_index + for c_name in data["children"][node_name]: + c_node = self._create_node_from_asf_data(skeleton, c_name, data, node, level+1) + if c_node is not None: + skeleton.nodes[node_name].children.append(c_node) + else: + node.index = node.quaternion_frame_index + channels = [] + end_site_name = node_name +"EndSite" + end_site_node = SkeletonJointNode(end_site_name, channels, node, level+1) + end_site_node.quaternion_frame_index = -1 + end_site_node.fixed = True + skeleton.nodes[end_site_name] = end_site_node + skeleton.nodes[end_site_name].children = [] + skeleton.nodes[node_name].children.append(end_site_node) + return node + + + @classmethod + def set_meta_info(cls, skeleton): + skeleton.max_level = skeleton._get_max_level() + skeleton._set_joint_weights() + skeleton.parent_dict = skeleton._get_parent_dict() + skeleton._chain_names = skeleton._generate_chain_names() + skeleton.aligning_root_node = skeleton.root + if skeleton.reference_frame is None: + skeleton.reference_frame = SkeletonBuilder.generate_reference_frame(skeleton) + skeleton.reference_frame_length = len(skeleton.reference_frame) + create_identity_frame(skeleton) diff --git a/build/lib/anim_utils/animation_data/skeleton_models.py b/build/lib/anim_utils/animation_data/skeleton_models.py new file mode 100644 index 0000000..80b957a --- /dev/null +++ b/build/lib/anim_utils/animation_data/skeleton_models.py @@ -0,0 +1,1314 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +""" This module contains mappings from a standard set of human joints to different skeletons + +""" +import numpy as np +import collections + + + +ROCKETBOX_TOOL_BONES = [{ + "new_node_name": 'LeftToolEndSite', + "parent_node_name": 'LeftHand', + "new_node_offset": [6.1522069, -0.09354633, 3.33790343] +}, { + "new_node_name": 'RightToolEndSite', + "parent_node_name": 'RightHand', + "new_node_offset": [6.1522069, 0.09354633, 3.33790343] +}, { + "new_node_name": 'RightScrewDriverEndSite', + "parent_node_name": 'RightHand', + "new_node_offset": [22.1522069, -9.19354633, 3.33790343] +}, { + "new_node_name": 'LeftScrewDriverEndSite', + "parent_node_name": 'LeftHand', + "new_node_offset": [22.1522069, 9.19354633, 3.33790343] +} +] +ROCKETBOX_FREE_JOINTS_MAP = {"LeftHand": ["Spine", "LeftArm", "LeftForeArm"], + "RightHand": ["Spine", "RightArm", "RightForeArm"], + "LeftToolEndSite": ["Spine", "LeftArm", "LeftForeArm"], + "RightToolEndSite": ["Spine", "RightArm", "RightForeArm"], # , "RightHand" + "Head": [], + "RightScrewDriverEndSite": ["Spine", "RightArm", "RightForeArm"], + "LeftScrewDriverEndSite": ["Spine", "LeftArm", "LeftForeArm"] + } +ROCKETBOX_REDUCED_FREE_JOINTS_MAP = {"LeftHand": ["LeftArm", "LeftForeArm"], + "RightHand": ["RightArm", "RightForeArm"], + "LeftToolEndSite": ["LeftArm", "LeftForeArm"], + "RightToolEndSite": ["RightArm", "RightForeArm"], + "Head": [], + "RightScrewDriverEndSite": ["RightArm", "RightForeArm"], + "LeftScrewDriverEndSite": ["LeftArm", "LeftForeArm"] + } +DEG2RAD = np.pi / 180 +hand_bounds = [{"dim": 0, "min": 30 * DEG2RAD, "max": 180 * DEG2RAD}, + {"dim": 1, "min": -15 * DEG2RAD, "max": 120 * DEG2RAD}, + {"dim": 1, "min": -40 * DEG2RAD, "max": 40 * DEG2RAD}] + + +ROCKETBOX_BOUNDS = {"LeftArm": [], # {"dim": 1, "min": 0, "max": 90} + "RightArm": [] # {"dim": 1, "min": 0, "max": 90},{"dim": 0, "min": 0, "max": 90} + , "RightHand": hand_bounds, # [[-90, 90],[0, 0],[-90,90]] + "LeftHand": hand_bounds # [[-90, 90],[0, 0],[-90,90]] + } + +ROCKETBOX_ANIMATED_JOINT_LIST = ["Hips", "Spine", "Spine_1", "Neck", "Head", "LeftShoulder", "LeftArm", "LeftForeArm", + "LeftHand", "RightShoulder", "RightArm", "RightForeArm", "RightHand", "LeftUpLeg", + "LeftLeg", "LeftFoot", "RightUpLeg", "RightLeg", "RightFoot"] + + +IK_CHAINS_DEFAULT_SKELETON = dict() +IK_CHAINS_DEFAULT_SKELETON["right_ankle"] = {"root": "right_hip", "joint": "right_knee", "joint_axis": [1, 0, 0], "end_effector_dir": [0,0,1]} +IK_CHAINS_DEFAULT_SKELETON["left_ankle"] = {"root": "left_hip", "joint": "left_knee", "joint_axis": [1, 0, 0], "end_effector_dir": [0,0,1]} + + +IK_CHAINS_RAW_SKELETON = dict() +IK_CHAINS_RAW_SKELETON["RightFoot"] = {"root": "RightUpLeg", "joint": "RightLeg", "joint_axis": [1, 0, 0], "end_effector_dir": [0,0,1]} +IK_CHAINS_RAW_SKELETON["LeftFoot"] = {"root": "LeftUpLeg", "joint": "LeftLeg", "joint_axis": [1, 0, 0], "end_effector_dir": [0,0,1]} + +#IK_CHAINS_RAW_SKELETON["RightToeBase"] = {"root": "RightUpLeg", "joint": "RightLeg", "joint_axis": [1, 0, 0], "end_effector_dir": [0,0,1]} +#IK_CHAINS_RAW_SKELETON["LeftToeBase"] = {"root": "LeftUpLeg", "joint": "LeftLeg", "joint_axis": [1, 0, 0], "end_effector_dir": [0,0,1]} + +IK_CHAINS_RAW_SKELETON["RightHand"] = {"root": "RightArm", "joint": "RightForeArm", "joint_axis": [0, 1, 0], "end_effector_dir": [1,0,0]} +IK_CHAINS_RAW_SKELETON["LeftHand"] = {"root": "LeftArm", "joint": "LeftForeArm", "joint_axis": [0, 1, 0], "end_effector_dir": [1,0,0]} + + +IK_CHAINS_ROCKETBOX_SKELETON = dict() +IK_CHAINS_ROCKETBOX_SKELETON["RightFoot"] = {"root": "RightUpLeg", "joint": "RightLeg", "joint_axis": [0, 1, 0], "end_effector_dir": [0,0,1]} +IK_CHAINS_ROCKETBOX_SKELETON["LeftFoot"] = {"root": "LeftUpLeg", "joint": "LeftLeg", "joint_axis": [0, 1, 0], "end_effector_dir": [0,0,1]} +IK_CHAINS_ROCKETBOX_SKELETON["RightHand"] = {"root": "RightArm", "joint": "RightForeArm", "joint_axis": [0, 1, 0], "end_effector_dir": [1,0,0]} +IK_CHAINS_ROCKETBOX_SKELETON["LeftHand"] = {"root": "LeftArm", "joint": "LeftForeArm", "joint_axis": [0, 1, 0], "end_effector_dir": [1,0,0]} + + +IK_CHAINS_GAME_ENGINE_SKELETON = dict() +IK_CHAINS_GAME_ENGINE_SKELETON["foot_l"] = {"root": "thigh_l", "joint": "calf_l", "joint_axis": [1, 0, 0], "end_effector_dir": [0,0,1]} +IK_CHAINS_GAME_ENGINE_SKELETON["foot_r"] = {"root": "thigh_r", "joint": "calf_r", "joint_axis": [1, 0, 0], "end_effector_dir": [0,0,1]} +IK_CHAINS_GAME_ENGINE_SKELETON["hand_r"] = {"root": "upperarm_r", "joint": "lowerarm_r", "joint_axis": [1, 0, 0], "end_effector_dir": [1,0,0]} +IK_CHAINS_GAME_ENGINE_SKELETON["hand_l"] = {"root": "upperarm_l", "joint": "lowerarm_l", "joint_axis": [1, 0, 0], "end_effector_dir": [1,0,0]} + + + +IK_CHAINS_CUSTOM_SKELETON = dict() +IK_CHAINS_CUSTOM_SKELETON["L_foot_jnt"] = {"root": "L_upLeg_jnt", "joint": "L_lowLeg_jnt", "joint_axis": [1, 0, 0], "end_effector_dir": [0,0,1]} +IK_CHAINS_CUSTOM_SKELETON["R_foot_jnt"] = {"root": "R_upLeg_jnt", "joint": "R_lowLeg_jnt", "joint_axis": [1, 0, 0], "end_effector_dir": [0,0,1]} +IK_CHAINS_CUSTOM_SKELETON["R_hand_jnt"] = {"root": "R_upArm_jnt", "joint": "R_lowArm_jnt", "joint_axis": [1, 0, 0], "end_effector_dir": [1,0,0]} +IK_CHAINS_CUSTOM_SKELETON["L_hand_jnt"] = {"root": "L_upArm_jnt", "joint": "L_lowArm_jnt", "joint_axis": [1, 0, 0], "end_effector_dir": [1,0,0]} + + +RIGHT_SHOULDER = "RightShoulder" +RIGHT_ELBOW = "RightElbow" +RIGHT_WRIST = "RightHand" +ELBOW_AXIS = [0,1,0] + +LOCOMOTION_ACTIONS = ["walk", "carryRight", "carryLeft", "carryBoth"] +DEFAULT_WINDOW_SIZE = 20 +LEFT_FOOT = "LeftFoot" +RIGHT_FOOT = "RightFoot" +RIGHT_TOE = "RightToeBase" +LEFT_TOE = "LeftToeBase" +LEFT_HEEL = "LeftHeel" +RIGHT_HEEL = "RightHeel" + +OFFSET = 0 +RAW_SKELETON_FOOT_JOINTS = [RIGHT_TOE, LEFT_TOE, RIGHT_HEEL,LEFT_HEEL] +HEEL_OFFSET = [0, -6.480602, 0] +RAW_SKELETON_JOINTS = collections.OrderedDict() +RAW_SKELETON_JOINTS["root"] = "Hips" +RAW_SKELETON_JOINTS["pelvis"] = "Hips" +RAW_SKELETON_JOINTS["spine"] = "Spine" +RAW_SKELETON_JOINTS["spine_1"] = "Spine1" +RAW_SKELETON_JOINTS["left_clavicle"] = "LeftShoulder" +RAW_SKELETON_JOINTS["right_clavicle"] = "RightShoulder" +RAW_SKELETON_JOINTS["left_shoulder"] = "LeftArm" +RAW_SKELETON_JOINTS["right_shoulder"] = "RightArm" +RAW_SKELETON_JOINTS["left_elbow"] = "LeftForeArm" +RAW_SKELETON_JOINTS["right_elbow"] = "RightForeArm" +RAW_SKELETON_JOINTS["left_wrist"] = "LeftHand" +RAW_SKELETON_JOINTS["right_wrist"] = "RightHand" +RAW_SKELETON_JOINTS["left_hip"] = "LeftUpLeg" +RAW_SKELETON_JOINTS["right_hip"] = "RightUpLeg" +RAW_SKELETON_JOINTS["left_knee"] = "LeftLeg" +RAW_SKELETON_JOINTS["right_knee"] = "RightLeg" +RAW_SKELETON_JOINTS["left_ankle"] = "LeftFoot" +RAW_SKELETON_JOINTS["right_ankle"] = "RightFoot" +RAW_SKELETON_JOINTS["left_toe"] = "LeftToeBase" +RAW_SKELETON_JOINTS["right_toe"] = "RightToeBase" +RAW_SKELETON_JOINTS["left_heel"] = "LeftHeel" +RAW_SKELETON_JOINTS["right_heel"] = "RightHeel" +RAW_SKELETON_JOINTS["neck"] = "Neck" +RAW_SKELETON_JOINTS["head"] = "Head" +RAW_SKELETON_MODEL = collections.OrderedDict() +RAW_SKELETON_MODEL["joints"] = RAW_SKELETON_JOINTS +RAW_SKELETON_MODEL["foot_joints"] = RAW_SKELETON_FOOT_JOINTS +RAW_SKELETON_MODEL["heel_offset"] = [0, -6.480602, 0] +RAW_SKELETON_MODEL["ik_chains"] = IK_CHAINS_RAW_SKELETON + + + +GAME_ENGINE_JOINTS = collections.OrderedDict() +GAME_ENGINE_JOINTS["root"] = "Root" +GAME_ENGINE_JOINTS["pelvis"] = "pelvis" +GAME_ENGINE_JOINTS["spine"] = "spine_01" +GAME_ENGINE_JOINTS["spine_1"] = "spine_02" +GAME_ENGINE_JOINTS["spine_2"] = "spine_03" +GAME_ENGINE_JOINTS["left_clavicle"] = "clavicle_l" +GAME_ENGINE_JOINTS["right_clavicle"] = "clavicle_r" +GAME_ENGINE_JOINTS["left_shoulder"] = "upperarm_l" +GAME_ENGINE_JOINTS["right_shoulder"] = "upperarm_r" +GAME_ENGINE_JOINTS["left_elbow"] = "lowerarm_l" +GAME_ENGINE_JOINTS["right_elbow"] = "lowerarm_r" +GAME_ENGINE_JOINTS["left_wrist"] = "hand_l" +GAME_ENGINE_JOINTS["right_wrist"] = "hand_r" +GAME_ENGINE_JOINTS["left_finger"] = "middle_03_l" +GAME_ENGINE_JOINTS["right_finger"] = "middle_03_r" +GAME_ENGINE_JOINTS["left_hip"] = "thigh_l" +GAME_ENGINE_JOINTS["right_hip"] = "thigh_r" +GAME_ENGINE_JOINTS["left_knee"] = "calf_l" +GAME_ENGINE_JOINTS["right_knee"] = "calf_r" +GAME_ENGINE_JOINTS["left_ankle"] = "foot_l" +GAME_ENGINE_JOINTS["right_ankle"] = "foot_r" +GAME_ENGINE_JOINTS["left_toe"] = "ball_l" +GAME_ENGINE_JOINTS["right_toe"] = "ball_r" +GAME_ENGINE_JOINTS["left_heel"] = "heel_l" +GAME_ENGINE_JOINTS["right_heel"] = "heel_r" +GAME_ENGINE_JOINTS["neck"] = "neck_01" +GAME_ENGINE_JOINTS["head"] = "head" + +GAME_ENGINE_SKELETON_MODEL = collections.OrderedDict() +GAME_ENGINE_SKELETON_MODEL["joints"] = GAME_ENGINE_JOINTS +GAME_ENGINE_SKELETON_MODEL["foot_joints"] = ["foot_l", "foot_r", "ball_r", "ball_l", "heel_r", "heel_l"] +GAME_ENGINE_SKELETON_MODEL["heel_offset"] = (np.array([0, 2.45, 3.480602]) * 2.5).tolist() +GAME_ENGINE_SKELETON_MODEL["ik_chains"] = IK_CHAINS_GAME_ENGINE_SKELETON + +GAME_ENGINE_SKELETON_COS_MAP = collections.defaultdict(dict) +GAME_ENGINE_SKELETON_COS_MAP["Game_engine"]["y"] = [0, 1, 0] +GAME_ENGINE_SKELETON_COS_MAP["Game_engine"]["x"] = [1, 0, 0] +GAME_ENGINE_SKELETON_COS_MAP["head"]["y"] = [0, 1, 0] +GAME_ENGINE_SKELETON_COS_MAP["head"]["x"] = [1, 0, 0] +GAME_ENGINE_SKELETON_MODEL["cos_map"] = GAME_ENGINE_SKELETON_COS_MAP + + + +GAME_ENGINE_WRONG_ROOT_JOINTS = collections.OrderedDict() +GAME_ENGINE_WRONG_ROOT_JOINTS["root"] = "Game_engine" +GAME_ENGINE_WRONG_ROOT_JOINTS["pelvis"] = "pelvis" +GAME_ENGINE_WRONG_ROOT_JOINTS["spine"] = "spine_01" +GAME_ENGINE_WRONG_ROOT_JOINTS["spine_1"] = "spine_02" +GAME_ENGINE_WRONG_ROOT_JOINTS["spine_2"] = "spine_03" +GAME_ENGINE_WRONG_ROOT_JOINTS["left_clavicle"] = "clavicle_l" +GAME_ENGINE_WRONG_ROOT_JOINTS["right_clavicle"] = "clavicle_r" +GAME_ENGINE_WRONG_ROOT_JOINTS["left_shoulder"] = "upperarm_l" +GAME_ENGINE_WRONG_ROOT_JOINTS["right_shoulder"] = "upperarm_r" +GAME_ENGINE_WRONG_ROOT_JOINTS["left_elbow"] = "lowerarm_l" +GAME_ENGINE_WRONG_ROOT_JOINTS["right_elbow"] = "lowerarm_r" +GAME_ENGINE_WRONG_ROOT_JOINTS["left_wrist"] = "hand_l" +GAME_ENGINE_WRONG_ROOT_JOINTS["right_wrist"] = "hand_r" +GAME_ENGINE_WRONG_ROOT_JOINTS["left_finger"] = "middle_03_l" +GAME_ENGINE_WRONG_ROOT_JOINTS["right_finger"] = "middle_03_r" +GAME_ENGINE_WRONG_ROOT_JOINTS["left_hip"] = "thigh_l" +GAME_ENGINE_WRONG_ROOT_JOINTS["right_hip"] = "thigh_r" +GAME_ENGINE_WRONG_ROOT_JOINTS["left_knee"] = "calf_l" +GAME_ENGINE_WRONG_ROOT_JOINTS["right_knee"] = "calf_r" +GAME_ENGINE_WRONG_ROOT_JOINTS["left_ankle"] = "foot_l" +GAME_ENGINE_WRONG_ROOT_JOINTS["right_ankle"] = "foot_r" +GAME_ENGINE_WRONG_ROOT_JOINTS["left_toe"] = "ball_l" +GAME_ENGINE_WRONG_ROOT_JOINTS["right_toe"] = "ball_r" +GAME_ENGINE_WRONG_ROOT_JOINTS["left_heel"] = "heel_l" +GAME_ENGINE_WRONG_ROOT_JOINTS["right_heel"] = "heel_r" +GAME_ENGINE_WRONG_ROOT_JOINTS["neck"] = "neck_01" +GAME_ENGINE_WRONG_ROOT_JOINTS["head"] = "head" + +GAME_ENGINE_WRONG_ROOT_SKELETON_MODEL = collections.OrderedDict() +GAME_ENGINE_WRONG_ROOT_SKELETON_MODEL["name"] = "game_engine_wrong_root" +GAME_ENGINE_WRONG_ROOT_SKELETON_MODEL["joints"] = GAME_ENGINE_WRONG_ROOT_JOINTS +GAME_ENGINE_WRONG_ROOT_SKELETON_MODEL["foot_joints"] = ["foot_l", "foot_r", "ball_r", "ball_l", "heel_r", "heel_l"] +GAME_ENGINE_WRONG_ROOT_SKELETON_MODEL["heel_offset"] = (np.array([0, 2.45, 3.480602]) * 2.5).tolist() +GAME_ENGINE_WRONG_ROOT_SKELETON_MODEL["ik_chains"] = IK_CHAINS_GAME_ENGINE_SKELETON + +GAME_ENGINE_WRONG_ROOT_SKELETON_COS_MAP = collections.defaultdict(dict) +GAME_ENGINE_WRONG_ROOT_SKELETON_COS_MAP["Game_engine"]["y"] = [0, 1, 0] +GAME_ENGINE_WRONG_ROOT_SKELETON_COS_MAP["Game_engine"]["x"] = [0, 0, -1] +GAME_ENGINE_WRONG_ROOT_SKELETON_COS_MAP["head"]["y"] = [0, 1, 0] +GAME_ENGINE_WRONG_ROOT_SKELETON_COS_MAP["head"]["x"] = [1, 0, 0] +GAME_ENGINE_WRONG_ROOT_SKELETON_MODEL["cos_map"] = GAME_ENGINE_WRONG_ROOT_SKELETON_COS_MAP + + +ROCKETBOX_JOINTS = collections.OrderedDict() +ROCKETBOX_JOINTS["root"] = "Hips" +ROCKETBOX_JOINTS["pelvis"] = "Hips" +ROCKETBOX_JOINTS["spine"] = "Spine" +ROCKETBOX_JOINTS["spine_1"] = "Spine_1" +ROCKETBOX_JOINTS["left_clavicle"] = "LeftShoulder" +ROCKETBOX_JOINTS["right_clavicle"] = "RightShoulder" +ROCKETBOX_JOINTS["left_shoulder"] = "LeftArm" +ROCKETBOX_JOINTS["right_shoulder"] = "RightArm" +ROCKETBOX_JOINTS["left_elbow"] = "LeftForeArm" +ROCKETBOX_JOINTS["right_elbow"] = "RightForeArm" +ROCKETBOX_JOINTS["left_wrist"] = "LeftHand" +ROCKETBOX_JOINTS["right_wrist"] = "RightHand" +ROCKETBOX_JOINTS["left_finger"] = "Bip01_L_Finger2" +ROCKETBOX_JOINTS["right_finger"] = "Bip01_R_Finger2" +ROCKETBOX_JOINTS["left_hip"] = "LeftUpLeg" +ROCKETBOX_JOINTS["right_hip"] = "RightUpLeg" +ROCKETBOX_JOINTS["left_knee"] = "LeftLeg" +ROCKETBOX_JOINTS["right_knee"] = "RightLeg" +ROCKETBOX_JOINTS["left_ankle"] = "LeftFoot" +ROCKETBOX_JOINTS["right_ankle"] = "RightFoot" +ROCKETBOX_JOINTS["left_toe"] = "Bip01_L_Toe0" +ROCKETBOX_JOINTS["right_toe"] = "Bip01_R_Toe0" +ROCKETBOX_JOINTS["left_heel"] = "LeftHeel" +ROCKETBOX_JOINTS["right_heel"] = "RightHeel" +ROCKETBOX_JOINTS["neck"] = "Neck" +ROCKETBOX_JOINTS["head"] = "Head" + +ROCKETBOX_SKELETON_MODEL = collections.OrderedDict() +ROCKETBOX_SKELETON_MODEL["name"] = "rocketbox" +ROCKETBOX_SKELETON_MODEL["joints"] = ROCKETBOX_JOINTS +ROCKETBOX_SKELETON_MODEL["heel_offset"] = [0, -6.480602, 0] +ROCKETBOX_SKELETON_MODEL["foot_joints"] = RAW_SKELETON_FOOT_JOINTS +ROCKETBOX_SKELETON_MODEL["ik_chains"] = IK_CHAINS_RAW_SKELETON +ROCKETBOX_SKELETON_COS_MAP = collections.defaultdict(dict) +ROCKETBOX_SKELETON_COS_MAP["LeftHand"]["y"] = [0.99969438, -0.01520068, -0.01949593] +ROCKETBOX_SKELETON_COS_MAP["LeftHand"]["x"] = [-1.52006822e-02, -9.99884452e-01, 1.48198341e-04] +ROCKETBOX_SKELETON_COS_MAP["RightHand"]["y"] = [0.99969439, 0.01520077, -0.01949518] +ROCKETBOX_SKELETON_COS_MAP["RightHand"]["x"] = [ 1.52007742e-02, -9.99884451e-01, -1.48193551e-04] + +ROCKETBOX_SKELETON_COS_MAP["LeftHand"]["y"] = [1, 0, 0] +ROCKETBOX_SKELETON_COS_MAP["RightHand"]["y"] = [1, 0, 0] +ROCKETBOX_SKELETON_COS_MAP["LeftHand"]["x"] = [0, 0, 1] +ROCKETBOX_SKELETON_COS_MAP["RightHand"]["x"] = [0, 0, -1] + + +ROCKETBOX_SKELETON_MODEL["cos_map"] = ROCKETBOX_SKELETON_COS_MAP + + +CMU_SKELETON_JOINTS = collections.OrderedDict() +CMU_SKELETON_JOINTS["root"] = "hip" +CMU_SKELETON_JOINTS["pelvis"] = "hip" +CMU_SKELETON_JOINTS["spine"] = "abdomen" +CMU_SKELETON_JOINTS["spine_1"] = "chest" +CMU_SKELETON_JOINTS["left_clavicle"] = "lCollar" +CMU_SKELETON_JOINTS["right_clavicle"] = "rCollar" +CMU_SKELETON_JOINTS["left_shoulder"] = "lShldr" +CMU_SKELETON_JOINTS["right_shoulder"] = "rShldr" +CMU_SKELETON_JOINTS["left_elbow"] = "lForeArm" +CMU_SKELETON_JOINTS["right_elbow"] = "rForeArm" +CMU_SKELETON_JOINTS["left_wrist"] = "lHand" +CMU_SKELETON_JOINTS["right_wrist"] = "rHand" +CMU_SKELETON_JOINTS["left_hip"] = "lThigh" +CMU_SKELETON_JOINTS["right_hip"] = "rThigh" +CMU_SKELETON_JOINTS["left_knee"] = "lShin" +CMU_SKELETON_JOINTS["right_knee"] = "rShin" +CMU_SKELETON_JOINTS["left_ankle"] = "lFoot" +CMU_SKELETON_JOINTS["right_ankle"] = "rFoot" +CMU_SKELETON_JOINTS["left_toe"] = "lFoot_EndSite" +CMU_SKELETON_JOINTS["right_toe"] = "rFoot_EndSite" +CMU_SKELETON_JOINTS["left_heel"] = None +CMU_SKELETON_JOINTS["right_heel"] = None +CMU_SKELETON_JOINTS["neck"] = "neck" +CMU_SKELETON_JOINTS["head"] = "head" +CMU_SKELETON_MODEL = collections.OrderedDict() +CMU_SKELETON_MODEL["cmu"] = "cmu" +CMU_SKELETON_MODEL["joints"] = CMU_SKELETON_JOINTS +CMU_SKELETON_MODEL["foot_joints"] = [] +CMU_SKELETON_MODEL["foot_correction"] = {"x":-22, "y":3} + +MOVIEMATION_SKELETON_JOINTS = collections.OrderedDict() +MOVIEMATION_SKELETON_JOINTS["root"] = "Hips" +MOVIEMATION_SKELETON_JOINTS["pelvis"] = "Hips" +MOVIEMATION_SKELETON_JOINTS["spine"] = "Ab" +MOVIEMATION_SKELETON_JOINTS["spine_1"] = "Chest" +MOVIEMATION_SKELETON_JOINTS["left_clavicle"] = "LeftCollar" +MOVIEMATION_SKELETON_JOINTS["right_clavicle"] = "RightCollar" +MOVIEMATION_SKELETON_JOINTS["left_shoulder"] = "LeftShoulder" +MOVIEMATION_SKELETON_JOINTS["right_shoulder"] = "RightShoulder" +MOVIEMATION_SKELETON_JOINTS["left_elbow"] = "LeftElbow" +MOVIEMATION_SKELETON_JOINTS["right_elbow"] = "RightElbow" +MOVIEMATION_SKELETON_JOINTS["left_wrist"] = "LeftWrist" +MOVIEMATION_SKELETON_JOINTS["right_wrist"] = "RightWrist" +MOVIEMATION_SKELETON_JOINTS["left_hip"] = "LeftHip" +MOVIEMATION_SKELETON_JOINTS["right_hip"] = "RightHip" +MOVIEMATION_SKELETON_JOINTS["left_knee"] = "LeftKnee" +MOVIEMATION_SKELETON_JOINTS["right_knee"] = "RightKnee" +MOVIEMATION_SKELETON_JOINTS["left_ankle"] = "LeftAnkle" +MOVIEMATION_SKELETON_JOINTS["right_ankle"] = "RightAnkle" +MOVIEMATION_SKELETON_JOINTS["left_toe"] = "LeftAnkle_EndSite" +MOVIEMATION_SKELETON_JOINTS["right_toe"] = "RightAnkle_EndSite" +MOVIEMATION_SKELETON_JOINTS["left_heel"] = None +MOVIEMATION_SKELETON_JOINTS["right_heel"] = None +MOVIEMATION_SKELETON_JOINTS["neck"] = "Neck" +MOVIEMATION_SKELETON_JOINTS["head"] = "Head" +MOVIEMATION_SKELETON_MODEL = collections.OrderedDict() +MOVIEMATION_SKELETON_MODEL["name"] = "moviemation" +MOVIEMATION_SKELETON_MODEL["joints"] = MOVIEMATION_SKELETON_JOINTS +MOVIEMATION_SKELETON_MODEL["foot_joints"] = [] + + +MCS_SKELETON_JOINTS = collections.OrderedDict() +MCS_SKELETON_JOINTS["root"] = "Hips" +MCS_SKELETON_JOINTS["pelvis"] = "Hips" +MCS_SKELETON_JOINTS["spine"] = None +MCS_SKELETON_JOINTS["spine_1"] = "Chest" +MCS_SKELETON_JOINTS["left_clavicle"] = "LeftCollar" +MCS_SKELETON_JOINTS["right_clavicle"] = "RightCollar" +MCS_SKELETON_JOINTS["left_shoulder"] = "LeftShoulder" +MCS_SKELETON_JOINTS["right_shoulder"] = "RightShoulder" +MCS_SKELETON_JOINTS["left_elbow"] = "LeftElbow" +MCS_SKELETON_JOINTS["right_elbow"] = "RightElbow" +MCS_SKELETON_JOINTS["left_wrist"] = "LeftWrist" +MCS_SKELETON_JOINTS["right_wrist"] = "RightWrist" +MCS_SKELETON_JOINTS["left_hip"] = "LeftHip" +MCS_SKELETON_JOINTS["right_hip"] = "RightHip" +MCS_SKELETON_JOINTS["left_knee"] = "LeftKnee" +MCS_SKELETON_JOINTS["right_knee"] = "RightKnee" +MCS_SKELETON_JOINTS["left_ankle"] = "LeftAnkle" +MCS_SKELETON_JOINTS["right_ankle"] = "RightAnkle" +MCS_SKELETON_JOINTS["left_toe"] = "LeftAnkle_EndSite" +MCS_SKELETON_JOINTS["right_toe"] = "RightAnkle_EndSite" +MCS_SKELETON_JOINTS["left_heel"] = None +MCS_SKELETON_JOINTS["right_heel"] = None +MCS_SKELETON_JOINTS["neck"] = "Neck" +MCS_SKELETON_JOINTS["head"] = "Head" +MCS_SKELETON_MODEL = collections.OrderedDict() +MCS_SKELETON_MODEL["name"] = "mcs" +MCS_SKELETON_MODEL["joints"] = MCS_SKELETON_JOINTS +MCS_SKELETON_MODEL["foot_joints"] = [] + + + +MH_CMU_SKELETON_JOINTS = collections.OrderedDict() +MH_CMU_SKELETON_JOINTS["root"] = None#"CMU compliant skeleton" +MH_CMU_SKELETON_JOINTS["pelvis"] = "Hips" +MH_CMU_SKELETON_JOINTS["spine"] = "LowerBack" +MH_CMU_SKELETON_JOINTS["spine_1"] = "Spine" +MH_CMU_SKELETON_JOINTS["spine_2"] = "Spine1" +MH_CMU_SKELETON_JOINTS["left_clavicle"] = "LeftShoulder" +MH_CMU_SKELETON_JOINTS["right_clavicle"] = "RightShoulder" +MH_CMU_SKELETON_JOINTS["left_shoulder"] = "LeftArm" +MH_CMU_SKELETON_JOINTS["right_shoulder"] = "RightArm" +MH_CMU_SKELETON_JOINTS["left_elbow"] = "LeftForeArm" +MH_CMU_SKELETON_JOINTS["right_elbow"] = "RightForeArm" +MH_CMU_SKELETON_JOINTS["left_wrist"] = "LeftHand" +MH_CMU_SKELETON_JOINTS["right_wrist"] = "RightHand" +MH_CMU_SKELETON_JOINTS["left_finger"] = "LeftHandFinger1" +MH_CMU_SKELETON_JOINTS["right_finger"] = "RightHandFinger1" +MH_CMU_SKELETON_JOINTS["left_hip"] = "LeftUpLeg" +MH_CMU_SKELETON_JOINTS["right_hip"] = "RightUpLeg" +MH_CMU_SKELETON_JOINTS["left_knee"] = "LeftLeg" +MH_CMU_SKELETON_JOINTS["right_knee"] = "RightLeg" +MH_CMU_SKELETON_JOINTS["left_ankle"] = "LeftFoot" +MH_CMU_SKELETON_JOINTS["right_ankle"] = "RightFoot" +MH_CMU_SKELETON_JOINTS["left_toe"] = "LeftToeBase" +MH_CMU_SKELETON_JOINTS["right_toe"] = "RightToeBase" +MH_CMU_SKELETON_JOINTS["left_heel"] = None +MH_CMU_SKELETON_JOINTS["right_heel"] = None +MH_CMU_SKELETON_JOINTS["neck"] = "Neck" +MH_CMU_SKELETON_JOINTS["head"] = "Head" + +MH_CMU_SKELETON_MODEL = collections.OrderedDict() +MH_CMU_SKELETON_MODEL["name"] = "mh_cmu" +MH_CMU_SKELETON_MODEL["joints"] = MH_CMU_SKELETON_JOINTS +MH_CMU_SKELETON_MODEL["foot_joints"] = [] +MH_CMU_SKELETON_MODEL["foot_correction"] = {"x":-22, "y":3} +MH_CMU_SKELETON_MODEL["flip_x_axis"] = False + +MH_CMU_2_SKELETON_JOINTS = collections.OrderedDict() +MH_CMU_2_SKELETON_JOINTS["root"] = None#"CMU compliant skeleton" +MH_CMU_2_SKELETON_JOINTS["pelvis"] = "Hips" +MH_CMU_2_SKELETON_JOINTS["spine"] = "LowerBack" +MH_CMU_2_SKELETON_JOINTS["spine_1"] = "Spine" +MH_CMU_2_SKELETON_JOINTS["spine_2"] = "Spine1" +MH_CMU_2_SKELETON_JOINTS["left_clavicle"] = "LeftShoulder" +MH_CMU_2_SKELETON_JOINTS["right_clavicle"] = "RightShoulder" +MH_CMU_2_SKELETON_JOINTS["left_shoulder"] = "LeftArm" +MH_CMU_2_SKELETON_JOINTS["right_shoulder"] = "RightArm" +MH_CMU_2_SKELETON_JOINTS["left_elbow"] = "LeftForeArm" +MH_CMU_2_SKELETON_JOINTS["right_elbow"] = "RightForeArm" +MH_CMU_2_SKELETON_JOINTS["left_wrist"] = "LeftHand" +MH_CMU_2_SKELETON_JOINTS["right_wrist"] = "RightHand" +MH_CMU_2_SKELETON_JOINTS["left_hip"] = "LeftUpLeg" +MH_CMU_2_SKELETON_JOINTS["right_hip"] = "RightUpLeg" +MH_CMU_2_SKELETON_JOINTS["left_knee"] = "LeftLeg" +MH_CMU_2_SKELETON_JOINTS["right_knee"] = "RightLeg" +MH_CMU_2_SKELETON_JOINTS["left_ankle"] = "LeftFoot" +MH_CMU_2_SKELETON_JOINTS["right_ankle"] = "RightFoot" +MH_CMU_2_SKELETON_JOINTS["left_toe"] = "LeftToeBase" +MH_CMU_2_SKELETON_JOINTS["right_toe"] = "RightToeBase" +MH_CMU_2_SKELETON_JOINTS["left_heel"] = None +MH_CMU_2_SKELETON_JOINTS["right_heel"] = None +MH_CMU_2_SKELETON_JOINTS["neck"] = "Neck" +MH_CMU_2_SKELETON_JOINTS["head"] = "Head" + +MH_CMU_2_SKELETON_MODEL = collections.OrderedDict() +MH_CMU_2_SKELETON_MODEL["name"] = "mh_cmu2" +MH_CMU_2_SKELETON_MODEL["joints"] = MH_CMU_2_SKELETON_JOINTS +MH_CMU_2_SKELETON_MODEL["foot_joints"] = [] +MH_CMU_2_SKELETON_MODEL["foot_correction"] = {"x":-22, "y":3} +MH_CMU_2_SKELETON_MODEL["flip_x_axis"] = False +#MH_CMU_2_SKELETON_MODEL["foot_joints"] = [] +#MH_CMU_2_SKELETON_MODEL["x_cos_fixes"] = ["Neck", "Pelvis", "Spine", "Spine1", "LowerBack", "LeftArm"] +#MH_CMU_2_SKELETON_COS_MAP = collections.defaultdict(dict) +#MH_CMU_2_SKELETON_COS_MAP["Hips"]["y"] = [0, 1,0] +#MH_CMU_2_SKELETON_COS_MAP["Hips"]["x"] = [1, 0, 0] +#MH_CMU_2_SKELETON_MODEL["cos_map"] = MH_CMU_2_SKELETON_COS_MAP + +ICLONE_SKELETON_JOINTS = collections.OrderedDict() +ICLONE_SKELETON_JOINTS["root"] = "CC_Base_BoneRoot" +ICLONE_SKELETON_JOINTS["pelvis"] = "CC_Base_Hip"#CC_Base_Pelvis +ICLONE_SKELETON_JOINTS["spine"] = "CC_Base_Waist" +ICLONE_SKELETON_JOINTS["spine_1"] = "CC_Base_Spine01" +ICLONE_SKELETON_JOINTS["spine_2"] = "CC_Base_Spine02" +ICLONE_SKELETON_JOINTS["left_clavicle"] = "CC_Base_L_Clavicle" +ICLONE_SKELETON_JOINTS["right_clavicle"] = "CC_Base_R_Clavicle" +ICLONE_SKELETON_JOINTS["left_shoulder"] = "CC_Base_L_Upperarm" +ICLONE_SKELETON_JOINTS["right_shoulder"] = "CC_Base_R_Upperarm" +ICLONE_SKELETON_JOINTS["left_elbow"] = "CC_Base_L_Forearm" +ICLONE_SKELETON_JOINTS["right_elbow"] = "CC_Base_R_Forearm" +ICLONE_SKELETON_JOINTS["left_wrist"] = "CC_Base_L_Hand" +ICLONE_SKELETON_JOINTS["right_wrist"] = "CC_Base_R_Hand" +ICLONE_SKELETON_JOINTS["left_hip"] = "CC_Base_L_Thigh" +ICLONE_SKELETON_JOINTS["right_hip"] = "CC_Base_R_Thigh" +ICLONE_SKELETON_JOINTS["left_knee"] = "CC_Base_L_Calf" +ICLONE_SKELETON_JOINTS["right_knee"] = "CC_Base_R_Calf" +ICLONE_SKELETON_JOINTS["left_ankle"] = "CC_Base_L_Foot" +ICLONE_SKELETON_JOINTS["right_ankle"] = "CC_Base_R_Foot" +ICLONE_SKELETON_JOINTS["left_toe"] = "CC_Base_L_ToeBase" +ICLONE_SKELETON_JOINTS["right_toe"] = "CC_Base_R_ToeBase" +ICLONE_SKELETON_JOINTS["left_heel"] = None +ICLONE_SKELETON_JOINTS["right_heel"] = None +ICLONE_SKELETON_JOINTS["neck"] = "CC_Base_NeckTwist01" +ICLONE_SKELETON_JOINTS["head"] = "CC_Base_Head" + +ICLONE_SKELETON_MODEL = collections.OrderedDict() +ICLONE_SKELETON_MODEL["name"] = "iclone" +ICLONE_SKELETON_MODEL["joints"] = ICLONE_SKELETON_JOINTS +ICLONE_SKELETON_MODEL["foot_joints"] = [] +ICLONE_SKELETON_MODEL["x_cos_fixes"] = ["CC_Base_L_Thigh", "CC_Base_R_Thigh", "CC_Base_L_Calf", "CC_Base_R_Calf"] +ICLONE_SKELETON_COS_MAP = collections.defaultdict(dict) +ICLONE_SKELETON_COS_MAP["CC_Base_L_Foot"]["y"] = [0, 0.4, 0.6] +ICLONE_SKELETON_COS_MAP["CC_Base_L_Foot"]["x"] = [-1, 0, 0] +ICLONE_SKELETON_COS_MAP["CC_Base_R_Foot"]["y"] = [0, 0.4, 0.6] +ICLONE_SKELETON_COS_MAP["CC_Base_R_Foot"]["x"] = [-1, 0, 0] +ICLONE_SKELETON_MODEL["cos_map"] = ICLONE_SKELETON_COS_MAP +ICLONE_SKELETON_MODEL["foot_correction"] = {"x":-40, "y":3} +ICLONE_SKELETON_MODEL["flip_x_axis"] = True + + +CUSTOM_SKELETON_JOINTS = collections.OrderedDict() + +CUSTOM_SKELETON_JOINTS["root"] = None +CUSTOM_SKELETON_JOINTS = dict() +CUSTOM_SKELETON_JOINTS["pelvis"] = "FK_back1_jnt" +CUSTOM_SKELETON_JOINTS["spine"] = "FK_back2_jnt" +CUSTOM_SKELETON_JOINTS["spine_1"] = "FK_back3_jnt" +CUSTOM_SKELETON_JOINTS["left_clavicle"] = "L_shoulder_jnt" +CUSTOM_SKELETON_JOINTS["right_clavicle"] = "R_shoulder_jnt" +CUSTOM_SKELETON_JOINTS["left_shoulder"] = "L_upArm_jnt" +CUSTOM_SKELETON_JOINTS["right_shoulder"] = "R_upArm_jnt" +CUSTOM_SKELETON_JOINTS["left_elbow"] = "L_lowArm_jnt" +CUSTOM_SKELETON_JOINTS["right_elbow"] = "R_lowArm_jnt" +CUSTOM_SKELETON_JOINTS["left_wrist"] = "L_hand_jnt" +CUSTOM_SKELETON_JOINTS["right_wrist"] = "R_hand_jnt" +CUSTOM_SKELETON_JOINTS["left_hip"] = "L_upLeg_jnt" +CUSTOM_SKELETON_JOINTS["right_hip"] = "R_upLeg_jnt" +CUSTOM_SKELETON_JOINTS["left_knee"] = "L_lowLeg_jnt" +CUSTOM_SKELETON_JOINTS["right_knee"] = "R_lowLeg_jnt" +CUSTOM_SKELETON_JOINTS["left_ankle"] = "L_foot_jnt" +CUSTOM_SKELETON_JOINTS["right_ankle"] = "R_foot_jnt" +CUSTOM_SKELETON_JOINTS["left_toe"] = "L_toe_jnt" +CUSTOM_SKELETON_JOINTS["right_toe"] = "R_toe_jnt" +CUSTOM_SKELETON_JOINTS["neck"] = "FK_back4_jnt" +CUSTOM_SKELETON_JOINTS["head"] = "head_jnt" + +CUSTOM_SKELETON_JOINTS["left_hold_point"] = "L_hand_jnt_hold_point" +CUSTOM_SKELETON_JOINTS["right_hold_point"] = "R_hand_jnt_hold_point" + +CUSTOM_SKELETON_JOINTS["right_thumb_base"] = "R_thumb_base_jnt" +CUSTOM_SKELETON_JOINTS["right_thumb_mid"] = "R_thumb_mid_jnt" +CUSTOM_SKELETON_JOINTS["right_thumb_tip"] = "R_thumb_tip_jnt" +CUSTOM_SKELETON_JOINTS["right_thumb_end"] = "R_thumb_end_jnt" + +CUSTOM_SKELETON_JOINTS["right_index_finger_root"] = "R_index_root_jnt" +CUSTOM_SKELETON_JOINTS["right_index_finger_base"] = "R_index_base_jnt" +CUSTOM_SKELETON_JOINTS["right_index_finger_mid"] = "R_index_mid_jnt" +CUSTOM_SKELETON_JOINTS["right_index_finger_tip"] = "R_index_tip_jnt" +CUSTOM_SKELETON_JOINTS["right_index_finger_end"] = "R_index_end_jnt" + +CUSTOM_SKELETON_JOINTS["right_middle_finger_root"] = "R_middle_root_jnt" +CUSTOM_SKELETON_JOINTS["right_middle_finger_base"] = "R_middle_base_jnt" +CUSTOM_SKELETON_JOINTS["right_middle_finger_mid"] = "R_middle_mid_jnt" +CUSTOM_SKELETON_JOINTS["right_middle_finger_tip"] = "R_middle_tip_jnt" +CUSTOM_SKELETON_JOINTS["right_middle_finger_end"] = "R_middle_end_jnt" + +CUSTOM_SKELETON_JOINTS["right_ring_finger_root"] = "R_ring_base_jnt" +CUSTOM_SKELETON_JOINTS["right_ring_finger_base"] = "R_ring_root_jnt" +CUSTOM_SKELETON_JOINTS["right_ring_finger_mid"] = "R_ring_mid_jnt" +CUSTOM_SKELETON_JOINTS["right_ring_finger_tip"] = "R_ring_tip_jnt" +CUSTOM_SKELETON_JOINTS["right_ring_finger_end"] = "R_ring_end_jnt" + +CUSTOM_SKELETON_JOINTS["right_pinky_finger_root"] = "R_pinky_root_jnt" +CUSTOM_SKELETON_JOINTS["right_pinky_finger_base"] = "R_pinky_base_jnt" +CUSTOM_SKELETON_JOINTS["right_pinky_finger_mid"] = "R_pinky_mid_jnt" +CUSTOM_SKELETON_JOINTS["right_pinky_finger_tip"] = "R_pinky_tip_jnt" +CUSTOM_SKELETON_JOINTS["right_pinky_finger_end"] = "R_pinky_end_jnt" + +CUSTOM_SKELETON_JOINTS["left_thumb_base"] = "L_thumb_base_jnt" +CUSTOM_SKELETON_JOINTS["left_thumb_mid"] = "L_thumb_mid_jnt" +CUSTOM_SKELETON_JOINTS["left_thumb_tip"] = "L_thumb_tip_jnt" +CUSTOM_SKELETON_JOINTS["left_thumb_end"] = "L_thumb_end_jnt" + +CUSTOM_SKELETON_JOINTS["left_index_finger_root"] = "L_index_root_jnt" +CUSTOM_SKELETON_JOINTS["left_index_finger_base"] = "L_index_base_jnt" +CUSTOM_SKELETON_JOINTS["left_index_finger_mid"] = "L_index_mid_jnt" +CUSTOM_SKELETON_JOINTS["left_index_finger_tip"] = "L_index_tip_jnt" +CUSTOM_SKELETON_JOINTS["left_index_finger_end"] = "L_index_end_jnt" + +CUSTOM_SKELETON_JOINTS["left_middle_finger_root"] = "L_middle_root_jnt" +CUSTOM_SKELETON_JOINTS["left_middle_finger_base"] = "L_middle_base_jnt" +CUSTOM_SKELETON_JOINTS["left_middle_finger_mid"] = "L_middle_mid_jnt" +CUSTOM_SKELETON_JOINTS["left_middle_finger_tip"] = "L_middle_tip_jnt" +CUSTOM_SKELETON_JOINTS["left_middle_finger_end"] = "L_middle_end_jnt" + +CUSTOM_SKELETON_JOINTS["left_ring_finger_root"] = "L_ring_base_jnt" +CUSTOM_SKELETON_JOINTS["left_ring_finger_base"] = "L_ring_root_jnt" +CUSTOM_SKELETON_JOINTS["left_ring_finger_mid"] = "L_ring_mid_jnt" +CUSTOM_SKELETON_JOINTS["left_ring_finger_tip"] = "L_ring_tip_jnt" +CUSTOM_SKELETON_JOINTS["left_ring_finger_end"] = "L_ring_end_jnt" + +CUSTOM_SKELETON_JOINTS["left_pinky_finger_root"] = "L_pinky_root_jnt" +CUSTOM_SKELETON_JOINTS["left_pinky_finger_base"] = "L_pinky_base_jnt" +CUSTOM_SKELETON_JOINTS["left_pinky_finger_mid"] = "L_pinky_mid_jnt" +CUSTOM_SKELETON_JOINTS["left_pinky_finger_tip"] = "L_pinky_tip_jnt" +CUSTOM_SKELETON_JOINTS["left_pinky_finger_end"] = "L_pinky_end_jnt" + + + + +CUSTOM_SKELETON_MODEL = collections.OrderedDict() +CUSTOM_SKELETON_MODEL["name"] = "custom" +CUSTOM_SKELETON_MODEL["joints"] = CUSTOM_SKELETON_JOINTS +CUSTOM_SKELETON_MODEL["foot_joints"] = [] +CUSTOM_SKELETON_COS_MAP = collections.defaultdict(dict) +CUSTOM_SKELETON_COS_MAP["R_foot_jnt"]["y"] = [1, 0, 0] +#CUSTOM_SKELETON_COS_MAP["R_foot_jnt"]["y"] = [0.4, 0.6, 0] +CUSTOM_SKELETON_COS_MAP["R_foot_jnt"]["x"] = [0, 0, 1] +CUSTOM_SKELETON_COS_MAP["L_foot_jnt"]["y"] = [1, 0, 0] +#CUSTOM_SKELETON_COS_MAP["L_foot_jnt"]["y"] = [0.4, 0.6, 0] +CUSTOM_SKELETON_COS_MAP["L_foot_jnt"]["x"] = [0, 0, 1] + +CUSTOM_SKELETON_COS_MAP["L_upLeg_jnt"]["y"] = [1, 0, 0] +CUSTOM_SKELETON_COS_MAP["L_upLeg_jnt"]["x"] = [0, 0, 1] +CUSTOM_SKELETON_COS_MAP["L_lowLeg_jnt"]["y"] = [1, 0, 0] +CUSTOM_SKELETON_COS_MAP["L_lowLeg_jnt"]["x"] = [0, 0, 1] + + +CUSTOM_SKELETON_COS_MAP["R_upLeg_jnt"]["y"] = [1, 0, 0] +CUSTOM_SKELETON_COS_MAP["R_upLeg_jnt"]["x"] = [0, 0, 1] +CUSTOM_SKELETON_COS_MAP["R_lowLeg_jnt"]["y"] = [1, 0, 0] +CUSTOM_SKELETON_COS_MAP["R_lowLeg_jnt"]["x"] = [0, 0, 1] + +CUSTOM_SKELETON_COS_MAP["FK_back2_jnt"]["y"] = [0, 1, 0] +CUSTOM_SKELETON_COS_MAP["FK_back2_jnt"]["x"] = [1, 0, 0] + +CUSTOM_SKELETON_COS_MAP["R_shoulder_jnt"]["y"] = [0, 0, -1] +CUSTOM_SKELETON_COS_MAP["R_shoulder_jnt"]["x"] = [0, -1, 0] +CUSTOM_SKELETON_COS_MAP["R_upArm_jnt"]["y"] = [0, 0, -1] +CUSTOM_SKELETON_COS_MAP["R_upArm_jnt"]["x"] = [0, -1, 0] +CUSTOM_SKELETON_COS_MAP["R_lowArm_jnt"]["y"] = [0, 0, -1] +CUSTOM_SKELETON_COS_MAP["R_lowArm_jnt"]["x"] = [0, -1, 0] + +CUSTOM_SKELETON_COS_MAP["L_shoulder_jnt"]["y"] = [0, 0, 1] +CUSTOM_SKELETON_COS_MAP["L_shoulder_jnt"]["x"] = [0, -1, 0] +CUSTOM_SKELETON_COS_MAP["L_upArm_jnt"]["y"] = [0, 0, 1] +CUSTOM_SKELETON_COS_MAP["L_upArm_jnt"]["x"] = [0, -1, 0] +CUSTOM_SKELETON_COS_MAP["L_lowArm_jnt"]["y"] = [0, 0, 1] +CUSTOM_SKELETON_COS_MAP["L_lowArm_jnt"]["x"] = [0, -1, 0] + +CUSTOM_SKELETON_COS_MAP["R_hand_jnt"]["y"] = [0, 0, -1] +CUSTOM_SKELETON_COS_MAP["R_hand_jnt"]["x"] = [0, -1, 0] + +CUSTOM_SKELETON_COS_MAP["L_hand_jnt"]["y"] = [0, 0, 1] +CUSTOM_SKELETON_COS_MAP["L_hand_jnt"]["x"] = [0, -1, 0] +if False: + CUSTOM_SKELETON_COS_MAP["FK_back3_jnt"]["y"] = [0, 0.7, -0.3] + CUSTOM_SKELETON_COS_MAP["FK_back3_jnt"]["x"] = [1, 0, 0] + CUSTOM_SKELETON_COS_MAP["head_jnt"]["y"] = [0.6, 0.4, 0] + CUSTOM_SKELETON_COS_MAP["head_jnt"]["x"] = [0, 0, -1] + +for k, j in CUSTOM_SKELETON_JOINTS.items(): + if j is None: + continue + if "finger" in k: + if "L_" in j: + CUSTOM_SKELETON_COS_MAP[j]["x"] = [0, 1, 0] + CUSTOM_SKELETON_COS_MAP[j]["y"] = [0, 0, 1] + elif "R_" in j: + CUSTOM_SKELETON_COS_MAP[j]["x"] = [0, -1, 0] + CUSTOM_SKELETON_COS_MAP[j]["y"] = [0, 0, -1] + elif "thumb" in k: + if "L_" in j: + CUSTOM_SKELETON_COS_MAP[j]["x"] = [1, 0, 0] + CUSTOM_SKELETON_COS_MAP[j]["y"] = [0, 0, 1] + elif "R_" in j: + CUSTOM_SKELETON_COS_MAP[j]["x"] = [-1, 0, 0] + CUSTOM_SKELETON_COS_MAP[j]["y"] = [0, 0, -1] + +CUSTOM_SKELETON_MODEL["cos_map"] = CUSTOM_SKELETON_COS_MAP +CUSTOM_SKELETON_MODEL["foot_joints"] = [] +CUSTOM_SKELETON_MODEL["heel_offset"] = [0, -6.480602, 0] +CUSTOM_SKELETON_MODEL["ik_chains"] = IK_CHAINS_CUSTOM_SKELETON +CUSTOM_SKELETON_MODEL["aligning_root_node"] = "FK_back1_jnt" +CUSTOM_SKELETON_MODEL["free_joints_map"] = {"R_hand_jnt_hold_point":["FK_back2_jnt", "R_upArm_jnt", "R_lowArm_jnt"], "L_hand_jnt_hold_point": ["FK_back2_jnt", "L_upArm_jnt", "L_lowArm_jnt"]} +CUSTOM_SKELETON_MODEL["relative_head_dir"] = [0.0, -1.0, 0.0] +CUSTOM_SKELETON_MODEL["flip_x_axis"] = True + + + +CAPTURY_SKELETON_JOINTS = collections.OrderedDict() +CAPTURY_SKELETON_JOINTS["root"] = "Hips" +CAPTURY_SKELETON_JOINTS["pelvis"] = "Hips" +CAPTURY_SKELETON_JOINTS["spine"] = "Spine" +CAPTURY_SKELETON_JOINTS["spine_1"] = "Spine1" +CAPTURY_SKELETON_JOINTS["spine_2"] = "Spine3" +CAPTURY_SKELETON_JOINTS["left_clavicle"] = "LeftShoulder" +CAPTURY_SKELETON_JOINTS["right_clavicle"] = "RightShoulder" +CAPTURY_SKELETON_JOINTS["left_shoulder"] = "LeftArm" +CAPTURY_SKELETON_JOINTS["right_shoulder"] = "RightArm" +CAPTURY_SKELETON_JOINTS["left_elbow"] = "LeftForeArm" +CAPTURY_SKELETON_JOINTS["right_elbow"] = "RightForeArm" +CAPTURY_SKELETON_JOINTS["left_wrist"] = "LeftHand" +CAPTURY_SKELETON_JOINTS["right_wrist"] = "RightHand" +CAPTURY_SKELETON_JOINTS["left_finger"] = "LeftHand_EndSite" +CAPTURY_SKELETON_JOINTS["right_finger"] = "RightHand_EndSite" +CAPTURY_SKELETON_JOINTS["left_hip"] = "LeftUpLeg" +CAPTURY_SKELETON_JOINTS["right_hip"] = "RightUpLeg" +CAPTURY_SKELETON_JOINTS["left_knee"] = "LeftLeg" +CAPTURY_SKELETON_JOINTS["right_knee"] = "RightLeg" +CAPTURY_SKELETON_JOINTS["left_ankle"] = "LeftFoot" +CAPTURY_SKELETON_JOINTS["right_ankle"] = "RightFoot" +CAPTURY_SKELETON_JOINTS["left_toe"] = "LeftToeBase" +CAPTURY_SKELETON_JOINTS["right_toe"] = "RightToeBase" +CAPTURY_SKELETON_JOINTS["left_heel"] = None +CAPTURY_SKELETON_JOINTS["right_heel"] = None +CAPTURY_SKELETON_JOINTS["neck"] = "Neck" +CAPTURY_SKELETON_JOINTS["head"] = "Head" +CAPTURY_SKELETON_COS_MAP = collections.defaultdict(dict) +CAPTURY_SKELETON_COS_MAP["LeftHand"]["y"] = [-1, 0, 0] +CAPTURY_SKELETON_COS_MAP["LeftHand"]["x"] = [0, -1, 0] +CAPTURY_SKELETON_COS_MAP["LeftArm"]["y"] = [-1, 0, 0] +CAPTURY_SKELETON_COS_MAP["LeftArm"]["x"] = [0, -1, 0] +CAPTURY_SKELETON_COS_MAP["LeftShoulder"]["y"] = [-1, 0, 0] +CAPTURY_SKELETON_COS_MAP["LeftShoulder"]["x"] = [0, -1, 0] + +CAPTURY_SKELETON_COS_MAP["LeftForeArm"]["y"] = [-1, 0, 0] +CAPTURY_SKELETON_COS_MAP["LeftForeArm"]["x"] = [0, -1, 0] +CAPTURY_SKELETON_COS_MAP["RightHand"]["y"] = [1, 0, 0] +CAPTURY_SKELETON_COS_MAP["RightHand"]["x"] = [0, 1, 0] +CAPTURY_SKELETON_COS_MAP["RightArm"]["y"] = [1, 0, 0] +CAPTURY_SKELETON_COS_MAP["RightArm"]["x"] = [0, 1, 0] +CAPTURY_SKELETON_COS_MAP["RightShoulder"]["y"] = [1, 0, 0] +CAPTURY_SKELETON_COS_MAP["RightShoulder"]["x"] = [0, 1, 0] + +CAPTURY_SKELETON_COS_MAP["RightForeArm"]["y"] = [1, 0, 0] +CAPTURY_SKELETON_COS_MAP["RightForeArm"]["x"] = [0, 1, 0] + + +CAPTURY_SKELETON_MODEL = collections.OrderedDict() +CAPTURY_SKELETON_MODEL["name"] = "captury" +CAPTURY_SKELETON_MODEL["joints"] = CAPTURY_SKELETON_JOINTS +CAPTURY_SKELETON_MODEL["foot_joints"] = [] +CAPTURY_SKELETON_MODEL["cos_map"] = CAPTURY_SKELETON_COS_MAP + + +HOLDEN_SKELETON_JOINTS = collections.OrderedDict() +HOLDEN_SKELETON_JOINTS["root"] = None +HOLDEN_SKELETON_JOINTS["pelvis"] = "Hips" +HOLDEN_SKELETON_JOINTS["spine"] = "Spine" +HOLDEN_SKELETON_JOINTS["spine_1"] = "Spine1" +HOLDEN_SKELETON_JOINTS["spine_2"] = None +HOLDEN_SKELETON_JOINTS["left_clavicle"] = "LeftShoulder" +HOLDEN_SKELETON_JOINTS["right_clavicle"] = "RightShoulder" +HOLDEN_SKELETON_JOINTS["left_shoulder"] = "LeftArm" +HOLDEN_SKELETON_JOINTS["right_shoulder"] = "RightArm" +HOLDEN_SKELETON_JOINTS["left_elbow"] = "LeftForeArm" +HOLDEN_SKELETON_JOINTS["right_elbow"] = "RightForeArm" +HOLDEN_SKELETON_JOINTS["left_wrist"] = "LeftHand" +HOLDEN_SKELETON_JOINTS["right_wrist"] = "RightHand" +HOLDEN_SKELETON_JOINTS["left_finger"] = None +HOLDEN_SKELETON_JOINTS["right_finger"] = None +HOLDEN_SKELETON_JOINTS["left_hip"] = "LeftUpLeg" +HOLDEN_SKELETON_JOINTS["right_hip"] = "RightUpLeg" +HOLDEN_SKELETON_JOINTS["left_knee"] = "LeftLeg" +HOLDEN_SKELETON_JOINTS["right_knee"] = "RightLeg" +HOLDEN_SKELETON_JOINTS["left_ankle"] = "LeftFoot" +HOLDEN_SKELETON_JOINTS["right_ankle"] = "RightFoot" +HOLDEN_SKELETON_JOINTS["left_toe"] = "LeftToeBase" +HOLDEN_SKELETON_JOINTS["right_toe"] = "RightToeBase" +HOLDEN_SKELETON_JOINTS["left_heel"] = None +HOLDEN_SKELETON_JOINTS["right_heel"] = None +HOLDEN_SKELETON_JOINTS["neck"] = "Neck1" +HOLDEN_SKELETON_JOINTS["head"] = "Head" +HOLDEN_SKELETON_COS_MAP = collections.defaultdict(dict) + +HOLDEN_SKELETON_MODEL = collections.OrderedDict() +HOLDEN_SKELETON_MODEL["name"] = "holden" +HOLDEN_SKELETON_MODEL["joints"] = HOLDEN_SKELETON_JOINTS +HOLDEN_SKELETON_MODEL["foot_joints"] = [] +HOLDEN_SKELETON_MODEL["cos_map"] = HOLDEN_SKELETON_COS_MAP + +MIXAMO_SKELETON_JOINTS = collections.OrderedDict() +MIXAMO_SKELETON_JOINTS["root"] = None +MIXAMO_SKELETON_JOINTS["pelvis"] = "mixamorig:Hips" +MIXAMO_SKELETON_JOINTS["spine"] = "mixamorig:Spine" +MIXAMO_SKELETON_JOINTS["spine_1"] = "mixamorig:Spine1" +MIXAMO_SKELETON_JOINTS["spine_2"] = "mixamorig:Spine2" +MIXAMO_SKELETON_JOINTS["left_clavicle"] = "mixamorig:LeftShoulder" +MIXAMO_SKELETON_JOINTS["right_clavicle"] = "mixamorig:RightShoulder" +MIXAMO_SKELETON_JOINTS["left_shoulder"] = "mixamorig:LeftArm" +MIXAMO_SKELETON_JOINTS["right_shoulder"] = "mixamorig:RightArm" +MIXAMO_SKELETON_JOINTS["left_elbow"] = "mixamorig:LeftForeArm" +MIXAMO_SKELETON_JOINTS["right_elbow"] = "mixamorig:RightForeArm" +MIXAMO_SKELETON_JOINTS["left_wrist"] = "mixamorig:LeftHand" +MIXAMO_SKELETON_JOINTS["right_wrist"] = "mixamorig:RightHand" +MIXAMO_SKELETON_JOINTS["left_finger"] = None +MIXAMO_SKELETON_JOINTS["right_finger"] = None +MIXAMO_SKELETON_JOINTS["left_hip"] = "mixamorig:LeftUpLeg" +MIXAMO_SKELETON_JOINTS["right_hip"] = "mixamorig:RightUpLeg" +MIXAMO_SKELETON_JOINTS["left_knee"] = "mixamorig:LeftLeg" +MIXAMO_SKELETON_JOINTS["right_knee"] = "mixamorig:RightLeg" +MIXAMO_SKELETON_JOINTS["left_ankle"] = "mixamorig:LeftFoot" +MIXAMO_SKELETON_JOINTS["right_ankle"] = "mixamorig:RightFoot" +MIXAMO_SKELETON_JOINTS["left_toe"] = "mixamorig:LeftToeBase" +MIXAMO_SKELETON_JOINTS["right_toe"] = "mixamorig:RightToeBase" +MIXAMO_SKELETON_JOINTS["left_heel"] = None +MIXAMO_SKELETON_JOINTS["right_heel"] = None +MIXAMO_SKELETON_JOINTS["neck"] = "mixamorig:Neck" +MIXAMO_SKELETON_JOINTS["head"] = "mixamorig:Head" +MIXAMO_SKELETON_COS_MAP = collections.defaultdict(dict) +MIXAMO_SKELETON_COS_MAP["mixamorig:LeftUpLeg"]["x"] = [-1, 0, 0] +MIXAMO_SKELETON_COS_MAP["mixamorig:LeftUpLeg"]["y"] = [0, 1, 0] +MIXAMO_SKELETON_COS_MAP["mixamorig:RightUpLeg"]["x"] = [-1, 0, 0] +MIXAMO_SKELETON_COS_MAP["mixamorig:RightUpLeg"]["y"] = [0, 1, 0] +MIXAMO_SKELETON_COS_MAP["mixamorig:LeftLeg"]["x"] = [-1, 0, 0] +MIXAMO_SKELETON_COS_MAP["mixamorig:LeftLeg"]["y"] = [0, 1, 0] +MIXAMO_SKELETON_COS_MAP["mixamorig:RightLeg"]["x"] = [-1, 0, 0] +MIXAMO_SKELETON_COS_MAP["mixamorig:RightLeg"]["y"] = [0, 1, 0] +MIXAMO_SKELETON_COS_MAP["mixamorig:LeftFoot"]["x"] = [-1, 0, 0] +MIXAMO_SKELETON_COS_MAP["mixamorig:LeftFoot"]["y"] = [0, 1, 0] +MIXAMO_SKELETON_COS_MAP["mixamorig:RightFoot"]["x"] = [-1, 0, 0] +MIXAMO_SKELETON_COS_MAP["mixamorig:RightFoot"]["y"] = [0, 1, 0] + +MIXAMO_SKELETON_MODEL = collections.OrderedDict() +MIXAMO_SKELETON_MODEL["name"] = "mixamo" +MIXAMO_SKELETON_MODEL["joints"] = MIXAMO_SKELETON_JOINTS +MIXAMO_SKELETON_MODEL["foot_joints"] = [] +MIXAMO_SKELETON_MODEL["cos_map"] = MIXAMO_SKELETON_COS_MAP +MIXAMO_SKELETON_MODEL["foot_correction"] = {"x":22, "y":3} +MIXAMO_SKELETON_MODEL["flip_x_axis"] = True +MIXAMO_SKELETON_MODEL["fixed_joint_corrections"] = dict() +MIXAMO_SKELETON_MODEL["fixed_joint_corrections"]["RightClavicle"] = -90 +MIXAMO_SKELETON_MODEL["fixed_joint_corrections"]["LeftClavicle"] = 90 + +MAX_SKELETON_JOINTS = collections.OrderedDict() +MAX_SKELETON_JOINTS["root"] = None +MAX_SKELETON_JOINTS["pelvis"] = "Hips" +MAX_SKELETON_JOINTS["spine"] = None +MAX_SKELETON_JOINTS["spine_1"] = None +MAX_SKELETON_JOINTS["spine_2"] = "Chest" +MAX_SKELETON_JOINTS["left_clavicle"] = "LeftCollar" +MAX_SKELETON_JOINTS["right_clavicle"] = "RightCollar" +MAX_SKELETON_JOINTS["left_shoulder"] = "LeftShoulder" +MAX_SKELETON_JOINTS["right_shoulder"] = "RightShoulder" +MAX_SKELETON_JOINTS["left_elbow"] = "LeftElbow" +MAX_SKELETON_JOINTS["right_elbow"] = "RightElbow" +MAX_SKELETON_JOINTS["left_wrist"] = "LeftWrist" +MAX_SKELETON_JOINTS["right_wrist"] = "RightWrist" +MAX_SKELETON_JOINTS["left_finger"] = "LeftWrist_EndSite" +MAX_SKELETON_JOINTS["right_finger"] = "RightWrist_EndSite" +MAX_SKELETON_JOINTS["left_hip"] = "LeftHip" +MAX_SKELETON_JOINTS["right_hip"] = "RightHip" +MAX_SKELETON_JOINTS["left_knee"] = "LeftKnee" +MAX_SKELETON_JOINTS["right_knee"] = "RightKnee" +MAX_SKELETON_JOINTS["left_ankle"] = "LeftAnkle" +MAX_SKELETON_JOINTS["right_ankle"] = "RightAnkle" +MAX_SKELETON_JOINTS["left_toe"] = None +MAX_SKELETON_JOINTS["right_toe"] = None +MAX_SKELETON_JOINTS["left_heel"] = None +MAX_SKELETON_JOINTS["right_heel"] = None +MAX_SKELETON_JOINTS["neck"] = "Neck" +MAX_SKELETON_JOINTS["head"] = "Head" +MAX_SKELETON_COS_MAP = collections.defaultdict(dict) + +MAX_SKELETON_COS_MAP["LeftShoulder"]["y"] = [0, -1, 0] +MAX_SKELETON_COS_MAP["RightShoulder"]["y"] = [0, -1, 0] +MAX_SKELETON_COS_MAP["LeftShoulder"]["x"] = [-1, 0, 0] +MAX_SKELETON_COS_MAP["RightShoulder"]["x"] = [-1, 0, 0] + +MAX_SKELETON_COS_MAP["LeftElbow"]["y"] = [0, -1, 0] +MAX_SKELETON_COS_MAP["RightElbow"]["y"] = [0, -1, 0] +MAX_SKELETON_COS_MAP["LeftElbow"]["x"] = [-1, 0, 0] +MAX_SKELETON_COS_MAP["RightElbow"]["x"] = [-1, 0, 0] + +MAX_SKELETON_COS_MAP["LeftWrist"]["y"] = [0, -1, 0] +MAX_SKELETON_COS_MAP["RightWrist"]["y"] = [0, -1, 0] +MAX_SKELETON_COS_MAP["LeftWrist"]["x"] = [-1, 0, 0] +MAX_SKELETON_COS_MAP["RightWrist"]["x"] = [-1, 0, 0] + + +MAX_SKELETON_MODEL = collections.OrderedDict() +MAX_SKELETON_MODEL["name"] = "max" +MAX_SKELETON_MODEL["joints"] = MAX_SKELETON_JOINTS +MAX_SKELETON_MODEL["foot_joints"] = [] +MAX_SKELETON_MODEL["cos_map"] = MAX_SKELETON_COS_MAP + +AACHEN_SKELETON_JOINTS = collections.OrderedDict() +AACHEN_SKELETON_JOINTS["root"] = None#"reference" +AACHEN_SKELETON_JOINTS["pelvis"] = "Hips" +AACHEN_SKELETON_JOINTS["spine"] = "LowerBack" +AACHEN_SKELETON_JOINTS["spine_1"] = "Spine" +AACHEN_SKELETON_JOINTS["spine_2"] = "Spine1" +AACHEN_SKELETON_JOINTS["left_clavicle"] = "LeftShoulder" +AACHEN_SKELETON_JOINTS["right_clavicle"] = "RightShoulder" +AACHEN_SKELETON_JOINTS["left_shoulder"] = "LeftArm" +AACHEN_SKELETON_JOINTS["right_shoulder"] = "RightArm" +AACHEN_SKELETON_JOINTS["left_elbow"] = "LeftForeArm" +AACHEN_SKELETON_JOINTS["right_elbow"] = "RightForeArm" +AACHEN_SKELETON_JOINTS["left_wrist"] = "LeftHand" +AACHEN_SKELETON_JOINTS["right_wrist"] = "RightHand" +AACHEN_SKELETON_JOINTS["left_finger"] = "LeftFingerBase" +AACHEN_SKELETON_JOINTS["right_finger"] = "RightFingerBase" +AACHEN_SKELETON_JOINTS["left_hip"] = "LeftUpLeg" +AACHEN_SKELETON_JOINTS["right_hip"] = "RightUpLeg" +AACHEN_SKELETON_JOINTS["left_knee"] = "LeftLeg" +AACHEN_SKELETON_JOINTS["right_knee"] = "RightLeg" +AACHEN_SKELETON_JOINTS["left_ankle"] = "LeftFoot" +AACHEN_SKELETON_JOINTS["right_ankle"] = "RightFoot" +AACHEN_SKELETON_JOINTS["left_toe"] = "LeftToeBase" +AACHEN_SKELETON_JOINTS["right_toe"] = "RightToeBase" +AACHEN_SKELETON_JOINTS["left_heel"] = None +AACHEN_SKELETON_JOINTS["right_heel"] = None +AACHEN_SKELETON_JOINTS["neck"] = "Neck" +AACHEN_SKELETON_JOINTS["head"] = "Head" +AACHEN_SKELETON_MODEL = collections.OrderedDict() +AACHEN_SKELETON_MODEL["name"] = "aachen" +AACHEN_SKELETON_MODEL["joints"] = AACHEN_SKELETON_JOINTS +AACHEN_SKELETON_MODEL["foot_joints"] = [] + +AACHEN_SKELETON_COS_MAP = collections.defaultdict(dict) +AACHEN_SKELETON_COS_MAP["Hips"]["y"] = [0, 1, 0] +AACHEN_SKELETON_COS_MAP["Hips"]["x"] = [1, 0, 0] +AACHEN_SKELETON_MODEL["cos_map"] = AACHEN_SKELETON_COS_MAP + + +MH_CMU_RED_SKELETON_JOINTS = collections.OrderedDict() +MH_CMU_RED_SKELETON_JOINTS["root"] = "root" +MH_CMU_RED_SKELETON_JOINTS["pelvis"] = "pelvis" +MH_CMU_RED_SKELETON_JOINTS["thorax"] = "Spine" +MH_CMU_RED_SKELETON_JOINTS["left_clavicle"] = "lclavicle" +MH_CMU_RED_SKELETON_JOINTS["right_clavicle"] = "rclavicle" +MH_CMU_RED_SKELETON_JOINTS["left_shoulder"] = "lhumerus" +MH_CMU_RED_SKELETON_JOINTS["right_shoulder"] = "rhumerus" +MH_CMU_RED_SKELETON_JOINTS["left_elbow"] = "lradius" +MH_CMU_RED_SKELETON_JOINTS["right_elbow"] = "rradius" +MH_CMU_RED_SKELETON_JOINTS["left_wrist"] = "lhand" +MH_CMU_RED_SKELETON_JOINTS["right_wrist"] = "rhand" +MH_CMU_RED_SKELETON_JOINTS["left_hip"] = "lfemur" +MH_CMU_RED_SKELETON_JOINTS["right_hip"] = "rfemur" +MH_CMU_RED_SKELETON_JOINTS["left_knee"] = "ltibia" +MH_CMU_RED_SKELETON_JOINTS["right_knee"] = "rtibia" +MH_CMU_RED_SKELETON_JOINTS["left_ankle"] = "lfoot" +MH_CMU_RED_SKELETON_JOINTS["right_ankle"] = "rfoot" +MH_CMU_RED_SKELETON_JOINTS["left_toe"] = "ltoes" +MH_CMU_RED_SKELETON_JOINTS["right_toe"] = "rtoes" +MH_CMU_RED_SKELETON_JOINTS["left_heel"] = None +MH_CMU_RED_SKELETON_JOINTS["right_heel"] = None +MH_CMU_RED_SKELETON_JOINTS["neck"] = "head" +MH_CMU_RED_SKELETON_JOINTS["head"] = "head_EndSite" +MH_CMU_RED_SKELETON_MODEL = collections.OrderedDict() +MH_CMU_RED_SKELETON_MODEL["name"] = "mh_cmu_red" +MH_CMU_RED_SKELETON_MODEL["joints"] = MH_CMU_RED_SKELETON_JOINTS +MH_CMU_RED_SKELETON_MODEL["foot_joints"] = [] + +RAW2_SKELETON_FOOT_JOINTS = [RIGHT_TOE, LEFT_TOE, RIGHT_HEEL,LEFT_HEEL] + +RAW2_SKELETON_JOINTS = collections.OrderedDict() +RAW2_SKELETON_JOINTS["root"] = "Hips" +RAW2_SKELETON_JOINTS["pelvis"] = "ToSpine" +RAW2_SKELETON_JOINTS["spine"] = "Spine" +RAW2_SKELETON_JOINTS["spine_1"] = "Spine1" +RAW2_SKELETON_JOINTS["spine_2"] = "Neck" +RAW2_SKELETON_JOINTS["left_clavicle"] = "LeftShoulder" +RAW2_SKELETON_JOINTS["right_clavicle"] = "RightShoulder" +RAW2_SKELETON_JOINTS["left_shoulder"] = "LeftArm" +RAW2_SKELETON_JOINTS["right_shoulder"] = "RightArm" +RAW2_SKELETON_JOINTS["left_elbow"] = "LeftForeArm" +RAW2_SKELETON_JOINTS["right_elbow"] = "RightForeArm" +RAW2_SKELETON_JOINTS["left_wrist"] = "LeftHand" +RAW2_SKELETON_JOINTS["right_wrist"] = "RightHand" +RAW2_SKELETON_JOINTS["left_hip"] = "LeftUpLeg" +RAW2_SKELETON_JOINTS["right_hip"] = "RightUpLeg" +RAW2_SKELETON_JOINTS["left_knee"] = "LeftLeg" +RAW2_SKELETON_JOINTS["right_knee"] = "RightLeg" +RAW2_SKELETON_JOINTS["left_ankle"] = "LeftFoot" +RAW2_SKELETON_JOINTS["right_ankle"] = "RightFoot" +RAW2_SKELETON_JOINTS["left_toe"] = "LeftToeBase" +RAW2_SKELETON_JOINTS["right_toe"] = "RightToeBase" +RAW2_SKELETON_JOINTS["left_heel"] = "LeftHeel" +RAW2_SKELETON_JOINTS["right_heel"] = "RightHeel" +RAW2_SKELETON_JOINTS["neck"] = "Neck" +RAW2_SKELETON_JOINTS["head"] = "Head" + +RAW2_SKELETON_COS_MAP = collections.defaultdict(dict) +RAW2_SKELETON_COS_MAP["Neck"]["x"] = [1, 0, 0] +RAW2_SKELETON_COS_MAP["Neck"]["y"] = [0, 1, 0] +RAW2_SKELETON_COS_MAP["Head"]["x"] = [-1, 0, 0] +RAW2_SKELETON_COS_MAP["Head"]["y"] = [0, 1, 0] + +""" +RAW2_SKELETON_COS_MAP["LeftShoulder"]["x"] = [0, 1, 0] +RAW2_SKELETON_COS_MAP["LeftShoulder"]["y"] = [-1, 0, 0] +RAW2_SKELETON_COS_MAP["LeftForeArm"]["x"] = [-1, 0, 0] +RAW2_SKELETON_COS_MAP["LeftForeArm"]["y"] = [0, 1, 0] +RAW2_SKELETON_COS_MAP["LeftArm"]["x"] = [-1, 0, 0] +RAW2_SKELETON_COS_MAP["LeftArm"]["y"] = [0, 1, 0] +RAW2_SKELETON_COS_MAP["LeftHand"]["x"] = [-1, 0, 0] +RAW2_SKELETON_COS_MAP["LeftHand"]["y"] = [0, 1, 0] +RAW2_SKELETON_COS_MAP["RightShoulder"]["x"] = [0, 1, 0] +RAW2_SKELETON_COS_MAP["RightShoulder"]["y"] = [-1, 0, 0] +RAW2_SKELETON_COS_MAP["RightArm"]["x"] = [-1, 0, 0] +RAW2_SKELETON_COS_MAP["RightArm"]["y"] = [0, 1, 0] +RAW2_SKELETON_COS_MAP["RightForeArm"]["x"] = [-1, 0, 0] +RAW2_SKELETON_COS_MAP["RightForeArm"]["y"] = [0, 1, 0] +RAW2_SKELETON_COS_MAP["RightHand"]["x"] = [-1, 0, 0] +RAW2_SKELETON_COS_MAP["RightHand"]["y"] = [0, 1, 0] +""" +RAW2_SKELETON_MODEL = collections.OrderedDict() +RAW2_SKELETON_MODEL["name"] = "raw2" +RAW2_SKELETON_MODEL["joints"] = RAW2_SKELETON_JOINTS +RAW2_SKELETON_MODEL["foot_joints"] = RAW2_SKELETON_FOOT_JOINTS +RAW2_SKELETON_MODEL["heel_offset"] = [0, -6.480602, 0] +RAW2_SKELETON_MODEL["ik_chains"] = IK_CHAINS_RAW_SKELETON +RAW2_SKELETON_MODEL["cos_map"] = RAW2_SKELETON_COS_MAP + + + + +NOITOM_SKELETON_JOINTS = collections.OrderedDict() +NOITOM_SKELETON_JOINTS["root"] = None +NOITOM_SKELETON_JOINTS["pelvis"] = "Hips" +NOITOM_SKELETON_JOINTS["spine"] = "Spine" +NOITOM_SKELETON_JOINTS["spine_1"] = "Spine1" +NOITOM_SKELETON_JOINTS["spine_2"] = "Spine2" +NOITOM_SKELETON_JOINTS["left_clavicle"] = "LeftShoulder" +NOITOM_SKELETON_JOINTS["right_clavicle"] = "RightShoulder" +NOITOM_SKELETON_JOINTS["left_shoulder"] = "LeftArm" +NOITOM_SKELETON_JOINTS["right_shoulder"] = "RightArm" +NOITOM_SKELETON_JOINTS["left_elbow"] = "LeftForeArm" +NOITOM_SKELETON_JOINTS["right_elbow"] = "RightForeArm" +NOITOM_SKELETON_JOINTS["left_wrist"] = "LeftHand" +NOITOM_SKELETON_JOINTS["right_wrist"] = "RightHand" +#NOITOM_SKELETON_JOINTS["left_finger"] = "RightInHandMiddle" +#NOITOM_SKELETON_JOINTS["right_finger"] = "LeftInHandMiddle" +NOITOM_SKELETON_JOINTS["left_hip"] = "LeftUpLeg" +NOITOM_SKELETON_JOINTS["right_hip"] = "RightUpLeg" +NOITOM_SKELETON_JOINTS["left_knee"] = "LeftLeg" +NOITOM_SKELETON_JOINTS["right_knee"] = "RightLeg" +NOITOM_SKELETON_JOINTS["left_ankle"] = "LeftFoot" +NOITOM_SKELETON_JOINTS["right_ankle"] = "RightFoot" +NOITOM_SKELETON_JOINTS["left_toe"] = "LeftFoot_EndSite" +NOITOM_SKELETON_JOINTS["right_toe"] = "RightFoot_EndSite" +NOITOM_SKELETON_JOINTS["left_heel"] = None +NOITOM_SKELETON_JOINTS["right_heel"] = None +NOITOM_SKELETON_JOINTS["neck"] = "Neck" +NOITOM_SKELETON_JOINTS["head"] = "Head" + +NOITOM_SKELETON_JOINTS["right_thumb_base"] = "RightHandThumb1" +NOITOM_SKELETON_JOINTS["right_thumb_mid"] = "RightHandThumb2" +NOITOM_SKELETON_JOINTS["right_thumb_tip"] = "RightHandThumb3" +NOITOM_SKELETON_JOINTS["right_thumb_end"] = "RightHandThumb3_EndSite" + +NOITOM_SKELETON_JOINTS["right_index_finger_root"] = "RightInHandIndex" +NOITOM_SKELETON_JOINTS["right_index_finger_base"] = "RightHandIndex1" +NOITOM_SKELETON_JOINTS["right_index_finger_mid"] = "RightHandIndex2" +NOITOM_SKELETON_JOINTS["right_index_finger_tip"] = "RightHandIndex3" +NOITOM_SKELETON_JOINTS["right_index_finger_end"] = "RightHandIndex3_EndSite" + +NOITOM_SKELETON_JOINTS["right_middle_finger_root"] = "RightInHandMiddle" +NOITOM_SKELETON_JOINTS["right_middle_finger_base"] = "RightHandMiddle1" +NOITOM_SKELETON_JOINTS["right_middle_finger_mid"] = "RightHandMiddle2" +NOITOM_SKELETON_JOINTS["right_middle_finger_tip"] = "RightHandMiddle3" +NOITOM_SKELETON_JOINTS["right_middle_finger_end"] = "RightHandMiddle3_EndSite" + +NOITOM_SKELETON_JOINTS["right_ring_finger_root"] = "RightInHandRing" +NOITOM_SKELETON_JOINTS["right_ring_finger_base"] = "RightHandRing1" +NOITOM_SKELETON_JOINTS["right_ring_finger_mid"] = "RightHandRing2" +NOITOM_SKELETON_JOINTS["right_ring_finger_tip"] = "RightHandRing3" +NOITOM_SKELETON_JOINTS["right_ring_finger_end"] = "RightHandRing3_EndSite" + +NOITOM_SKELETON_JOINTS["right_pinky_finger_root"] = "RightInHandPinky" +NOITOM_SKELETON_JOINTS["right_pinky_finger_base"] = "RightHandPinky1" +NOITOM_SKELETON_JOINTS["right_pinky_finger_mid"] = "RightHandPinky2" +NOITOM_SKELETON_JOINTS["right_pinky_finger_tip"] = "RightHandPinky3" +NOITOM_SKELETON_JOINTS["right_pinky_finger_end"] = "RightHandPinky3_EndSite" + + +NOITOM_SKELETON_JOINTS["left_thumb_base"] = "LeftHandThumb1" +NOITOM_SKELETON_JOINTS["left_thumb_mid"] = "LeftHandThumb2" +NOITOM_SKELETON_JOINTS["left_thumb_tip"] = "LeftHandThumb3" +NOITOM_SKELETON_JOINTS["left_thumb_end"] = "LeftHandThumb3_EndSite" + +NOITOM_SKELETON_JOINTS["left_index_finger_root"] = "LeftInHandIndex" +NOITOM_SKELETON_JOINTS["left_index_finger_base"] = "LeftHandIndex1" +NOITOM_SKELETON_JOINTS["left_index_finger_mid"] = "LeftHandIndex2" +NOITOM_SKELETON_JOINTS["left_index_finger_tip"] = "LeftHandIndex3" +NOITOM_SKELETON_JOINTS["left_index_finger_end"] = "LeftHandIndex3_EndSite" + +NOITOM_SKELETON_JOINTS["left_middle_finger_root"] = "LeftInHandMiddle" +NOITOM_SKELETON_JOINTS["left_middle_finger_base"] = "LeftHandMiddle1" +NOITOM_SKELETON_JOINTS["left_middle_finger_mid"] = "LeftHandMiddle2" +NOITOM_SKELETON_JOINTS["left_middle_finger_tip"] = "LeftHandMiddle3" +NOITOM_SKELETON_JOINTS["left_middle_finger_end"] = "LeftHandMiddle3_EndSite" + +NOITOM_SKELETON_JOINTS["left_ring_finger_root"] = "LeftInHandRing" +NOITOM_SKELETON_JOINTS["left_ring_finger_base"] = "LeftHandRing1" +NOITOM_SKELETON_JOINTS["left_ring_finger_mid"] = "LeftHandRing2" +NOITOM_SKELETON_JOINTS["left_ring_finger_tip"] = "LeftHandRing3" +NOITOM_SKELETON_JOINTS["left_ring_finger_end"] = "LeftHandRing3_EndSite" + +NOITOM_SKELETON_JOINTS["left_pinky_finger_root"] = "LeftInHandPinky" +NOITOM_SKELETON_JOINTS["left_pinky_finger_base"] = "LeftHandPinky1" +NOITOM_SKELETON_JOINTS["left_pinky_finger_mid"] = "LeftHandPinky2" +NOITOM_SKELETON_JOINTS["left_pinky_finger_tip"] = "LeftHandPinky3" +NOITOM_SKELETON_JOINTS["left_pinky_finger_end"] = "LeftHandPinky3_EndSite" + + +NOITOM_SKELETON_MODEL = collections.OrderedDict() +NOITOM_SKELETON_MODEL["name"] = "noitom" +NOITOM_SKELETON_MODEL["joints"] = NOITOM_SKELETON_JOINTS +NOITOM_SKELETON_COS_MAP = collections.defaultdict(dict) +NOITOM_SKELETON_COS_MAP["LeftHand"]["x"] = [0, -1, 0] +NOITOM_SKELETON_COS_MAP["LeftHand"]["y"] = [1, 0, 0] +NOITOM_SKELETON_COS_MAP["RightHand"]["x"] = [0, 1, 0] +NOITOM_SKELETON_COS_MAP["RightHand"]["y"] = [-1, 0, 0] +for k, j in NOITOM_SKELETON_JOINTS.items(): + if j is None: + continue + if "finger" in k or "thumb" in k: + if "Left" in j: + NOITOM_SKELETON_COS_MAP[j]["x"] = [0, 1, 0] + NOITOM_SKELETON_COS_MAP[j]["y"] = [1, 0, 0] + elif "Right" in j: + NOITOM_SKELETON_COS_MAP[j]["x"] = [0, 1, 0] + NOITOM_SKELETON_COS_MAP[j]["y"] = [-1, 0, 0] +NOITOM_SKELETON_MODEL["cos_map"] = NOITOM_SKELETON_COS_MAP + + + + + + + +MO_SKELETON_JOINTS = collections.OrderedDict() +MO_SKELETON_JOINTS["root"] = None +MO_SKELETON_JOINTS["pelvis"] = "Hips" +MO_SKELETON_JOINTS["spine"] = "Spine" +MO_SKELETON_JOINTS["spine_1"] = "Spine1" +MO_SKELETON_JOINTS["spine_2"] = "Spine2" +MO_SKELETON_JOINTS["left_clavicle"] = "LeftShoulder" +MO_SKELETON_JOINTS["right_clavicle"] = "RightShoulder" +MO_SKELETON_JOINTS["left_shoulder"] = "LeftArm" +MO_SKELETON_JOINTS["right_shoulder"] = "RightArm" +MO_SKELETON_JOINTS["left_elbow"] = "LeftForeArm" +MO_SKELETON_JOINTS["right_elbow"] = "RightForeArm" +MO_SKELETON_JOINTS["left_wrist"] = "LeftHand" +MO_SKELETON_JOINTS["right_wrist"] = "RightHand" +#MO_SKELETON_JOINTS["left_finger"] = "RightInHandMiddle" +#MO_SKELETON_JOINTS["right_finger"] = "LeftInHandMiddle" +MO_SKELETON_JOINTS["left_hip"] = "LeftUpLeg" +MO_SKELETON_JOINTS["right_hip"] = "RightUpLeg" +MO_SKELETON_JOINTS["left_knee"] = "LeftLeg" +MO_SKELETON_JOINTS["right_knee"] = "RightLeg" +MO_SKELETON_JOINTS["left_ankle"] = "LeftFoot" +MO_SKELETON_JOINTS["right_ankle"] = "RightFoot" +MO_SKELETON_JOINTS["left_toe"] = "LeftToeBase" +MO_SKELETON_JOINTS["right_toe"] = "RightToeBase" +MO_SKELETON_JOINTS["left_heel"] = None +MO_SKELETON_JOINTS["right_heel"] = None +MO_SKELETON_JOINTS["neck"] = "Neck" +MO_SKELETON_JOINTS["head"] = "Head" + +MO_SKELETON_MODEL = collections.OrderedDict() +MO_SKELETON_MODEL["name"] = "mo" +MO_SKELETON_MODEL["joints"] = MO_SKELETON_JOINTS +MO_SKELETON_COS_MAP = collections.defaultdict(dict) +MO_SKELETON_COS_MAP["LeftHand"]["x"] = [0, -1, 0] +MO_SKELETON_COS_MAP["LeftHand"]["y"] = [1, 0, 0] +MO_SKELETON_COS_MAP["RightHand"]["x"] = [0, 1, 0] +MO_SKELETON_COS_MAP["RightHand"]["y"] = [-1, 0, 0] +MO_SKELETON_MODEL["cos_map"] = NOITOM_SKELETON_COS_MAP + + +SKELETON_MODELS = collections.OrderedDict() +SKELETON_MODELS["rocketbox"] = ROCKETBOX_SKELETON_MODEL +SKELETON_MODELS["game_engine"] = GAME_ENGINE_SKELETON_MODEL +SKELETON_MODELS["raw"] = RAW_SKELETON_MODEL +SKELETON_MODELS["cmu"] = CMU_SKELETON_MODEL +SKELETON_MODELS["mcs"] = MCS_SKELETON_MODEL +SKELETON_MODELS["mh_cmu"] = MH_CMU_SKELETON_MODEL +SKELETON_MODELS["mh_cmu2"] = MH_CMU_2_SKELETON_MODEL +SKELETON_MODELS["iclone"] = ICLONE_SKELETON_MODEL +SKELETON_MODELS["moviemation"] = MOVIEMATION_SKELETON_MODEL +SKELETON_MODELS["custom"] = CUSTOM_SKELETON_MODEL +SKELETON_MODELS["captury"] = CAPTURY_SKELETON_MODEL +SKELETON_MODELS["holden"] = HOLDEN_SKELETON_MODEL +SKELETON_MODELS["mixamo"] = MIXAMO_SKELETON_MODEL +SKELETON_MODELS["max"] = MAX_SKELETON_MODEL +SKELETON_MODELS["aachen"] = AACHEN_SKELETON_MODEL +SKELETON_MODELS["mh_cmu_red"] = MH_CMU_RED_SKELETON_MODEL +SKELETON_MODELS["game_engine_wrong_root"] = GAME_ENGINE_WRONG_ROOT_SKELETON_MODEL +SKELETON_MODELS["raw2"] = RAW2_SKELETON_MODEL +SKELETON_MODELS["noitom"] = NOITOM_SKELETON_MODEL +SKELETON_MODELS["mo"] = MO_SKELETON_MODEL + +STANDARD_JOINTS = ["root","pelvis", "spine_1", "spine_2", "neck", "left_clavicle", "head", "left_shoulder", "left_elbow", "left_wrist", "right_clavicle", "right_shoulder", + "left_hip", "left_knee", "left_ankle", "left_toe", "right_elbow", "right_hip", "right_knee", "right_ankle", "right_toe"] + +FINGER_JOINTS = ["left_thumb_base","left_thumb_mid", "left_thumb_tip","left_thumb_end", + "left_index_finger_root","left_index_finger_base","left_index_finger_mid", "left_index_finger_tip","left_index_finger_end", + "left_middle_finger_root","left_middle_finger_base","left_middle_finger_mid","left_middle_finger_tip","left_middle_finger_end", + "left_ring_finger_root","left_ring_finger_base","left_ring_finger_mid","left_ring_finger_tip", "left_ring_finger_end", + "left_pinky_finger_root","left_pinky_finger_base","left_pinky_finger_mid","left_pinky_finger_tip", "left_pinky_finger_end", + + "right_thumb_base","right_thumb_mid","right_thumb_tip","right_thumb_end", + "right_index_finger_root","right_index_finger_base","right_index_finger_mid","right_index_finger_tip","right_index_finger_end", + "right_middle_finger_root","right_middle_finger_base","right_middle_finger_mid","right_middle_finger_tip","right_middle_finger_end", + "right_ring_finger_root","right_ring_finger_base","right_ring_finger_mid","right_ring_finger_tip","right_ring_finger_end", + "right_pinky_finger_root","right_pinky_finger_base","right_pinky_finger_mid","right_pinky_finger_tip","right_pinky_finger_end" + ] +UPPER_BODY_JOINTS = ["spine", "spine_1", "spine_2","neck", "head" + "left_clavicle", "right_clavicle", + "left_shoulder", "right_shoulder", + "left_elbow", "right_elbow", + "left_wrist", "right_wrist", + "left_hold_point","right_hold_point" +] + + + +JOINT_CHILD_MAP = dict() +JOINT_CHILD_MAP["root"] = "pelvis" +JOINT_CHILD_MAP["pelvis"] = "spine" +JOINT_CHILD_MAP["spine"] = "spine_1" +JOINT_CHILD_MAP["spine_1"] = "spine_2" +JOINT_CHILD_MAP["spine_2"] = "neck" +JOINT_CHILD_MAP["neck"] = "head" +JOINT_CHILD_MAP["left_clavicle"] = "left_shoulder" +JOINT_CHILD_MAP["left_shoulder"] = "left_elbow" +JOINT_CHILD_MAP["left_elbow"] = "left_wrist" +JOINT_CHILD_MAP["left_wrist"] = "left_finger" # TODO rename to hand center +JOINT_CHILD_MAP["right_clavicle"] = "right_shoulder" +JOINT_CHILD_MAP["right_shoulder"] = "right_elbow" +JOINT_CHILD_MAP["right_elbow"] = "right_wrist" +JOINT_CHILD_MAP["right_wrist"] = "right_finger" # TODO rename to hand center +JOINT_CHILD_MAP["left_hip"] = "left_knee" +JOINT_CHILD_MAP["left_knee"] = "left_ankle" +JOINT_CHILD_MAP["right_elbow"] = "right_wrist" +JOINT_CHILD_MAP["right_hip"] = "right_knee" +JOINT_CHILD_MAP["right_knee"] = "right_ankle" +JOINT_CHILD_MAP["left_ankle"] = "left_toe" +JOINT_CHILD_MAP["right_ankle"] = "right_toe" + +JOINT_PARENT_MAP = {v: k for k, v in JOINT_CHILD_MAP.items()} + +FABRIK_CHAINS = dict() +FABRIK_CHAINS["right_wrist"] = {"joint_order": ["right_shoulder", "right_elbow", "right_wrist"]} +FABRIK_CHAINS["left_wrist"] = {"joint_order": ["left_shoulder", "left_elbow", "left_wrist"]} + +JOINT_CONSTRAINTS = dict() +JOINT_CONSTRAINTS["right_elbow"] = {"type":"hinge", "swing_axis": [0,-1,0], "twist_axis": [0,0,1], "k1":-90, "k2":90} +JOINT_CONSTRAINTS["left_elbow"] = {"type": "hinge", "swing_axis": [0,-1,0], "twist_axis": [0,0,1], "k1":-90, "k2":90} +#JOINT_CONSTRAINTS["right_shoulder"] = {"type":"cone", "axis": [0,0,1], "k": 0.4} +#JOINT_CONSTRAINTS["left_shoulder"] = {"type": "cone", "axis": [0,0,1], "k": 0.4} +#JOINT_CONSTRAINTS["spine"] = {"type":"cone", "axis": [0,1,0], "k": 45)} +JOINT_CONSTRAINTS["spine_1"] = {"type":"spine", "axis": [0,1,0], "tk1": -90, "tk2": 90, "sk1": -0, "sk2": 0} +##OINT_CONSTRAINTS["spine"] = {"type":"shoulder", "axis": [0,1,0], "k": 20), "k1": -90), "k2": 90) } +JOINT_CONSTRAINTS["left_shoulder"] = {"type":"shoulder", "axis": [0,0,1], "k": 0.4, "k1": -180, "k2": 180 , "stiffness":0.9} +JOINT_CONSTRAINTS["right_shoulder"] = {"type":"shoulder", "axis": [0,0,1], "k": 0.4, "k1": -180, "k2": 180, "stiffness":0.9} + +JOINT_CONSTRAINTS["left_wrist"] = {"type":"cone", "axis": [0,0,1], "k": 90} +JOINT_CONSTRAINTS["right_wrist"] = {"type":"cone", "axis": [0,0,1], "k": 90} +#JOINT_CONSTRAINTS["head"] = {"type":"cone", "axis": [0,1,0], "k": 0)} +#JOINT_CONSTRAINTS["neck"] = {"type":"head", "axis": [0,1,0], "k1": -65), "k2": 65)} +JOINT_CONSTRAINTS["head"] = {"type":"head", "axis": [0,1,0], "tk1": -85, "tk2": 85, "sk1": -45, "sk2": 45} +JOINT_CONSTRAINTS["left_hold_point"] = {"type":"static"} +JOINT_CONSTRAINTS["right_hold_point"] = {"type":"static"} + +STANDARD_MIRROR_MAP_LEFT = {"left_hip": "right_hip", + "left_knee": "right_knee", + "left_ankle":"right_ankle", + "left_clavicle": "right_clavicle", + "left_shoulder": "right_shoulder", + "left_elbow": "right_elbow", + "left_elbow": "right_elbow", + "left_wrist": "right_wrist", + "left_hold_point": "right_hold_point" + } + +STANDARD_MIRROR_MAP_LEFT["left_thumb_base"] = "right_thumb_base" +STANDARD_MIRROR_MAP_LEFT["left_thumb_mid"] = "right_thumb_mid" +STANDARD_MIRROR_MAP_LEFT["left_thumb_tip"] = "right_thumb_tip" +STANDARD_MIRROR_MAP_LEFT["left_thumb_end"] = "right_thumb_end" + +STANDARD_MIRROR_MAP_LEFT["left_index_finger_root"] = "right_index_finger_root" +STANDARD_MIRROR_MAP_LEFT["left_index_finger_base"] = "right_index_finger_base" +STANDARD_MIRROR_MAP_LEFT["left_index_finger_mid"] = "right_index_finger_mid" +STANDARD_MIRROR_MAP_LEFT["left_index_finger_tip"] = "right_index_finger_tip" +STANDARD_MIRROR_MAP_LEFT["left_index_finger_end"] = "right_index_finger_end" + +STANDARD_MIRROR_MAP_LEFT["left_middle_finger_root"] = "right_middle_finger_root" +STANDARD_MIRROR_MAP_LEFT["left_middle_finger_base"] = "right_middle_finger_base" +STANDARD_MIRROR_MAP_LEFT["left_middle_finger_mid"] = "right_middle_finger_mid" +STANDARD_MIRROR_MAP_LEFT["left_middle_finger_tip"] = "right_middle_finger_tip" +STANDARD_MIRROR_MAP_LEFT["left_middle_finger_end"] = "right_middle_finger_end" + +STANDARD_MIRROR_MAP_LEFT["left_ring_finger_root"] = "right_ring_finger_root" +STANDARD_MIRROR_MAP_LEFT["left_ring_finger_base"] = "right_ring_finger_base" +STANDARD_MIRROR_MAP_LEFT["left_ring_finger_mid"] = "right_ring_finger_mid" +STANDARD_MIRROR_MAP_LEFT["left_ring_finger_tip"] = "right_ring_finger_tip" +STANDARD_MIRROR_MAP_LEFT["left_ring_finger_end"] = "right_ring_finger_end" + +STANDARD_MIRROR_MAP_LEFT["left_pinky_finger_root"] = "right_pinky_finger_root" +STANDARD_MIRROR_MAP_LEFT["left_pinky_finger_base"] = "right_pinky_finger_base" +STANDARD_MIRROR_MAP_LEFT["left_pinky_finger_mid"] = "right_pinky_finger_mid" +STANDARD_MIRROR_MAP_LEFT["left_pinky_finger_tip"] = "right_pinky_finger_tip" +STANDARD_MIRROR_MAP_LEFT["left_pinky_finger_end"] = "right_pinky_finger_end" + +STANDARD_MIRROR_MAP_RIGHT = dict() +for key, value in STANDARD_MIRROR_MAP_LEFT.items(): + STANDARD_MIRROR_MAP_RIGHT[value] = key + +STANDARD_MIRROR_MAP = dict() +STANDARD_MIRROR_MAP.update(STANDARD_MIRROR_MAP_LEFT) +STANDARD_MIRROR_MAP.update(STANDARD_MIRROR_MAP_RIGHT) diff --git a/build/lib/anim_utils/animation_data/skeleton_node.py b/build/lib/anim_utils/animation_data/skeleton_node.py new file mode 100644 index 0000000..008e862 --- /dev/null +++ b/build/lib/anim_utils/animation_data/skeleton_node.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import numpy as np +from transformations import quaternion_matrix, euler_matrix, euler_from_matrix, quaternion_from_matrix, quaternion_about_axis +from .constants import ROTATION_TYPE_EULER, ROTATION_TYPE_QUATERNION +from .utils import rotation_order_to_string + +SKELETON_NODE_TYPE_ROOT = 0 +SKELETON_NODE_TYPE_JOINT = 1 +SKELETON_NODE_TYPE_END_SITE = 2 +ORIGIN = point = np.array([0, 0, 0, 1]) + + +class SkeletonNodeBase(object): + def __init__(self, node_name, channels, parent=None, level=0, channel_indices=None): + self.parent = parent + self.node_name = node_name + self.channels = channels + self.level = level + self.children = [] + self.index = -1 + self.quaternion_frame_index = -1 + self.euler_frame_index = -1 + self.node_type = None + self.offset = [0.0, 0.0, 0.0] + self.rotation = np.array([1.0, 0.0, 0.0, 0.0]) + self.euler_angles = np.array([0.0, 0.0, 0.0]) + self.translation = np.array([0.0, 0.0, 0.0]) + self.fixed = True + self.cached_global_matrix = None + self.joint_constraint = None + self.format_meta_data = dict() # store format specific data + self.stiffness = 0 + self.rotation_order = [c for c in self.channels if "rotation" in c] + self._rotation_order_str = rotation_order_to_string(self.rotation_order) + self.channel_indices = channel_indices + self.translation_order = [c for c in self.channels if "position" in c] + if self.channel_indices is not None: + self.rotation_channel_indices = [self.channel_indices[self.channels.index(r)] for r in self.rotation_order] + self.translation_channel_indices = [self.channel_indices[self.channels.index(t)] for t in self.translation_order] + + def clear_cached_global_matrix(self): + self.cached_global_matrix = None + + def get_global_position(self, quaternion_frame, use_cache=False): + global_matrix = self.get_global_matrix(quaternion_frame, use_cache) + point = np.dot(global_matrix, ORIGIN) + return point[:3] + + def get_global_position_from_euler(self, euler_frame, use_cache=False): + global_matrix = self.get_global_matrix_from_euler_frame(euler_frame, use_cache) + point = np.dot(global_matrix, ORIGIN) + return point[:3] + + def get_global_orientation_quaternion(self, quaternion_frame, use_cache=False): + global_matrix = self.get_global_matrix(quaternion_frame, use_cache) + return quaternion_from_matrix(global_matrix) + + def get_global_orientation_euler(self, quaternion_frame, use_cache=False): + global_matrix = self.get_global_matrix(quaternion_frame, use_cache) + return euler_from_matrix(global_matrix) + + def get_global_matrix(self, quaternion_frame, use_cache=False): + if self.cached_global_matrix is not None and use_cache: + return self.cached_global_matrix + else: + if self.parent is not None: + parent_matrix = self.parent.get_global_matrix(quaternion_frame, use_cache) + self.cached_global_matrix = np.dot(parent_matrix, self.get_local_matrix(quaternion_frame)) + return self.cached_global_matrix + else: + self.cached_global_matrix = self.get_local_matrix(quaternion_frame) + return self.cached_global_matrix + + def get_global_matrix_from_euler_frame(self, euler_frame, use_cache=False): + if self.parent is not None: + parent_matrix = self.parent.get_global_matrix_from_euler_frame(euler_frame) + self.cached_global_matrix = np.dot(parent_matrix, self.get_local_matrix_from_euler(euler_frame)) + return self.cached_global_matrix + else: + self.cached_global_matrix = self.get_local_matrix_from_euler(euler_frame) + return self.cached_global_matrix + + def get_global_matrix_from_anim_frame(self, frame, use_cache=False): + if self.cached_global_matrix is not None and use_cache: + return self.cached_global_matrix + else: + if self.parent is not None: + parent_matrix = self.parent.get_global_matrix2(frame, use_cache) + self.cached_global_matrix = np.dot(parent_matrix, frame[self.node_name]) + return self.cached_global_matrix + else: + self.cached_global_matrix = frame[self.node_name] + return self.cached_global_matrix + + def get_local_matrix(self, quaternion_frame): + pass + + def get_local_matrix_from_euler(self, euler_frame): + pass + + def get_frame_parameters(self, frame, rotation_type): + pass + + def get_number_of_frame_parameters(self, rotation_type): + pass + + def to_unity_format(self, joints, scale=1.0, joint_name_map=None): + joint_desc = dict() + joint_desc["name"] = self.node_name + if type(self.offset) == list: + offset = list(self.offset) + else: + offset = self.offset.tolist() + offset[0] *= -scale + offset[1] *= scale + offset[2] *= scale + joint_desc["offset"] = offset + + if type(self.rotation) == list: + rotation = list(self.rotation) + else: + rotation = self.rotation.tolist() + rotation[0] *= -scale + rotation[1] *= -scale + rotation[2] *= scale + rotation[3] *= scale + joint_desc["rotation"] = rotation + if joint_name_map is not None: + if self.node_name in joint_name_map: + joint_desc["targetName"] = joint_name_map[self.node_name] + else: + joint_desc["targetName"] = "none" + else: + joint_desc["targetName"] = self.node_name + joint_desc["children"] = [] + joints.append(joint_desc) + for c in self.children: + joint_desc["children"].append(c.node_name) + c.to_unity_format(joints, scale, joint_name_map=joint_name_map) + + def get_fk_chain_list(self): + pass + + def get_parent_name(self, animated_joint_list): + ''' + :return: string, the name of parent node. If parent node is None, return None + ''' + if self.parent is None: + return None + else: + if animated_joint_list is None: + return self.parent.node_name + else: + if self.parent.node_name in animated_joint_list: + return self.parent.node_name + else: + return self.parent.get_parent_name(animated_joint_list) + + +class SkeletonRootNode(SkeletonNodeBase): + def __init__(self, node_name, channels, parent=None, level=0, channel_indices=None): + super(SkeletonRootNode, self).__init__(node_name, channels, parent, level, channel_indices) + self.node_type = SKELETON_NODE_TYPE_ROOT + + def get_local_matrix(self, quaternion_frame): + local_matrix = quaternion_matrix(quaternion_frame[3:7]) + local_matrix[:3, 3] = [t + o for t, o in zip(quaternion_frame[:3], self.offset)] + return local_matrix + + def get_local_matrix_from_euler(self, euler_frame): + if not self.rotation_order is None and not self.rotation_channel_indices is None: + local_matrix = euler_matrix(*euler_frame[self.rotation_channel_indices], self._rotation_order_str) + else: + raise ValueError("no rotation order!") + if len(self.translation_channel_indices)> 0: + local_matrix[:3, 3] = [t + o for t, o in zip(euler_frame[self.translation_channel_indices], self.offset)] + else: + local_matrix[:3, 3] = self.offset + return local_matrix + + def get_frame_parameters(self, frame, rotation_type): + if not self.fixed: + return frame[:7].tolist() + else: + return [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0] + + def get_euler_frame_parameters(self, euler_frame): + if not self.fixed: + return euler_frame[:6].tolist() + else: + return [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + + def get_number_of_frame_parameters(self, rotation_type): + if rotation_type == ROTATION_TYPE_QUATERNION: + return 7 + else: + return 6 + + def get_fk_chain_list(self): + return [self.node_name] + +class SkeletonJointNode(SkeletonNodeBase): + def __init__(self, node_name, channels, parent=None, level=0, channel_indices=None): + super(SkeletonJointNode, self).__init__(node_name, channels, parent, level, channel_indices) + self.node_type = SKELETON_NODE_TYPE_JOINT + + def get_local_matrix(self, quaternion_frame): + if not self.fixed: + frame_index = self.quaternion_frame_index * 4 + 3 + m = quaternion_matrix(quaternion_frame[frame_index: frame_index + 4]) + else: + m = quaternion_matrix(self.rotation) + m[:3, 3] = self.offset + return m + + def get_local_matrix_from_euler(self, euler_frame): + if not self.fixed: + if not self.rotation_order is None: + local_matrix = euler_matrix(*euler_frame[self.rotation_channel_indices], self._rotation_order_str) + else: + raise ValueError("no rotation order!") + else: + local_matrix = euler_matrix(*np.radians(self.euler_angles), axes='rxyz') + # local_matrix[:3, 3] = self.offset + if self.translation_channel_indices != []: + local_matrix[:3, 3] = [t + o for t, o in zip(euler_frame[self.translation_channel_indices], self.offset)] + else: + local_matrix[:3, 3] = self.offset + return local_matrix + + def get_frame_parameters(self, frame, rotation_type): + if rotation_type == ROTATION_TYPE_QUATERNION: + if not self.fixed: + frame_index = self.quaternion_frame_index * 4 + 3 + return frame[frame_index:frame_index+4].tolist() + else: + return self.rotation + else: + if not self.fixed: + frame_index = self.quaternion_frame_index * 3 + 3 + return frame[frame_index:frame_index+3].tolist() + else: + return [0.0, 0.0, 0.0] + + def get_number_of_frame_parameters(self, rotation_type): + if rotation_type == ROTATION_TYPE_QUATERNION: + return 4 + else: + return 3 + + def get_fk_chain_list(self): + fk_chain = [self.node_name] + if self.parent is not None: + fk_chain += self.parent.get_fk_chain_list() + return fk_chain + +class SkeletonEndSiteNode(SkeletonNodeBase): + def __init__(self, node_name, channels, parent=None, level=0): + super(SkeletonEndSiteNode, self).__init__(node_name, channels, parent, level) + self.node_type = SKELETON_NODE_TYPE_END_SITE + + def get_local_matrix(self, quaternion_frame): + local_matrix = np.identity(4) + local_matrix[:3, 3] = self.offset + return local_matrix + + def get_local_matrix_from_euler(self, euler_frame): + local_matrix = np.identity(4) + local_matrix[:3, 3] = self.offset + return local_matrix + + def get_frame_parameters(self, frame, rotation_type): + return None + + def get_number_of_frame_parameters(self, rotation_type): + return 0 + + def get_fk_chain_list(self): + fk_chain = [] + if self.parent is not None: + fk_chain += self.parent.get_fk_chain_list() + return fk_chain diff --git a/build/lib/anim_utils/animation_data/utils.py b/build/lib/anim_utils/animation_data/utils.py new file mode 100644 index 0000000..b8794bb --- /dev/null +++ b/build/lib/anim_utils/animation_data/utils.py @@ -0,0 +1,947 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH, Daimler AG. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +""" +Created on Thu Feb 12 20:11:35 2015 + +@author: Erik Herrmann, Han Du +""" +import os +import glob +from math import sqrt, radians, sin, cos, isnan +from copy import deepcopy +import numpy as np +from scipy.stats import linregress +from collections import OrderedDict +from .constants import * +from transformations import quaternion_matrix, euler_from_matrix, \ + quaternion_from_matrix, euler_matrix, \ + quaternion_multiply, \ + quaternion_about_axis, \ + rotation_matrix, \ + quaternion_conjugate, \ + quaternion_inverse, \ + rotation_from_matrix, \ + quaternion_slerp, \ + quaternion_conjugate, quaternion_inverse, rotation_from_matrix + + +def rotation_order_to_string(rotation_order): + r_order_string = "r" + for c in rotation_order: + if c == "Xrotation": + r_order_string += "x" + elif c == "Yrotation": + r_order_string += "y" + elif c == "Zrotation": + r_order_string += "z" + return r_order_string + + +def get_arc_length_from_points(points): + """ + Note: accuracy depends on the granulariy of points + """ + points = np.asarray(points) + arc_length = 0.0 + last_p = None + for p in points: + if last_p is not None: + delta = p - last_p + arc_length += np.linalg.norm(delta) + last_p = p + return arc_length + + + +def quaternion_data_smoothing(quat_frames): + smoothed_quat_frames = {} + filenames = quat_frames.keys() + smoothed_quat_frames_data = np.asarray(deepcopy(quat_frames.values())) + print('quaternion frame data shape: ', smoothed_quat_frames_data.shape) + assert len(smoothed_quat_frames_data.shape) == 3, ('The shape of quaternion frames is not correct!') + n_samples, n_frames, n_dims = smoothed_quat_frames_data.shape + n_joints = (n_dims - 3)/4 + for i in range(n_joints): + ref_quat = smoothed_quat_frames_data[0, 0, i*LEN_QUAT + LEN_ROOT : (i+1)*LEN_QUAT + LEN_ROOT] + for j in range(1, n_samples): + test_quat = smoothed_quat_frames_data[j, 0, i*LEN_QUAT + LEN_ROOT : (i+1)*LEN_QUAT + LEN_ROOT] + if not areQuatClose(ref_quat, test_quat): + smoothed_quat_frames_data[j, :, i*LEN_QUAT + LEN_ROOT : (i+1)*LEN_QUAT + LEN_ROOT] *= -1 + for i in range(len(filenames)): + smoothed_quat_frames[filenames[i]] = smoothed_quat_frames_data[i] + return smoothed_quat_frames + + +def areQuatClose(quat1, quat2): + dot = np.dot(quat1, quat2) + if dot < 0.0: + return False + else: + return True + + +def get_step_length(frames): + """ + + :param frames: a list of euler or quaternion frames + :return step_len: travelled distance of root joint + """ + root_points = extract_root_positions(frames) + step_len = get_arc_length_from_points(root_points) + return step_len + + +def quaternion_from_vector_to_vector(a, b): + """src: http://stackoverflow.com/questions/1171849/finding-quaternion-representing-the-rotation-from-one-vector-to-another + http://wiki.ogre3d.org/Quaternion+and+Rotation+Primer""" + + v = np.cross(a, b) + w = np.sqrt((np.linalg.norm(a) ** 2) * (np.linalg.norm(b) ** 2)) + np.dot(a, b) + q = np.array([w, v[0], v[1], v[2]]) + if np.dot(q,q) != 0: + return q/ np.linalg.norm(q) + else: + idx = np.nonzero(a)[0] + q = np.array([0, 0, 0, 0]) + q[1 + ((idx + 1) % 2)] = 1 # [0, 0, 1, 0] for a rotation of 180 around y axis + return q + + +def convert_euler_frame_to_reduced_euler_frame(bvhreader, euler_frame): + if type(euler_frame) != list: + euler_frame = list(euler_frame) + reduced_euler_frame = euler_frame[:LEN_ROOT] + for node_name in bvhreader.node_names: + if not node_name.startswith('Bip') and 'EndSite' not in node_name: + x_idx = bvhreader.node_channels.index((node_name, 'Xrotation')) + reduced_euler_frame += euler_frame[x_idx: x_idx + LEN_EULER] + return reduced_euler_frame + + +def convert_euler_frames_to_reduced_euler_frames(bvhreader, euler_frames): + reduced_euler_frames = [] + for euler_frame in euler_frames: + reduced_euler_frames.append(convert_euler_frame_to_reduced_euler_frame(bvhreader, euler_frame)) + return reduced_euler_frames + + +def euler_substraction(theta1, theta2): + ''' compute the angular distance from theta2 to theta1, positive value is anti-clockwise, negative is clockwise + @param theta1, theta2: angles in degree + ''' + theta1 %= 360 + theta2 %= 360 + if theta1 > 180: + theta1 -= 360 + elif theta1 < -180: + theta1 += 360 + + if theta2 > 180: + theta2 -= 360 + elif theta2 < - 180: + theta2 += 360 + + theta = theta1 - theta2 + if theta > 180: + theta -= 360 + elif theta < -180: + theta += 360 + if theta > 180 or theta < - 180: + raise ValueError(' exception value') + return theta + + +def get_cartesian_coordinates_from_quaternion(skeleton, node_name, quaternion_frame, return_gloabl_matrix=False): + """Returns cartesian coordinates for one node at one frame. Modified to + handle frames with omitted values for joints starting with "Bip" + + Parameters + ---------- + * node_name: String + \tName of node + * skeleton: Skeleton + \tBVH data structure read from a file + + """ + if skeleton.node_names[node_name]["level"] == 0: + root_frame_position = quaternion_frame[:3] + root_node_offset = skeleton.node_names[node_name]["offset"] + + return [t + o for t, o in + zip(root_frame_position, root_node_offset)] + + else: + # Names are generated bottom to up --> reverse + chain_names = list(skeleton.gen_all_parents(node_name)) + chain_names.reverse() + chain_names += [node_name] # Node is not in its parent list + + offsets = [skeleton.node_names[nodename]["offset"] + for nodename in chain_names] + root_position = quaternion_frame[:3].flatten() + offsets[0] = [r + o for r, o in zip(root_position, offsets[0])] + + j_matrices = [] + count = 0 + for node_name in chain_names: + index = skeleton.node_name_frame_map[node_name] * 4 + 3 + j_matrix = quaternion_matrix(quaternion_frame[index: index + 4]) + j_matrix[:, 3] = offsets[count] + [1] + j_matrices.append(j_matrix) + count += 1 + + global_matrix = np.identity(4) + for j_matrix in j_matrices: + global_matrix = np.dot(global_matrix, j_matrix) + if return_gloabl_matrix: + return global_matrix + else: + point = np.array([0, 0, 0, 1]) + point = np.dot(global_matrix, point) + return point[:3].tolist() + + +def convert_euler_frame_to_cartesian_frame(skeleton, euler_frame, animated_joints=None): + """ + converts euler frames to cartesian frames by calling get_cartesian_coordinates for each joint + """ + if animated_joints is None: + n_joints = len(skeleton.animated_joints) + cartesian_frame = np.zeros([n_joints, LEN_ROOT_POS]) + for i in range(n_joints): + cartesian_frame[i] = skeleton.nodes[skeleton.animated_joints[i]].get_global_position_from_euler(euler_frame) + else: + n_joints = len(animated_joints) + cartesian_frame = np.zeros([n_joints, LEN_ROOT_POS]) + for i in range(n_joints): + cartesian_frame[i] = skeleton.nodes[animated_joints[i]].get_global_position_from_euler(euler_frame) + return cartesian_frame + + +def convert_quaternion_frame_to_cartesian_frame(skeleton, quat_frame): + """ + Converts quaternion frames to cartesian frames by calling get_cartesian_coordinates_from_quaternion for each joint + """ + cartesian_frame = [] + for node_name in skeleton.node_names: + # ignore Bip joints and end sites + if not node_name.startswith( + "Bip") and "children" in list(skeleton.node_names[node_name].keys()): + cartesian_frame.append( + get_cartesian_coordinates_from_quaternion( + skeleton, + node_name, + quat_frame)) # get_cartesian_coordinates2 + + return cartesian_frame + + +def convert_quaternion_frames_to_cartesian_frames(skeleton, quat_frames): + cartesian_frames = [] + for i in range(len(quat_frames)): + cartesian_frames.append(convert_quaternion_frame_to_cartesian_frame(skeleton, quat_frames[i])) + return cartesian_frames + + +def transform_invariant_point_cloud_distance_2d(a, b, weights=None): + ''' + + :param a: + :param b: + :param weights: + :return: + ''' + theta, offset_x, offset_z = align_point_clouds_2D(a, b, weights) + transformed_b = transform_point_cloud(b, theta, offset_x, offset_z) + dist = calculate_point_cloud_distance(a, transformed_b) + return dist + + +def align_point_clouds_2D(a, b, weights=None): + ''' + Finds aligning 2d transformation of two equally sized point clouds. + from Motion Graphs paper by Kovar et al. + Parameters + --------- + *a: list + \t point cloud + *b: list + \t point cloud + *weights: list + \t weights of correspondences + Returns + ------- + *theta: float + \t angle around y axis in radians + *offset_x: float + \t + *offset_z: float + + ''' + if len(a) != len(b): + raise ValueError("two point cloud should have the same number points: "+str(len(a))+","+str(len(b))) + n_points = len(a) + if weights is None: + weights = np.ones(n_points) + numerator_left = 0.0 + denominator_left = 0.0 + # if not weights: + # weight = 1.0/n_points # todo set weight base on joint level + weighted_sum_a_x = 0.0 + weighted_sum_b_x = 0.0 + weighted_sum_a_z = 0.0 + weighted_sum_b_z = 0.0 + sum_of_weights = 0.0 + for index in range(n_points): + numerator_left += weights[index] * (a[index][0] * b[index][2] - + b[index][0] * a[index][2]) + denominator_left += weights[index] * (a[index][0] * b[index][0] + + a[index][2] * b[index][2]) + sum_of_weights += weights[index] + weighted_sum_a_x += weights[index] * a[index][0] + weighted_sum_b_x += weights[index] * b[index][0] + weighted_sum_a_z += weights[index] * a[index][2] + weighted_sum_b_z += weights[index] * b[index][2] + numerator_right = 1.0 / sum_of_weights * \ + (weighted_sum_a_x * weighted_sum_b_z - + weighted_sum_b_x * weighted_sum_a_z) + numerator = numerator_left - numerator_right + denominator_right = 1.0 / sum_of_weights * \ + (weighted_sum_a_x * weighted_sum_b_x + + weighted_sum_a_z * weighted_sum_b_z) + denominator = denominator_left - denominator_right + theta = np.arctan2(numerator, denominator) + offset_x = (weighted_sum_a_x - weighted_sum_b_x * np.cos(theta) - weighted_sum_b_z * np.sin(theta)) / sum_of_weights + offset_z = (weighted_sum_a_z + weighted_sum_b_x * np.sin(theta) - weighted_sum_b_z * np.cos(theta)) / sum_of_weights + return theta, offset_x, offset_z + + +def convert_euler_frames_to_cartesian_frames(skeleton, euler_frames, animated_joints=None): + """ + converts to euler frames to cartesian frames + """ + cartesian_frames = [] + for euler_frame in euler_frames: + cartesian_frames.append( + convert_euler_frame_to_cartesian_frame(skeleton, euler_frame, animated_joints)) + return np.array(cartesian_frames) + + + +def transform_point_by_quaternion(point, quaternion, offset, origin=None): + """ + http://math.stackexchange.com/questions/40164/how-do-you-rotate-a-vector-by-a-unit-quaternion + :param point: + :param quaternion: + :return: + """ + if origin is not None: + origin = np.asarray(origin) + # print "point",point,origin + point = point - origin + else: + origin = [0,0,0] + homogenous_point = np.append([0], point) + tmp_q = quaternion_multiply(quaternion, homogenous_point) + temp_q = quaternion_multiply(tmp_q, quaternion_conjugate(quaternion)) + new_point = [temp_q[i+1] + offset[i] + origin[i] for i in range(3)] + return new_point + + +def transform_point_by_quaternion_faster(point, quaternion, offset, origin=None): + """ + http://blog.molecular-matters.com/2013/05/24/a-faster-quaternion-vector-multiplication/ + :param point: + :param quaternion: + :return: + """ + if origin is not None: + origin = np.asarray(origin) + # print "point",point,origin + point = point - origin + else: + origin = [0,0,0] + t = 2 * np.cross(quaternion[1:], point) + new_point = np.array(point) + quaternion[0] * t + np.cross(quaternion[1:], t) + new_point = [new_point[i] + offset[i] + origin[i] for i in range(3)] + return new_point + + +def transform_point_by_quaternion_faster2(point, quaternion, offset, origin=None): + """ + http://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion + :param point: + :param quaternion: + :return: + """ + if origin is not None: + origin = np.asarray(origin) + # print "point",point,origin + point = point - origin + else: + origin = [0,0,0] + u = quaternion[1:] + s = quaternion[0] + new_point = 2.0 * np.dot(u, point) * u + (s*s - np.dot(u, u)) * point + 2.0 * s * np.cross(u, point) + new_point = [new_point[i] + offset[i] + origin[i] for i in range(3)] + return new_point + + +def euler_angles_to_rotation_matrix(euler_angles, rotation_order=DEFAULT_ROTATION_ORDER): + # generate rotation matrix based on rotation order + assert len(euler_angles) == 3, ('The length of rotation angles should be 3') + if round(euler_angles[0], 3) == 0 and round(euler_angles[2], 3) == 0: + # rotation about y axis + R = rotation_matrix(np.deg2rad(euler_angles[1]), [0, 1, 0]) + else: + euler_angles = np.deg2rad(euler_angles) + R = euler_matrix(euler_angles[0], euler_angles[1], euler_angles[2], axes=rotation_order_to_string(rotation_order)) + return R + + +def create_transformation_matrix(translation, euler_angles, rotation_order=DEFAULT_ROTATION_ORDER): + #m = np.eye(4,4) + m = euler_angles_to_rotation_matrix(euler_angles, rotation_order) + m[:3,3] = translation + return m + + +def transform_point(point, euler_angles, offset, origin=None, rotation_order=DEFAULT_ROTATION_ORDER): + """ + rotate point around y axis and translate it by an offset + Parameters + --------- + *point: numpy.array + \t coordinates + *angles: list of floats + \tRotation angles in degrees + *offset: list of floats + \tTranslation + """ + assert len(point) == 3, ('the point should be a list of length 3') + # translate point to original point + point = np.asarray(point) + if origin is not None: + origin = np.asarray(origin) + point = point - origin + point = np.insert(point, len(point), 1) + R = euler_angles_to_rotation_matrix(euler_angles, rotation_order=rotation_order) + rotated_point = np.dot(R, point) + if origin is not None: + rotated_point[:3] += origin + #print rotated_point, + transformed_point = rotated_point[:3] + offset + return transformed_point + + +def smooth_quaternion_frames(quaternion_frames, discontinuity, window=20): + """ Smooth quaternion frames given discontinuity frame + + Parameters + ---------- + quaternion_frames: list + \tA list of quaternion frames + discontinuity : int + The frame where the discontinuity is. (e.g. the transitionframe) + window : (optional) int, default is 20 + The smoothing window + Returns + ------- + None. + """ + n_joints = (len(quaternion_frames[0]) - 3) / 4 + # smooth quaternion + n_frames = len(quaternion_frames) + for i in range(n_joints): + for j in range(n_frames - 1): + q1 = np.array(quaternion_frames[j][3 + i * 4: 3 + (i + 1) * 4]) + q2 = np.array(quaternion_frames[j + 1][3 + i * 4:3 + (i + 1) * 4]) + if np.dot(q1, q2) < 0: + quaternion_frames[ + j + 1][3 + i * 4:3 + (i + 1) * 4] = -quaternion_frames[j + 1][3 + i * 4:3 + (i + 1) * 4] + # generate curve of smoothing factors + d = float(discontinuity) + w = float(window) + smoothing_factors = [] + for f in range(n_frames): + value = 0.0 + if d - w <= f < d: + tmp = (f - d + w) / w + value = 0.5 * tmp ** 2 + elif d <= f <= d + w: + tmp = (f - d + w) / w + value = -0.5 * tmp ** 2 + 2 * tmp - 2 + smoothing_factors.append(value) + smoothing_factors = np.array(smoothing_factors) + new_quaternion_frames = [] + for i in range(len(quaternion_frames[0])): + current_value = quaternion_frames[:, i] + magnitude = current_value[int(d)] - current_value[int(d) - 1] + new_value = current_value + (magnitude * smoothing_factors) + new_quaternion_frames.append(new_value) + new_quaternion_frames = np.array(new_quaternion_frames).T + return new_quaternion_frames + + +def smooth_quaternion_frames_partially(quaternion_frames, joint_parameter_indices, discontinuity, window=20): + """ Smooth quaternion frames given discontinuity frame + + Parameters + ---------- + quaternion_frames: list + \tA list of quaternion frames + parameters_indices: list + \tThe list of joint parameter indices that should be smoothed + discontinuity : int + The frame where the discontinuity is. (e.g. the transitionframe) + window : (optional) int, default is 20 + The smoothing window + Returns + ------- + None. + """ + n_joints = (len(quaternion_frames[0]) - 3) / 4 + # smooth quaternion + n_frames = len(quaternion_frames) + for i in range(n_joints): + for j in range(n_frames - 1): + q1 = np.array(quaternion_frames[j][3 + i * 4: 3 + (i + 1) * 4]) + q2 = np.array(quaternion_frames[j + 1][3 + i * 4:3 + (i + 1) * 4]) + if np.dot(q1, q2) < 0: + quaternion_frames[ + j + 1][3 + i * 4:3 + (i + 1) * 4] = -quaternion_frames[j + 1][3 + i * 4:3 + (i + 1) * 4] + # generate curve of smoothing factors + transition_frame = float(discontinuity) + window_size = float(window) + smoothing_factors = [] + for frame_number in range(n_frames): + value = 0.0 + if transition_frame - window_size <= frame_number < transition_frame: + tmp = (frame_number - transition_frame + window_size) / window_size + value = 0.5 * tmp ** 2 + elif transition_frame <= frame_number <= transition_frame + window_size: + tmp = (frame_number - transition_frame + window_size) / window_size + value = -0.5 * tmp ** 2 + 2 * tmp - 2 + smoothing_factors.append(value) + #smoothing_factors = np.array(smoothing_factors) + transition_frame = int(transition_frame) + #new_quaternion_frames = [] + magnitude_vector = np.array(quaternion_frames[transition_frame]) - quaternion_frames[transition_frame-1] + #print "magnitude ", magnitude_vector + for i in joint_parameter_indices: + joint_parameter_index = i*4+3 + for frame_index in range(n_frames): + quaternion_frames[frame_index][joint_parameter_index] = quaternion_frames[frame_index][joint_parameter_index]+magnitude_vector[joint_parameter_index] * smoothing_factors[frame_index] + + + + +def get_rotation_angle(point1, point2): + """ + estimate the rotation angle from point2 to point1 + point1, point2 are normalized points + rotate point2 to be the same as point1 + + Parameters + ---------- + *point1, point2: list or numpy array + \tUnit 2d points + + Return + ------ + *rotation_angle: float (in degree) + \tRotation angle from point2 to point1 + """ + theta1 = point_to_euler_angle(point1) + theta2 = point_to_euler_angle(point2) + rotation_angle = euler_substraction(theta2, theta1) + return rotation_angle + + +def point_to_euler_angle(vec): + ''' + @brief: covert a 2D point vec = (cos, sin) to euler angle (in degree) + The output range is [-180, 180] + ''' + vec = np.array(vec) + theta = np.rad2deg(np.arctan2(vec[1], vec[0])) + return theta + + +def calculate_point_cloud_distance(a, b): + """ + calculates the distance between two point clouds with equal length and + corresponding distances + """ + assert len(a) == len(b) + distance = 0 + n_points = len(a) + for i in range(n_points): + d = [a[i][0] - b[i][0], a[i][1] - b[i][1], a[i][2] - b[i][2]] + distance += sqrt(d[0] ** 2 + d[1] ** 2 + d[2] ** 2) + return distance / n_points + + +def rotate_and_translate_point(p, theta, offset_x, offset_z): + """rotate and translate a 3d point, first rotation them translation + theta is in radians + """ + rotation_angles = [0, theta, 0] + rotation_angles = np.rad2deg(rotation_angles) + offset = [offset_x, 0, offset_z] + transformed_point = transform_point(p, rotation_angles, offset) + return transformed_point + + +def transform_point_cloud(point_cloud, theta, offset_x, offset_z): + """ + transforms points in a point cloud by a rotation around y and a translation + along x and z + """ + transformed_point_cloud = [] + for p in point_cloud: + if p is not None: + transformed_point_cloud.append( + rotate_and_translate_point(p, theta, offset_x, offset_z)) + return transformed_point_cloud + + +def calculate_pose_distance(skeleton, euler_frames_a, euler_frames_b): + ''' Converts euler frames to point clouds and finds the aligning transformation + and calculates the distance after the aligning transformation + ''' + + # theta, offset_x, offset_z = find_aligning_transformation(bvh_reader, euler_frames_a, euler_frames_b, node_name_map) + point_cloud_a = convert_euler_frame_to_cartesian_frame( + skeleton, euler_frames_a[-1]) + point_cloud_b = convert_euler_frame_to_cartesian_frame( + skeleton, euler_frames_b[0]) + + weights = skeleton.joint_weights + theta, offset_x, offset_z = align_point_clouds_2D( + point_cloud_a, point_cloud_b, weights) + t_point_cloud_b = transform_point_cloud( + point_cloud_b, theta, offset_x, offset_z) + error = calculate_point_cloud_distance(point_cloud_a, t_point_cloud_b) + return error + + +def calculate_frame_distance(skeleton, + euler_frame_a, + euler_frame_b, + weights=None, + return_transform=False): + point_cloud_a = convert_euler_frame_to_cartesian_frame(skeleton, + euler_frame_a) + point_cloud_b = convert_euler_frame_to_cartesian_frame(skeleton, + euler_frame_b) + theta, offset_x, offset_z = align_point_clouds_2D(point_cloud_a, point_cloud_b, weights=weights) + t_point_cloud_b = transform_point_cloud(point_cloud_b, theta, offset_x, offset_z) + error = calculate_point_cloud_distance(point_cloud_a, t_point_cloud_b) + if return_transform: + return error, theta, offset_x, offset_z + else: + return error + + +def quat_distance(quat_a, quat_b): + # normalize quaternion vector first + quat_a = np.asarray(quat_a) + quat_b = np.asarray(quat_b) + quat_a /= np.linalg.norm(quat_a) + quat_b /= np.linalg.norm(quat_b) + rotmat_a = quaternion_matrix(quat_a) + rotmat_b = quaternion_matrix(quat_b) + diff_mat = rotmat_a - rotmat_b + tmp = np.ravel(diff_mat) + diff = np.linalg.norm(tmp) + return diff + + +def calculate_weighted_frame_distance_quat(quat_frame_a, + quat_frame_b, + weights): + assert len(quat_frame_a) == len(quat_frame_b) and \ + len(quat_frame_a) == (len(weights) - 2) * LEN_QUAT + LEN_ROOT + diff = 0 + for i in range(len(weights) - 2): + quat1 = quat_frame_a[(i+1)*LEN_QUAT+LEN_ROOT_POS: (i+2)*LEN_QUAT+LEN_ROOT_POS] + quat2 = quat_frame_b[(i+1)*LEN_QUAT+LEN_ROOT_POS: (i+2)*LEN_QUAT+LEN_ROOT_POS] + tmp = quat_distance(quat1, quat2)*weights[i] + diff += tmp + return diff + + + +def extract_root_positions(frames): + """ + + :param frames: a list of euler or quaternion frames + :return roots_2D: a list of root position in 2D space + """ + roots_2D = [] + for i in range(len(frames)): + position_2D = [frames[i][0], frames[i][2]] + roots_2D.append(position_2D) + + return roots_2D + + +def pose_orientation_quat(quaternion_frame, param_range=(3,7), ref_offset=[0, 0, 1, 1]): + """Estimate pose orientation from root orientation + """ + ref_offset = np.array(ref_offset) + q = quaternion_frame[param_range[0]:param_range[1]] + rotmat = quaternion_matrix(q) + rotated_point = np.dot(rotmat, ref_offset) + dir_vec = np.array([rotated_point[0], rotated_point[2]]) + dir_vec /= np.linalg.norm(dir_vec) + return dir_vec + +def pose_orientation_euler(euler_frame, ref_offset = np.array([0, 0, 1, 1])): + ''' + only works for rocketbox skeleton + :param euler_frame: + :return: + ''' + rot_angles = euler_frame[3:6] + rot_angles_rad = np.deg2rad(rot_angles) + rotmat = euler_matrix(*rot_angles_rad, 'rxyz') + rotated_point = np.dot(rotmat, ref_offset) + dir_vec = np.array([rotated_point[0],0, rotated_point[2]]) + dir_vec /= np.linalg.norm(dir_vec) + return dir_vec + + +def pose_orientation_general(euler_frame, joints, skeleton, rotation_order=['Xrotation', 'Yrotation', 'Zrotation']): + ''' + estimate pose heading direction, the position of joints define a body plane, the projection of the normal of + the body plane is the heading direction + :param euler_frame: + :param joints: + :return: + ''' + points = [] + for joint in joints: + points.append(skeleton.nodes[joint].get_global_position_from_euler(euler_frame, rotation_order)) + points = np.asarray(points) + body_plane = BodyPlane(points) + normal_vec = body_plane.normal_vector + dir_vec = np.array([normal_vec[0], normal_vec[2]]) + return dir_vec/np.linalg.norm(dir_vec) + + +def pose_orientation_from_point_cloud(point_cloud, body_plane_joint_indices): + assert len(point_cloud.shape) == 2 + points = [] + for index in body_plane_joint_indices: + points.append(point_cloud[index]) + points = np.asarray(points) + body_plane = BodyPlane(points) + normal_vec = body_plane.normal_vector + dir_vec = np.array([normal_vec[0], normal_vec[2]]) + return dir_vec/np.linalg.norm(dir_vec) + + +def get_trajectory_dir_from_2d_points(points): + """Estimate the trajectory heading + + Parameters + *\Points: numpy array + Step 1: fit the points with a 2d straight line + Step 2: estimate the direction vector from first and last point + """ + points = np.asarray(points) + dir_vector = points[-1] - points[0] + slope, intercept, r_value, p_value, std_err = linregress(*points.T) + if isnan(slope): + orientation_vec1 = np.array([0, 1]) + orientation_vec2 = np.array([0, -1]) + else: + orientation_vec1 = np.array([slope, 1]) + orientation_vec2 = np.array([-slope, -1]) + if np.dot( + orientation_vec1, + dir_vector) > np.dot( + orientation_vec2, + dir_vector): + orientation_vec = orientation_vec1 + else: + orientation_vec = orientation_vec2 + orientation_vec = orientation_vec / np.linalg.norm(orientation_vec) + return orientation_vec + + +def get_dir_from_2d_points(points): + points = np.asarray(points) + dir = points[-1] - points[2] + dir = dir/np.linalg.norm(dir) + return dir + +def pose_up_vector_euler(euler_frame): + """ + Estimate up vector of given euler frame. Assume the pose is aligned in negative Z axis, so the projection of + 3D up vector into XOY plane (Y is vertical axis) is taken into consideration. + :param euler_frame: + :return: + """ + ref_offset = np.array([1, 0, 0, 1]) + rot_angles = euler_frame[3:6] + rot_angles_rad = np.deg2rad(rot_angles) + rotmat = euler_matrix(rot_angles_rad[0], + rot_angles_rad[1], + rot_angles_rad[2], + 'rxyz') + rotated_point = np.dot(rotmat, ref_offset) + up_vec = np.array([rotated_point[0], rotated_point[1]]) + up_vec /= np.linalg.norm(up_vec) + return up_vec + + + +def quaternion_rotate_vector(q, vector): + """ src: http://math.stackexchange.com/questions/40164/how-do-you-rotate-a-vector-by-a-unit-quaternion + Args: + q: + vector: + + Returns: + + """ + tmp_result = quaternion_multiply(q, vector) + return quaternion_multiply(tmp_result, quaternion_conjugate(q))[1:] + + +def point_rotation_by_quaternion(point, q): + """ + Rotate a 3D point by quaternion + :param point: [x, y, z] + :param q: [qw, qx, qy, qz] + :return rotated_point: [x, y, ] + """ + r = [0, point[0], point[1], point[2]] + q_conj = [q[0], -1*q[1], -1*q[2], -1*q[3]] + return quaternion_multiply(quaternion_multiply(q, r), q_conj)[1:] + + +def get_lookAt_direction(bvh_analyzer, frame_index): + ref_vec = np.array([0, 0, 1, 0]) + global_trans = bvh_analyzer.get_global_transform("Head", frame_index) + dir_vec = np.dot(global_trans, ref_vec) + return np.asarray([dir_vec[0], dir_vec[2]]) + + +def find_rotation_angle_between_two_vector(v1, v2): + from scipy.optimize import minimize + ''' + positive angle means counterclockwise, negative angle means clockwise + :return: + ''' + def objective_function(theta, data): + src_dir, target_dir = data + rotmat = np.zeros((2, 2)) + rotmat[0, 0] = np.cos(theta) + rotmat[0, 1] = -np.sin(theta) + rotmat[1, 0] = np.sin(theta) + rotmat[1, 1] = np.cos(theta) + rotated_src_dir = np.dot(rotmat, src_dir) + return np.linalg.norm(rotated_src_dir - target_dir) + + initial_guess = 0 + params = [v1, v2] + res = minimize(objective_function, initial_guess, args=params, method='L-BFGS-B') + return np.rad2deg(res.x[0]) + + +def get_rotation_angles_for_vectors(dir_vecs, ref_dir, up_axis): + ''' + + :param dir_vecs: nd array N*3 + :param ref_dir: 3d array + :param up_axis: 3d array + :return: + ''' + axis_indice = np.where(up_axis == 0) + angles = [] + for i in range(len(dir_vecs)): + angles.append(get_rotation_angle(dir_vecs[i][axis_indice], ref_dir[axis_indice])) + return np.deg2rad(np.asarray(angles)) + + +def smooth_quat_frames(quat_frames): + smoothed_quat_frames = OrderedDict() + filenames = list(quat_frames.keys()) + smoothed_quat_frames_data = np.asarray(deepcopy(list(quat_frames.values()))) + print(('quaternion frame data shape: ', smoothed_quat_frames_data.shape)) + assert len(smoothed_quat_frames_data.shape) == 3, ('The shape of quaternion frames is not correct!') + n_samples, n_frames, n_dims = smoothed_quat_frames_data.shape + n_joints = int((n_dims - 3) / 4) + for i in range(n_joints): + ref_quat = smoothed_quat_frames_data[0, 0, + i * LEN_QUAT + LEN_ROOT: (i + 1) * LEN_QUAT + LEN_ROOT] + for j in range(1, n_samples): + test_quat = smoothed_quat_frames_data[j, 0, + i * LEN_QUAT + LEN_ROOT: (i + 1) * LEN_QUAT + LEN_ROOT] + if not areQuatClose(ref_quat, test_quat): + smoothed_quat_frames_data[j, :, + i * LEN_QUAT + LEN_ROOT: (i + 1) * LEN_QUAT + LEN_ROOT] *= -1 + for i in range(len(filenames)): + smoothed_quat_frames[filenames[i]] = smoothed_quat_frames_data[i] + return smoothed_quat_frames + + +def combine_motion_clips(clips, motion_len, window_step): + ''' + + :param clips: + :param motion_len: + :param window_step: + :return: + ''' + clips = np.asarray(clips) + n_clips, window_size, n_dims = clips.shape + + ## case 1: motion length is smaller than window_step + if motion_len <= window_step: + assert n_clips == 1 + left_index = (window_size - motion_len) // 2 + (window_size - motion_len) % 2 + right_index = window_size - (window_size - motion_len) // 2 + return clips[0][left_index: right_index] + + ## case 2: motion length is larger than window_step and smaller than window + if motion_len > window_step and motion_len <= window_size: + assert n_clips == 2 + left_index = (window_size - motion_len) // 2 + (window_size - motion_len) % 2 + right_index = window_size - (window_size - motion_len) // 2 + return clips[0][left_index: right_index] + + residue_frames = motion_len % window_step + print('residue_frames: ', residue_frames) + ## case 3: residue frames is smaller than window step + if residue_frames <= window_step: + residue_frames += window_step + combined_frames = np.concatenate(clips[0:n_clips - 2, :window_step], axis=0) + left_index = (window_size - residue_frames) // 2 + (window_size - residue_frames) % 2 + right_index = window_size - (window_size - residue_frames) // 2 + combined_frames = np.concatenate((combined_frames, clips[-2, left_index:right_index]), axis=0) + return combined_frames diff --git a/build/lib/anim_utils/motion_editing/__init__.py b/build/lib/anim_utils/motion_editing/__init__.py new file mode 100644 index 0000000..88e715b --- /dev/null +++ b/build/lib/anim_utils/motion_editing/__init__.py @@ -0,0 +1,4 @@ +from .motion_editing import MotionEditing +from .motion_grounding import MotionGrounding, add_heels_to_skeleton +from .footplant_constraint_generator import FootplantConstraintGenerator +from .utils import get_average_joint_position, get_average_joint_direction diff --git a/build/lib/anim_utils/motion_editing/analytical_inverse_kinematics.py b/build/lib/anim_utils/motion_editing/analytical_inverse_kinematics.py new file mode 100644 index 0000000..e450eb7 --- /dev/null +++ b/build/lib/anim_utils/motion_editing/analytical_inverse_kinematics.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +""" Analytical IK for arms and legs based on Section 5.3 of [1] and Section 4.4 of [2] + +[1] Lee, Jehee, and Sung Yong Shin. "A hierarchical approach to interactive motion editing for human-like figures." +Proceedings of the 26th annual conference on Computer graphics and interactive techniques. 1999. +[2] Lucas Kovar, John Schreiner, and Michael Gleicher. "Footskate cleanup for motion capture editing." Proceedings of the 2002 ACM SIGGRAPH/Eurographics symposium on Computer animation. ACM, 2002. + +""" +import math +import numpy as np +import scipy.integrate as integrate +from transformations import quaternion_multiply, quaternion_about_axis, quaternion_matrix, quaternion_from_matrix, quaternion_inverse + + +def normalize(v): + return v/np.linalg.norm(v) + + +def quaternion_from_axis_angle(axis, angle): + q = [1,0,0,0] + q[1] = axis[0] * math.sin(angle / 2) + q[2] = axis[1] * math.sin(angle / 2) + q[3] = axis[2] * math.sin(angle / 2) + q[0] = math.cos(angle / 2) + return normalize(q) + + +def find_rotation_between_vectors(a, b): + """http://math.stackexchange.com/questions/293116/rotating-one-3d-vector-to-another""" + if np.array_equiv(a, b): + return [1, 0, 0, 0] + + axis = normalize(np.cross(a, b)) + dot = np.dot(a, b) + if dot >= 1.0: + return [1, 0, 0, 0] + angle = math.acos(dot) + q = quaternion_from_axis_angle(axis, angle) + return q + +def calculate_angle(upper_limb, lower_limb, ru, rl, target_length): + upper_limb_sq = upper_limb * upper_limb + lower_limb_sq = lower_limb * lower_limb + ru_sq = ru * ru + rl_sq = rl * rl + lusq_rusq = upper_limb_sq - ru_sq + lusq_rusq = max(0, lusq_rusq) + llsq_rlsq = lower_limb_sq - rl_sq + llsq_rlsq = max(0, llsq_rlsq) + temp = upper_limb_sq + rl_sq + temp += 2 * math.sqrt(lusq_rusq) * math.sqrt(llsq_rlsq) + temp += - (target_length*target_length) + temp /= 2 * ru * rl + print("temp",temp) + temp = max(-1, temp) + return math.acos(temp) + + +def calculate_angle2(upper_limb,lower_limb,target_length): + """ get angle between upper and lower limb based on desired length + https://www.mathsisfun.com/algebra/trig-solving-sss-triangles.html""" + a = upper_limb + b = lower_limb + c = target_length + temp = (a*a + b*b - c*c) / (2 * a * b) + temp = min(1, temp) + temp = max(-1, temp) + angle = math.acos(temp) + return angle + + +def damp_angle(orig_angle, target_angle, p=0.1*np.pi, a=0.01): + """ src: Kovar et al. [2] Section 4.4. eq. 10 and 11""" + def func(x): + if x < p: + return 1.0 + elif p <= x <= np.pi: + return a * ((x-p)/(np.pi-p)) + else: + return 0.0 + res = integrate.quad(func, orig_angle, target_angle) + return orig_angle + res[0] + + +def to_local_coordinate_system(skeleton, frame, joint_name, q): + """ given a global rotation concatenate it with an existing local rotation and bring it to the local coordinate system""" + # delta*parent*old_local = parent*new_local + # inv_parent*delta*parent*old_local = new_local + if skeleton.nodes[joint_name].parent is not None: + global_m = quaternion_matrix(q) + parent_joint = skeleton.nodes[joint_name].parent.node_name + parent_m = skeleton.nodes[parent_joint].get_global_matrix(frame, use_cache=False) + old_global = np.dot(parent_m, skeleton.nodes[joint_name].get_local_matrix(frame)) + new_global = np.dot(global_m, old_global) + new_local = np.dot(np.linalg.inv(parent_m), new_global) + return quaternion_from_matrix(new_local) + else: + return q + + + + +def calculate_limb_joint_rotation(skeleton, root, joint, end_effector, local_joint_axis, frame, target_position): + """ find angle so the distance from root to end effector is equal to the distance from the root to the target""" + root_pos = skeleton.nodes[root].get_global_position(frame) + joint_pos = skeleton.nodes[joint].get_global_position(frame) + end_effector_pos = skeleton.nodes[end_effector].get_global_position(frame) + + upper_limb = np.linalg.norm(root_pos - joint_pos) + lower_limb = np.linalg.norm(joint_pos - end_effector_pos) + target_length = np.linalg.norm(root_pos - target_position) + #current_length = np.linalg.norm(root_pos - end_effector_pos) + #current_angle = calculate_angle2(upper_limb, lower_limb, current_length) + target_angle = calculate_angle2(upper_limb, lower_limb, target_length) + + joint_delta_angle = np.pi - target_angle + joint_delta_q = quaternion_about_axis(joint_delta_angle, local_joint_axis) + joint_delta_q = normalize(joint_delta_q) + return joint_delta_q + + +def calculate_limb_root_rotation(skeleton, root, end_effector, frame, target_position): + """ find angle between the vectors end_effector - root and target- root """ + + # align vectors + root_pos = skeleton.nodes[root].get_global_position(frame) + end_effector_pos = skeleton.nodes[end_effector].get_global_position(frame) + src_delta = end_effector_pos - root_pos + src_dir = src_delta / np.linalg.norm(src_delta) + + target_delta = target_position - root_pos + target_dir = target_delta / np.linalg.norm(target_delta) + + root_delta_q = find_rotation_between_vectors(src_dir, target_dir) + root_delta_q = normalize(root_delta_q) + + return to_local_coordinate_system(skeleton, frame, root, root_delta_q) + + +class AnalyticalLimbIK(object): + def __init__(self, skeleton, limb_root, limb_joint, end_effector, joint_axis, local_end_effector_dir, damp_angle=None, damp_factor=0.01): + self.skeleton = skeleton + self.limb_root = limb_root + self.limb_joint = limb_joint + self.end_effector = end_effector + self.local_joint_axis = joint_axis + self.local_end_effector_dir = local_end_effector_dir + joint_idx = self.skeleton.animated_joints.index(self.limb_joint) * 4 + 3 + self.joint_indices = [joint_idx, joint_idx + 1, joint_idx + 2, joint_idx + 3] + root_idx = self.skeleton.animated_joints.index(self.limb_root) * 4 + 3 + self.root_indices = [root_idx, root_idx + 1, root_idx + 2, root_idx + 3] + end_effector_idx = self.skeleton.animated_joints.index(self.end_effector) * 4 + 3 + self.end_effector_indices = [end_effector_idx, end_effector_idx + 1, end_effector_idx + 2, end_effector_idx + 3] + self.damp_angle = damp_angle + self.damp_factor = damp_factor + + @classmethod + def init_from_dict(cls, skeleton, joint_name, data, damp_angle=None, damp_factor=None): + limb_root = data["root"] + limb_joint = data["joint"] + joint_axis = data["joint_axis"] + end_effector_dir = data["end_effector_dir"] + return AnalyticalLimbIK(skeleton, limb_root, limb_joint, joint_name, joint_axis, end_effector_dir, damp_angle, damp_factor) + + def calculate_limb_joint_rotation(self, frame, target_position): + """ find angle so the distance from root to end effector is equal to the distance from the root to the target""" + root_pos = self.skeleton.nodes[self.limb_root].get_global_position(frame) + joint_pos = self.skeleton.nodes[self.limb_joint].get_global_position(frame) + end_effector_pos = self.skeleton.nodes[self.end_effector].get_global_position(frame) + + upper_limb = np.linalg.norm(root_pos - joint_pos) + lower_limb = np.linalg.norm(joint_pos - end_effector_pos) + target_length = np.linalg.norm(root_pos - target_position) + current_length = np.linalg.norm(root_pos - end_effector_pos) + current_angle = calculate_angle2(upper_limb, lower_limb, current_length) + target_angle = calculate_angle2(upper_limb, lower_limb, target_length) + if self.damp_angle is not None: + target_angle = damp_angle(current_angle, target_angle, self.damp_angle, self.damp_factor) + #if abs(target_angle - np.pi) < self.min_angle: + # target_angle -= self.min_angle + joint_delta_angle = np.pi - target_angle + joint_delta_q = quaternion_about_axis(joint_delta_angle, self.local_joint_axis) + joint_delta_q = normalize(joint_delta_q) + frame[self.joint_indices] = joint_delta_q + return joint_delta_q + + def calculate_limb_root_rotation(self, frame, target_position): + """ find angle between the vectors end_effector - root and target- root """ + + # align vectors + root_pos = self.skeleton.nodes[self.limb_root].get_global_position(frame) + end_effector_pos = self.skeleton.nodes[self.end_effector].get_global_position(frame) + src_delta = end_effector_pos - root_pos + src_dir = src_delta / np.linalg.norm(src_delta) + + target_delta = target_position - root_pos + target_dir = target_delta / np.linalg.norm(target_delta) + + root_delta_q = find_rotation_between_vectors(src_dir, target_dir) + root_delta_q = normalize(root_delta_q) + + frame[self.root_indices] = self._to_local_coordinate_system(frame, self.limb_root, root_delta_q) + + def _to_local_coordinate_system(self, frame, joint_name, q): + """ given a global rotation concatenate it with an existing local rotation and bring it to the local coordinate system""" + # delta*parent*old_local = parent*new_local + # inv_parent*delta*parent*old_local = new_local + global_m = quaternion_matrix(q) + parent_joint = self.skeleton.nodes[joint_name].parent.node_name + parent_m = self.skeleton.nodes[parent_joint].get_global_matrix(frame, use_cache=False) + old_global = np.dot(parent_m, self.skeleton.nodes[joint_name].get_local_matrix(frame)) + new_global = np.dot(global_m, old_global) + new_local = np.dot(np.linalg.inv(parent_m), new_global) + return quaternion_from_matrix(new_local) + + def calculate_end_effector_rotation(self, frame, target_dir): + src_dir = self.get_joint_dir(frame, self.end_effector) + global_delta_q = find_rotation_between_vectors(src_dir, target_dir) + new_local_q = self._to_local_coordinate_system(frame, self.end_effector, global_delta_q) + frame[self.end_effector_indices] = new_local_q + + def set_end_effector_rotation(self, frame, target_orientation): + #print "set orientation", target_orientation + q = self.get_global_joint_orientation(self.end_effector, frame) + delta_orientation = quaternion_multiply(target_orientation, quaternion_inverse(q)) + new_local_q = self._to_local_coordinate_system(frame, self.end_effector, delta_orientation) + frame[self.end_effector_indices] = new_local_q + + def get_global_joint_orientation(self, joint_name, frame): + m = self.skeleton.nodes[joint_name].get_global_matrix(frame) + m[:3, 3] = [0, 0, 0] + return normalize(quaternion_from_matrix(m)) + + def get_joint_dir(self, frame, joint_name): + pos1 = self.skeleton.nodes[joint_name].get_global_position(frame) + pos2 = self.skeleton.nodes[joint_name].children[0].get_global_position(frame) + return normalize(pos2 - pos1) + + + def apply(self, frame, position, orientation): + if position is not None: + # 1 calculate joint angle based on the distance to target position + self.calculate_limb_joint_rotation(frame, position) + + # 2 calculate limb root rotation to align the end effector with the target position + self.calculate_limb_root_rotation(frame, position) + + # 3 orient end effector + if orientation is not None: + self.set_end_effector_rotation2(frame, orientation) + return frame + + + def set_end_effector_rotation2(self, frame, target_orientation): + #print("set", target_orientation) + new_local_q = self.to_local_cos2(self.end_effector, frame, target_orientation) + frame[self.end_effector_indices] = new_local_q + + def to_local_cos2(self, joint_name, frame, q): + # bring into parent coordinate system + parent_joint = self.skeleton.nodes[joint_name].parent.node_name + pm = self.skeleton.nodes[parent_joint].get_global_matrix(frame)#[:3, :3] + inv_p = quaternion_inverse(quaternion_from_matrix(pm)) + normalize(inv_p) + return quaternion_multiply(inv_p, q) diff --git a/build/lib/anim_utils/motion_editing/coordinate_cyclic_descent.py b/build/lib/anim_utils/motion_editing/coordinate_cyclic_descent.py new file mode 100644 index 0000000..da675ed --- /dev/null +++ b/build/lib/anim_utils/motion_editing/coordinate_cyclic_descent.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import numpy as np +from transformations import quaternion_from_matrix, quaternion_matrix, quaternion_slerp, quaternion_about_axis, quaternion_multiply, quaternion_inverse +from ..animation_data.joint_constraints import quaternion_to_axis_angle +from math import acos +import math +from ..animation_data.utils import get_rotation_angle + + +def normalize(v): + return v / np.linalg.norm(v) + + +def to_local_coordinate_system(skeleton, frame, joint_name, q, use_cache=True): + """ given a global rotation bring it to the local coordinate system""" + if skeleton.nodes[joint_name].parent is not None: + global_m = quaternion_matrix(q) + parent_joint = skeleton.nodes[joint_name].parent.node_name + + # delete global matrix of parent_joint but use cache to save time + skeleton.nodes[parent_joint].cached_global_matrix = None + parent_m = skeleton.nodes[parent_joint].get_global_matrix(frame, use_cache=use_cache) + + new_local = np.dot(np.linalg.inv(parent_m), global_m) + return quaternion_from_matrix(new_local) + else: + return q + + +def quaternion_from_vector_to_vector(a, b): + """src: http://stackoverflow.com/questions/1171849/finding-quaternion-representing-the-rotation-from-one-vector-to-another + http://wiki.ogre3d.org/Quaternion+and+Rotation+Primer""" + + v = np.cross(a, b) + w = np.sqrt((np.linalg.norm(a) ** 2) * (np.linalg.norm(b) ** 2)) + np.dot(a, b) + q = np.array([w, v[0], v[1], v[2]]) + if np.dot(q,q) != 0: + return q/ np.linalg.norm(q) + else: + idx = np.nonzero(a)[0] + q = np.array([0, 0, 0, 0]) + q[1 + ((idx + 1) % 2)] = 1 # [0, 0, 1, 0] for a rotation of 180 around y axis + return q + + +def orient_end_effector_to_target(skeleton, root, end_effector, frame, constraint): + """ find angle between the vectors end_effector - root and target- root """ + + # align vectors + root_pos = skeleton.nodes[root].get_global_position(frame) + if constraint.offset is not None: + m = skeleton.nodes[end_effector].get_global_matrix(frame) + end_effector_pos = np.dot(m, constraint.offset)[:3] + else: + end_effector_pos = skeleton.nodes[end_effector].get_global_position(frame) + + src_delta = end_effector_pos - root_pos + src_dir = src_delta / np.linalg.norm(src_delta) + + target_delta = constraint.position - root_pos + target_dir = target_delta / np.linalg.norm(target_delta) + + root_delta_q = quaternion_from_vector_to_vector(src_dir, target_dir) + root_delta_q = normalize(root_delta_q) + + if skeleton.nodes[root].stiffness > 0: + t = 1 - skeleton.nodes[root].stiffness + root_delta_q = quaternion_slerp([1,0,0,0], root_delta_q, t) + root_delta_q = normalize(root_delta_q) + + global_m = quaternion_matrix(root_delta_q) + parent_joint = skeleton.nodes[root].parent.node_name + parent_m = skeleton.nodes[parent_joint].get_global_matrix(frame, use_cache=True) + old_global = np.dot(parent_m, skeleton.nodes[root].get_local_matrix(frame)) + new_global = np.dot(global_m, old_global) + q = quaternion_from_matrix(new_global) + return normalize(q) + + +def orient_node_to_target(skeleton,frame,node_name, end_effector, constraint): + #o = skeleton.animated_joints.index(node_name) * 4 + 3 + o = skeleton.nodes[node_name].quaternion_frame_index * 4 + 3 + q = orient_end_effector_to_target(skeleton, node_name, end_effector, frame, constraint) + q = to_local_coordinate_system(skeleton, frame, node_name, q) + frame[o:o + 4] = q + return frame + + +def apply_joint_constraint(skeleton,frame,node_name): + #o = skeleton.animated_joints.index(node_name) * 4 + 3 + o = skeleton.nodes[node_name].quaternion_frame_index * 4 + 3 + q = np.array(frame[o:o + 4]) + q = skeleton.nodes[node_name].joint_constraint.apply(q) + frame[o:o + 4] = q + return frame + +def set_global_orientation(skeleton, frame, node_name, orientation): + """ set orientation of parent of node assuming node is an end point""" + parent_node = skeleton.nodes[node_name].parent + if parent_node is not None and parent_node.parent is not None: + parent_name = parent_node.node_name + m = quaternion_matrix(orientation) + #o = skeleton.animated_joints.index(node_name) * 4 + 3 + o = skeleton.nodes[parent_name].quaternion_frame_index * 4 + 3 + parent_m = skeleton.nodes[parent_name].parent.get_global_matrix(frame, use_cache=True) + local_m = np.dot(np.linalg.inv(parent_m), m) + q = quaternion_from_matrix(local_m) + frame[o:o + 4] = normalize(q) + return frame + +AXES = ["x", "y"] +def set_tool_orientation(skeleton, frame, node_name, constraint): + """ set orientation of parent of node based on coordinate system assuming node is an end point""" + parent_node = skeleton.nodes[node_name].parent + parent_name = parent_node.node_name + o = parent_node.quaternion_frame_index * 4 + 3 + for a in AXES: + # delete global matrix of parent_joint but use cache to save time + parent_node.cached_global_matrix = None + global_m = parent_node.get_global_matrix(frame, use_cache=True) + #add the offset of the end effector + global_m[:3, 3] += skeleton.nodes[node_name].offset + if a in constraint.src_tool_cos and a in constraint.dest_tool_cos: + tool_axis_offset = constraint.src_tool_cos[a] + global_tool_target_axis = constraint.dest_tool_cos[a] + src_axis = np.dot(global_m, tool_axis_offset)[:3] + delta_q = quaternion_from_vector_to_vector(src_axis, global_tool_target_axis) + delta_q = normalize(delta_q) + delta_m = quaternion_matrix(delta_q) + global_m = np.dot(delta_m, global_m) + q = quaternion_from_matrix(global_m) + q = to_local_coordinate_system(skeleton, frame, parent_name, q) + frame[o:o + 4] = normalize(q) + return frame + +def run_ccd(skeleton, frame, end_effector_name, constraint, eps=0.01, n_max_iter=50, chain_end_joint=None, verbose=False): + pos = skeleton.nodes[end_effector_name].get_global_position(frame) + error = np.linalg.norm(constraint.position-pos) + prev_error = error + n_iters = 0 + while error > eps and n_iters < n_max_iter: + node = skeleton.nodes[end_effector_name].parent + depth = 0 + + while node is not None and node.node_name != chain_end_joint and node.parent is not None: + static = False + if node.joint_constraint is not None and node.joint_constraint.is_static: + static = True + + if not static: + frame = orient_node_to_target(skeleton,frame, node.node_name, end_effector_name, constraint) + if constraint.orientation is not None: + frame = set_global_orientation(skeleton, frame, end_effector_name, constraint.orientation) + elif constraint.src_tool_cos is not None and constraint.dest_tool_cos is not None: + frame = set_tool_orientation(skeleton, frame, end_effector_name, constraint) + + if node.joint_constraint is not None: + frame = apply_joint_constraint(skeleton, frame, node.node_name) + node = node.parent + depth += 1 + + if constraint.offset is not None: + m = skeleton.nodes[end_effector_name].get_global_matrix(frame) + end_effector_pos = np.dot(m, constraint.offset)[:3] + else: + end_effector_pos = skeleton.nodes[end_effector_name].get_global_position(frame) + + error = np.linalg.norm(constraint.position - end_effector_pos) + n_iters += 1 + + if verbose: + print("error at", n_iters, ":", error, "c:",constraint.position,"pos:", pos, chain_end_joint) + return frame, error + + +LOOK_AT_DIR = [0, -1,0] +SPINE_LOOK_AT_DIR = [0,0,1] + +def look_at_target(skeleton, root, end_effector, frame, position, local_dir=LOOK_AT_DIR): + """ find angle between the look direction and direction between end effector and target""" + #direction of endeffector + m = skeleton.nodes[end_effector].get_global_matrix(frame) + #offset = skeleton.nodes[end_effector].offset + end_effector_dir = np.dot(m[:3,:3], local_dir) + end_effector_dir = end_effector_dir / np.linalg.norm(end_effector_dir) + + # direction from endeffector to target + end_effector_pos = m[:3, 3] + target_delta = position - end_effector_pos + target_dir = target_delta / np.linalg.norm(target_delta) + + # find rotation to align vectors + root_delta_q = quaternion_from_vector_to_vector(end_effector_dir, target_dir) + root_delta_q = normalize(root_delta_q) + + #apply global delta to get new global matrix of joint + global_m = quaternion_matrix(root_delta_q) + parent_joint = skeleton.nodes[root].parent.node_name + parent_m = skeleton.nodes[parent_joint].get_global_matrix(frame, use_cache=True) + old_global = np.dot(parent_m, skeleton.nodes[root].get_local_matrix(frame)) + new_global = np.dot(global_m, old_global) + q = quaternion_from_matrix(new_global) + return normalize(q) + + + +def orient_node_to_target_look_at(skeleton,frame,node_name, end_effector, position, local_dir=LOOK_AT_DIR): + o = skeleton.nodes[node_name].quaternion_frame_index * 4 + 3 + q = look_at_target(skeleton, node_name, end_effector, frame, position, local_dir) + q = to_local_coordinate_system(skeleton, frame, node_name, q) + frame[o:o + 4] = q + return frame + + + +def run_ccd_look_at(skeleton, frame, end_effector_name, position, eps=0.01, n_max_iter=1, local_dir=LOOK_AT_DIR, chain_end_joint=None, verbose=False): + error = np.inf + n_iter = 0 + while error > eps and n_iter < n_max_iter: + node = skeleton.nodes[end_effector_name].parent + depth = 0 + while node is not None and node.node_name != chain_end_joint and node.parent is not None:#and node.node_name != skeleton.root: + frame = orient_node_to_target_look_at(skeleton,frame, node.node_name, end_effector_name, position, local_dir) + if node.joint_constraint is not None: + frame = apply_joint_constraint(skeleton, frame, node.node_name) + node = node.parent + depth += 1 + + m = skeleton.nodes[end_effector_name].get_global_matrix(frame) + + end_effector_dir = np.dot(m[:3, :3], local_dir) + end_effector_dir = end_effector_dir / np.linalg.norm(end_effector_dir) + + # direction from endeffector to target + end_effector_pos = m[:3,3] + target_delta = position - end_effector_pos + target_dir = target_delta / np.linalg.norm(target_delta) + root_delta_q = quaternion_from_vector_to_vector(end_effector_dir, target_dir) + root_delta_q = normalize(root_delta_q) + v, a = quaternion_to_axis_angle(root_delta_q) + error = abs(a) + #error = np.linalg.norm(target_dir-end_effector_dir) + #print(error) + n_iter+=1 + if verbose: + print("error at", n_iter, ":", error, "c:",position,"pos:") + + return frame, error + + + + +def look_at_target_projected(skeleton, root, end_effector, frame, position, local_dir=LOOK_AT_DIR): + """ find angle between the look direction and direction from end effector to target projected on the xz plane""" + #direction of endeffector + m = skeleton.nodes[root].get_global_matrix(frame) + end_effector_dir = np.dot(m[:3,:3], local_dir) + end_effector_dir[1] = 0 + end_effector_dir = end_effector_dir / np.linalg.norm(end_effector_dir) + + # direction from endeffector to target + end_effector_pos = m[:3, 3] + target_delta = position - end_effector_pos + target_delta[1] = 0 + target_dir = target_delta / np.linalg.norm(target_delta) + + # find rotation to align vectors + root_delta_q = quaternion_from_vector_to_vector(end_effector_dir, target_dir) + root_delta_q = normalize(root_delta_q) + #apply global delta to get new global matrix of joint + global_m = quaternion_matrix(root_delta_q) + old_global = skeleton.nodes[root].get_global_matrix(frame) + new_global = np.dot(global_m, old_global) + q = quaternion_from_matrix(new_global) + return normalize(q) + +def quaternion_to_axis_angle(q): + """http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToAngle/ + + """ + a = 2* math.acos(q[0]) + s = math.sqrt(1- q[0]*q[0]) + if s < 0.001: + x = q[1] + y = q[2] + z = q[3] + else: + x = q[1] / s + y = q[2] / s + z = q[3] / s + v = np.array([x,y,z]) + if np.sum(v)> 0: + return normalize(v),a + else: + return v, a + +def swing_twist_decomposition(q, twist_axis): + """ code by janis sprenger based on + Dobrowsolski 2015 Swing-twist decomposition in Clifford algebra. https://arxiv.org/abs/1506.05481 + """ + q = normalize(q) + #twist_axis = np.array((q * offset))[0] + projection = np.dot(twist_axis, np.array([q[1], q[2], q[3]])) * twist_axis + twist_q = np.array([q[0], projection[0], projection[1],projection[2]]) + if np.linalg.norm(twist_q) == 0: + twist_q = np.array([1,0,0,0]) + twist_q = normalize(twist_q) + swing_q = quaternion_multiply(q, quaternion_inverse(twist_q))#q * quaternion_inverse(twist) + return swing_q, twist_q + + +def orient_node_to_target_look_at_projected(skeleton, frame,node_name, end_effector, position, local_dir=LOOK_AT_DIR, twist_axis=None, max_angle=None): + o = skeleton.nodes[node_name].quaternion_frame_index * 4 + 3 + q = look_at_target_projected(skeleton, node_name, end_effector, frame, position, local_dir) + q = to_local_coordinate_system(skeleton, frame, node_name, q) + + if max_angle is not None and twist_axis is not None: + t_min_angle = np.radians(-max_angle) + t_max_angle = np.radians(max_angle) + swing_q, twist_q = swing_twist_decomposition(q, twist_axis) + v, a = quaternion_to_axis_angle(twist_q) + sign = 1 + if np.sum(v) < 0: + sign = -1 + a *= sign + a = max(a, t_min_angle) + a = min(a, t_max_angle) + new_twist_q = quaternion_about_axis(a, twist_axis) + q = quaternion_multiply(swing_q, new_twist_q) + q = normalize(q) + frame[o:o + 4] = q + return frame diff --git a/build/lib/anim_utils/motion_editing/cubic_motion_spline.py b/build/lib/anim_utils/motion_editing/cubic_motion_spline.py new file mode 100644 index 0000000..c0a5944 --- /dev/null +++ b/build/lib/anim_utils/motion_editing/cubic_motion_spline.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import numpy as np +from scipy.interpolate import interp1d +from transformations import quaternion_multiply, quaternion_inverse + + +def get_quaternion_delta(a, b): + return quaternion_multiply(quaternion_inverse(b), a) + + +def add_frames(skeleton, a, b): + """ returns c = a + b""" + #print("add frames", len(a), len(b)) + c = np.zeros(len(a)) + c[:3] = a[:3] + b[:3] + for idx, j in enumerate(skeleton.animated_joints): + o = idx * 4 + 3 + q_a = a[o:o + 4] + q_b = b[o:o + 4] + #print(q_a,q_b) + q_prod = quaternion_multiply(q_a, q_b) + c[o:o + 4] = q_prod / np.linalg.norm(q_prod) + return c + + +def substract_frames(skeleton, a, b): + """ returns c = a - b""" + c = np.zeros(len(a)) + c[:3] = a[:3] - b[:3] + for idx, j in enumerate(skeleton.animated_joints): + o = idx*4 + 3 + q_a = a[o:o+4] + q_b = b[o:o+4] + q_delta = get_quaternion_delta(q_a, q_b) + c[o:o+4] = q_delta / np.linalg.norm(q_delta) + return c + + +def generate_knots(n_frames, n_knots): + """https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.LSQUnivariateSpline.html""" + knots = np.linspace(0, n_frames - 1, n_knots) + return knots + + +class CubicMotionSpline(object): + def __init__(self, skeleton, splines, n_dims): + self.skeleton = skeleton + self.n_dims = n_dims + self.splines = splines + + @classmethod + def fit_frames(cls, skeleton, times, frames, kind='cubic'): + if type(frames) is np.ndarray: + n_frames, n_dims = frames.shape + else: + n_dims = 1 + splines = [] + for d in range(n_dims): + s = interp1d(times, frames[:, d], kind=kind) + splines.append(s) + return CubicMotionSpline(skeleton, splines, n_dims) + + def evaluate(self, t): + return np.asarray([s(t) for s in self.splines]).T + + def get_displacement_map(self, frames): + d = [] + for idx, f in enumerate(frames): + s_f = self.evaluate(idx) + d.append(substract_frames(self.skeleton, f, s_f)) + return np.array(d) + + def to_frames(self, times): + frames = [] + for t in times: + frame = self.evaluate(t) + frames.append(frame) + return frames + + def add_spline_discrete(self, other, times): + new_frames = [] + for t in times: + f = add_frames(self.skeleton, self.evaluate(t), other.evaluate(t)) + new_frames.append(f) + new_frames = np.array(new_frames) + splines = [] + for d in range(self.n_dims): + s = interp1d(times, new_frames[:, d], kind='cubic') + splines.append(s) + self.splines = splines + + def plot(self, x): + import matplotlib.pyplot as plt + for s in self.splines: + plt.plot(x, s(x)) + plt.show() diff --git a/build/lib/anim_utils/motion_editing/fabrik_chain.py b/build/lib/anim_utils/motion_editing/fabrik_chain.py new file mode 100644 index 0000000..be358f2 --- /dev/null +++ b/build/lib/anim_utils/motion_editing/fabrik_chain.py @@ -0,0 +1,551 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +""" + https://www.sciencedirect.com/science/article/pii/S1524070311000178?via%3Dihub + + based on the pseudocode by Renzo Poddighe + https://project.dke.maastrichtuniversity.nl/robotlab/wp-content/uploads/Bachelor-thesis-Renzo-Poddighe.pdf +""" +import math +import numpy as np +from transformations import quaternion_inverse, quaternion_multiply, quaternion_from_matrix, euler_from_quaternion +from .analytical_inverse_kinematics import calculate_limb_joint_rotation, calculate_limb_root_rotation, to_local_coordinate_system + +def sign(x): + return 1 if x >= 0 else -1 + +def quaternion_to_av(q): + """ according to lee 2000 + the purely imaginary quaternion is identical to the angular velocity + the sign of the real part gives the direction + Since the unit quaternion space is folded by the antipodal equivalence, + the angular velocity is twice as fast + """ + return 2 * np.array(q[1:]) * sign(q[0]) + +def normalize(v): + return v/ np.linalg.norm(v) + +def get_quaternion_delta(a, b): + return quaternion_multiply(quaternion_inverse(b), a) + + +def quaternion_from_axis_angle(axis, angle): + q = [1,0,0,0] + q[1] = axis[0] * math.sin(angle / 2) + q[2] = axis[1] * math.sin(angle / 2) + q[3] = axis[2] * math.sin(angle / 2) + q[0] = math.cos(angle / 2) + return normalize(q) + + + +def get_offset_quat(a, b): + a_len = np.linalg.norm(a) + b_len = np.linalg.norm(b) + if a_len > 0 and b_len > 0: + q = quaternion_from_vector_to_vector(a/a_len,b/b_len) + q /= np.linalg.norm(q) + return q + else: + return [1,0,0,0] + +def quaternion_from_vector_to_vector(a, b): + """src: http://stackoverflow.com/questions/1171849/finding-quaternion-representing-the-rotation-from-one-vector-to-another + http://wiki.ogre3d.org/Quaternion+and+Rotation+Primer""" + + v = np.cross(a, b) + w = np.sqrt((np.linalg.norm(a) ** 2) * (np.linalg.norm(b) ** 2)) + np.dot(a, b) + q = np.array([w, v[0], v[1], v[2]]) + if np.dot(q,q) != 0: + return q/ np.linalg.norm(q) + else: + idx = np.nonzero(a)[0] + q = np.array([0, 0, 0, 0]) + q[1 + ((idx + 1) % 2)] = 1 # [0, 0, 1, 0] for a rotation of 180 around y axis + return q + +def quaternion_from_vector_to_vector2(a, b): + """http://math.stackexchange.com/questions/293116/rotating-one-3d-vector-to-another""" + if np.array_equiv(a, b): + return [1, 0, 0, 0] + + axis = normalize(np.cross(a, b)) + dot = np.dot(a, b) + if dot >= 1.0: + return [1, 0, 0, 0] + angle = math.acos(dot) + q = quaternion_from_axis_angle(axis, angle) + return q + +def to_local_cos(skeleton, node_name, frame, q): + # bring into parent coordinate system + pm = skeleton.nodes[node_name].get_global_matrix(frame)[:3,:3] + inv_p = quaternion_inverse(quaternion_from_matrix(pm)) + inv_p /= np.linalg.norm(inv_p) + return quaternion_multiply(inv_p, q) + + +def orient_joint_to_target(skeleton, node, frame, src_pos, target_pos): + if skeleton.nodes[node].parent is None: + parent_pos = [0, 0, 0] + else: + parent_pos = skeleton.nodes[node].parent.get_global_position(frame) + src_dir = normalize(src_pos - parent_pos) + target_dir = normalize(target_pos - parent_pos) + delta_q = quaternion_from_vector_to_vector(src_dir, target_dir) + return normalize(delta_q) + + +class FABRIKBone(object): + def __init__(self, name, child): + self.name = name + self.child = child + self.position = np.array([0, 0, 0], np.float32) # position of joint + self.length = 0 + self.is_root = False + self.is_leaf = False + + +ROOT_OFFSET = np.array([0,0,0], np.float32) + +class FABRIKChain(object): + def __init__(self, skeleton, bones, node_order, tolerance=0.01, delta_tolerance=0.0001, max_iter=500, frame_offset=3, root_offset=ROOT_OFFSET, activate_constraints=False): + self.skeleton = skeleton + self.bones = bones + self.node_order = node_order + self.reversed_node_order = list(reversed(node_order)) + self.tolerance = tolerance + self.max_iter = max_iter + self.target = None + self.root_pos = None + self.chain_length = 0 + self.root_offset = root_offset + self.activate_constraints = activate_constraints + self.frame_offset = frame_offset + self.delta_tolerance = delta_tolerance + + def set_positions_from_frame(self, frame, parent_length): + self.skeleton.clear_cached_global_matrices() + for idx, node in enumerate(self.node_order): + p = self.skeleton.nodes[node].get_global_position(frame, use_cache=True) + #print("pos ", node, p) + self.bones[node].position = p + if idx ==0: + self.root_pos = p + self.chain_length = 0 + for node in self.node_order: + next_node = self.bones[node].child + if next_node is None: + break + d = np.linalg.norm(self.bones[next_node].position - self.bones[node].position) + self.bones[node].length = d + #print("length ",node, d) + self.chain_length += d + self.parent_length = parent_length + + def target_is_reachable(self): + dist = np.linalg.norm(self.target - self.root_pos) + #print("unreachable", dist, self.chain_length) + return dist < self.chain_length+ self.parent_length + + def run(self, frame, target): + self.target = target + self.set_positions_from_frame(frame, 0) + self.solve() + return self.get_joint_parameters() + + def run_partial(self, frame, target): + self.target = target + self.set_positions_from_frame(frame, 0) + self.solve() + return self.set_partial_joint_parameters(frame) + + def run_partial_with_constraints(self, frame, target): + self.target = target + self.set_positions_from_frame(frame, 0) + self.solve_partial(frame) + return self.set_partial_joint_parameters(frame) + + def run_with_constraints(self, frame, target): + self.target = target + self.set_positions_from_frame(frame, 0) + self.solve_with_constraints() + return self.get_joint_parameters() + + def solve(self): + if not self.target_is_reachable(): + print("unreachable") + # if unreachable orient joints to target + self.orient_to_target() + else: + print("reachable") + # if reachable perform forward and backward reaching until tolerance is reached + iter = 0 + distance = self.get_error() + while distance > self.tolerance and iter< self.max_iter: + self.backward() + self.forward() + iter+=1 + distance = self.get_error() + print("iter",iter, distance) + + def solve_with_constraints(self): + if not self.target_is_reachable(): + print("unreachable") + # if unreachable orient joints to target + self.orient_to_target() + else: + print("reachable") + # if reachable perform forward and backward reaching until tolerance is reached + iter = 0 + distance = self.get_error() + while distance > self.tolerance and iter < self.max_iter: + self.backward() + self.forward() + self.apply_constraints() + iter+=1 + distance = self.get_error() + print("iter",iter, distance) + + def solve_partial(self, frame): + if not self.target_is_reachable(): + print("unreachable") + # if unreachable orient joints to target + self.orient_to_target() + else: + print("reachable") + # if reachable perform forward and backward reaching until tolerance is reached + iter = 0 + distance = self.get_error() + while distance > self.tolerance and iter < self.max_iter: + self.backward() + self.forward() + + self.set_partial_joint_parameters(frame) + self.set_positions_from_frame(frame, 0) + + iter += 1 + distance = self.get_error() + print("iter", iter, distance) + + def get_error(self): + end_effector = self.node_order[-1] + return np.linalg.norm(self.bones[end_effector].position - self.target) + + def orient_to_target(self): + for idx, node in enumerate(self.node_order[:-1]): + next_node = self.bones[node].child + if next_node is None: + print("Error: none at ",node) + break + r = np.linalg.norm(self.target - self.bones[node].position) + if r > 0: + l = self.bones[node].length / r + self.bones[next_node].position = (1 - l) * self.bones[node].position + l * self.target + + def backward(self): + end_effector = self.node_order[-1] + self.bones[end_effector].position = np.array(self.target) + n_points = len(self.node_order) + for idx in range(n_points - 2, -1, -1): + node = self.node_order[idx] + next_node = self.bones[node].child + r = np.linalg.norm(self.bones[next_node].position - self.bones[node].position) + if r > 0: + l = self.bones[node].length / r + self.bones[node].position = (1 - l) * self.bones[next_node].position + l * self.bones[node].position + + def forward(self): + root_node = self.node_order[0] + self.bones[root_node].position = self.root_pos + for idx, node in enumerate(self.node_order[:-1]): #for p_idx in range(0, self.n_points - 1, 1): + #next_node = self.node_order[idx + 1] + next_node = self.bones[node].child + r = np.linalg.norm(self.bones[next_node].position - self.bones[node].position) + if r > 0: + l = self.bones[node].length / r + self.bones[next_node].position = l * self.bones[next_node].position + (1 - l) * self.bones[node].position + + def apply_constraints(self): + frame = self.get_joint_parameters() + self.set_positions_from_frame(frame, 0) + return + + def get_joint_parameters(self): + n_joints = len(self.node_order) - 1 + frame = np.zeros(n_joints*4+3) + o = 3 + prev_point = self.root_offset + for idx, node in enumerate(self.node_order[:-1]): + #for node in self.skeleton.animated_joints: + next_node = self.bones[node].child + q = self.get_global_rotation(node, next_node) + frame[o:o + 4] = to_local_cos(self.skeleton, node, frame, q) + if self.skeleton.nodes[node].joint_constraint is not None and self.activate_constraints: + self.apply_constraint_with_swing(node, frame, o) + prev_point = self.bones[next_node].position + o += 4 + return frame + + def set_partial_joint_parameters(self, frame): + o = self.frame_offset + for idx, node in enumerate(self.node_order[:-1]): + next_node = self.bones[node].child + q = self.get_global_rotation_non_cos(node, next_node, frame) + frame[o:o + 4] = to_local_coordinate_system(self.skeleton,frame, node, q) + if self.skeleton.nodes[node].joint_constraint is not None and self.activate_constraints: + self.apply_constraint_with_swing(node, frame, o) + o += 4 + return frame + + def apply_constraint_with_swing(self, node, frame, o, eps=0.01): + old_q = np.array(frame[o:o + 4]) + + #remove twist rotation + swing_q, twist_q = self.skeleton.nodes[node].joint_constraint.split(old_q) + frame[o:o + 4] = twist_q + + # apply swing_q to parent + parent_q = np.array(frame[o - 4:o]) + new_parent_q = quaternion_multiply(parent_q, swing_q) + new_parent_q = normalize(new_parent_q) + frame[o - 4:o] = new_parent_q + new_node_pos = self.skeleton.nodes[node].children[0].get_global_position(frame) + diff = np.linalg.norm(new_node_pos - self.target) + + # calculate rotation fix if necessary + if diff > eps: + delta_q = orient_joint_to_target(self.skeleton, node, frame, new_node_pos, self.target) + aligned_parent_q = quaternion_multiply(delta_q, new_parent_q) + aligned_parent_q = normalize(aligned_parent_q) + frame[o - 4:o] = aligned_parent_q + + def apply_constraint_with_swing_global(self, node, frame, o, eps=0.01): + old_q = np.array(frame[o:o + 4]) + next_node = self.bones[node].child + parent_m = self.skeleton.nodes[node].get_global_matrix(frame)[:3, :3] + node_m = self.skeleton.nodes[next_node].get_global_matrix(frame)[:3, :3] + node_q = quaternion_from_matrix(node_m) + node_q = normalize(node_q) + # remove twist rotation + swing_q, twist_q = self.skeleton.nodes[node].joint_constraint.split_global(parent_m, node_q) + frame[o:o + 4] = twist_q + + # apply swing_q to parent + parent_q = np.array(frame[o - 4:o]) + new_parent_q = quaternion_multiply(parent_q, swing_q) + new_parent_q = normalize(new_parent_q) + frame[o - 4:o] = new_parent_q + new_node_pos = self.skeleton.nodes[node].children[0].get_global_position(frame) + diff = np.linalg.norm(new_node_pos - self.target) + return + # calculate rotation fix if necessary + if diff > eps: + delta_q = orient_joint_to_target(self.skeleton, node, frame, new_node_pos, self.target) + aligned_parent_q = quaternion_multiply(delta_q, new_parent_q) + aligned_parent_q = normalize(aligned_parent_q) + frame[o - 4:o] = aligned_parent_q + + + def apply_constraint_with_swing_and_lee(self, node, frame, o, eps=0.01): + old_q = frame[o:o + 4] + old_node_pos = self.skeleton.nodes[node].children[0].get_global_position(frame) + delta_q, new_q = self.skeleton.nodes[node].joint_constraint.split(old_q) + frame[o:o + 4] = new_q + new_node_pos = self.skeleton.nodes[node].children[0].get_global_position(frame) + diff = np.linalg.norm(new_node_pos - old_node_pos) + if diff > eps: + parent_q = frame[o - 4:o] + new_parent_q = quaternion_multiply(parent_q, delta_q) + new_parent_q = normalize(new_parent_q) + frame[o - 4:o] = new_parent_q + new_node_pos = self.skeleton.nodes[node].children[0].get_global_position(frame) + diff = np.linalg.norm(new_node_pos -old_node_pos) + if diff > eps: + parent_q = frame[o - 4:o] + root = None + if self.skeleton.nodes[node].parent is not None: + root = self.skeleton.nodes[node].parent.node_name + end_effector = self.skeleton.nodes[node].children[0].node_name + print("apply lee tolani",root, node, end_effector, diff) + local_axis = self.skeleton.nodes[node].joint_constraint.axis + #frame[o:o + 4] = calculate_limb_joint_rotation(self.skeleton, root, node, end_effector, local_axis, frame, self.target) + delta_q = calculate_limb_root_rotation(self.skeleton, root, end_effector, frame, self.target) + new_parent_q = quaternion_multiply(parent_q, delta_q) + new_parent_q = normalize(new_parent_q) + frame[o - 4:o] = new_parent_q + + def apply_constraint_with_swing2(self, node, parent_node, frame, o): + next_node = self.bones[node].child + target_global_m = self.skeleton.nodes[next_node].get_global_matrix(frame)[:3,:3] + old_q = frame[o:o + 4] + delta_q, new_q = self.skeleton.nodes[node].joint_constraint.split(old_q) + frame[o:o + 4] = new_q + new_global_m = self.skeleton.nodes[next_node].get_global_matrix(frame)[:3,:3] + delta_global_m = np.dot(np.linalg.inv(new_global_m), target_global_m) + actual_delta_q = normalize(quaternion_from_matrix(delta_global_m)) + parent_q = frame[o - 4:o] + new_parent_q = quaternion_multiply(actual_delta_q, parent_q) + new_parent_q = normalize(new_parent_q) + frame[o - 4:o] = new_parent_q + + + + def apply_constraint_with_vector(self, node, parent_node, frame, o): + next_node = self.bones[node].child + old_pos = self.bones[next_node].position + old_q = frame[o:o + 4] + twist_q, swing_q = self.skeleton.nodes[node].joint_constraint.split(old_q) + frame[o:o + 4] = swing_q + + # get global delta quaternion to apply on parent + parent_pos = self.skeleton.nodes[parent_node].get_global_position(frame) + next_node_pos = self.skeleton.nodes[next_node].get_global_position(frame) + position_delta = np.linalg.norm(old_pos-next_node_pos) + print("position delta", position_delta) + if position_delta < 0.001: + return + desired_offset = normalize(old_pos - parent_pos) + offset = normalize(next_node_pos - parent_pos) + delta_q = quaternion_from_vector_to_vector(offset, desired_offset) + print("deltaq", parent_node, node, next_node, next_node_pos, old_pos, twist_q, swing_q) + # apply global delta on parent + if True: + global_m = self.skeleton.nodes[parent_node].get_global_matrix(frame) + global_parent_q = normalize(quaternion_from_matrix(global_m)) + new_parent_q = quaternion_multiply(global_parent_q, delta_q) + new_parent_q = normalize(new_parent_q) + frame[o - 4:o] = to_local_cos(self.skeleton, parent_node, frame, new_parent_q) + else: + local_parent_q = frame[o - 4:o] + local_delta = to_local_cos(self.skeleton, parent_node, frame, delta_q) + + new_parent_q = quaternion_multiply(local_parent_q, local_delta) + new_parent_q = normalize(new_parent_q) + frame[o - 4:o] = new_parent_q + print(new_parent_q, local_parent_q, local_delta, delta_q) + + + + def get_global_rotation(self, node, next_node): + """ FIXME works only when identity frame coordinate system is the same as the offset """ + + #print("set", node, next_node) + target = self.bones[next_node].position - self.bones[node].position + next_offset = np.array(self.skeleton.nodes[next_node].offset) + target_len = np.linalg.norm(target) + if target_len > 0: + target /= target_len + next_offset /= np.linalg.norm(next_offset) + + # 1. sum over offsets of static nodes + local_offset = self.get_child_offset(node, next_node) + actual_offset = next_offset + local_offset + actual_offset /= np.linalg.norm(actual_offset) # actual_offset = [0.5, 0.5,0] + # 2. get global rotation + q = quaternion_from_vector_to_vector(actual_offset, target) + return q + + else: + #print("skip", target_len, self.bones[next_node].position) + return [1, 0, 0, 0] + + def get_global_rotation_non_cos(self, node, next_node, frame): + target_position = self.bones[next_node].position + root_pos = self.skeleton.nodes[node].get_global_position(frame) + #src_dir = np.dot(m, offset) + end_effector_pos = self.skeleton.nodes[next_node].get_global_position(frame) + src_delta = end_effector_pos - root_pos + src_dir = src_delta / np.linalg.norm(src_delta) + + target_delta = target_position - root_pos + target_dir = target_delta / np.linalg.norm(target_delta) + + #q = quaternion_from_vector_to_vector(offset, target_dir) + q = quaternion_from_vector_to_vector2(src_dir, target_dir) + q = normalize(q) + return q + + + def get_child_offset(self, node, child_node): + """ + """ + actual_offset = np.array([0, 0, 0], np.float) + while node is not None and self.skeleton.nodes[node].children[0].node_name != child_node: + local_offset = np.array(self.skeleton.nodes[node].children[0].offset) + local_offset /= np.linalg.norm(local_offset) + actual_offset = actual_offset + local_offset + node = self.skeleton.nodes[node].children[0].node_name + if len(self.skeleton.nodes[node].children) < 1: + node = None + + return actual_offset + + def get_joint_parameters_global(self): + n_joints = len(self.node_order)-1 + frame = np.zeros(n_joints*4+3) + o = self.frame_offset + for idx, node in enumerate(self.node_order[:-1]): + offset = np.array(self.skeleton.nodes[node].children[0].offset) + offset /= np.linalg.norm(offset) + next_node = self.bones[node].child + if idx == 0: + if self.skeleton.root == node: + dir_vector = self.bones[next_node].position + dir_vector_len = np.linalg.norm(dir_vector) + if dir_vector_len > 0 and np.linalg.norm(offset) > 0: + dir_vector /= dir_vector_len + q = quaternion_from_vector_to_vector(offset, dir_vector) + frame[o:o + 4] = q + else: + print("work around", offset,dir_vector_len, node) + frame[o:o + 4] = [1, 0, 0, 0] + else: + print("work root around") + frame[o:o + 4] = [1, 0, 0, 0] + + else: + q = self.get_global_rotation(node, next_node) + frame[o:o+4] =q + o += 4 + print(frame) + return frame + + def get_global_positions(self): + position_dict = dict() + for node in self.node_order: + position_dict[node] = self.bones[node].position + return position_dict + + + def get_end_effector_position(self): + root_node = self.node_order[-1] + return self.bones[root_node].position + + def get_next_nodes(self, next_nodes): + for idx, n in enumerate(self.node_order): + if idx+1 < len(self.node_order): + next_nodes[n] = self.node_order[idx+1] + else: + next_nodes[n] = None diff --git a/build/lib/anim_utils/motion_editing/fabrik_node.py b/build/lib/anim_utils/motion_editing/fabrik_node.py new file mode 100644 index 0000000..b8aea92 --- /dev/null +++ b/build/lib/anim_utils/motion_editing/fabrik_node.py @@ -0,0 +1,316 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import numpy as np +from .fabrik_chain import FABRIKChain, to_local_cos, quaternion_from_vector_to_vector, FABRIKBone, ROOT_OFFSET + + +def get_child_offset(skeleton, node, child_node): + actual_offset = np.array([0, 0, 0], np.float) + while node is not None and skeleton.nodes[node].children[0].node_name != child_node: + local_offset = np.array(skeleton.nodes[node].children[0].offset) + local_offset_len = np.linalg.norm(local_offset) + if local_offset_len > 0: + local_offset /= local_offset_len + actual_offset = actual_offset + local_offset + node = skeleton.nodes[node].children[0].node_name + if len(skeleton.nodes[node].children) < 1: + node = None + else: + node = None + return actual_offset + + +def get_global_joint_parameters_from_positions(skeleton, positions, next_nodes): + n_parameters = len(skeleton.animated_joints)*4+3 + frame = np.zeros(n_parameters) + frame[:3] = positions[skeleton.root] + o = 3 + print(skeleton.animated_joints) + print(next_nodes.keys()) + print(positions.keys()) + for node in skeleton.animated_joints: + next_node = next_nodes[node] + if next_node in positions: + next_offset = np.array(skeleton.nodes[next_node].offset) + next_offset /= np.linalg.norm(next_offset) + + # 1. sum over offsets of static nodes + local_offset = get_child_offset(skeleton, node, next_node) + #print(node, local_offset) + actual_offset = next_offset + local_offset + actual_offset /= np.linalg.norm(actual_offset) # actual_offset = [0.5, 0.5,0] + + + dir_to_child = positions[next_node] - positions[node] + dir_to_child /= np.linalg.norm(dir_to_child) + + # 2. get global rotation + global_q = quaternion_from_vector_to_vector(actual_offset, dir_to_child) + frame[o:o + 4] = global_q + else: + #print("ignore", node, next_node) + frame[o:o + 4] = [1,0,0,0] + o += 4 + return frame + + +def global_to_local_frame(skeleton, global_frame): + n_parameters = len(skeleton.animated_joints)*4+3 + frame = np.zeros(n_parameters) + o = 3 + for node in skeleton.animated_joints: + frame[o:o + 4] = to_local_cos(skeleton, node, frame, global_frame[o:o + 4]) + + o += 4 + + return frame + + +class FABRIKNode(object): + def __init__(self, skeleton, root, parent_chain=None, tolerance=0.01, max_iter=100, root_offset=ROOT_OFFSET): + self.root_pos = np.array([0,0,0], dtype=np.float) + self.skeleton = skeleton + self.n_parameters = len(self.skeleton.animated_joints)*4+3 + self.tolerance = tolerance + self.max_iter = max_iter + self.parent_chain = parent_chain + self.root = root + self.root_offset = root_offset + self.child_nodes = list() + # self.target = None + # self.position = None + self.is_leaf = True + self.construct_from_skeleton(skeleton, root, tolerance, max_iter) + + def construct_from_skeleton(self, skeleton, root, tolerance, max_iter): + """ there need to be two end effectors""" + children = [c.node_name for c in skeleton.nodes[root].children] + for c in children: + self.is_leaf = False + current_node = c + node_order = [self.root] + while current_node is not None: + n_children = len(skeleton.nodes[current_node].children) + if n_children == 1: # append to chain + child_node = skeleton.nodes[current_node].children[0].node_name + # only add list to joints + if not skeleton.nodes[current_node].fixed: + node_order.append(current_node)# skip fixed nodes + current_node = child_node + else: # stop chain # split up by adding child nodes + if n_children > 0: + node_order.append(current_node) + bones = dict() + for idx, node in enumerate(node_order): + child_node = None + if idx+1 < len(node_order): + child_node = node_order[idx + 1] + bones[node] = FABRIKBone(node, child_node) + if idx == 0 and self.parent_chain is None : + bones[node].is_root = True + else: + bones[node].is_root = False + parent_chain = FABRIKChain(skeleton, bones, node_order) + print("construct node at",self.root , current_node, node_order) + node = FABRIKNode(skeleton, current_node, parent_chain, tolerance, max_iter) + self.child_nodes.append(node) + current_node = None + + def backward_stage(self): + for c in self.child_nodes: + c.backward_stage() # calculate backward update of children of child to start at the end effectors + + if not self.is_leaf: + positions = [c.get_chain_root_position() for c in self.child_nodes] + t = np.mean(positions, axis=0) + #print("centroid",t) + #print("no leaf") + else: + t = self.parent_chain.target + #print("leaf") + + if self.parent_chain is not None: + self.parent_chain.target = t + if self.target_is_reachable(): + self.parent_chain.backward() + else: + print("unreachable") + # if unreachable orient joints to target + self.parent_chain.orient_to_target() + + def target_is_reachable(self): + return self.parent_chain.target_is_reachable() + + def forward_stage(self): + # calculate forward update of parent chain + if self.parent_chain is not None: + #if self.parent_chain.target_is_reachable(): + self.parent_chain.forward() + #else: + # self.parent_chain.orient_to_target() + + # calculate forward update of children of child + for c in self.child_nodes: + if self.parent_chain is not None: + c.parent_chain.root_pos = self.parent_chain.get_end_effector_position() + else: + c.parent_chain.root_pos = self.root_pos + c.forward_stage() + + def get_chain_root_position(self): + root_node = self.parent_chain.node_order[0] + return self.parent_chain.bones[root_node].position + + + def set_positions_from_frame(self, frame, parent_length): + if self.parent_chain is not None: + self.parent_chain.set_positions_from_frame(frame, parent_length) + parent_length += self.parent_chain.chain_length + for c in self.child_nodes: + c.set_positions_from_frame(frame, parent_length) + + def get_error(self): + error = 0 + for c in self.child_nodes: + error += c.get_error() + if self.is_leaf: + if self.parent_chain.target_is_reachable(): + error += self.parent_chain.get_error() + else: + error += 0 + return error + + def solve(self): + iter = 0 + distance = self.get_error() + while distance > self.tolerance and iter < self.max_iter: + self.backward_stage() + self.forward_stage() + distance = self.get_error() + iter += 1 + n_out_of_reach = self.targets_out_of_reach() + if n_out_of_reach > 0: + self.orient_to_targets() + print("solved", distance, n_out_of_reach) + self.print_end_effectors() + + def print_end_effectors(self): + n_targets = 0 + for c in self.child_nodes: + if c.is_leaf: + print(c.root, c.parent_chain.target, c.parent_chain.get_end_effector_position()) + else: + c.print_end_effectors() + return n_targets + + def targets_out_of_reach(self): + n_targets = 0 + for c in self.child_nodes: + if c.is_leaf and not c.parent_chain.target_is_reachable(): + n_targets+=1 + else: + n_targets += c.targets_out_of_reach() + return n_targets + + def orient_to_targets(self): + for c in self.child_nodes: + if c.is_leaf and not c.parent_chain.target_is_reachable(): + c.parent_chain.orient_to_target() + else: + c.orient_to_targets() + + def get_joint_parameters(self, frame): + #print("get joint parameters") + for c in self.child_nodes: + print("from", c.parent_chain.node_order[0], "to", c.parent_chain.node_order[-1]) + global_frame = c.parent_chain.get_joint_parameters_global() + src = 3 + if self.parent_chain is None: + animated_joints = c.parent_chain.node_order + else: + animated_joints = c.parent_chain.node_order[1:] + for j in animated_joints: #ignore root rotation + dest = self.skeleton.animated_joints.index(j)*4 +3 + frame[dest:dest+4] = to_local_cos(self.skeleton, j, frame, global_frame[src:src+4]) + src += 4 + c.get_joint_parameters(frame) + return frame + + def set_targets(self, targets): + #print("set targets",targets) + if self.is_leaf: + if self.root in targets: + self.target = targets[self.root] + self.parent_chain.target = self.target + #print("set target",self.root) + else: + self.target = self.parent_chain.get_end_effector_position() # keep the initial position as target for unconstrained leafs + self.parent_chain.target = self.target + print("set end effector position") + else: + for c in self.child_nodes: + c.set_targets(targets) + + def run(self, orig_frame, targets): + self.set_positions_from_frame(orig_frame, 0) + self.set_targets(targets) + self.solve() + if False: + frame = np.zeros(self.n_parameters) + self.get_joint_parameters(frame) + else: + joint_pos = dict() + self.get_global_positions(joint_pos) + next_nodes = dict() + self.get_next_nodes(next_nodes) + frame = get_global_joint_parameters_from_positions(self.skeleton, joint_pos, next_nodes) + frame = global_to_local_frame(self.skeleton, frame) + #print("done") + #self.print_frame_parameters(frame) + return frame + + def get_next_nodes(self, next_nodes): + if self.parent_chain is not None: + self.parent_chain.get_next_nodes(next_nodes) + for c in self.child_nodes: + c.get_next_nodes(next_nodes) + + def print_frame_parameters(self, frame): + idx = 3 + for n in self.skeleton.nodes: + if len(self.skeleton.nodes[n].children) > 0: + print(n, frame[idx: idx+4]) + idx +=4 + return + + def draw(self, m,v,p, l): + if self.parent_chain is not None: + self.parent_chain.draw(m,v,p,l) + for c in self.child_nodes: + c.draw(m, v, p, l) + + def get_global_positions(self, positions): + if self.parent_chain is not None: + positions.update(self.parent_chain.get_global_positions()) + for c in self.child_nodes: + c.get_global_positions(positions) \ No newline at end of file diff --git a/build/lib/anim_utils/motion_editing/footplant_constraint_generator.py b/build/lib/anim_utils/motion_editing/footplant_constraint_generator.py new file mode 100644 index 0000000..7cd4ad6 --- /dev/null +++ b/build/lib/anim_utils/motion_editing/footplant_constraint_generator.py @@ -0,0 +1,692 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import collections +import numpy as np +from scipy.interpolate import UnivariateSpline +from transformations import quaternion_from_matrix, quaternion_multiply, quaternion_matrix, quaternion_slerp, quaternion_inverse +from ..animation_data.skeleton_models import * +from .motion_grounding import MotionGroundingConstraint, FOOT_STATE_SWINGING +from .utils import get_average_joint_position, get_average_joint_direction, normalize, get_average_direction_from_target +from ..animation_data.utils import quaternion_from_vector_to_vector + +def get_velocity_and_acceleration(scalars): + """ https://stackoverflow.com/questions/40226357/second-derivative-in-python-scipy-numpy-pandas + """ + x = np.linspace(0, len(scalars), len(scalars)) + ys = np.array(scalars, dtype=np.float32) + y_spl = UnivariateSpline(x, ys, s=0, k=4) + velocity = y_spl.derivative(n=1) + acceleration = y_spl.derivative(n=2) + return velocity(x), acceleration(x) + +def get_joint_vertical_velocity_and_acceleration(skeleton, frames, joints): + joint_heights = dict() + for joint in joints: + ps = [] + for frame in frames: + p = skeleton.nodes[joint].get_global_position(frame) + ps.append(p[1]) + if len(ps)> 1: + v, a = get_velocity_and_acceleration(ps) + joint_heights[joint] = ps[:-1], v, a + elif len(ps) == 1: + joint_heights[joint] = [ps[0]], [0], [0] + else: + joint_heights[joint] = [], [], [] + + return joint_heights + + +def get_joint_height(skeleton, frames, joints): + joint_heights = dict() + for joint in joints: + ps = [] + for frame in frames: + p = skeleton.nodes[joint_name].get_global_position(frame) + ps.append(p) + joint_heights[joint] = positions + return joint_heights + + +def guess_ground_height(skeleton, frames, start_frame, n_frames, foot_joints): + minimum_height = np.inf + joint_heights = get_joint_height(skeleton, frames[start_frame:start_frame+n_frames], foot_joints) + for joint in joint_heights: + p = joint_heights[joint] + pT = np.array(p).T + new_min_height = min(pT[1]) + if new_min_height < minimum_height: + minimum_height = new_min_height + return minimum_height + + +def get_quat_delta(qa, qb): + """ get quaternion from quat a to quat b """ + return quaternion_multiply(qb, quaternion_inverse(qa)) + + +def get_leg_state(plant_range, foot_definitions, side, frame): + state = "swinging" + for foot in foot_definitions[side]: + if side in plant_range: + if foot in plant_range[side]: + if plant_range[side][foot]["start"] is not None and plant_range[side][foot]["start"] <= frame <= plant_range[side][foot]["end"]: + state = "planted" + return state + + +def create_leg_state_model(plant_range, start_frame, end_frame, foot_definitions): + model = collections.OrderedDict() + for f in range(start_frame, end_frame): + model[f] = dict() + for side in foot_definitions: + model[f][side] = get_leg_state(plant_range, foot_definitions, side, f) + return model + + +def regenerate_ankle_constraint_with_new_orientation(position, joint_offset, new_orientation): + """ move ankle so joint is on the ground using the new orientation""" + m = quaternion_matrix(new_orientation)[:3, :3] + target_offset = np.dot(m, joint_offset) + ca = position - target_offset + return ca + +def regenerate_ankle_constraint_with_new_orientation2(position, joint_offset, delta): + """ move ankle so joint is on the ground using the new orientation""" + m = quaternion_matrix(delta)[:3, :3] + target_offset = np.dot(m, joint_offset) + ca = position + target_offset + return ca + +def get_global_orientation(skeleton, joint_name, frame): + m = skeleton.nodes[joint_name].get_global_matrix(frame) + m[:3, 3] = [0, 0, 0] + return normalize(quaternion_from_matrix(m)) + + +def convert_plant_range_to_ground_contacts(start_frame, end_frame, plant_range): + print("convert plant range", start_frame, end_frame) + ground_contacts = collections.OrderedDict() + for f in range(start_frame, end_frame+1): + ground_contacts[f] = [] + for side in list(plant_range.keys()): + for joint in list(plant_range[side].keys()): + if plant_range[side][joint]["start"] is not None: + start = plant_range[side][joint]["start"] + end = plant_range[side][joint]["end"] + print(joint, start,end) + for f in range(start, end): + ground_contacts[f].append(joint) + return ground_contacts + + +def find_first_frame(skeleton, frames, joint_name, start_frame, end_frame, tolerance=2.0): + p = skeleton.nodes[joint_name].get_global_position(frames[end_frame-1]) + h = p[1] + search_window = list(range(start_frame, end_frame)) + for f in reversed(search_window): + tp = skeleton.nodes[joint_name].get_global_position(frames[f]) + if abs(tp[1]-h) > tolerance: + return f + return start_frame + + +def find_last_frame(skeleton, frames, joint_name, start_frame, end_frame, tolerance=2.0): + p = skeleton.nodes[joint_name].get_global_position(frames[start_frame]) + h = p[1] + search_window = list(range(start_frame, end_frame)) + for f in search_window: + tp = skeleton.nodes[joint_name].get_global_position(frames[f]) + if abs(tp[1] - h) > tolerance: + return f + return end_frame + + +def merge_constraints(a,b): + for key, item in list(b.items()): + if key in a: + a[key] += b[key] + else: + a[key] = b[key] + return a + +def align_quaternion(q, ref_q): + if np.dot(ref_q, q) < 0: + return -q + else: + return q + + +def blend(x): + return 2 * x * x * x - 3 * x * x + 1 + + +def get_plant_frame_range(step, search_window): + start_frame = step.start_frame + end_frame = step.end_frame + 1 + half_window = search_window / 2 + end_offset = -5 + plant_range = dict() + L = "left" + R = "right" + plant_range[L] = dict() + plant_range[R] = dict() + + plant_range[L]["start"] = None + plant_range[L]["end"] = None + plant_range[R]["start"] = None + plant_range[R]["end"] = None + + if step.node_key[1] == "beginLeftStance": + plant_range[R]["start"] = start_frame + plant_range[R]["end"] = end_frame - half_window + end_offset + plant_range[L]["start"] = start_frame + plant_range[L]["end"] = start_frame + 20 + + elif step.node_key[1] == "beginRightStance": + plant_range[L]["start"] = start_frame + plant_range[L]["end"] = end_frame - half_window + end_offset + plant_range[R]["start"] = start_frame + plant_range[R]["end"] = start_frame + 20 + + elif step.node_key[1] == "endLeftStance": + plant_range[R]["start"] = start_frame + half_window + plant_range[R]["end"] = end_frame + plant_range[L]["start"] = end_frame - 20 + plant_range[L]["end"] = end_frame + + elif step.node_key[1] == "endRightStance": + plant_range[L]["start"] = start_frame + half_window + plant_range[L]["end"] = end_frame + plant_range[R]["start"] = end_frame - 20 + plant_range[R]["end"] = end_frame + + elif step.node_key[1] == "leftStance": + plant_range[R]["start"] = start_frame + half_window + plant_range[R]["end"] = end_frame - half_window + end_offset + + elif step.node_key[1] == "rightStance": + plant_range[L]["start"] = start_frame + half_window + plant_range[L]["end"] = end_frame - half_window + end_offset + return plant_range + + +def get_plant_frame_range_using_search(skeleton, motion_vector, step, foot_definitions, search_window, lift_tolerance): + """ Use the assumption that depending on the motion primitive different + feet are grounded at the beginning, middle and end of the step to find the plant range for each joint. + Starting from those frames search forward and backward until the foot is lifted. + """ + start_frame = step.start_frame + end_frame = step.end_frame + 1 + frames = motion_vector.frames + w = search_window + search_start = max(end_frame - w, start_frame) # start of search range for the grounding range + search_end = min(start_frame + w, end_frame) # end of search range for the grounding range + plant_range = dict() + L = "left" + R = "right" + joint_types = ["heel", "toe"] # first check heel then toe + plant_range[L] = dict() + plant_range[R] = dict() + for side in list(plant_range.keys()): + plant_range[side] = dict() + for joint_type in joint_types: + joint = foot_definitions[side][joint_type] + plant_range[side][joint] = dict() + plant_range[side][joint]["start"] = None + plant_range[side][joint]["end"] = None + + if step.node_key[1] == "beginLeftStance": + plant_range[side][joint]["start"] = start_frame + plant_range[side][joint]["end"] = find_last_frame(skeleton, frames, joint, start_frame, search_end, lift_tolerance) + + elif step.node_key[1] == "beginRightStance": + plant_range[side][joint]["start"] = start_frame + plant_range[side][joint]["end"] = find_last_frame(skeleton, frames, joint, start_frame, search_end, lift_tolerance) + + elif step.node_key[1] == "endLeftStance": + heel = foot_definitions[side]["heel"] + if plant_range[side][heel]["start"] is not None: + local_search_start = plant_range[side][heel]["start"] + else: + local_search_start = start_frame + plant_range[side][joint]["start"] = find_first_frame(skeleton, frames, joint, local_search_start, end_frame, lift_tolerance+3.0) + plant_range[side][joint]["end"] = end_frame + + elif step.node_key[1] == "endRightStance": + heel = foot_definitions[side]["heel"] + if plant_range[side][heel]["start"] is not None: + local_search_start = plant_range[side][heel]["start"] + else: + local_search_start = start_frame + plant_range[side][joint]["start"] = find_first_frame(skeleton, frames, joint, local_search_start, end_frame, lift_tolerance+3.0) + plant_range[side][joint]["end"] = end_frame + + elif step.node_key[1] == "leftStance" and side == R: + middle_frame = int((end_frame-start_frame)/2) + start_frame + plant_range[side][joint]["start"] = find_first_frame(skeleton, frames, joint, search_start, middle_frame, lift_tolerance) + plant_range[side][joint]["end"] = find_last_frame(skeleton, frames, joint, middle_frame + 1, search_end, lift_tolerance) + + elif step.node_key[1] == "rightStance" and side == L: + middle_frame = int((end_frame - start_frame) / 2) + start_frame + plant_range[side][joint]["start"] = find_first_frame(skeleton, frames, joint, search_start, middle_frame, lift_tolerance) + plant_range[side][joint]["end"] = find_last_frame(skeleton, frames, joint, middle_frame + 1, search_end, lift_tolerance) + return plant_range + + + +class SceneInterface(object): + def __init__(self, height): + self.h = height + + def get_height(self,x, z): + return self.h + +def create_ankle_constraint(skeleton, frames, ankle_joint_name, heel_joint_name, toe_joint, frame_idx, end_frame, ground_height): + """ create constraint on ankle position and orientation """ + c = get_average_joint_position(skeleton, frames, heel_joint_name, frame_idx, end_frame) + c[1] = ground_height + a = get_average_joint_position(skeleton, frames, ankle_joint_name, frame_idx, end_frame) + h = get_average_joint_position(skeleton, frames, heel_joint_name, frame_idx, end_frame) + target_heel_offset = a - h + ca = c + target_heel_offset + avg_direction = None + if len(skeleton.nodes[ankle_joint_name].children) > 0: + avg_direction = get_average_direction_from_target(skeleton, frames, ca, toe_joint, + frame_idx, end_frame) + toe_length = np.linalg.norm(skeleton.nodes[toe_joint].offset) + ct = ca + avg_direction * toe_length + ct[1] = ground_height + avg_direction = normalize(ct - ca) + return MotionGroundingConstraint(frame_idx, ankle_joint_name, ca, avg_direction) + + +class FootplantConstraintGenerator(object): + def __init__(self, skeleton, settings, scene_interface=None): + self.skeleton = skeleton + self.left_foot = skeleton.skeleton_model["joints"]["left_ankle"] + self.right_foot = skeleton.skeleton_model["joints"]["right_ankle"] + self.right_heel = skeleton.skeleton_model["joints"]["right_heel"] + self.left_heel = skeleton.skeleton_model["joints"]["left_heel"] + self.right_toe = skeleton.skeleton_model["joints"]["right_toe"] + self.left_toe = skeleton.skeleton_model["joints"]["left_toe"] + self.contact_joints = [self.right_heel, self.left_heel, self.right_toe, self.left_toe] + + self.foot_definitions = {"right": {"heel": self.right_heel, "toe": self.right_toe, "ankle": self.right_foot}, + "left": {"heel": self.left_heel, "toe": self.left_toe, "ankle": self.left_foot}} + + if scene_interface is None: + self.scene_interface = SceneInterface(0) + else: + self.scene_interface = scene_interface + + self.contact_tolerance = settings["contact_tolerance"] + self.foot_lift_tolerance = settings["foot_lift_tolerance"] + self.constraint_generation_range = settings["constraint_range"] + self.smoothing_constraints_window = settings["smoothing_constraints_window"] + self.foot_lift_search_window = settings["foot_lift_search_window"] + + self.position_constraint_buffer = dict() + self.orientation_constraint_buffer = dict() + self.velocity_tolerance = 0 + if "velocity_tolerance" in settings: + self.velocity_tolerance = settings["velocity_tolerance"] + + def detect_ground_contacts(self, frames, joints, ground_height=0): + """https://stackoverflow.com/questions/3843017/efficiently-detect-sign-changes-in-python + """ + joint_y_vel_acc = get_joint_vertical_velocity_and_acceleration(self.skeleton, frames, joints) + ground_contacts = [] + for f in frames: + ground_contacts.append([]) + for joint in joints: + ps, yv, ya = joint_y_vel_acc[joint] + for idx, p in enumerate(ps): + velocity = np.sqrt(yv[idx]*yv[idx]) + if p - ground_height < self.contact_tolerance or velocity < self.velocity_tolerance :#and zero_crossings[frame_idx] + ground_contacts[idx].append(joint) + # copy contacts of frame not covered by velocity + if len(ground_contacts) > 1: + ground_contacts[-1] = ground_contacts[-2] + return self.filter_outliers(ground_contacts, joints) + + def filter_outliers(self, ground_contacts, joints): + n_frames = len(ground_contacts) + filtered_ground_contacts = [[] for idx in range(n_frames)] + filtered_ground_contacts[0] = ground_contacts[0] + filtered_ground_contacts[-1] = ground_contacts[-1] + frames_indices = range(1,n_frames-1) + for frame_idx in frames_indices: + filtered_ground_contacts[frame_idx] = [] + prev_frame = ground_contacts[frame_idx - 1] + current_frame = ground_contacts[frame_idx] + next_frame = ground_contacts[frame_idx + 1] + for joint in joints: + if joint in current_frame: + if joint not in prev_frame and joint not in next_frame: + continue + else: + filtered_ground_contacts[frame_idx].append(joint) + return filtered_ground_contacts + + def generate_from_graph_walk(self, motion_vector): + """ the interpolation range must start at end_frame-1 because this is the last modified frame """ + self.position_constraint_buffer = dict() + self.orientation_constraint_buffer = dict() + + constraints = dict() + for frame_idx in range(motion_vector.n_frames): + self.position_constraint_buffer[frame_idx] = dict() + self.orientation_constraint_buffer[frame_idx] = dict() + constraints[frame_idx] = [] + + ground_contacts = collections.OrderedDict() + blend_ranges = dict() + blend_ranges[self.right_foot] = [] + blend_ranges[self.left_foot] = [] + graph_walk = motion_vector.graph_walk + for idx, step in enumerate(graph_walk.steps): + step_length = step.end_frame - step.start_frame + if step_length <= 0: + print("small frame range ", idx, step.node_key, step.start_frame, step.end_frame) + + continue + if step.node_key[0] in LOCOMOTION_ACTIONS: + step_plant_range = get_plant_frame_range_using_search(self.skeleton, motion_vector, step, self.foot_definitions, + self.foot_lift_search_window, self.foot_lift_tolerance) + leg_state_model = create_leg_state_model(step_plant_range, step.start_frame, step.end_frame, self.foot_definitions) + step_ground_contacts = convert_plant_range_to_ground_contacts(step.start_frame, step.end_frame, step_plant_range) + ground_contacts.update(step_ground_contacts) + for frame_idx, joint_names in list(step_ground_contacts.items()): + constraints[frame_idx] = self.generate_grounding_constraints(motion_vector.frames, frame_idx, + joint_names) + + self.set_smoothing_constraints(motion_vector.frames, constraints) + + n_frames = len(motion_vector.frames) + blend_ranges = self.generate_blend_ranges(constraints, n_frames) + + return constraints, blend_ranges, ground_contacts + + def generate(self, motion_vector, ground_contacts=None): + self.position_constraint_buffer = dict() + self.orientation_constraint_buffer = dict() + + constraints = dict() + if ground_contacts is None: + ground_contacts = self.detect_ground_contacts(motion_vector.frames, self.contact_joints) + # generate constraints + for frame_idx, joint_names in enumerate(ground_contacts): + constraints[frame_idx] = self.generate_grounding_constraints(motion_vector.frames, frame_idx, joint_names) + + self.set_smoothing_constraints(motion_vector.frames, constraints) + + n_frames = len(motion_vector.frames) + blend_ranges = self.generate_blend_ranges(constraints, n_frames) + return constraints, blend_ranges + + def generate_blend_ranges(self, constraints, n_frames): + blend_ranges = dict() + blend_ranges[self.right_foot] = [] + blend_ranges[self.left_foot] = [] + + state = dict() + state[self.right_foot] = -1 + state[self.left_foot] = -1 + + joint_names = [self.right_foot, self.left_foot] + + for frame_idx in range(n_frames): + if frame_idx in constraints: + for j in joint_names: + constrained_joints = [c.joint_name for c in constraints[frame_idx]] + if j in constrained_joints: + if state[j] == -1: + # start new constrained range + blend_ranges[j].append([frame_idx, n_frames]) + state[j] = len(blend_ranges[j])-1 + else: + # update constrained range + idx = state[j] + blend_ranges[j][idx][1] = frame_idx + else: + state[j] = -1 # stop constrained range no constraint defined + else: + for c in joint_names: + state[c.joint_name] = -1 # stop constrained range no constraint defined + return blend_ranges + + def generate_grounding_constraints(self, frames, frame_idx, joint_names): + self.position_constraint_buffer[frame_idx] = dict() + self.orientation_constraint_buffer[frame_idx] = dict() + + new_constraints = [] + end_frame = frame_idx + self.constraint_generation_range + for side in self.foot_definitions: + foot_joints = self.foot_definitions[side] + c = None + if foot_joints["heel"] in joint_names and foot_joints["toe"] in joint_names: + c = self.generate_grounding_constraint_from_heel_and_toe(frames, foot_joints["ankle"], foot_joints["heel"], foot_joints["toe"], frame_idx, end_frame) + elif foot_joints["heel"] in joint_names: + c = self.generate_grounding_constraint_from_heel(frames, foot_joints["ankle"], foot_joints["heel"], frame_idx, end_frame) + elif foot_joints["toe"] in joint_names: + c = self.generate_grounding_constraint_from_toe(frames, foot_joints["ankle"], foot_joints["toe"], frame_idx, end_frame) + if c is not None: + c.heel_offset = self.skeleton.nodes[foot_joints["heel"]].offset + new_constraints.append(c) + return new_constraints + + def generate_grounding_constraint_from_heel_and_toe(self, frames, ankle_joint_name, heel_joint_name, toe_joint_name, frame_idx, end_frame): + """ create constraint on ankle position and orientation """ + # get target global ankle orientation based on the direction between grounded heel and toe + ct = self.get_previous_joint_position_from_buffer(frames, frame_idx, end_frame, toe_joint_name) + ct[1] = self.scene_interface.get_height(ct[0], ct[2]) + ch = self.get_previous_joint_position_from_buffer(frames, frame_idx, end_frame, heel_joint_name) + ch[1] = self.scene_interface.get_height(ct[0], ct[2]) + target_direction = normalize(ct - ch) + t = self.skeleton.nodes[toe_joint_name].get_global_position(frames[frame_idx]) + h = self.skeleton.nodes[heel_joint_name].get_global_position(frames[frame_idx]) + original_direction = normalize(t-h) + + global_delta_q = quaternion_from_vector_to_vector(original_direction, target_direction) + global_delta_q = normalize(global_delta_q) + + m = self.skeleton.nodes[ankle_joint_name].get_global_matrix(frames[frame_idx]) + m[:3, 3] = [0, 0, 0] + oq = quaternion_from_matrix(m) + oq = normalize(oq) + orientation = normalize(quaternion_multiply(global_delta_q, oq)) + + self.orientation_constraint_buffer[frame_idx][ankle_joint_name] = orientation # save orientation to buffer + + # set target ankle position based on the grounded heel and the global target orientation of the ankle + m = quaternion_matrix(orientation)[:3,:3] + target_heel_offset = np.dot(m, self.skeleton.nodes[heel_joint_name].offset) + ca = ch - target_heel_offset + #print "set ankle constraint both", ch, ca, target_heel_offset + constraint = MotionGroundingConstraint(frame_idx, ankle_joint_name, ca, None, orientation) + constraint.toe_position = ct + constraint.heel_position = ch + return constraint + + def generate_grounding_constraint_from_heel(self, frames, ankle_joint_name, heel_joint_name, frame_idx, end_frame): + """ create constraint on the ankle position without an orientation constraint""" + #print "add ankle constraint from heel" + ch = self.get_previous_joint_position_from_buffer(frames, frame_idx, end_frame, heel_joint_name) + a = self.skeleton.nodes[ankle_joint_name].get_global_position(frames[frame_idx]) + h = self.skeleton.nodes[heel_joint_name].get_global_position(frames[frame_idx]) + ch[1] = self.scene_interface.get_height(ch[0], ch[2]) # set heel constraint on the ground + target_heel_offset = a - h # difference between unmodified heel and ankle + ca = ch + target_heel_offset # move ankle so heel is on the ground + #print "set ankle constraint single", ch, ca, target_heel_offset + constraint = MotionGroundingConstraint(frame_idx, ankle_joint_name, ca, None, None) + constraint.heel_position = ch + return constraint + + def generate_grounding_constraint_from_toe(self, frames, ankle_joint_name, toe_joint_name, frame_idx, end_frame): + """ create a constraint on the ankle position based on the toe constraint position""" + #print "add toe constraint" + ct = self.get_previous_joint_position_from_buffer(frames, frame_idx, end_frame, toe_joint_name) + ct[1] = self.scene_interface.get_height(ct[0], ct[2]) # set toe constraint on the ground + a = self.skeleton.nodes[ankle_joint_name].get_global_position(frames[frame_idx]) + t = self.skeleton.nodes[toe_joint_name].get_global_position(frames[frame_idx]) + + target_toe_offset = a - t # difference between unmodified toe and ankle at the frame + ca = ct + target_toe_offset # move ankle so toe is on the ground + constraint = MotionGroundingConstraint(frame_idx, ankle_joint_name, ca, None, None) + + constraint.toe_position = ct + constraint.global_toe_offset = target_toe_offset + return constraint + + def set_smoothing_constraints(self, frames, constraints): + """ Set orientation constraints to singly constrained frames to smooth the foot orientation (Section 4.2) + """ + for frame_idx in constraints: + for constraint in constraints[frame_idx]: + if constraint.joint_name not in self.orientation_constraint_buffer[frame_idx]: # singly constrained + self.set_smoothing_constraint(frames, constraint, frame_idx, self.smoothing_constraints_window) + + def set_smoothing_constraint(self, frames, constraint, frame_idx, window): + backward_hit = None + forward_hit = None + + start_window = max(frame_idx - window, 0) + end_window = min(frame_idx + window, len(frames)) + + # look backward + search_window = list(range(start_window, frame_idx)) + for f in reversed(search_window): + if constraint.joint_name in self.orientation_constraint_buffer[f]: + backward_hit = f + break + # look forward + for f in range(frame_idx, end_window): + if constraint.joint_name in self.orientation_constraint_buffer[f]: + forward_hit = f + break + + # update q + oq = get_global_orientation(self.skeleton, constraint.joint_name, frames[frame_idx]) + if backward_hit is not None and constraint.toe_position is not None: + bq = self.orientation_constraint_buffer[backward_hit][constraint.joint_name] + j = frame_idx - backward_hit + t = float(j + 1) / (window + 1) + global_q = normalize(quaternion_slerp(bq, oq, t, spin=0, shortestpath=True)) + constraint.orientation = normalize(global_q) + delta = get_quat_delta(oq, global_q) + delta = normalize(delta) + # rotate stored vector by global delta + if constraint.joint_name == self.left_foot: + + constraint.position = regenerate_ankle_constraint_with_new_orientation2(constraint.toe_position, + constraint.global_toe_offset, + delta) + else: + constraint.position = regenerate_ankle_constraint_with_new_orientation2(constraint.toe_position, + constraint.global_toe_offset, + delta) + + elif forward_hit is not None and constraint.heel_position is not None: + k = forward_hit - frame_idx + t = float(k + 1) / (window + 1) + fq = self.orientation_constraint_buffer[forward_hit][constraint.joint_name] + global_q = normalize(quaternion_slerp(oq, fq, t, spin=0, shortestpath=True)) + constraint.orientation = normalize(global_q) + constraint.position = regenerate_ankle_constraint_with_new_orientation(constraint.heel_position, + constraint.heel_offset, + constraint.orientation) + + def get_previous_joint_position_from_buffer(self, frames, frame_idx, end_frame, joint_name): + """ Gets the joint position of the previous frame from the buffer if it exists. + otherwise the position is calculated for the current frame and updated in the buffer + """ + prev_frame_idx = frame_idx - 1 + prev_p = self.get_joint_position_from_buffer(prev_frame_idx, joint_name) + if prev_p is not None: + self.position_constraint_buffer[frame_idx][joint_name] = prev_p + return prev_p + else: + self.update_joint_position_in_buffer(frames, frame_idx, end_frame, joint_name) + p = self.position_constraint_buffer[frame_idx][joint_name] + p[1] = self.scene_interface.get_height(p[0], p[2]) + #print "joint constraint",joint_name, p + return p + + def get_joint_position_from_buffer(self, frame_idx, joint_name): + if frame_idx not in self.position_constraint_buffer: + return None + if joint_name not in self.position_constraint_buffer[frame_idx]: + return None + return self.position_constraint_buffer[frame_idx][joint_name] + + def update_joint_position_in_buffer(self, frames, frame_idx, end_frame, joint_name): + end_frame = min(end_frame, frames.shape[0]) + if frame_idx not in self.position_constraint_buffer: + self.position_constraint_buffer[frame_idx] = dict() + if joint_name not in self.position_constraint_buffer[frame_idx]: + p = get_average_joint_position(self.skeleton, frames, joint_name, frame_idx, end_frame) + #p[1] = self.scene_interface.get_height(p[0], p[2]) + #print "add", joint_name, p, frame_idx, end_frame + self.position_constraint_buffer[frame_idx][joint_name] = p + + def generate_ankle_constraints_legacy(self, frames, frame_idx, joint_names, prev_constraints, prev_joint_names): + end_frame = frame_idx + 10 + new_constraints = dict() + temp_constraints = {"left": None, "right": None} + if self.right_heel and self.right_toe in joint_names: + if self.right_heel and self.right_toe in prev_joint_names: + temp_constraints["right"] = prev_constraints["right"] + else: + temp_constraints["right"] = self.create_ankle_constraints_from_heel_and_toe(frames, self.right_foot, self.right_heel, frame_idx, end_frame) + if self.left_heel and self.left_toe in joint_names: + if joint_names == prev_joint_names: + temp_constraints["left"] = prev_constraints["left"] + else: + temp_constraints["left"] = self.create_ankle_constraints_from_heel_and_toe(frames, self.left_foot,self.left_heel, frame_idx, end_frame) + for side in temp_constraints: + new_constraints[side] = temp_constraints[side][0] + return new_constraints + + def create_ankle_constraints_from_heel_and_toe(self, frames, ankle_joint_name, heel_joint_name, start_frame, end_frame): + """ create constraint on ankle position and orientation """ + constraints = dict() + ct = get_average_joint_position(self.skeleton, frames, heel_joint_name, start_frame, end_frame) + ct[1] = self.scene_interface.get_height(ct[0], ct[2]) + pa = get_average_joint_position(self.skeleton, frames, ankle_joint_name, start_frame, end_frame) + ph = get_average_joint_position(self.skeleton, frames, heel_joint_name, start_frame, end_frame) + delta = ct - ph + ca = pa + delta + avg_direction = None + if len(self.skeleton.nodes[ankle_joint_name].children) > 0: + child_joint_name = self.skeleton.nodes[ankle_joint_name].children[0].node_name + avg_direction = get_average_joint_direction(self.skeleton, frames, ankle_joint_name, child_joint_name, + start_frame, end_frame) + #print "constraint ankle at",pa, ct,ca, start_frame, end_frame, avg_direction + for frame_idx in range(start_frame, end_frame): + c = MotionGroundingConstraint(frame_idx, ankle_joint_name, ca, avg_direction) + constraints[frame_idx] = [] + constraints[frame_idx].append(c) + return constraints + + diff --git a/build/lib/anim_utils/motion_editing/hybrit_ik.py b/build/lib/anim_utils/motion_editing/hybrit_ik.py new file mode 100644 index 0000000..94884b4 --- /dev/null +++ b/build/lib/anim_utils/motion_editing/hybrit_ik.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +""" Hybrit IK using numerical IK based on an exponential displacement map and analytical IK +based on Section 5.1 of [1] + +Use current configuration as q_0 +Find v such that (q_i = q_0_i exp(v_i) for 0 self.success_threshold and iter_counter < self.max_retries: + r = minimize(self._objective, guess, args=(self.skeleton, reference, constraints, self.joint_weights, self.analytical_ik), + method=self.ik_settings["optimization_method"], + tol=self.ik_settings["tolerance"], + options=self._optimization_options) + guess = r["x"] + error = self._objective(guess, self.skeleton, reference, constraints, self.joint_weights, self.analytical_ik) + iter_counter += 1 + + exp_frame = guess + return exp_frame + + def modify_frame(self, reference, constraints): + exp_frame = self.run(reference, constraints) + d = convert_exp_frame_to_quat_frame(self.skeleton, exp_frame) + q_frame = add_quat_frames(self.skeleton, reference, d) + for c in constraints: + if c.joint_name in self.analytical_ik: + q_frame = self.analytical_ik[c.joint_name].apply(q_frame, c.position, c.orientation) + return q_frame diff --git a/build/lib/anim_utils/motion_editing/ik_constraints.py b/build/lib/anim_utils/motion_editing/ik_constraints.py new file mode 100644 index 0000000..4894ec1 --- /dev/null +++ b/build/lib/anim_utils/motion_editing/ik_constraints.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import numpy as np + +SPATIAL_CONSTRAINT_TYPE_KEYFRAME_POSITION = "keyframe_position" +SPATIAL_CONSTRAINT_TYPE_TWO_HAND_POSITION = "keyframe_two_hands" +SPATIAL_CONSTRAINT_TYPE_KEYFRAME_RELATIVE_POSITION = "keyframe_relative_position" +SPATIAL_CONSTRAINT_TYPE_KEYFRAME_LOOK_AT = "keyframe_look_at" +SUPPORTED_CONSTRAINT_TYPES = [SPATIAL_CONSTRAINT_TYPE_KEYFRAME_POSITION, + SPATIAL_CONSTRAINT_TYPE_TWO_HAND_POSITION, + SPATIAL_CONSTRAINT_TYPE_KEYFRAME_LOOK_AT, + SPATIAL_CONSTRAINT_TYPE_KEYFRAME_RELATIVE_POSITION] +class IKConstraint(object): + @staticmethod + def evaluate(params, data): + pass + + +class JointIKConstraint(IKConstraint): + def __init__(self, joint_name, position, orientation, keyframe, free_joints, step_idx=-1, frame_range=None, look_at=False, optimize=True, offset=None, look_at_pos=None): + self.joint_name = joint_name + self.position = position + self.orientation = orientation + self.keyframe = keyframe + self.frame_range = frame_range + self.free_joints = free_joints + self.step_idx = step_idx + self.look_at = look_at + self.optimize = optimize + self.offset = offset + self.look_at_pos = look_at_pos + # set in case it is a relative constraint + self.relative_parent_joint_name = None # joint the offsets points from to the target + self.relative_offset = None + + # tool orientation constraint + self.src_tool_cos = None # tool coordinate system + self.dest_tool_cos = None # target direction + + # set a fk chain root to reduce the degrees of freedom + self.fk_chain_root = None + + + @staticmethod + def evaluate(parameters, data): + pose, free_joints, target_joint, target_position, target_orientation, offset = data + pose.set_channel_values(parameters, free_joints) + #parent_joint = pose.get_parent_joint(target_joint) + #pose.apply_bounds_on_joint(parent_joint) + if target_orientation is not None: + parent_joint = pose.get_parent_joint(target_joint) + if parent_joint is not None: + pose.set_hand_orientation(parent_joint, target_orientation) + #pose.apply_bounds_on_joint(parent_joint) + if offset is not None: + m = pose.evaluate_matrix(target_joint) + p = np.dot(m, offset)[:3] + d = target_position - p + else: + d = pose.evaluate_position(target_joint) - target_position + return np.dot(d, d) + + def data(self, ik, free_joints=None): + if free_joints is None: + free_joints = self.free_joints + if ik.optimize_orientation: + orientation = self.orientation + else: + orientation = None + return ik.pose, free_joints, self.joint_name, self.position, orientation, self.offset + + def get_joint_names(self): + return [self.joint_name] + +class RelativeJointIKConstraint(IKConstraint): + def __init__(self, ref_joint_name, target_joint_name, rel_position, keyframe, free_joints, step_idx=-1, frame_range=None): + self.ref_joint_name = ref_joint_name + self.target_joint_name = target_joint_name + self.rel_position = rel_position + self.keyframe = keyframe + self.frame_range = frame_range + self.free_joints = free_joints + self.step_idx = step_idx + + @staticmethod + def evaluate(parameters, data): + pose, free_joints, ref_joint, target_joint, target_delta = data + pose.set_channel_values(parameters, free_joints) + ref_matrix = pose.skeleton.nodes[ref_joint].get_global_matrix(pose.get_vector()) + target = np.dot(ref_matrix, target_delta)[:3] + d = pose.evaluate_position(target_joint) - target + return np.dot(d, d) + + def data(self, ik, free_joints=None): + if free_joints is None: + free_joints = self.free_joints + return ik.pose, free_joints, self.ref_joint_name, self.target_joint_name, self.rel_position + + def get_joint_names(self): + return [self.target_joint_name] + + +class TwoJointIKConstraint(IKConstraint): + def __init__(self, joint_names, target_positions, target_center, target_delta, target_direction, keyframe, free_joints): + self.joint_names = joint_names + self.target_positions = target_positions + self.target_center = target_center + self.target_delta = target_delta + self.target_direction = target_direction + self.keyframe = keyframe + self.free_joints = free_joints + + @staticmethod + def evaluate(parameters, data): + pose, free_joints, joint_names, target_positions, target_center, target_delta, target_direction = data + pose.set_channel_values(parameters, free_joints) + left = pose.evaluate_position(joint_names[0]) + right = pose.evaluate_position(joint_names[1]) + delta_vector = right - left + residual_vector = [0.0, 0.0, 0.0] + #get distance to center + residual_vector[0] = np.linalg.norm(target_center - (left + 0.5 * delta_vector)) + #get difference to distance between hands + delta = np.linalg.norm(delta_vector) + residual_vector[1] = abs(target_delta - delta) + #print "difference", residual_vector[1] + #get difference of global orientation + direction = delta_vector/delta + + residual_vector[2] = abs(target_direction[0] - direction[0]) + \ + abs(target_direction[1] - direction[1]) + \ + abs(target_direction[2] - direction[2]) + residual_vector[2] *= 10.0 + + #print (target_center, (left + 0.5 * delta_vector), left, right) + #error = np.linalg.norm(left-target_positions[0]) + np.linalg.norm(right-target_positions[1]) + return sum(residual_vector)#error#residual_vector[0]#sum(residual_vector) + + def data(self, ik, free_joints=None): + if free_joints is None: + free_joints = self.free_joints + return ik.pose, free_joints, self.joint_names, self.target_positions, self.target_center, self.target_delta, self.target_direction + + def get_joint_names(self): + return self.joint_names + + diff --git a/build/lib/anim_utils/motion_editing/ik_constraints_builder.py b/build/lib/anim_utils/motion_editing/ik_constraints_builder.py new file mode 100644 index 0000000..7d3cde8 --- /dev/null +++ b/build/lib/anim_utils/motion_editing/ik_constraints_builder.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import collections +from copy import deepcopy +import numpy as np +from transformations import quaternion_from_matrix +from anim_utils.motion_editing.motion_editing import KeyframeConstraint +from .ik_constraints import SPATIAL_CONSTRAINT_TYPE_KEYFRAME_POSITION, SPATIAL_CONSTRAINT_TYPE_KEYFRAME_RELATIVE_POSITION, SUPPORTED_CONSTRAINT_TYPES + +class IKConstraintsBuilder(object): + def __init__(self, skeleton, force_orientation=False): + self.skeleton = skeleton + self.force_orientation = force_orientation + + def convert_to_ik_constraints(self, constraints, frame_offset=0, time_function=None, constrain_orientation=True): + ik_constraints = collections.OrderedDict() + for c in constraints: + if c.constraint_type not in SUPPORTED_CONSTRAINT_TYPES or "generated" in c.semantic_annotation.keys(): + print("skip unsupported constraint") + continue + start_frame_idx = self.get_global_frame_idx(c.canonical_keyframe, frame_offset, time_function) + if c.canonical_end_keyframe is not None: + print("apply ik constraint on region") + end_frame_idx = self.get_global_frame_idx(c.canonical_end_keyframe, frame_offset, time_function) + else: + print("no end keyframe defined") + end_frame_idx = start_frame_idx+1 + + for frame_idx in range(start_frame_idx, end_frame_idx): + ik_constraint = self.convert_mg_constraint_to_ik_constraint(frame_idx, c, constrain_orientation) + + ik_constraint.src_tool_cos = c.src_tool_cos + ik_constraint.dest_tool_cos = c.dest_tool_cos + + ik_constraint.orientation = None + if c.orientation is not None: + if c.constrain_orientation_in_region: + if start_frame_idx < frame_idx and frame_idx < end_frame_idx -1: + ik_constraint.inside_region_orientation = True + elif frame_idx == start_frame_idx: + ik_constraint.inside_region_orientation = True + ik_constraint.keep_orientation = True + elif frame_idx == start_frame_idx: + ik_constraint.orientation = c.orientation + + + ik_constraint.position = None + if c.position is not None: + if c.constrain_position_in_region: + ik_constraint.inside_region_position = False + if start_frame_idx < frame_idx and frame_idx < end_frame_idx -1: + ik_constraint.inside_region_position = True + elif frame_idx == end_frame_idx-1: + ik_constraint.end_of_region = True + elif frame_idx == start_frame_idx: + ik_constraint.position = c.position + + if ik_constraint is not None: + if frame_idx not in ik_constraints: + ik_constraints[frame_idx] = dict() + if c.joint_name not in ik_constraints[frame_idx]: + ik_constraints[frame_idx][c.joint_name] = [] + ik_constraints[frame_idx][c.joint_name] = ik_constraint + + return ik_constraints + + def get_global_frame_idx(self, mp_frame_idx, frame_offset, time_function): + if time_function is not None: + frame_idx = frame_offset + int(time_function[mp_frame_idx]) + 1 + else: + frame_idx = frame_offset + int(mp_frame_idx) + return frame_idx + + def convert_mg_constraint_to_ik_constraint(self, frame_idx, mg_constraint, constrain_orientation=False): + if mg_constraint.constraint_type == SPATIAL_CONSTRAINT_TYPE_KEYFRAME_POSITION: + ik_constraint = self._create_keyframe_ik_constraint(mg_constraint, frame_idx, constrain_orientation, look_at=mg_constraint.look_at) + elif mg_constraint.constraint_type == SPATIAL_CONSTRAINT_TYPE_KEYFRAME_RELATIVE_POSITION: + ik_constraint = KeyframeConstraint(frame_idx, mg_constraint.joint_name, mg_constraint.position, None, mg_constraint.look_at, mg_constraint.offset) + else: + ik_constraint = None + return ik_constraint + + def _create_keyframe_ik_constraint(self, constraint, keyframe, constrain_orientation, look_at): + if constrain_orientation: + orientation = constraint.orientation + else: + orientation = None + return KeyframeConstraint(keyframe, constraint.joint_name, constraint.position, orientation, look_at) + + def generate_relative_constraint(self, keyframe, frame, joint_name, relative_joint_name): + joint_pos = self.skeleton.nodes[joint_name].get_global_position(frame) + rel_joint_pos = self.skeleton.nodes[relative_joint_name].get_global_position(frame) + #create a keyframe constraint but indicate that it is a relative constraint + ik_constraint = KeyframeConstraint(keyframe, joint_name, None, None, None) + ik_constraint.relative_parent_joint_name = relative_joint_name + ik_constraint.relative_offset = joint_pos - rel_joint_pos + return ik_constraint + + def generate_mirror_constraint(self, keyframe, ref_frame, frame, mirror_joint_name): + """ generate a constraint on the mirror joint with the similar offset to the root as in the reference frame""" + ref_mirror_joint_pos = self.skeleton.nodes[mirror_joint_name].get_global_position(ref_frame) + ref_root_joint_pos = self.skeleton.nodes[self.skeleton.root].get_global_position(ref_frame) + ref_offset_from_root = ref_mirror_joint_pos-ref_root_joint_pos + + target_root_joint_pos = self.skeleton.nodes[self.skeleton.root].get_global_position(frame) + mirror_joint_pos = ref_offset_from_root + target_root_joint_pos + print("generate mirror constraint on",mirror_joint_name, mirror_joint_pos, ref_mirror_joint_pos) + #create a keyframe constraint and set the original position as constraint + ik_constraint = KeyframeConstraint(keyframe, mirror_joint_name, mirror_joint_pos, None, None) + return ik_constraint + + + def generate_orientation_constraint_from_frame(self, frame, joint_name): + if joint_name in self.skeleton.nodes: + m = self.skeleton.nodes[joint_name].get_global_matrix(frame)#[:3,:3] + return quaternion_from_matrix(m) + + def convert_to_ik_constraints_with_relative(self, frames, constraints, frame_offset=0, time_function=None, constrain_orientation=True): + """ Create an orderered dictionary containing for each constrained frame, a KeyframeConstraint for the constrained joint""" + ik_constraints = collections.OrderedDict() + for c in constraints: + if c.constraint_type not in SUPPORTED_CONSTRAINT_TYPES or "generated" in c.semantic_annotation.keys(): + print("skip unsupported constraint") + continue + start_frame_idx = self.get_global_frame_idx(c.canonical_keyframe, frame_offset, time_function) + if c.canonical_end_keyframe is not None: + print("apply ik constraint on region") + end_frame_idx = self.get_global_frame_idx(c.canonical_end_keyframe, frame_offset, time_function) + else: + print("no end keyframe defined") + end_frame_idx = start_frame_idx + 1 + if c.orientation is None and self.force_orientation: + c.orientation = self.generate_orientation_constraint_from_frame(frames[start_frame_idx], c.joint_name) + + base_ik_constraint = self.convert_mg_constraint_to_ik_constraint(start_frame_idx, c, constrain_orientation) + + relative_ik_constraint = None + if base_ik_constraint is None: + continue + base_ik_constraint.src_tool_cos = c.src_tool_cos + base_ik_constraint.dest_tool_cos = c.dest_tool_cos + for frame_idx in range(start_frame_idx, end_frame_idx): + ik_constraint = deepcopy(base_ik_constraint) + if frame_idx not in ik_constraints: + ik_constraints[frame_idx] = dict() + + ik_constraint.orientation = None + if c.orientation is not None and constrain_orientation: + if c.constrain_orientation_in_region: + ik_constraint.orientation = c.orientation + if start_frame_idx < frame_idx < end_frame_idx -1: + ik_constraint.inside_region_orientation = True + elif frame_idx == start_frame_idx: + ik_constraint.inside_region_orientation = True + ik_constraint.keep_orientation = True + elif frame_idx == start_frame_idx: + ik_constraint.orientation = c.orientation + + ik_constraint.position = None + if c.position is not None: + if c.constrain_position_in_region and c.relative_joint_name is None:# ignore region if it is relative + ik_constraint.inside_region_position = False + ik_constraint.position = c.position + if start_frame_idx < frame_idx < end_frame_idx -1: + ik_constraint.inside_region_position = True + elif frame_idx == end_frame_idx-1: + ik_constraint.end_of_region = True + elif frame_idx == start_frame_idx: + ik_constraint.position = c.position + # overwrite with relative constraint if larger than start frame + if c.relative_joint_name is not None and frame_idx == start_frame_idx: + relative_ik_constraint = self.generate_relative_constraint(frame_idx, frames[frame_idx], + c.joint_name, + c.relative_joint_name) + relative_ik_constraint.orientation = ik_constraint.orientation + ik_constraints[frame_idx][c.joint_name] = ik_constraint + + elif relative_ik_constraint is not None and frame_idx > start_frame_idx: + relative_ik_constraint.frame_idx = frame_idx + ik_constraints[frame_idx][c.joint_name] = relative_ik_constraint + else: + ik_constraints[frame_idx][c.joint_name] = ik_constraint + + # add also a mirror constraint + if c.mirror_joint_name is not None: + _ik_constraint = self.generate_mirror_constraint(frame_idx, frames[0], frames[frame_idx], c.mirror_joint_name) + ik_constraints[frame_idx][c.mirror_joint_name] = _ik_constraint + + # add also a parent constraint + if c.constrained_parent is not None and c.vector_to_parent is not None: + print("generate parent constraint", c.vector_to_parent) + # "get length of vector and calculate constraint position" + a = self.skeleton.nodes[c.joint_name].get_global_position(frames[0]) + b = self.skeleton.nodes[c.constrained_parent].get_global_position(frames[0]) + bone_length = np.linalg.norm(b-a) + c.vector_to_parent /= np.linalg.norm(c.vector_to_parent) + parent_pos = c.position + c.vector_to_parent * bone_length + + #create a keyframe constraint but indicate that it is a relative constraint + _ik_constraint = KeyframeConstraint(frame_idx, c.constrained_parent, parent_pos, None, None) + + + #remove degrees of freedom of the child constraint + ik_constraints[frame_idx][c.joint_name].fk_chain_root = c.constrained_parent + + #change order so that the parent constraint is applied first + # TODO order constraints based on joint order in fk chain + reversed_ordered_dict = collections.OrderedDict() + reversed_ordered_dict[c.constrained_parent] = _ik_constraint + for k, v in ik_constraints[frame_idx].items(): + reversed_ordered_dict[k] = v + ik_constraints[frame_idx] = reversed_ordered_dict + return ik_constraints diff --git a/build/lib/anim_utils/motion_editing/motion_editing.py b/build/lib/anim_utils/motion_editing/motion_editing.py new file mode 100644 index 0000000..82260fa --- /dev/null +++ b/build/lib/anim_utils/motion_editing/motion_editing.py @@ -0,0 +1,900 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +from copy import copy +import numpy as np +import collections +from transformations import quaternion_matrix, euler_from_matrix, quaternion_multiply, quaternion_matrix, quaternion_from_matrix +from .numerical_ik_quat import NumericalInverseKinematicsQuat +from .numerical_ik_exp import NumericalInverseKinematicsExp +from .skeleton_pose_model import SkeletonPoseModel +from .cubic_motion_spline import CubicMotionSpline, get_quaternion_delta +from ..animation_data.motion_blending import smooth_joints_around_transition_using_slerp, create_transition_using_slerp, \ + smooth_quaternion_frames, create_transition_for_joints_using_slerp, \ + BLEND_DIRECTION_BACKWARD, BLEND_DIRECTION_FORWARD, smooth_translation_in_quat_frames +from ..utilities.log import write_message_to_log, LOG_MODE_DEBUG +from .utils import convert_exp_frame_to_quat_frame +from .fabrik_chain import FABRIKChain, FABRIKBone +from ..animation_data.skeleton import LOOK_AT_DIR, SPINE_LOOK_AT_DIR + +SPATIAL_CONSTRAINT_TYPE_KEYFRAME_POSITION = "keyframe_position" +SPATIAL_CONSTRAINT_TYPE_KEYFRAME_RELATIVE_POSITION = "keyframe_relative_position" +SUPPORTED_CONSTRAINT_TYPES = [SPATIAL_CONSTRAINT_TYPE_KEYFRAME_POSITION, SPATIAL_CONSTRAINT_TYPE_KEYFRAME_RELATIVE_POSITION] + + + +def create_fabrik_chain(skeleton, frame, node_order, activate_constraints=False): + bones = dict() + root = node_order[0] + root_offset = skeleton.nodes[root].get_global_position(frame) + frame_offset = skeleton.animated_joints.index(root)*4 + 3 + for idx, j in enumerate(node_order[:-1]): + bones[j] = FABRIKBone(j, node_order[idx + 1]) + if idx == 0: + bones[j].is_root = True + else: + bones[j].is_root = False + + bones[node_order[-1]] = FABRIKBone(node_order[-1], None) + max_iter = 50 + chain = FABRIKChain(skeleton, bones, node_order, max_iter=max_iter, frame_offset=frame_offset, root_offset=root_offset, + activate_constraints=activate_constraints) + return chain + +def add_frames(skeleton, a, b): + """ returns c = a + b""" + c = np.zeros(len(a)) + c[:3] = a[:3] + b[:3] + for idx, j in enumerate(skeleton.animated_joints): + o = idx * 4 + 3 + q_a = a[o:o + 4] + q_b = b[o:o + 4] + q_prod = quaternion_multiply(q_a, q_b) + c[o:o + 4] = q_prod / np.linalg.norm(q_prod) + return c + +def add_reduced_frames(skeleton, a, delta, joints): + """ returns c = a + delta where delta are the parameters of the joints list""" + c = np.array(a) + o = 0 + for idx, j in enumerate(skeleton.animated_joints): + if j not in joints: + continue + if j == skeleton.root: + dest = 0 + c[:3] = a[dest:dest+3] + delta[o:o+3] + q_dest = dest+3 + q_o = o+3 + q_a = a[q_dest:q_dest + 4] + q_delta = delta[q_o:q_o + 4] + + q_prod = quaternion_multiply(q_a, q_delta) + c[q_dest:q_dest + 4] = q_prod / np.linalg.norm(q_prod) + o += 7 + else: + dest = idx* 4 + 3 + + q_a = a[dest:dest + 4] + q_delta = delta[o:o + 4] + + q_prod = quaternion_multiply(q_a, q_delta) + c[dest:dest + 4] = q_prod / np.linalg.norm(q_prod) + o += 4 + return c + +def substract_frames(skeleton, a, b): + """ returns c = a - b""" + c = np.zeros(len(a)) + c[:3] = a[:3] - b[:3] + for idx, j in enumerate(skeleton.animated_joints): + o = idx*4 + 3 + q_a = a[o:o+4] + q_b = b[o:o+4] + if np.dot(q_a, q_b) < 0: + q_a *= -1 + q_delta = get_quaternion_delta(q_a, q_b) + q_delta = q_delta / np.linalg.norm(q_delta) + #dot = np.sum(q_delta) + #if dot < 0: + # q_delta = -q_delta + c[o:o+4] = q_delta + return c + + +class KeyframeConstraint(object): + def __init__(self, frame_idx, joint_name, position, orientation=None, look_at=False, offset=None, look_at_pos=None): + self.frame_idx = frame_idx + self.joint_name = joint_name + self.position = position + self.orientation = orientation + self.look_at = look_at + self.look_at_pos = look_at_pos + self.offset = offset # tool offset + self.inside_region_position = False + self.end_of_region = False + self.inside_region_orientation = False + self.keep_orientation = False + + # set in case it is a has a relative constraint + self.relative_parent_joint_name = None # joint the offsets points from to the target + self.relative_offset = None + + # tool orientation constraint + self.src_tool_cos = None # tool coordinate system + self.dest_tool_cos = None # target direction + + # set a fk chain root to reduce the degrees of freedom + self.fk_chain_root = None + + def instantiate_relative_constraint(self, skeleton, frame): + """ turn relative constraint into a normal constraint""" + ppos = skeleton.nodes[self.relative_parent_joint_name].get_global_position(frame) + pos = ppos + self.relative_offset + return KeyframeConstraint(self.frame_idx, self.joint_name, pos, orientation=self.orientation) + + def evaluate(self, skeleton, frame): + if self.orientation is not None: + parent_joint = skeleton.nodes[self.joint_name].parent + if parent_joint is not None: + m = quaternion_matrix(self.orientation) + parent_m = parent_joint.get_global_matrix(frame, use_cache=False) + local_m = np.dot(np.linalg.inv(parent_m), m) + q = quaternion_from_matrix(local_m) + idx = skeleton.animated_joints.index(parent_joint.node_name) + # idx = skeleton.nodes[c.joint_name].quaternion_frame_index * 4 + frame[idx:idx + 4] = q + if self.offset is not None: + m = skeleton.nodes[self.joint_name].get_global_matrix(frame) + p = np.dot(m, self.offset)[:3] + d = self.position - p + else: + d = self.position - skeleton.nodes[self.joint_name].get_global_position(frame) + return np.dot(d, d) + + +class MotionEditing(object): + def __init__(self, skeleton, ik_settings): + self.skeleton = skeleton + self._ik_settings = ik_settings + self.window = int(self._ik_settings["interpolation_window"]) + self.transition_window = int(self._ik_settings["transition_window"]) + self.verbose = False + self.use_euler = self._ik_settings["use_euler_representation"] + self.solving_method = self._ik_settings["solving_method"] + self.success_threshold = self._ik_settings["success_threshold"] + self.max_retries = int(self._ik_settings["max_retries"]) + self.activate_look_at = self._ik_settings["activate_look_at"] + self.optimize_orientation = self._ik_settings["optimize_orientation"] + self.elementary_action_max_iterations = int(self._ik_settings["elementary_action_max_iterations"]) + self.elementary_action_epsilon = self._ik_settings["elementary_action_optimization_eps"] + self.adapt_hands_during_both_hand_carry = self._ik_settings["adapt_hands_during_carry_both"] + self.pose = SkeletonPoseModel(self.skeleton, self.use_euler) + self._ik = NumericalInverseKinematicsQuat(self.pose, self._ik_settings) + self._ik_exp = NumericalInverseKinematicsExp(self.skeleton, self._ik_settings) + self._fabrik_chains = dict() + + def add_fabrik_chain(self, joint_name, node_order, activate_constraints=False): + self._fabrik_chains[joint_name] = create_fabrik_chain(self.skeleton, self.skeleton.reference_frame, node_order, activate_constraints) + + def add_constraints_to_skeleton(self, joint_constraints): + """ legacy interface""" + self.skeleton.add_constraints(joint_constraints) + + def modify_motion_vector(self, motion_vector): + for idx, action_ik_constraints in enumerate(motion_vector.ik_constraints): + write_message_to_log("Apply IK to elementary action " + str(idx), LOG_MODE_DEBUG) + self._optimize_action_ik_constraints(motion_vector, action_ik_constraints) + + def _optimize_action_ik_constraints(self, motion_vector, action_ik_constraints): + i = 0 + last_error = None + keep_running = True + trajectory_weights = 1.0 + # modify individual keyframes based on constraints + while keep_running: + error = 0.0 + if "trajectories" in list(action_ik_constraints.keys()): + constraints = action_ik_constraints["trajectories"] + c_error = self._modify_motion_vector_using_trajectory_constraint_list(motion_vector, constraints) + error += c_error * trajectory_weights + if "keyframes" in list(action_ik_constraints.keys()): + constraints = action_ik_constraints["keyframes"] + error += self._modify_motion_vector_using_keyframe_constraint_list(motion_vector, constraints) + if last_error is not None: + delta = abs(last_error - error) + else: + delta = np.inf + last_error = error + i += 1 + keep_running = i < self.elementary_action_max_iterations and delta > self.elementary_action_epsilon + write_message_to_log("IK iteration " + str(i) + " " + str(error) + " " + str(delta) + " " + str( + self.elementary_action_epsilon), LOG_MODE_DEBUG) + + def _modify_motion_vector_using_keyframe_constraint_list(self, motion_vector, constraints): + error = 0.0 + for keyframe, keyframe_constraints in list(constraints.items()): + keyframe = int(keyframe) + if "single" in list(keyframe_constraints.keys()): + for c in keyframe_constraints["single"]: + if c.optimize: + if c.frame_range is not None: + error += self._modify_motion_vector_using_keyframe_constraint_range(motion_vector, c, + c.frame_range) + else: + error += self._modify_frame_using_keyframe_constraint(motion_vector, c, keyframe) + start = keyframe + end = keyframe + 1 + if self.activate_look_at and c.look_at: + self._look_at_in_range(motion_vector.frames, c.position, start, end) + print("set hand orientation", c.orientation) + if c.orientation is not None and self.optimize_orientation: + self._set_hand_orientation(motion_vector.frames, c.orientation, c.joint_name, keyframe, start, end) + return error + + def _modify_frame_using_keyframe_constraint(self, motion_vector, constraint, keyframe): + self.set_pose_from_frame(motion_vector.frames[keyframe]) + error = self._ik.modify_pose_general(constraint) + motion_vector.frames[keyframe] = self.pose.get_vector() + if self.window > 0: + self.interpolate_around_keyframe(motion_vector.frames, constraint.get_joint_names(), keyframe, self.window) + return error + + def _modify_motion_vector_using_keyframe_constraint_range(self, motion_vector, constraint, frame_range): + error = 0.0 + for frame in range(frame_range[0], frame_range[1] + 1): + self.set_pose_from_frame(motion_vector.frames[frame]) + error += self._ik.modify_pose_general(constraint) + motion_vector.frames[frame] = self.pose.get_vector() + + self._create_transition_for_frame_range(motion_vector.frames, frame_range[0], frame_range[1], + self.pose.free_joints_map[constraint.joint_name]) + return error + + def interpolate_around_keyframe(self, frames, joint_names, keyframe, window): + write_message_to_log("Smooth and interpolate" + str(joint_names), LOG_MODE_DEBUG) + for target_joint_name in joint_names: + joint_parameter_indices = self._extract_free_parameter_indices(self.pose.free_joints_map[target_joint_name]) + for joint_name in self.pose.free_joints_map[target_joint_name]: + smooth_joints_around_transition_using_slerp(frames, joint_parameter_indices[joint_name], keyframe, window) + + def _look_at_in_range(self, frames, position, start, end): + start = max(0, start) + end = min(frames.shape[0], end) + for idx in range(start, end): + self.set_pose_from_frame(frames[idx]) + self.pose.lookat(position) + frames[idx] = self.pose.get_vector() + self._create_transition_for_frame_range(frames, start, end - 1, [self.pose.head_joint]) + + def _create_transition_for_frame_range(self, frames, start, end, target_joints): + for target_joint in target_joints: + joint_parameter_indices = list(range(*self.pose.extract_parameters_indices(target_joint))) + transition_start = max(start - self.transition_window, 0) + transition_end = min(end + self.transition_window, frames.shape[0]) - 1 + create_transition_using_slerp(frames, transition_start, start, joint_parameter_indices) + create_transition_using_slerp(frames, end, transition_end, joint_parameter_indices) + + def _set_hand_orientation(self, frames, orientation, joint_name, keyframe, start, end): + parent_joint_name = self.pose.get_parent_joint(joint_name) + self.set_pose_from_frame(frames[keyframe]) + self.pose.set_hand_orientation(parent_joint_name, orientation) + start = max(0, start) + end = min(frames.shape[0], end) + self._create_transition_for_frame_range(frames, start, end - 1, [parent_joint_name]) + + def set_pose_from_frame(self, reference_frame): + self.pose.set_pose_parameters(reference_frame) + self.pose.clear_cache() + + def _extract_free_parameter_indices(self, free_joints): + """get parameter indices of joints from reference frame + """ + indices = {} + for joint_name in free_joints: + indices[joint_name] = list(range(*self.pose.extract_parameters_indices(joint_name))) + return indices + + def _modify_motion_vector_using_trajectory_constraint_list(self, motion_vector, constraints): + error = 0.0 + for c in constraints: + if c["fixed_range"]: + error += self._modify_motion_vector_using_trajectory_constraint(motion_vector, c) + else: + error += self._modify_motion_vector_using_trajectory_constraint_search_start(motion_vector, c) + return error + + def _modify_motion_vector_using_trajectory_constraint(self, motion_vector, traj_constraint): + error_sum = 0.0 + d = traj_constraint["delta"] + trajectory = traj_constraint["trajectory"] + start_idx = traj_constraint["start_frame"] + end_idx = traj_constraint["end_frame"] - 1 + end_idx = min(len(motion_vector.frames) - 1, end_idx) + n_frames = end_idx - start_idx + 1 + target_direction = None + if traj_constraint["constrain_orientation"]: + target_direction = trajectory.get_direction() + if np.linalg.norm(target_direction) == 0: + target_direction = None + + full_length = n_frames * d + for idx in range(n_frames): + t = (idx * d) / full_length + target_position = trajectory.query_point_by_parameter(t) + keyframe = start_idx + idx + self.set_pose_from_frame(motion_vector.frames[keyframe]) + error = np.inf + iter_counter = 0 + while error > self.success_threshold and iter_counter < self.max_retries: + error = self._ik.modify_pose(traj_constraint["joint_name"], target_position, target_direction) + iter_counter += 1 + error_sum += error + motion_vector.frames[keyframe] = self.pose.get_vector() + parent_joint = self.pose.get_parent_joint(traj_constraint["joint_name"]) + + if traj_constraint["joint_name"] in list(self.pose.free_joints_map.keys()): + free_joints = self.pose.free_joints_map[traj_constraint["joint_name"]] + free_joints = list(set(free_joints + [parent_joint])) + else: + free_joints = [parent_joint] + self._create_transition_for_frame_range(motion_vector.frames, start_idx, end_idx, free_joints) + return error_sum + + def _modify_motion_vector_using_trajectory_constraint_search_start(self, motion_vector, traj_constraint): + error_sum = 0.0 + trajectory = traj_constraint["trajectory"] + start_target = trajectory.query_point_by_parameter(0.0) + start_idx = self._find_corresponding_frame(motion_vector, + traj_constraint["start_frame"], + traj_constraint["end_frame"], + traj_constraint["joint_name"], + start_target) + n_frames = traj_constraint["end_frame"]-start_idx + 1 + arc_length = 0.0 + self.set_pose_from_frame(motion_vector.frames[start_idx]) + prev_position = self.pose.evaluate_position(traj_constraint["joint_name"]) + for idx in range(n_frames): + keyframe = start_idx+idx + self.set_pose_from_frame(motion_vector.frames[keyframe]) + current_position = self.pose.evaluate_position(traj_constraint["joint_name"]) + arc_length += np.linalg.norm(prev_position-current_position) + prev_position = current_position + if arc_length >= trajectory.full_arc_length: + break + target = trajectory.query_point_by_absolute_arc_length(arc_length) + + error = np.inf + iter_counter = 0 + while error > self.success_threshold and iter_counter < self.max_retries: + error = self._ik.modify_pose(traj_constraint["joint_name"], target) + iter_counter += 1 + error_sum += error + motion_vector.frames[keyframe] = self.pose.get_vector() + + self._create_transition_for_frame_range(motion_vector.frames, start_idx, keyframe-1, self.pose.free_joints_map[traj_constraint["joint_name"]]) + return error_sum + + def _find_corresponding_frame(self, motion_vector, start_idx, end_idx, target_joint, target_position): + closest_start_frame = copy(start_idx) + min_error = np.inf + n_frames = end_idx - start_idx + for idx in range(n_frames): + keyframe = start_idx + idx + self.set_pose_from_frame(motion_vector.frames[keyframe]) + position = self.pose.evaluate_position(target_joint) + error = np.linalg.norm(position - target_position) + if error <= min_error: + min_error = error + closest_start_frame = keyframe + return closest_start_frame + + def generate_zero_frame(self): + n_dims = len(self.skeleton.animated_joints) * 4 + 3 + zero_frame = np.zeros(n_dims) + for j in range(len(self.skeleton.animated_joints)): + o = j * 4 + 3 + zero_frame[o:o + 4] = [1, 0, 0, 0] + return zero_frame + + def generate_delta_frames(self, frames, constraints, influence_range=40): + n_frames = frames.shape[0] + zero_frame = self.generate_zero_frame() + constrained_frames = list(constraints.keys()) + delta_frames = collections.OrderedDict() + delta_frames[0] = zero_frame + delta_frames[1] = zero_frame + for f in range(0, n_frames, influence_range): + delta_frames[f] = zero_frame + delta_frames[f+1] = zero_frame + delta_frames[n_frames - 2] = zero_frame + delta_frames[n_frames - 1] = zero_frame + for frame_idx, frame_constraints in constraints.items(): + # delete zero frames in range around constraint + start = max(frame_idx - influence_range, min(frame_idx, 2)) + end = min(frame_idx + influence_range, max(frame_idx, n_frames - 2)) + for i in range(start, end): + if i in delta_frames and i not in constrained_frames: + del delta_frames[i] + + frame_constraints = list(frame_constraints.values()) + exp_frame = self._ik_exp.run(frames[frame_idx], frame_constraints) + + n_dims = len(self.skeleton.animated_joints) * 4 + 3 + delta_frames[frame_idx] = np.zeros(n_dims) + delta_frames[frame_idx][3:] = convert_exp_frame_to_quat_frame(self.skeleton, exp_frame) + delta_frames = collections.OrderedDict(sorted(delta_frames.items(), key=lambda x: x[0])) + return list(delta_frames.keys()), np.array(list(delta_frames.values())) + + def modify_motion_vector2(self, motion_vector, plot=False): + motion_vector.frames = self.edit_motion_using_displacement_map(motion_vector.frames, motion_vector.ik_constraints, plot=plot) + self.apply_orientation_constraints(motion_vector.frames, motion_vector.ik_constraints) + + def edit_motion_using_displacement_map(self, frames, constraints, influence_range=40, plot=False): + """ References + Witkin and Popovic: Motion Warping, 1995. + Bruderlin and Williams: Motion Signal Processing, 1995. + Lee and Shin: A Hierarchical Approach to Interactive Motion Editing for Human-like Figures, 1999. + """ + d_times, delta_frames = self.generate_delta_frames(frames, constraints, influence_range) + return self.add_delta_curve(frames, d_times, delta_frames, plot=plot) + + def get_reduced_frame(self, frame, joint_list): + n_dims = len(joint_list)*4 + if self.skeleton.root in joint_list: + n_dims += 3 + rf = np.zeros(n_dims) + o = 0 + for idx, j in enumerate(self.skeleton.animated_joints): + if j not in joint_list: + continue + if j == self.skeleton.root: + src = 0 + rf[o:o+7] = frame[src:src+7] + o+=7 + else: + src = idx * 4 + 3 + rf[o:o+4] = frame[src:src+4] + o+=4 + return rf + + def add_delta_curve(self, frames, d_times, delta_frames, plot=False): + n_frames = len(frames) + times = list(range(n_frames)) + d_curve = CubicMotionSpline.fit_frames(self.skeleton, d_times, delta_frames) + new_frames = [] + for t in times: + d_frame = d_curve.evaluate(t) + f = add_frames(self.skeleton, frames[t], d_frame) + new_frames.append(f) + if plot: + t = np.linspace(0, n_frames - 1, num=100, endpoint=True) + d_curve.plot(t) + return np.array(new_frames) + + def add_reduced_delta_curve(self, frames, d_times, delta_frames, joint_list=None, plot=False): + n_frames = len(frames) + times = list(range(n_frames)) + if joint_list is not None: + reduced_delta_frames = [] + for f in delta_frames: + rf = self.get_reduced_frame(f, joint_list) + reduced_delta_frames.append(rf) + reduced_delta_frames = np.array(reduced_delta_frames) + else: + reduced_delta_frames = delta_frames + d_curve = CubicMotionSpline.fit_frames(self.skeleton, d_times, reduced_delta_frames) + new_frames = [] + if joint_list is None: + joint_list = self.skeleton.animated_joints + for t in times: + d_frame = d_curve.evaluate(t) + f = add_reduced_frames(self.skeleton, frames[t], d_frame, joint_list) + new_frames.append(f) + + if plot: + t = np.linspace(0, n_frames - 1, num=100, endpoint=True) + d_curve.plot(t) + return np.array(new_frames) + + def generate_delta_frames_using_ccd(self, frames, constraints, n_max_iter=25, root_joint=None, influence_range=40): + n_frames = frames.shape[0] + zero_frame = self.generate_zero_frame() + constrained_frames = list(constraints.keys()) + delta_frames = collections.OrderedDict() + delta_frames[0] = zero_frame + delta_frames[1] = zero_frame + joint_list = set() + chain_end_joints = dict() + for frame_idx, frame_constraints in constraints.items(): + for joint_name in frame_constraints: + if joint_name not in chain_end_joints: + chain_end_joints[joint_name] = root_joint + for j in self.get_fk_chain(joint_name, root_joint): + joint_list.add(j) + for f in range(0, n_frames, influence_range): + delta_frames[f] = zero_frame + delta_frames[f+1] = zero_frame + delta_frames[n_frames - 2] = zero_frame + delta_frames[n_frames - 1] = zero_frame + + + for frame_idx, frame_constraints in constraints.items(): + # delete zero frames in range around constraint + start = max(frame_idx - influence_range, min(frame_idx, 2)) + end = min(frame_idx + influence_range, max(frame_idx, n_frames - 2)) + for i in range(start, end): + if i in delta_frames and i not in constrained_frames: + del delta_frames[i] + + frame_constraints = list(frame_constraints.values()) + frame_copy = np.array(frames[frame_idx]) + new_frame = self.skeleton.reach_target_positions(frame_copy, frame_constraints, chain_end_joints, n_max_iter=n_max_iter, verbose=False) + delta_frames[frame_idx] = substract_frames(self.skeleton, new_frame, frames[frame_idx]) + delta_frames = collections.OrderedDict(sorted(delta_frames.items(), key=lambda x: x[0])) + return list(delta_frames.keys()), np.array(list(delta_frames.values())), list(joint_list) + + def edit_motion_using_displacement_map_and_ccd(self, frames, constraints, n_max_iter=100, root_joint=None, transition_window=None, plot=False): + """ Apply IK and create a transition using a displacement map + References: + Witkin and Popovic: Motion Warping, 1995. + Bruderlin and Williams: Motion Signal Processing, 1995. + Lee and Shin: A Hierarchical Approach to Interactive Motion Editing for Human-like Figures, 1999. + Args: + frames(np.array): input frames to be modified + constraints(list): list of constraints + n_max_iter(int): optional maximum ik iterations + root_joint(str): optional root joint of ik chain + transition_window(int): optional blending window size + plot(bool): optional plot delta curve + Returns: + frames(np.array): modifed frames + """ + if transition_window is None: + transition_window = self.transition_window + d_times, delta_frames, joint_list = self.generate_delta_frames_using_ccd(frames, constraints, n_max_iter, root_joint, transition_window) + if self.skeleton.skeleton_model is not None and "joints" in self.skeleton.skeleton_model and "neck" in self.skeleton.skeleton_model["joints"]: + joint_name = self.skeleton.skeleton_model["joints"]["neck"] + if joint_name is not None: + joint_list.append(joint_name) + return self.add_reduced_delta_curve(frames, d_times, delta_frames, joint_list, plot=plot) + + def apply_orientation_constraints(self, frames, constraints): + for frame_idx, frame_constraints in constraints.items(): + for joint_name, c in frame_constraints.items(): + if c.orientation is not None and self.optimize_orientation: + start = c.frame_idx + end = c.frame_idx + 1 + if self.activate_look_at and c.look_at: + self._look_at_in_range(frames, c.position, start, end) + print("set hand orientation", c.orientation) + self._set_hand_orientation(frames, c.orientation, c.joint_name, c.frame_idx, start, end) + + def edit_motion_using_fabrik(self, frames, constraints): + new_frames = np.array(frames) + for frame_idx, frame_constraints in constraints.items(): + joint_names = [] + fk_nodes = set() + for joint_name, c in frame_constraints.items(): + print("use fabrik on", joint_name, "at", frame_idx) + if joint_name in self._fabrik_chains: + joint_names += self._fabrik_chains[joint_name].node_order[:1] + new_frame = self._fabrik_chains[joint_name].run_partial_with_constraints(frames[frame_idx], c.position) + new_frames[frame_idx] = new_frame + joint_fk_nodes = self.skeleton.nodes[joint_name].get_fk_chain_list() + fk_nodes.update(joint_fk_nodes) + + if self.window > 0: + self.interpolate_around_frame(fk_nodes, new_frames, frame_idx, self.window) + return new_frames + + def edit_motion_to_look_at_target(self, frames, look_at_target, spine_target, start_idx, end_idx, orient_spine=False, look_at_dir=LOOK_AT_DIR, spine_look_at_dir=SPINE_LOOK_AT_DIR): + if look_at_target is None: + return frames + spine_joint_name = self.skeleton.skeleton_model["joints"]["spine_1"] + head_joint_name = self.skeleton.skeleton_model["joints"]["head"] + self.skeleton.clear_cached_global_matrices() + fk_nodes = None + for frame_idx in range(start_idx, end_idx): + if orient_spine and spine_target is not None: + frames[frame_idx] = self.skeleton.look_at_projected(frames[frame_idx], spine_joint_name, spine_target, local_dir=spine_look_at_dir) + frames[frame_idx] = self.skeleton.look_at(frames[frame_idx], head_joint_name, look_at_target, n_max_iter=2, local_dir=look_at_dir, chain_end_joint=spine_joint_name) + n_joints = len(self.skeleton.animated_joints) + fk_nodes = self.skeleton.nodes[head_joint_name].get_fk_chain_list() + if fk_nodes is not None: + self.interpolate_around_frame(fk_nodes, frames, start_idx, self.window) + if end_idx < len(frames): + self.interpolate_around_frame(fk_nodes, frames, end_idx, self.window) + return frames + + def get_static_joints(self, frame_constraints): + static_joints = set() + for joint_name, c in frame_constraints.items(): + if c.inside_region_position: + static_joints.add(joint_name) + return static_joints + + def find_free_root_joints(self, constraints, joint_chains): + """ check for each joint in the constraints if it is free""" + root_joints = dict() + for c in constraints: + root_joints[c.joint_name] = None + for free_joint in joint_chains[c.joint_name]: + is_free = True + for joint_name in joint_chains: + if joint_name == c.joint_name: + continue + if free_joint in joint_chains[joint_name]: + is_free = False + if not is_free: + root_joints[c.joint_name] = free_joint + print("set root joint for ", c.joint_name, "to", free_joint) + break + return root_joints + + def get_fk_chain(self, joint_name, root_joint): + joint_chain = [] + joint_fk_nodes = self.skeleton.nodes[joint_name].get_fk_chain_list() + abort = False + if root_joint is not None: # remove root joint + for j in joint_fk_nodes: + joint_chain.append(j) + if abort: + break + if j == root_joint: + abort = True + else: + joint_chain = joint_fk_nodes + print("copy fk chain", joint_name, joint_chain) + return joint_chain + + def get_active_constraints(self, new_frames, frame_idx, frame_constraints, joint_chain_buffer, prev_static_joints, root_joint): + + keep_static_joints = True + static_joints = self.get_static_joints(frame_constraints) + active_constraints = [] + region_overlaps = [] + fk_nodes = set() + for joint_name, c in frame_constraints.items(): + copied_joints = False + if joint_name not in joint_chain_buffer: + j_root_joint = root_joint + if c.fk_chain_root is not None: + j_root_joint = c.fk_chain_root + joint_chain_buffer[joint_name] = self.get_fk_chain(joint_name, None) # copy entire chain + if c.inside_region_position and prev_static_joints == static_joints: + #print("copy parameters for", joint_name, len(joint_chain_buffer[joint_name])) + #copy guess from previous frame if it is part of a region + #print("copy parameters", frame_idx) + self.copy_joint_parameters(joint_chain_buffer[joint_name], new_frames, frame_idx - 1, frame_idx) + copied_joints = True + if not copied_joints or not keep_static_joints: + #if c.orientation is not None: + # print("use ccd on", joint_name, "at", frame_idx, " with orientation") + #else: + # print("use ccd on", joint_name, "at", frame_idx) + active_constraints.append(c) + fk_nodes.update(joint_chain_buffer[joint_name]) + if c.inside_region_position and prev_static_joints != static_joints: + region_overlaps.append(frame_idx) + return active_constraints, fk_nodes, region_overlaps, joint_chain_buffer, static_joints + + def edit_motion_using_ccd(self, frames, constraints, n_max_iter=100, root_joint=None, activate_smoothing=True): + """ edit frame parameters using ccd and applying blending""" + + new_frames = np.array(frames) + joint_chain_buffer = dict() + delta_frames = dict() + n_frames = len(frames) + prev_static_joints = set() + + region_overlaps = [] + for frame_idx, frame_constraints in constraints.items(): + active_constraints, fk_nodes, _region_overlaps, joint_chain_buffer, static_joints = self.get_active_constraints(new_frames, frame_idx, frame_constraints, joint_chain_buffer, prev_static_joints, root_joint) + region_overlaps += _region_overlaps + if len(active_constraints) > 0: + #print("find free joints at", frame_idx) + if len(static_joints) > 0: + chain_end_joints = self.find_free_root_joints(active_constraints, joint_chain_buffer) + elif root_joint is not None: + chain_end_joints = dict() + for c in active_constraints: + chain_end_joints[c.joint_name] = root_joint + else: + chain_end_joints = None + # init frame with changes from prev frame if it was edited + prev_frame_idx = frame_idx-1 + if prev_frame_idx in delta_frames: + new_frames[frame_idx] = add_frames(self.skeleton, frames[frame_idx], delta_frames[prev_frame_idx]) + #print("apply delta",delta_frames[prev_frame_idx]) + + new_frame = self.skeleton.reach_target_positions(new_frames[frame_idx], active_constraints, chain_end_joints, n_max_iter=n_max_iter, verbose=False) + delta_frames[frame_idx] = substract_frames(self.skeleton,new_frame, frames[frame_idx]) + + # interpolate outside of region constraints + is_at_border = self.is_at_constrain_region_border(frame_idx, constraints) + if is_at_border and self.window > 0 and len(active_constraints) > 0 and len(fk_nodes) > 0 and activate_smoothing: + #print("outside of region", list(prev_static_joints), list(static_joints)) + fk_nodes = list(fk_nodes) + #fk_nodes = self.skeleton.animated_joints + self.interpolate_around_frame(fk_nodes, new_frames, frame_idx, self.window) + #new_frames = smooth_quaternion_frames(new_frames, frame_idx, self.window, False) + prev_static_joints = static_joints + if activate_smoothing: + for frame_idx in region_overlaps: + new_frames = smooth_quaternion_frames(new_frames, frame_idx, self.window, False) + return new_frames + + def is_at_constrain_region_border(self, frame_idx, constraints): + """check if the frame index is at the border of a constrained region """ + prev_frame_unconstrained = True + if frame_idx + 1 in constraints: + prev_frame_unconstrained = len(constraints[frame_idx+1]) == 0 + next_frame_unconstrained = True + if frame_idx-1 in constraints: + next_frame_unconstrained = len(constraints[frame_idx-1]) == 0 + return next_frame_unconstrained or prev_frame_unconstrained + + def apply_carry_constraints(self, frames, constraints): + print("generate carry constraints") + n_frames = frames.shape[0] + active_orientations = dict() + for frame_idx in range(0, n_frames): + # update active orientations + if frame_idx in constraints: + for joint_name, c in constraints[frame_idx].items(): + if c.keep_orientation and c.orientation is not None: + active_orientations[c.joint_name] = c.orientation + elif c.joint_name in active_orientations: + active_orientations[c.joint_name] = None + #else: + # print("no constraint on frame", frame_idx, c.keep_orientation) + # apply active orientations + for joint_name in active_orientations: + if active_orientations[joint_name] is not None: + #print("set orientation for", joint_name, "at", frame_idx) + frames[frame_idx] = self.skeleton.set_joint_orientation(frames[frame_idx], joint_name, active_orientations[joint_name] ) + return frames + + def set_global_joint_orientations(self, frames, constraints, frame_offset=0, time_function=None): + for c in constraints: + if c.constraint_type not in SUPPORTED_CONSTRAINT_TYPES or "generated" in c.semantic_annotation.keys(): + #print("skip unsupported constraint") + continue + joint_name = c.joint_name + start_frame_idx = self.get_global_frame_idx(c.canonical_keyframe, frame_offset, time_function) + if c.constrain_orientation_in_region and c.canonical_end_keyframe is not None: + end_frame_idx = self.get_global_frame_idx(c.canonical_end_keyframe, frame_offset, time_function) + for frame_idx in range(start_frame_idx, end_frame_idx): + frames[frame_idx] = self.skeleton.set_joint_orientation(frames[frame_idx], joint_name, c.orientation) + return frames + + def get_global_frame_idx(self, mp_frame_idx, frame_offset, time_function): + if time_function is not None: + frame_idx = frame_offset + int(time_function[mp_frame_idx]) + 1 + else: + frame_idx = frame_offset + int(mp_frame_idx) + return frame_idx + + def set_joint_orientation(self, joint_name, frames, start_idx, end_idx, target_orientation): + for frame_idx in range(start_idx, end_idx): + frames[frame_idx] = self.skeleton.set_joint_orientation(frames[frame_idx], joint_name, target_orientation) + + def copy_joint_parameters(self, nodes, frames, src_idx, dst_idx): + for node in nodes: + if self.skeleton.nodes[node].quaternion_frame_index == 0: + frames[dst_idx][:7] = frames[src_idx][:7] + else: + o = self.skeleton.nodes[node].quaternion_frame_index * 4 + 3 + frames[dst_idx][o:o+4] = frames[src_idx][o:o+4] + + def interpolate_around_frame(self, fk_nodes, frames, keyframe, window): + print("interpolate around frame", keyframe) + for node in fk_nodes: + o = self.skeleton.nodes[node].quaternion_frame_index * 4 + 3 + indices = list(range(o,o+4)) + smooth_joints_around_transition_using_slerp(frames, indices, keyframe, window) + #window = 1000 + #h_window = int(window / 2) + #start_idx = max(keyframe - h_window, 0) + #end_idx = min(keyframe + h_window, len(frames)) + #self.apply_joint_constraints(frames, start_idx, end_idx) + + def apply_joint_constraints(self, frames, start_idx, end_idx): + return frames + #print("apply joint constraints in range", start_idx, end_idx) + for frame_idx in range(start_idx, end_idx): + frames[frame_idx] = self.skeleton.apply_joint_constraints(frames[frame_idx]) + return frames + + def resample_motion(self, frames, resample_factor): + n_frames = len(frames) + times = list(range(0, n_frames)) + spline = CubicMotionSpline.fit_frames(self.skeleton, times, frames) + n_dest_frames = n_frames*resample_factor + step_size = (n_frames-1)/n_dest_frames + streched_times = np.arange(0,n_frames-1,step_size) + #print(streched_times) + new_frames = [] + for t in streched_times: + f = spline.evaluate(t) + new_frames.append(f) + return np.array(new_frames) + + def copy_joint_values_from_src(self, left_frames, right_frames, joint_list, joint_index_list, src_start, src_end, dest_start, dest_end): + n_copied_frames = src_end - src_start + n_dest_frames = dest_end - dest_start + modified_frames = np.array(right_frames) + if n_copied_frames > 1: + src_frames = self.stretch_motion(left_frames[src_start:src_end], n_dest_frames) + else: + src_frames = [] + for i in range(n_dest_frames): + src_frames.append(left_frames[src_start]) + src_frames = np.array(src_frames) + #print("copy ", n_copied_frames, n_dest_frames) + for frame_idx in range(n_dest_frames): + modified_frames[dest_start+frame_idx][joint_index_list] = src_frames[frame_idx][joint_index_list] + return modified_frames + + def apply_blending(self, frames, joint_list, joint_index_list, dest_start, dest_end, n_blend_range): + n_frames = len(frames) + blend_start = max(dest_start- n_blend_range, 0) + start_window = dest_start -blend_start + blend_end = min(dest_end +n_blend_range, n_frames-1) + end_window = blend_end- dest_end + #remove root indices + print("blend ", dest_start, dest_end, n_blend_range, start_window, end_window) + quat_joint_index_list = list(joint_index_list) + if self.skeleton.root in joint_list: + # apply root smnoothing and remove from index list + if start_window > 0: + frames = smooth_translation_in_quat_frames(frames, dest_start, start_window) + if end_window > 0: + frames = smooth_translation_in_quat_frames(frames, dest_end, end_window) + for i in range(3): + quat_joint_index_list.remove(i) + + if len(quat_joint_index_list) > 0: + o = 0 + for j in joint_list: + q_indices = quat_joint_index_list[o:o+4] + if start_window > 0: + frames = create_transition_for_joints_using_slerp(frames, q_indices, blend_start, dest_start, start_window, BLEND_DIRECTION_FORWARD) + if end_window > 0: + print(j, q_indices) + frames = create_transition_for_joints_using_slerp(frames, q_indices, dest_end, blend_end, end_window, BLEND_DIRECTION_BACKWARD) + o += 4 + return frames + + def stretch_motion(self, frames, n_dest_frames): + n_frames = len(frames) + times = list(range(0, n_frames)) + spline = CubicMotionSpline.fit_frames(self.skeleton, times, frames) + step_size = (n_frames-1)/n_dest_frames + streched_times = np.arange(0,n_frames-1,step_size) + new_frames = [] + for t in streched_times: + f = spline.evaluate(t) + new_frames.append(f) + print("new frames", len(new_frames)) + return new_frames + \ No newline at end of file diff --git a/build/lib/anim_utils/motion_editing/motion_filtering.py b/build/lib/anim_utils/motion_editing/motion_filtering.py new file mode 100644 index 0000000..4bd61b0 --- /dev/null +++ b/build/lib/anim_utils/motion_editing/motion_filtering.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import numpy as np +from ..motion_vector import MotionVector + + +def smooth(x, window_len=11,window='hanning'): + """smooth the data using a window with requested size. + http://scipy-cookbook.readthedocs.io/items/SignalSmooth.html + + This method is based on the convolution of a scaled window with the signal. + The signal is prepared by introducing reflected copies of the signal + (with the window size) in both ends so that transient parts are minimized + in the begining and end part of the output signal. + + input: + x: the input signal + window_len: the dimension of the smoothing window; should be an odd integer + window: the type of window from 'flat', 'hanning', 'hamming', 'bartlett', 'blackman' + flat window will produce a moving average smoothing. + + output: + the smoothed signal + + example: + + t=linspace(-2,2,0.1) + x=sin(t)+randn(len(t))*0.1 + y=smooth(x) + + see also: + + numpy.hanning, numpy.hamming, numpy.bartlett, numpy.blackman, numpy.convolve + scipy.signal.lfilter + + TODO: the window parameter could be the window itself if an array instead of a string + NOTE: length(output) != length(input), to correct this: return y[(window_len/2-1):-(window_len/2)] instead of just y. + """ + + if x.ndim != 1: + raise (ValueError, "smooth only accepts 1 dimension arrays.") + + if x.size < window_len: + raise (ValueError, "Input vector needs to be bigger than window size.") + + if window_len<3: + return x + + if not window in ['flat', 'hanning', 'hamming', 'bartlett', 'blackman']: + raise(ValueError, "Window is on of 'flat', 'hanning', 'hamming', 'bartlett', 'blackman'") + + + s=np.r_[x[window_len-1:0:-1],x,x[-2:-window_len-1:-1]] + #print(len(s)) + if window == 'flat': #moving average + w=np.ones(window_len,'d') + else: + w=eval('np.'+window+'(window_len)') + + y=np.convolve(w/w.sum(),s,mode='valid') + return y + + +def low_pass(src, alpha = 0.5): + """ http://blog.thomnichols.org/2011/08/smoothing-sensor-data-with-a-low-pass-filter + https://en.wikipedia.org/wiki/Low-pass_filter + """ + + n_frames = len(src.frames) + n_dims = len(src.frames[0]) + new_frames = [src.frames[0]] + for i in range(1, n_frames): + new_f = [] + for j in range(n_dims): + v = new_frames[i-1][j] + alpha * (src.frames[i][j] - new_frames[i-1][j]) + print("filter", v, src.frames[i][j]) + new_f.append(v) + new_frames.append(new_f) + + target_motion = MotionVector() + target_motion.skeleton = src.skeleton + target_motion.frames = np.array(new_frames) + target_motion.n_frames = len(new_frames) + return target_motion + + +def moving_average(src, window=4): + """ https://www.wavemetrics.com/products/igorpro/dataanalysis/signalprocessing/smoothing.htm#MovingAverage + """ + n_frames = len(src.frames) + n_dims = len(src.frames[0]) + new_frames = np.zeros(src.frames.shape) + new_frames[0,:] = src.frames[0,:] + hw = int(window/2) + for i in range(1, n_frames): + for j in range(n_dims): + start = max(0, i-hw) + end = min(n_frames-1, i+hw) + w = end-start + #print(i, j, start, end, w, n_frames, n_dims) + #print("filter", v, src.frames[i, j]) + new_frames[i, j] = np.sum(src.frames[start:end, j])/w + target_motion = MotionVector() + target_motion.skeleton = src.skeleton + target_motion.frames = np.array(new_frames) + print("new shape", target_motion.frames.shape) + target_motion.n_frames = len(new_frames) + return target_motion \ No newline at end of file diff --git a/build/lib/anim_utils/motion_editing/motion_grounding.py b/build/lib/anim_utils/motion_editing/motion_grounding.py new file mode 100644 index 0000000..3f27488 --- /dev/null +++ b/build/lib/anim_utils/motion_editing/motion_grounding.py @@ -0,0 +1,483 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import collections +import numpy as np +from copy import copy +from transformations import quaternion_from_matrix, quaternion_matrix, quaternion_multiply, quaternion_slerp +from .analytical_inverse_kinematics import AnalyticalLimbIK +from .numerical_ik_exp import NumericalInverseKinematicsExp +from .utils import normalize, project_on_intersection_circle, smooth_root_positions +from ..animation_data.skeleton_node import SkeletonEndSiteNode +from ..animation_data.utils import quaternion_from_vector_to_vector +from ..animation_data.motion_blending import create_transition_for_joints_using_slerp, BLEND_DIRECTION_FORWARD, BLEND_DIRECTION_BACKWARD, smooth_translation_in_quat_frames +from ..animation_data.skeleton_models import IK_CHAINS_DEFAULT_SKELETON + +FOOT_STATE_GROUNDED = 0 +FOOT_STATE_SWINGING = 1 + +def add_heels_to_skeleton(skeleton, left_foot, right_foot, left_heel, right_heel, offset=[0, -5, 0]): + + left_heel_node = SkeletonEndSiteNode(left_heel, [], skeleton.nodes[left_foot]) + left_heel_node.offset = np.array(offset) + skeleton.nodes[left_heel] = left_heel_node + skeleton.nodes[left_foot].children.append(left_heel_node) + + right_heel_node = SkeletonEndSiteNode(right_heel, [], skeleton.nodes[right_foot]) + right_heel_node.offset = np.array(offset) + skeleton.nodes[right_heel] = right_heel_node + skeleton.nodes[right_foot].children.append(right_heel_node) + return skeleton + + + +def get_heel_offset(skeleton, foot_name, toe_name, frame): + """ calculate heel offset from foot assuming the "y" axis of the foot coordinate system is aligned to the ground + """ + m = skeleton.nodes[foot_name].get_global_matrix(frame) + foot_position = m[:3,3] + print("foot_position", foot_position) + toe_offset = skeleton.nodes[toe_name].offset + #if len(skeleton.nodes[toe_name].children) > 0: + # toe_offset += skeleton.nodes[toe_name].children[0].offset + up_vector = np.array(skeleton.skeleton_model["cos_map"][foot_name]["y"]) + up_vector /= np.linalg.norm(up_vector) + #project toe offset on up vector that should be aligned with the ground + scale = np.dot(up_vector, toe_offset) + + # get global position of toe aligned to ground + local_offset = scale*up_vector + local_offset = [local_offset[0],local_offset[1],local_offset[2], 1] + projected_toe_pos = np.dot(m, local_offset)[:3] + print("projected_toe_pos", projected_toe_pos) + # use offset from projected toe position to position to get the global heel position + toe_pos = skeleton.nodes[toe_name].get_global_position(frame) + heel_position = foot_position + (toe_pos - projected_toe_pos) + + # bring into local coordinate system + heel_position = [heel_position[0],heel_position[1],heel_position[2], 1] + heel_offset = np.dot(np.linalg.inv(m), heel_position)[:3] + print("heel_offset", heel_offset) + print("toe_offset", toe_offset) + return heel_offset + + + +def get_heel_offset2(skeleton, foot_name, toe_name, frame): + """ calculate heel offset from foot assuming the "y" axis of the foot coordinate system is aligned to the ground + """ + m = skeleton.nodes[foot_name].get_global_matrix(frame) + foot_position = m[:3,3] + print("foot_position", foot_position) + toe_offset = skeleton.nodes[toe_name].offset + #if len(skeleton.nodes[toe_name].children) > 0: + # toe_offset += skeleton.nodes[toe_name].children[0].offset + foot_cos_map =skeleton.skeleton_model["cos_map"][foot_name] + up_vector = np.array(foot_cos_map["y"], dtype=np.float32) + up_vector /= np.linalg.norm(up_vector) + x_vector = np.array(foot_cos_map["x"], dtype=np.float32) + x_vector /= np.linalg.norm(x_vector) + z_vector = np.cross(up_vector, x_vector) + z_vector /= np.linalg.norm(z_vector) + #project toe offset on up vector that should be aligned with the ground + scale = np.dot(z_vector, toe_offset) + heel_offset = scale*z_vector + # bring into local coordinate system + print("heel_offse2", heel_offset) + print("toe_offset", toe_offset) + return heel_offset + +def add_temporary_heels_to_skeleton(skeleton, left_foot, right_foot, left_toe, right_toe, left_heel, right_heel): + + left_heel_node = SkeletonEndSiteNode(left_heel, [], skeleton.nodes[left_foot]) + left_heel_node.offset = get_heel_offset2(skeleton, left_foot, left_toe, skeleton.reference_frame) + skeleton.nodes[left_heel] = left_heel_node + skeleton.nodes[left_foot].children.append(left_heel_node) + skeleton.skeleton_model["joints"]["left_heel"] = left_heel + + right_heel_node = SkeletonEndSiteNode(right_heel, [], skeleton.nodes[right_foot]) + right_heel_node.offset = get_heel_offset2(skeleton, right_foot, right_toe, skeleton.reference_frame) + skeleton.nodes[right_heel] = right_heel_node + skeleton.nodes[right_foot].children.append(right_heel_node) + skeleton.skeleton_model["joints"]["right_heel"] = right_heel + return skeleton + +def create_grounding_constraint_from_frame(skeleton, frames, frame_idx, joint_name): + position = skeleton.nodes[joint_name].get_global_position(frames[frame_idx]) + m = skeleton.nodes[joint_name].get_global_matrix(frames[frame_idx]) + m[:3, 3] = [0, 0, 0] + orientation = normalize(quaternion_from_matrix(m)) + return MotionGroundingConstraint(frame_idx, joint_name, position, None, orientation) + + +def generate_ankle_constraint_from_toe(skeleton, frames, frame_idx, ankle_joint_name, heel_joint, toe_joint_name, target_ground_height, toe_pos=None): + """ create a constraint on the ankle position based on the toe constraint position""" + #print "add toe constraint" + if toe_pos is None: + ct = skeleton.nodes[toe_joint_name].get_global_position(frames[frame_idx]) + ct[1] = target_ground_height # set toe constraint on the ground + else: + ct = toe_pos + + a = skeleton.nodes[ankle_joint_name].get_global_position(frames[frame_idx]) + t = skeleton.nodes[toe_joint_name].get_global_position(frames[frame_idx]) + + target_toe_offset = a - t # difference between unmodified toe and ankle at the frame + ca = ct + target_toe_offset # move ankle so toe is on the ground + + m = skeleton.nodes[heel_joint].get_global_matrix(frames[frame_idx]) + m[:3, 3] = [0, 0, 0] + oq = quaternion_from_matrix(m) + oq = normalize(oq) + + return MotionGroundingConstraint(frame_idx, ankle_joint_name, ca, None, oq) + + +def create_ankle_constraint_from_toe_and_heel(skeleton, frames, frame_idx, ankle_joint, heel_joint, toe_joint,heel_offset, target_ground_height, heel_pos=None, toe_pos=None, is_swinging=False): + if toe_pos is None: + ct = skeleton.nodes[toe_joint].get_global_position(frames[frame_idx]) + ct[1] = target_ground_height + else: + ct = toe_pos + if heel_pos is None: + ch = skeleton.nodes[heel_joint].get_global_position(frames[frame_idx]) + ch[1] = target_ground_height + else: + ch = heel_pos + target_direction = normalize(ct - ch) + t = skeleton.nodes[toe_joint].get_global_position(frames[frame_idx]) + h = skeleton.nodes[heel_joint].get_global_position(frames[frame_idx]) + original_direction = normalize(t - h) + + global_delta_q = quaternion_from_vector_to_vector(original_direction, target_direction) + global_delta_q = normalize(global_delta_q) + + m = skeleton.nodes[heel_joint].get_global_matrix(frames[frame_idx]) + m[:3, 3] = [0, 0, 0] + oq = quaternion_from_matrix(m) + oq = normalize(oq) + orientation = normalize(quaternion_multiply(global_delta_q, oq)) + + # set target ankle position based on the grounded heel and the global target orientation of the ankle + m = quaternion_matrix(orientation)[:3, :3] + target_heel_offset = np.dot(m, heel_offset) + ca = ch - target_heel_offset + print("set ankle constraint both", ch, ca, target_heel_offset, target_ground_height) + foot_state = FOOT_STATE_GROUNDED + if is_swinging: + foot_state = FOOT_STATE_SWINGING + return MotionGroundingConstraint(frame_idx, ankle_joint, ca, None, orientation, foot_state) + + +def interpolate_constraints(c1, c2): + p = (c1.position + c2.position)/2 + o = quaternion_slerp(c1.orientation, c2.orientation, 0.5) + o = normalize(o) + return MotionGroundingConstraint(c1.frame_idx, c1.joint_name, p, None, o) + + +class MotionGroundingConstraint(object): + def __init__(self, frame_idx, joint_name, position, direction=None, orientation=None, foot_state=FOOT_STATE_GROUNDED): + self.frame_idx = frame_idx + self.joint_name = joint_name + self.position = position + self.direction = direction + self.orientation = orientation + self.toe_position = None + self.heel_position = None + self.global_toe_offset = None + self.foot_state = foot_state + + def evaluate(self, skeleton, q_frame): + d = self.position - skeleton.nodes[self.joint_name].get_global_position(q_frame) + return np.dot(d, d) + + +class IKConstraintSet(object): + def __init__(self, frame_range, joint_names, positions): + self.frame_range = frame_range + self.joint_names = joint_names + self.constraints = [] + for idx in range(frame_range[0], frame_range[1]): + for idx, joint_name in enumerate(joint_names): + c = MotionGroundingConstraint(idx, joint_name, positions[idx], None) + self.constraints.append(c) + + def add_constraint(self, c): + self.constraints.append(c) + + def evaluate(self, skeleton, q_frame): + error = 0 + for c in self.constraints: + d = c.position - skeleton.nodes[c.joint_name].get_global_position(q_frame) + error += np.dot(d, d) + return error + + +def add_fixed_dofs_to_frame(skeleton, frame): + o = 3 + full_frame = frame[:3].tolist() + for key, node in list(skeleton.nodes.items()): + if len(node.children) == 0: + continue + if not node.fixed: + full_frame += frame[o:o+4].tolist() + o += 4 + else: + full_frame += node.rotation.tolist() + return full_frame + +def extract_ik_chains(skeleton, damp_angle, damp_factor): + joints_map = skeleton.skeleton_model["joints"] + cos_map = skeleton.skeleton_model["cos_map"] + new_ik_chains = dict() + for j in IK_CHAINS_DEFAULT_SKELETON: + mapped_j = joints_map[j] + root_joint = IK_CHAINS_DEFAULT_SKELETON[j]["root"] + free_joint = IK_CHAINS_DEFAULT_SKELETON[j]["joint"] + if root_joint in joints_map and free_joint in joints_map: + mapped_free_joint = joints_map[free_joint] + if mapped_free_joint in cos_map: + data = copy(IK_CHAINS_DEFAULT_SKELETON[j]) + data["root"] = joints_map[root_joint] + data["joint"] = mapped_free_joint + data["joint_axis"] = cos_map[mapped_free_joint]["x"] + data["end_effector_dir"] = cos_map[mapped_free_joint]["y"] + new_ik_chains[mapped_j] = AnalyticalLimbIK.init_from_dict(skeleton, mapped_j, data, damp_angle=damp_angle, damp_factor=damp_factor) + return new_ik_chains + +class MotionGrounding(object): + def __init__(self, skeleton, ik_settings, skeleton_model, use_analytical_ik=True, damp_angle=None, damp_factor=None): + self.skeleton = skeleton + self._ik = NumericalInverseKinematicsExp(skeleton, ik_settings) + self._constraints = collections.OrderedDict() + self.transition_window = 10 + self.root_smoothing_window = 20 + self.translation_blend_window = 40 + self._blend_ranges = collections.OrderedDict() + self.use_analytical_ik = use_analytical_ik + self.skeleton_model = skeleton_model + self.damp_angle = damp_angle + self.damp_factor = damp_factor + if "joints" in skeleton_model and "left_toe" in skeleton_model["joints"] and "right_toe" in skeleton_model["joints"]: + joints_map = skeleton_model["joints"] + self.ik_chains = extract_ik_chains(skeleton, self.damp_angle, self.damp_factor) + + add_temporary_heels_to_skeleton(skeleton, joints_map["left_ankle"], joints_map["right_ankle"], joints_map["left_toe"], joints_map["right_toe"], "left_heel", "right_heel") + self.initialized = True + else: + self.ik_chains = dict() + self.initialized = False + + + def set_constraints(self, constraints): + self._constraints = constraints + + def add_constraint(self, joint_name, frame_range, position, direction=None): + for frame_idx in range(*frame_range): + c = MotionGroundingConstraint(frame_idx, joint_name, position, direction) + if frame_idx not in list(self._constraints.keys()): + self._constraints[frame_idx] = [] + self._constraints[frame_idx].append(c) + + def add_blend_range(self, joint_names, frame_range): + if frame_range not in list(self._constraints.keys()): + self._blend_ranges[frame_range] = [] + for j in joint_names: + self._blend_ranges[frame_range].append(j) + + def clear_constraints(self): + self._constraints = collections.OrderedDict() + + def clear_blend_ranges(self): + self._blend_ranges = collections.OrderedDict() + + def clear(self): + self.clear_constraints() + self.clear_blend_ranges() + + def run(self, motion_vector, scene_interface=None): + new_frames = motion_vector.frames[:] + if scene_interface is not None: + self.shift_root_to_ground(new_frames, scene_interface) + self.shift_root_to_reach_constraints(new_frames) + if len(new_frames) > 1: + self.blend_at_transitions(new_frames) + if self.use_analytical_ik: + self.apply_analytical_ik(new_frames) + else: + self.apply_ik_constraints(new_frames) + if len(new_frames) > 1: + self.blend_at_transitions(new_frames) + return new_frames + + def apply_on_frame(self, frame, scene_interface): + x = frame[0] + z = frame[2] + target_ground_height = scene_interface.get_height(x, z) + shift = target_ground_height - frame[1] + frame[1] += shift + #self.apply_analytical_ik_on_frame(frame, constraints) + return frame + + def _blend_around_frame_range(self, frames, start, end, joint_names): + for joint_name in joint_names: + transition_start = max(start - self.transition_window, 0) + transition_end = min(end + self.transition_window, frames.shape[0]-1) - 1 + forward_steps = start - transition_start + backward_steps = transition_end - end + if joint_name == self.skeleton.root: + if start > 0: + frames = smooth_translation_in_quat_frames(frames, start, self.translation_blend_window) + temp_frame = min(end + 1, frames.shape[0]-1) + frames = smooth_translation_in_quat_frames(frames, temp_frame, self.translation_blend_window) + + idx = self._ik.skeleton.animated_joints.index(joint_name)*4+3 + joint_parameter_indices = [idx, idx+1, idx+2, idx+3] + if start > 0: + create_transition_for_joints_using_slerp(frames, joint_parameter_indices, transition_start, start, forward_steps, BLEND_DIRECTION_FORWARD) + create_transition_for_joints_using_slerp(frames, joint_parameter_indices, end, transition_end, backward_steps, BLEND_DIRECTION_BACKWARD) + + def apply_ik_constraints(self, frames): + for frame_idx, constraints in self._constraints.items(): + if 0 <= frame_idx < len(frames): + frames[frame_idx] = self._ik.modify_frame(frames[frame_idx], constraints) + + def shift_root_to_reach_constraints(self, frames): + root_positions = self.generate_root_positions_from_foot_constraints(frames) + root_positions = smooth_root_positions(root_positions, self.root_smoothing_window) + self.apply_root_constraints(frames, root_positions) + + def generate_root_positions_from_foot_constraints(self, frames): + root_constraints = [] + for frame_idx, constraints in self._constraints.items(): + if 0 <= frame_idx < len(frames): + grounding_constraints = [c for c in constraints if c.foot_state==FOOT_STATE_GROUNDED] + n_constraints = len(grounding_constraints) + p = None + if n_constraints == 1: + p = self.generate_root_constraint_for_one_foot(frames[frame_idx], grounding_constraints[0]) + elif n_constraints > 1: + p = self.generate_root_constraint_for_two_feet(frames[frame_idx], grounding_constraints[0], grounding_constraints[1]) + if p is None: + p = frames[frame_idx, :3] + root_constraints.append(p) + return np.array(root_constraints) + + def apply_root_constraints(self, frames, constraints): + for frame_idx, p in enumerate(constraints): + if p is not None: + frames[frame_idx][:3] = p + + def generate_root_constraint_for_one_foot(self, frame, c): + pelvis = self.skeleton.skeleton_model["joints"]["pelvis"] + pelvis_pos = self.skeleton.nodes[pelvis].get_global_position(frame) + target_length = np.linalg.norm(c.position - pelvis_pos) + limb_length = self.get_limb_length(c.joint_name) + if target_length < limb_length: + return frame[:3] # no change + new_root_pos = (c.position + normalize(root_pos - c.position) * limb_length) + if np.linalg.norm(self.skeleton.nodes[pelvis].offset) > 0.0: + root_m = self.skeleton.nodes[self.skeleton.root].get_global_matrix(frame)[:3,:3] + new_root_pos -= np.dot(root_m, self.skeleton.nodes[pelvis].offset) + else: + new_root_pos -= self.skeleton.nodes[pelvis].offset + return new_root_pos + + def generate_root_constraint_for_two_feet(self, frame, constraint1, constraint2, limb_length_offset=0.0): + """ Set the root position to the projection on the intersection of two spheres """ + + pelvis = self.skeleton.skeleton_model["joints"]["pelvis"] + m = self.skeleton.nodes[pelvis].get_global_matrix(frame) + p = m[:3, 3] + #print("root position", root, p) + t1 = np.linalg.norm(constraint1.position - p) + t2 = np.linalg.norm(constraint2.position - p) + + c1 = constraint1.position + r1 = self.get_limb_length(constraint1.joint_name) + limb_length_offset + #p1 = c1 + r1 * normalize(p-c1) + c2 = constraint2.position + r2 = self.get_limb_length(constraint2.joint_name) + limb_length_offset + #(r1, r2, t1,t2) + #p2 = c2 + r2 * normalize(p-c2) + if t1 < r1 and t2 < r2: + return None + #print("adapt root for two constraints", constraint1.position, r1, constraint2.position, r2) + new_root_pos = project_on_intersection_circle(p, c1, r1, c2, r2) + if np.linalg.norm(self.skeleton.nodes[pelvis].offset) > 0.0: + root_m = self.skeleton.nodes[self.skeleton.root].get_global_matrix(frame)[:3,:3] + new_root_pos -= np.dot(root_m, self.skeleton.nodes[pelvis].offset) + else: + new_root_pos -= self.skeleton.nodes[pelvis].offset + return new_root_pos + + def get_limb_length(self, joint_name): + limb_length = np.linalg.norm(self.skeleton.nodes[joint_name].offset) + limb_length += np.linalg.norm(self.skeleton.nodes[joint_name].parent.offset) + return limb_length + + def apply_analytical_ik(self, frames): + n_frames = len(frames) + for frame_idx, constraints in self._constraints.items(): + if 0 <= frame_idx < n_frames and len(constraints)> 0: + self.apply_analytical_ik_on_frame(frames[frame_idx], constraints) + + def apply_analytical_ik_on_frame(self, frame, constraints): + for c in constraints: + if c.joint_name in self.ik_chains: + frame = self.ik_chains[c.joint_name].apply(frame, c.position, c.orientation) + else: + print("could not find ik chain definition for ", c.joint_name) + frame = self._ik.modify_frame(frame, constraints) + return frame + + def apply_orientation_constraints_on_frame(self, frame, constraints): + for c in constraints: + self.ik_chains[c.joint_name].set_end_effector_rotation2(frame, c.orientation) + return frame + + def blend_at_transitions(self, frames): + for frame_range, joint_names in self._blend_ranges.items(): + start = frame_range[0] + end = frame_range[1] + self._blend_around_frame_range(frames, start, end, joint_names) + return frames + + def shift_root_to_ground(self, frames, scene_interface): + for idx, frame in enumerate(frames): + x = frames[idx][0] + z = frames[idx][2] + target_ground_height = scene_interface.get_height(x, z) + root_pos = self.get_projected_root_pos(frames[idx]) + shift = target_ground_height - root_pos[1] + frames[idx][1] += shift + + def get_projected_root_pos(self, frame): + pelvis = self.skeleton.skeleton_model["joints"]["pelvis"] + ground_pos = self.skeleton.nodes[pelvis].get_global_position(frame) + if np.linalg.norm(self.skeleton.nodes[pelvis].offset) > 0.0: + root_m = self.skeleton.nodes[self.skeleton.root].get_global_matrix(frame)[:3,:3] + ground_pos -= np.dot(root_m, self.skeleton.nodes[pelvis].offset) + else: + ground_pos -= self.skeleton.nodes[pelvis].offset + return ground_pos diff --git a/build/lib/anim_utils/motion_editing/numerical_ik_exp.py b/build/lib/anim_utils/motion_editing/numerical_ik_exp.py new file mode 100644 index 0000000..2f9e727 --- /dev/null +++ b/build/lib/anim_utils/motion_editing/numerical_ik_exp.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +""" Numerical IK using exponential displacement map based on Section 5.1 of [1] + +Use current configuration as q_0 +Find v such that (q_i = q_0_i exp(v_i) for 0 self.success_threshold and iter_counter < self.max_retries: + r = minimize(self._objective, guess, args=(self.skeleton, reference, constraints, self.joint_weights), + method=self.ik_settings["optimization_method"], + tol=self.ik_settings["tolerance"], + options=self._optimization_options) + exp_frame = r["x"] + exp_frame[:9] = 0 + error = self._objective(exp_frame, self.skeleton, reference, constraints, self.joint_weights) + iter_counter += 1 + + return exp_frame + + def modify_frame(self, reference, constraints): + exp_frame = self.run(reference, constraints) + d = convert_exp_frame_to_quat_frame(self.skeleton, exp_frame) + q_frame = add_quat_frames(self.skeleton, reference, d) + return q_frame diff --git a/build/lib/anim_utils/motion_editing/numerical_ik_quat.py b/build/lib/anim_utils/motion_editing/numerical_ik_quat.py new file mode 100644 index 0000000..c5d7786 --- /dev/null +++ b/build/lib/anim_utils/motion_editing/numerical_ik_quat.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. + +import time +import numpy as np +from scipy.optimize import minimize +from ..utilities.log import write_log, write_message_to_log, LOG_MODE_DEBUG + + +def obj_inverse_kinematics(s, data): + pose, free_joints, target_joint, target_position, target_direction = data + pose.set_channel_values(s, free_joints) + if target_direction is not None: + parent_joint = pose.get_parent_joint(target_joint) + pose.point_in_direction(parent_joint, target_direction) + position = pose.evaluate_position(target_joint) + d = position - target_position + return np.dot(d, d) + + +class NumericalInverseKinematicsQuat(object): + def __init__(self, skeleton_pose_model, ik_settings): + self._ik_settings = ik_settings + self.verbose = False + self.pose = skeleton_pose_model + self.success_threshold = self._ik_settings["success_threshold"] + self.max_retries = self._ik_settings["max_retries"] + self.optimize_orientation = self._ik_settings["optimize_orientation"] + + def _run_optimization(self, objective, initial_guess, data, cons=None): + return minimize(objective, initial_guess, args=(data,), + method=self._ik_settings["optimization_method"],#"SLSQP",#best result using L-BFGS-B + constraints=cons, tol=self._ik_settings["tolerance"], + options={'maxiter': self._ik_settings["max_iterations"], 'disp': self.verbose})#,'eps':1.0 + + def modify_pose(self, joint_name, target, direction=None): + error = 0.0 + if joint_name in list(self.pose.free_joints_map.keys()): + free_joints = self.pose.free_joints_map[joint_name] + error = self._modify_using_optimization(joint_name, target, free_joints, direction) + return error + + def modify_pose_general(self, constraint): + free_joints = constraint.free_joints + initial_guess = self._extract_free_parameters(free_joints) + data = constraint.data(self, free_joints) + if self.verbose: + self.pose.set_channel_values(initial_guess, free_joints) + p = self.pose.evaluate_position(constraint.joint_name) + write_message_to_log( + "Start optimization for joint " + constraint.joint_name + " " + str(len(initial_guess)) + + " " + str(len(free_joints)) + " " + str(p), LOG_MODE_DEBUG) + cons = None + #start = time.time() + error = np.inf + iter_counter = 0 + result = None + while error > self.success_threshold and iter_counter < self.max_retries: + result = self._run_optimization(constraint.evaluate, initial_guess, data, cons) + error = constraint.evaluate(result["x"], data) + iter_counter += 1 + # write_log("finished optimization in",time.time()-start,"seconds with error", error)#,result["x"].tolist(), initial_guess.tolist() + if result is not None: + self.pose.set_channel_values(result["x"], free_joints) + return error + + def _extract_free_parameters(self, free_joints): + """get parameters of joints from reference frame + """ + parameters = list() + for joint_name in free_joints: + parameters += self.pose.extract_parameters(joint_name).tolist() + return np.asarray(parameters) + + def _extract_free_parameter_indices(self, free_joints): + """get parameter indices of joints from reference frame + """ + indices = {} + for joint_name in free_joints: + indices[joint_name] = list(range(*self.pose.extract_parameters_indices(joint_name))) + return indices + + def _modify_using_optimization(self, target_joint, target_position, free_joints, target_direction=None): + initial_guess = self._extract_free_parameters(free_joints) + data = self.pose, free_joints, target_joint, target_position, target_direction + if self.verbose: + write_message_to_log("Start optimization for joint " + target_joint + " " + str(len(initial_guess)) + + " " + str(len(free_joints)), LOG_MODE_DEBUG) + start = time.time() + cons = None#self.pose.generate_constraints(free_joints) + result = self._run_optimization(obj_inverse_kinematics, initial_guess, data, cons) + position = self.pose.evaluate_position(target_joint) + error = np.linalg.norm(position-target_position) + if self.verbose: + write_message_to_log("Finished optimization in " + str(time.time()-start) + " seconds with error " + str(error), LOG_MODE_DEBUG) #,result["x"].tolist(), initial_guess.tolist() + self.pose.set_channel_values(result["x"], free_joints) + return error + + def optimize_joint(self, objective, target_joint, target_position, target_orientation, free_joint): + initial_guess = self.pose.extract_parameters(free_joint) # self._extract_free_parameters([free_joint]) + data = self.pose, [free_joint], target_joint, target_position, None + result = self._run_optimization(objective, initial_guess, data) + # apply constraints here + self.pose.apply_bounds(free_joint) + return result["x"] + + def evaluate_delta(self, parameters, target_joint, target_position, free_joints): + self.pose.set_channel_values(parameters, free_joints) + position = self.pose.evaluate_position(target_joint) + d = position - target_position + return np.dot(d, d) diff --git a/build/lib/anim_utils/motion_editing/skeleton_pose_model.py b/build/lib/anim_utils/motion_editing/skeleton_pose_model.py new file mode 100644 index 0000000..777bc5d --- /dev/null +++ b/build/lib/anim_utils/motion_editing/skeleton_pose_model.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import numpy as np +from transformations import quaternion_matrix, quaternion_from_matrix +from ..animation_data.quaternion_frame import convert_quaternion_frames_to_euler_frames, euler_to_quaternion, quaternion_to_euler +from ..animation_data.utils import quaternion_from_vector_to_vector +from ..animation_data.constants import LEN_QUAT, LEN_ROOT_POS + + +def convert_euler_to_quat(euler_frame, joints): + quat_frame = euler_frame[:3].tolist() + offset = 3 + step = 3 + for joint in joints: + e = euler_frame[offset:offset+step] + q = euler_to_quaternion(e) + quat_frame += list(q) + offset += step + return np.array(quat_frame) + + +class SkeletonPoseModel(object): + def __init__(self, skeleton, use_euler=False): + self.channels = skeleton.get_channels() + pose_parameters = skeleton.reference_frame + self.skeleton = skeleton + self.use_euler = use_euler + if self.use_euler: + self.pose_parameters = convert_quaternion_frames_to_euler_frames([pose_parameters])[0]#change to euler + else: + self.pose_parameters = pose_parameters + self.n_channels = {} + self.channels_start = {} + self.types = {} + self.modelled_joints = [] + channel_idx = 0 + for joint, ch in list(self.channels.items()): + self.channels_start[joint] = channel_idx + self.n_channels[joint] = len(ch) + if len(ch) == LEN_QUAT: + self.types[joint] = "rot" + elif len(ch) == LEN_ROOT_POS + LEN_QUAT: + self.types[joint] = "trans" + else: + self.types[joint] = "rot" + channel_idx += self.n_channels[joint] + if self.n_channels[joint] > 0: + self.modelled_joints.append(joint) + self.free_joints_map = skeleton.free_joints_map + if "head" in skeleton.skeleton_model: + self.head_joint = skeleton.skeleton_model["joints"]["head"] + if "neck" in skeleton.skeleton_model: + self.neck_joint = skeleton.skeleton_model["joints"]["neck"] + if "cos_map" in skeleton.skeleton_model: + hand_joint = skeleton.skeleton_model["joints"]["right_wrist"] + if hand_joint in skeleton.skeleton_model["cos_map"]: + cos = skeleton.skeleton_model["cos_map"][hand_joint] + self.relative_hand_dir = np.array(list(cos["y"]) + [0]) + self.relative_hand_cross = np.array(list(cos["x"]) + [0]) + up_vec = list(np.cross(cos["y"], cos["x"])) + self.relative_hand_up = np.array(up_vec + [0]) + else: + self.relative_hand_dir = np.array([1.0, 0.0, 0.0, 0.0]) + self.relative_hand_cross = np.array([0.0,1.0,0.0, 0.0]) + self.relative_hand_up = np.array([0.0, 0.0, 1.0, 0.0]) + if "relative_head_dir" in skeleton.skeleton_model.keys(): + vec = list(skeleton.skeleton_model["relative_head_dir"]) + self.relative_head_dir = np.array(vec+[0]) + else: + self.relative_head_dir = np.array([0.0, 0.0, 1.0, 0.0]) + + self.bounds = dict()#skeleton.bounds + + def set_pose_parameters(self, pose_parameters): + self.pose_parameters = pose_parameters + + def set_channel_values(self, parameters, free_joints): + p_idx = 0 + for joint_name in free_joints: + n_channels = self.n_channels[joint_name] + p_end = p_idx + n_channels + f_start = self.channels_start[joint_name] + f_end = f_start + n_channels + #print f_start, f_end + self.pose_parameters[f_start:f_end] = parameters[p_idx:p_end] + p_idx += n_channels + return self.pose_parameters + + def extract_parameters_indices(self, joint_name): + f_start = self.channels_start[joint_name] + f_end = f_start + self.n_channels[joint_name] + return f_start, f_end + + def extract_parameters(self, joint_name): + f_start, f_end = self.extract_parameters_indices(joint_name) + return self.pose_parameters[f_start: f_end] + + def get_vector(self): + if self.use_euler: + return convert_euler_to_quat(self.pose_parameters, self.modelled_joints)#convert to quat + else: + return self.pose_parameters + + def evaluate_position_with_cache(self, target_joint, free_joints): + for joint in free_joints: + self.skeleton.nodes[joint].get_global_position(self.pose_parameters, use_cache=True) + return self.skeleton.nodes[target_joint].get_global_position(self.pose_parameters, use_cache=True)#get_vector() + + def evaluate_position(self, target_joint): + return self.skeleton.nodes[target_joint].get_global_position(self.pose_parameters) + + def evaluate_orientation(self, target_joint): + return self.skeleton.nodes[target_joint].get_global_orientation_quaternion(self.pose_parameters) + + def evaluate_matrix(self, target_joint): + return self.skeleton.nodes[target_joint].get_global_matrix(self.pose_parameters) + + def apply_bounds(self, free_joint): + if free_joint in list(self.bounds.keys()): + euler_angles = self.get_euler_angles(free_joint) + for bound in self.bounds[free_joint]: + self.apply_bound_on_joint(euler_angles, bound) + + if self.use_euler: + self.set_channel_values(euler_angles, [free_joint]) + else: + q = euler_to_quaternion(euler_angles) + self.set_channel_values(q, [free_joint]) + return + + def apply_bound_on_joint(self, euler_angles, bound): + if "min" in list(bound.keys()): + euler_angles[bound["dim"]] = max(euler_angles[bound["dim"]],bound["min"]) + if "max" in list(bound.keys()): + euler_angles[bound["dim"]] = min(euler_angles[bound["dim"]],bound["max"]) + + def get_euler_angles(self, joint): + if self.use_euler: + euler_angles = self.extract_parameters(joint) + else: + q = self.extract_parameters(joint) + euler_angles = quaternion_to_euler(q) + return euler_angles + + def generate_constraints(self, free_joints): + """ TODO add bounds on axis components of the quaternion according to + Inverse Kinematics with Dual-Quaternions, Exponential-Maps, and Joint Limits by Ben Kenwright + or try out euler based ik + """ + cons = [] + idx = 0 + for joint_name in free_joints: + if joint_name in list(self.bounds.keys()): + start = idx + for bound in self.bounds[joint_name]: + if "min" in list(bound.keys()): + cons.append(({"type": 'ineq', "fun": lambda x: x[start+bound["dim"]]-bound["min"]})) + if "max" in list(bound.keys()): + cons.append(({"type": 'ineq', "fun": lambda x: bound["max"]-x[start+bound["dim"]]})) + idx += self.n_channels[joint_name] + return tuple(cons) + + def clear_cache(self): + self.skeleton.clear_cached_global_matrices() + + def lookat(self, point): + head_position = self.evaluate_position(self.head_joint) + target_dir = point - head_position + target_dir /= np.linalg.norm(target_dir) + head_direction = self.get_joint_direction(self.head_joint, self.relative_head_dir) + delta_q = quaternion_from_vector_to_vector(head_direction, target_dir) + delta_matrix = quaternion_matrix(delta_q) + + #delta*parent*old_local = parent*new_local + #inv_parent*delta*parent*old_local = new_local + parent_m = self.skeleton.nodes[self.neck_joint].get_global_matrix(self.pose_parameters, use_cache=False) + old_local = np.dot(parent_m, self.skeleton.nodes[self.head_joint].get_local_matrix(self.pose_parameters)) + m = np.dot(delta_matrix, old_local) + new_local = np.dot(np.linalg.inv(parent_m),m) + new_local_q = quaternion_from_matrix(new_local) + self.set_channel_values(new_local_q, [self.head_joint]) + + def set_hand_orientation_using_direction_vectors(self, joint_name, orientation): + m = quaternion_matrix(orientation) + target_dir = np.dot(m, self.relative_hand_dir) + target_dir /= np.linalg.norm(target_dir) + cross_dir = np.dot(m, self.relative_hand_cross) + cross_dir /= np.linalg.norm(cross_dir) + self.point_in_direction(joint_name, target_dir[:3], cross_dir[:3]) + + def set_hand_orientation(self, joint_name, orientation): + #print "set hand orientation" + m = quaternion_matrix(orientation) + parent_joint_name = self.get_parent_joint(joint_name) + parent_m = self.skeleton.nodes[parent_joint_name].get_global_matrix(self.pose_parameters, use_cache=False) + local_m = np.dot(np.linalg.inv(parent_m), m) + q = quaternion_from_matrix(local_m) + self.set_channel_values(q, [joint_name]) + + + def point_in_direction(self, joint_name, target_dir, target_cross=None): + parent_joint_name = self.get_parent_joint(joint_name) + joint_direction = self.get_joint_direction(joint_name, self.relative_hand_dir) + delta_q = quaternion_from_vector_to_vector(joint_direction, target_dir) + delta_matrix = quaternion_matrix(delta_q) + #delta*parent*old_local = parent*new_local + #inv_parent*delta*parent*old_local = new_local + parent_m = self.skeleton.nodes[parent_joint_name].get_global_matrix(self.pose_parameters, use_cache=False) + old_local = np.dot(parent_m, self.skeleton.nodes[joint_name].get_local_matrix(self.pose_parameters)) + m = np.dot(delta_matrix, old_local) + new_local = np.dot(np.linalg.inv(parent_m),m) + new_local_q = quaternion_from_matrix(new_local) + self.set_channel_values(new_local_q, [joint_name]) + + #rotate around orientation vector + if target_cross is not None: + joint_cross = self.get_joint_direction(joint_name, self.relative_hand_cross) + delta_q = quaternion_from_vector_to_vector(joint_cross, target_cross) + delta_matrix = quaternion_matrix(delta_q) + parent_m = self.skeleton.nodes[parent_joint_name].get_global_matrix(self.pose_parameters, use_cache=False) + old_local = np.dot(parent_m, self.skeleton.nodes[joint_name].get_local_matrix(self.pose_parameters)) + m = np.dot(delta_matrix, old_local) + new_local = np.dot(np.linalg.inv(parent_m),m) + new_local_q = quaternion_from_matrix(new_local) + self.point_hand_cross_dir_in_direction(joint_name, target_cross, parent_joint_name) + + def point_hand_cross_dir_in_direction(self,joint_name,target_cross,parent_joint_name): + joint_cross = self.get_joint_direction(joint_name, self.relative_hand_cross) + delta_q = quaternion_from_vector_to_vector(joint_cross, target_cross) + delta_matrix = quaternion_matrix(delta_q) + parent_m = self.skeleton.nodes[parent_joint_name].get_global_matrix(self.pose_parameters, use_cache=False) + old_local = np.dot(parent_m, self.skeleton.nodes[joint_name].get_local_matrix(self.pose_parameters)) + m = np.dot(delta_matrix, old_local) + new_local = np.dot(np.linalg.inv(parent_m), m) + new_local_q = quaternion_from_matrix(new_local) + self.set_channel_values(new_local_q, [joint_name]) + + def point_hand_up_dir_in_direction(self, joint_name, target_up, parent_joint_name): + joint_cross = self.get_joint_direction(joint_name, self.relative_hand_up) + delta_q = quaternion_from_vector_to_vector(joint_cross, target_up) + delta_matrix = quaternion_matrix(delta_q) + parent_m = self.skeleton.nodes[parent_joint_name].get_global_matrix(self.pose_parameters, use_cache=False) + old_local = np.dot(parent_m, self.skeleton.nodes[joint_name].get_local_matrix(self.pose_parameters)) + m = np.dot(delta_matrix, old_local) + new_local = np.dot(np.linalg.inv(parent_m), m) + new_local_q = quaternion_from_matrix(new_local) + self.set_channel_values(new_local_q, [joint_name]) + + def set_joint_orientation(self, joint_name, target_q): + global_q = self.skeleton.nodes[joint_name].get_global_orientation_quaternion(self.pose_parameters, use_cache=False) + global_m = quaternion_matrix(global_q) + target_m = quaternion_matrix(target_q) + delta_m = np.linalg.inv(global_m)*target_m + local_m = self.skeleton.nodes[joint_name].get_local_matrix(self.pose_parameters) + new_local_m = np.dot(delta_m, local_m) + new_local_q = quaternion_from_matrix(new_local_m) + self.set_channel_values(new_local_q, [joint_name]) + + def get_joint_direction(self, joint_name, ref_vector): + q = self.evaluate_orientation(joint_name) + q /= np.linalg.norm(q) + rotation_matrix = quaternion_matrix(q) + vec = np.dot(rotation_matrix, ref_vector)[:3] + return vec/np.linalg.norm(vec) + + def get_parent_joint(self, joint_name): + if joint_name not in list(self.skeleton.parent_dict.keys()): + return None + return self.skeleton.parent_dict[joint_name] + + diff --git a/build/lib/anim_utils/motion_editing/utils.py b/build/lib/anim_utils/motion_editing/utils.py new file mode 100644 index 0000000..a18f918 --- /dev/null +++ b/build/lib/anim_utils/motion_editing/utils.py @@ -0,0 +1,597 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import numpy as np +import math +from matplotlib import pyplot as plt +import json +from transformations import quaternion_multiply, quaternion_inverse, quaternion_matrix, quaternion_from_matrix, euler_from_quaternion +from scipy.interpolate import UnivariateSpline +#from anim_utils.animation_data.constants import DEFAULT_ROTATION_ORDER + +DEFAULT_ROTATION_ORDER = ['Xrotation','Yrotation','Zrotation'] + +def normalize(v): + return v/np.linalg.norm(v) + + +def quaternion_from_axis_angle(axis, angle): + q = [1,0,0,0] + if np.linalg.norm(axis) > 0: + q[1] = axis[0] * math.sin(angle / 2) + q[2] = axis[1] * math.sin(angle / 2) + q[3] = axis[2] * math.sin(angle / 2) + q[0] = math.cos(angle / 2) + q = normalize(q) + return q + + +def exp_map_to_quaternion(e): + angle = np.linalg.norm(e) + if angle > 0: + axis = e / angle + q = quaternion_from_axis_angle(axis, angle) + else: + q = [1, 0, 0, 0] + return q + + +def convert_exp_frame_to_quat_frame(skeleton, e): + src_offset = 0 + dest_offset = 0 + n_joints = len(skeleton.animated_joints) + q = np.zeros(n_joints*4) + for node in skeleton.animated_joints: + e_i = e[src_offset:src_offset+3] + q[dest_offset:dest_offset+4] = exp_map_to_quaternion(e_i) + src_offset += 3 + dest_offset += 4 + return q + + +def add_quat_frames(skeleton, q_frame1, q_frame2, dest_offset=3): + src_offset = 0 + new_quat_frame = np.zeros(len(q_frame1)) + new_quat_frame[:3] = q_frame1[:3] + for node in skeleton.animated_joints: + new_q = quaternion_multiply(q_frame1[dest_offset:dest_offset + 4], q_frame2[src_offset:src_offset + 4]) + new_quat_frame[dest_offset:dest_offset+4] = new_q + dest_offset += 4 + src_offset += 4 + return new_quat_frame + + +def get_3d_rotation_between_vectors(a, b): + v = np.cross(a, b) + s = np.linalg.norm(v) + if s ==0: + return np.eye(3) + c = np.dot(a,b) + v_x = np.array([[0, -v[2], v[1]], + [v[2], 0, -v[0]], + [-v[1], v[0], 0]]) + v_x_2 = np.dot(v_x,v_x) + r = np.eye(3) + v_x + (v_x_2* (1-c/s**2)) + return r + + +def normalize_quaternion(q): + return quaternion_inverse(q) / np.dot(q, q) + + +def get_average_joint_position(skeleton, frames, joint_name, start_frame, end_frame): + end_frame = min(end_frame, frames.shape[0]) + temp_positions = [] + for idx in range(start_frame, end_frame): + frame = frames[idx] + pos = skeleton.nodes[joint_name].get_global_position(frame) + temp_positions.append(pos) + return np.mean(temp_positions, axis=0) + + +def get_average_joint_direction(skeleton, frames, joint_name, child_joint_name, start_frame, end_frame,ground_height=0): + temp_dirs = [] + for idx in range(start_frame, end_frame): + frame = frames[idx] + pos1 = skeleton.nodes[joint_name].get_global_position(frame) + pos2 = skeleton.nodes[child_joint_name].get_global_position(frame) + #pos2[1] = ground_height + joint_dir = pos2 - pos1 + joint_dir /= np.linalg.norm(joint_dir) + temp_dirs.append(joint_dir) + return np.mean(temp_dirs, axis=0) + +def get_average_direction_from_target(skeleton, frames, target_pos, child_joint_name, start_frame, end_frame,ground_height=0): + temp_dirs = [] + for idx in range(start_frame, end_frame): + frame = frames[idx] + pos2 = skeleton.nodes[child_joint_name].get_global_position(frame) + pos2[1] = ground_height + joint_dir = pos2 - target_pos + joint_dir /= np.linalg.norm(joint_dir) + temp_dirs.append(joint_dir) + return np.mean(temp_dirs, axis=0) + + +def to_local_cos(skeleton, node_name, frame, q): + # bring into parent coordinate system + pm = skeleton.nodes[node_name].get_global_matrix(frame)[:3,:3] + #pm[:3, 3] = [0, 0, 0] + inv_pm = np.linalg.inv(pm) + r = quaternion_matrix(q)[:3,:3] + lr = np.dot(inv_pm, r)[:3,:3] + q = quaternion_from_matrix(lr) + return q + +def get_dir_on_plane(x, n): + axb = np.cross(x,n) + d = np.cross(n, normalize(axb)) + d = normalize(d) + return d + +def project2(x,n): + """ get direction on plane based on cross product and then project onto the direction """ + d = get_dir_on_plane(x, n) + return project_on_line(x, d) + +def project_vec3(x, n): + """" project vector on normal of plane and then substract from vector to get projection on plane """ + w = project_on_line(x, n) + v = x-w + return v + +def project(x, n): + """ http://www.euclideanspace.com/maths/geometry/elements/plane/lineOnPlane/""" + l = np.linalg.norm(x) + a = normalize(x) + b = normalize(n) + axb = np.cross(a,b) + bxaxb = np.cross(b, axb) + return l * bxaxb + +def project_on_line(x, v): + """https://en.wikipedia.org/wiki/Scalar_projection""" + s = np.dot(x, v) / np.dot(v, v) + return s * v + +def project_onto_plane(x, n): + """https://stackoverflow.com/questions/17915475/how-may-i-project-vectors-onto-a-plane-defined-by-its-orthogonal-vector-in-pytho""" + nl = np.linalg.norm(n) + d = np.dot(x, n) / nl + p = [d * normalize(n)[i] for i in range(len(n))] + return [x[i] - p[i] for i in range(len(x))] + + +def project_vec_on_plane(vec, n): + """https://math.stackexchange.com/questions/633181/formula-to-project-a-vector-onto-a-plane""" + n = normalize(n) + d = np.dot(vec, n) + return vec - np.dot(d, n) + + +def distance_from_point_to_line(p1, p2, vec): + proj = p2+project_on_line(p1, vec) + return np.linalg.norm(proj - p1) + + +def limb_projection(p1, center, n): + #s1 = np.dot(p1, n) / np.dot(p1, p1) + #proj_p1 = p1 - s1*n + + #s2 = np.dot(p2, n) / np.dot(p2, p2) + #proj_p2 = p2 - s2 * n + proj_p1 = project_vec3(p1, n) + proj_center = project_vec3(center, n) + + d = np.linalg.norm(proj_p1-proj_center) + return d + + +def plot_line(ax, start, end,label=None, color=None): + x = start[0], end[0] + y = start[1], end[1] + ax.plot(x, y, label=label, color=color) + +def convert_to_foot_positions(joint_heights): + + n_frames = len(list(joint_heights.items())[0][1][0]) + print(n_frames) + foot_positions = [] + for f in range(n_frames): + foot_positions.append(dict()) + for joint, data in list(joint_heights.items()): + ps, yv, ya = data + for frame_idx, p in enumerate(ps): + foot_positions[frame_idx].update({joint: p}) + return foot_positions + +def plot_foot_positions(ax, foot_positions, bodies,step_size=5): + for f, data in enumerate(foot_positions): + if f%step_size != 0: + continue + for body in [list(bodies.values())[0]]: + start_j = body["start"] + end_j = body["end"] + start = f, data[start_j][1] + end = f+5, data[end_j][1] + plot_line(ax, start, end, color="k") + + +def get_vertical_acceleration(skeleton, frames, joint_name): + """ https://stackoverflow.com/questions/40226357/second-derivative-in-python-scipy-numpy-pandas + """ + ps = [] + for frame in frames: + p = skeleton.nodes[joint_name].get_global_position(frame) + ps.append(p) + ps = np.array(ps) + x = np.linspace(0, len(frames), len(frames)) + ys = np.array(ps[:, 1]) + y_spl = UnivariateSpline(x, ys, s=0, k=4) + velocity = y_spl.derivative(n=1) + acceleration = y_spl.derivative(n=2) + return ps, velocity(x), acceleration(x) + + +def quaternion_to_axis_angle(q): + """http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToAngle/ + + """ + a = 2* math.acos(q[0]) + x = q[1] / math.sqrt(1-q[0]*q[0]) + y = q[2] / math.sqrt(1-q[0]*q[0]) + z = q[3] / math.sqrt(1-q[0]*q[0]) + return normalize([x,y,z]),a + +def get_delta_quaternion(q1,q2): + return quaternion_multiply(quaternion_inverse(q1), q2) + + +def get_angular_velocity(skeleton, frames, joint): + """ http://answers.unity3d.com/questions/49082/rotation-quaternion-to-angular-velocity.html + """ + idx = skeleton.animated_joints.index(joint) * 4 + 3 + angular_velocity = [[0,0,0]] + prev_q = frames[0, idx:idx + 4] + for frame_idx, frame in enumerate(frames[1:]): + q = frames[frame_idx, idx:idx+4] + q_delta = get_delta_quaternion(prev_q, q) + axis, angle = quaternion_to_axis_angle(q_delta) + a = axis * angle + angular_velocity.append(a) + prev_q = q + return np.array(angular_velocity) + + +def get_angular_velocities(skeleton, frames, joints): + anglular_velocity = dict() + for joint in joints: + anglular_velocity[joint] = get_angular_velocity(skeleton, frames, joint) + return anglular_velocity + + +def plot_joint_heights(joint_heights, ground_height=0, frame_range=(None,None)): + plt.figure(1) + ax = plt.subplot(111) + n_frames = 0 + for joint, data in list(joint_heights.items()): + ps, yv, ya = data + if frame_range == (None, None): + start, end = 0, len(ps) + else: + start, end = frame_range + n_frames = end- start + x = np.linspace(start,end, n_frames) + plt.plot(x, ps[start:end,1], label=joint) + plot_line(ax, (start, ground_height),(end, ground_height), "ground") + foot_positions = convert_to_foot_positions(joint_heights) + bodies = {"left":{"start":"LeftHeel", "end": "LeftToeBase"}, "right":{"start":"RightHeel", "end": "RightToeBase"}} + #plot_foot_positions(ax, foot_positions, bodies) + plt.legend() + plt.show(True) + + +def plot_angular_velocities(angular_velocities, frame_range=(None,None)): + plt.figure(1) + ax = plt.subplot(111) + n_frames = 0 + for joint, data in list(angular_velocities.items()): + if frame_range == (None, None): + start, end = 0, len(data) + else: + start, end = frame_range + n_frames = end- start + x = np.linspace(start,end, n_frames) + v = list(map(np.linalg.norm, data[start:end])) + plt.plot(x, np.rad2deg(v), label=joint) + plt.legend() + plt.show(True) + + +def export_constraints(constraints, file_path): + unique_dict = dict() + for frame_idx in constraints: + for c in constraints[frame_idx]: + key = tuple(c.position) + unique_dict[key] = None + + points = [] + for p in list(unique_dict.keys()): + points.append(p) + data = dict() + data["points"] = points + with open(file_path, "w") as out: + json.dump(data, out) + +def plot_constraints(constraints, ground_height=0): + colors ={"RightFoot":"r", "LeftFoot":"g"} + plt.figure(1) + joint_constraints = dict() + ax = plt.subplot(111) + + for frame_idx in constraints: + for c in constraints[frame_idx]: + if c.joint_name not in list(joint_constraints.keys()): + joint_constraints[c.joint_name] = [] + joint_constraints[c.joint_name].append(c.position) + for joint_name in list(joint_constraints.keys()): + temp = np.array(joint_constraints[joint_name]) + y = temp[:, 1] + n_frames = len(y) + x = np.linspace(0, n_frames, n_frames) + + ax.scatter(x,y, label=joint_name, c=colors[joint_name]) + + plot_line(ax, (0, ground_height), (n_frames, ground_height), "ground") + plt.legend() + plt.show(True) + + +def get_random_color(): + random_color = np.random.rand(3, ) + if np.sum(random_color) < 0.5: + random_color += np.array([0, 0, 1]) + return random_color.tolist() + + +def convert_ground_contacts_to_annotation(ground_contacts, joints, n_frames): + data = dict() + data["color_map"] = {j : get_random_color() for j in joints} + data["semantic_annotation"] = dict() + for idx in range(n_frames): + if idx in ground_contacts: + for label in ground_contacts[idx]: + if label not in data["semantic_annotation"]: + data["semantic_annotation"][label] = [] + data["semantic_annotation"][label].append(idx) + return data + + +def save_ground_contact_annotation(ground_contacts, joints, n_frames, filename): + data = convert_ground_contacts_to_annotation(ground_contacts, joints, n_frames) + with open(filename, "w") as out: + json.dump(data, out) + + +def load_ground_contact_annotation(filename, n_frames): + ground_contacts = [[] for f in range(n_frames)] + with open(filename, "r") as in_file: + annotation_data = json.load(in_file) + semantic_annotation = annotation_data["semantic_annotation"] + for label in list(semantic_annotation.keys()): + for idx in semantic_annotation[label]: + ground_contacts[idx].append(label) + + return ground_contacts + + +def save_ground_contact_annotation_merge_labels(ground_contacts, n_frames, left_foot, right_foot, filename): + data = dict() + contact_label = "contact" + no_contact_label = "no_contact" + data["color_map"] = {left_foot: [1,0,0], + right_foot: [0,1,0], + contact_label: [0,0,1], + no_contact_label: [1,1,1]} + data["frame_annotation"] = [] + for idx in range(n_frames): + if left_foot in ground_contacts[idx] and right_foot in ground_contacts[idx]: + annotation = contact_label + elif left_foot in ground_contacts[idx]: + annotation = left_foot + elif right_foot in ground_contacts[idx]: + annotation = right_foot + else: + annotation = no_contact_label + + data["frame_annotation"].append(annotation) + with open(filename, "w") as out: + json.dump(data, out) + +def get_intersection_circle(center1, radius1, center2, radius2): + """ Calculate the projection on the intersection of two spheres defined by two constraints on the ankles and the leg lengths + + http://mrl.snu.ac.kr/Papers/tog2001.pdf + """ + delta = center2 - center1 + d = np.linalg.norm(delta) + c_n = delta / d # normal + + # radius (eq 27) + r1_sq = radius1 * radius1 + r2_sq = radius2 * radius2 + d_sq = d * d + nom = r1_sq - r2_sq + d_sq + c_r_sq = r1_sq - ((nom * nom) / (4 * d_sq)) + if c_r_sq < 0: # no intersection + print("no intersection", c_r_sq) + return + c_r = math.sqrt(c_r_sq) + + # center (eq 29) + x = (r1_sq - r2_sq + d_sq) / (2 * d) + c_c = center1 + x * c_n + return c_c, c_r, c_n + + +def get_intersection_circle2(center1, radius1, center2, radius2): + """ src: http://mathworld.wolfram.com/Sphere-SphereIntersection.html + """ + delta = center2 - center1 + d = np.linalg.norm(delta) + c_n = delta / d # normal + + R = radius1 + r = radius2 + + # translation of the circle center along the normal (eq 5) + x = (d*d - r*r + R*R)/(2*d) + c_c = center1 + x * c_n + + # radius of the sphere (eq 9) + sq = (-d+r-R)*(-d-r+R)*(-d+r+R)*(d+r+R) + c_r = (1/(2*d)) * math.sqrt(sq) + return c_c, c_r, c_n + +def project_point_onto_plane(point, point_on_plane, normal): + """ calculate the projection on the intersection of two spheres defined by two constraints on the ankles and the leg lengths + + http://mrl.snu.ac.kr/Papers/tog2001.pdf + """ + h = np.dot(np.dot(normal, point_on_plane - point), normal) + pp = point + h + return pp + +def project_on_intersection_circle(p, center1, radius1, center2, radius2): + """ calculate the projection on the intersection of two spheres defined by two constraints on the ankles and the leg lengths + + http://mrl.snu.ac.kr/Papers/tog2001.pdf + """ + + c_c, c_r, c_n = get_intersection_circle(center1, radius1, center2, radius2) + + # project root position onto plane on which the circle lies + pp = project_point_onto_plane(p, c_c, c_n) + + # project projected point onto circle + delta = normalize(pp - c_c) + p_c = c_c + delta * c_r + + # set the root position to the projection on the intersection + return p_c + +def smooth_root_positions(positions, window): + h_window = int(window/2) + smoothed_positions = [] + n_pos = len(positions) + for idx, p in enumerate(positions): + start = max(idx-h_window, 0) + end = min(idx + h_window, n_pos) + #print start, end, positions[start:end] + avg_p = np.average(positions[start:end], axis=0) + smoothed_positions.append(avg_p) + return smoothed_positions + + + +def move_to_ground(skeleton, frames, ground_height, foot_joints, start_frame=0, n_frames=5): + minimum_height = guess_ground_height(skeleton, frames, start_frame, n_frames, foot_joints) + for f in frames: + f[:3] += [0, ground_height-minimum_height, 0] + return frames + + +def get_limb_length(skeleton, joint_name, offset=1): + limb_length = np.linalg.norm(skeleton.nodes[joint_name].offset) + limb_length += np.linalg.norm(skeleton.nodes[joint_name].parent.offset) + return limb_length + offset + + +def global_position_to_root_translation(skeleton, frame, joint_name, p): + """ determine necessary root translation to achieve a global position""" + + tframe = np.array(frame) + tframe[:3] = [0,0,0] + parent_joint = skeleton.nodes[joint_name].parent.node_name + parent_m = skeleton.nodes[parent_joint].get_global_matrix(tframe, use_cache=False) + old_global = np.dot(parent_m, skeleton.nodes[joint_name].get_local_matrix(tframe)) + return p - old_global[:3,3] + + +def generate_root_constraint_for_one_foot(skeleton, frame, c): + root = skeleton.aligning_root_node + root_pos = skeleton.nodes[root].get_global_position(frame) + target_length = np.linalg.norm(c.position - root_pos) + limb_length = get_limb_length(skeleton, c.joint_name) + if target_length >= limb_length: + new_root_pos = (c.position + normalize(root_pos - c.position) * limb_length) + #print "one constraint on ", c.joint_name, "- before", root_pos, "after", new_root_pos + return global_position_to_root_translation(skeleton, frame, root, new_root_pos) + + else: + print("no change") + + +def generate_root_constraint_for_two_feet(skeleton, frame, constraint1, constraint2, length_offset=1.0): + """ Set the root position to the projection on the intersection of two spheres """ + root = skeleton.aligning_root_node + # root = self.skeleton.root + p = skeleton.nodes[root].get_global_position(frame) + offset = skeleton.nodes[root].get_global_position(skeleton.identity_frame) + t1 = np.linalg.norm(constraint1.position - p) + t2 = np.linalg.norm(constraint2.position - p) + + c1 = constraint1.position + r1 = get_limb_length(skeleton, constraint1.joint_name)- length_offset + # p1 = c1 + r1 * normalize(p-c1) + c2 = constraint2.position + r2 = get_limb_length(skeleton, constraint2.joint_name) - length_offset + # p2 = c2 + r2 * normalize(p-c2) + if r1 > t1 and r2 > t2: + #print ("no root constraint", t1,t2, r1, r2) + return None + #print ("adapt root for two constraints", t1, t2, r1, r2) + p_c = project_on_intersection_circle(p, c1, r1, c2, r2) + p = global_position_to_root_translation(skeleton, frame, root, p_c) + tframe = np.array(frame) + tframe[:3] = p + new_p = skeleton.nodes[root].get_global_position(tframe) + return p#p_c - offset + + +def smooth_root_translation_at_end(frames, d, window): + root_pos = frames[d, :3] + start_idx = d-window + start = frames[start_idx, :3] + end = root_pos + for i in range(window): + t = float(i) / (window) + frames[start_idx + i, :3] = start * (1 - t) + end * t + + +def smooth_root_translation_at_start(frames, d, window): + start = frames[d, :3] + start_idx = d+window + end = frames[start_idx, :3] + for i in range(window): + t = float(i) / (window) + frames[d + i, :3] = start * (1 - t) + end * t diff --git a/build/lib/anim_utils/retargeting/__init__.py b/build/lib/anim_utils/retargeting/__init__.py new file mode 100644 index 0000000..ceecb03 --- /dev/null +++ b/build/lib/anim_utils/retargeting/__init__.py @@ -0,0 +1,5 @@ +from .analytical import retarget_from_src_to_target, Retargeting, generate_joint_map +from .constrained_retargeting import retarget_from_src_to_target as retarget_from_src_to_target_constrained +from .point_cloud_retargeting import retarget_from_point_cloud_to_target +from .constants import ROCKETBOX_TO_GAME_ENGINE_MAP, ADDITIONAL_ROTATION_MAP,GAME_ENGINE_TO_ROCKETBOX_MAP, ROCKETBOX_ROOT_OFFSET +from .point_cloud_retargeting import PointCloudRetargeting diff --git a/build/lib/anim_utils/retargeting/analytical.py b/build/lib/anim_utils/retargeting/analytical.py new file mode 100644 index 0000000..f3dc1fb --- /dev/null +++ b/build/lib/anim_utils/retargeting/analytical.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +""" +Functions for retargeting based on the paper "Using an Intermediate Skeleton and Inverse Kinematics for Motion Retargeting" +by Monzani et al. +See: http://www.vis.uni-stuttgart.de/plain/vdl/vdl_upload/91_35_retargeting%20monzani00using.pdf +""" +import numpy as np +import math +from transformations import quaternion_matrix, quaternion_multiply, quaternion_about_axis, quaternion_from_matrix +from .constants import OPENGL_UP_AXIS +from .utils import normalize, align_axis, find_rotation_between_vectors, align_root_translation, to_local_cos, get_quaternion_rotation_by_name, apply_additional_rotation_on_frames, project_vector_on_axis, quaternion_from_vector_to_vector +from ..animation_data.skeleton_models import JOINT_CHILD_MAP + + +def create_local_cos_map_using_child_map(skeleton, up_vector, x_vector, child_map=None): + joint_cos_map = dict() + for j in list(skeleton.nodes.keys()): + joint_cos_map[j] = dict() + joint_cos_map[j]["y"] = up_vector + joint_cos_map[j]["x"] = x_vector + + if j == skeleton.root: + joint_cos_map[j]["x"] = (-np.array(x_vector)).tolist() + else: + o = np.array([0, 0, 0]) + if child_map is not None and j in child_map: + child_name = child_map[j] + node = skeleton.nodes[child_name] + o = np.array(node.offset) + elif len(skeleton.nodes[j].children) > 0: + node = skeleton.nodes[j].children[0] + o = np.array(node.offset) + o = normalize(o) + if sum(o * o) > 0: + joint_cos_map[j]["y"] = o + return joint_cos_map + + +def create_local_cos_map(skeleton, up_vector, x_vector): + joint_cos_map = dict() + for j in list(skeleton.nodes.keys()): + joint_cos_map[j] = dict() + joint_cos_map[j]["y"] = up_vector + joint_cos_map[j]["x"] = x_vector + if j == skeleton.root: + joint_cos_map[j]["x"] = (-np.array(x_vector)).tolist() + return joint_cos_map + + +def get_body_x_axis(skeleton): + rh = skeleton.skeleton_model["joints"]["right_hip"] + lh = skeleton.skeleton_model["joints"]["left_hip"] + return get_body_axis(skeleton, rh, lh) + +def get_body_y_axis(skeleton): + a = skeleton.skeleton_model["joints"]["pelvis"] + b = skeleton.skeleton_model["joints"]["head"] + return get_body_axis(skeleton, a,b) + +def get_quaternion_to_axis(skeleton, joint_a, joint_b, axis): + ident_f = skeleton.identity_frame + ap = skeleton.nodes[joint_a].get_global_position(ident_f) + bp = skeleton.nodes[joint_b].get_global_position(ident_f) + delta = bp - ap + delta /= np.linalg.norm(delta) + return quaternion_from_vector_to_vector(axis, delta) + + +def get_body_axis(skeleton, joint_a, joint_b, project=True): + ident_f = skeleton.identity_frame + ap = skeleton.nodes[joint_a].get_global_position(ident_f) + bp = skeleton.nodes[joint_b].get_global_position(ident_f) + delta = bp - ap + m = np.linalg.norm(delta) + if m != 0: + delta /= m + if project: + projection = project_vector_on_axis(delta) + return projection / np.linalg.norm(projection) + else: + return delta + else: + return None + +def rotate_axes(cos, q): + m = quaternion_matrix(q)[:3, :3] + for key, a in list(cos.items()): + cos[key] = np.dot(m, a) + cos[key] = normalize(cos[key]) + return cos + +def get_child_joint(skeleton, inv_joint_map, node_name): + """ Warning output is random if there are more than one child joints + and the value is not specified in the JOINT_CHILD_MAP """ + child_node = None + if len(skeleton.nodes[node_name].children) > 0: + child_node = skeleton.nodes[node_name].children[-1] + if node_name in inv_joint_map: + joint_name = inv_joint_map[node_name] + while joint_name in JOINT_CHILD_MAP: + child_joint_name = JOINT_CHILD_MAP[joint_name] + + # check if child joint is mapped + joint_key = None + if child_joint_name in skeleton.skeleton_model["joints"]: + joint_key = skeleton.skeleton_model["joints"][child_joint_name] + + if joint_key is not None and joint_key in skeleton.nodes: # return child joint + child_node = skeleton.nodes[joint_key] + return child_node + else: #keep traversing until end of child map is reached + if child_joint_name in JOINT_CHILD_MAP: + joint_name = JOINT_CHILD_MAP[child_joint_name] + else: + break + return child_node + +def create_local_cos_map_from_skeleton_axes_with_map(skeleton, flip=1.0, project=True): + body_x_axis = get_body_x_axis(skeleton)*flip + body_y_axis = get_body_y_axis(skeleton) + if np.dot(body_y_axis, body_x_axis) != 0: + body_x_axis = [1,0,0] + if np.dot(body_y_axis, body_x_axis) != 0: + body_x_axis = [0,1,0] + if np.dot(body_y_axis, body_x_axis) != 0: + body_x_axis = [0,0,1] + print("body x axis", body_x_axis) + print("body y axis", body_y_axis) + inv_joint_map = dict((v,k) for k, v in skeleton.skeleton_model["joints"].items()) + joint_cos_map = dict() + for j in list(skeleton.nodes.keys()): + joint_cos_map[j] = dict() + joint_cos_map[j]["y"] = body_y_axis + joint_cos_map[j]["x"] = body_x_axis + + node = skeleton.nodes[j] + child_node = get_child_joint(skeleton, inv_joint_map, node.node_name) + if child_node is None: + continue + + y_axis = get_body_axis(skeleton, j, child_node.node_name, project) + if y_axis is not None: + joint_cos_map[j]["y"] = y_axis + #check if the new y axis is similar to the x axis + z_vector = np.cross(y_axis, joint_cos_map[j]["x"]) + if np.linalg.norm(z_vector) == 0.0: + joint_cos_map[j]["x"] = body_y_axis * -np.sum(joint_cos_map[j]["y"]) + #check for angle and rotate + q = get_quaternion_to_axis(skeleton, j, child_node.node_name, y_axis) + rotate_axes(joint_cos_map[j], q) + else: + joint_cos_map[j]["y"] = None + joint_cos_map[j]["x"] = None + return joint_cos_map + + +def align_root_joint(axes, global_src_x_vec, max_iter_count=10): + # handle special case for the root joint + # apply only the y axis rotation of the Hip to the Game_engine node + not_aligned = True + q = [1, 0, 0, 0] + iter_count = 0 + while not_aligned: + qx, axes = align_axis(axes, "x", global_src_x_vec) # first find rotation to align x axis + q = quaternion_multiply(qx, q) + q = normalize(q) + qy, axes = align_axis(axes, "y", OPENGL_UP_AXIS) # then add a rotation to let the y axis point up + q = quaternion_multiply(qy, q) + q = normalize(q) + dot_y = np.dot(axes["y"], OPENGL_UP_AXIS) + dot_y = min(1, max(dot_y, -1)) + a_y = math.acos(dot_y) + dot_x = np.dot(axes["x"], global_src_x_vec) + dot_x = min(1, max(dot_x, -1)) + a_x = math.acos(dot_x) + iter_count += 1 + not_aligned = a_y > 0.1 or a_x > 0.1 and iter_count < max_iter_count + return q + +def align_joint(local_target_axes, global_src_up_vec, global_src_x_vec): + # first align the twist axis + q, axes = align_axis(local_target_axes, "y", global_src_up_vec) + q = normalize(q) + # then align the swing axis + qx, axes = align_axis(axes, "x", global_src_x_vec) + q = quaternion_multiply(qx, q) + q = normalize(q) + + # handle cases when twist axis alignment was lost + dot = np.dot(axes["y"], global_src_up_vec) + if dot <= -1: + q180 = quaternion_about_axis(np.deg2rad(180), global_src_x_vec) + q180 = normalize(q180) + q = quaternion_multiply(q180, q) + q = normalize(q) + elif abs(dot) != 1.0: + qy, axes = align_axis(axes, "y", global_src_up_vec) + q = quaternion_multiply(qy, q) + q = normalize(q) + return q + + +def find_rotation_analytically(new_skeleton, joint_name, global_src_up_vec, global_src_x_vec, frame, joint_cos_map, apply_spine_fix=False, apply_root_fix=False, max_iter_count=10): + local_target_axes = joint_cos_map[joint_name] + if joint_name == new_skeleton.root and apply_root_fix: + q = align_root_joint(local_target_axes, global_src_x_vec, max_iter_count) + else: + q = align_joint(local_target_axes, global_src_up_vec, global_src_x_vec) + return to_local_cos(new_skeleton, joint_name, frame, q) + + +def create_correction_map(target_skeleton,target_to_src_joint_map, src_cos_map, target_cos_map): + correction_map = dict() + for target_name in target_to_src_joint_map: + src_name = target_to_src_joint_map[target_name] + if src_name in src_cos_map and target_name is not None and target_name in target_cos_map: + src_zero_vector_y = src_cos_map[src_name]["y"] + target_zero_vector_y = target_cos_map[target_name]["y"] + src_zero_vector_x = src_cos_map[src_name]["x"] + target_zero_vector_x = target_cos_map[target_name]["x"] + if target_zero_vector_y is not None and src_zero_vector_y is not None: + q = quaternion_from_vector_to_vector(target_zero_vector_y, src_zero_vector_y) + q = normalize(q) + + m = quaternion_matrix(q)[:3, :3] + target_zero_vector_x = normalize(np.dot(m, target_zero_vector_x)) + qx = quaternion_from_vector_to_vector(target_zero_vector_x, src_zero_vector_x) + q = quaternion_multiply(qx, q) + q = normalize(q) + correction_map[target_name] = q + return correction_map + + +class Retargeting(object): + def __init__(self, src_skeleton, target_skeleton, target_to_src_joint_map, scale_factor=1.0, additional_rotation_map=None, constant_offset=None, place_on_ground=False, force_root_translation=False, ground_height=0): + self.src_skeleton = src_skeleton + self.target_skeleton = target_skeleton + self.target_to_src_joint_map = target_to_src_joint_map + self.src_to_target_joint_map = {v: k for k, v in list(self.target_to_src_joint_map.items())} + self.scale_factor = scale_factor + self.n_params = len(self.target_skeleton.animated_joints) * 4 + 3 + self.ground_height = ground_height + self.rotation_offsets = additional_rotation_map + self.src_inv_joint_map = dict((v,k) for k, v in src_skeleton.skeleton_model["joints"].items()) + self.src_child_map = dict() + for src_name in self.src_skeleton.animated_joints: + src_child = get_child_joint(self.src_skeleton, self.src_inv_joint_map, src_name) + if src_child is not None: + self.src_child_map[src_name] = src_child.node_name + else: + self.src_child_map[src_name] = None + self.target_cos_map = create_local_cos_map_from_skeleton_axes_with_map(self.target_skeleton) + self.src_cos_map = create_local_cos_map_from_skeleton_axes_with_map(self.src_skeleton) + + if "cos_map" in target_skeleton.skeleton_model: + self.target_cos_map.update(target_skeleton.skeleton_model["cos_map"]) + if "cos_map" in src_skeleton.skeleton_model: + self.src_cos_map.update(src_skeleton.skeleton_model["cos_map"]) + self.correction_map = dict() + + spine_joints = ["pelvis","spine", "spine_1","spine_2"] + target_joint_map = self.target_skeleton.skeleton_model["joints"] + self.target_spine_joints =[target_joint_map[j] for j in spine_joints if j in target_joint_map] + + self.correction_map = create_correction_map(self.target_skeleton, self.src_to_target_joint_map, self.src_cos_map, self.target_cos_map) + self.constant_offset = constant_offset + self.place_on_ground = place_on_ground + self.force_root_translation = force_root_translation + self.apply_spine_fix = "neck" in target_joint_map and self.src_skeleton.animated_joints != self.target_skeleton.animated_joints + if "root" in target_joint_map: + self.apply_root_fix = self.target_skeleton.skeleton_model["joints"]["root"] is not None # aligns root up axis with src skeleton up axis + # make sure the src root joint in the target is not None + target_root = self.target_skeleton.skeleton_model["joints"]["root"] + if self.apply_root_fix and target_to_src_joint_map[target_root] is None: + target_to_src_joint_map[target_root] = self.src_skeleton.root + else: + self.apply_root_fix = False + if scale_factor <= 0: + self.auto_scale_factor() + + def auto_scale_factor(self): + """ estimate scale from leg length by gemlongman """ + target_hip_h = self.target_skeleton.get_body_hip2foot_height() + src_hip_h = self.src_skeleton.get_body_hip2foot_height() + self.scale_factor = target_hip_h / src_hip_h + print("debug scale_factor :" + str(target_hip_h)+ " / " +str(src_hip_h) + " = " +str(self.scale_factor)) + + def rotate_bone(self, src_name, target_name, src_frame, target_frame, guess): + q = guess + src_x_axis = self.src_cos_map[src_name]["x"] + src_up_axis = self.src_cos_map[src_name]["y"] + if self.src_cos_map[src_name]["y"] is not None and self.target_cos_map[target_name]["y"] is not None: + global_m = self.src_skeleton.nodes[src_name].get_global_matrix(src_frame)[:3, :3] + global_src_up_vec = normalize(np.dot(global_m, src_up_axis)) + global_src_x_vec = normalize(np.dot(global_m, src_x_axis)) + apply_spine_fix = self.apply_spine_fix and target_name in self.target_spine_joints + q = find_rotation_analytically(self.target_skeleton, target_name, global_src_up_vec, global_src_x_vec, target_frame, self.target_cos_map, apply_spine_fix, self.apply_root_fix) + return q + + def rotate_bone_fast(self, src_name, target_name, src_frame, target_frame, quess): + q = quess + src_child_name = self.src_skeleton.nodes[src_name].children[0].node_name + if src_child_name in self.src_to_target_joint_map: + m = self.src_skeleton.nodes[src_name].get_global_matrix(src_frame)[:3, :3] + gq = quaternion_from_matrix(m) + correction_q = self.correction_map[target_name] + q = quaternion_multiply(gq, correction_q) + q = normalize(q) + q = to_local_cos(self.target_skeleton, target_name, target_frame, q) + return q + + def retarget_frame(self, src_frame, ref_frame): + target_frame = np.zeros(self.n_params) + # copy the root translation assuming the rocketbox skeleton with static offset on the hips is used as source + target_frame[0] = src_frame[0] * self.scale_factor + target_frame[1] = src_frame[1] * self.scale_factor + target_frame[2] = src_frame[2] * self.scale_factor + + if self.constant_offset is not None: + target_frame[:3] += self.constant_offset + animated_joints = self.target_skeleton.animated_joints + target_offset = 3 + + for target_name in animated_joints: + q = get_quaternion_rotation_by_name(target_name, self.target_skeleton.reference_frame, self.target_skeleton, root_offset=3) + + if target_name in self.target_to_src_joint_map.keys(): + + src_name = self.target_to_src_joint_map[target_name] + if src_name is not None and len(self.src_skeleton.nodes[src_name].children)>0: + q = self.rotate_bone(src_name, target_name, src_frame, target_frame, q) + + if ref_frame is not None: + # align quaternion to the reference frame to allow interpolation + # http://physicsforgames.blogspot.de/2010/02/quaternions.html + ref_q = ref_frame[target_offset:target_offset + 4] + if np.dot(ref_q, q) < 0: + q = -q + target_frame[target_offset:target_offset + 4] = q + target_offset += 4 + + # apply offset on the root taking the orientation into account + if self.force_root_translation: + aligning_root = self.target_skeleton.skeleton_model["joints"]["pelvis"] + target_frame = align_root_translation(self.target_skeleton, target_frame, src_frame, aligning_root, self.scale_factor) + return target_frame + + def run(self, src_frames, frame_range): + n_frames = len(src_frames) + target_frames = [] + if n_frames > 0: + if frame_range is None: + frame_range = (0, n_frames) + if self.rotation_offsets is not None: + src_frames = apply_additional_rotation_on_frames(self.src_skeleton.animated_joints, src_frames, self.rotation_offsets) + + ref_frame = None + for idx, src_frame in enumerate(src_frames[frame_range[0]:frame_range[1]]): + target_frame = self.retarget_frame(src_frame, ref_frame) + if ref_frame is None: + ref_frame = target_frame + target_frames.append(target_frame) + target_frames = np.array(target_frames) + if self.place_on_ground: + delta = target_frames[0][1] - self.ground_height + target_frames[:,1] -= delta + return target_frames + + +def generate_joint_map(src_model, target_model, joint_filter=None): + joint_map = dict() + for j in src_model["joints"]: + if joint_filter is not None and j not in joint_filter: + continue + if j in target_model["joints"]: + src = src_model["joints"][j] + target = target_model["joints"][j] + joint_map[target] = src + return joint_map + + +def retarget_from_src_to_target(src_skeleton, target_skeleton, src_frames, joint_map=None, additional_rotation_map=None, scale_factor=1.0, frame_range=None, place_on_ground=False, joint_filter=None, force_root_translation=False): + if joint_map is None: + joint_map = generate_joint_map(src_skeleton.skeleton_model, target_skeleton.skeleton_model, joint_filter) + print('joint map', joint_map) + retargeting = Retargeting(src_skeleton, target_skeleton, joint_map, scale_factor, additional_rotation_map=additional_rotation_map, place_on_ground=place_on_ground, force_root_translation=force_root_translation) + return retargeting.run(src_frames, frame_range) diff --git a/build/lib/anim_utils/retargeting/constants.py b/build/lib/anim_utils/retargeting/constants.py new file mode 100644 index 0000000..408a040 --- /dev/null +++ b/build/lib/anim_utils/retargeting/constants.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import numpy as np +GAME_ENGINE_REFERENCE_POSE_EULER = [0.202552772072, -0.3393745422363281, 10.18097736938018, 0.0, 0.0, 88.15288821532792, -3.3291626376861925, 172.40743933061506, 90.48198857145417, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -5.267224765943357, 5.461144951918523, -108.06852912064531, -15.717336936646204, 0.749500429122681, -31.810810127019366, 5.749795741186075, -0.64655017163842, -43.79621907038145, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 26.628020277759394, 18.180233818114445, 89.72419760530946, 18.24367060730651, 1.5799727651772104, 39.37862756278345, 45.669771502815834, 0.494263941559835, 19.71385918379141, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 31.738570778606658, -0.035796158863762605, -10.010293103153826, 0.0, 0.0, 0.0, 520.8293416407181, -6.305803932868075, -1.875562438841992, 23.726055821805346, 0.0010593260744296063, 3.267962297354599, -60.93853290197474, 0.0020840827755293063, -2.8705207369072694, 0.0, 0.0, 0.0, -158.31965133452601, -12.378967235699056, 6.357392524527775, 19.81125436520809, -0.03971871449276927, -11.895292807406602, -70.75282007667651, -1.2148004469780682, 20.150610072602195, 0.0, 0.0, 0.0] +GAME_ENGINE_T_POSE_QUAT = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.5, 0.5, 0.5, 0.5, -0.4034116551104901, 0.5807400765583645, 0.40341165511049004, 0.5807400765583645, 0.9718569538669156, 0.23557177509311245, -1.3791773619254053e-32, -9.140441879201479e-33, 0.9842093673640157, -0.17700825176506327, -5.454487879647522e-34, 3.0328292674444404e-33, 0.9988215725658287, 0.04853314513943063, -5.451814187658488e-11, -6.046242580699848e-10, 0.7247803814040166, 0.012592769755452584, 0.06644647837285525, -0.6856527447575631, 0.9983567056586047, 0.03270386259217816, -0.025876101002818737, -0.03930360078850377, 0.9965682409037466, 0.05203248433861332, -0.057026576671593956, 0.029871915718325807, 0.9297560338521583, 0.0938946447178257, 0.3530751373573903, -0.045557223234931374, 0.983733142437575, -0.0773187561935722, 0.14664502687446831, -0.06918200997050479, 0.9992691595380858, -0.015669716666339383, -0.028769659982579642, -0.019695518275262152, 0.9975311344163887, 0.04375497165440451, 0.034215414983264525, -0.04297026533543685, 0.9536776598189488, -0.04860775743712403, 0.2797821416180022, -0.09928826874727571, 0.9987647232093224, 0.005804170391239798, -0.03763753948846192, 0.03191794009533443, 0.9966582124862393, 0.051895568999548565, 0.046770662050906374, -0.042329216544453346, 0.9329213679196267, -0.08913572545954243, 0.33917282714358277, -0.08169661592258975, 0.9995615741071782, 0.009141850996134456, -0.017560293698507947, 0.022016407835221498, 0.9998819715691583, -0.0037191151629644803, 0.010434642587798942, -0.010645625742176174, 0.9322424364818923, -0.04990228518632508, 0.342604219994313, -0.1051482286944295, 0.9980915335828159, 0.005996155911584098, -0.04281223678247725, 0.044095907817706795, 0.9963697981737442, 0.045136674083357205, 0.05368016564496531, -0.048252935208484254, 0.2349924837443164, 0.31132145224589375, 0.8367206208319933, 0.38439054180573773, 0.7909589011799936, 0.46413884444997205, 0.3410569180890096, -0.20649292564252283, 0.9802741605131092, 0.09191251147798253, 0.14965372781082692, 0.09065551398812742, 0.7247803805906335, 0.012592769950269537, -0.06644647823624146, 0.6856527456270242, 0.9983567056586047, 0.03270386259217816, 0.02587610100281874, 0.039303600788503784, 0.9965682518976952, 0.05203243726125053, 0.05702650678920791, -0.029871764354427382, 0.9297559857385495, 0.09389467503083726, -0.3530751148595373, 0.045558317035600863, 0.9837333505577588, -0.0773206378561847, -0.14664363717997042, 0.06917989329673528, 0.9992693467177038, -0.015665213954855695, 0.028764041362077865, 0.019697809691502192, 0.9975314823914712, 0.04375194186430184, -0.03421120658988061, 0.042968623024728494, 0.9536775069845468, -0.04860738335400616, -0.2797828498945051, 0.09928792403976064, 0.9987646449635478, 0.005801503439721487, 0.03763947021552947, -0.03191859662597178, 0.9966579624324041, 0.051897847903993356, -0.046772431757685674, 0.04233035471733049, 0.9329213062387378, -0.0891352715094854, -0.3391729808717024, 0.08169717734010994, 0.9995615367687105, 0.009137951896895397, 0.01756132082424807, -0.022018902302604802, 0.9998819504532211, -0.0037172268673331425, -0.010435123687251087, 0.010647796763214817, 0.9322427205484806, -0.049904462569957994, -0.34260361428243297, 0.10514665035361338, 0.9980915983630915, 0.005999462058971182, 0.04281045823362956, -0.04409571858853277, 0.9963701280372368, 0.04513257610822263, -0.0536778650865031, 0.048252516293463533, -0.23499072279120342, -0.3113794298572072, 0.8366855880933993, 0.3844209119450591, 0.7910190999603561, 0.4640967281746974, -0.34091221681567874, 0.206595911918094, 0.9802667852924755, 0.09194228474474263, -0.14970309938761828, -0.09062355081331422, 0.9778768812145672, 0.20918127350714558, 2.5771780613672396e-10, 5.811305966263625e-10, 0.9909380491927413, -0.1343197031789609, -5.066775135883771e-11, -6.8679137736776675e-12, -0.1336980695398881, 0.966385580820868, 0.04173833379190176, -0.2155960270392212, 0.9725347805290746, 0.061775087466743615, 0.22395896183352518, -0.014224016457752058, 0.8338287133786118, -0.5510358060089089, 0.022043010546568757, -0.02456263274824042, 0.9633980641899633, -0.2673236757692123, 0.018056317099312928, 0.00872878577337811, -0.1336980711986006, 0.9663855803373587, -0.04173831577523998, 0.21559603166581406, 0.9725347764612302, 0.06177510013852306, -0.2239589750418668, 0.014224031586612895, 0.8338286703052938, -0.551035871807389, -0.022043023480577864, 0.0245626072356164, 0.963398245692903, -0.26732302190877494, -0.018056289743723242, -0.008728834635171634] +ROCKETBOX_TO_GAME_ENGINE_MAP = dict() +ROCKETBOX_TO_GAME_ENGINE_MAP["Hips"] = "Game_engine" +ROCKETBOX_TO_GAME_ENGINE_MAP["Hips"] = "pelvis" +ROCKETBOX_TO_GAME_ENGINE_MAP["Spine"] = "spine_01" +ROCKETBOX_TO_GAME_ENGINE_MAP["Spine_1"] = "spine_02" +ROCKETBOX_TO_GAME_ENGINE_MAP["LeftShoulder"] = "clavicle_l" +ROCKETBOX_TO_GAME_ENGINE_MAP["RightShoulder"] = "clavicle_r" +ROCKETBOX_TO_GAME_ENGINE_MAP["LeftArm"] = "upperarm_l" +ROCKETBOX_TO_GAME_ENGINE_MAP["RightArm"] = "upperarm_r" +ROCKETBOX_TO_GAME_ENGINE_MAP["LeftForeArm"] = "lowerarm_l" +ROCKETBOX_TO_GAME_ENGINE_MAP["RightForeArm"] = "lowerarm_r" +ROCKETBOX_TO_GAME_ENGINE_MAP["LeftHand"] = "hand_l" +ROCKETBOX_TO_GAME_ENGINE_MAP["RightHand"] = "hand_r" +ROCKETBOX_TO_GAME_ENGINE_MAP["LeftUpLeg"] = "thigh_l" +ROCKETBOX_TO_GAME_ENGINE_MAP["RightUpLeg"] = "thigh_r" +ROCKETBOX_TO_GAME_ENGINE_MAP["LeftLeg"] = "calf_l" +ROCKETBOX_TO_GAME_ENGINE_MAP["RightLeg"] = "calf_r" +ROCKETBOX_TO_GAME_ENGINE_MAP["LeftFoot"] = "foot_l" +ROCKETBOX_TO_GAME_ENGINE_MAP["RightFoot"] = "foot_r" +ROCKETBOX_TO_GAME_ENGINE_MAP["Bip01_L_Toe0"] = "ball_l" +ROCKETBOX_TO_GAME_ENGINE_MAP["Bip01_R_Toe0"] = "ball_r" +ROCKETBOX_TO_GAME_ENGINE_MAP["Bip01_L_Finger3"] = "middle_01_l" +ROCKETBOX_TO_GAME_ENGINE_MAP["Bip01_R_Finger3"] = "middle_01_r" +ROCKETBOX_TO_GAME_ENGINE_MAP["Head"] = "neck_01" +GAME_ENGINE_TO_ROCKETBOX_MAP = {v:k for k,v in list(ROCKETBOX_TO_GAME_ENGINE_MAP.items())} +ADDITIONAL_ROTATION_MAP = dict() +ADDITIONAL_ROTATION_MAP["LeftShoulder"] = [0, 0, -20] +ADDITIONAL_ROTATION_MAP["LeftArm"] = [0, 0, 20] +ADDITIONAL_ROTATION_MAP["RightShoulder"] = [0, 0, 20] +ADDITIONAL_ROTATION_MAP["RightArm"] = [0, 0, -20] +OPENGL_UP_AXIS = np.array([0, 1, 0]) +ROCKETBOX_ROOT_OFFSET = np.array([0, 100.949997, 0]) +EXTRA_ROOT_NAME = "Root" +ROOT_JOINT = "Hips" +ROOT_CHILDREN = ["Spine", "LeftUpLeg","RightUpLeg"] +EXTREMITIES = ["RightUpLeg", "LeftUpLeg", "RightLeg", "LeftLeg", "RightArm", "LeftArm", "RightForeArm", "LeftForeArm"] +GAME_ENGINE_ROOT_JOINT = ROCKETBOX_TO_GAME_ENGINE_MAP[ROOT_JOINT] +GAME_ENGINE_ROOT_CHILDREN = ["spine_01", "clavicle_l", "clavicle_r"]#[ROCKETBOX_TO_GAME_ENGINE_MAP[k] for k in ROOT_CHILDREN] +GAME_ENGINE_EXTREMITIES = [ROCKETBOX_TO_GAME_ENGINE_MAP[k] for k in EXTREMITIES] +GAME_ENGINE_SPINE_OFFSET_LIST = ["pelvis", "spine_01"] # list of bones that have an additional rotation offset +AXES = [[1,0,0], [0,1,0], [0,0,1]] + diff --git a/build/lib/anim_utils/retargeting/constrained_retargeting.py b/build/lib/anim_utils/retargeting/constrained_retargeting.py new file mode 100644 index 0000000..b52796e --- /dev/null +++ b/build/lib/anim_utils/retargeting/constrained_retargeting.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import numpy as np +from transformations import quaternion_from_matrix, quaternion_matrix +from .analytical import Retargeting, generate_joint_map, apply_additional_rotation_on_frames +from ..motion_editing.hybrit_ik import HybritIK + +IK_SETTINGS = { + "tolerance": 0.05, + "optimization_method": "L-BFGS-B", + "max_iterations": 1000, + "interpolation_window": 120, + "transition_window": 60, + "use_euler_representation": False, + "solving_method": "unconstrained", + "activate_look_at": True, + "max_retries": 5, + "success_threshold": 5.0, + "optimize_orientation": True, + "elementary_action_max_iterations": 5, + "elementary_action_optimization_eps": 1.0, + "adapt_hands_during_carry_both": True, + "constrain_place_orientation": False, +} +CONSTRAINED_JOINTS = ["left_wrist","right_wrist", "left_ankle", "right_ankle", "neck"] + + +class KeyframeConstraint(object): + def __init__(self, frame_idx, joint_name, position, orientation=None, look_at=False): + self.frame_idx = frame_idx + self.joint_name = joint_name + self.position = position + self.orientation = orientation + self.look_at = look_at + + def evaluate(self, skeleton, frame): + if self.orientation is not None: + parent_joint = skeleton.nodes[self.joint_name].parent + if parent_joint is not None: + m = quaternion_matrix(self.orientation) + parent_m = parent_joint.get_global_matrix(frame, use_cache=False) + local_m = np.dot(np.linalg.inv(parent_m), m) + q = quaternion_from_matrix(local_m) + idx = skeleton.animated_joints.index(parent_joint.node_name) + # idx = skeleton.nodes[c.joint_name].quaternion_frame_index * 4 + frame[idx:idx + 4] = q + d = self.position - skeleton.nodes[self.joint_name].get_global_position(frame) + return np.dot(d, d) + + +class ConstrainedRetargeting(Retargeting): + def __init__(self, src_skeleton, target_skeleton, target_to_src_joint_map, scale_factor=1.0, additional_rotation_map=None, constant_offset=None, place_on_ground=False, ground_height=0): + Retargeting.__init__(self, src_skeleton, target_skeleton, target_to_src_joint_map, scale_factor, additional_rotation_map, constant_offset, place_on_ground, ground_height) + src_joint_map = src_skeleton.skeleton_model["joints"] + self.constrained_joints = [src_joint_map[j] for j in CONSTRAINED_JOINTS] + self.ik = HybritIK(target_skeleton, IK_SETTINGS) + target_joint_map = target_skeleton.skeleton_model["joints"] + self.ik.add_analytical_ik(target_joint_map["left_wrist"], target_joint_map["left_elbow"], target_joint_map["left_shoulder"], [1,0,0],[0,0,1]) + self.ik.add_analytical_ik(target_joint_map["right_wrist"], target_joint_map["right_elbow"], target_joint_map["right_shoulder"], [1, 0, 0], [0, 0, 1]) + self.ik.add_analytical_ik(target_joint_map["right_ankle"], target_joint_map["right_knee"], target_joint_map["right_hip"], [1, 0, 0], [0, 0, 1]) + self.ik.add_analytical_ik(target_joint_map["left_ankle"], target_joint_map["left_knee"], target_joint_map["left_hip"], [1, 0, 0], [0, 0, 1]) + + def generate_ik_constraints(self, frame): + constraints = [] + for j in self.constrained_joints: + p = self.src_skeleton.nodes[j].get_global_position(frame) + c = KeyframeConstraint(0,j, p) + constraints.append(c) + return constraints + + def run(self, src_frames, frame_range): + n_frames = len(src_frames) + target_frames = [] + if n_frames > 0: + if frame_range is None: + frame_range = (0, n_frames) + + if self.additional_rotation_map is not None: + src_frames = apply_additional_rotation_on_frames(self.src_skeleton.animated_joints, src_frames, self.additional_rotation_map) + + ref_frame = None + for idx, src_frame in enumerate(src_frames[frame_range[0]:frame_range[1]]): + print("retarget frame", idx) + ik_constraints = self.generate_ik_constraints(src_frame) + target_frame = self.retarget_frame(src_frame, ref_frame) + target_frame = self.ik.modify_frame(target_frame, ik_constraints) + if ref_frame is None: + ref_frame = target_frame + target_frames.append(target_frame) + target_frames = np.array(target_frames) + if self.place_on_ground: + delta = target_frames[0][1] - self.ground_height + target_frames[:,1] -= delta + return target_frames + + +def retarget_from_src_to_target(src_skeleton, target_skeleton, src_frames, joint_map=None, + additional_rotation_map=None, scale_factor=1.0, frame_range=None, + place_on_ground=False): + if joint_map is None: + joint_map = generate_joint_map(src_skeleton.skeleton_model, target_skeleton.skeleton_model) + retargeting = ConstrainedRetargeting(src_skeleton, target_skeleton, joint_map, scale_factor, + additional_rotation_map=additional_rotation_map, place_on_ground=place_on_ground) + return retargeting.run(src_frames, frame_range) diff --git a/build/lib/anim_utils/retargeting/fast_point_cloud_retargeting.py b/build/lib/anim_utils/retargeting/fast_point_cloud_retargeting.py new file mode 100644 index 0000000..041c0e7 --- /dev/null +++ b/build/lib/anim_utils/retargeting/fast_point_cloud_retargeting.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +""" +Functions for retargeting based on the paper "Using an Intermediate Skeleton and Inverse Kinematics for Motion Retargeting" +by Monzani et al. +See: http://www.vis.uni-stuttgart.de/plain/vdl/vdl_upload/91_35_retargeting%20monzani00using.pdf +""" +import numpy as np +import math +from .constants import OPENGL_UP_AXIS, GAME_ENGINE_SPINE_OFFSET_LIST +from .utils import normalize, align_axis, find_rotation_between_vectors, align_root_translation, to_local_cos, get_quaternion_rotation_by_name, apply_additional_rotation_on_frames, project_vector_on_axis, quaternion_from_vector_to_vector +from transformations import quaternion_matrix, quaternion_multiply, quaternion_about_axis, quaternion_from_matrix +from .point_cloud_retargeting import create_local_cos_map_from_skeleton_axes_with_map, get_parent_map, get_children_map, JOINT_CHILD_MAP + + + + +def apply_manual_fixes(joint_cos_map, joints): + for j in joints: + if j in joint_cos_map: + joint_cos_map[j]["x"] *= -1 + + +def get_child_joint2(skeleton_model, inv_joint_map, node_name, src_children_map): + """ Warning output is random if there are more than one child joints + and the value is not specified in the JOINT_CHILD_MAP """ + child_name = None + if node_name in src_children_map and len(src_children_map[node_name]) > 0: + child_name = src_children_map[node_name][-1] + if node_name in inv_joint_map: + joint_name = inv_joint_map[node_name] + while joint_name in JOINT_CHILD_MAP: + _child_joint_name = JOINT_CHILD_MAP[joint_name] + + # check if child joint is mapped + joint_key = None + if _child_joint_name in skeleton_model["joints"]: + joint_key = skeleton_model["joints"][_child_joint_name] + + if joint_key is not None: # return child joint + child_name = joint_key + return child_name + else: #keep traversing until end of child map is reached + if _child_joint_name in JOINT_CHILD_MAP: + joint_name = JOINT_CHILD_MAP[_child_joint_name] + #print(joint_name) + else: + break + return child_name + + +class FastPointCloudRetargeting(object): + def __init__(self, src_skeleton, src_joints, target_skeleton, target_to_src_joint_map, scale_factor=1.0, additional_rotation_map=None, constant_offset=None, place_on_ground=False, ground_height=0): + self.src_skeleton = src_skeleton + self.src_joints = src_joints + self.src_model = src_skeleton.skeleton_model + self.target_skeleton = target_skeleton + self.target_to_src_joint_map = target_to_src_joint_map + self.src_to_target_joint_map = {v: k for k, v in list(self.target_to_src_joint_map.items())} + print("src to traget map", self.src_to_target_joint_map) + print("target to src map", self.target_to_src_joint_map) + self.scale_factor = scale_factor + self.n_params = len(self.target_skeleton.animated_joints) * 4 + 3 + self.ground_height = ground_height + self.additional_rotation_map = additional_rotation_map + self.src_inv_joint_map = dict((v,k) for k, v in src_skeleton.skeleton_model["joints"].items()) + + self.target_cos_map = create_local_cos_map_from_skeleton_axes_with_map(self.target_skeleton) + self.src_cos_map = create_local_cos_map_from_skeleton_axes_with_map(self.src_skeleton, flip=1.0, project=True) + + if "cos_map" in target_skeleton.skeleton_model: + self.target_cos_map.update(target_skeleton.skeleton_model["cos_map"]) + if "x_cos_fixes" in target_skeleton.skeleton_model: + apply_manual_fixes(self.target_cos_map, target_skeleton.skeleton_model["x_cos_fixes"]) + if "cos_map" in src_skeleton.skeleton_model: + self.src_cos_map.update(src_skeleton.skeleton_model["cos_map"]) + if "x_cos_fixes" in src_skeleton.skeleton_model: + apply_manual_fixes(self.src_cos_map, src_skeleton.skeleton_model["x_cos_fixes"]) + self.correction_map = dict() + self.create_correction_map() + self.constant_offset = constant_offset + self.place_on_ground = place_on_ground + self.apply_spine_fix = self.src_skeleton.animated_joints != self.target_skeleton.animated_joints + + self.src_child_map = dict() + self.src_parent_map = get_parent_map(src_joints) + src_children_map = get_children_map(src_joints) + for src_name in self.src_joints: + src_child = get_child_joint2(self.src_model, self.src_inv_joint_map, src_name, src_children_map) + if src_child is not None: + self.src_parent_map[src_child] = src_name + self.src_child_map[src_name] = src_child + else: + self.src_child_map[src_name] = None + # print("ch",self.src_child_map) + # for j in ["pelvis", "spine", "spine_1", "spine_2"]: + # if j in target_joints: + src_joint_map = self.src_model["joints"] + for j in ["neck", "spine_2", "spine_1", "spine"]: + if j in src_joint_map: + self.src_parent_map["spine_03"] = "pelvis" + self.src_child_map[src_joint_map["pelvis"]] = src_joint_map["neck"] # "pelvis" "neck_01" + + self.constant_offset = constant_offset + self.place_on_ground = place_on_ground + self.temp_frame_data = dict() + + self.target_cos_map = create_local_cos_map_from_skeleton_axes_with_map(self.target_skeleton) + if "cos_map" in target_skeleton.skeleton_model: + self.target_cos_map.update(target_skeleton.skeleton_model["cos_map"]) + if "x_cos_fixes" in target_skeleton.skeleton_model: + apply_manual_fixes(self.target_cos_map, target_skeleton.skeleton_model["x_cos_fixes"]) + + target_joints = self.target_skeleton.skeleton_model["joints"] + self.target_spine_joints = [target_joints[j] for j in ["neck", "spine_2", "spine_1", "spine"] if + j in target_joints] # ["spine_03", "neck_01"] + self.target_ball_joints = [target_joints[j] for j in ["left_shoulder", "right_shoulder", "left_hip", "right_hip"] if + j in target_joints] # ["thigh_r", "thigh_l", "upperarm_r", "upperarm_l"] + self.target_ankle_joints = [target_joints[j] for j in ["left_ankle", "right_ankle"] if j in target_joints] + self.clavicle_joints = [target_joints[j] for j in ["right_clavicle", "left_clavicle"] if j in target_joints] + if "neck" in target_joints: + self.target_neck_joint = target_joints["neck"] + else: + self.target_neck_joint = None + + def create_correction_map(self): + self.correction_map = dict() + joint_map = self.target_skeleton.skeleton_model["joints"] + for target_name in self.target_to_src_joint_map: + src_name = self.target_to_src_joint_map[target_name] + if src_name in self.src_cos_map and target_name is not None: + src_zero_vector_y = self.src_cos_map[src_name]["y"] + target_zero_vector_y = self.target_cos_map[target_name]["y"] + src_zero_vector_x = self.src_cos_map[src_name]["x"] + target_zero_vector_x = self.target_cos_map[target_name]["x"] + if target_zero_vector_y is not None and src_zero_vector_y is not None: + q = quaternion_from_vector_to_vector(target_zero_vector_y, src_zero_vector_y) + q = normalize(q) + + if target_name in [joint_map["pelvis"], joint_map["spine"], joint_map["spine_1"]] and False:#,, joint_map["spine_2"] + # add offset rotation to spine based on an upright reference pose + m = quaternion_matrix(q)[:3, :3] + v = normalize(np.dot(m, target_zero_vector_y)) + node = self.target_skeleton.nodes[target_name] + t_pose_global_m = node.get_global_matrix(self.target_skeleton.reference_frame)[:3, :3] + global_original = np.dot(t_pose_global_m, v) + global_original = normalize(global_original) + qoffset = find_rotation_between_vectors(OPENGL_UP_AXIS, global_original) + q = quaternion_multiply(qoffset, q) + q = normalize(q) + + m = quaternion_matrix(q)[:3, :3] + target_zero_vector_x = normalize(np.dot(m, target_zero_vector_x)) + qx = quaternion_from_vector_to_vector(target_zero_vector_x, src_zero_vector_x) + q = quaternion_multiply(qx, q) + q = normalize(q) + self.correction_map[target_name] = q + + def rotate_bone(self, src_name, target_name, src_frame, target_frame, quess): + q = quess + #src_child_name = self.src_skeleton.nodes[src_name].children[0].node_name + #print("in", src_child_name in self.src_to_target_joint_map) + if target_name in self.correction_map: + m = self.src_skeleton.nodes[src_name].get_global_matrix(src_frame)[:3, :3] + gq = quaternion_from_matrix(m) + correction_q = self.correction_map[target_name] + q = quaternion_multiply(gq, correction_q) + q = normalize(q) + q = to_local_cos(self.target_skeleton, target_name, target_frame, q) + return q + + def get_rotation_from_pc(self, src_name, target_name, src_pc, src_frame): + child_name = self.src_child_map[src_name] + global_src_up_vec, global_src_x_vec = self.estimate_src_joint_cos(src_name, child_name, target_name, src_pc) + if "Spine" in src_name: + print("axis",src_name, global_src_up_vec, global_src_up_vec) + q = [1, 0, 0, 0] + local_target_axes = self.src_cos_map[src_name] + qy, axes = align_axis(local_target_axes, "y", global_src_up_vec) + q = quaternion_multiply(qy, q) + q = normalize(q) + if global_src_x_vec is not None: + qx, axes = align_axis(axes, "x", global_src_x_vec) + q = quaternion_multiply(qx, q) + q = normalize(q) + return to_local_cos(self.src_skeleton, src_name, src_frame, q) + + def convert_pc_to_frame(self, src_pc, ref_frame): + src_frame = np.zeros(ref_frame.shape) + src_frame[:3] = np.array(src_frame[0]) + src_frame_offset = 3 + animated_joints = self.src_skeleton.animated_joints + for src_name in animated_joints: + q = get_quaternion_rotation_by_name(src_name, self.src_skeleton.reference_frame, + self.src_skeleton, + root_offset=3) + if src_name in self.src_joints.keys(): + src_child_name = self.src_child_map[src_name] + if src_child_name in self.src_joints.keys() and src_name in self.src_to_target_joint_map.keys(): + target_name = self.src_to_target_joint_map[src_name] + q = self.get_rotation_from_pc(src_name, target_name, src_pc, src_frame) + else: + print("ignore pc", src_name, src_name in self.src_to_target_joint_map.keys()) + if ref_frame is not None: + q = q if np.dot(ref_frame[src_frame_offset:src_frame_offset + 4], q) >= 0 else -q + src_frame[src_frame_offset:src_frame_offset + 4] = q + src_frame_offset += 4 + #print("src", src_frame[3:7]) + return src_frame + + def retarget_frame(self, src_pc, ref_frame): + print("fast retargeting") + self.temp_frame_data = dict() + src_frame = self.convert_pc_to_frame(src_pc, ref_frame) + target_frame = np.zeros(self.n_params) + # copy the root translation assuming the rocketbox skeleton with static offset on the hips is used as source + target_frame[:3] = np.array(src_frame[:3]) * self.scale_factor + + if self.constant_offset is not None: + target_frame[:3] += self.constant_offset + animated_joints = self.target_skeleton.animated_joints + target_offset = 3 + self.src_spine_joints = ["Spine", "Spine1"] + for target_name in animated_joints: + q = get_quaternion_rotation_by_name(target_name, self.target_skeleton.reference_frame, self.target_skeleton, root_offset=3) + if target_name in self.target_to_src_joint_map.keys(): + src_name = self.target_to_src_joint_map[target_name] + if src_name is not None and len(self.src_skeleton.nodes[src_name].children)>0 and src_name not in self.src_spine_joints: + q = self.rotate_bone(src_name, target_name, src_frame, target_frame, q) + print("rotate", src_name, target_name) + else: + print("ignore", src_name,target_name) + if ref_frame is not None and False: + # align quaternion to the reference frame to allow interpolation + # http://physicsforgames.blogspot.de/2010/02/quaternions.html + ref_q = ref_frame[target_offset:target_offset + 4] + if np.dot(ref_q, q) < 0: + q = -q + target_frame[target_offset:target_offset + 4] = q + target_offset += 4 + + return target_frame + + + def estimate_src_joint_cos(self, src_name, child_name, target_name, src_frame): + joint_idx = self.src_joints[src_name]["index"] + child_idx = self.src_joints[child_name]["index"] + global_src_up_vec = src_frame[child_idx] - src_frame[joint_idx] + global_src_up_vec /= np.linalg.norm(global_src_up_vec) + self.temp_frame_data[src_name] = global_src_up_vec + if target_name == self.target_skeleton.skeleton_model["joints"]["pelvis"]: + left_hip = self.src_model["joints"]["left_hip"] + right_hip = self.src_model["joints"]["right_hip"] + left_hip_idx = self.src_joints[left_hip]["index"] + right_hip_idx = self.src_joints[right_hip]["index"] + global_src_x_vec = src_frame[left_hip_idx] - src_frame[right_hip_idx] + global_src_x_vec /= np.linalg.norm(global_src_x_vec) + elif target_name in self.target_spine_joints or target_name == "CC_Base_Waist": # find x vector from shoulders + left_shoulder = self.src_model["joints"]["left_shoulder"] + right_shoulder = self.src_model["joints"]["right_shoulder"] + left_shoulder_idx = self.src_joints[left_shoulder]["index"] + right_shoulder_idx = self.src_joints[right_shoulder]["index"] + global_src_x_vec = src_frame[left_shoulder_idx] - src_frame[right_shoulder_idx] + global_src_x_vec /= np.linalg.norm(global_src_x_vec) + elif target_name in self.target_ball_joints: # use x vector of child + child_child_name = self.src_child_map[child_name] + child_child_idx = self.src_joints[child_child_name]["index"] + child_global_src_up_vec = src_frame[child_child_idx] - src_frame[child_idx] + child_global_src_up_vec /= np.linalg.norm(child_global_src_up_vec) + + global_src_x_vec = np.cross(global_src_up_vec, child_global_src_up_vec) + + global_src_x_vec /= np.linalg.norm(global_src_x_vec) + + else: # find x vector by cross product with parent + global_src_x_vec = None + if src_name in self.src_parent_map: + parent_joint = self.src_parent_map[src_name] + # print("estimate cos", src_name, target_name) + # if target_name in self.clavicle_joints: + # parent_joint = self.target_neck_joint + # print("set parent for", target_name, "to", self.target_neck_joint) + if parent_joint in self.temp_frame_data: + global_parent_up_vector = self.temp_frame_data[parent_joint] + global_src_x_vec = np.cross(global_src_up_vec, global_parent_up_vector) + global_src_x_vec /= np.linalg.norm(global_src_x_vec) + # print("apply",src_name, parent_joint, global_src_x_vec) + # if target_name in ["calf_l", "calf_r","thigh_r","thigh_l", "spine_03","neck_01","lowerarm_r","lowerarm_l"]: + if target_name not in self.target_ankle_joints: + global_src_x_vec = - global_src_x_vec + # global_src_x_vec = None + # if global_src_x_vec is None: + # print("did not find vector", target_name, parent_joint, self.target_skeleton.root) + # else: + # print("ignore", target_name) + + return global_src_up_vec, global_src_x_vec + +def generate_joint_map(src_model, target_model, joint_filter=None): + joint_map = dict() + for j in src_model["joints"]: + if joint_filter is not None and j not in joint_filter: + continue + if j in target_model["joints"]: + src = src_model["joints"][j] + target = target_model["joints"][j] + joint_map[target] = src + return joint_map + + + diff --git a/build/lib/anim_utils/retargeting/point_cloud_retargeting.py b/build/lib/anim_utils/retargeting/point_cloud_retargeting.py new file mode 100644 index 0000000..67ec882 --- /dev/null +++ b/build/lib/anim_utils/retargeting/point_cloud_retargeting.py @@ -0,0 +1,523 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +""" +Functions for retargeting based on the paper "Using an Intermediate Skeleton and Inverse Kinematics for Motion Retargeting" +by Monzani et al. +See: http://www.vis.uni-stuttgart.de/plain/vdl/vdl_upload/91_35_retargeting%20monzani00using.pdf +""" +import numpy as np +import math +from .constants import OPENGL_UP_AXIS, GAME_ENGINE_SPINE_OFFSET_LIST +from transformations import quaternion_matrix, quaternion_multiply, quaternion_about_axis, quaternion_inverse, quaternion_from_matrix +from .utils import normalize, align_axis, find_rotation_between_vectors, align_root_translation, to_local_cos, get_quaternion_rotation_by_name, apply_additional_rotation_on_frames, project_vector_on_axis, quaternion_from_vector_to_vector +from ..animation_data.skeleton_models import JOINT_CHILD_MAP +from .analytical import create_local_cos_map_from_skeleton_axes_with_map + +JOINT_CHILD_MAP = dict() +JOINT_CHILD_MAP["root"] = "pelvis" +JOINT_CHILD_MAP["pelvis"] = "spine_2" +JOINT_CHILD_MAP["spine_2"] = "neck" +JOINT_CHILD_MAP["neck"] = "head" +JOINT_CHILD_MAP["left_clavicle"] = "left_shoulder" +JOINT_CHILD_MAP["left_shoulder"] = "left_elbow" +JOINT_CHILD_MAP["left_elbow"] = "left_wrist" +JOINT_CHILD_MAP["left_wrist"] = "left_finger" +JOINT_CHILD_MAP["right_clavicle"] = "right_shoulder" +JOINT_CHILD_MAP["right_shoulder"] = "right_elbow" +JOINT_CHILD_MAP["right_elbow"] = "right_wrist" +JOINT_CHILD_MAP["right_wrist"] = "right_finger" +JOINT_CHILD_MAP["left_hip"] = "left_knee" +JOINT_CHILD_MAP["left_knee"] = "left_ankle" +JOINT_CHILD_MAP["right_elbow"] = "right_wrist" +JOINT_CHILD_MAP["right_hip"] = "right_knee" +JOINT_CHILD_MAP["right_knee"] = "right_ankle" +JOINT_CHILD_MAP["left_ankle"] = "left_toe" +JOINT_CHILD_MAP["right_ankle"] = "right_toe" + + +def estimate_correction(target_zero_vector_y, target_zero_vector_x, src_zero_vector_y, src_zero_vector_x): + q = quaternion_from_vector_to_vector(target_zero_vector_y, src_zero_vector_y) + q = normalize(q) + m = quaternion_matrix(q)[:3, :3] + target_zero_vector_x = normalize(np.dot(m, target_zero_vector_x)) + qx = quaternion_from_vector_to_vector(target_zero_vector_x, src_zero_vector_x) + q = quaternion_multiply(qx, q) + q = normalize(q) + return q + +def create_correction_map(target_skeleton, target_to_src_joint_map, target_cos_map, src_cos_map): + correction_map = dict() + joint_map = target_skeleton.skeleton_model["joints"] + for target_name in target_to_src_joint_map: + src_name = target_to_src_joint_map[target_name] + if src_name in src_cos_map and target_name is not None: + src_zero_vector_y = src_cos_map[src_name]["y"] + target_zero_vector_y = target_cos_map[target_name]["y"] + src_zero_vector_x = src_cos_map[src_name]["x"] + target_zero_vector_x = target_cos_map[target_name]["x"] + if target_zero_vector_y is not None and src_zero_vector_y is not None: + q = estimate_correction(target_zero_vector_y, target_zero_vector_x, src_zero_vector_y, src_zero_vector_x) + correction_map[target_name] = q + return correction_map + + +def get_quaternion_to_axis(skeleton, joint_a, joint_b, axis): + ident_f = skeleton.identity_frame + ap = skeleton.nodes[joint_a].get_global_position(ident_f) + bp = skeleton.nodes[joint_b].get_global_position(ident_f) + delta = bp - ap + delta /= np.linalg.norm(delta) + return quaternion_from_vector_to_vector(axis, delta) + + +def rotate_axes2(cos, q): + m = quaternion_matrix(q)[:3, :3] + aligned_axes = dict() + for key, a in list(cos.items()): + aligned_axes[key] = np.dot(m, a) + aligned_axes[key] = normalize(aligned_axes[key]) + return aligned_axes + + +def get_child_joint(skeleton_model, inv_joint_map, node_name, src_children_map): + """ Warning output is random if there are more than one child joints + and the value is not specified in the JOINT_CHILD_MAP """ + child_name = None + if node_name in src_children_map and len(src_children_map[node_name]) > 0: + child_name = src_children_map[node_name][-1] + if node_name in inv_joint_map: + joint_name = inv_joint_map[node_name] + while joint_name in JOINT_CHILD_MAP: + _child_joint_name = JOINT_CHILD_MAP[joint_name] + + # check if child joint is mapped + joint_key = None + if _child_joint_name in skeleton_model["joints"]: + joint_key = skeleton_model["joints"][_child_joint_name] + + if joint_key is not None: # return child joint + child_name = joint_key + return child_name + else: #keep traversing until end of child map is reached + if _child_joint_name in JOINT_CHILD_MAP: + joint_name = JOINT_CHILD_MAP[_child_joint_name] + #print(joint_name) + else: + break + return child_name + +def rotate_axes_in_place(cos, q): + m = quaternion_matrix(q)[:3, :3] + for key, a in list(cos.items()): + cos[key] = np.dot(m, a) + cos[key] = normalize(cos[key]) + return cos + +def align_axis_in_place(axes, key, new_vec): + q = quaternion_from_vector_to_vector(axes[key], new_vec) + aligned_axes = rotate_axes_in_place(axes, q) + return q, aligned_axes + + +def to_local_cos_fast(skeleton, node_name, frame, q): + # bring into parent coordinate system + pm = np.array(skeleton.nodes[node_name].get_global_matrix(frame, use_cache=True)[:3,:3]) + inv_p = quaternion_inverse(quaternion_from_matrix(pm)) + return quaternion_multiply(inv_p, q) + +def align_root_joint(new_skeleton, free_joint_name, axes, global_src_up_vec, global_src_x_vec,joint_cos_map, max_iter_count=10): + # handle special case for the root joint + # apply only the y axis rotation of the Hip to the Game_engine node + q = [1, 0, 0, 0] + #apply first time + qx, axes = align_axis_in_place(axes, "x", global_src_x_vec) # first find rotation to align x axis + q = quaternion_multiply(qx, q) + q = normalize(q) + + qy, axes = align_axis_in_place(axes, "y", global_src_up_vec) # then add a rotation to let the y axis point up + q = quaternion_multiply(qy, q) + q = normalize(q) + + #apply second time + qx, axes = align_axis_in_place(axes, "x", global_src_x_vec) # first find rotation to align x axis + q = quaternion_multiply(qx, q) + q = normalize(q) + qy, axes = align_axis_in_place(axes, "y", global_src_up_vec) # then add a rotation to let the y axis point up + q = quaternion_multiply(qy, q) + q = normalize(q) + + # print("handle special case for pelvis") + # handle special case of applying the x axis rotation of the Hip to the pelvis + node = new_skeleton.nodes[free_joint_name] + t_pose_global_m = node.get_global_matrix(new_skeleton.reference_frame)[:3, :3] + global_original = np.dot(t_pose_global_m, joint_cos_map[free_joint_name]["y"]) + global_original = normalize(global_original) + qoffset = find_rotation_between_vectors(OPENGL_UP_AXIS, global_original) + q = quaternion_multiply(q, qoffset) + q = normalize(q) + return q + +def align_joint(local_target_axes, up_vec, x_vec): + q = [1, 0, 0, 0] + qy, axes = align_axis_in_place(local_target_axes, "y", up_vec) + q = quaternion_multiply(qy, q) + q = normalize(q) + + # then align the twisting angles + if x_vec is not None: + qx, axes = align_axis_in_place(axes, "x", x_vec) + q = quaternion_multiply(qx, q) + q = normalize(q) + # print("set twist angle", free_joint_name, twist_angle) + return q + +def find_rotation_analytically(new_skeleton, free_joint_name, target, frame, joint_cos_map, is_root=False, max_iter_count=10, twist_angle=None): + global_src_up_vec = target[0] + if twist_angle is None: + global_src_x_vec = target[1] + else: + global_src_x_vec = None + local_target_axes = dict(joint_cos_map[free_joint_name]) + + if is_root: + q = align_root_joint(new_skeleton, free_joint_name, local_target_axes, global_src_up_vec,global_src_x_vec, joint_cos_map, max_iter_count) + + else: + # first align the bone vectors + q = [1, 0, 0, 0] + qy, axes = align_axis_in_place(local_target_axes, "y", global_src_up_vec) + q = quaternion_multiply(qy, q) + q = normalize(q) + + # then align the twisting angles + if global_src_x_vec is not None: + qx, axes = align_axis_in_place(axes, "x", global_src_x_vec) + q = quaternion_multiply(qx, q) + q = normalize(q) + + #if "FK" in free_joint_name: + # q = to_local_cos(new_skeleton, free_joint_name, frame, q) + if new_skeleton.nodes[free_joint_name].parent is not None: + #if "upLeg" in free_joint_name: # it does not work for the legs for some reason + # q = to_local_cos(new_skeleton, new_skeleton.nodes[free_joint_name].parent.node_name, frame, q) + #else: + q = to_local_cos(new_skeleton, new_skeleton.nodes[free_joint_name].parent.node_name, frame, q) + + if twist_angle is not None: + # separate rotation + local_twist_axis = np.array(joint_cos_map[free_joint_name]["y"]) + swing_q, twist_q = swing_twist_decomposition(q, local_twist_axis) + # replace + twist_q = quaternion_about_axis(-twist_angle, local_twist_axis) + q = quaternion_multiply(swing_q, twist_q) + q = normalize(q) + return q + + +def find_rotation_analytically_with_guess(new_skeleton, free_joint_name, target, frame, joint_cos_map, prev_global_q, is_root=False, max_iter_count = 10): + global_src_up_vec = target[0] + global_src_x_vec = target[1] + local_target_axes = joint_cos_map[free_joint_name] + rotated_axes = rotate_axes2(local_target_axes, prev_global_q) + #print("rotate",local_target_axes, rotated_axes, prev_global_q) + #print("") + if is_root: + q = align_root_joint(new_skeleton, free_joint_name, rotated_axes, global_src_up_vec,global_src_x_vec, joint_cos_map, max_iter_count) + else: + q = align_joint(rotated_axes, global_src_up_vec, global_src_x_vec) + q = quaternion_multiply(q, prev_global_q) + q = normalize(q) + return to_local_cos_fast(new_skeleton, free_joint_name, frame, q) + + +def get_parent_map(joints): + """Returns a dict of node names to their parent node's name""" + parent_dict = dict() + for joint_name in joints.keys(): + parent_dict[joint_name] = joints[joint_name]['parent'] + return parent_dict + + +def get_children_map(joints): + """Returns a dict of node names to a list of children names""" + child_dict = dict() + for joint_name in joints.keys(): + parent_name = joints[joint_name]['parent'] + if parent_name not in child_dict: + child_dict[parent_name] = list() + child_dict[parent_name].append(joint_name) + return child_dict + +def swing_twist_decomposition(q, twist_axis): + """ code by janis sprenger based on + Dobrowsolski 2015 Swing-twist decomposition in Clifford algebra. https://arxiv.org/abs/1506.05481 + """ + #q = normalize(q) + #twist_axis = np.array((q * offset))[0] + projection = np.dot(twist_axis, np.array([q[1], q[2], q[3]])) * twist_axis + twist_q = np.array([q[0], projection[0], projection[1],projection[2]]) + if np.linalg.norm(twist_q) == 0: + twist_q = np.array([1,0,0,0]) + twist_q = normalize(twist_q) + swing_q = quaternion_multiply(q, quaternion_inverse(twist_q))#q * quaternion_inverse(twist) + return swing_q, twist_q + + +class PointCloudRetargeting(object): + def __init__(self, src_joints, src_model, target_skeleton, target_to_src_joint_map, scale_factor=1.0, additional_rotation=None, constant_offset=None, place_on_ground=False, ground_height=0): + self.src_joints = src_joints + self.src_model = src_model + self.target_skeleton = target_skeleton + self.target_to_src_joint_map = target_to_src_joint_map + if target_skeleton.skeleton_model["joints"]["pelvis"] is not None: + self.target_skeleton_root = target_skeleton.skeleton_model["joints"]["pelvis"] + else: + self.target_skeleton_root = target_skeleton.root + + #FIXME: enable spine during retargeting + for j in [ "spine_1", "spine"]:#"spine_2", + k = self.target_skeleton.skeleton_model["joints"][j] + self.target_to_src_joint_map[k] = None + + self.src_to_target_joint_map = {v: k for k, v in list(self.target_to_src_joint_map.items())} + self.scale_factor = scale_factor + self.n_params = len(self.target_skeleton.animated_joints) * 4 + 3 + self.ground_height = ground_height + self.additional_rotation = additional_rotation + self.src_inv_joint_map = dict((v,k) for k, v in src_model["joints"].items()) + self.src_child_map = dict() + self.src_parent_map = get_parent_map(src_joints) + src_children_map = get_children_map(src_joints) + for src_name in self.src_joints: + src_child = get_child_joint(self.src_model, self.src_inv_joint_map, src_name, src_children_map) + if src_child is not None: + self.src_parent_map[src_child] = src_name + self.src_child_map[src_name] = src_child + else: + self.src_child_map[src_name] = None + #print("ch",self.src_child_map) + #for j in ["pelvis", "spine", "spine_1", "spine_2"]: + # if j in target_joints: + src_joint_map = self.src_model["joints"] + for j in ["neck", "spine_2", "spine_1", "spine"]: + if j in src_joint_map: + self.src_parent_map["spine_03"] = "pelvis" + #self.src_child_map[src_joint_map["pelvis"]] = [ "left_eye", "right_eye", "left_ear", "right_ear"]#src_joint_map["neck"]#"pelvis" "neck_01" + + self.constant_offset = constant_offset + self.place_on_ground = place_on_ground + self.current_frame_data = dict() + self.prev_frame_data = dict() + self.frame_idx = 0 + self.target_cos_map = create_local_cos_map_from_skeleton_axes_with_map(self.target_skeleton) + if "cos_map" in target_skeleton.skeleton_model: + self.target_cos_map.update(target_skeleton.skeleton_model["cos_map"]) + + target_joints = self.target_skeleton.skeleton_model["joints"] + self.target_spine_joints = [target_joints[j] for j in ["neck", "spine_2", "spine_1", "spine"] if j in target_joints]#["spine_03", "neck_01"] + self.target_ball_joints = [target_joints[j] for j in ["left_shoulder", "right_shoulder", "left_hip", "right_hip"] if j in target_joints]# ["thigh_r", "thigh_l", "upperarm_r", "upperarm_l"] + self.target_ankle_joints = [target_joints[j] for j in ["left_ankle", "right_ankle"] if j in target_joints] + self.clavicle_joints = [target_joints[j] for j in ["right_clavicle", "left_clavicle"] if j in target_joints] + self.twist_angle_joints = [target_joints[j] for j in ["right_knee","left_knee","left_shoulder", "right_shoulder", "right_clavicle", "left_clavicle","right_elbow", "left_elbow","left_wrist", "right_wrist"] if j in target_joints] + if "neck" in target_joints: + self.target_neck_joint = target_joints["neck"] + else: + self.target_neck_joint = None + + left_hip = self.src_model["joints"]["left_hip"] + right_hip = self.src_model["joints"]["right_hip"] + self.left_hip_idx = self.src_joints[left_hip]["index"] + self.right_hip_idx = self.src_joints[right_hip]["index"] + left_shoulder = self.src_model["joints"]["left_shoulder"] + right_shoulder = self.src_model["joints"]["right_shoulder"] + self.left_shoulder_idx = self.src_joints[left_shoulder]["index"] + self.right_shoulder_idx = self.src_joints[right_shoulder]["index"] + self.ref_rotation = dict() + for target_name in self.target_skeleton.animated_joints: + self.ref_rotation[target_name] = get_quaternion_rotation_by_name(target_name, self.target_skeleton.reference_frame, + self.target_skeleton, root_offset=3) + + + def estimate_src_joint_cos(self, src_name, child_name, target_name, src_frame): + joint_idx = self.src_joints[src_name]["index"] + child_idx = self.src_joints[child_name]["index"] + if isinstance(child_idx, list): + child_pos = np.mean([src_frame[idx] for idx in child_idx]) + else: + child_pos = src_frame[joint_idx] + global_src_up_vec = src_frame[child_idx] - child_pos + global_src_up_vec /= np.linalg.norm(global_src_up_vec) + if target_name == self.target_skeleton.skeleton_model["joints"]["pelvis"]: + global_src_x_vec = src_frame[self.left_hip_idx] - src_frame[self.right_hip_idx] + global_src_x_vec /= np.linalg.norm(global_src_x_vec) + elif target_name in self.target_spine_joints or target_name == "CC_Base_Waist": # find x vector from shoulders + global_src_x_vec = src_frame[self.left_shoulder_idx] - src_frame[self.right_shoulder_idx] + global_src_x_vec /= np.linalg.norm(global_src_x_vec) + elif target_name in self.target_ball_joints: # use x vector of child + child_child_name = self.src_child_map[child_name] + child_child_idx = self.src_joints[child_child_name]["index"] + child_global_src_up_vec = src_frame[child_child_idx] - src_frame[child_idx] + child_global_src_up_vec /= np.linalg.norm(child_global_src_up_vec) + angle = abs(math.acos(np.dot(global_src_up_vec, child_global_src_up_vec))) + if angle < 0.01 and src_name in self.prev_frame_data:#np.linalg.norm(delta) < 0.2 + print("use stored at ", self.frame_idx,"for", src_name, "for child") + global_src_up_vec = self.prev_frame_data[src_name][0] + global_src_x_vec = self.prev_frame_data[src_name][1] + else: + global_src_x_vec = np.cross(global_src_up_vec, child_global_src_up_vec) + norm = np.linalg.norm(global_src_x_vec) + global_src_x_vec /= norm + else: # find x vector by cross product with parent + global_src_x_vec = None + if src_name in self.src_parent_map: + parent_joint = self.src_parent_map[src_name] + if parent_joint in self.current_frame_data: + global_parent_up_vector = self.current_frame_data[parent_joint][0] + global_src_x_vec = np.cross(global_src_up_vec, global_parent_up_vector) + norm = np.linalg.norm(global_src_x_vec) + global_src_x_vec /= norm + if target_name not in self.target_ankle_joints: + global_src_x_vec = -global_src_x_vec + return global_src_up_vec, global_src_x_vec + + def get_src_cos_from_joint_map(self, src_name, target_name, src_frame): + src_cos = None + if src_name not in self.src_child_map.keys() or self.src_child_map[src_name] is None: + return src_cos + if self.src_child_map[src_name] in self.src_to_target_joint_map:# and or target_name =="neck_01" or target_name.startswith("hand") + child_name = self.src_child_map[src_name] + if child_name not in self.src_joints.keys(): + return src_cos + src_cos = self.estimate_src_joint_cos(src_name, child_name, target_name, src_frame) + self.current_frame_data[src_name] = src_cos + + return src_cos + + + def get_src_cos_from_multiple_points(self, src_frame, cos_def): + up_start = np.mean([src_frame[self.src_joints[name]["index"]] for name in cos_def["up_start"]], axis=0) + up_end = np.mean([src_frame[self.src_joints[name]["index"]] for name in cos_def["up_end"]], axis=0) + global_src_up_vec = up_end - up_start + x_start = np.mean([src_frame[self.src_joints[name]["index"]] for name in cos_def["x_start"]], axis=0) + x_end = np.mean([src_frame[self.src_joints[name]["index"]] for name in cos_def["x_end"]], axis=0) + global_src_x_vec = x_end - x_start + global_src_up_vec /= np.linalg.norm(global_src_up_vec) + global_src_x_vec /= np.linalg.norm(global_src_x_vec) + return global_src_up_vec, global_src_x_vec + + + def retarget_frame(self, src_frame, prev_frame, pose_angles=None): + + target_joints = self.target_skeleton.skeleton_model["joints"] + joint_map = dict() + for k, v in target_joints.items(): + joint_map[v] = k + self.target_skeleton.clear_cached_global_matrices() + target_frame = np.zeros(self.n_params) + self.prev_frame_data = self.current_frame_data + self.current_frame_data = dict() + + # copy the root translation assuming the rocketbox skeleton with static offset on the hips is used as source + #target_frame[:3] = np.array(src_frame[0]) * self.scale_factor + target_frame[:3] = src_frame[self.left_hip_idx] + (src_frame[self.right_hip_idx] - src_frame[self.left_hip_idx])/2 + target_frame[:3] *= self.scale_factor + if self.constant_offset is not None: + target_frame[:3] += self.constant_offset + target_offset = 3 + for target_name in self.target_skeleton.animated_joints: + q = self.ref_rotation[target_name] + src_cos = None + default_name = None + if target_name in joint_map: + default_name = joint_map[target_name] + if default_name in self.src_model["cos_defs"]: + src_cos = self.get_src_cos_from_multiple_points(src_frame, self.src_model["cos_defs"][default_name]) + elif target_name in self.target_to_src_joint_map: + src_name = self.target_to_src_joint_map[target_name] + if src_name is not None and src_name in self.src_joints: + src_cos = self.get_src_cos_from_joint_map(src_name, target_name, src_frame) + if src_cos is not None and src_cos[1] is not None: + twist_angle = None + if pose_angles is not None and self.target_skeleton.nodes[target_name].parent is not None and target_name in self.twist_angle_joints: + joint_idx = self.src_joints[src_name]["index"] + twist_angle = pose_angles[joint_idx][0] + is_root = target_name == self.target_skeleton_root + q = find_rotation_analytically(self.target_skeleton, target_name, src_cos, target_frame, self.target_cos_map, is_root=is_root, twist_angle=twist_angle) + q = q/np.linalg.norm(q) + + #if ref_frame is not None: + # q = q if np.dot(ref_frame[target_offset:target_offset + 4], q) >= 0 else -q + + if prev_frame is not None: + prev_q = normalize(prev_frame[target_offset:target_offset + 4]) + if np.dot(q, prev_q) < 0: + q = -q + inv_q = normalize(quaternion_inverse(q)) + delta_q = normalize(quaternion_multiply(inv_q, prev_q)) + theta = 2 * np.arccos(delta_q[0]) + if abs(theta) > np.pi: + print("keep", self.frame_idx, theta,src_name, q, prev_q) + q = prev_q + target_frame[target_offset:target_offset + 4] = q + target_offset += 4 + return target_frame + + def run(self, src_frames, frame_range): + n_frames = len(src_frames) + target_frames = [] + self.frame_idx = 0 + if n_frames > 0: + if frame_range is None: + frame_range = (0, n_frames) + if self.additional_rotation is not None: + src_frames = apply_additional_rotation_on_frames(self.src_skeleton.animated_joints, src_frames, self.additional_rotation) + + prev_frame = None + for idx, src_frame in enumerate(src_frames[frame_range[0]:frame_range[1]]): + target_frame = self.retarget_frame(src_frame, prev_frame) + prev_frame = target_frame + target_frames.append(target_frame) + self.frame_idx += 1 + target_frames = np.array(target_frames) + if self.place_on_ground: + delta = target_frames[0][1] - self.ground_height + target_frames[:, 1] -= delta + return target_frames + + +def generate_joint_map(src_model, target_model): + joint_map = dict() + for j in src_model["joints"]: + if j in target_model["joints"]: + src = src_model["joints"][j] + target = target_model["joints"][j] + joint_map[target] = src + return joint_map + + +def retarget_from_point_cloud_to_target(src_joints, src_model, target_skeleton, src_frames, joint_map=None, additional_rotation=None, scale_factor=1.0, frame_range=None, place_on_ground=False): + if joint_map is None: + joint_map = generate_joint_map(src_model, target_skeleton.skeleton_model) + constant_offset = -np.array(target_skeleton.nodes[target_skeleton.root].offset) + retargeting = PointCloudRetargeting(src_joints, src_model, target_skeleton, joint_map, scale_factor, additional_rotation=additional_rotation, place_on_ground=place_on_ground, constant_offset=constant_offset) + return retargeting.run(src_frames, frame_range) diff --git a/build/lib/anim_utils/retargeting/utils.py b/build/lib/anim_utils/retargeting/utils.py new file mode 100644 index 0000000..41ebc27 --- /dev/null +++ b/build/lib/anim_utils/retargeting/utils.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +import numpy as np +import math +from transformations import quaternion_from_matrix, euler_matrix, quaternion_matrix, quaternion_multiply, euler_from_quaternion, quaternion_from_euler, quaternion_inverse, euler_from_matrix +from ..animation_data.utils import quaternion_from_vector_to_vector + +AXES = [[1,0,0],[0,1,0],[0,0,1], [-1,0,0],[0,-1,0],[0,0,-1]] + + +def get_angle_between_vectors(v1, v2): + q = quaternion_from_vector_to_vector(v1, v2) + v = q[1:] + sin_theta = np.linalg.norm(v) + sin_theta = min(sin_theta, 1.0) + abs_angle = 2 * math.asin(sin_theta) + return abs_angle + + +def project_vector_on_axis(v): + min_idx = -1 + min_angle = np.inf + for idx, a in enumerate(AXES): + angle = get_angle_between_vectors(v, a) + if angle < min_angle: + min_angle = angle + min_idx = idx + length = np.linalg.norm(v) + return np.array(AXES[min_idx]) * length + + +def align_root_translation(target_skeleton, target_frame, src_frame, root_node="pelvis", src_scale_factor=1.0): + target_pos = target_skeleton.nodes[root_node].get_global_position(target_frame) + target_pos = np.array([target_pos[0], target_pos[2]]) + src_pos = np.array([src_frame[0], src_frame[2]])*src_scale_factor + delta = src_pos - target_pos + target_frame[0] += delta[0] + target_frame[2] += delta[1] + return target_frame + + +def align_quaternion_frames(target_skeleton, frames): + """align quaternions for blending + src: http://physicsforgames.blogspot.de/2010/02/quaternions.html + """ + ref_frame = None + new_frames = [] + for frame in frames: + if ref_frame is None: + ref_frame = frame + else: + offset = 3 + for joint in target_skeleton.animated_joints: + q = frame[offset:offset + 4] + ref_q = ref_frame[offset:offset + 4] + dot = np.dot(ref_q, q) + if dot < 0: + frame[offset:offset + 4] = -q + offset += 4 + new_frames.append(frame) + return new_frames + + +def get_coordinate_system_axes(skeleton, joint_name, frame, axes): + global_m = skeleton.nodes[joint_name].get_global_matrix(frame)[:3,:3] + dirs = [] + for axis in axes: + direction = np.dot(global_m, axis) + direction /= np.linalg.norm(direction) + dirs.append(direction) + return np.array(dirs) + + +def rotate_axes(axes, q): + m = quaternion_matrix(q)[:3, :3] + aligned_axes = dict() + for key, a in axes.items(): + aligned_axes[key] = np.dot(m, a) + aligned_axes[key] = normalize(aligned_axes[key]) + return aligned_axes + + +def align_axis(axes, key, new_vec): + q = quaternion_from_vector_to_vector(axes[key], new_vec) + aligned_axes = rotate_axes(axes, q) + return q, aligned_axes + + +def get_quaternion_rotation_by_name(joint_name, frame, skeleton, root_offset=3): + assert joint_name in skeleton.animated_joints + joint_index = skeleton.animated_joints.index(joint_name) + return frame[joint_index * 4 + root_offset : (joint_index + 1) * 4 + root_offset] + + +def normalize(v): + return v/np.linalg.norm(v) + + +def filter_dofs(q, fixed_dims): + e = list(euler_from_quaternion(q)) + for d in fixed_dims: + e[d] = 0 + q = quaternion_from_euler(*e) + return q + + +def quaternion_from_axis_angle(axis, angle): + q = [1,0,0,0] + q[1] = axis[0] * math.sin(angle / 2) + q[2] = axis[1] * math.sin(angle / 2) + q[3] = axis[2] * math.sin(angle / 2) + q[0] = math.cos(angle / 2) + return normalize(q) + + +def find_rotation_between_vectors(a, b): + """http://math.stackexchange.com/questions/293116/rotating-one-3d-vector-to-another""" + if np.array_equiv(a, b): + return [1, 0, 0, 0] + + axis = normalize(np.cross(a, b)) + dot = np.dot(a, b) + if dot >= 1.0: + return [1, 0, 0, 0] + angle = math.acos(dot) + q = quaternion_from_axis_angle(axis, angle) + return q + + +def to_local_cos_old(skeleton, node_name, frame, q): + # bring into parent coordinate system + pm = skeleton.nodes[node_name].get_global_matrix(frame)#[:3,:3] + inv_pm = np.linalg.inv(pm) + r = quaternion_matrix(q)[:3,:3] + lr = np.dot(inv_pm, r)[:3,:3] + q = quaternion_from_matrix(lr) + return q + + +def to_local_cos(skeleton, node_name, frame, q): + # bring into parent coordinate system + pm = skeleton.nodes[node_name].get_global_matrix(frame)#[:3,:3] + inv_p = quaternion_inverse(quaternion_from_matrix(pm)) + normalize(inv_p) + return quaternion_multiply(inv_p, q) + + +def to_global_cos(skeleton, node_name, frame, q): + pm = skeleton.nodes[node_name].get_global_matrix(frame)#[:3,:3] + r = quaternion_matrix(q)[:3, :3] + lr = np.dot(pm, r)[:3, :3] + q = quaternion_from_matrix(lr) + return q + + +def apply_additional_rotation_on_frames(animated_joints, frames, rotation_offsets): + new_frames = [] + for frame in frames: + new_frame = frame[:] + for idx, name in enumerate(animated_joints): + if name in rotation_offsets: + euler = np.radians(rotation_offsets[name]) + additional_q = quaternion_from_euler(*euler) + offset = idx * 4 + 3 + q = new_frame[offset:offset + 4] + new_frame[offset:offset + 4] = quaternion_multiply(q, additional_q) + + new_frames.append(new_frame) + return new_frames diff --git a/build/lib/anim_utils/utilities/__init__.py b/build/lib/anim_utils/utilities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/anim_utils/utilities/custom_math.py b/build/lib/anim_utils/utilities/custom_math.py new file mode 100644 index 0000000..be3f9ec --- /dev/null +++ b/build/lib/anim_utils/utilities/custom_math.py @@ -0,0 +1,651 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +# encoding: UTF-8 +__author__ = 'hadu01' + +import numpy as np +import math +from transformations.transformations import _AXES2TUPLE, _TUPLE2AXES, _NEXT_AXIS, quaternion_from_euler +LEN_CARTESIAN = 3 +LEN_QUATERNION = 4 + + +def diff_quat(q0, q1): + """ + Args: + q0, q1: (qw, qx, qy, qz) + + Returns: + float: angle in radians + """ + w0, x0, y0, z0 = q0 + w1, x1, y1, z1 = q1 + d1 = w1*w1+x1*x1+y1*y1+z1*z1 + + w = x1 * x0 + y1 * y0 + z1 * z0 + w1 * w0 + x = -x1 * w0 - y1 * z0 + z1 * y0 + w1 * x0 + y = x1 * z0 - y1 * w0 - z1 * x0 + w1 * y0 + z = -x1 * y0 + y1 * x0 - z1 * w0 + w1 * z0 + return (w, x, y, z) + + +def euler_matrix_jac(ai, aj, ak, axes='rxyz', der_axis='x'): + try: + firstaxis, parity, repetition, frame = _AXES2TUPLE[axes] + except (AttributeError, KeyError): + _TUPLE2AXES[axes] # validation + firstaxis, parity, repetition, frame = axes + + i = firstaxis + j = _NEXT_AXIS[i + parity] + k = _NEXT_AXIS[i - parity + 1] + + if frame: + ai, ak = ak, ai + if parity: + ai, aj, ak = -ai, -aj, -ak + si, sj, sk = math.sin(ai), math.sin(aj), math.sin(ak) + ci, cj, ck = math.cos(ai), math.cos(aj), math.cos(ak) + cc, cs = ci * ck, ci * sk + sc, ss = si * ck, si * sk + M = np.zeros([4, 4]) + # rotation matrix + # if repetition: + # M[i, i] = cj + # M[i, j] = sj * si + # M[i, k] = sj * ci + # M[j, i] = sj * sk + # M[j, j] = -cj * ss + cc + # M[j, k] = -cj * cs - sc + # M[k, i] = -sj * ck + # M[k, j] = cj * sc + cs + # M[k, k] = cj * cc - ss + # else: + # M[i, i] = cj * ck + # M[i, j] = sj * sc - cs + # M[i, k] = sj * cc + ss + # M[j, i] = cj * sk + # M[j, j] = sj * ss + cc + # M[j, k] = sj * cs - sc + # M[k, i] = -sj + # M[k, j] = cj * si + # M[k, k] = cj * ci + ''' + first order derivatives: + for ai: + d(si)/d(ai) = ci, d(sj)/d(ai) = 0, d(sk)/d(ai) = 0 + d(ci)/d(ai) = -si, d(cj)/d(ai) = 0, d(ck)/d(ai) = 0 + d(cc)/d(ai) = -si * ck = -sc, d(cs)/d(ai) = -si * sk = -ss + d(sc)/d(ai) = ci * ck = cc, d(ss)/d(ai) = ci * sk = cs + + for aj: + d(si)/d(aj) = 0, d(sj)/d(aj) = cj, d(sk)/d(aj) = 0 + d(ci)/d(aj) = 0, d(cj)/d(aj) = -sj, d(ck)/d(aj) = 0 + d(cc)/d(aj) = 0, d(cs)/d(aj) = 0 + d(sc)/d(aj) = 0, d(ss)/d(aj) = 0 + + for ak: + d(si)/d(ak) = 0, d(sj)/d(ak) = 0, d(sk)/d(ak) = ck + d(ci)/d(ak) = 0, d(cj)/d(ak) = 0, d(ck)/d(ak) = -sk + d(cc)/d(ak) = -ci * sk = -cs, d(cs)/d(ak) = ci * ck = cc + d(sc)/d(ak) = -si * sk = -ss, d(ss)/d(ak) = si * ck = sc + ''' + if repetition: + if der_axis == 'x': + M[i, i] = 0 + M[i, j] = sj * ci + M[i, k] = -sj * si + M[j, i] = 0 + M[j, j] = -cj * cc + sc + M[j, k] = cj * ss - cc + M[k, i] = 0 + M[k, j] = cj * cc - ss + M[k, k] = -cj * sc - cs + elif der_axis == 'y': + M[i, i] = -sj + M[i, j] = cj * si + M[i, k] = cj * ci + M[j, i] = cj * sk + M[j, j] = sj * ss + M[j, k] = sj * cs + M[k, i] = -cj * ck + M[k, j] = -sj * sc + M[k, k] = -sj * cc + elif der_axis == 'z': + M[i, i] = 0 + M[i, j] = 0 + M[i, k] = 0 + M[j, i] = sj * ck + M[j, j] = -cj * sc - cs + M[j, k] = -cj * cc + ss + M[k, i] = sj * sk + M[k, j] = -cj * ss + cc + M[k, k] = -cj * cs - sc + else: + raise ValueError('Unknown axis type!') + else: + if der_axis == 'x': + if not frame: + M[i, i] = 0 + M[i, j] = sj * cc + ss + M[i, k] = -sj * sc + cs + M[j, i] = 0 + M[j, j] = sj * cs - sc + M[j, k] = -sj * ss - cc + M[k, i] = 0 + M[k, j] = cj * ci + M[k, k] = -cj * si + # if not parity: + # M[i, i] = 0 + # M[i, j] = sj * cc + ss + # M[i, k] = -sj * sc + cs + # M[j, i] = 0 + # M[j, j] = sj * cs - sc + # M[j, k] = -sj * ss - cc + # M[k, i] = 0 + # M[k, j] = cj * ci + # M[k, k] = -cj * si + # else: + # M[i, i] = 0 + # M[i, j] = -sj * cc + ss + # M[i, k] = -sj * sc - cs + # M[j, i] = 0 + # M[j, j] = -sj * cs - sc + # M[j, k] = -sj * ss + cc + # M[k, i] = 0 + # M[k, j] = -cj * ci + # M[k, k] = -cj * si + else: + M[i, i] = -cj * sk + M[i, j] = -sj * ss - cc + M[i, k] = -sj * cs + sc + M[j, i] = cj * ck + M[j, j] = sj * sc - cs + M[j, k] = sj * cc + ss + M[k, i] = 0 + M[k, j] = 0 + M[k, k] = 0 + # if not parity: + # M[i, i] = -cj * sk + # M[i, j] = -sj * ss - cc + # M[i, k] = -sj * cs + sc + # M[j, i] = cj * ck + # M[j, j] = sj * sc - cs + # M[j, k] = sj * cc + ss + # M[k, i] = 0 + # M[k, j] = 0 + # M[k, k] = 0 + # else: + # M[i, i] = -cj * sk + # M[i, j] = -sj * ss + cc + # M[i, k] = -sj * cs - sc + # M[j, i] = -cj * ck + # M[j, j] = -sj * sc - cs + # M[j, k] = -sj * cc + ss + # M[k, i] = 0 + # M[k, j] = 0 + # M[k, k] = 0 + elif der_axis == 'y': + M[i, i] = -sj * ck + M[i, j] = cj * sc + M[i, k] = cj * cc + M[j, i] = -sj * sk + M[j, j] = cj * ss + M[j, k] = cj * cs + M[k, i] = -cj + M[k, j] = -sj * si + M[k, k] = -sj * ci + # if not parity: + # M[i, i] = -sj * ck + # M[i, j] = cj * sc + # M[i, k] = cj * cc + # M[j, i] = -sj * sk + # M[j, j] = cj * ss + # M[j, k] = cj * cs + # M[k, i] = -cj + # M[k, j] = -sj * si + # M[k, k] = -sj * ci + # else: + # M[i, i] = -sj * ck + # M[i, j] = -cj * sc + # M[i, k] = -cj * cc + # M[j, i] = -sj * sk + # M[j, j] = -cj * ss + # M[j, k] = -cj * cs + # M[k, i] = cj + # M[k, j] = -sj * si + # M[k, k] = -sj * ci + elif der_axis == 'z': + if not frame: + M[i, i] = -cj * sk + M[i, j] = -sj * ss - cc + M[i, k] = -sj * cs + sc + M[j, i] = cj * ck + M[j, j] = sj * sc - cs + M[j, k] = sj * cc + ss + M[k, i] = 0 + M[k, j] = 0 + M[k, k] = 0 + # if not parity: + # M[i, i] = -cj * sk + # M[i, j] = -sj * ss - cc + # M[i, k] = -sj * cs + sc + # M[j, i] = cj * ck + # M[j, j] = sj * sc - cs + # M[j, k] = sj * cc + ss + # M[k, i] = 0 + # M[k, j] = 0 + # M[k, k] = 0 + # else: + # M[i, i] = -cj * sk + # M[i, j] = -sj * ss + cc + # M[i, k] = -sj * cs - sc + # M[j, i] = -cj * ck + # M[j, j] = -sj * sc - cs + # M[j, k] = -sj * cc + ss + # M[k, i] = 0 + # M[k, j] = 0 + # M[k, k] = 0 + else: + M[i, i] = 0 + M[i, j] = sj * cc + ss + M[i, k] = -sj * sc + cs + M[j, i] = 0 + M[j, j] = sj * cs - sc + M[j, k] = -sj * ss - cc + M[k, i] = 0 + M[k, j] = cj * ci + M[k, k] = -cj * si + # if not parity: + # M[i, i] = 0 + # M[i, j] = sj * cc + ss + # M[i, k] = -sj * sc + cs + # M[j, i] = 0 + # M[j, j] = sj * cs - sc + # M[j, k] = -sj * ss - cc + # M[k, i] = 0 + # M[k, j] = cj * ci + # M[k, k] = -cj * si + # else: + # M[i, i] = 0 + # M[i, j] = -sj * cc + ss + # M[i, k] = -sj * sc - cs + # M[j, i] = 0 + # M[j, j] = -sj * cs - sc + # M[j, k] = -sj * ss + cc + # M[k, i] = 0 + # M[k, j] = -cj * ci + # M[k, k] = -cj * si + else: + raise ValueError('Unknown axis type!') + if parity: + M = -M + return M + + +def quaternion_inv(q): + """ + Inverse of quaternion q + Args: + q: (qw, qx, qy, qz) + """ + w, x, y, z = q + d = w*w + x*x + y*y + z*z + q_inv = (w/d, -x/d, -y/d, -z/d) + return q_inv + + +def normalize_quaternion(q): + q = np.asarray(q) + normalized_q = q/np.linalg.norm(q) + return normalized_q + + +def error_measure_3d_mat(raw_data, + reconstructed_data): + ''' + Compute the mean squared error bewteen original data and reconstructed data + The data matrix is array3D: n_samples * n_frames * n_dims + ''' + raw_data = np.asarray(raw_data) + reconstructed_data = np.asarray(reconstructed_data) + assert raw_data.shape == reconstructed_data.shape + diff = raw_data - reconstructed_data + n_samples, n_frames, n_dim = diff.shape + err = 0 + for i in range(n_samples): + for j in range(n_frames): + err += np.linalg.norm(diff[i, j]) + err = err/(n_samples * n_frames) + return err + + +def err_quat_data(raw_data, + reconstructed_data): + raw_data = np.asarray(raw_data) + reconstructed_data = np.asarray(reconstructed_data) + assert raw_data.shape == reconstructed_data.shape + diff = raw_data - reconstructed_data + n_samples, n_frames, n_dim = diff.shape + err = 0 + for i in range(n_samples): + for j in range(n_frames): + err += np.linalg.norm(diff[i, j][3:]) + + err = err/(n_samples * n_frames) + return err + + +def quat_to_expmap(q): + """ + map quaternion to exponential map + :param q: [qw, qx, qy, qz] + :return: + """ + q = q/np.linalg.norm(q) + theta = 2 * np.arccos(q[0]) + if theta < 1e-3: + auxillary = 1/(0.5 + theta ** 2 / 48) + else: + auxillary = theta / np.sin(0.5*theta) + vx = q[1] * auxillary + vy = q[2] * auxillary + vz = q[3] * auxillary + return np.array([vx, vy, vz]) + +def euler_to_expmap(euler_angles): + ''' + convert euler angles to exponential map + :param euler_angles: degree + :return: + ''' + euler_angles_rad = np.deg2rad(euler_angles) + quat = quaternion_from_euler(*euler_angles) + return quat_to_expmap(quat) + + +def angle_axis_from_quaternion(q): + q = q/np.linalg.norm(q) + theta = 2 * np.arccos(q[0]) + if theta < 1e-3: + axis = np.array([0, 0, 0]) + else: + x = q[1] / np.sin(0.5 * theta) + y = q[2] / np.sin(0.5 * theta) + z = q[3] / np.sin(0.5 * theta) + axis = np.array([x, y, z]) + return theta, axis + + +def quat_to_logmap(q): + q = q/np.linalg.norm(q) + theta = 2 * np.arccos(q[0]) + if theta < 1e-3: + auxillary = 1/(0.5 + theta ** 2 / 48) + else: + auxillary = theta / np.sin(0.5*theta) + vx = q[1] * auxillary + vy = q[2] * auxillary + vz = q[3] * auxillary + return np.array([vx, vy, vz]) + + +def expmap_to_quat(exp_map): + theta = np.linalg.norm(exp_map) + print('theta is: ', theta) + if np.abs(theta) < 1e-3: + auxillary = 0.5 + theta ** 2 / 48 + else: + auxillary = np.sin(0.5*theta) / theta + qw = np.cos(theta/2) + qx = exp_map[0]*auxillary + qy = exp_map[1]*auxillary + qz = exp_map[2]*auxillary + return [qw, qx, qy, qz] + + +def logmap_to_quat(v): + theta = np.linalg.norm(v) + if theta < 1e-3: + auxillary = 0.5 + theta ** 2 / 48 + else: + auxillary = np.sin(0.5*theta) / theta + qw = np.cos(theta/2) + qx = v[0]*auxillary + qy = v[1]*auxillary + qz = v[2]*auxillary + return [qw, qx, qy, qz] + + +def areQuatClose(quat1, quat2): + dot = np.dot(quat1, quat2) + if dot < 0.0: + return False + else: + return True + + +def cartesian_splines_distance(raw_splines, reconstructed_splines, skeleton, weighted_error=True): + """ + Calculate the Euclidean distance between motion represented as Cartesian splines + :param raw_splines: Cartesian spline coefficience matrix + :param reconstructed_splines: spline coefficience matrix + :param skeleton: + :param weighted_error: + :return: + """ + raw_splines = np.asarray(raw_splines) + reconstructed_splines = np.asarray(reconstructed_splines) + n_samples, n_basis, n_dims = raw_splines.shape + assert n_dims%LEN_CARTESIAN == 0 + n_joints = n_dims/LEN_CARTESIAN + if not weighted_error: + return error_measure_3d_mat(raw_splines, reconstructed_splines)/n_joints + else: + joint_weights = skeleton.joint_weights[:-4] # ignore the last two tool joints + weight_vector = np.ones(n_dims) + for i in range(n_joints): + weight_vector[i*LEN_QUATERNION: (i+1)*LEN_QUATERNION] *= joint_weights[i] + weight_mat = np.diag(weight_vector) + return error_measure_3d_mat(np.dot(raw_splines, weight_mat), + np.dot(reconstructed_splines, weight_mat))/n_joints + +def angle_between_vectors(v1, v2): + """ + Compute angle (in radians) between two 3d vectors (vq, v2) + :param v1: (x, y, z) + :param v2: (x, y, z) + :return: + """ + v1 = np.asarray(v1) + v2 = np.asarray(v2) + assert len(v1) == 3 + assert len(v2) == 3 + theta = np.arccos(np.dot(v1, v2)/(np.linalg.norm(v1) * np.linalg.norm(v2))) + return theta + + +def angle_between_2d_vector(v1, v2): + v1 = np.asarray(v1) + v2 = np.asarray(v2) + theta = np.arccos(np.dot(v1, v2)/np.linalg.norm(v1) * np.linalg.norm(v2)) + return theta + + + +def find_local_maximum(signal, neighbor_size): + ''' + find local maximums of a signal in a given beighbor size + :param signal: + :param neighbor_size: + :return: + ''' + # mirror the signal on the boundary with k = stepsize/2 + signal_len = len(signal) + localMax = [] + k = int(np.floor(neighbor_size/2)) + extendedSignal = np.zeros(signal_len + 2*k) + extendedSignal[k: -k] = signal + for i in range(k): + extendedSignal[i] = signal[0] + extendedSignal[-i-1] = signal[-1] + for j in range(0, signal_len): + searchArea = extendedSignal[j: j+neighbor_size] + maximum = np.max(searchArea) + if maximum == extendedSignal[j+k]: + localMax.append(j) + + return localMax + + +def find_local_minimum(signal, neighbor_size): + ''' + find local maximums of a signal in a given beighbor size + :param signal: + :param neighbor_size: + :return: + ''' + # mirror the signal on the boundary with k = stepsize/2 + signal_len = len(signal) + localMin = [] + k = int(np.floor(neighbor_size/2)) + extendedSignal = np.zeros(signal_len + 2*k) + for i in range(signal_len): + extendedSignal[i+k] = signal[i] + for i in range(k): + extendedSignal[i] = signal[0] + extendedSignal[-i-1] = signal[-1] + for j in range(1, signal_len-1): + searchArea = extendedSignal[j: j+neighbor_size] + minimum = np.min(searchArea) + if minimum == extendedSignal[j+k]: + localMin.append(j) + + return localMin + + +def linear_weights_curve(window_size): + ''' + create a linear weight curve, the starting and ending points are zero, and the peak is in the middle + :param window_size: + :return: + ''' + middle_point = window_size/2.0 + weights = np.zeros(window_size) + for i in range(0, int(np.floor(middle_point))+1): + weights[i] = (1.0/middle_point) * i + for i in range(int(np.ceil(middle_point)), window_size): + weights[i] = -(1.0/middle_point) * i + 2.0 + return weights + + +def sin_weights_curve(window_size): + ''' + create a sine weight curve, the starting and ending points are zero, and the peak is in the middle + :param window_size: + :return: + ''' + return np.sin(np.arange(0, window_size) * np.pi / (window_size - 1)) + + +def power_weights_curve(window_size): + ''' + create a power function weight curve, the starting and ending points are zero, and the peak is in the middle + :param window_size: + :return: + ''' + middle_point = window_size/2.0 + weights = np.zeros(window_size) + a = (1.0/middle_point) ** 2 + for i in range(0, int(np.floor(middle_point))+1): + weights[i] = a * (i**2) + for i in range(int(np.ceil(middle_point)), window_size): + weights[i] = a * (i-window_size + 1) ** 2 + return weights + + +def tdot_mat(mat, out=None): + ''' + a copy of implementation of M*M_T from GPy + :param mat: + :param out: + :return: + ''' + from scipy.linalg import blas + """returns np.dot(mat, mat.T), but faster for large 2D arrays of doubles.""" + if (mat.dtype != 'float64') or (len(mat.shape) != 2): + return np.dot(mat, mat.T) + nn = mat.shape[0] + if out is None: + out = np.zeros((nn, nn)) + else: + assert(out.dtype == 'float64') + assert(out.shape == (nn, nn)) + # FIXME: should allow non-contiguous out, and copy output into it: + assert(8 in out.strides) + # zeroing needed because of dumb way I copy across triangular answer + out[:] = 0.0 + + # # Call to DSYRK from BLAS + mat = np.asfortranarray(mat) + out = blas.dsyrk(alpha=1.0, a=mat, beta=0.0, c=out, overwrite_c=1, + trans=0, lower=0) + + symmetrify(out, upper=True) + return np.ascontiguousarray(out) + + +def symmetrify(A, upper=False): + """ + Take the square matrix A and make it symmetrical by copting elements from + the lower half to the upper + + works IN PLACE. + + note: tries to use cython, falls back to a slower numpy version + """ + _symmetrify_numpy(A, upper) + + +def _symmetrify_numpy(A, upper=False): + triu = np.triu_indices_from(A,k=1) + if upper: + A.T[triu] = A[triu] + else: + A[triu] = A.T[triu] + + +def quaternion_from_vectors(vec1, vec2): + vec1 = vec1/np.linalg.norm(vec1) + vec2 = vec2/np.linalg.norm(vec2) + inner_prod = np.dot(vec1, vec2) + cross_prod = np.cross(vec1, vec2) + cross_prod = cross_prod / np.linalg.norm(cross_prod) + theta = np.arccos(inner_prod) + sin_half = np.sin(theta/2.0) + w = np.cos(theta/2.0) + print(w) + x = cross_prod[0] * sin_half + y = cross_prod[1] * sin_half + z = cross_prod[2] * sin_half + return np.array([w, x, y, z]) diff --git a/build/lib/anim_utils/utilities/io_helper_functions.py b/build/lib/anim_utils/utilities/io_helper_functions.py new file mode 100644 index 0000000..d615fb0 --- /dev/null +++ b/build/lib/anim_utils/utilities/io_helper_functions.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +""" +Created on Wed Feb 04 12:56:39 2015 + +@author: Han Du, Martin Manns, Erik Herrmann +""" +import os +import sys +import glob +import json +import collections +import math +import numpy as np +from datetime import datetime +from ..animation_data.bvh import BVHWriter, BVHReader + + +def load_json_file(filename, use_ordered_dict=True): + """ Load a dictionary from a file + + Parameters + ---------- + * filename: string + \tThe path to the saved json file. + * use_ordered_dict: bool + \tIf set to True dicts are read as OrderedDicts. + """ + tmp = None + with open(filename, 'r') as infile: + if use_ordered_dict: + tmp = json.JSONDecoder( + object_pairs_hook=collections.OrderedDict).decode( + infile.read()) + else: + tmp = json.load(infile) + infile.close() + return tmp + + +def write_to_json_file(filename, serializable, indent=4): + with open(filename, 'w') as outfile: + tmp = json.dumps(serializable, indent=4) + outfile.write(tmp) + outfile.close() + + +def gen_file_paths(directory, mask='*mm.json'): + """Generator of input file paths + + Parameters + ---------- + + * dir: String + \tPath of input folder, in which the input files reside + * mask: String, defaults to '*.bvh' + \tMask pattern for file search + + """ + + if not directory.endswith(os.sep): + directory += os.sep + + for filepath in glob.glob(directory + mask): + yield filepath + + +def clean_path(path): + path = path.replace('/', os.sep).replace('\\', os.sep) + if os.sep == '\\' and '\\\\?\\' not in path: + # fix for Windows 260 char limit + relative_levels = len([directory for directory in path.split(os.sep) + if directory == '..']) + cwd = [ + directory for directory in os.getcwd().split( + os.sep)] if ':' not in path else [] + path = '\\\\?\\' + os.sep.join(cwd[:len(cwd) - relative_levels] + [ + directory for directory in path.split(os.sep) if directory != ''][relative_levels:]) + return path + + +def load_bvh_files_from_folder(data_folder): + bvhfiles = glob.glob(os.path.join(data_folder, '*.bvh')) + # order filenames alphabetically + tmp = {} + for item in bvhfiles: + filename = os.path.split(item)[-1] + bvhreader = BVHReader(item) + tmp[filename] = bvhreader.frames + motion_data = collections.OrderedDict(sorted(tmp.items())) + return motion_data diff --git a/build/lib/anim_utils/utilities/log.py b/build/lib/anim_utils/utilities/log.py new file mode 100644 index 0000000..b8a44c2 --- /dev/null +++ b/build/lib/anim_utils/utilities/log.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# +# Copyright 2019 DFKI GmbH. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +LOG_MODE_ERROR = -1 +LOG_MODE_INFO = 1 +LOG_MODE_DEBUG = 2 + +_lines = [] +_active = True +_mode = LOG_MODE_INFO + +def activate(): + global _active + _active = True + + +def deactivate(): + global _active + _active = False + +def set_log_mode(mode): + global _mode + _mode = mode + +def write_log(*args): + global _active + global _lines + if _active: + line = " ".join(map(str, args)) + print(line) + _lines.append(line) + + +def write_message_to_log(message, mode=LOG_MODE_INFO): + global _active + global _lines + if _active and _mode >= mode: + print(message) + _lines.append(message) + + +def save_log(filename): + global _lines + with open(filename, "wb") as outfile: + for l in _lines: + outfile.write(l+"\n") + + +def clear_log(): + global _lines + _lines = [] diff --git a/dist/anim_utils-0.1-py3.10.egg b/dist/anim_utils-0.1-py3.10.egg new file mode 100644 index 0000000000000000000000000000000000000000..0ed1477043c8ca351edf20f02e06f1115d502efb GIT binary patch literal 388594 zcmaI7bC74v?l}Az+ctJ)$F^i=uRsI1%W zvm*K&tHH9;v06$#98wtjOS10W+nM908@semg%UFQ9o{o^KTyiR_tzU)J zwDT_)E?*p3dr1!B?0N}4&8Q$PVp*uEGCZ_UIF(Fl1Wo;ezYEx51n063>zFHo zqeik2xia-gf1P5cjt&jq$kQWov%FB}$E@9K9G}m6o6@qqgu^arGv;wviQ_s~ODHME z!kJ`_Wsgl4m$9~b;A)mKev-PtJIe>6pW4CQPmU+fqaF;f+5x5XOyOz$YF%T z*lGhyDp|!|gwR68qo?*S3G7##Mbqb^TqjYdBx+IT&L@|WW+0)XqoZ&UcG@f!+Wfll zV~#HP{VWg0pa7(X(c5=vX1p3sN65eON&Xkkh`hFjs6 zbCOf@W28ZlvOu-0);Oa_gjG+hbfk1-?%mgXPU(MW7MNgYV4PTh_+kI3qqU19bczzs0r0KVt5_{7icYjt7tO__oXX zWQ~5(LN7y(-(8Foc;5^B^j4o2DuS~jYCcq~Wh|)DK2%m}rbpRxH^1}q9vWWXefSdy zl%roj7-y%}XSNc-oa2J?_uWus+{vC0eDuXfkzZ@+@-b$ZMLjZQu_01Zgvv9p>o>hK z0)f(HSas>UU#ThA{#Ok_ZgTwG?uB6E0O2oW#o9AUX(7aoD;`2IsJE--DRCK1m)*b) zW^XvQGE-b24^yho+XL#`JfIEzVTs4j&17uQp}1sLby)=J^YOh>!=u^2kc2_p%~ZQ< zH(!)-sel1&9WM)y%+6}5VaestT4;#wDumJzGJ>bxNX(pDvbi_-z6R3{H!034iK;a_ zx50&qRmrv(_#M}&yLbMO5JXif`9Xi^O8c;o4Gw)Xh5-g8P5BALgL=0oqE$JyyH96Q z@+I}<(Z_9_%yMBeZ@I&MB!1PV0oQBir{nnckr~$CsY#Yr0NeO9s?jnG9`&j$^F^ z?pB%dI13Q(upgknpQIo6yFH#8YFTM#6U%ywuI8-pt8weMT}YYWH`3#m0C5zM4OhGR z^DFUI)8~)<_szlm*3#Af&xPx>cgx;Gl$lsI_Cj0v2`2LSZY@xn9?yn0*g7nMT_hF3 zi@j2PMr`bULjx`f(jw)+P;O-c1P^}fOLcQskBc~>Df)iZSxVmY(@xdh193MI`*dw+ zQJxr74Q5oGG<8sjp30+Eou0q)pNg=xZ+8R{N(7pb z<6x>lvcfTDdapv1a;s|w%%W*N7i`bMyctA?12v393gT?GB zL}WRKz_V!HDlA1@?&6{wGYr0UGN8al008)_*M5lNj+WAmwHAZxx8O>&%; z#$kVf;=u^x_Z}RW2hx{PPvcCQl!&0W3~DQC|G36V2uQ67Mv#W3V6|V&t2YnXuUkCD zRv1pp^w~1JO5fwcC8qtP)CQX8Vg(5%PT0!6hQ(sf$DR{BZ;lqw)RoUTxzPJh6-feL zW(4`G8h^C}fcNhzVr*h>Vry(-YviG4ZDDKW^pCFW0|WjOo9BQ)knAtkKY;e{*p4O+ zE*6d^|H7J(p{5ueqZ=EO-W#WyqA5F|l${1mBo&G97_xX3X^$Yyc=Z_4{x8a5!mHPC ze<{NJ1@wQX>}+SRXKmtYV*L+8Qj-&tF?2LzlN3k)&YITW)$-p3`;XPq)54y{$-va) zKZN1#pjZFf3jp|^*Wc%#E)8rgZ1h~5Ev%jB^zwGT(ZuoLp&hOY#TB0Dhrc|N z{}r3`KY9O0cmrn(J6kw&`Yz|;l6lgW&&IT-QgpKi6$a@U3}HsZWWszUXm+)gLLeT>u_n;hLTwluDegVme5LkboAb0pbs z9O4&tBe|@9vzdQ}ZNeBa^#K|H$RGy*NdD)r85kK^8(7%#;J}9T+vHOZlj}!3S*}Y944|9qCxoi;NJhql1vz+NrOVTW0$H~ z`Q@*d6&|83T2Ln;2_xLn>7Wl$L;_l)9;FTgB@Yls?o7A|vbL#jz78k-oJw)(H{FG9 zw-ku{A^u^Ava3E{KgF{b#D>RboY+Y_hFGWTk*!f?E-uI4CmLVeuh>DL_SJEMkgsVg zh+x^i=jhE}>ijiy87|ArF179QDHgc)#3L53cP9;I5A3is;I}hQfoDDwr(fR^T!>^W z5L!%D|+K$wGZR{bKTzBKaW>C zgXYq#y0CV=ag%h*D6Tvgdf4nA)_omY$2|op{moInGhGGN9T%GqJ(!VpkQ_fmFtuJT z*W|^H0Nhn>3=Cl)14oAJ>CrchV0vOGb;zv3uT!`$9&%H7Z1Z~ zBi&h1Pn4RSETWVeGF24&=^Nt*R87^1*ITABrbZG+V9h4tA|bJ)%@UMSMw~qk9_%9q z1xUB{}`;<#;HRVG11zyFl*up04 zd0GX%x7n{sgN{KdXhQDHH$SHFXV*XE19|e3Z;6@l+!~*pSy$!iq6;@$e6XAH%|2b9d;5ogY+({1d7>(=U3*~PV&%Z2X%yGPpRJxVtJ}kfQzQ((Z zX6ZT#)$EO7t-i$*T7#Z@4Ub?1YNYp?%I0gs5N7$&RTZh3V=oU>vF+fjk1y!{gD#9W zH5E-Wmj60pQKp|`0z3MbrrgrVJj`+{ja#h#G3>Z)`Fp|PVxA-(-HggZGuX{d9VmSU z>ZXXq^i#8=tmYCC$4qUSP@<%x;SC<2Wl?mxL=;fXWz_L0A5T7)TvDrO!-hT;7%sFy z`?ES93fDSB&%m$R)5$_13#UnJ)wK@`*uv8*pD-$KTOoLyWf=ob zDi=D4v56d+1auV@1HHV2CTPQ59%XPT2ErrS>CJc6QEb=7RMkHSJu3J4iOn}W@W<14 zP_=kl1sQ=3s+0YJd=$1GZ@`{o$0_7L^Vsq;RJ}ro==8n9A8o{sDWihRw=4Qc;*Qi+ z;+;b{3-eF*VV%54wIRe_XAyIBL>L!0ig!@)>UTqbDv6%ga+RF%?sy?^M;}F|06(PB zN&Zwi{bil;n?Gn=F}aKf#tUJ&77Cp^VX)k5Usy^3Es~BK)L8m!hc-@rs>MoTOz|Bf z6;$iP^x%H^+&yiAv**_~y?RZj3DuP$S9lCz{GW4FREe$h_}Wj1E7Uh&4p3^>fkaUg zhcB|2ZpK3TC%C>x->m5V) zLvc5#awTYTP%I(8F>JW!ab1?okJb}@T$Cla@lsq5pp|IhW}7qZ5UP=VtG<>59~~LR z@rgKbf`)Y{H9D*pvyBS;qQ599vcb|YAQ>AJ{ggJ?q8`qE@L@OGjI>-LvK$gk%;Q9M zuB=vao5J%LIo?!L-`B9xZ4sqXW#eR1obE$Y_%(etW#Oi(`a)DH7qjNYtOzMC1P1Pb zWCz3rlUIdFYOKKx0z1*u(GY3(p)ugfYxq1=u8}1Yj_(0#$G}_I^iPbGA&U238~VOWkE; zP0z;6t7B3DQPBKXjlE)!63_7t{_K5vZ~CytYghUZLTaaiPD4fDeRn`Qi$l`qw4|Kk zn!~h_hWcAny~ddpcM`B(1w%v6wM8nj{9S_ubzRg!cyF$$9^Z7Nr!xA_%)8DbUXu_X z<7R%=}ZV~`K%S*Lr<9>)#V*RTl)K)I}n@0;pNA-AdkuH4T$%B zz7ip*&A4cP%KV6Evb>Wi3pX!p4CZo<^dWqF#Nwz3vm{7RmrI*nc5pHO@Q4b z)sYU)+8SK)ia`L>CSrGCkafEoFxM)NGQ`{MJ-geTDz1_)NmYz= zUyHHcYNKV$_o!6bwZZT7Qmmqm%^NU&9W$4%%ZZ5#n?Ee;EX-aX^`?V(QDZp9MiE*d~kAOa-4=CaSblWi8f1)Nc}X;~O_%-Rn{+4D4ldnpWhuD^0#0 z{w&aRyGmZ>l)%w#J4~Cd;2Y9G5=zJpEVHY(P{Z7lsS~T!{k! zg#R;YHgq-r8!=yUe>!20)%V`3NzWnx!#Jhpc&5k`J=O(NBSWF`AKwq6HXB0JGh8@! zMj7)ge_o$ny>8(l-s#-w77H$F)Gcf-Y%eS@d5QJD8)OypZHU|=aQo_Km3n9VPWd&C z#3jWlXW`sCxw_6)>Suh6;dfb&aNaoq&V^9;7+G3%Umy|B#;qf!flkU0#n^H{(d%XS zY=L;pZCxia&LM?v-2*%CwWaSd5cm>dQhN~ZeG~PI( zKGet+bt;+%jXUNR<&HlJBQe3!tBw~1-AMW!Diql7!CJP0YYt^Hw-GmCd^rwMp=rH< zS#lVR_A{R|^Zwd9cORu7TckmgG_LH-133(7Jf(Tm--r-ngA^e<7+Ewf36~u`JD-Jh zc+`7#~qWn}Bc(Voi`z;N*}lDD!G=<=4gqUZH_4}SjT%ybbe%5On$+majrt>_^@5-G|L0Yft26_N^?cn0>42H;g?ylT=Q3}#| z81~7Zp!K&7+jI`y-&(y?F&nkD4Py&Zz))w082HrvHiW!)uI>DEKsf@_`~*7xg5+C0 zX>f88S#Yrx2;GJB;KteT#(v_}j$J+6IqQN!*meu(oPMvYNrx5Y>O6a>nai5yTFybx z8NdPL5CKWT^Xa;tD%2vdS2?j>!M&AbH46A~2j{`XyRd^R|A7OgR0)e`pg8|{$&HbGPr1T1?=_si|S|J-XNg5_TeIB>-CCmXsUi)F%o zgS0m+%M#UFA(acT##Q@$Fo&%ZN2+0xAowszg!rP}R)~vT#Ges%91oA`+@&ML2+fFY z2dW8=MT7FOR>o&XL?!S-dJ8K$e_|DMOH(w0MT~|Yp%~Vbz{h_5~-LR!WL?1FX#yQSU5gDH?mQQ2*Tg)#{l@jd^1RR|Q;Vxgr;06c=I}d8NexC_C z_$$=A-;_yR_HMNb9LiMzgXAWNFUtY>P6PIB!k=v7sJR0I9=(eFhzc}Edh-^}xSBmL z#2@4)XhqKdwXVHV+NnC=eQebH8~P|s{SheKtOn5$b9gVR63DOsh> zo=GDHp^p;P73QkvwA{PFdU#K0ho-1uA6C_-8D2eA%Th12y;J&)0UlNW&A=uTRr{h1 zs2h5g8;bULkS%iQ{WtSfo)kpi?c`Hz8#>(hD#CLlEUB_H>%rqW;y^`-xyr6tfs`;y zr~1~~`%6<&XpV#yrit$kp!76A-OYd_n-OYT^d3qYheRa4dU}_~wsG?rZ`t8p#1B4rM1aji%TXx1ZIag={p~!xFqh+*TQ&iTuQl)XDQ2v17Bb-46CJlJ07Igy) zdx`40QqXd)>UwPwb8ldNRA!vZth|1dI#&i|sNKwq^CIYSw_tC;))|~-~QRkqfDk{1w zgWi6y-h4{}~AYfku zqRp67VEi-(A6=11y4sIoRF5)e2#F`U#0p%>ua$c+=)|TC@K1G&OZkW|IysaCLX&9= z(+T}So$u(`_UJ|&?8gqX&sSNq;TafQ&A{G2<;ABCNifiLY+cQ2`LXfay)r?ws-q;9 zZLVq_{OnInTPqxMv2r&lW;VS_$vo;l(euv?e>~Z8Dq!6(A8NA^SqQnI{yd@oQKNc9 zvsB#Hoq2%i;E4r#S9g1RsUs1!YaP_tW1XjYnW&%^b$GJ{7nQx5)Xw6P zupIQV?`^NkWnRWP)Bm0Z0#_vzUN$1~7%@ygC*9nPhn*?zMn+ScO>-YYk^$nsG*sVbt2 zwQ-7a+{!Ya*Td&*nB{3ry&(X!NWaf$!4eMv{6>O+!5h<4SmO2Rk&zXrUs|D~} z))XkP5+f zU0~Ak%(LT_Ko;iD4Giyj|15gv+u&v}SE3OxgP$J1nBw`l7Tri#3m4{X$Wtb}PDM|R z8VZX9Ax8JGXhmdxZ~i$O7%PEaik?Tp4^NlrmrfWIsmd=NAq@==c_jnWTZ^7whycm~ zCJ@M+(7iFK<+^~Kq2+tc?1wiya5qNTL_ySMp>7K`{Po4iS_9#JqJ$@Ib*mJFYwjMFzQ1hD+e2awrs}Hpck3PH-?NN5It@$F7NN^$=0vmf8ej?6;3MH>T zUZvuwoxLc}URNvcR*8zietqp?D*EDrURcjhhXQ^FWgB{L z@B*P5)_3qB{U+#uG)tI0gfnDAG+*&lnR@b(C^$#&k0=~tE-ATU@9Zvf;ez|kYH4?Hr0&?@q%KAsBsuD*w(M~)v)$#f}$yESGg|j_Kkv~q7*O$HK=tx z?^`EUpEc&rcJ(PVwIH(AfLla81|wdVnsh0PWsazd=#`i+$%S1P39xT7fDcw1t^;e7 zqXQHxo6OT{y5rke5CrPi0nlyG<%eBYDXd;CN0x(*?Q$0vYUn!Z3T5LxQ@VNRg=Ttl zwK(ArDblnCfrK*MZQzMwh~uAZG98o(R2>Bw1-f*`oOV1H*c!Rskn?xAM%R|w0uy_r ze7s6J8o~qj*c)7h8dWi^73ouOT%f6HsiXbUaYEWqBvEufa=}l%UrC;Q?i4FKYMK>Nd&Z5wr~{QVkdu&RlRF#!Z}-omW^sLOUhRtriL5JBnW1C>bN(mKm15& zrU#Zoc5<*ruQ*qey@$A@M%y-h<_5Ru!J7@A~qIN_=)efyC32abEyZ z)U%k}1oF(+pF?Sa99>4^^yMz8k#CsV9FF&%C1I<@>OY(R>{UAQ>n{BsQL$0WvFNm@ zk6fxWDu3XhCCXb_Nj?tOi|F2-sGiQW?oLF(8;H40$8R>MZz`n9@$QjnJPDvu6ULk9 zN=bpUa`Bd^A!9{QrAnTlCvsI*)Q&`Va6_g1=`<%<-aN{ntFeejQ{00b|4Z15mxkU3 z`%+~;=GOr6P$4m1=EGnK>2tJtzh)$!rOJ`T4FEneTT&&m5ecOb6RU}mzOi7f-1ZS2 z294@Zt()@6NtW$LV&nazX$dx1Az2258!5elk9wb9Lorjt1@$j7Ymbs5iI65tYSaAu170_H<~)AzhEno$Gt;B06zt>@F3W)>14%65A1%SqI~aC zHAsAz?LDc3KPGb;BfCUwVDwu@NbIEBDfLKrlx!JUTOQ)QrT%UlLhT^4*FD_9JCcic=6CYM{`9iUHte_pX7ejVT7NTz5 z;Rk%ZDS>Tdf-S|?zC$Sn3^IeXt;ru=82y1V+PdCH+wB))Dbrz%Mp{3Vk+LdBPS{=9 zCmejHXx3}2Q1aX~L{Y9S^V?FH#!yYn5Z_CjXqKdkGG$y5UAD3lFtCoa?I@4)_nc9L zidEcG&}hsdr6mV5b;aH0xA_?Y#)5RubjoSyI3|rqbgf2yh0fjr8J!23y6PQz^77;U zS>aW3ixh*Xdf?ra?G4rRhA4r)-z*Dh9HGv+P7{9!)noV^zJ;Pv?=N_6v|=FK4*Zry zZQkA2~dU86wM1zoN+l`i`qGvE`Gom`_3-dRVv%z=|K7Fpxnf~}XC2WP&cu;zN z$V|7?Drr@7&+7Q}ZF1l3PM!VU@2^_E`4+ap_~5)0Aa*=F z{ssPy1qo#kjXC9~T1|8jW48A$1!nN3pzCWhhxo#ZF$c8?atSSfB@|18n4 z7oaf3(T=c^jIz*?M@>sCEL$%er>{zs+(T{OCnM)|ZZ^8UUL87N{!Z-g();q2bCsW= zd8)sK^&Nz3AX(&L|uerw$mi?a8*CMX*Sqfwjk)3e<%5@ybdcMQ(-exp8{ z;kBn0ze4OHaLxj^3&5Q%7w>YAfR|MBb4@)<_@9oWHnvaoH~|MMx8a3rBQttGm0Yu$ zuW-TYayMhI8`w{Gks0)!*dK7iaX~h~qykEfTrDdg$pmm*ovmgmfN-D}e;&^l$j+Zl zZq=KidMHzPu~|8fI>3@vAihqx3Qq+a>AaKuQ#m-JS(~)<)GcwbdTh}=0>_Afc>SCq zXw7+Ct>3E%g=_d>_zgd0o8jj~roN?_LdPkxtJg0#oWeUCe5``#zSX+;;k3IQwq=-I zI!?A?R$iJU+F}OJN%QnxYeGX}ILyp63c<*J+B5F-hMbv%}K0Wlk&TBM_r_rp!v8;A&?AO_b?NMp>jV zh=>$JYdBWL17}Bhm*fv(a4)ycf>EXY?%feT%r?${=K25i%*ns+3_BVgy_(X~b-d z!kJch0L4H$jyNejCL&0rJ~7KEPNUm^L`Avdl8u8B3g-LfsK$ZMoh zvMgzd9&c5?lgc~C$EFA2_7=2B!dA&PvrRkmtvfss5{yi;B(>41T_bfH6Lk0Gn|SN} z={CeHORz6c;1V+-uq#A;Lm8Dw@H36(p<*ZP?UU}2f1eTo$h*)D!<6s?;XlpC%TDY? z=WkgLC^rB=`#;Uc($2!xS?^y)`HvZ0YS}pLu_J#?*Ut81jB<3jyV*UL&}xm4 zOb2g*I`X^PQl=DwLf+_G8)1n*E$g*XuP53-jv1S^p8aG>0V?lYq?2p?(@=WXIvffS zQ-8^t-bwnU)ONF>SgcHRocoQsaxWr2X?mV_uy?l%|5}Hww>We`1Eb1%+;WVZl>`Q} z@)9u#au#ZB8L!cYd24Fikl&w;Me+dYk{3P$3=g}-UesdM=_DvH54t3&1~|As0HQ@Goo~K8oZO0 z2nrJc#4At{mN1h&i{b=G(GYk z#u~YA7nDKz{!MWED8v$Tm%sfHJNY2^Wynx@Xdw9V?OZ+4Xk<3&(iTluiY-E$Web$H zdMZ>+IYbSLm9J?Mq8Rb&aQfv8+u~R4)n;ZXi47Q;Rev8ra&S;4J$*=&#*U zfm9`Lf&O5Iq#h%sc+j4bNF5-{ej)xqxPY5(e(KC-Ho>`@apIt@!yBVcyM$@~jVG^O z7uqTqMO%a&uT{dohr*RxoACgV-c3AT;%=$=D_`PX*A5Ku`Y*5bR&bEN`NyQ`sY{oU zz&Y9HkGHGBg-`x15VKY`lI5JNX~f^~UWO7BRYq|9xb)lVpZ8Rl=DpY2ZlQUwj;mO% z0<`U*^)`;{R|s6kZp)Xv?cL|wPvGcff2X1|=gNd&sou8gst*1HggH7<|rouE)Z!4pa}} z-~p`Q&&_@cX(*@84$02@V3CxW}oC^d)?YHp^g6qrAN1ME&a{!%__on3bGmkglh9P z@+n{dvoEn@vMhJ(DOhhCT)6}gACd|rOUO)@zQ_+;iO@&zGhh<`<5<8@mc1~6jlOEA zjT;Qs@22|2VJMsJfDIFzFt8um2FcTDlEoBRKuZQJBVRcxt<7$vzJ4;}Ml)qVD=u}@&P z(wXdr26oDk+!c`a;6}BYguG?busm&&x5hbo1=vx)2k-l9;p+G?ZQVs zn~xJ>_l$^?SJfDZAq`^AGbaQ7Q2eOd^(mpX)%5M#;D8xTcO)P2|K(l}V~6X`b;aRr zvCsMjg0-VrDo8%JG{ES2wZi1jdEq^s9Rs3C28j({R?z>eyV|S2OJJmH?ycZ8&htL( z+RxHrSMS#{^80eaAZ0mFg;J%JBQT4{u!edi5Hx5tfqR=zK5L-!S;%7Yro3RF1B);r z98!CjCfq@Blc4s4OZ5;kk{LOSNpGO~Vb*JYh>i>oMVILGv@#}Y^(tfLi&&&0IkMi+ zK%p{{J%3=}ctHFpMusqdb)fI2n7mR{g!WI-l@8^zP0dY&I2|CR+>NW&mgF0zdpTf|sKH<`7Q*S}rIBG3>I0xAF&}u5wFjL2 zG=j0qls9O>{tOK(P-GliALm@hG1`qNL-M91JHBZG?%F3G9d>Ne^nO&8xU;MU!9oFL zSC%&d<@;fq)u|$ho>-osn?eQ=D*0ZZVKVt1N(KG)G(%2@G!EnVwfrvnowOPq=l3~lbU1hc>wVg ziKhaUXb`WVPMpvF=Su6^c`U|Tx)6&7=^(ud@F&bf)SkAE>;;Dt{-i`unM|LFnp~k? zmvSraQ|5#7^{dpl*F4Z9kPJvyv6BkO>1nwS_&gKSKXW{bhdM$b;`Voa7;v)`Ny&r7 z)K+=y9R{mq9GK9RRv)esi56rDgyNTUDx}fwDCm&VQLr;AlxCt5iS!XAA*6P8e38cw zB~N7c>`E)s-=^Fd_!>$XfUab#yyN1g-weovR&dyaX`B!RK&$P{-`3t`{U5;AxY2z~ zZQ+X=1_Zcy?ND%Ca7}#*Iw7Lj>w;PvMzR7s5z#2=p$=S#>q;zD_36Z6q=m-$hEt>2 z0(QY8cyk zGlj%bPTSNiv~~NX4X`2iLm9(FB%NtynKuz-z$ONU8tlwjA@51ElO{wfs4fvV3A~~N zDcQegVMxOIL1 zsWGlB&4`VJCMqrke43BzL#Y^RA?6~)@#os^C^0$UM}>GxD=K#VWEsLJq+<@uG?J8bWsZcYKA+pD5_v{3%su4<%Lv z%zYJT5gat7u)Tw{3nOYN=00z7P-jO5AR(0sL<*+(4d#*khD~x8KL<%AmUK=bY&w$< zOw6C@oPDhjvG2l?%E#tTuc~c1beK8#Yg2;>d+smDopXfRIlI5?RF1w2$GZe9R&ujR z^QiO+EHlDfm|PB}AGHo)Ce#m8T%^Z3v`Li`Z{li923`e8l_uO=&{G&6f#aza-B*h( zO$yH=Ph5(d=n1CzLq2MijN5zlTEcrkwRPZ;AWvkq&>9Yi(Jhbij7_)|?qgiLQK5{BrHOnvMp1L6^RrD+rU025Yh zhivP1z@Ywp$=5`NVpNQ`9G=;ZOe(c^d%vZcytnSGYn->#$ayL}=EsCoJmA+_wz_R)-^sbEHNjZAY)J&Z#OXi3a# zX$y(u_t^MfD~(m5FVOAhA)sh7Xi}i~ep-l)r?nbaNWqn-nNk>}F)rM%=qdb8Iwkl; zPMSR!J=7FQyctRy4YpG&kg;?^SbeQr3X$&gilNbQ5YBOjA)6CMoXUx#U0w&ivvLR= z>;w_>%h_ifybv58kj6H~36<>bFc)EYCgi2i-c%Yu5m!|&d%06x{wdAtpNMuJgEqRe z3Q&l{%O6BEU6vJS^7}XuB7gSa&q$AEvDyx?toDf2-7hoB3Oe)<&dsmfjm}0--l%v6 zDBfuGo~8$dHt> z+F_YtzNqkJfC0O0~^GPmgwi|vg%wn(RbKQ5Use0C(C zq=udt1c9 z*6d$}JL`_v5)IdHk(#dt{Fbv*o%K?%C;_C_C_jGC4RDzS3L(Ow7TTPMZpIj2FWZ^w zG`sQ*Hazi8N;&mCO&7EoCZcHh; z`g%SgxIR7|!+W*j_SQzt#c}I`OCSpq!6369GZ6S0xz!Vtc%Rj4Omak-S~EWHb6wRl z8~VP)RJ&&b1es+&31J7gjzEf`ZUk1G;e!*+Q+eMQAZ{q`lSnc_dF724%o}v?=pU61 z*kRDQ4N{z^fxd54cX3t0eo;JQP(udbn|X%wfafoBnlQ-Cv!@fHA^87{dPF?{P-5#N zQG*PDK|z9$h`Z+aSU#n*8C}tCfk-G?v=jIn7q*=5^i0Tf3t*`@5L1$*X6ZcRovNf92 zh2zZ6Bv55R{!m$jLtTJy_~G){7bIt&&`Sy!Qb0`)kIOb-OCIr-(I1VwdP67oE%DA# ziBcq@aufMSsEgF=XD+IFDt!B7ZEkg4e$SsZYaFNgTx@<+IxY4XhE;35c%RhkaehAk zH4|7u&uZImHP`#p=8oQmJg!ZuxaRL;qgo#?m}Fbes{7r2mr^)6uFZ*aIklWCt|xH% zPBtIc&DYsekkzjk{qk^^wN`%kc)ljDwCi1gbD86gh3`cpJFZXKSrfYe>nI=V$G4mb zx#ep7_m}eL+oKjZVr`&Z+tT+I2=6t5wM}2V7vSnEIIC|M-iGz(>T|39nnx(-lg+~D zG&TaiOP8(g>&9ocWi=c3mlC-7<}K}3QYQJ!yN<2?6F*@9Ri`#v_k-2(bJCZS= zaXr;}_T*{<0O<{`nP-(I=^n7>OGujf?9m90NX28NuGeD%<%0I z&pu%Bpl*Xp|B?}RAV!E7Y$4*y&QTjFAQ34?g9pHY9a0ZR*xe5dOKlSd0cp^GL%V|& z#fG4|#e@~_BeClze(Xn=X?f^X1(cQ?-mX|I0vsUi08oOU1|yTeP!!ZVl`62B;)VEy z0kD~;{MMuJ+8MVF8nBurAQ zSBEBW-+_@3++Qxh9;yl=tiLoa=s4QK`!K7$U)X`Agz!Vc>XiLaN(I{B=k2`Ok+2o# zRhcg1$7eV)hx`YtxOPU+aed-Ob*gg|LPd$wa1w^;nes!OrUa1v`zJ|1P`H|Xxg$8F zQCMh&16n?OU4H@}MazQSs7sWTSvo{mZcD91dk_2K8{}drm(n;Z@0M>*cnjx}98wY4 zPY&5uXqfU?l*aQ#&i=+NElRSU7+M`@=hsZ6V^9fviVpx~mq?&b5H(-2PMzuy=_8md zZxlHwP09BpCSSAe5O>Fasq!7StU|6sp{Sj5nG^g;0U$nu4{sR}C~#$+iIg6J%h29i zpysxE5&&4RH7h39g)Zg?w;%mlF;%K4JT#R|w7O5?p>*Me7heK;1+jjA+~Crfmk9=~ zDM2x%{Y1()f)JnYpvy~EgbE9lK+k%)5Z#C#Snr)2$-o3=BSFXDoo(FMTQ595t>0HQcjM}HSL*#VFKDwhecCSe!5`^565%VX|GHG zYwxo;(Ujh_KbMr=oC8J?IJe)@yzM`guN|sYellilWkT~DNYQuVP82Mj6OZ?^94Kz7 zYe+HEIiOC6Z(K3F8eKLJ2{eUKvp`5lzFR^(_F#_!cN2gtl{X|#h-G>u?u=M%2)0&- zdy+mCS@n`b)}1RlcfPi?b~O@y)_Dv8t5$`QWsJ&5x(a9Vb8{&##WHywg|O%Lhz8lu z_KQA<-+tmDT9W$Vjc{gyq=P$h@Sw_!^Y9Y!o7HCWg==@{qb{(HUe% z%&+47`8MP>6(ai1w*fZRWkX3UH`7noGo-LvB8eQ?rIc*%+A9YyWL5tDNp|F0TRzHv zoEDy@g{1sb9K_|0Y$f#432vQJ8=EeluG5qaz`@e}SL zTrGPKd?Exck+bsVO-CnM0|H%r{>svAML)$nMChw961LCW`=}O!LiZ>q58HQs$=yV3 z`t467oG3-iFZ11a&vQEDnunf4i@?GlYlf}K5CzE-5c(>MD1W((j;jyKtFJFOPFj!j zSuZYQ>P4;ab=>}ZGF3h^wO9FbQD>%a6nOq={ zN50sq5048vfCPXCE28XmgZ+Pp9jyHxf-((fh?yV%#1g*pNcj}EnioAsHtefYWp(AZ zt)z#V`H;XKGJoX4kL)Z}u&q-}2XHf)D87_u`a-C~i7+wLB9@?W2-QR>R|Z85%ct$- z*waf=VFxK)PjV3iYQFH(&dDaI4(4#syYA=Y!JQiecn7SQ>`8u;v;xjw0x%|x1{c_O zj(65RsD$T8hQg3XzN2C-F~BukD#^!lhrn!?6h<`y!wbfK z!D!`*y<)kww56u@n8;HJo65v{#(B%VZ9&p4v%6xHkt+?3D?-&Tj@BZFJ0Sg57krTz zAQFxV`+tm`LzG}qvPILjZQHhO+pe^2+qP}nwryLL&R_j!J?r@!zZ)ygj(q^gQXg;3 zLDe%|5C&#A#ds%P_jqEF!1c;XMG+kN#XR$Un)|hM3~gCJz+<3UJWgOZsHFWHP&FCd zt8=r)jJ>;!^_slt2y}6KLS4kj%8WBILq)T|XLO6(fpF;4Bwjxjw7kh5CnH04J8GbU zT?DOyVZG3OTvU56WoGE=2#Q9$L*16QZ%d$vL0f$>##c5s=8b2O-@LVct2Tu(WVlckBfJy4L;#Pv%}V`?V70nx9j*$Qoe6gk}SYlq*4?5s!}1B2FxxOJDyhED6Wan)Fel~>il9K zcA2pJS$M7bG^`(noA?^cZ)*tMT(~-dX0FVge9G;5Py$ugtn!xKw#Wm+Xp^$W#+iJk zqblY#?y{Uqze7Q6Ig2nm1}HA)Bu|Oq?BRe#|C8b-Hvs=*6k|PX`JG&Pa#o@wolKU8 zKI51vN}IGFD;rpa7|V%#p+h`={1`pX1p2u^iz&vnMWfOGREToK74v-b75Mv2g{)N! z)}{LUO$Uv($UypN5ai^Z)~#V`+=eySnzK%wMAJI*E}dtM>g9*Hy>L4yqM|fzNWU+ z47>8(I6O4dT`VY#|JNP8ih$yaF@SQ>zuyycxam6R={gA7i-b5;0VCWkmF2Iw=x9-tf2EdpHkSg?UWsEU?=KKCw|p!b}*4 zj&+QEZjc)~^QOhFY6=~1?3s~^PRd%%M@5az75iFV5 z+S#iIC-OgL;tvFWC~>u`E0-uJ+-8?Hvxh@>6ptvgW6v*Rjig)WGkdzrm#42Ijn#YLQ+RYR zBk_9gm!oPcyJ*W#v&pAw$>u;SJ0K&guh7D@s#T4!`?wTW9F=fSY)nbeU@7yzfW}hs zXIg`lnMo3cTflzkNJA=Oj8t{Nd*2!PvC1#+0uqCEth|6VT;@}+G`1*xyXV|jq!Yix zkqb7L8lZl`N_aLAjorT-Gp^kwNh}vSQWYx-L8l@$({PS%Has@j_|}!|k+|%-P(QX` zf7)-D0To2@m72#&sqs0amI8$?pxEs=*lWz?5|XqFi-~3t3Gdm0{#~i-34}*>h3kc= zu31N=qHSg6V3~((fR^Mw@`qTd#|ec?bUrVXE7PsF#J8aoTi7X$Ut!*sjfM-H_5tge zr}GR@QAaghr#o&5lLI_v3pfq3ap_)7k0buMQU4~g-m$#vx9RwdkOMstZZGZG)e+}N~!*WtQ)V< zdI$;a>e!M4ZHmd6tfZo$(x0i7HX#B(JWTH-dk~vkxoO6KdQNa!8cUFAr#3C zH}lT=%albm9hf1RHGz{v88u5lsCOf+T^r=fC`zYg}_eT=k8XH(6`Rf9#wdGs^$K|X-UPXq9x2QnVp0{oQF}8N7h5EoD4WBGg9zG0jr_ z!w$MLo1liKcS>vG$+36nEN3%YzqHXq*ucW!~G@!mF#1R)) z&)nV5zHFx8eKR%~mVuH4#22)8Qp69UFP7ASzuy**(v4XoO9^Mi+Outh2_QtDxnUe0 zUVj}RV*ptHR!e-1kjhFGoCsuj>h8Si2oqjinW8tY4I!4ln3< zZn_Q+=kR5p?ngJi0XXLGaeeI+l!NRv_TnV%Og%>MC)l)DhT1f+CpkIUw86Ns-*>1$ zxdWM=^L!0k2G7G6&0_*5v16TDS7F7GlUkeZPIRrJAy{wKEW}cbD4KTA9Xz{ zE9urD_RhhxtxjxfNe1g+iYa1-{t|>3yZ)JbL0czPhW8x`{zzj^|71EUW8*5`Ni%v2 z4Na6j=yb5~t=lbEx6*Vp=|);GaxM_O_Ahj39gkw?gyvn%46gu^JeEyvX;ra5j$wD{qs&dAm>Rq? zD*6Fo#y2^9<>sU2>!4q1u}c{xEcb&~>~oOyHO7y(Ws*f$Rl z33flB!KnR^;7|+><a3nnT%t(+208Pu-3DKQ2VYT$Q28*Ma~nC$)(#zhlGi6a_STkz zxs4a-1Axw{)37UQMQ%o+HG5D6Rm(`!7_`EoDZCT4(7o&g@Oh`0HQ#=E(+^SPy?Bjl4KM4 zI|u6YG(s_VnFW$B!vw;w#d6<1I%ceAz`uVgs@afv#yL3d?>W#=qR=eJY0bd*k9xybioh^XwkS*A40Q;ok=rVbEZ-vvBfsKL%xX>jx9~*KI{f zYmnGqPv0B+UpNFgxeY~bcX4rG_s{3YD-U67;^cje`uS%Q7ogmHo^H;72J~{EQlQrs z_Hg_8PhA5WRc-hC_i05(MN#}nuczmSAp4E%0I%^q{5)M8o-YS)_t8DS_OCzB-T)-s zVEyFcPawdUmh-elI#2Ha~#m8)9Yv5L52GHGZp`UTykJY@Q~X} zu*`lt|Ca-Ag9{H{4jwRzv-KRdy^Tq;a(6WvGlur1Ow^|uOHeTyur5!|1Mk?rlPoZA zabDUgO{yDF*2Ac5bzvLEV0P74Rzp9!ep3ING|_9s_2)v&QvIgQKON&)Ho1G8@LTk_>(QCHblsfXe*^sq9rFRXUg!Yh)2O!DGx!c*FV zvEbxGe%>72@xNH!9C+Ciez`fhdN76xB!o#)!H4fgd|xoN71hVeq$KxJgQ^&UJ$T)m zU-sUNJbvH?dw~wH{DTtw-yA&rANuwB-yhe{hX}GR?w(*p5`a!D>9g(D!USNG@Rrpj zW&P7q6MnPM**Gl8o@%KxGxOp~@D7*rtqZFZhCn*SZn>yDHS-K>0`u%O&+YwSk+!lN z;{5tLS9xoy9rhTQ>NpJRXTk-+RH8S~Q|Jl{V)G&erdr#lJQgd-IAsd?E_$cy(`*(+ z6ts(9aHk0@YsRMk5~8Dcd~9eAmYCS4jq~$1G>zGNs(HKP4R$FPP)EmbuqEr>H1CFXuj_leeN08{ihbT*RIAQcv z$DkxM=EE8E*&1BRMha0)!st;JMj`}+x8d&m&By%^8>pxFIHn70Z)CLEl6`$e6Yq!z zcz(z^AVuwo^=b>R4&jttKerRgQ{;w zorb60XUVDxM+-FE^co%~+4_1=4?98wiFSnVV(rK&%K2|qA2fAtT@msS*5*9Orpi<4 zJ+JR=)P@Ui_nADwjpjgzxZ!Hh zZzI-vmj!c!$UEsOSgsEJT-idU=G4>cn7~K{%jfZ>-0zu;6q^+i$KL-8N4bf(f0dZv zu%w>3RfFlXC0JyLG&Ge=gYt}WA26)fsdDuNQ1FIu}`F>|9Gd~Lcfgb|5{N9tr#^oh8(V2m*qy1)K?SSWD5TbL@}x-XG0Ejno#=3ld%Y_ zv(PI;%MVk?aFK!$k`B8$&dWBc1bL{*m9bKMRIP3Je9HEv#I-~wla}(nE(N@;m7B*v znZ;)|?Jao^fUByi?Z8n*jRPSSIVQ-j%R)h-o^rB|;v@XT3a~DEzG|LvPK)YBl>F^Z zTHhq&=QI#_#)QrwDi zeygbo^9ksix=^^n#Jd|umrrJKIJVlVr>Bv5p5pGJhZ&P6upVJay|T4Q4xE=}9TvpP z^RUgK(?E9U!h%}JAa6bc&&-rw_#4S4@$Z`&{>fy2WO{`i{+MuLM2TnI@o_ zg#gl*YbSzGB+_23naG0*29gmPz&m0=UOdJftHl+o<|5*y6WOpA$++uq?9yDSq&a0m z?+wUn{LAFt86cAz(jI{bn&^&pe;(tKeW#1O`}&Vz={HXNP07^BkBi^Rm4}^2#Q1iV zs68k1?v1q{jYoX#3SA3+P0(mA3d18L-$FD54+2Msnd+pze;uS7Ct#^)5@NjoeJl?4 zjzRz`Z6hN@lcpw2U4x<>v|SnJT)~zfMm3^j*65YI@S+M zMCCZMJ!1_M@T39Gjk63XnCJ6qq7XS8HnVh@YPg(o=H`2^AV%erx2Cj*iKiSG95=z8 zFcb;1Q$_?9u985^$0pZ&9ri8?{53%w0Zb2bs^^mas8q;!m5H`c7M|F>)(AXf#q>Yn7pKa@~E1->-M; z?<#Jg9Dw@S9faRd?lIP4h$f+H)PoQua2q(sQeG28O@m|-3n7Y9Hi`e7R58{ryYrrS zBLmiqhRcDaEajWPDwfM%zQ1wiT9md1Nz}}fdeoU6og8G;faT}Fo%T#;|el!=@;Y1d}joyCK3to)uvq<_ z$TpU4yQKithtzsidF(i6xzMN7?y(#CZ3@EuN2K2a7BMaM+C%S&v|>(qnIyl@)|R0Q z_^o?zI2My8-wEbNbeG-M*GRraS{2$F{Mu37z6yws*Cj*h4e4n`;CXmexs^VbJ5-*V z?B^P!!;`ZEd-kb$e-ZDp|1toDMiJ5+y>7RQB2{o1D83e4AQaTyk4=gNkk|ZL5>P8^ zG4Z+vr9_(6l?8;*Ke*5W{R+yju{QeK1y0L-ap+Bkz`UZICclPL&s@=%bmVw*v=c#aTkXpr1NoaA?+ciZF$TL%s;XVj8U47t|eUObt0WGZls!akXh&4 z^@BTc^WP~&CTiofL42gT>!G>TXtXkgdH@O33E)eYkMkedmVQw$DE^^KRozBszB%TO zQyQMm^FN_KaNL=Vlq}C(eiM&69H0nHfCnz-%O%#PA}nRQ99YgY=1m3CJ6_iASHB^x zJ+^aqcPKc5LPIV*H)1ED^BFeFIrR;i;%+7!|5DS=otz5LqdKDr_YOPl=@Y0@N9BFj zQ%-jImIe>#oT;O5*57KjRki9h+VoxISsBHfpYnCr3MI-LDm};+a&1w zH7@>Sp~eW6W$tQwLqhkpnD@weNFv#NwkP{6?muDOyHIJSs>mCVLv>3Hl}#3ArWjQ< zW}=JzaH1xV30C!;m99>K&C6CuA_-Kv&B5m_?tpXJTsy95a(ReUe-%qdM5enAPx>;A zQOl_+{06=o1bK7~Q<_fOjnSfV<+gKC5l-~gb(EiE`1aTd>oo?eQ9GH*{}%L^np5`p zpdon*vK3KKT79~R;@1B4x`W?*H8b3*M-vLm5@@9$)0ONk59vk$h3!D@_t!&jS%XqF@76^ot;tJr!CiiH8hhkGxTt3B&5#Hf-0~5Nc?Pjw``=m!MjcQ0g z$S9;^71EwZoMkQFPxZ?jqyOvII+w#lGhS}fIbb``c+qg?+33hkRNdOxl7J`hw&Va4 zY=40s*bkuX?1OI@_K|Pfh{zfKH$;)f%H)>q;yfG5xSFG zdi@NT7p3<3GKbaJd(XDr$*}qf9J{cHK3l+V|KBC{2 zrHC3tif<8?6Yu>~`po9u?Co=ZW#l5`tlt$Ghepdi5V7aPW<7765D#5vN>AdVsdyNk z#_B36WNbzmGmQX2NjZfZ?|`#kIc@<~dIMU%KH?cNWR-q8SbQA>X2Yc8iZe~6BV2U| zWVEIckScdxu?Ez78SYBY*7}=(0>Ns zcC~l%JSofg+e`DjNG3uVRm_XNVq+>FQ_*dP$1Z>Z?R9oMGQOfC3MX1D4fwQJuFp*D z<|0OZpM!`F(Qow7lJ03HT(sW6leX==THIVrU7v(~i^MwtY??~Y1_<_B)dAK83Rx3J zAFSh5IWaed-||)IMUDY`@ZtYp8wNVhY8pfKHDX+9si%pd#vVaaMRxM>D{>b|THh6g z!}eaLsJ`mtB%RUSG-%#$E>K}Z3MwBPA{d0HU|c=b3((t#fWAExj)O$IKvYwsv-&Qq zj&{*C&+|A~d1+g%SVlIwV3&GxvzWX9l&z#UoW1HUykkCH=!DnYTzB?UMRNxeyP?@L zuE`RULAZw*FZ4gJ>do48(@-4_onjnV1o`Qo z13{2(l1d+&ov$xx`#uHfoU4cp((?kM+Tn@QHyJn~W|P%iib-BL61HKh`zhb$!JgQ| zg-I{s@h8Bn$+wfbR4L_Ai;9lh^RLPno7dnj&iB)fK<;6}>Wfgau|SPm>_CNr4`bTi zQo(o3k`T_FgKl!lCWQHYq|q7R61G}Tn$KgRcRVo?VqfdZpA?zi+E@Vr?v4m~9yI6ExixF}oe(40><9lxvdqHbQ zT65wB5RDl|#Lo43a!90?3|2$GTMQ_~H|#e8S#Xx4I8w$9IFcKLwQPKOu~w1QH#v?|GFzZQ=dgyrLTuxfD)SD=X`lGVAcD1i(6hrF-QJ$9cmK7u!eZPX+58`40(y!;lh zvLIt@k~3Ro@%0tom2mdc%2(TrUZx-am@!_s9TM>x|5^XeGAC(Xw@KU318lI#q5kC2 zSG9L2FNm-lbSGP|0*GE+0!bztBZH{24!YJIN~+6+`tBDRz~@=nwTWn{fDwokL9AQ3 z+Y(TI#e2yd=PdXJEnZ^$l`_R38^ne$49hN|Q{iJA(87E!MuCIAYrV~SN8_@Ns|dBX zNk@w|;pijatS(Aviy#U=Wo}u>Iva!Hjq9*mzbAxH)BICH&95+p>2U}=w57Rs)fM_ zrKsA1C#5g@c08O~L$(ARQfOZMOcPM44G%_vmHW=La!ZfBs-)=HCn(*HH!f7o(I)U4 zu~AoPa6#xDmeUi~k&k4@pP}{xyyusH-bR@K!rn(z5`66|P~6a-)g#;J4c_x{>?rGt z26i~ffdzJ3yaO)ttTvwUCArqWLinHKzG%N2iFImSox4sCtU$(sK>fF&&VpF$8#+>bV=NV0<^hYjc(jQ(!P&u!_kj%5C93npG z*TCGCi@y*~78Ka}fOAb}UK33Zji98_>-pB+`%|LP8$dq@;@tGy0V?;vs zgooAlDjYVjzL|I-EzesYEpl4c_X_8ws#dOQ1bbCmyS2kxs-ot)Wpsyyzn_31=UN4v z0fLXxlwP)T+1Ht5TP9+{`qBV@iE^uhQrN4YbO0{i7KF^TWxaET$ANN7l`-IQh70CJ z1!cJ3PVs+pAxhrP>E5mUCN2Jb4JVxxyvkqX-JPP4L z{qftM#)7xow}$aJ15&JbXp}xf9pD(9O87-y)nkWWk(I7isS!h94;d{=2=AL$8fh!0 zjA7!$yUL^@VG;LVF{>6Tinw&Q-HHLlO0+Lrf=YN)Y^=5(zu>fK{DmM^F{shHEHFP(5;-q zYW$Zi1K!kE%&J9*7rK2qSfa)4krFg!blOmaE0WkSxvSZT{HV;M*2cR<6Wm8@Kdxt} zfkj?6E2qVm17I^mJ!$@|6OERauR+t6lWfEx*Jdu{=Zb}z%S`d7x`qrN_~lAh>?x0& zAO98|-qje51XZ_F%VLXxtZivk9jeTj4Y!lvDYg2A^v%@@OqUpCOHjIcJR9 zg_hk-OoLRZ7b;q=X&>{DCP65khdb{D#U8pM@a&18_2B8o51AI+Cw0 zGsc}8brbB?Cq1z^}F+8RA|M5&ONM#jNt{H$~WS(27Zmyp(sp zmp&luddrga7SJ5Y;tSnX&_kx{olt)KbwKq?P4Wc8sS9~(r5e2 z49x0O1Y4ci;e)LKwlizsGNohNp~)6y$K_*}ixS$6F-iaJ#)20+H^hi#RAAfHPWSCb zw(bfyLeEQT6s$5K-^pk7d5cUejaWqR0kY&i$0-#)j0?wXUFbouNHJu`Q4}iftfw7 z+RGSKmw4DG$o0mJ8jC47C`2s5x>z>}c8Qj=X3I3J<(3ZmUhm((h)4|4QCLn}iK3`0 zibW+_w00Rtu|Bx@%aw)N19Ki>q7G0Xom4fquKk5vup#MKP;PrhL~SlF&cbEcL@AdKJz`EdyKN>8qRkbRzUL@zNOuM-vJyyXI90I>8>(vsX??^ z3@Dhu+Vf`XoEQBEul6?ULe%@^pHx{g&NVmGBZvl-{cl$6;!50wTsWbF>xDZzAW_%* zfoYE)&Z@N_f~~~azZST^!$h4aZ;SqWo{?XSV4Fyey5e#bq@=2g+uI z?=vuyWLq3Yk9h!k>H0unq-SB{RtUz0mY zn=L|$TIc0GzYc|o-h&oCaweWS7eov1$zsFvqPr`t+Ne+QX0GgS=^}NipkI;NJ!(fE ziFK(@x;}2Vp8EyOD!oMYQ7UIo)CyCGZk+^NTRsSrIB^D5UAc;@oCk zuA>V|j)(cJib=m)(~p{^Jn6!aGk*4aph4APVqgknZo_30pi+k6KOF{KM#4zm#l!v> z$I^Ec=``&{TtKN}(v%NpRTpwH< zY5pU2G%U*5euQxYSOQAS*<}h)&q_E{9fRq{Vd`B6%RP)i^L-DM)m`*n*ac%jvN^_| zgG*g*56`!$!aYM{-_#E3;C1l1YWjD*74oqqS#_hrcLLzQOngp_%+o2TWYOSw$Q&6{ z>N^9fUf#K!_M1R=Bd|()>=uT~ zYnUwhry}3ba=k+j-_LO6EpoSRb-fgBzm(?L2)OoKj1u0wJ6B~c zchL={1pb`6v^4Zil>VF$mPje`Agg#xVZiHPu_t()oAk(elZ#7-EwU5fStN9&jRYX6 z3L@MwAVVOz&~HZ{eU}?;?xKZV%B*0jtsS0viiS2CxmbRMS8dVIl4U-O#;_k?OGMoMQw8=d9L&oNdK*|U?4=kiH z>Mj~SKBPIYpPts)8X9UQ_ePWku|BnFGD-35WDM6$I%>ri6itYQh|>1KOFZFPhI z5@}b3Mlzf@ANf`FlPdU|_Ea;wh&+RLC3AlikWud8N(%t%dd{kADH;fm7o)zX#3<;f z_s(gU4ORS*=>WH@XW`GIU}oPD`DC>-eY}0Dw$|@OqeYzZzu3qONHy;gHr}r;MuV_c3n$9_~^?@ zW;UuRow8R14TyDsb|VH`lIsG6&AQ7et~BZsxfe9;c@4_`cj! zstza-*Wu>!@qOnl)1LUA_y#0TX+qVjSaPxazi>xUb#ywmg;fO`Igduf$w*AV1qL(F zcs>1<1{tRjpi_ZP@zUXI)fx`a%^;~g0<+K|P)v(5CPdnR!o?Uj@zr|7@rlwsrl8^v zAn8g>XfTL`2@V|x8D^O@bfS}}&o7xpKLgdPoER9wkmi)(Fb^Co+V!Xy>J^tu9m8O~|7OB`ZB1A5ur@(Qn zH7dt6rkRtpinvoWh$v}XtdKB>v7(O=S^1SB=1N6lDdfp9-f734(lwZ&2oIUuxdcd# zGp~nK0m$XZr9NV{kaF90ibQR?k9V} z{zw5gA#VHn{x>`SXen&%{Vz_~>#*8|)au48-{2KhJdb^mRh*kL1SS=ySNwvwV3R zDU2|GSQ>V$D@z-E{^)aw&539CL#*QM>T#M)yeNQQE1rMgIWRx=F!ZZK8^a2zklIQ- zuI6x}tp{)u8_@y%ZUPaZxp)HyW#%d$AYANh=R@=oug*+wyfeFALl(|R_~?7DBV$YP zn4ExRWl@el?l$%l?K1R)dMf+SJGWFi*p`>ju>GUGwEWX3>CxIX!q24a+Y?9{`i){8 z=uA|LRu7?5BUM|+OU9pxZ9?eRxxwgCL1#0wGvTu2_vNQmjmN(Z zosZ60FJCqQW@o3BSs1)(PMA5^pj|YeX01S*B9jC1e}sS3y7wSf5HSf<+RgtO`TFsM z$s?%BbJ(08ICVXDXmVMm$(=soN*&FPF(ORy@C$|5yvqX`k+-TInV_71vvJ^1`HppKzK)U zVo?n3{>y-0+pbrwgxP=+q+*8iS5_w|rw|aUk>^ZvagJNWwOzYecZtyEZljQDRpZF9 zL&f5~!{?(x{n-$>2#d@z|89+Qe_=L1{-t9aN;4BV5V)3~c;d9awv&UE=?@D{ipI~J z*r9mYm-d}UPF*h*Oax>otlEq<=8^Y?PYIh!8G18LdO^S;@HN3T}$J}Wo+kA3hKEf{c< z){HNAs6^h|!q_?hy?mEBqQY5rD?ZQjCcj@)eAwNY9#8KpGpa*Xn%dXxOD6;GVko(k zc8}+97*@KKQDIy%eO(F_iiU3(myy0+9YoYQ3gPekKM96%7qXa`3Lmg6_4rxwpIq17 zy`h>uCaQl+wi-m$cZZt^eII61`-{doauysy+{fiAa+`utLA@-OC#t$xW&iSlLBS6s zt1A*WaN@=Q{=7&uu)jze zNaQ2TlAnAzP~ zTz5$xh5|YuKZ+Q0=_ZKUfK$52FvU@l9%N7FUb_p*CWa2fsgV>q0KpuvwCwc5q78!; zV~z`MK12E^+UNFeGJ-mWtxF=!>R=;8AkU`q!76c!MmVh%L0Xo`qr7Di&n~f~6NGKz zIRN(;k@$(~>HeEW64S{nuV)cPKngS3oCV+oCLnp%+&WM|4B#XXTr>Rt*?Ago7$J-7xJ?qaI=8aPHD(K zUnrkVz4}$3OmX}*Cfl>b(*O;O#yL$vw30lDVV9C zoe@g&9ZDg^h^d06^-1N~(iuv&-If7K?ptWrQH#(dp)&S`2nrd@gsNW6G+BCY6BhOz zze>2T5v@gTLxbj4MehPxx`^FvGmoQKd-1m8AQfP`z90VCaY%N4)E~oE`WPhd=DG~+ zSb&NL;x*db*wx_0YJT2lzC%{}i1PfK_%V{OuU4^uuig7a92u(EfjA6$Z(!%@4D&xH zIvYmyWgRVJKK-TX09&PRJBJ!veEpVi6aNkEI|j?ZsB{4Bb}#f{1Z8epS8-be==C4K zi=QxYCeAxO9fpUUe+`E0gSm3yZ+qcts;(G2@V0fH4epiUo3DI3FH8sjsd=($U7|Le z7kX~=UNH6m>_emDt=~>M$3m<5?&ELY*Rh@#*w2D~+#%w~*12$isXxFd`!tGjB|C+! zgJU~z;A~|BV?lEPg8*lJ>4yZ%rWyc|+`+0p73-GI3fFo7n-LqMj&DM*jQ0u2dEy#3 zQjdiP7Q^0c$w|Voo-m%UBE^{CjrV4eLY92=2ZzI8;DaKtyZd$gDT$&5x%{;0u2t|} z()2FHcBSx|4ds3;Sa7(=p)=(Ka*;p$_d^_}IQ}5KMHnB$I9r<&U&~(&lYkLBoxM?M ze$XZi5a5q1e)@zK=l7V}yW(tBtqvtf?*AT*!O)db${0m-=Y%TAET%44u$bYg2smOF z>We8PmxHdb=58@>LT&JzO%@qWjROXvsib#EW`Du{5CK@CIG_7d$T&`V*nB!4a`Df8 z7z$W-@yC-{XQOx^WKGy$j@7G}euQ`09HVp_CHnV12IM@SZ9~2^0L=xnZv&*mFKCH8 zznb*Im;q(8v}W=1cGjCy$2N7b9UhlwcCfh=GoV;Dpi1{(@EJQaswj7wC_QF6;ZD$G zBCIf+q84D4@Nhtd)w(x3hXgyZWywC0wi0XHI~25X?wOY}Dh$+nIpHLR;cLNTc$cya z_5w}*z2?;4=>XR3tY-`4#Q)d6^3$LB8wrc>H6t6wE_z~jmWg5}m;cyxVKO-LtPjHN zP=3=VQ1hyPEJ*eVlT90grkURx*wGfaAiX7B{uO#Ahlx*MT##0-;gOEA0mB04oK9O5 zSOUo&4@GU_g}^;8&W(;``vGJtYn5NJt$gDJ#T%xMil+IArucEk+s3Zrd;g+M~Lrw;yu|$Q*<)PFv zr8*W5Ab=WYmZb7rDae;dtO@&tE5Zn6L1fB?m}w>o-UMWdMs+M*T+n~|Ljn8lUJCMY zaIUB-5p&O>x9G{Vpo#_f674ujSv^)3w~tGq%4f?@&~T)>GGj_Nzy<;wyHTj665rzS zBXDX_pkfZmS35A1iN^}iO(ETTWV{i)-m^8if>N4RjWT8~!g(%|Z<{hBHvrBn4GKD( z91AdZnL{->NiO_I(4`31OI3jlXrP*5XheRDea*Oz2w+S5 zMvWaQW;HYlBn<(Jv+x%?d+?2nUztN7cUWK0+lxlAOn|WeR~fu6Ni(K}#-(a_FTY$U zPihJ_3cAh{K{=3d#u|^xLhT{njnOni?9XPFRox_Tomo3$2m+g6XNhEwAoc4w{$O|84NqAP9=(!;FzrWylPF+9K)VU)oE zsTss~d$pLSu*Mh#fas6rEsJU4B+9LAqPT*YU5}?~lPWFrVHSEcYQj@659NUOi5>S| zn(%~)-E(VB_~-jeWFl?aH*ghH;hNxfQctNR3eFqn{MG5)0`-V9y|>NW5Z|-UM6&9g z+w5$dzR#QEwmt49gt&@DVHzc9Eb=f*cMST{F#1;T;jnn=d1?F)?ZdR^B{uNCMPgrX}eyz>5YFI4u zbNB&c;oNesJIOYc8ny8Jt)Zf7Wf$hC-GldL4<|bRg2-w_gXwZ=*i^ol2Dg3p#zsus zRvrgv5k-jx~xRSL^ zK?Tl*iwL_2A!O9r1&a>1B@cZy9yTDTUWMQ27HwTGF5O0ibjp ze%h3da&DUmt(MOB@mb$SY-NfcYY6;U|5qArza{*cdUufeUcuJOoWX+UYfum-r)*Ct z+w814FUsqyjeGT_xK0g-Wnf%1*c(j20_W7Q&-G}vWj}K#( z=NCA)WV$}^$`~N}V3(VN%u*rOH8z|NHj}W=&Tn*z2Yc_f6H2$P55)dGY+IGpJWHEy zOBD8p%$U2@zhRYD{4NC=c@nl#KGP(ay-aRvl$wOJ4g_esR)-4oEhS{E9;JA)4fhat z%U8Xvq54xDE4mf0d=xWrPF3XSV!U~Gk4rcQUk#Z|VS{FG0-R)&7<(3`)?OznY)nwd z>Ke?C;sbi81P0Uk2}3WsJ(6i^{L5JB@_Z)zTPU?_-`ixP~I#fpcgR7|3yE z+~LKVT3PbdcY`R^3(v>Tf5kTxMoZq=%1O(OtoWF%P?bXB&%$wAzyEIczoOj`M1Q0C zDU-fX4{oqafIbXeL|kv{O5^U9mQ<=I=btoV!B^TYiEx5X$MR+~oRGbll>%usVgi&b zg9Lez1bympHq{y31glg4G?` z=h{+GiwPS#5}#qJ>&jlyonFQBf2!Kte#W)6V&6uKydr1|JH)iV-B`SKQE$0Y00B5? zH$5szGPydX#vw%GoBEu9{=_Nwj015J<+0a_Bl9XG+aA6BE6XbJxy^e)m&?YAcoO;L z_um7%`k-rY1Ofm6zaRhr)&DZEyP6t1+d2N{v;*7P?uhNt%NMk21n5yLA!i0}6YC7P zJ7VcAbDeYRVwcDqqETdIb25>5a-uZs=WiAvWyJE(gzc6vJVy(*_Hj2yHYQxybCq2u zcdDzp;daRb@CKjh6$zia%Y9@v*;W}#x3>B5pSw3NN7tXD^sjnN-SzIjnWb6r4Ga;I zA@{{0gJ2i#z zHMIGMVG_PAQ~4Q&P2$GGK?}}F$qZPc62u@E@8QyroG_0Yg+|Qzq{$;5{Iy9je0U0~ z_0q-$(wwPz{+Aqc^2<~8q7>v3g%lnls$P=UVH|X;XUOdUj<-}E`Q8dfg-0a75g{ZT z*jtW?rs&#T48f==oke{AI+eO6+m(_7O${IkWro=#IhQWoLr`hy#7y{j^}R7>#Y}j} z*P1TmkD3-R8AF}i*=ZgPyW7)uX&?lqLz70tnRv;ECd40)+#Ssl7WnJTi{5c4WX(;y zU3@t41h3p3Jw2S*a=-pbHBU`C3DSap?K9 zj=j6n0dyVVOu~*mIkcnY?dZao9etR)a%aT}1~CzGqsh;h_nAAfWYO5}LA8=7F~33T zOne!;yN|C!*%?ANa7=k}^!&18{Mt3*qb^Q1Ah7o01Ue5srxFangt>cgV(aEJXG7J8 z@WpWKpq$Nc6r|n`jE1vDk^2dUmPXvU1b4FK$fGd3wHQ`6fD{zjA>Ivn@HI_1u;&yR z&zde$ME_H%bYcSF#DwImaPjd^&e9EBll5Mnd}hPa4PxaaqCRjFL;xD~GT_o=4?Nl^ z$V{;df9y5*X!l?XEZA>KvU@6fPgrh(prR39jIEB5c-^-L$a?Qj;yU;S=nbkPVYzi( zNV6>_uSFbX(Yc}(A0+HHZ;SYI0FcGaZ5YC;LiXGzC0Nh*dtQ-NC+bfx9b5nf3wm~N2-dYTGzx@lPJ5w4(C|BL| z=FaQ*zmmhn8f(4_NYR_tsj zYbwNv4_hxLd*sc^+1`GC$$xu6Mm0>nXqr_ws+#K?7)V}<8NxbgPTSsu(jXSaV z>QjlI>@!1*Wr(0Io()Dg(fnUpb(kpJ7$~+I0sFA}6H*GvOpKLfTSm-f`$;Yxe46JU zkk<_!ofymq^NKtw03{})9A9r>+vU(ca5D9t9L#{L1mlfF08xGn>j6$8ANlGvc2b?_ z3v_drDm4bhr6!BJ#qv;4r)UfZgRj61+ODo3fD*7Z5+H7PE#~G56KfXv8q0;B`!8F* zskk!LFTW_FUwvMwiEAL42S%Say_kA%pc6Ad2(NRnDebyJdc_$ zY-@q6jdde&y}WPNr>`G3eK+5^5W*f@f`T~ux$76UsEIjLKb!rtICuuAN^n=HUEnHY zzQ#mC6x61=0phEu6I664Y)~eW#_Cub5^q)aByH#^d$ z*)ymDpDoOkT|4^_QKJIOnie!7iqmHNNqO!}pg6tXZ3ezcT+;YZ~A=s~wMMiNc@%d2E$7>88Y}qlo4)po~I~W#dzkDM* z-5L>xr~dRAwjC|-jt+X=hf;SS&f)fUf@A7rtFZAys$!eRnTXWkXoBw=CJi?9_j*0B zk~w%({U*-IWwa?s%-aS{3=!bE`V!-1ne8%!mwDS4$L(4WJv@_tZV`hb&V(nh#?j6< zYOVc^pG(528g8dIsf*t@V0>P)U?0d*LvhO#K^Y>gg!IEg2D z_I21OQn7BQ*+UapwTOTFMQc zQ6-YTbH(SeFsy_tB9ScQfdVCOpVdi%jsj(S|xY z$ZbQ%oyX3zedn1OS`i=|LC(=$6hT%IM&eu~{V;deB$;$SvQbJMlJG%6Liy#8{-j!h zEK7(wZ3-%O_(PJ#ZJ1m}L;Kw8n;#9#{aawzeaNK}f*TV4a)|?6g4Ii8t>jpn_Vi2h zS(ReE>h2QHvYUn@@&Y*4PX%yJ^*qrW0V90|0>T;3=fPHHbvnj{y&$nxh{a5I`Ab&` z?P}aMNFz%q-Kfb#^eA}7Em0CpPGU%ftw+i8*$brz-dFRGJc(3&)l+%E6C(Mrm9Mv% zl25rK1~fvC*=tuTnI}VA(?Khmwz0O?nxn2vn#k-?3&qZBve&Mwl0G=Cmu4Lg)$3Ny zFXvS?SAB}A);i48YQOikGR|7jE?@~ZDA#}jD7Z%}#Q43>DR*`jah=a_B!LqPsf3|7 zIl6O(_*>PTG>X~_y|fZdb-ZV79SH+vg)@6MrDuF5uKidiN_eRAo^cuij<%B>M$%-> zcuX!QB(c{Jq<49|Z#eFS->rNu_Uh%X#808nvf3QM;1~kuQeem0Jw3}n(-gPEY1o6n z&dcTC8GHuv85=WCez3trJPY3HI66nuX~*wdtvsDEnkG<$_>uKp+swfx(KOx8qaXt5 z4`|pBZDfW&k0ECEmC)so^m0a(zT9y|$fpUvPGpKq##H%$<{ntwNio*0o^omCPiFkSNVFQ>g)i@+_ zwd}v&kfs_L7)7oZbV*$E2qOqX9Ld%s%RbK#wZy7tMM~oc3jD?#!R+4|=zK%Q%I7zH z+D?VTp{0+b!mY|JIL?rurK*jp;t2R(c&9W*4dFI$bqx&BMuh zY69f8DQMjhonFiZjt{~LD}Hl#J4*gH7ijvDZxYrygy~`#F8d&~=;>g<#hFFT>-L(c zMGHJ7&a~cK392&7lP;vOwU$u2lLx;wjvXLyuB4HvHT}{+(8LzKQ%jWri_*y8>$OU4 zv|cC*dZNNUisZf<)rTto(0!JFDV_sGzTI@@bvV?P@~TZQRSfidklaaWpN$nm>(*OM ziHnzmRC_a1H&&a%HYblesF5e7?@UGzIXl+_!lbD z%(Tl9$LK;saXJt|G74vKkGq0Qur#zdYKqwnV;jF%{fy23YHQopWgpR>9{=<2hnvKu zWAskz7zGA9)Z>cl`$R^NaJN%^gt5=K$;su(rx_4fWf>ZQ3dCaojav)VSf0v3tr|*% zqv_FD_2|eBBP^?ue#U0Z!0bIHB6f!m9sMjQ##(xn#Huav0?&~q{z2#jN^eqec_&53 z8SvPDAs)8_x!2pZn8X_eAvJ^Rt%Yt;XL9DUOg41F6Z7Z){Q4I6mJ!h((^bc;B4Si4 z6%EePxg#JZ!IEysfM+SN`TFU*{Zu1*`c3fwmZx7l8xp$kt`kiwp@q^VQnwg2;K z>|t^__Qh_M)_JO?8ue1R_Xpu80t&;+onyCHMuRPY3dW5YUxOH-*HIX0mgyyZHC?>> ziW@1BHjX=~KxtmHc3!J7%DtYs4iTNnH@j84p`AP4pKr9%HxBG(xNzH#2dWoU?=Nx@ zCV=5>ZXV=`X6STk`f_?1nfw@J>>pn5mxiaQoj?zFo^d*4eS2re@t-%QeDl{smTuIx zqtg=j&Eq)^Dxp$!EY~+9Hqs1Dgxy;pWY0M_T_i#F1lscCO7-&%=1)Dk`gaq{CXb0v z2itp07r^;hrW-!ex5v7++qW{5f2d^QN*rHPceYYu;-!aUcGfNF@-Yo&wZ|`yV`p>C ze=NTxuuXqqjM>M(?X*wubIT<|a+Xq0oyo~CgdiI1SBc5V8;n_4YpQXiYMX{qi-W?E7v$2*iga08VFtq)REGq0$M9lVX}S19KZNfU(?sNy5h{^lKS|2(RFcW%0* zs}_c>#J_}9hp)W)Iatydq2;gq^k_6N5+}s4h{pN@sza4sCt&dQ`)dbIC76ciA5WcjxnRY1*p?}entU%fWoJ`(QN;k1N@t*FN z1i-GTMn@F>`1X~X3e5Whr63ui;L!0_9+3+^R6@S8)>wNp)uzwmb{`f-+fB1VjTVk` zUG^8kZK`?l3T4)18NsAZEW~FxE*w|v>*tUqQ?Ab;u4Ua@&e-NoP&~g4+ly|1MZtB4 z>kR0Zv8sdi7xtBj5;LMx4gq=m^s?DxBBpS%CO=!E1}IMLVVeW87k|Uqi=a7vOMwQu zNJpSLFQSG+`O}5?n^`yqFe3=t7n?-*{94++$YaqNN95}be@MY+6GUHcxP?N#SsYh+_$I%>Z%S(B4vN7yYzls<>#6{|+xMXuEe<`)D6eQn=Tzs}(9> z%pqRYxEq?cJSIJUY0y-OeV;o2yh|3sE}a1B-a{W@3d0zRIKYevrtiCid0pJedrbc< zS2>FB%6h>FtqF1r<~Yi5qIMv-#*8oA?f4BhGrl9R03N-B0p(M^o5>zGnvGJSl3oav z0qnG^pWy0ZIq7tbMp{e|zR=Nbx-}W_NSLM_Gm6W3m@HuBC+`L)azU`^**gfETob z^;AVBC5*EmMO!p&(=Mo(^U;SGdK4O`52dy*P zM2Y$Uv)lY}y{*!Gl=O6x1@@18rT#6S6g2~L0yrEpL*9c0MX++{v{xhntwIIPM%n0l zn&t@BK+f4_BUmESN+Ys{uzmyzFY%Cwtrv0Z^+I{~a;FoIqSiSXBq9)Y~s#7Tq!9zL9Ry1Gwgf5*N? zv;Bi?7IWT-WxI74MOSCjDcCbkrjZa*9kvX)hQUwPvpx4TaP7Mtqy9JsHy~_s3!lQ@ zP|5>&yZ~|qUFJfOK@@t<%yWQt_PrUbOOt97+_dVMeeqt#qb@&3QrWM zIe-1efp}dQ)(l2cqvj-g8_MqB;0oj{9^3@`u7yw0T)ZQPvzFHj{UznF0wH$GX}mSF zIh>z(A&X=ueqQET@8aw{kBuSgjM;TkCTTRDzqgMv%?Np75ghH3MU$R8EqH*BQjZm& zQ?g??8D)%mGCJjy^Vf1EQRpnJ6AG7ZBBP|PBJF+)B3y!wXvaMbJQ0jCLDLybNw559 zmBBcNfc)^X zc{VSz14o_l>gxi~SHPka_vmYo!=FdR?!!@$#Hffz)4>GRv{B~Zmf^9^E#)Z~V_$I6 zKAT^Tp=Z}aBn(lnrHwnO%BhHUh3l-;$&P-wjB)RbrSqj|pS(Ghj?aFtv!C;6GC2`b z#`T9>X)@K-%4IZ>tyWdGm91O`d+x?Jsdwk8G)7^H{N8cR_^292jV5xP;e5Z%)w%)) zN(VV8_ON-el0R-A@jqg{5nd9@cL>P`^mGSt!;(b4KNWu;&b%0Q@&ducm7TL3I90NJ ztMNQF!GX}O-GfWG4XFQthJjy;^8zB!j-T+%V}^%n6ro$maBVb;c9l}mzmC<$+~0Am z*SrOBYHyj?wJ+u%Qq&@8F1uNGr%eX^bIeyA(}c$@%Zq$@|7c~gO0$YFrAiqnU;XQ@ zTWD=Yv#Uw(PiIg(ho<0sUbu(GQ5MVMcufD6piBe^;qO-^_JYX5kJMU*C@!nw>f?hW zaraVgtD2YJ%Z5`q4eOkpFiG;_Rk`Q8=yYHiv&DL5tzFW4mYsNn-f7O=O%>84G8P4} z!;fEXTK14%-|k-|^Wal!=o~etQ>?uM^XVA{0&!2x-)mxhbC^AD9b+`|?bZ7K{61*o z_QovY+jSx)kdFpIU;@j%y-Owd$MA>q{i4p-*Vl@ZibS-Uu$r_|i#eSz;lH1pcQ0q? z^WVOTl02~>b|%4prHE5Ou&a!YmyiqX{)`L<@L;aA$S=-X4!LqoWbb3;g8QM2&GSvG z_u%1k4ys0rh5J?LG>}`NfUO0>IG9b*=7{y`8#XKhGOCR^z{W&%%^HuD>WtPye+>Ff>&12-j4N|9ta7Ko`HpQ$v6fzpN*a$r;6|M)1*Za=)%+n-uuza_|DrJFLL6)lCDc0I)>Js+DgOz(hL z{PEpHQ3)e{x96emXrv{rc5r*7%tUgM`Pq?DE@-DDu-#gS+sE9tK7vk5S{aJ? zw1lM>g*B7xqf|Y*v`xi}&CeC0f&#B8S7r};TDk4JVbPry$p3vsyqM{&2#sA)zc3+@ zr9-ck+qh6I<08ohB_R>gO&TW$V-yGT>v<;=^T#JFn}EQ?vvsTzG%d-8^a^hC&oHys za=;hi>(XMfD#0Q(e)*V-92JX-i};I~%jQzyqCMY|_s`q5(<;OVXaUEBjroNyHZj&< z%8j2scb*!t^l4unECqF}#Zq~cK4@G?6TIM~@(WU-+fP6^OLtT#18+)>y>7E{a8iOv}F zez48`IlQZboz-Z~SpI#L8^O|4ecpGK$8R30r{lX>?5mcuu{%Ja{x(+f-z`Ec^pS_K z(3Yjl4wn2+>&D;x*p@?CcduqzKI^Y3vtNe2MrR^@-cQPQjXLACe6rRxFHb%=Ty}kV z#Ne#MJ^1ZeXXs^lEQ7RemeQzS7~@!E8RXt%77{q7JDb0PS(v`O7e}tEMs!}R$&~06 zuZalW$%=fhFu3!Rioxw$Caatmw|rD>d;-cKUOlu+-y6w4r6muCfPUmJ9tGP7DYGd5 zJYx~xDAZ@Q+tg;Gwf0Ko;Ss#Kaef2;cd6t_AllUNPbOhm0|Al$uTsgy#?02t)xrLs zMB3u}FQS*@KcZJ-jb@ugvQljMbY!Y1Z`@ULYUb3LDxG!^9R*rtLJDEW`j7t>e1IoX z%Hiem^iq|sS)?UfCvNPucyXk~&z*Cs1*1dJA-Tf!>V0EI`Pg)GuEnKYr|)FZh8FDtn4$T8qad*(+n~o`Y5y zgCgE`tQn7|gi{QQT*-Mtq@OL&38^guw1_Vk{l#b)*mrz@Pk~Yi3Turmw+4fcdH_Ts zM~rz!t2x8vbmvc+9}Yc^<@nzWG3ea`S@y;cs8?-tx&yC7G1S3%hytnZED-zvG2p>D zlB8lH5EP*}OAYSZz?QZ(l3a1t&zZ=MqjQS5doSDuSyIv8%{04Caq*OV0RR4}2{se% zc#}b7;HSaHvIIlN9CDa8AYBnnw&yQCz17cv3iE2ZKqKy_tTG}VoCnx^v{z9mi?m@U z@A#s;k})=s-g^SMU8LDi&NF<mgh zNiqEZ3Q;EH3ASQkJ^xUQf8Wa5(@@tQA*F^N#~<@_4a`17`H%7%CujIRT)XEH!`Hq2 zg#_TirLnhL)-KNx`DwxC9f*^1yXBvaBekD*940;8M!?8KvoK*E!t_p_9}j*`pM^ug z$=gsuF1*0M=AFBz@Goi8;BWSV;Az4S4F|#g;AGC=X27jWb`LJx3^1b)q@}{@(b$sX z?eoIg7<$*ItDBe879@&1@ZH<)=N5X*!q2K8p}V1Ozneecz3;Dr``6y#WlR|F8yKIi z9%-xtBZHs;{Pu6GCPYk&a=(A5e#FJxZtja{KtOA|1!Zu2g z9wFiT<3GNb-|E=L-tIo0*~9a`uP3YZzRop0mtgIpa9AW}v}* zK>#qQ|LF91g{d7^;N|_nt~1;-<6oXBs=oxCX%frmU;o4k+9tbYlyV+uL#0vt%U%0DfBId+&V5;svrk`7 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

4cQzRH)G4FpT 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

hm;FHlgl7y+AuF`jLs1Kqf*B#? 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í{ItI#7ohc#e3lf89#3IaO3K+<; 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 u