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.
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()
@@ -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()