Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files- utils/eval_utils.py +57 -0
- utils/mesh_to_pc.py +84 -0
- utils/save_utils.py +648 -0
- utils/skeleton_data_loader.py +97 -0
utils/eval_utils.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modified from https://github.com/zhan-xu/RigNet
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
##### for quantitative calculation
|
| 6 |
+
def chamfer_dist(pt1, pt2):
|
| 7 |
+
pt1 = pt1[np.newaxis, :, :]
|
| 8 |
+
pt2 = pt2[:, np.newaxis, :]
|
| 9 |
+
dist = np.sqrt(np.sum((pt1 - pt2) ** 2, axis=2))
|
| 10 |
+
min_left = np.mean(np.min(dist, axis=0))
|
| 11 |
+
min_right = np.mean(np.min(dist, axis=1))
|
| 12 |
+
return (min_left + min_right) / 2
|
| 13 |
+
|
| 14 |
+
def oneway_chamfer(pt_src, pt_dst):
|
| 15 |
+
pt1 = pt_src[np.newaxis, :, :]
|
| 16 |
+
pt2 = pt_dst[:, np.newaxis, :]
|
| 17 |
+
dist = np.sqrt(np.sum((pt1 - pt2) ** 2, axis=2))
|
| 18 |
+
avg_dist = np.mean(np.min(dist, axis=0))
|
| 19 |
+
return avg_dist
|
| 20 |
+
|
| 21 |
+
def joint2bone_chamfer_dist(joints1, bones1, joints2, bones2):
|
| 22 |
+
bone_sample_1 = sample_skel(joints1, bones1)
|
| 23 |
+
bone_sample_2 = sample_skel(joints2, bones2)
|
| 24 |
+
dist1 = oneway_chamfer(joints1, bone_sample_2)
|
| 25 |
+
dist2 = oneway_chamfer(joints2, bone_sample_1)
|
| 26 |
+
return (dist1 + dist2) / 2
|
| 27 |
+
|
| 28 |
+
def bone2bone_chamfer_dist(joints1, bones1, joints2, bones2):
|
| 29 |
+
bone_sample_1 = sample_skel(joints1, bones1)
|
| 30 |
+
bone_sample_2 = sample_skel(joints2, bones2)
|
| 31 |
+
return chamfer_dist(bone_sample_1, bone_sample_2)
|
| 32 |
+
|
| 33 |
+
def sample_bone(p_pos, ch_pos):
|
| 34 |
+
ray = ch_pos - p_pos
|
| 35 |
+
|
| 36 |
+
bone_length = np.linalg.norm(p_pos - ch_pos)
|
| 37 |
+
num_step = np.round(bone_length / 0.005).astype(int)
|
| 38 |
+
i_step = np.arange(0, num_step + 1)
|
| 39 |
+
unit_step = ray / (num_step + 1e-30)
|
| 40 |
+
unit_step = np.repeat(unit_step[np.newaxis, :], num_step + 1, axis=0)
|
| 41 |
+
res = p_pos + unit_step * i_step[:, np.newaxis]
|
| 42 |
+
return res
|
| 43 |
+
|
| 44 |
+
def sample_skel(joints, bones):
|
| 45 |
+
bone_sample = []
|
| 46 |
+
for parent_idx, child_idx in bones:
|
| 47 |
+
p_pos = joints[parent_idx]
|
| 48 |
+
ch_pos = joints[child_idx]
|
| 49 |
+
res = sample_bone(p_pos, ch_pos)
|
| 50 |
+
bone_sample.append(res)
|
| 51 |
+
|
| 52 |
+
if bone_sample:
|
| 53 |
+
bone_sample = np.concatenate(bone_sample, axis=0)
|
| 54 |
+
else:
|
| 55 |
+
bone_sample = np.empty((0, 3))
|
| 56 |
+
|
| 57 |
+
return bone_sample
|
utils/mesh_to_pc.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modified from https://github.com/buaacyw/MeshAnything
|
| 2 |
+
import mesh2sdf.core
|
| 3 |
+
import numpy as np
|
| 4 |
+
import skimage.measure
|
| 5 |
+
import trimesh
|
| 6 |
+
import time
|
| 7 |
+
from typing import List, Tuple
|
| 8 |
+
|
| 9 |
+
class MeshProcessor:
|
| 10 |
+
"""A class to handle mesh normalization, watertight conversion and point cloud sampling."""
|
| 11 |
+
|
| 12 |
+
@staticmethod
|
| 13 |
+
def normalize_mesh_vertices(vertices: np.ndarray, scaling_factor: float = 0.95) -> Tuple[np.ndarray, np.ndarray, float]:
|
| 14 |
+
"""
|
| 15 |
+
Normalize mesh vertices to be centered at origin and scaled appropriately.
|
| 16 |
+
"""
|
| 17 |
+
min_bounds = vertices.min(axis=0)
|
| 18 |
+
max_bounds = vertices.max(axis=0)
|
| 19 |
+
|
| 20 |
+
center = (min_bounds + max_bounds) * 0.5
|
| 21 |
+
max_dimension = (max_bounds - min_bounds).max()
|
| 22 |
+
scale = 2.0 * scaling_factor / max_dimension
|
| 23 |
+
|
| 24 |
+
normalized_vertices = (vertices - center) * scale
|
| 25 |
+
return normalized_vertices, center, scale
|
| 26 |
+
|
| 27 |
+
@staticmethod
|
| 28 |
+
def convert_to_watertight(mesh: trimesh.Trimesh, octree_depth: int = 7) -> trimesh.Trimesh:
|
| 29 |
+
"""
|
| 30 |
+
Convert to watertight using mesh2sdf and marching cubes.
|
| 31 |
+
"""
|
| 32 |
+
grid_size = 2 ** octree_depth
|
| 33 |
+
iso_level = 2 / grid_size
|
| 34 |
+
|
| 35 |
+
# Normalize vertices for SDF computation
|
| 36 |
+
normalized_vertices, original_center, original_scale = MeshProcessor.normalize_mesh_vertices(mesh.vertices)
|
| 37 |
+
|
| 38 |
+
# Compute signed distance field
|
| 39 |
+
sdf = mesh2sdf.core.compute(normalized_vertices, mesh.faces, size=grid_size)
|
| 40 |
+
|
| 41 |
+
# Run marching cubes algorithm
|
| 42 |
+
vertices, faces, normals, _ = skimage.measure.marching_cubes(np.abs(sdf), iso_level)
|
| 43 |
+
|
| 44 |
+
# Transform vertices back to original coordinate system
|
| 45 |
+
vertices = vertices / grid_size * 2 - 1 # Map to [-1, 1] range
|
| 46 |
+
vertices = vertices / original_scale + original_center
|
| 47 |
+
|
| 48 |
+
# Create new watertight mesh
|
| 49 |
+
watertight_mesh = trimesh.Trimesh(vertices, faces, normals=normals)
|
| 50 |
+
return watertight_mesh
|
| 51 |
+
|
| 52 |
+
@staticmethod
|
| 53 |
+
def convert_meshes_to_point_clouds(
|
| 54 |
+
meshes: List[trimesh.Trimesh],
|
| 55 |
+
points_per_mesh: int = 8192,
|
| 56 |
+
apply_marching_cubes: bool = False,
|
| 57 |
+
octree_depth: int = 7
|
| 58 |
+
) -> List[np.ndarray]:
|
| 59 |
+
"""
|
| 60 |
+
Process a list of meshes into point clouds with normals.
|
| 61 |
+
"""
|
| 62 |
+
point_clouds_with_normals = []
|
| 63 |
+
processed_meshes = []
|
| 64 |
+
|
| 65 |
+
for mesh in meshes:
|
| 66 |
+
# Optionally convert to watertight mesh
|
| 67 |
+
if apply_marching_cubes:
|
| 68 |
+
start_time = time.time()
|
| 69 |
+
mesh = MeshProcessor.convert_to_watertight(mesh, octree_depth=octree_depth)
|
| 70 |
+
processing_time = time.time() - start_time
|
| 71 |
+
print(f"Marching cubes complete! Time: {processing_time:.2f}s")
|
| 72 |
+
|
| 73 |
+
# Store processed mesh
|
| 74 |
+
processed_meshes.append(mesh)
|
| 75 |
+
|
| 76 |
+
# Sample points and get corresponding face normals
|
| 77 |
+
points, face_indices = mesh.sample(points_per_mesh, return_index=True)
|
| 78 |
+
point_normals = mesh.face_normals[face_indices]
|
| 79 |
+
|
| 80 |
+
# Combine points and normals
|
| 81 |
+
points_with_normals = np.concatenate([points, point_normals], axis=-1, dtype=np.float16)
|
| 82 |
+
point_clouds_with_normals.append(points_with_normals)
|
| 83 |
+
|
| 84 |
+
return point_clouds_with_normals
|
utils/save_utils.py
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
import os
|
| 15 |
+
import numpy as np
|
| 16 |
+
import cv2
|
| 17 |
+
import json
|
| 18 |
+
import trimesh
|
| 19 |
+
|
| 20 |
+
from collections import deque, defaultdict
|
| 21 |
+
from scipy.cluster.hierarchy import linkage, fcluster
|
| 22 |
+
from scipy.spatial.distance import cdist
|
| 23 |
+
|
| 24 |
+
from data_utils.pyrender_wrapper import PyRenderWrapper
|
| 25 |
+
from data_utils.data_loader import DataLoader
|
| 26 |
+
|
| 27 |
+
def save_mesh(vertices, faces, filename):
|
| 28 |
+
|
| 29 |
+
mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
|
| 30 |
+
mesh.export(filename, file_type='obj')
|
| 31 |
+
|
| 32 |
+
def pred_joints_and_bones(bone_coor):
|
| 33 |
+
"""
|
| 34 |
+
get joints (j,3) and bones (b,2) from (b,2,3), preserve the parent-child relationship
|
| 35 |
+
"""
|
| 36 |
+
parent_coords = bone_coor[:, 0, :] # (b, 3)
|
| 37 |
+
child_coords = bone_coor[:, 1, :] # (b, 3)
|
| 38 |
+
|
| 39 |
+
all_coords = np.vstack([parent_coords, child_coords]) # (2b, 3)
|
| 40 |
+
pred_joints, indices = np.unique(all_coords, axis=0, return_inverse=True)
|
| 41 |
+
|
| 42 |
+
b = bone_coor.shape[0]
|
| 43 |
+
parent_indices = indices[:b]
|
| 44 |
+
child_indices = indices[b:]
|
| 45 |
+
|
| 46 |
+
pred_bones = np.column_stack([parent_indices, child_indices])
|
| 47 |
+
|
| 48 |
+
valid_bones = pred_bones[parent_indices != child_indices]
|
| 49 |
+
|
| 50 |
+
return pred_joints, valid_bones
|
| 51 |
+
|
| 52 |
+
def find_connected_components(joints, bones):
|
| 53 |
+
"""Find connected components in the skeleton graph."""
|
| 54 |
+
n_joints = len(joints)
|
| 55 |
+
graph = defaultdict(list)
|
| 56 |
+
|
| 57 |
+
# Build adjacency list
|
| 58 |
+
for parent, child in bones:
|
| 59 |
+
graph[parent].append(child)
|
| 60 |
+
graph[child].append(parent)
|
| 61 |
+
|
| 62 |
+
visited = [False] * n_joints
|
| 63 |
+
components = []
|
| 64 |
+
|
| 65 |
+
for i in range(n_joints):
|
| 66 |
+
if not visited[i]:
|
| 67 |
+
component = []
|
| 68 |
+
queue = deque([i])
|
| 69 |
+
visited[i] = True
|
| 70 |
+
|
| 71 |
+
while queue:
|
| 72 |
+
node = queue.popleft()
|
| 73 |
+
component.append(node)
|
| 74 |
+
|
| 75 |
+
for neighbor in graph[node]:
|
| 76 |
+
if not visited[neighbor]:
|
| 77 |
+
visited[neighbor] = True
|
| 78 |
+
queue.append(neighbor)
|
| 79 |
+
|
| 80 |
+
components.append(component)
|
| 81 |
+
|
| 82 |
+
return components
|
| 83 |
+
|
| 84 |
+
def ensure_skeleton_connectivity(joints, bones, root_index=None, merge_distance_threshold=0.01):
|
| 85 |
+
"""
|
| 86 |
+
Ensure skeleton is fully connected.
|
| 87 |
+
- If distance < merge_distance_threshold: merge joints
|
| 88 |
+
- If distance >= merge_distance_threshold: connect with bone
|
| 89 |
+
"""
|
| 90 |
+
current_joints = joints.copy()
|
| 91 |
+
current_bones = list(bones)
|
| 92 |
+
current_root = root_index
|
| 93 |
+
|
| 94 |
+
iteration = 0
|
| 95 |
+
while True:
|
| 96 |
+
components = find_connected_components(current_joints, current_bones)
|
| 97 |
+
if len(components) == 1:
|
| 98 |
+
# print("Successfully ensured skeleton connectivity")
|
| 99 |
+
break
|
| 100 |
+
|
| 101 |
+
# Find the globally closest pair of components
|
| 102 |
+
min_distance = float('inf')
|
| 103 |
+
best_pair = None
|
| 104 |
+
|
| 105 |
+
for i in range(len(components)):
|
| 106 |
+
for j in range(i + 1, len(components)):
|
| 107 |
+
comp1_joints = current_joints[components[i]]
|
| 108 |
+
comp2_joints = current_joints[components[j]]
|
| 109 |
+
|
| 110 |
+
distances = cdist(comp1_joints, comp2_joints)
|
| 111 |
+
min_idx = np.unravel_index(np.argmin(distances), distances.shape)
|
| 112 |
+
distance = distances[min_idx]
|
| 113 |
+
|
| 114 |
+
if distance < min_distance:
|
| 115 |
+
min_distance = distance
|
| 116 |
+
best_pair = (i, j, components[i][min_idx[0]], components[j][min_idx[1]], min_idx)
|
| 117 |
+
|
| 118 |
+
if best_pair is None:
|
| 119 |
+
print("Warning: Could not find valid component pair to connect")
|
| 120 |
+
break
|
| 121 |
+
|
| 122 |
+
comp1_idx, comp2_idx, joint1_idx, joint2_idx, min_idx = best_pair
|
| 123 |
+
|
| 124 |
+
if min_distance < merge_distance_threshold:
|
| 125 |
+
# Merge the joints
|
| 126 |
+
# print(f"Iteration {iteration + 1}: Merging closest joints {joint1_idx} and {joint2_idx} "
|
| 127 |
+
# f"(distance: {min_distance:.4f})")
|
| 128 |
+
|
| 129 |
+
# Always merge joint2 into joint1
|
| 130 |
+
merge_map = {joint2_idx: joint1_idx}
|
| 131 |
+
|
| 132 |
+
# Update bones
|
| 133 |
+
updated_bones = []
|
| 134 |
+
for parent, child in current_bones:
|
| 135 |
+
new_parent = merge_map.get(parent, parent)
|
| 136 |
+
new_child = merge_map.get(child, child)
|
| 137 |
+
if new_parent != new_child: # Remove self-loops
|
| 138 |
+
updated_bones.append([new_parent, new_child])
|
| 139 |
+
|
| 140 |
+
# Update root
|
| 141 |
+
if current_root == joint2_idx:
|
| 142 |
+
current_root = joint1_idx
|
| 143 |
+
|
| 144 |
+
# Remove the merged joint and update indices
|
| 145 |
+
joint_to_remove = joint2_idx
|
| 146 |
+
mask = np.ones(len(current_joints), dtype=bool)
|
| 147 |
+
mask[joint_to_remove] = False
|
| 148 |
+
current_joints = current_joints[mask]
|
| 149 |
+
|
| 150 |
+
# Create index mapping for remaining joints
|
| 151 |
+
old_to_new = {}
|
| 152 |
+
new_idx = 0
|
| 153 |
+
for old_idx in range(len(mask)):
|
| 154 |
+
if mask[old_idx]:
|
| 155 |
+
old_to_new[old_idx] = new_idx
|
| 156 |
+
new_idx += 1
|
| 157 |
+
|
| 158 |
+
# Update bone indices
|
| 159 |
+
current_bones = [[old_to_new[parent], old_to_new[child]]
|
| 160 |
+
for parent, child in updated_bones
|
| 161 |
+
if parent in old_to_new and child in old_to_new]
|
| 162 |
+
|
| 163 |
+
# Update root index
|
| 164 |
+
if current_root is not None and current_root in old_to_new:
|
| 165 |
+
current_root = old_to_new[current_root]
|
| 166 |
+
|
| 167 |
+
else:
|
| 168 |
+
# Connect with bone
|
| 169 |
+
# print(f"Iteration {iteration + 1}: Connecting closest components with bone {joint1_idx} -> {joint2_idx} "
|
| 170 |
+
# f"(distance: {min_distance:.4f})")
|
| 171 |
+
current_bones.append([joint1_idx, joint2_idx])
|
| 172 |
+
|
| 173 |
+
iteration += 1
|
| 174 |
+
|
| 175 |
+
# prevent infinite loops
|
| 176 |
+
if iteration > len(joints):
|
| 177 |
+
print(f"Warning: Maximum iterations reached ({iteration}), stopping")
|
| 178 |
+
break
|
| 179 |
+
|
| 180 |
+
current_bones = np.array(current_bones) if len(current_bones) > 0 else np.array([]).reshape(0, 2)
|
| 181 |
+
|
| 182 |
+
# Final connectivity verification
|
| 183 |
+
final_components = find_connected_components(current_joints, current_bones)
|
| 184 |
+
if len(final_components) == 1:
|
| 185 |
+
pass
|
| 186 |
+
else:
|
| 187 |
+
print(f"Warning: Still have {len(final_components)} disconnected components after {iteration} iterations")
|
| 188 |
+
|
| 189 |
+
return current_joints, current_bones, current_root
|
| 190 |
+
|
| 191 |
+
def merge_duplicate_joints_and_fix_bones(joints, bones, tolerance=0.0025, root_index=None):
|
| 192 |
+
"""
|
| 193 |
+
merge duplicate joints that are within a certain tolerance distance, and fix bones to maintain connectivity.
|
| 194 |
+
Also merge bones that become duplicates after joint merging.
|
| 195 |
+
"""
|
| 196 |
+
n_joints = len(joints)
|
| 197 |
+
|
| 198 |
+
# find merge joint groups
|
| 199 |
+
merge_groups = []
|
| 200 |
+
used = [False] * n_joints
|
| 201 |
+
|
| 202 |
+
for i in range(n_joints):
|
| 203 |
+
if used[i]:
|
| 204 |
+
continue
|
| 205 |
+
|
| 206 |
+
# find all joints within tolerance distance to joint i
|
| 207 |
+
group = [i]
|
| 208 |
+
for j in range(i + 1, n_joints):
|
| 209 |
+
if not used[j]:
|
| 210 |
+
dist = np.linalg.norm(joints[i] - joints[j])
|
| 211 |
+
if dist < tolerance:
|
| 212 |
+
group.append(j)
|
| 213 |
+
used[j] = True
|
| 214 |
+
|
| 215 |
+
used[i] = True
|
| 216 |
+
merge_groups.append(group)
|
| 217 |
+
|
| 218 |
+
# if len(group) > 1:
|
| 219 |
+
# print(f"find duplicate joints group: {group}")
|
| 220 |
+
|
| 221 |
+
# build merge map: choose representative joint
|
| 222 |
+
merge_map = {}
|
| 223 |
+
for group in merge_groups:
|
| 224 |
+
if root_index is not None and root_index in group:
|
| 225 |
+
representative = root_index
|
| 226 |
+
else:
|
| 227 |
+
representative = group[0] # else choose the first one as representative
|
| 228 |
+
for joint_idx in group:
|
| 229 |
+
merge_map[joint_idx] = representative
|
| 230 |
+
|
| 231 |
+
# track root joint change
|
| 232 |
+
intermediate_root_index = None
|
| 233 |
+
if root_index is not None:
|
| 234 |
+
intermediate_root_index = merge_map.get(root_index, root_index)
|
| 235 |
+
# if intermediate_root_index != root_index:
|
| 236 |
+
# print(f"root joint index changed from {root_index} to {intermediate_root_index}")
|
| 237 |
+
|
| 238 |
+
# update bones: remove self-loop bones, and merge duplicate bones
|
| 239 |
+
updated_bones = []
|
| 240 |
+
|
| 241 |
+
for parent, child in bones:
|
| 242 |
+
new_parent = merge_map.get(parent, parent)
|
| 243 |
+
new_child = merge_map.get(child, child)
|
| 244 |
+
|
| 245 |
+
if new_parent != new_child: # remove self-loop bones
|
| 246 |
+
updated_bones.append([new_parent, new_child])
|
| 247 |
+
|
| 248 |
+
# remove duplicate bones
|
| 249 |
+
unique_bones = []
|
| 250 |
+
seen_bones = set()
|
| 251 |
+
|
| 252 |
+
for bone in updated_bones:
|
| 253 |
+
bone_key = tuple(bone) # keep the order of [parent, child]
|
| 254 |
+
if bone_key not in seen_bones:
|
| 255 |
+
seen_bones.add(bone_key)
|
| 256 |
+
unique_bones.append(bone)
|
| 257 |
+
|
| 258 |
+
# re-index joints to remove unused joints
|
| 259 |
+
used_joint_indices = set()
|
| 260 |
+
for parent, child in unique_bones:
|
| 261 |
+
used_joint_indices.add(parent)
|
| 262 |
+
used_joint_indices.add(child)
|
| 263 |
+
if intermediate_root_index is not None:
|
| 264 |
+
used_joint_indices.add(intermediate_root_index)
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
used_joint_indices = sorted(list(used_joint_indices))
|
| 268 |
+
|
| 269 |
+
# new index for used joints
|
| 270 |
+
old_to_new = {old_idx: new_idx for new_idx, old_idx in enumerate(used_joint_indices)}
|
| 271 |
+
|
| 272 |
+
final_joints = joints[used_joint_indices]
|
| 273 |
+
final_bones = np.array([[old_to_new[parent], old_to_new[child]]
|
| 274 |
+
for parent, child in unique_bones])
|
| 275 |
+
|
| 276 |
+
final_root_index = None
|
| 277 |
+
if intermediate_root_index is not None:
|
| 278 |
+
final_root_index = old_to_new[intermediate_root_index]
|
| 279 |
+
if root_index is not None and final_root_index != root_index:
|
| 280 |
+
print(f"final root index: {root_index} -> {final_root_index}")
|
| 281 |
+
|
| 282 |
+
removed_joints = n_joints - len(final_joints)
|
| 283 |
+
removed_bones = len(bones) - len(final_bones)
|
| 284 |
+
|
| 285 |
+
# print
|
| 286 |
+
# if removed_joints > 0 or removed_bones > 0:
|
| 287 |
+
# print(f"merge results:")
|
| 288 |
+
# print(f" joint number: {n_joints} -> {len(final_joints)} (remove {removed_joints})")
|
| 289 |
+
# print(f" bone number: {len(bones)} -> {len(final_bones)} (remove {removed_bones})")
|
| 290 |
+
|
| 291 |
+
# Ensure skeleton connectivity with relaxed threshold
|
| 292 |
+
final_joints, final_bones, final_root_index = ensure_skeleton_connectivity(
|
| 293 |
+
final_joints, final_bones, final_root_index,
|
| 294 |
+
merge_distance_threshold=tolerance*8 # More relaxed threshold for connectivity
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
if root_index is not None:
|
| 298 |
+
return final_joints, final_bones, final_root_index
|
| 299 |
+
else:
|
| 300 |
+
return final_joints, final_bones
|
| 301 |
+
|
| 302 |
+
def save_skeleton_to_txt(pred_joints, pred_bones, pred_root_index, hier_order, vertices, filename='skeleton.txt'):
|
| 303 |
+
"""
|
| 304 |
+
save skeleton to txt file, the format follows Rignet (joints, root, hier)
|
| 305 |
+
|
| 306 |
+
if hier_order: the first joint index in bone is root joint index, and parent-child relationship is established in bones.
|
| 307 |
+
else: we set the joint nearest to the mesh center as the root joint, and then build hierarchy starting from root.
|
| 308 |
+
"""
|
| 309 |
+
|
| 310 |
+
num_joints = pred_joints.shape[0]
|
| 311 |
+
|
| 312 |
+
# assign joint names
|
| 313 |
+
joint_names = [f'joint{i}' for i in range(num_joints)]
|
| 314 |
+
|
| 315 |
+
adjacency = defaultdict(list)
|
| 316 |
+
for bone in pred_bones:
|
| 317 |
+
idx_a, idx_b = bone
|
| 318 |
+
adjacency[idx_a].append(idx_b)
|
| 319 |
+
adjacency[idx_b].append(idx_a)
|
| 320 |
+
|
| 321 |
+
# find root joint
|
| 322 |
+
if hier_order:
|
| 323 |
+
root_idx = pred_root_index
|
| 324 |
+
else:
|
| 325 |
+
centroid = np.mean(vertices, axis=0)
|
| 326 |
+
distances = np.linalg.norm(pred_joints - centroid, axis=1)
|
| 327 |
+
root_idx = np.argmin(distances)
|
| 328 |
+
|
| 329 |
+
root_name = joint_names[root_idx]
|
| 330 |
+
|
| 331 |
+
# build hierarchy
|
| 332 |
+
parent_map = {}
|
| 333 |
+
|
| 334 |
+
if hier_order:
|
| 335 |
+
visited = set()
|
| 336 |
+
|
| 337 |
+
for parent_idx, child_idx in pred_bones:
|
| 338 |
+
if child_idx not in parent_map:
|
| 339 |
+
parent_map[child_idx] = parent_idx
|
| 340 |
+
visited.add(child_idx)
|
| 341 |
+
visited.add(parent_idx)
|
| 342 |
+
|
| 343 |
+
parent_map[root_idx] = None
|
| 344 |
+
|
| 345 |
+
else:
|
| 346 |
+
visited = set([root_idx])
|
| 347 |
+
queue = deque([root_idx])
|
| 348 |
+
parent_map[root_idx] = None
|
| 349 |
+
|
| 350 |
+
while queue:
|
| 351 |
+
current_idx = queue.popleft()
|
| 352 |
+
for neighbor_idx in adjacency[current_idx]:
|
| 353 |
+
if neighbor_idx not in visited:
|
| 354 |
+
parent_map[neighbor_idx] = current_idx
|
| 355 |
+
visited.add(neighbor_idx)
|
| 356 |
+
queue.append(neighbor_idx)
|
| 357 |
+
|
| 358 |
+
if len(visited) != num_joints:
|
| 359 |
+
print(f"bones are not fully connected, leaving {num_joints - len(visited)} joints unconnected.")
|
| 360 |
+
|
| 361 |
+
# save joints
|
| 362 |
+
joints_lines = []
|
| 363 |
+
for idx, coord in enumerate(pred_joints):
|
| 364 |
+
name = joint_names[idx]
|
| 365 |
+
joints_line = f'joints {name} {coord[0]:.8f} {coord[1]:.8f} {coord[2]:.8f}'
|
| 366 |
+
joints_lines.append(joints_line)
|
| 367 |
+
|
| 368 |
+
# save root name
|
| 369 |
+
root_line = f'root {root_name}'
|
| 370 |
+
|
| 371 |
+
# save hierarchy
|
| 372 |
+
hier_lines = []
|
| 373 |
+
for child_idx, parent_idx in parent_map.items():
|
| 374 |
+
if parent_idx is not None:
|
| 375 |
+
parent_name = joint_names[parent_idx]
|
| 376 |
+
child_name = joint_names[child_idx]
|
| 377 |
+
hier_line = f'hier {parent_name} {child_name}'
|
| 378 |
+
hier_lines.append(hier_line)
|
| 379 |
+
|
| 380 |
+
with open(filename, 'w') as file:
|
| 381 |
+
for line in joints_lines:
|
| 382 |
+
file.write(line + '\n')
|
| 383 |
+
|
| 384 |
+
file.write(root_line + '\n')
|
| 385 |
+
|
| 386 |
+
for line in hier_lines:
|
| 387 |
+
file.write(line + '\n')
|
| 388 |
+
|
| 389 |
+
def save_skeleton_to_txt_joint(pred_joints, pred_bones, filename='skeleton.txt'):
|
| 390 |
+
"""
|
| 391 |
+
save skeleton to txt file, the format follows Rignet (joints, root, hier)
|
| 392 |
+
"""
|
| 393 |
+
|
| 394 |
+
num_joints = pred_joints.shape[0]
|
| 395 |
+
|
| 396 |
+
# assign joint names
|
| 397 |
+
joint_names = [f'joint{i}' for i in range(num_joints)]
|
| 398 |
+
|
| 399 |
+
# find potential root joints
|
| 400 |
+
all_parents = set([bone[0] for bone in pred_bones])
|
| 401 |
+
all_children = set([bone[1] for bone in pred_bones])
|
| 402 |
+
potential_roots = all_parents - all_children
|
| 403 |
+
|
| 404 |
+
# determine root joint
|
| 405 |
+
if not potential_roots:
|
| 406 |
+
print("Warning: No joint is only a parent, choosing the first joint as root.")
|
| 407 |
+
root_idx = pred_bones[0, 0]
|
| 408 |
+
else:
|
| 409 |
+
if len(potential_roots) > 1:
|
| 410 |
+
print(f"Warning: Multiple potential root joints found ({len(potential_roots)}), choosing the first one.")
|
| 411 |
+
root_idx = list(potential_roots)[0]
|
| 412 |
+
|
| 413 |
+
root_name = joint_names[root_idx]
|
| 414 |
+
|
| 415 |
+
# build hierarchy
|
| 416 |
+
parent_map = {}
|
| 417 |
+
visited = set()
|
| 418 |
+
|
| 419 |
+
for parent_idx, child_idx in pred_bones:
|
| 420 |
+
if child_idx not in parent_map:
|
| 421 |
+
parent_map[child_idx] = parent_idx
|
| 422 |
+
visited.add(child_idx)
|
| 423 |
+
visited.add(parent_idx)
|
| 424 |
+
|
| 425 |
+
parent_map[root_idx] = None
|
| 426 |
+
|
| 427 |
+
if len(visited) != num_joints:
|
| 428 |
+
print(f"Warning: bones are not fully connected, leaving {num_joints - len(visited)} joints unconnected.")
|
| 429 |
+
|
| 430 |
+
# save joints
|
| 431 |
+
joints_lines = []
|
| 432 |
+
for idx, coord in enumerate(pred_joints):
|
| 433 |
+
name = joint_names[idx]
|
| 434 |
+
joints_line = f'joints {name} {coord[0]:.8f} {coord[1]:.8f} {coord[2]:.8f}'
|
| 435 |
+
joints_lines.append(joints_line)
|
| 436 |
+
|
| 437 |
+
# save root name
|
| 438 |
+
root_line = f'root {root_name}'
|
| 439 |
+
|
| 440 |
+
# save hierarchy
|
| 441 |
+
hier_lines = []
|
| 442 |
+
for child_idx, parent_idx in parent_map.items():
|
| 443 |
+
if parent_idx is not None:
|
| 444 |
+
parent_name = joint_names[parent_idx]
|
| 445 |
+
child_name = joint_names[child_idx]
|
| 446 |
+
hier_line = f'hier {parent_name} {child_name}'
|
| 447 |
+
hier_lines.append(hier_line)
|
| 448 |
+
|
| 449 |
+
with open(filename, 'w') as file:
|
| 450 |
+
for line in joints_lines:
|
| 451 |
+
file.write(line + '\n')
|
| 452 |
+
|
| 453 |
+
file.write(root_line + '\n')
|
| 454 |
+
|
| 455 |
+
for line in hier_lines:
|
| 456 |
+
file.write(line + '\n')
|
| 457 |
+
return root_idx
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
def save_skeleton_obj(joints, bones, save_path, root_index=None, radius_sphere=0.01,
|
| 461 |
+
radius_bone=0.005, segments=16, stacks=16, use_cone=False):
|
| 462 |
+
"""
|
| 463 |
+
Save skeletons to obj file, each connection contains two red spheres (joint) and one blue cylinder (bone).
|
| 464 |
+
if root index is known, set root sphere to green.
|
| 465 |
+
"""
|
| 466 |
+
|
| 467 |
+
all_vertices = []
|
| 468 |
+
all_colors = []
|
| 469 |
+
all_faces = []
|
| 470 |
+
vertex_offset = 0
|
| 471 |
+
|
| 472 |
+
# create spheres for joints
|
| 473 |
+
for i, joint in enumerate(joints):
|
| 474 |
+
# define color
|
| 475 |
+
if root_index is not None and i == root_index:
|
| 476 |
+
color = (0, 1, 0) # green for root joint
|
| 477 |
+
else:
|
| 478 |
+
color = (1, 0, 0) # red for other joints
|
| 479 |
+
|
| 480 |
+
# create joint sphere
|
| 481 |
+
sphere_vertices, sphere_faces = create_sphere(joint, radius=radius_sphere, segments=segments, stacks=stacks)
|
| 482 |
+
all_vertices.extend(sphere_vertices)
|
| 483 |
+
all_colors.extend([color] * len(sphere_vertices))
|
| 484 |
+
|
| 485 |
+
# adjust face index
|
| 486 |
+
adjusted_sphere_faces = [(v1 + vertex_offset, v2 + vertex_offset, v3 + vertex_offset) for (v1, v2, v3) in sphere_faces]
|
| 487 |
+
all_faces.extend(adjusted_sphere_faces)
|
| 488 |
+
vertex_offset += len(sphere_vertices)
|
| 489 |
+
|
| 490 |
+
# create bones
|
| 491 |
+
for bone in bones:
|
| 492 |
+
parent_idx, child_idx = bone
|
| 493 |
+
parent = joints[parent_idx]
|
| 494 |
+
child = joints[child_idx]
|
| 495 |
+
|
| 496 |
+
try:
|
| 497 |
+
bone_vertices, bone_faces = create_bone(parent, child, radius=radius_bone, segments=segments, use_cone=use_cone)
|
| 498 |
+
except ValueError as e:
|
| 499 |
+
print(f"Skipping connection {parent_idx}-{child_idx}, reason: {e}")
|
| 500 |
+
continue
|
| 501 |
+
|
| 502 |
+
all_vertices.extend(bone_vertices)
|
| 503 |
+
all_colors.extend([(0, 0, 1)] * len(bone_vertices)) # blue
|
| 504 |
+
|
| 505 |
+
# adjust face index
|
| 506 |
+
adjusted_bone_faces = [(v1 + vertex_offset, v2 + vertex_offset, v3 + vertex_offset) for (v1, v2, v3) in bone_faces]
|
| 507 |
+
all_faces.extend(adjusted_bone_faces)
|
| 508 |
+
vertex_offset += len(bone_vertices)
|
| 509 |
+
|
| 510 |
+
# save to obj
|
| 511 |
+
obj_lines = []
|
| 512 |
+
for v, c in zip(all_vertices, all_colors):
|
| 513 |
+
obj_lines.append(f"v {v[0]} {v[1]} {v[2]} {c[0]} {c[1]} {c[2]}")
|
| 514 |
+
obj_lines.append("")
|
| 515 |
+
|
| 516 |
+
for face in all_faces:
|
| 517 |
+
obj_lines.append(f"f {face[0]} {face[1]} {face[2]}")
|
| 518 |
+
|
| 519 |
+
with open(save_path, 'w') as obj_file:
|
| 520 |
+
obj_file.write("\n".join(obj_lines))
|
| 521 |
+
|
| 522 |
+
def create_sphere(center, radius=0.01, segments=16, stacks=16):
|
| 523 |
+
vertices = []
|
| 524 |
+
faces = []
|
| 525 |
+
for i in range(stacks + 1):
|
| 526 |
+
lat = np.pi / 2 - i * np.pi / stacks
|
| 527 |
+
xy = radius * np.cos(lat)
|
| 528 |
+
z = radius * np.sin(lat)
|
| 529 |
+
for j in range(segments):
|
| 530 |
+
lon = j * 2 * np.pi / segments
|
| 531 |
+
x = xy * np.cos(lon) + center[0]
|
| 532 |
+
y = xy * np.sin(lon) + center[1]
|
| 533 |
+
vertices.append((x, y, z + center[2]))
|
| 534 |
+
for i in range(stacks):
|
| 535 |
+
for j in range(segments):
|
| 536 |
+
first = i * segments + j
|
| 537 |
+
second = first + segments
|
| 538 |
+
third = first + 1 if (j + 1) < segments else i * segments
|
| 539 |
+
fourth = second + 1 if (j + 1) < segments else (i + 1) * segments
|
| 540 |
+
faces.append((first + 1, second + 1, fourth + 1))
|
| 541 |
+
faces.append((first + 1, fourth + 1, third + 1))
|
| 542 |
+
return vertices, faces
|
| 543 |
+
|
| 544 |
+
def create_bone(start, end, radius=0.005, segments=16, use_cone=False):
|
| 545 |
+
dir_vector = np.array(end) - np.array(start)
|
| 546 |
+
height = np.linalg.norm(dir_vector)
|
| 547 |
+
if height == 0:
|
| 548 |
+
raise ValueError("Start and end points cannot be the same for a cone.")
|
| 549 |
+
dir_vector = dir_vector / height
|
| 550 |
+
|
| 551 |
+
z = np.array([0, 0, 1])
|
| 552 |
+
if np.allclose(dir_vector, z):
|
| 553 |
+
R = np.identity(3)
|
| 554 |
+
elif np.allclose(dir_vector, -z):
|
| 555 |
+
R = np.array([[-1,0,0],[0,-1,0],[0,0,1]])
|
| 556 |
+
else:
|
| 557 |
+
v = np.cross(z, dir_vector)
|
| 558 |
+
s = np.linalg.norm(v)
|
| 559 |
+
c = np.dot(z, dir_vector)
|
| 560 |
+
kmat = np.array([[0, -v[2], v[1]],
|
| 561 |
+
[v[2], 0, -v[0]],
|
| 562 |
+
[-v[1], v[0], 0]])
|
| 563 |
+
R = np.identity(3) + kmat + np.matmul(kmat, kmat) * ((1 - c) / (s**2))
|
| 564 |
+
|
| 565 |
+
theta = np.linspace(0, 2 * np.pi, segments, endpoint=False)
|
| 566 |
+
base_circle = np.array([np.cos(theta), np.sin(theta), np.zeros(segments)]) * radius
|
| 567 |
+
|
| 568 |
+
vertices = []
|
| 569 |
+
for point in base_circle.T:
|
| 570 |
+
rotated = np.dot(R, point) + np.array(start)
|
| 571 |
+
vertices.append(tuple(rotated))
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
faces = []
|
| 575 |
+
|
| 576 |
+
if use_cone:
|
| 577 |
+
vertices.append(tuple(end))
|
| 578 |
+
|
| 579 |
+
apex_idx = segments + 1
|
| 580 |
+
for i in range(segments):
|
| 581 |
+
next_i = (i + 1) % segments
|
| 582 |
+
faces.append((i + 1, next_i + 1, apex_idx))
|
| 583 |
+
else:
|
| 584 |
+
top_circle = np.array([np.cos(theta), np.sin(theta), np.ones(segments)]) * radius
|
| 585 |
+
for point in top_circle.T:
|
| 586 |
+
point_scaled = np.array([point[0], point[1], height])
|
| 587 |
+
rotated = np.dot(R, point_scaled) + np.array(start)
|
| 588 |
+
vertices.append(tuple(rotated))
|
| 589 |
+
for i in range(segments):
|
| 590 |
+
next_i = (i + 1) % segments
|
| 591 |
+
faces.append((i + 1, next_i + 1, next_i + segments + 1))
|
| 592 |
+
faces.append((i + 1, next_i + segments + 1, i + segments + 1))
|
| 593 |
+
|
| 594 |
+
return vertices, faces
|
| 595 |
+
|
| 596 |
+
def render_mesh_with_skeleton(joints, bones, vertices, faces, output_dir, filename, prefix='pred', root_idx=None):
|
| 597 |
+
"""
|
| 598 |
+
Render the mesh with skeleton using PyRender.
|
| 599 |
+
"""
|
| 600 |
+
loader = DataLoader()
|
| 601 |
+
|
| 602 |
+
raw_size = (960, 960)
|
| 603 |
+
renderer = PyRenderWrapper(raw_size)
|
| 604 |
+
|
| 605 |
+
save_dir = os.path.join(output_dir, 'render_results')
|
| 606 |
+
os.makedirs(save_dir, exist_ok=True)
|
| 607 |
+
|
| 608 |
+
loader.joints = joints
|
| 609 |
+
loader.bones = bones
|
| 610 |
+
loader.root_idx = root_idx
|
| 611 |
+
|
| 612 |
+
mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
|
| 613 |
+
mesh.visual.vertex_colors[:, 3] = 100 # set transparency
|
| 614 |
+
loader.mesh = mesh
|
| 615 |
+
v = mesh.vertices
|
| 616 |
+
xmin, ymin, zmin = v.min(axis=0)
|
| 617 |
+
xmax, ymax, zmax = v.max(axis=0)
|
| 618 |
+
loader.bbox_center = np.array([(xmax + xmin)/2, (ymax + ymin)/2, (zmax + zmin)/2])
|
| 619 |
+
loader.bbox_size = np.array([xmax - xmin, ymax - ymin, zmax - zmin])
|
| 620 |
+
loader.bbox_scale = max(xmax - xmin, ymax - ymin, zmax - zmin)
|
| 621 |
+
loader.normalize_coordinates()
|
| 622 |
+
|
| 623 |
+
input_dict = loader.query_mesh_rig()
|
| 624 |
+
|
| 625 |
+
angles = [0, np.pi/2, np.pi, 3*np.pi/2]
|
| 626 |
+
distance = np.max(loader.bbox_size) * 2
|
| 627 |
+
|
| 628 |
+
subfolder_path = os.path.join(save_dir, filename + '_' + prefix)
|
| 629 |
+
|
| 630 |
+
os.makedirs(subfolder_path, exist_ok=True)
|
| 631 |
+
|
| 632 |
+
for i, angle in enumerate(angles):
|
| 633 |
+
renderer.set_camera_view(angle, loader.bbox_center, distance)
|
| 634 |
+
renderer.align_light_to_camera()
|
| 635 |
+
|
| 636 |
+
color = renderer.render(input_dict)[0]
|
| 637 |
+
|
| 638 |
+
output_filename = f"{filename}_{prefix}_view{i+1}.png"
|
| 639 |
+
output_filepath = os.path.join(subfolder_path, output_filename)
|
| 640 |
+
cv2.imwrite(output_filepath, color)
|
| 641 |
+
|
| 642 |
+
|
| 643 |
+
def save_args(args, output_dir, filename="config.json"):
|
| 644 |
+
args_dict = vars(args)
|
| 645 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 646 |
+
config_path = os.path.join(output_dir, filename)
|
| 647 |
+
with open(config_path, 'w') as f:
|
| 648 |
+
json.dump(args_dict, f, indent=4)
|
utils/skeleton_data_loader.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
import torch
|
| 15 |
+
from torch import is_tensor
|
| 16 |
+
from torch.utils.data import Dataset
|
| 17 |
+
from torch.nn.utils.rnn import pad_sequence
|
| 18 |
+
from data_utils.save_npz import normalize_to_unit_cube
|
| 19 |
+
|
| 20 |
+
import numpy as np
|
| 21 |
+
|
| 22 |
+
class SkeletonData(Dataset):
|
| 23 |
+
"""
|
| 24 |
+
A PyTorch Dataset to load and process skeleton data.
|
| 25 |
+
"""
|
| 26 |
+
def __init__(self, data, args, is_training):
|
| 27 |
+
self.data = data
|
| 28 |
+
|
| 29 |
+
self.input_pc_num = args.input_pc_num
|
| 30 |
+
self.is_training = is_training
|
| 31 |
+
|
| 32 |
+
self.hier_order = args.hier_order
|
| 33 |
+
print(f"[Dataset] Created from {len(self.data)} entries")
|
| 34 |
+
|
| 35 |
+
def __len__(self):
|
| 36 |
+
return len(self.data)
|
| 37 |
+
|
| 38 |
+
def __getitem__(self, idx):
|
| 39 |
+
data = self.data[idx]
|
| 40 |
+
|
| 41 |
+
joints = data['joints']
|
| 42 |
+
vertices = data['vertices']
|
| 43 |
+
pc_normal = data['pc_w_norm']
|
| 44 |
+
|
| 45 |
+
indices = np.random.choice(pc_normal.shape[0], self.input_pc_num, replace=False)
|
| 46 |
+
pc_normal = pc_normal[indices, :]
|
| 47 |
+
|
| 48 |
+
pc_coor = pc_normal[:, :3]
|
| 49 |
+
normal = pc_normal[:, 3:]
|
| 50 |
+
if np.linalg.norm(normal, axis=1, keepdims=True).min() < 0.99:
|
| 51 |
+
print("normal reroll")
|
| 52 |
+
return self.__getitem__(np.random.randint(0, len(self.data)))
|
| 53 |
+
|
| 54 |
+
data_dict = {}
|
| 55 |
+
|
| 56 |
+
# normalize normal
|
| 57 |
+
normal = normal / np.linalg.norm(normal, axis=1, keepdims=True)
|
| 58 |
+
|
| 59 |
+
# scale to -0.5 to 0.5
|
| 60 |
+
_, center, scale = normalize_to_unit_cube(vertices.copy(), scale_factor=0.9995)
|
| 61 |
+
joints = (joints - center) * scale # align joints with pc first
|
| 62 |
+
|
| 63 |
+
bounds = np.array([pc_coor.min(axis=0), pc_coor.max(axis=0)])
|
| 64 |
+
pc_center = (bounds[0] + bounds[1])[None, :] / 2
|
| 65 |
+
pc_scale = (bounds[1] - bounds[0]).max() + 1e-5
|
| 66 |
+
pc_coor = (pc_coor - pc_center) / pc_scale
|
| 67 |
+
joints = (joints - pc_center) / pc_scale
|
| 68 |
+
|
| 69 |
+
joints = joints.clip(-0.5, 0.5)
|
| 70 |
+
|
| 71 |
+
data_dict['joints'] = torch.from_numpy(np.asarray(joints).astype(np.float16))
|
| 72 |
+
data_dict['bones'] = torch.from_numpy(data['bones'].astype(np.int64))
|
| 73 |
+
pc_coor = pc_coor / np.abs(pc_coor).max() * 0.9995
|
| 74 |
+
data_dict['pc_normal'] = torch.from_numpy(np.concatenate([pc_coor, normal], axis=-1).astype(np.float16))
|
| 75 |
+
data_dict['vertices'] = torch.from_numpy(data['vertices'].astype(np.float16))
|
| 76 |
+
data_dict['faces'] = torch.from_numpy(data['faces'].astype(np.int64))
|
| 77 |
+
data_dict['uuid'] = data['uuid']
|
| 78 |
+
data_dict['root_index'] = str(data['root_index'])
|
| 79 |
+
data_dict['transform_params'] = torch.tensor([
|
| 80 |
+
center[0], center[1], center[2],
|
| 81 |
+
scale,
|
| 82 |
+
pc_center[0][0], pc_center[0][1], pc_center[0][2],
|
| 83 |
+
pc_scale
|
| 84 |
+
], dtype=torch.float32)
|
| 85 |
+
|
| 86 |
+
return data_dict
|
| 87 |
+
|
| 88 |
+
@classmethod
|
| 89 |
+
def load(cls, args, is_training=True):
|
| 90 |
+
loaded_data = np.load(args.dataset_path, allow_pickle=True)
|
| 91 |
+
data = []
|
| 92 |
+
for item in loaded_data["arr_0"]:
|
| 93 |
+
data.append(item)
|
| 94 |
+
print(f"[Dataset] Loaded {len(data)} entries")
|
| 95 |
+
return cls(data, args, is_training)
|
| 96 |
+
|
| 97 |
+
|