349 lines
11 KiB
Python
349 lines
11 KiB
Python
import numpy as np
|
|
import open3d as o3d
|
|
import pymeshlab
|
|
import torch
|
|
import trimesh
|
|
from pymeshlab import Percentage
|
|
|
|
|
|
### Mesh Utils ###
|
|
##### read mesh
|
|
def read_mesh_from_path(mesh_path):
|
|
ms = pymeshlab.MeshSet()
|
|
ms.load_new_mesh(mesh_path)
|
|
return ms
|
|
|
|
|
|
def mesh_to_meshlab(vertices, faces):
|
|
mesh = pymeshlab.Mesh(vertex_matrix=vertices, face_matrix=faces)
|
|
ms = pymeshlab.MeshSet()
|
|
ms.add_mesh(mesh)
|
|
return ms
|
|
|
|
|
|
def meshlab_to_mesh(ms):
|
|
m = ms.current_mesh()
|
|
return m.vertex_matrix(), m.face_matrix(), m.vertex_normal_matrix()
|
|
|
|
|
|
##### decimation
|
|
def decimate_quadric_edge_collapse_with_texture(
|
|
ms, targetfacenum=None, preservenormal=True, verbose=False
|
|
):
|
|
# targetfacenum: int, Target number of faces.
|
|
# preservenormal: bool, Preserve the normals of the original mesh.
|
|
if verbose:
|
|
print("Starting decimation ... ")
|
|
m = ms.current_mesh()
|
|
if targetfacenum is None:
|
|
targetfacenum = int(m.face_number() * 0.5)
|
|
if verbose:
|
|
print("... Initial face number is %d ... " % m.face_number())
|
|
ms.meshing_decimation_quadric_edge_collapse_with_texture(
|
|
targetfacenum=targetfacenum, preservenormal=preservenormal
|
|
)
|
|
if verbose:
|
|
print("... Decimated face number is %d ... " % m.face_number())
|
|
print("Decimation done!\n ")
|
|
|
|
|
|
def decimate_quadric_edge_collapse(
|
|
ms, targetfacenum=None, preservenormal=True, verbose=False
|
|
):
|
|
# targetfacenum: int, Target number of faces.
|
|
# preservenormal: bool, Preserve the normals of the original mesh.
|
|
if verbose:
|
|
print("Starting decimation ... ")
|
|
m = ms.current_mesh()
|
|
if targetfacenum is None:
|
|
targetfacenum = int(m.face_number() * 0.5)
|
|
if verbose:
|
|
print("... Initial face number is %d ... " % m.face_number())
|
|
ms.meshing_decimation_quadric_edge_collapse(
|
|
targetfacenum=targetfacenum, preservenormal=preservenormal
|
|
)
|
|
if verbose:
|
|
print("... Decimated face number is %d ... " % m.face_number())
|
|
print("Decimation done!\n ")
|
|
|
|
|
|
##### vertex merge
|
|
def merge_close_vertices(ms, threshold=0.0001, verbose=False):
|
|
# threshold: float, Merge together all the vertices that are nearer than the specified threshold.
|
|
if verbose:
|
|
print("Starting merge vertices ... ")
|
|
m = ms.current_mesh()
|
|
if verbose:
|
|
print("... Initial vertex number is %d ... " % m.vertex_number())
|
|
ms.meshing_merge_close_vertices(threshold=Percentage(threshold * 100))
|
|
if verbose:
|
|
print("... Merged vertex number is %d ... " % m.vertex_number())
|
|
print("Merge vertices done!\n ")
|
|
|
|
|
|
##### Island Removal
|
|
def remove_isolated_pieces(ms, mincomponentsize=25, diameter=None, verbose=False):
|
|
# mincomponentsize: Delete isolated connected components composed by a limited number of triangles
|
|
# diameter: Delete isolated connected components whose diameter is smaller than the specified constant
|
|
if verbose:
|
|
print("Starting remove isolated pieces ... ")
|
|
m = ms.current_mesh()
|
|
if verbose:
|
|
print("... Initial face number is %d ... " % m.face_number())
|
|
if diameter is None:
|
|
ms.meshing_remove_connected_component_by_face_number(
|
|
mincomponentsize=mincomponentsize, removeunref=True
|
|
)
|
|
else:
|
|
ms.meshing_remove_connected_component_by_diameter(
|
|
mincomponentdiag=Percentage(diameter), removeunref=True
|
|
)
|
|
if verbose:
|
|
print("... Isolated removed face number is %d ... " % m.face_number())
|
|
print("Remove isolated pieces done!\n ")
|
|
|
|
|
|
##### hole filling
|
|
def fix_hole(ms, maxholesize=30, verbose=False):
|
|
# maxholesize: int, Maximum size of the hole to be filled.
|
|
if verbose:
|
|
print("Starting fix holes ... ")
|
|
m = ms.current_mesh()
|
|
if verbose:
|
|
print("... Initial face number is %d ... " % m.face_number())
|
|
ms.meshing_close_holes(maxholesize=maxholesize)
|
|
if verbose:
|
|
print("... Fixed hole face number is %d ... " % m.face_number())
|
|
print("Fix holes done!\n ")
|
|
|
|
|
|
##### repair non manifold edges
|
|
def repair_non_manifold(ms, verbose=False):
|
|
if verbose:
|
|
print("Starting repair non manifold edges ... ")
|
|
m = ms.current_mesh()
|
|
if verbose:
|
|
print("... Initial face number is %d ... " % m.face_number())
|
|
ms.meshing_repair_non_manifold_edges()
|
|
ms.meshing_repair_non_manifold_vertices(vertdispratio=0.1)
|
|
ms.meshing_remove_duplicate_faces()
|
|
if verbose:
|
|
print("... Fixed non manifold edges face number is %d ... " % m.face_number())
|
|
print("Repair non manifold edges done!\n ")
|
|
|
|
|
|
##### laplacian_smooth
|
|
def laplacian_smooth(ms, stepsmoothnum=3, verbose=False):
|
|
# stepsmoothnum: int, Number of smoothing steps to be performed
|
|
if verbose:
|
|
print("Starting laplacian smooth ... ")
|
|
m = ms.current_mesh()
|
|
ms.apply_coord_laplacian_smoothing(stepsmoothnum=stepsmoothnum)
|
|
if verbose:
|
|
print("Laplacian smooth done!\n ")
|
|
|
|
|
|
##### taubin_smooth
|
|
def taubin_smooth(ms, stepsmoothnum=3, verbose=False):
|
|
if verbose:
|
|
print("Starting Taubin smooth ... ")
|
|
m = ms.current_mesh()
|
|
ms.apply_coord_taubin_smoothing(stepsmoothnum=stepsmoothnum)
|
|
if verbose:
|
|
print("Taubin smooth done!\n ")
|
|
|
|
|
|
##### compute_normal
|
|
def compute_normal(ms, weightmode="Simple Average", verbose=False):
|
|
if verbose:
|
|
print("Starting compute_normal_per_vertex ... ")
|
|
m = ms.current_mesh()
|
|
ms.compute_normal_per_vertex(weightmode=weightmode)
|
|
if verbose:
|
|
print("compute_normal_per_vertex done!\n ")
|
|
|
|
|
|
### Pre-process Mesh ###
|
|
def process_mesh(
|
|
vertices,
|
|
faces,
|
|
threshold=0.0001,
|
|
mincomponentRatio=0.02,
|
|
targetfacenum=50000,
|
|
maxholesize=30,
|
|
stepsmoothnum=10,
|
|
verbose=False,
|
|
):
|
|
ms = mesh_to_meshlab(vertices, faces)
|
|
|
|
### Vertex Merge
|
|
merge_close_vertices(ms, threshold=threshold, verbose=verbose)
|
|
|
|
### Island Removal
|
|
faces = ms.current_mesh().face_matrix()
|
|
remove_isolated_pieces(
|
|
ms, mincomponentsize=int(len(faces) * mincomponentRatio), verbose=verbose
|
|
)
|
|
|
|
### Hole Filling
|
|
repair_non_manifold(ms) # repair before fix hole
|
|
fix_hole(ms, maxholesize=maxholesize, verbose=verbose)
|
|
|
|
### Taubin Smoothing
|
|
taubin_smooth(ms, stepsmoothnum=stepsmoothnum, verbose=verbose)
|
|
|
|
vertices, faces, _ = meshlab_to_mesh(ms)
|
|
if faces.shape[0] > targetfacenum:
|
|
device = o3d.core.Device("CPU:0")
|
|
dtype_f = o3d.core.float32
|
|
dtype_i = o3d.core.int64
|
|
mesh = o3d.t.geometry.TriangleMesh(device)
|
|
mesh.vertex.positions = o3d.core.Tensor(
|
|
vertices.astype(np.float32), dtype_f, device
|
|
)
|
|
mesh.triangle.indices = o3d.core.Tensor(faces.astype(np.int64), dtype_i, device)
|
|
simplified_mesh = mesh.simplify_quadric_decimation(
|
|
target_reduction=1.0 - float(targetfacenum) / faces.shape[0]
|
|
)
|
|
ms.clear()
|
|
vertices = simplified_mesh.vertex.positions.numpy()
|
|
faces = simplified_mesh.triangle.indices.numpy()
|
|
mesh = pymeshlab.Mesh(vertex_matrix=vertices, face_matrix=faces)
|
|
ms.add_mesh(mesh)
|
|
|
|
### Mesh Simplification/Decimation
|
|
# decimate_quadric_edge_collapse(ms, targetfacenum=targetfacenum, verbose=verbose)
|
|
taubin_smooth(ms, stepsmoothnum=stepsmoothnum, verbose=verbose)
|
|
repair_non_manifold(ms, verbose=verbose)
|
|
compute_normal(ms, verbose=verbose)
|
|
return meshlab_to_mesh(ms)
|
|
|
|
|
|
### UV Un-Warp ###
|
|
def uv_parameterize_uvatlas(
|
|
vertices,
|
|
faces,
|
|
size=1024,
|
|
gutter=2.5,
|
|
max_stretch=0.1666666716337204,
|
|
parallel_partitions=16,
|
|
nthreads=0,
|
|
):
|
|
device = o3d.core.Device("CPU:0")
|
|
dtype_f = o3d.core.float32
|
|
dtype_i = o3d.core.int64
|
|
|
|
mesh = o3d.t.geometry.TriangleMesh(device)
|
|
|
|
mesh.vertex.positions = o3d.core.Tensor(
|
|
vertices.astype(np.float32), dtype_f, device
|
|
)
|
|
mesh.triangle.indices = o3d.core.Tensor(faces.astype(np.int64), dtype_i, device)
|
|
|
|
mesh.compute_uvatlas(
|
|
size=size,
|
|
gutter=gutter,
|
|
max_stretch=max_stretch,
|
|
parallel_partitions=parallel_partitions,
|
|
nthreads=nthreads,
|
|
)
|
|
|
|
return mesh.triangle.texture_uvs.numpy() # (#F, 3, 2)
|
|
|
|
|
|
### Pack All ###
|
|
def process_raw(mesh_path, save_path, preprocess=True, device="cpu"):
|
|
scene = trimesh.load(mesh_path, force="mesh", process=False)
|
|
if isinstance(scene, trimesh.Trimesh):
|
|
mesh = scene
|
|
elif isinstance(scene, trimesh.scene.Scene):
|
|
mesh = trimesh.Trimesh()
|
|
for obj in scene.geometry.values():
|
|
mesh = trimesh.util.concatenate([mesh, obj])
|
|
else:
|
|
raise ValueError(f"Unknown mesh type at {mesh_path}.")
|
|
|
|
vertices = mesh.vertices
|
|
faces = mesh.faces
|
|
|
|
mesh_post_process_options = {
|
|
"mincomponentRatio": 0.02,
|
|
"targetfacenum": 50000,
|
|
"maxholesize": 100,
|
|
"stepsmoothnum": 10,
|
|
"verbose": False,
|
|
}
|
|
|
|
if preprocess:
|
|
v_pos, t_pos_idx, normals = process_mesh(
|
|
vertices=vertices,
|
|
faces=faces,
|
|
**mesh_post_process_options,
|
|
)
|
|
else:
|
|
v_pos, t_pos_idx, normals = vertices, faces, mesh.vertex_normals
|
|
|
|
v_tex_np = (
|
|
uv_parameterize_uvatlas(v_pos, t_pos_idx).reshape(-1, 2).astype(np.float32)
|
|
)
|
|
|
|
v_pos = torch.from_numpy(v_pos).to(device=device, dtype=torch.float32)
|
|
t_pos_idx = torch.from_numpy(t_pos_idx).to(device=device, dtype=torch.long)
|
|
v_tex = torch.from_numpy(v_tex_np).to(device=device, dtype=torch.float32)
|
|
normals = torch.from_numpy(normals).to(device=device, dtype=torch.float32)
|
|
assert v_tex.shape[0] == t_pos_idx.shape[0] * 3
|
|
t_tex_idx = torch.arange(
|
|
t_pos_idx.shape[0] * 3,
|
|
device=device,
|
|
dtype=torch.long,
|
|
).reshape(-1, 3)
|
|
# uv, index = torch.unique(v_tex, dim=0, return_inverse=True) # 这样实现是2毫秒
|
|
# super efficient de-duplication
|
|
v_tex_u_uint32 = v_tex_np[..., 0].view(np.uint32)
|
|
v_tex_v_uint32 = v_tex_np[..., 1].view(np.uint32)
|
|
v_hashed = (v_tex_u_uint32.astype(np.uint64) << 32) | v_tex_v_uint32
|
|
v_hashed = torch.from_numpy(v_hashed.view(np.int64)).to(v_pos.device)
|
|
|
|
t_pos_idx_f3 = torch.arange(
|
|
t_pos_idx.shape[0] * 3, device=t_pos_idx.device, dtype=torch.long
|
|
).reshape(-1, 3)
|
|
v_pos_f3 = v_pos[t_pos_idx].reshape(-1, 3)
|
|
normals_f3 = normals[t_pos_idx].reshape(-1, 3)
|
|
|
|
v_hashed_dedup, inverse_indices = torch.unique(v_hashed, return_inverse=True)
|
|
dedup_size, full_size = v_hashed_dedup.shape[0], inverse_indices.shape[0]
|
|
indices = torch.scatter_reduce(
|
|
torch.full(
|
|
[dedup_size],
|
|
fill_value=full_size,
|
|
device=inverse_indices.device,
|
|
dtype=torch.long,
|
|
),
|
|
index=inverse_indices,
|
|
src=torch.arange(full_size, device=inverse_indices.device, dtype=torch.int64),
|
|
dim=0,
|
|
reduce="amin",
|
|
)
|
|
v_tex = v_tex[indices]
|
|
t_tex_idx = inverse_indices.reshape(-1, 3)
|
|
|
|
v_pos = v_pos_f3[indices]
|
|
normals = normals_f3[indices]
|
|
|
|
normals = normals.to(dtype=torch.float32, device=device)
|
|
|
|
# either flip uv or flip texture
|
|
# here we flip uv
|
|
uv_to_save = v_tex.clone()
|
|
uv_to_save[:, 1] = 1.0 - uv_to_save[:, 1]
|
|
|
|
visual = trimesh.visual.TextureVisuals(uv=uv_to_save.cpu().numpy())
|
|
tmesh = trimesh.Trimesh(
|
|
vertices=v_pos.cpu().numpy(),
|
|
faces=t_tex_idx.cpu().numpy(),
|
|
vertex_normals=normals.cpu().numpy(),
|
|
visual=visual,
|
|
process=False,
|
|
)
|
|
tmesh.export(save_path)
|