instance-rig 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- instance_rig-1.2.0.dist-info/METADATA +162 -0
- instance_rig-1.2.0.dist-info/RECORD +26 -0
- instance_rig-1.2.0.dist-info/WHEEL +5 -0
- instance_rig-1.2.0.dist-info/entry_points.txt +2 -0
- instance_rig-1.2.0.dist-info/licenses/LICENSE +21 -0
- instance_rig-1.2.0.dist-info/top_level.txt +1 -0
- instancerig/BodyPixDetector.py +78 -0
- instancerig/BodyPixMapping.py +69 -0
- instancerig/InstanceRig.py +491 -0
- instancerig/__init__.py +0 -0
- instancerig/__main__.py +108 -0
- instancerig/_collada.py +60 -0
- instancerig/_gltf.py +169 -0
- instancerig/_io.py +12 -0
- instancerig/_math.py +163 -0
- instancerig/_pose_correction.py +154 -0
- instancerig/_preview.py +129 -0
- instancerig/_utils.py +58 -0
- instancerig/_vision.py +44 -0
- instancerig/app/AsyncInstanceRig.py +112 -0
- instancerig/app/__init__.py +0 -0
- instancerig/app/base_app.py +89 -0
- instancerig/model/Joint.py +121 -0
- instancerig/model/RemoteAsset.py +46 -0
- instancerig/model/Rig.py +11 -0
- instancerig/model/__init__.py +5 -0
instancerig/_gltf.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from typing import List, Dict, Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import open3d as o3d
|
|
5
|
+
import pygltflib
|
|
6
|
+
from volumesh import Volumesh
|
|
7
|
+
from volumesh.GLTFMeshSequence import GLTFMeshSequence
|
|
8
|
+
|
|
9
|
+
from instancerig import _math
|
|
10
|
+
from instancerig.model.Joint import Joint
|
|
11
|
+
from instancerig.model.Rig import Rig
|
|
12
|
+
|
|
13
|
+
MESH_NAME = "mesh"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _append_joints(gltf: pygltflib.GLTF2, joint: Joint, joint_nodes: Dict[Joint, pygltflib.Node]) -> int:
|
|
17
|
+
child_ids: List[int] = []
|
|
18
|
+
|
|
19
|
+
relative_position = joint.relative_position
|
|
20
|
+
relative_rotation = joint.relative_rotation
|
|
21
|
+
|
|
22
|
+
for child in joint.children:
|
|
23
|
+
child_id = _append_joints(gltf, child, joint_nodes)
|
|
24
|
+
child_ids.append(child_id)
|
|
25
|
+
|
|
26
|
+
# Convert Euler angles to quaternion for GLTF
|
|
27
|
+
rx, ry, rz = np.radians(relative_rotation)
|
|
28
|
+
rotation_quaternion = _math.get_quaternion_from_euler(rx, ry, rz)
|
|
29
|
+
|
|
30
|
+
node = joint_nodes[joint]
|
|
31
|
+
node.children = child_ids
|
|
32
|
+
node.translation = list(relative_position)
|
|
33
|
+
node.rotation = rotation_quaternion
|
|
34
|
+
node.scale = [1.0, 1.0, 1.0]
|
|
35
|
+
|
|
36
|
+
node_id = gltf.nodes.index(node)
|
|
37
|
+
return node_id
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def export(output_path: str, mesh: o3d.geometry.TriangleMesh, rig: Rig, as_glb: bool = False):
|
|
41
|
+
gltf = export_to_gtlf(mesh, rig)
|
|
42
|
+
|
|
43
|
+
if as_glb:
|
|
44
|
+
gltf.save_binary(output_path)
|
|
45
|
+
else:
|
|
46
|
+
gltf.save_json(output_path)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def export_to_gtlf(mesh: o3d.geometry.TriangleMesh, rig: Rig) -> pygltflib.GLTF2:
|
|
50
|
+
sequence = GLTFMeshSequence()
|
|
51
|
+
gltf = sequence.gltf
|
|
52
|
+
|
|
53
|
+
# append basic mesh infos
|
|
54
|
+
_append_mesh(sequence, mesh)
|
|
55
|
+
sequence_node = gltf.nodes[0]
|
|
56
|
+
mesh_node = gltf.nodes[1]
|
|
57
|
+
attributes = gltf.meshes[0].primitives[0].attributes
|
|
58
|
+
|
|
59
|
+
# make mesh root node
|
|
60
|
+
gltf.nodes.remove(sequence_node)
|
|
61
|
+
|
|
62
|
+
# add joint nodes to keep node-id the same as in skin weights (with delta)
|
|
63
|
+
joint_nodes: Dict[Joint, pygltflib.Node] = {}
|
|
64
|
+
joint_ids = []
|
|
65
|
+
for joint in list(rig.root_joint):
|
|
66
|
+
node = pygltflib.Node(name=joint.name)
|
|
67
|
+
|
|
68
|
+
node_id = len(gltf.nodes)
|
|
69
|
+
joint_ids.append(node_id)
|
|
70
|
+
gltf.nodes.append(node)
|
|
71
|
+
|
|
72
|
+
joint_nodes[joint] = node
|
|
73
|
+
|
|
74
|
+
# update joint nodes
|
|
75
|
+
root_joint_id = _append_joints(gltf, rig.root_joint, joint_nodes)
|
|
76
|
+
mesh_node.children = [root_joint_id]
|
|
77
|
+
|
|
78
|
+
# https://www.khronos.org/files/gltf20-reference-guide.pdf
|
|
79
|
+
# https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_019_SimpleSkin.md
|
|
80
|
+
# https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_020_Skins.md
|
|
81
|
+
|
|
82
|
+
# add bind matrices
|
|
83
|
+
gltf_bind_matrices = np.stack([j.inverse_bind_matrix.flatten(order="F") for j in list(rig.root_joint)])
|
|
84
|
+
|
|
85
|
+
gltf_bind_matrices = np.float32(gltf_bind_matrices)
|
|
86
|
+
inverse_bind_matrices_id = len(gltf.accessors)
|
|
87
|
+
_add_vector_data(sequence, gltf_bind_matrices, component_type=pygltflib.FLOAT, type=pygltflib.MAT4, target=None)
|
|
88
|
+
|
|
89
|
+
# add skin
|
|
90
|
+
skin = pygltflib.Skin(name="InstanceRigSkin", joints=joint_ids, inverseBindMatrices=inverse_bind_matrices_id)
|
|
91
|
+
skin_id = len(gltf.skins)
|
|
92
|
+
gltf.skins.append(skin)
|
|
93
|
+
mesh_node.skin = skin_id
|
|
94
|
+
|
|
95
|
+
# add joint_ids and weights
|
|
96
|
+
gltf_joint_ids = np.uint8(rig.joint_ids)
|
|
97
|
+
attributes.JOINTS_0 = len(gltf.accessors)
|
|
98
|
+
sequence._add_vector_data(gltf_joint_ids, component_type=pygltflib.UNSIGNED_BYTE, type=pygltflib.VEC4)
|
|
99
|
+
|
|
100
|
+
gltf_joint_weights = np.float32(rig.weights)
|
|
101
|
+
attributes.WEIGHTS_0 = len(gltf.accessors)
|
|
102
|
+
sequence._add_vector_data(gltf_joint_weights, component_type=pygltflib.FLOAT, type=pygltflib.VEC4)
|
|
103
|
+
|
|
104
|
+
return sequence.pack()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _append_mesh(sequence: GLTFMeshSequence, mesh: o3d.geometry.TriangleMesh):
|
|
108
|
+
# directly copied from volumesh -> todo: create own method in volumesh
|
|
109
|
+
name = MESH_NAME
|
|
110
|
+
|
|
111
|
+
# default arguments
|
|
112
|
+
points = np.float32(np.asarray(mesh.vertices))
|
|
113
|
+
triangles = np.uint32(np.asarray(mesh.triangles))
|
|
114
|
+
|
|
115
|
+
# optional arguments
|
|
116
|
+
colors = np.float32(np.asarray(mesh.vertex_colors))
|
|
117
|
+
|
|
118
|
+
# necessary to be GLTF compliant
|
|
119
|
+
mesh = mesh.normalize_normals()
|
|
120
|
+
normals = np.float32(np.asarray(mesh.vertex_normals))
|
|
121
|
+
|
|
122
|
+
# convert triangle_uvs into vertex_uvs
|
|
123
|
+
triangle_uvs = np.float32(np.asarray(mesh.triangle_uvs))
|
|
124
|
+
vertex_uvs = Volumesh._calculate_vertex_uvs(triangles, triangle_uvs) if len(triangle_uvs) > 0 else None
|
|
125
|
+
|
|
126
|
+
textures = [np.asarray(tex) for tex in mesh.textures if not tex.is_empty()]
|
|
127
|
+
texture = textures[0] if len(textures) > 0 else None
|
|
128
|
+
|
|
129
|
+
sequence.append_mesh(
|
|
130
|
+
points,
|
|
131
|
+
triangles,
|
|
132
|
+
colors,
|
|
133
|
+
normals,
|
|
134
|
+
vertex_uvs,
|
|
135
|
+
texture,
|
|
136
|
+
name=name,
|
|
137
|
+
compressed=False,
|
|
138
|
+
jpeg_textures=False,
|
|
139
|
+
jpeg_quality=100,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _add_vector_data(
|
|
144
|
+
sequence: GLTFMeshSequence,
|
|
145
|
+
array: np.ndarray,
|
|
146
|
+
component_type: int = pygltflib.FLOAT,
|
|
147
|
+
type: int = pygltflib.VEC3,
|
|
148
|
+
target: Optional[int] = pygltflib.ARRAY_BUFFER,
|
|
149
|
+
):
|
|
150
|
+
array_blob = array.tobytes()
|
|
151
|
+
sequence.gltf.accessors.append(
|
|
152
|
+
pygltflib.Accessor(
|
|
153
|
+
bufferView=len(sequence.gltf.bufferViews),
|
|
154
|
+
componentType=component_type,
|
|
155
|
+
count=len(array),
|
|
156
|
+
type=str(type),
|
|
157
|
+
max=array.max(axis=0).tolist(),
|
|
158
|
+
min=array.min(axis=0).tolist(),
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
sequence.gltf.bufferViews.append(
|
|
162
|
+
pygltflib.BufferView(
|
|
163
|
+
buffer=0,
|
|
164
|
+
byteOffset=len(sequence.data),
|
|
165
|
+
byteLength=len(array_blob),
|
|
166
|
+
target=target,
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
sequence.data += array_blob
|
instancerig/_io.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import pickle
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def save_object(file_path: str, obj: Any):
|
|
6
|
+
with open(file_path, "wb") as file:
|
|
7
|
+
pickle.dump(obj, file)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_object(file_path: str) -> Any:
|
|
11
|
+
with open(file_path, "rb") as file:
|
|
12
|
+
return pickle.load(file)
|
instancerig/_math.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from instancerig.model import Vector
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def interpolate(a: Vector, b: Vector, t: float) -> Vector:
|
|
8
|
+
return np.asarray([(t * b[0]) + ((1 - t) * a[0]), (t * b[1]) + ((1 - t) * a[1]), (t * b[2]) + ((1 - t) * a[2])])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def distance(a: Vector, b: Vector) -> Vector:
|
|
12
|
+
return np.linalg.norm(np.asarray(a) - np.asarray(b), axis=0)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def dot_product_angle(v1: Vector, v2: Vector) -> float:
|
|
16
|
+
if np.linalg.norm(v1) == 0 or np.linalg.norm(v2) == 0:
|
|
17
|
+
print("Zero magnitude vector!")
|
|
18
|
+
else:
|
|
19
|
+
vector_dot_product = np.dot(v1, v2)
|
|
20
|
+
arccos = np.arccos(vector_dot_product / (np.linalg.norm(v1) * np.linalg.norm(v2)))
|
|
21
|
+
angle = np.degrees(arccos)
|
|
22
|
+
return float(angle)
|
|
23
|
+
return 0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def angle_between_points(a: Vector, b: Vector, axis: Vector, axis_id: int) -> float:
|
|
27
|
+
v = b - a
|
|
28
|
+
angle = dot_product_angle(axis, v)
|
|
29
|
+
return angle
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def normalize(data: np.ndarray) -> np.ndarray:
|
|
33
|
+
low, high = min(data), max(data)
|
|
34
|
+
if high - low == 0:
|
|
35
|
+
return np.zeros_like(data)
|
|
36
|
+
a = -low
|
|
37
|
+
b = 1 / (a + high)
|
|
38
|
+
return (data + a) * b
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_quaternion_from_euler(roll, pitch, yaw):
|
|
42
|
+
"""
|
|
43
|
+
Convert an Euler angle to a quaternion.
|
|
44
|
+
|
|
45
|
+
Input
|
|
46
|
+
:param roll: The roll (rotation around x-axis) angle in radians.
|
|
47
|
+
:param pitch: The pitch (rotation around y-axis) angle in radians.
|
|
48
|
+
:param yaw: The yaw (rotation around z-axis) angle in radians.
|
|
49
|
+
|
|
50
|
+
Output
|
|
51
|
+
:return qx, qy, qz, qw: The orientation in quaternion [x,y,z,w] format
|
|
52
|
+
"""
|
|
53
|
+
qx = np.sin(roll / 2) * np.cos(pitch / 2) * np.cos(yaw / 2) - np.cos(roll / 2) * np.sin(pitch / 2) * np.sin(yaw / 2)
|
|
54
|
+
qy = np.cos(roll / 2) * np.sin(pitch / 2) * np.cos(yaw / 2) + np.sin(roll / 2) * np.cos(pitch / 2) * np.sin(yaw / 2)
|
|
55
|
+
qz = np.cos(roll / 2) * np.cos(pitch / 2) * np.sin(yaw / 2) - np.sin(roll / 2) * np.sin(pitch / 2) * np.cos(yaw / 2)
|
|
56
|
+
qw = np.cos(roll / 2) * np.cos(pitch / 2) * np.cos(yaw / 2) + np.sin(roll / 2) * np.sin(pitch / 2) * np.sin(yaw / 2)
|
|
57
|
+
|
|
58
|
+
return [qx, qy, qz, qw]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_rotation_matrix_from_euler(roll, pitch, yaw):
|
|
62
|
+
"""
|
|
63
|
+
Convert Euler angles to a rotation matrix.
|
|
64
|
+
Assumes XYZ order.
|
|
65
|
+
"""
|
|
66
|
+
R_x = np.array([[1, 0, 0], [0, math.cos(roll), -math.sin(roll)], [0, math.sin(roll), math.cos(roll)]])
|
|
67
|
+
|
|
68
|
+
R_y = np.array([[math.cos(pitch), 0, math.sin(pitch)], [0, 1, 0], [-math.sin(pitch), 0, math.cos(pitch)]])
|
|
69
|
+
|
|
70
|
+
R_z = np.array([[math.cos(yaw), -math.sin(yaw), 0], [math.sin(yaw), math.cos(yaw), 0], [0, 0, 1]])
|
|
71
|
+
|
|
72
|
+
R = np.dot(R_z, np.dot(R_y, R_x))
|
|
73
|
+
return R
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_euler_from_matrix(R):
|
|
77
|
+
"""
|
|
78
|
+
Convert a rotation matrix to Euler angles (roll, pitch, yaw) in radians.
|
|
79
|
+
Assumes XYZ order.
|
|
80
|
+
"""
|
|
81
|
+
sy = math.sqrt(R[0, 0] * R[0, 0] + R[1, 0] * R[1, 0])
|
|
82
|
+
singular = sy < 1e-6
|
|
83
|
+
|
|
84
|
+
if not singular:
|
|
85
|
+
x = math.atan2(R[2, 1], R[2, 2])
|
|
86
|
+
y = math.atan2(-R[2, 0], sy)
|
|
87
|
+
z = math.atan2(R[1, 0], R[0, 0])
|
|
88
|
+
else:
|
|
89
|
+
x = math.atan2(-R[1, 2], R[1, 1])
|
|
90
|
+
y = math.atan2(-R[2, 0], sy)
|
|
91
|
+
z = 0
|
|
92
|
+
|
|
93
|
+
return np.array([x, y, z])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_quaternion_from_matrix(R):
|
|
97
|
+
"""
|
|
98
|
+
Convert a rotation matrix to a quaternion [x, y, z, w].
|
|
99
|
+
"""
|
|
100
|
+
trace = np.trace(R)
|
|
101
|
+
if trace > 0:
|
|
102
|
+
S = np.sqrt(trace + 1.0) * 2
|
|
103
|
+
qw = 0.25 * S
|
|
104
|
+
qx = (R[2, 1] - R[1, 2]) / S
|
|
105
|
+
qy = (R[0, 2] - R[2, 0]) / S
|
|
106
|
+
qz = (R[1, 0] - R[0, 1]) / S
|
|
107
|
+
elif (R[0, 0] > R[1, 1]) and (R[0, 0] > R[2, 2]):
|
|
108
|
+
S = np.sqrt(1.0 + R[0, 0] - R[1, 1] - R[2, 2]) * 2
|
|
109
|
+
qw = (R[2, 1] - R[1, 2]) / S
|
|
110
|
+
qx = 0.25 * S
|
|
111
|
+
qy = (R[0, 1] + R[1, 0]) / S
|
|
112
|
+
qz = (R[0, 2] + R[2, 0]) / S
|
|
113
|
+
elif R[1, 1] > R[2, 2]:
|
|
114
|
+
S = np.sqrt(1.0 + R[1, 1] - R[0, 0] - R[2, 2]) * 2
|
|
115
|
+
qw = (R[0, 2] - R[2, 0]) / S
|
|
116
|
+
qx = (R[0, 1] + R[1, 0]) / S
|
|
117
|
+
qy = 0.25 * S
|
|
118
|
+
qz = (R[1, 2] + R[2, 1]) / S
|
|
119
|
+
else:
|
|
120
|
+
S = np.sqrt(1.0 + R[2, 2] - R[0, 0] - R[1, 1]) * 2
|
|
121
|
+
qw = (R[1, 0] - R[0, 1]) / S
|
|
122
|
+
qx = (R[0, 2] + R[2, 0]) / S
|
|
123
|
+
qy = (R[1, 2] + R[2, 1]) / S
|
|
124
|
+
qz = 0.25 * S
|
|
125
|
+
|
|
126
|
+
return [qx, qy, qz, qw]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_alignment_matrix(target_vector, source_vector=np.array([0, 1, 0])):
|
|
130
|
+
"""
|
|
131
|
+
Calculate rotation matrix to align source_vector to target_vector.
|
|
132
|
+
"""
|
|
133
|
+
target_vector = target_vector / (np.linalg.norm(target_vector) + 1e-6)
|
|
134
|
+
source_vector = source_vector / (np.linalg.norm(source_vector) + 1e-6)
|
|
135
|
+
|
|
136
|
+
v = np.cross(source_vector, target_vector)
|
|
137
|
+
c = np.dot(source_vector, target_vector)
|
|
138
|
+
s = np.linalg.norm(v)
|
|
139
|
+
|
|
140
|
+
if s < 1e-6:
|
|
141
|
+
# Vectors are parallel
|
|
142
|
+
if c > 0:
|
|
143
|
+
return np.eye(3)
|
|
144
|
+
else:
|
|
145
|
+
# Vectors are opposite
|
|
146
|
+
# Find an arbitrary perpendicular vector to rotate 180 degrees
|
|
147
|
+
if np.abs(source_vector[0]) < 0.1:
|
|
148
|
+
perp = np.array([1, 0, 0])
|
|
149
|
+
else:
|
|
150
|
+
perp = np.array([0, 1, 0])
|
|
151
|
+
axis = np.cross(source_vector, perp)
|
|
152
|
+
axis = axis / np.linalg.norm(axis)
|
|
153
|
+
# Rotation of 180 degrees around axis
|
|
154
|
+
# Rodrigues formula for 180 deg: I + 2 * K^2
|
|
155
|
+
# K is skew symmetric matrix of axis
|
|
156
|
+
# Or just construct rotation matrix manually
|
|
157
|
+
# R = 2 * outer(axis, axis) - I
|
|
158
|
+
return 2 * np.outer(axis, axis) - np.eye(3)
|
|
159
|
+
|
|
160
|
+
kmat = np.array([[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]])
|
|
161
|
+
|
|
162
|
+
R = np.eye(3) + kmat + np.dot(kmat, kmat) * ((1 - c) / (s**2))
|
|
163
|
+
return R
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from typing import Dict, Tuple
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import open3d as o3d
|
|
5
|
+
|
|
6
|
+
from instancerig import _math
|
|
7
|
+
from instancerig.model.Joint import Joint
|
|
8
|
+
from instancerig.model.Rig import Rig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TPoseConverter:
|
|
12
|
+
def __init__(self, rig: Rig, mesh: o3d.geometry.TriangleMesh):
|
|
13
|
+
self.rig = rig
|
|
14
|
+
self.mesh = mesh
|
|
15
|
+
self.joints = {j.name: j for j in list(rig.root_joint)}
|
|
16
|
+
|
|
17
|
+
# Define the coordinate system.
|
|
18
|
+
# Z as Forward, Y as Up, X as Right.
|
|
19
|
+
self.forward = np.array([0, 0, 1])
|
|
20
|
+
self.up = np.array([0, 1, 0])
|
|
21
|
+
self.right = np.array([1, 0, 0])
|
|
22
|
+
|
|
23
|
+
def convert_to_t_pose(self) -> Tuple[o3d.geometry.TriangleMesh, Rig]:
|
|
24
|
+
parameters = self._calculate_correction_parameters()
|
|
25
|
+
|
|
26
|
+
# Define rotations (in degrees) for joints to reach T-pose.
|
|
27
|
+
# Rotation around Z-axis.
|
|
28
|
+
rotations = {
|
|
29
|
+
"rightShoulder": np.array([0, 0, parameters["ShoulderAngleShiftRight"]]),
|
|
30
|
+
"leftShoulder": np.array([0, 0, -parameters["ShoulderAngleShiftLeft"]]),
|
|
31
|
+
"rightHip": np.array([0, 0, parameters["HipAngleShiftRight"]]),
|
|
32
|
+
"leftHip": np.array([0, 0, -parameters["HipAngleShiftLeft"]]),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
self._apply_t_pose(rotations)
|
|
36
|
+
|
|
37
|
+
return self.mesh, self.rig
|
|
38
|
+
|
|
39
|
+
def _calculate_correction_parameters(self) -> Dict[str, float]:
|
|
40
|
+
def get_pos(name):
|
|
41
|
+
if name not in self.joints:
|
|
42
|
+
return np.array([0.0, 0.0, 0.0])
|
|
43
|
+
return self.joints[name].world_position
|
|
44
|
+
|
|
45
|
+
def project_2d(v):
|
|
46
|
+
# Project vector onto the plane perpendicular to forward (the XY plane)
|
|
47
|
+
return v - np.dot(v, self.forward) * self.forward
|
|
48
|
+
|
|
49
|
+
def angle_between(a_name, b_name, c_name):
|
|
50
|
+
a, b, c = get_pos(a_name), get_pos(b_name), get_pos(c_name)
|
|
51
|
+
|
|
52
|
+
# Project points to 2D (XY plane) to ignore flexion/extension (forward/backward bending)
|
|
53
|
+
# This ensures we only correct the abduction/adduction (side split)
|
|
54
|
+
a = project_2d(a)
|
|
55
|
+
b = project_2d(b)
|
|
56
|
+
c = project_2d(c)
|
|
57
|
+
|
|
58
|
+
v1 = a - b
|
|
59
|
+
v2 = c - b
|
|
60
|
+
return _math.dot_product_angle(v1, v2)
|
|
61
|
+
|
|
62
|
+
def angle_to_up(start, end):
|
|
63
|
+
a = project_2d(end) - project_2d(start)
|
|
64
|
+
up_proj = project_2d(self.up)
|
|
65
|
+
return _math.dot_product_angle(up_proj, a)
|
|
66
|
+
|
|
67
|
+
# Calculate angles relative to the desired T-pose
|
|
68
|
+
right_shoulder_angle = angle_between("rightHip", "rightShoulder", "rightElbow") - 90
|
|
69
|
+
left_shoulder_angle = angle_between("leftHip", "leftShoulder", "leftElbow") - 90
|
|
70
|
+
|
|
71
|
+
# Hip angle calculation:
|
|
72
|
+
# 180 - angle gives the positive correction needed to close the legs (Adduction).
|
|
73
|
+
# We use projected angles to avoid over-correction due to forward knee bend.
|
|
74
|
+
right_hip_angle = angle_between("rightShoulder", "rightHip", "rightKnee") - 180
|
|
75
|
+
left_hip_angle = angle_between("leftShoulder", "leftHip", "leftKnee") - 180
|
|
76
|
+
|
|
77
|
+
# Calculate torso tilt correction
|
|
78
|
+
right_side_angle = 180.0 - angle_to_up(get_pos("rightShoulder"), get_pos("rightHip"))
|
|
79
|
+
left_side_angle = 180.0 - angle_to_up(get_pos("leftShoulder"), get_pos("leftHip"))
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
"ShoulderAngleShiftRight": right_shoulder_angle + right_side_angle,
|
|
83
|
+
"ShoulderAngleShiftLeft": left_shoulder_angle + left_side_angle,
|
|
84
|
+
"HipAngleShiftRight": right_hip_angle + right_side_angle,
|
|
85
|
+
"HipAngleShiftLeft": left_hip_angle + left_side_angle,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
def _apply_t_pose(self, joint_rotations: Dict[str, np.ndarray]):
|
|
89
|
+
node_transforms = {} # joint_name -> 4x4 matrix
|
|
90
|
+
|
|
91
|
+
def traverse(joint: Joint, parent_m: np.ndarray):
|
|
92
|
+
# Correction rotation for this joint
|
|
93
|
+
rel_euler = joint_rotations.get(joint.name, np.array([0.0, 0.0, 0.0]))
|
|
94
|
+
rx, ry, rz = np.radians(rel_euler)
|
|
95
|
+
R = _math.get_rotation_matrix_from_euler(rx, ry, rz)
|
|
96
|
+
|
|
97
|
+
# Transformation around joint position: T * R * T^-1
|
|
98
|
+
pos = joint.world_position
|
|
99
|
+
T = np.eye(4)
|
|
100
|
+
T[:3, 3] = pos
|
|
101
|
+
T_inv = np.eye(4)
|
|
102
|
+
T_inv[:3, 3] = -pos
|
|
103
|
+
|
|
104
|
+
R_4x4 = np.eye(4)
|
|
105
|
+
R_4x4[:3, :3] = R
|
|
106
|
+
|
|
107
|
+
local_m = T @ R_4x4 @ T_inv
|
|
108
|
+
|
|
109
|
+
# Cumulative transform
|
|
110
|
+
total_m = parent_m @ local_m
|
|
111
|
+
node_transforms[joint.name] = total_m
|
|
112
|
+
|
|
113
|
+
for child in joint.children:
|
|
114
|
+
traverse(child, total_m)
|
|
115
|
+
|
|
116
|
+
traverse(self.rig.root_joint, np.eye(4))
|
|
117
|
+
|
|
118
|
+
# Apply Linear Blend Skinning (LBS) to mesh vertices
|
|
119
|
+
vertices = np.asarray(self.mesh.vertices)
|
|
120
|
+
new_vertices = np.zeros_like(vertices)
|
|
121
|
+
|
|
122
|
+
joint_list = list(self.rig.root_joint)
|
|
123
|
+
joint_names = [j.name for j in joint_list]
|
|
124
|
+
matrices = np.array([node_transforms[name] for name in joint_names])
|
|
125
|
+
|
|
126
|
+
for i in range(len(vertices)):
|
|
127
|
+
v = np.append(vertices[i], 1.0)
|
|
128
|
+
v_new = np.zeros(4)
|
|
129
|
+
for j in range(4): # 4 joints per vertex
|
|
130
|
+
joint_idx = self.rig.joint_ids[i, j]
|
|
131
|
+
weight = self.rig.weights[i, j]
|
|
132
|
+
if weight > 0:
|
|
133
|
+
v_new += weight * (matrices[joint_idx] @ v)
|
|
134
|
+
new_vertices[i] = v_new[:3]
|
|
135
|
+
|
|
136
|
+
self.mesh.vertices = o3d.utility.Vector3dVector(new_vertices)
|
|
137
|
+
|
|
138
|
+
# Update joint positions and rotations in the rig
|
|
139
|
+
for joint in joint_list:
|
|
140
|
+
m = node_transforms[joint.name]
|
|
141
|
+
|
|
142
|
+
# Update position
|
|
143
|
+
pos_4 = np.append(joint.world_position, 1.0)
|
|
144
|
+
joint.world_position = (m @ pos_4)[:3]
|
|
145
|
+
|
|
146
|
+
# Update rotation
|
|
147
|
+
R_applied = m[:3, :3]
|
|
148
|
+
rx, ry, rz = np.radians(joint.world_rotation)
|
|
149
|
+
R_old = _math.get_rotation_matrix_from_euler(rx, ry, rz)
|
|
150
|
+
R_new = R_applied @ R_old
|
|
151
|
+
joint.world_rotation = np.degrees(_math.get_euler_from_matrix(R_new))
|
|
152
|
+
|
|
153
|
+
# Recompute normals
|
|
154
|
+
self.mesh.compute_vertex_normals()
|
instancerig/_preview.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from typing import List, Iterable, Tuple
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import open3d as o3d
|
|
5
|
+
from muke.model.KeyPoint3 import KeyPoint3
|
|
6
|
+
|
|
7
|
+
from instancerig._math import get_alignment_matrix
|
|
8
|
+
from instancerig.model import Vector
|
|
9
|
+
from instancerig.model.Joint import Joint
|
|
10
|
+
|
|
11
|
+
COLOR_SEQUENCE = [
|
|
12
|
+
(230, 25, 75),
|
|
13
|
+
(60, 180, 75),
|
|
14
|
+
(255, 225, 25),
|
|
15
|
+
(0, 130, 200),
|
|
16
|
+
(245, 130, 48),
|
|
17
|
+
(145, 30, 180),
|
|
18
|
+
(70, 240, 240),
|
|
19
|
+
(240, 50, 230),
|
|
20
|
+
(210, 245, 60),
|
|
21
|
+
(250, 190, 212),
|
|
22
|
+
(0, 128, 128),
|
|
23
|
+
(220, 190, 255),
|
|
24
|
+
(170, 110, 40),
|
|
25
|
+
(255, 250, 200),
|
|
26
|
+
(128, 0, 0),
|
|
27
|
+
(170, 255, 195),
|
|
28
|
+
(128, 128, 0),
|
|
29
|
+
(255, 215, 180),
|
|
30
|
+
(255, 255, 255),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
NICE_COLOR_SEQUENCE = [
|
|
34
|
+
[51, 87, 255], # Blue
|
|
35
|
+
[87, 255, 51], # Green
|
|
36
|
+
[255, 102, 51], # Red-Orange
|
|
37
|
+
[255, 51, 255], # Pink
|
|
38
|
+
[255, 255, 51], # Yellow
|
|
39
|
+
[255, 51, 102], # Coral
|
|
40
|
+
[51, 153, 255], # Sky Blue
|
|
41
|
+
[153, 51, 255], # Purple
|
|
42
|
+
[51, 255, 255], # Cyan
|
|
43
|
+
[255, 102, 51], # Orange
|
|
44
|
+
[51, 255, 153], # Mint Green
|
|
45
|
+
[204, 51, 255], # Lavender
|
|
46
|
+
[102, 255, 51], # Lime Green
|
|
47
|
+
[255, 51, 204], # Magenta
|
|
48
|
+
[51, 204, 255], # Light Blue
|
|
49
|
+
[255, 204, 51], # Gold
|
|
50
|
+
[51, 255, 204], # Turquoise
|
|
51
|
+
[255, 153, 51], # Tangerine
|
|
52
|
+
[51, 255, 51], # Bright Green
|
|
53
|
+
[255, 51, 255], # Orchid
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
PREVIEW_WIDTH = 720
|
|
57
|
+
PREVIEW_HEIGHT = 1280
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def draw_cone(
|
|
61
|
+
bottom_center: Iterable[float],
|
|
62
|
+
top_position: Iterable[float],
|
|
63
|
+
color: Tuple[float, float, float] = (0.6, 0.6, 0.9),
|
|
64
|
+
radius: float = 0.007,
|
|
65
|
+
tip_offset: float = 5e-3,
|
|
66
|
+
) -> o3d.geometry.TriangleMesh:
|
|
67
|
+
bottom: np.ndarray = np.asarray(bottom_center, dtype=float)
|
|
68
|
+
top: np.ndarray = np.asarray(top_position, dtype=float)
|
|
69
|
+
|
|
70
|
+
direction: np.ndarray = top - bottom
|
|
71
|
+
length: float = float(np.linalg.norm(direction))
|
|
72
|
+
|
|
73
|
+
cone: o3d.geometry.TriangleMesh = o3d.geometry.TriangleMesh.create_cone(radius=radius, height=length + 1e-6)
|
|
74
|
+
|
|
75
|
+
R: np.ndarray = get_alignment_matrix(target_vector=direction, source_vector=np.array([0.0, 0.0, 1.0], dtype=float))
|
|
76
|
+
cone.rotate(R, center=(0.0, 0.0, 0.0))
|
|
77
|
+
|
|
78
|
+
if length > 1e-12:
|
|
79
|
+
cone.translate(bottom + tip_offset * (direction / length))
|
|
80
|
+
else:
|
|
81
|
+
cone.translate(bottom)
|
|
82
|
+
|
|
83
|
+
cone.paint_uniform_color(np.asarray(color, dtype=float))
|
|
84
|
+
return cone
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def preview_3d_keypoints(mesh: o3d.geometry.TriangleMesh, keypoints: List[KeyPoint3], title: str = "3D KeyPoints"):
|
|
88
|
+
meshes = []
|
|
89
|
+
|
|
90
|
+
wireframe = o3d.geometry.LineSet.create_from_triangle_mesh(mesh)
|
|
91
|
+
meshes.append(wireframe)
|
|
92
|
+
|
|
93
|
+
for kp in keypoints:
|
|
94
|
+
marker: o3d.geometry.TriangleMesh = o3d.geometry.TriangleMesh.create_sphere(0.025)
|
|
95
|
+
marker.paint_uniform_color([1, 0.706, 0])
|
|
96
|
+
marker.translate(kp.predicted_position)
|
|
97
|
+
meshes.append(marker)
|
|
98
|
+
|
|
99
|
+
o3d.visualization.draw_geometries(meshes, window_name=title, width=PREVIEW_WIDTH, height=PREVIEW_HEIGHT)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def render_joints(
|
|
103
|
+
mesh: o3d.geometry.TriangleMesh, root_joint: Joint, markers: List[Vector] = None, title: str = "Joints"
|
|
104
|
+
):
|
|
105
|
+
meshes = []
|
|
106
|
+
if markers is None:
|
|
107
|
+
markers = []
|
|
108
|
+
|
|
109
|
+
wireframe = o3d.geometry.LineSet.create_from_triangle_mesh(mesh)
|
|
110
|
+
meshes.append(wireframe)
|
|
111
|
+
|
|
112
|
+
for joint in root_joint:
|
|
113
|
+
joint_marker: o3d.geometry.TriangleMesh = o3d.geometry.TriangleMesh.create_coordinate_frame(size=0.1)
|
|
114
|
+
joint_marker.translate(joint.world_position)
|
|
115
|
+
R = joint_marker.get_rotation_matrix_from_xyz(np.radians(joint.world_rotation))
|
|
116
|
+
joint_marker.rotate(R, center=joint.world_position)
|
|
117
|
+
meshes.append(joint_marker)
|
|
118
|
+
|
|
119
|
+
# render bones
|
|
120
|
+
for child in joint.children:
|
|
121
|
+
meshes.append(draw_cone(joint.world_position, child.world_position, color=[1.0, 0.2, 0.2], radius=0.02))
|
|
122
|
+
|
|
123
|
+
for marker_position in markers:
|
|
124
|
+
marker: o3d.geometry.TriangleMesh = o3d.geometry.TriangleMesh.create_sphere(radius=0.025)
|
|
125
|
+
marker.paint_uniform_color([1, 0.2, 0])
|
|
126
|
+
marker.translate(marker_position)
|
|
127
|
+
meshes.append(marker)
|
|
128
|
+
|
|
129
|
+
o3d.visualization.draw_geometries(meshes, window_name=title, width=PREVIEW_WIDTH, height=PREVIEW_HEIGHT)
|
instancerig/_utils.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def current_millis() -> int:
|
|
8
|
+
return time.time_ns() // 1_000_000
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_cache_dir() -> Path:
|
|
12
|
+
if cache_dir_env := os.getenv("INSTANCERIG_CACHE_DIR"):
|
|
13
|
+
path = Path(cache_dir_env)
|
|
14
|
+
else:
|
|
15
|
+
if platform.system() == "Windows":
|
|
16
|
+
path = Path.home() / "AppData" / "Local" / "instancerig"
|
|
17
|
+
else:
|
|
18
|
+
path = Path.home() / ".cache" / "instancerig"
|
|
19
|
+
|
|
20
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
return path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Watch:
|
|
25
|
+
def __init__(self, name: str = "Watch"):
|
|
26
|
+
self.name = name
|
|
27
|
+
self.start_time: int = 0
|
|
28
|
+
self.end_time: int = 0
|
|
29
|
+
self.running = False
|
|
30
|
+
|
|
31
|
+
def start(self):
|
|
32
|
+
self.start_time = current_millis()
|
|
33
|
+
self.running = True
|
|
34
|
+
|
|
35
|
+
def stop(self):
|
|
36
|
+
self.end_time = current_millis()
|
|
37
|
+
self.running = False
|
|
38
|
+
|
|
39
|
+
def elapsed(self) -> int:
|
|
40
|
+
if self.running:
|
|
41
|
+
return current_millis() - self.start_time
|
|
42
|
+
return self.end_time - self.start_time
|
|
43
|
+
|
|
44
|
+
def print(self):
|
|
45
|
+
print(f"{self.name}: {self.elapsed_str()}")
|
|
46
|
+
|
|
47
|
+
def elapsed_str(self, correction: int = 0) -> str:
|
|
48
|
+
delta = self.elapsed() - correction
|
|
49
|
+
time_info = time.strftime("%Hh %Mm %Ss {}ms".format(delta % 1000), time.gmtime(delta / 1000.0))
|
|
50
|
+
return time_info
|
|
51
|
+
|
|
52
|
+
def __enter__(self):
|
|
53
|
+
self.start()
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
57
|
+
self.stop()
|
|
58
|
+
self.print()
|