Skip to content

Commit

Permalink
Apply formatting.
Browse files Browse the repository at this point in the history
  • Loading branch information
fire committed Oct 6, 2024
1 parent a4049e9 commit aea65f5
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 294 deletions.
2 changes: 1 addition & 1 deletion modules/robust_skin_weight_transfer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ if find_closest_point_on_surface can get the normal of that point, then can it a

Chev — Today

you can use the index of the triangle to look up other informations
you can use the index of the triangle to look up other information
it's the backend of robust weight transfer. trying to port it from python to c++

janie bean — Today
Expand Down
1 change: 0 additions & 1 deletion modules/robust_skin_weight_transfer/meshes/cage.obj
Original file line number Diff line number Diff line change
Expand Up @@ -355,4 +355,3 @@ f 18 27 44
f 44 37 18
f 29 46 45
f 45 28 29

328 changes: 265 additions & 63 deletions modules/robust_skin_weight_transfer/src/main.py
Original file line number Diff line number Diff line change
@@ -1,75 +1,39 @@
import argparse
import json
import math
import os
import unittest
from typing import Tuple

import igl
import numpy as np
import scipy as sp
import robust_laplacian
import scipy as sp
import trimesh


def create_diamond():
# Define vertices for a small diamond shape scaled to mm
scale_factor = 0.005 # Scaling down from meters to millimeters
vertices = np.array([
[0, 0, 1], # Top vertex
[1, 0, 0], # Side vertex
[-1, 0, 0], # Side vertex
[0, 1, 0], # Side vertex
[0, -1, 0], # Side vertex
[0, 0, -1] # Bottom vertex
]) * scale_factor # Apply scale factor to each vertex

vertices = (
np.array(
[
[0, 0, 1], # Top vertex
[1, 0, 0], # Side vertex
[-1, 0, 0], # Side vertex
[0, 1, 0], # Side vertex
[0, -1, 0], # Side vertex
[0, 0, -1], # Bottom vertex
]
)
* scale_factor
) # Apply scale factor to each vertex

# Define faces connecting the vertices
faces = np.array([
[0, 1, 3],
[0, 3, 2],
[0, 2, 4],
[0, 4, 1],
[5, 3, 1],
[5, 2, 3],
[5, 4, 2],
[5, 1, 4]
])

faces = np.array([[0, 1, 3], [0, 3, 2], [0, 2, 4], [0, 4, 1], [5, 3, 1], [5, 2, 3], [5, 4, 2], [5, 1, 4]])

return trimesh.Trimesh(vertices=vertices, faces=faces)

def create_cages(source, target, outer_mesh_path):
"""
Creates a swept volume between a character body mesh and its clothing mesh with a specified margin.
Args:
source (str): Path to the character body mesh file (.obj).
target (str): Path to the clothing mesh file (.obj).
output_path (str): Path where the swept volume mesh will be saved (.obj).
margin (float): Margin distance in millimeters to be added to the clothing mesh.
"""
source_mesh = trimesh.load(source)
if hasattr(source_mesh, 'to_geometry'):
source_mesh = source_mesh.to_geometry()
target_mesh = trimesh.load(target)
if hasattr(target_mesh, 'to_geometry'):
target_mesh = target_mesh.to_geometry()
source_vertices = np.array(source_mesh.vertices)
outer_mesh = trimesh.Trimesh(vertices=source_vertices).convex_hull
target_vertices = np.array(target_mesh.vertices)
target_faces = np.array(target_mesh.faces)
distances, face_indices, points, barycentric = find_closest_point_on_surface(
source_vertices, target_vertices, target_faces
)
all_meshes = [target_mesh]
diamond_mesh_template = create_diamond()
test_points = np.array(target_mesh.vertices)
num_points_to_select = 200
selected_indices = np.random.choice(len(test_points), num_points_to_select, replace=False)
selected_test_points = test_points[selected_indices]
for point in selected_test_points:
diamond_mesh = diamond_mesh_template.copy()
diamond_mesh.apply_translation(point)
all_meshes.append(diamond_mesh)
outer_mesh = trimesh.util.concatenate(all_meshes).convex_hull
outer_mesh.export(outer_mesh_path)


def find_closest_point_on_surface(points, mesh_vertices, mesh_triangles):
"""
Expand Down Expand Up @@ -231,7 +195,7 @@ def inpaint(V2, F2, W2, Matched):

if not (Matched.ndim == 1 and len(Matched) == len(V2)):
return None, False

# Compute the laplacian
L, M = robust_laplacian.mesh_laplacian(V2, F2)
L = -L # Flip the sign of the Laplacian
Expand Down Expand Up @@ -308,6 +272,7 @@ def get_points_within_distance(vertices, vertex_id, distance=distance_threshold)

return smoothed_weights, vertices_ids_to_smooth


def load_mesh(mesh_path: str) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
v, vt, vn, f, ft, fn = igl.read_obj(mesh_path)

Expand Down Expand Up @@ -391,22 +356,259 @@ def main(source_mesh: str, target_mesh: str, output_file: str) -> None:
"bones": skin_bones_target.tolist(),
}

# Write the final mesh to the output file
output_file_path: str = os.path.join(current_folder, output_file)
with open(output_file_path, "w") as f:
json.dump(mesh_data, f)

import argparse
from argparse import ArgumentParser, Namespace

def create_cages(source, target, outer_mesh_path):
"""
Creates a swept volume between a character body mesh and its clothing mesh with a specified margin.
Args:
source (str): Path to the character body mesh file (.obj).
target (str): Path to the clothing mesh file (.obj).
output_path (str): Path where the swept volume mesh will be saved (.obj).
margin (float): Margin distance in millimeters to be added to the clothing mesh.
"""
source_mesh = trimesh.load(source)
if hasattr(source_mesh, "to_geometry"):
source_mesh = source_mesh.to_geometry()
target_mesh = trimesh.load(target)
if hasattr(target_mesh, "to_geometry"):
target_mesh = target_mesh.to_geometry()
source_vertices = np.array(source_mesh.vertices)
outer_mesh = trimesh.Trimesh(vertices=source_vertices).convex_hull
target_vertices = np.array(target_mesh.vertices)
target_faces = np.array(target_mesh.faces)
distances, face_indices, points, barycentric = find_closest_point_on_surface(
source_vertices, target_vertices, target_faces
)
all_meshes = [target_mesh]
diamond_mesh_template = create_diamond()
test_points = np.array(target_mesh.vertices)
num_points_to_select = 200
selected_indices = np.random.choice(len(test_points), num_points_to_select, replace=False)
selected_test_points = test_points[selected_indices]
for point in selected_test_points:
diamond_mesh = diamond_mesh_template.copy()
diamond_mesh.apply_translation(point)
all_meshes.append(diamond_mesh)
outer_mesh = trimesh.util.concatenate(all_meshes).convex_hull
outer_mesh.export(outer_mesh_path)


def parse_arguments():
# TODO output mesh with weight transferred.
parser = argparse.ArgumentParser(description="Generate a swept volume mesh between two given meshes.")
parser.add_argument("--source_mesh", type=str, required=True, help="Path to the source mesh file")
parser.add_argument("--target_mesh", type=str, required=True, help="Path to the target mesh file")
parser.add_argument("--cage_file", type=str, required=True, help="Path to the cage_file file")
parser.add_argument("--test", type=str, required=True, help="Run tests")
return parser.parse_args()


class TestMeshProcessing(unittest.TestCase):
def test_find_closest_point_on_surface(self):
vertices = np.array([[-1, -1, -1], [1, -1, -1], [1, 1, -1]])
triangles = np.array(
[
[0, 1, 2] # Only one triangle
]
)
test_points = np.array(
[
[0, 0, 0], # Inside the projected area of the triangle
[2, 2, 2], # Outside near the plane of the triangle
[0, 0, -2], # Directly outside the triangle in the normal direction
]
)
expected_distances = [1.0, 11.0, 1.0]
expected_indices = [0, 0, 0]
expected_points = [[0.0, 0.0, -1.0], [1.0, 1.0, -1.0], [0.0, 0.0, -1.0]]
expected_barycentric = [[0.5, 0.0, 0.5], [0.0, 0.0, 1.0], [0.5, 0.0, 0.5]]
distances, indices, points, barycentric = find_closest_point_on_surface(test_points, vertices, triangles)
np.testing.assert_array_almost_equal(distances, expected_distances)
np.testing.assert_array_equal(indices, expected_indices)
np.testing.assert_array_almost_equal(points, expected_points)
np.testing.assert_array_almost_equal(barycentric, expected_barycentric)
affine_matrix = np.array(
[
[1, 0, 0, 1], # Translation along x
[0, 1, 0, 2], # Translation along y
[0, 0, 1, 3], # Translation along z
[0, 0, 0, 1], # Homogeneous coordinate
]
)
original_vertices = np.array([[-1, -1, -1, 1], [1, -1, -1, 1], [1, 1, -1, 1]])
transformed_vertices = original_vertices @ affine_matrix.T
transformed_vertices = transformed_vertices[:, :3] # Remove homogeneous coordinate
test_points_transformed = np.array(
[
[1, 2, 2], # Inside the projected area of the triangle
[3, 4, 5], # Outside near the plane of the triangle
[1, 2, 1], # Directly outside the triangle in the normal direction
]
)
distances_transformed, indices_transformed, points_transformed, barycentric_transformed = (
find_closest_point_on_surface(test_points_transformed, transformed_vertices, triangles)
)
expected_distances = [0.0, 11.0, 1.0]
expected_indices = [0, 0, 0]
expected_points = [[1.0, 2.0, 2.0], [2.0, 3.0, 2.0], [1.0, 2.0, 2.0]]
np.testing.assert_array_almost_equal(distances_transformed, expected_distances)
np.testing.assert_array_equal(indices_transformed, expected_indices)
np.testing.assert_array_almost_equal(points_transformed, expected_points)
np.testing.assert_array_almost_equal(barycentric_transformed, expected_barycentric)

def test_interpolate_attribute_from_bary(self):
vertex_attributes = np.array([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]])
barycentric_coordinates = np.array([[0.2, 0.5, 0.3], [0.6, 0.3, 0.1]])
primitive_indices = np.array([0, 1])
mesh_triangles = np.array([[0, 1, 2], [2, 3, 4]])
expected_output = np.array(
[
[1 * 0.2 + 3 * 0.5 + 5 * 0.3, 2 * 0.2 + 4 * 0.5 + 6 * 0.3],
[5 * 0.6 + 7 * 0.3 + 9 * 0.1, 6 * 0.6 + 8 * 0.3 + 10 * 0.1],
]
)
result = interpolate_attribute_from_bary(
vertex_attributes, barycentric_coordinates, primitive_indices, mesh_triangles
)
np.testing.assert_array_almost_equal(result, expected_output)

def test_normalize_vector(self):
vector = np.array([3, 4, 0])
normalized = normalize_vector(vector)
expected = np.array([0.6, 0.8, 0])
np.testing.assert_array_almost_equal(normalized, expected)

def test_find_matches_closest_surface(self):
# Mock data setup
source_vertices = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]])
source_triangles = np.array([[0, 1, 2]])
source_normals = np.array([[0, 0, 1], [0, 0, 1], [0, 0, 1]])
source_weights = np.array([[1, 0], [0, 1], [0.5, 0.5]])

target_vertices = np.array(
[
[0.1, 0.1, 0],
[2, 2, 2], # This vertex should not match due to distance
]
)
target_triangles = np.array([[0, 1]])
target_normals = np.array(
[
[0, 0, 1],
[1, 0, 0], # This normal should not match due to angle
]
)

distance_threshold_squared = 0.5
angle_threshold_degrees = 10

# Expected output
expected_matched = np.array([True, False])
expected_weights = np.array([[0.85, 0.15], [0.25, 0.75]])

# Running the function
matched, target_weights = find_matches_closest_surface(
source_vertices,
source_triangles,
source_normals,
target_vertices,
target_triangles,
target_normals,
source_weights,
distance_threshold_squared,
angle_threshold_degrees,
)

# Asserting the results
np.testing.assert_array_equal(matched, expected_matched)
np.testing.assert_array_almost_equal(target_weights, expected_weights)

def test_is_valid_array(self):
valid_matrix = np.array([[1, 2], [3, 4]])
invalid_matrix = np.array([[np.nan, 2], [np.inf, 4]])
np.testing.assert_equal(is_valid_array(valid_matrix), True)
np.testing.assert_equal(is_valid_array(invalid_matrix), False)

def test_inpaint(self):
V2 = np.array(
[
[0, 0, 0],
[1, 0, 0],
[0, 1, 0],
[1, 1, 0], # This vertex needs inpainting
]
)
F2 = np.array([[0, 1, 2], [1, 2, 3]])
W2 = np.array(
[
[1, 0],
[0, 1],
[0.5, 0.5],
[0, 0], # Initial weights for the vertex that needs inpainting
]
)
Matched = np.array([True, True, True, False])
expected_W_inpainted = np.array(
[
[1.0, 0.0],
[0.0, 1.0],
[0.5, 0.5],
[0.117647, 0.882353], # Expected inpainted weights
]
)
W_inpainted, success = inpaint(V2, F2, W2, Matched)
np.testing.assert_equal(success, True)
np.testing.assert_array_almost_equal(W_inpainted, expected_W_inpainted)

def test_smooth(self):
target_vertices = np.array(
[
[0, 0, 0],
[1, 0, 0],
[0, 1, 0],
[1, 1, 0], # This vertex needs smoothing
[2, 1, 0], # Additional vertex for distance check
]
)
target_faces = np.array([[0, 1, 2], [1, 2, 3]])
skinning_weights = np.array(
[
[1, 0],
[0, 1],
[0.5, 0.5],
[0.25, 0.75], # Initial weights for the vertex that needs smoothing
[0.1, 0.9], # Additional vertex weight
]
)
matched = np.array([True, True, True, False, False])
distance_threshold = 1.5 # Distance threshold for smoothing

smoothed_weights, vertices_ids_to_smooth = smooth(
target_vertices,
target_faces,
skinning_weights,
matched,
distance_threshold,
num_smooth_iter_steps=1, # Single iteration for simplicity
smooth_alpha=0.2,
)

expected_smoothed_weights = np.array(
[[0.85, 0.15], [0.10666667, 0.89333333], [0.48044444, 0.51955556], [0.25871111, 0.74128889], [0.1, 0.9]]
)
expected_vertices_ids_to_smooth = np.array([True, True, True, True, True])

np.testing.assert_array_almost_equal(smoothed_weights, expected_smoothed_weights)
np.testing.assert_array_equal(vertices_ids_to_smooth, expected_vertices_ids_to_smooth)


if __name__ == "__main__":
args = parse_arguments()
create_cages(args.source_mesh, args.target_mesh, args.cage_file)
if args.test:
unittest.main()
else:
create_cages(args.source_mesh, args.target_mesh, args.cage_file)
Loading

0 comments on commit aea65f5

Please sign in to comment.