343 lines
14 KiB
Python
343 lines
14 KiB
Python
import torch
|
|
import numpy as np
|
|
import pymeshlab as pml
|
|
|
|
from .mesh import PointCloud
|
|
from ..shared_utils.sh_utils import SH2RGB, RGB2SH
|
|
|
|
def _base_face_areas(face_vertices_0, face_vertices_1, face_vertices_2):
|
|
"""Base function to compute the face areas."""
|
|
x1, x2, x3 = torch.split(face_vertices_0 - face_vertices_1, 1, dim=-1)
|
|
y1, y2, y3 = torch.split(face_vertices_1 - face_vertices_2, 1, dim=-1)
|
|
|
|
a = (x2 * y3 - x3 * y2) ** 2
|
|
b = (x3 * y1 - x1 * y3) ** 2
|
|
c = (x1 * y2 - x2 * y1) ** 2
|
|
areas = torch.sqrt(a + b + c) * 0.5
|
|
|
|
return areas
|
|
|
|
|
|
def _base_sample_points_selected_faces(face_vertices, face_features=None):
|
|
"""Base function to sample points over selected faces, sample one point per face.
|
|
The coordinates of the face vertices are interpolated to generate new samples.
|
|
|
|
Args:
|
|
face_vertices (tuple of torch.Tensor):
|
|
Coordinates of vertices, corresponding to selected faces to sample from.
|
|
A tuple of 3 entries corresponding to each of the face vertices.
|
|
Each entry is a torch.Tensor of shape :math:`(\\text{batch_size}, \\text{num_samples}, 3)`.
|
|
face_features (tuple of torch.Tensor, Optional):
|
|
Features of face vertices, corresponding to selected faces to sample from.
|
|
A tuple of 3 entries corresponding to each of the face vertices.
|
|
Each entry is a torch.Tensor of shape
|
|
:math:`(\\text{batch_size}, \\text{num_samples}, \\text{feature_dim})`.
|
|
|
|
Returns:
|
|
(torch.Tensor, torch.Tensor):
|
|
Sampled point coordinates of shape :math:`(\\text{batch_size}, \\text{num_samples}, 3)`.
|
|
Sampled points interpolated features of shape
|
|
:math:`(\\text{batch_size}, \\text{num_samples}, \\text{feature_dim})`.
|
|
If `face_vertices_features` arg is not specified, the returned interpolated features are None.
|
|
"""
|
|
|
|
face_vertices0, face_vertices1, face_vertices2 = face_vertices
|
|
|
|
sampling_shape = tuple(int(d) for d in face_vertices0.shape[:-1]) + (1,)
|
|
# u is proximity to middle point between v1 and v2 against v0.
|
|
# v is proximity to v2 against v1.
|
|
#
|
|
# The probability density for u should be f_U(u) = 2u.
|
|
# However, torch.rand use a uniform (f_X(x) = x) distribution,
|
|
# so using torch.sqrt we make a change of variable to have the desired density
|
|
# f_Y(y) = f_X(y ^ 2) * |d(y ^ 2) / dy| = 2y
|
|
u = torch.sqrt(torch.rand(sampling_shape,
|
|
device=face_vertices0.device,
|
|
dtype=face_vertices0.dtype))
|
|
|
|
v = torch.rand(sampling_shape,
|
|
device=face_vertices0.device,
|
|
dtype=face_vertices0.dtype)
|
|
w0 = 1 - u
|
|
w1 = u * (1 - v)
|
|
w2 = u * v
|
|
|
|
points = w0 * face_vertices0 + w1 * face_vertices1 + w2 * face_vertices2
|
|
|
|
features = None
|
|
if face_features is not None:
|
|
face_features0, face_features1, face_features2 = face_features
|
|
features = w0 * face_features0 + w1 * face_features1 + \
|
|
w2 * face_features2
|
|
|
|
return points, features
|
|
|
|
# Modified from https://github.com/NVIDIAGameWorks/kaolin/blob/master/kaolin/ops/mesh/trianglemesh.py#L159
|
|
def sample_points(vertices, faces, num_samples, areas=None, face_features=None):
|
|
r"""Uniformly sample points over the surface of triangle meshes.
|
|
|
|
First, face on which the point is sampled is randomly selected,
|
|
with the probability of selection being proportional to the area of the face.
|
|
then the coordinate on the face is uniformly sampled.
|
|
|
|
If ``face_features`` is defined for the mesh faces,
|
|
the sampled points will be returned with interpolated features as well,
|
|
otherwise, no feature interpolation will occur.
|
|
|
|
Args:
|
|
vertices (torch.Tensor):
|
|
The vertices of the meshes, of shape
|
|
:math:`(\text{batch_size}, \text{num_vertices}, 3)`.
|
|
faces (torch.LongTensor):
|
|
The faces of the mesh, of shape :math:`(\text{num_faces}, 3)`.
|
|
num_samples (int):
|
|
The number of point sampled per mesh.
|
|
Also the number of faces sampled per mesh, and then sample a single point per face.
|
|
areas (torch.Tensor, optional):
|
|
The areas of each face, of shape :math:`(\text{batch_size}, \text{num_faces})`,
|
|
can be preprocessed, for fast on-the-fly sampling,
|
|
will be computed if None (default).
|
|
face_features (torch.Tensor, optional):
|
|
Per-vertex-per-face features, matching ``faces`` order,
|
|
of shape :math:`(\text{batch_size}, \text{num_faces}, 3, \text{feature_dim})`.
|
|
For example:
|
|
|
|
1. Texture uv coordinates would be of shape
|
|
:math:`(\text{batch_size}, \text{num_faces}, 3, 2)`.
|
|
2. RGB color values would be of shape
|
|
:math:`(\text{batch_size}, \text{num_faces}, 3, 3)`.
|
|
|
|
When specified, it is used to interpolate the features for new sampled points.
|
|
|
|
See also:
|
|
:func:`~kaolin.ops.mesh.index_vertices_by_faces` for conversion of features defined per vertex
|
|
and need to be converted to per-vertex-per-face shape of :math:`(\text{num_faces}, 3)`.
|
|
|
|
Returns:
|
|
(torch.Tensor, torch.LongTensor, (optional) torch.Tensor):
|
|
the pointclouds of shape :math:`(\text{batch_size}, \text{num_samples}, 3)`,
|
|
and the indexes of the faces selected,
|
|
of shape :math:`(\text{batch_size}, \text{num_samples})`.
|
|
|
|
If ``face_features`` arg is specified, then the interpolated features of sampled points of shape
|
|
:math:`(\text{batch_size}, \text{num_samples}, \text{feature_dim})` are also returned.
|
|
"""
|
|
if faces.shape[-1] != 3:
|
|
raise NotImplementedError("sample_points is only implemented for triangle meshes")
|
|
faces_0, faces_1, faces_2 = torch.split(faces, 1, dim=1) # (num_faces, 3) -> tuple of (num_faces,)
|
|
face_v_0 = torch.index_select(vertices, 1, faces_0.reshape(-1)) # (batch_size, num_faces, 3)
|
|
face_v_1 = torch.index_select(vertices, 1, faces_1.reshape(-1)) # (batch_size, num_faces, 3)
|
|
face_v_2 = torch.index_select(vertices, 1, faces_2.reshape(-1)) # (batch_size, num_faces, 3)
|
|
|
|
if areas is None:
|
|
areas = _base_face_areas(face_v_0, face_v_1, face_v_2).squeeze(-1)
|
|
face_dist = torch.distributions.Categorical(areas)
|
|
face_choices = face_dist.sample([num_samples]).transpose(0, 1)
|
|
_face_choices = face_choices.unsqueeze(-1).repeat(1, 1, 3)
|
|
v0 = torch.gather(face_v_0, 1, _face_choices) # (batch_size, num_samples, 3)
|
|
v1 = torch.gather(face_v_1, 1, _face_choices) # (batch_size, num_samples, 3)
|
|
v2 = torch.gather(face_v_2, 1, _face_choices) # (batch_size, num_samples, 3)
|
|
face_vertices_choices = (v0, v1, v2)
|
|
|
|
# UV coordinates are available, make sure to calculate them for sampled points as well
|
|
face_features_choices = None
|
|
if face_features is not None:
|
|
feat_dim = face_features.shape[-1]
|
|
# (num_faces, 3) -> tuple of (num_faces,)
|
|
_face_choices = face_choices[..., None, None].repeat(1, 1, 3, feat_dim)
|
|
face_features_choices = torch.gather(face_features, 1, _face_choices)
|
|
face_features_choices = tuple(
|
|
tmp_feat.squeeze(2) for tmp_feat in torch.split(face_features_choices, 1, dim=2))
|
|
|
|
points, point_features = _base_sample_points_selected_faces(
|
|
face_vertices_choices, face_features_choices)
|
|
|
|
if point_features is not None:
|
|
return points, face_choices, point_features
|
|
else:
|
|
return points, face_choices
|
|
|
|
def poisson_mesh_reconstruction(points, normals=None):
|
|
# points/normals: [N, 3] np.ndarray
|
|
|
|
import open3d as o3d
|
|
|
|
pcd = o3d.geometry.PointCloud()
|
|
pcd.points = o3d.utility.Vector3dVector(points)
|
|
|
|
# outlier removal
|
|
pcd, ind = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=10)
|
|
|
|
# normals
|
|
if normals is None:
|
|
pcd.estimate_normals()
|
|
else:
|
|
pcd.normals = o3d.utility.Vector3dVector(normals[ind])
|
|
|
|
# visualize
|
|
o3d.visualization.draw_geometries([pcd], point_show_normal=False)
|
|
|
|
mesh, densities = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(
|
|
pcd, depth=9
|
|
)
|
|
vertices_to_remove = densities < np.quantile(densities, 0.1)
|
|
mesh.remove_vertices_by_mask(vertices_to_remove)
|
|
|
|
# visualize
|
|
o3d.visualization.draw_geometries([mesh])
|
|
|
|
vertices = np.asarray(mesh.vertices)
|
|
triangles = np.asarray(mesh.triangles)
|
|
|
|
print(
|
|
f"[INFO] poisson mesh reconstruction: {points.shape} --> {vertices.shape} / {triangles.shape}"
|
|
)
|
|
|
|
return vertices, triangles
|
|
|
|
|
|
def decimate_mesh(
|
|
verts, faces, target, backend="pymeshlab", remesh=False, optimalplacement=True
|
|
):
|
|
# optimalplacement: default is True, but for flat mesh must turn False to prevent spike artifect.
|
|
|
|
_ori_vert_shape = verts.shape
|
|
_ori_face_shape = faces.shape
|
|
|
|
if backend == "pyfqmr":
|
|
import pyfqmr
|
|
|
|
solver = pyfqmr.Simplify()
|
|
solver.setMesh(verts, faces)
|
|
solver.simplify_mesh(target_count=target, preserve_border=False, verbose=False)
|
|
verts, faces, normals = solver.getMesh()
|
|
else:
|
|
m = pml.Mesh(verts, faces)
|
|
ms = pml.MeshSet()
|
|
ms.add_mesh(m, "mesh") # will copy!
|
|
|
|
# filters
|
|
# ms.meshing_decimation_clustering(threshold=pml.Percentage(1))
|
|
ms.meshing_decimation_quadric_edge_collapse(
|
|
targetfacenum=int(target), optimalplacement=optimalplacement
|
|
)
|
|
|
|
if remesh:
|
|
# ms.apply_coord_taubin_smoothing()
|
|
ms.meshing_isotropic_explicit_remeshing(
|
|
iterations=3, targetlen=pml.Percentage(1)
|
|
)
|
|
|
|
# extract mesh
|
|
m = ms.current_mesh()
|
|
verts = m.vertex_matrix()
|
|
faces = m.face_matrix()
|
|
|
|
print(
|
|
f"[INFO] mesh decimation: {_ori_vert_shape} --> {verts.shape}, {_ori_face_shape} --> {faces.shape}"
|
|
)
|
|
|
|
return verts, faces
|
|
|
|
|
|
def clean_mesh(
|
|
verts,
|
|
faces,
|
|
v_pct=1,
|
|
min_f=64,
|
|
min_d=20,
|
|
repair=True,
|
|
remesh=True,
|
|
remesh_size=0.01,
|
|
):
|
|
# verts: [N, 3]
|
|
# faces: [N, 3]
|
|
|
|
_ori_vert_shape = verts.shape
|
|
_ori_face_shape = faces.shape
|
|
|
|
m = pml.Mesh(verts, faces)
|
|
ms = pml.MeshSet()
|
|
ms.add_mesh(m, "mesh") # will copy!
|
|
|
|
# filters
|
|
ms.meshing_remove_unreferenced_vertices() # verts not refed by any faces
|
|
|
|
if v_pct > 0:
|
|
ms.meshing_merge_close_vertices(
|
|
threshold=pml.Percentage(v_pct)
|
|
) # 1/10000 of bounding box diagonal
|
|
|
|
ms.meshing_remove_duplicate_faces() # faces defined by the same verts
|
|
ms.meshing_remove_null_faces() # faces with area == 0
|
|
|
|
if min_d > 0:
|
|
ms.meshing_remove_connected_component_by_diameter(
|
|
mincomponentdiag=pml.Percentage(min_d)
|
|
)
|
|
|
|
if min_f > 0:
|
|
ms.meshing_remove_connected_component_by_face_number(mincomponentsize=min_f)
|
|
|
|
if repair:
|
|
# ms.meshing_remove_t_vertices(method=0, threshold=40, repeat=True)
|
|
ms.meshing_repair_non_manifold_edges(method=0)
|
|
ms.meshing_repair_non_manifold_vertices(vertdispratio=0)
|
|
|
|
if remesh:
|
|
# ms.apply_coord_taubin_smoothing()
|
|
ms.meshing_isotropic_explicit_remeshing(
|
|
iterations=3, targetlen=pml.AbsoluteValue(remesh_size)
|
|
)
|
|
|
|
# extract mesh
|
|
m = ms.current_mesh()
|
|
verts = m.vertex_matrix()
|
|
faces = m.face_matrix()
|
|
|
|
print(
|
|
f"[INFO] mesh cleaning: {_ori_vert_shape} --> {verts.shape}, {_ori_face_shape} --> {faces.shape}"
|
|
)
|
|
|
|
return verts, faces
|
|
|
|
def read_gs_ply(plydata):
|
|
xyz = np.stack((np.asarray(plydata.elements[0]["x"]),
|
|
np.asarray(plydata.elements[0]["y"]),
|
|
np.asarray(plydata.elements[0]["z"])), axis=1)
|
|
opacities = np.asarray(plydata.elements[0]["opacity"])[..., np.newaxis]
|
|
|
|
features_dc = np.zeros((xyz.shape[0], 3, 1))
|
|
features_dc[:, 0, 0] = np.asarray(plydata.elements[0]["f_dc_0"])
|
|
features_dc[:, 1, 0] = np.asarray(plydata.elements[0]["f_dc_1"])
|
|
features_dc[:, 2, 0] = np.asarray(plydata.elements[0]["f_dc_2"])
|
|
|
|
extra_f_names = [p.name for p in plydata.elements[0].properties if p.name.startswith("f_rest_")]
|
|
#assert len(extra_f_names)!=3*(max_sh_degree + 1) ** 2 - 3:
|
|
max_sh_degree = int(((len(extra_f_names) + 3) / 3) ** 0.5 - 1)
|
|
features_extra = np.zeros((xyz.shape[0], len(extra_f_names)))
|
|
for idx, attr_name in enumerate(extra_f_names):
|
|
features_extra[:, idx] = np.asarray(plydata.elements[0][attr_name])
|
|
# Reshape (P,F*SH_coeffs) to (P, F, SH_coeffs except DC)
|
|
features_extra = features_extra.reshape((features_extra.shape[0], 3, (max_sh_degree + 1) ** 2 - 1))
|
|
|
|
scale_names = [p.name for p in plydata.elements[0].properties if p.name.startswith("scale_")]
|
|
scales = np.zeros((xyz.shape[0], len(scale_names)))
|
|
for idx, attr_name in enumerate(scale_names):
|
|
scales[:, idx] = np.asarray(plydata.elements[0][attr_name])
|
|
|
|
rot_names = [p.name for p in plydata.elements[0].properties if p.name.startswith("rot")]
|
|
rots = np.zeros((xyz.shape[0], len(rot_names)))
|
|
for idx, attr_name in enumerate(rot_names):
|
|
rots[:, idx] = np.asarray(plydata.elements[0][attr_name])
|
|
|
|
return xyz, features_dc, features_extra, opacities, scales, rots
|
|
|
|
def ply_to_points_cloud(plydata):
|
|
xyz, features_dc, features_extra, opacities, scales, rots = read_gs_ply(plydata)
|
|
features_dc = np.transpose(features_dc, (0, 2, 1)) # equivalent of torch.transpose(features_dc, 1, 2)
|
|
features_extra = np.transpose(features_extra, (0, 2, 1))
|
|
shs = np.concatenate((features_dc, features_extra), axis=1)
|
|
normals = np.zeros_like(xyz)
|
|
pcd = PointCloud(points=xyz, colors=SH2RGB(shs), normals=normals)
|
|
return pcd |