|
import argparse |
|
import logging |
|
|
|
import numpy as np |
|
import torch |
|
import trimesh |
|
|
|
from cube3d.inference.utils import load_config, load_model_weights, parse_structured |
|
from cube3d.model.autoencoder.one_d_autoencoder import OneDAutoEncoder |
|
|
|
MESH_SCALE = 0.96 |
|
|
|
|
|
def rescale(vertices: np.ndarray, mesh_scale: float = MESH_SCALE) -> np.ndarray: |
|
"""Rescale the vertices to a cube, e.g., [-1, -1, -1] to [1, 1, 1] when mesh_scale=1.0""" |
|
vertices = vertices |
|
bbmin = vertices.min(0) |
|
bbmax = vertices.max(0) |
|
center = (bbmin + bbmax) * 0.5 |
|
scale = 2.0 * mesh_scale / (bbmax - bbmin).max() |
|
vertices = (vertices - center) * scale |
|
return vertices |
|
|
|
|
|
def load_scaled_mesh(file_path: str) -> trimesh.Trimesh: |
|
""" |
|
Load a mesh and scale it to a unit cube, and clean the mesh. |
|
Parameters: |
|
file_obj: str | IO |
|
file_type: str |
|
Returns: |
|
mesh: trimesh.Trimesh |
|
""" |
|
mesh: trimesh.Trimesh = trimesh.load(file_path, force="mesh") |
|
mesh.remove_infinite_values() |
|
mesh.update_faces(mesh.nondegenerate_faces()) |
|
mesh.update_faces(mesh.unique_faces()) |
|
mesh.remove_unreferenced_vertices() |
|
if len(mesh.vertices) == 0 or len(mesh.faces) == 0: |
|
raise ValueError("Mesh has no vertices or faces after cleaning") |
|
mesh.vertices = rescale(mesh.vertices) |
|
return mesh |
|
|
|
|
|
def load_and_process_mesh(file_path: str, n_samples: int = 8192): |
|
""" |
|
Loads a 3D mesh from the specified file path, samples points from its surface, |
|
and processes the sampled points into a point cloud with normals. |
|
Args: |
|
file_path (str): The file path to the 3D mesh file. |
|
n_samples (int, optional): The number of points to sample from the mesh surface. Defaults to 8192. |
|
Returns: |
|
torch.Tensor: A tensor of shape (1, n_samples, 6) containing the processed point cloud. |
|
Each point consists of its 3D position (x, y, z) and its normal vector (nx, ny, nz). |
|
""" |
|
|
|
mesh = load_scaled_mesh(file_path) |
|
positions, face_indices = trimesh.sample.sample_surface(mesh, n_samples) |
|
normals = mesh.face_normals[face_indices] |
|
point_cloud = np.concatenate( |
|
[positions, normals], axis=1 |
|
) |
|
point_cloud = torch.from_numpy(point_cloud.reshape(1, -1, 6)).float() |
|
return point_cloud |
|
|
|
|
|
@torch.inference_mode() |
|
def run_shape_decode( |
|
shape_model: OneDAutoEncoder, |
|
output_ids: torch.Tensor, |
|
resolution_base: float = 8.0, |
|
chunk_size: int = 100_000, |
|
): |
|
""" |
|
Decodes the shape from the given output IDs and extracts the geometry. |
|
Args: |
|
shape_model (OneDAutoEncoder): The shape model. |
|
output_ids (torch.Tensor): The tensor containing the output IDs. |
|
resolution_base (float, optional): The base resolution for geometry extraction. Defaults to 8.43. |
|
chunk_size (int, optional): The chunk size for processing. Defaults to 100,000. |
|
Returns: |
|
tuple: A tuple containing the vertices and faces of the mesh. |
|
""" |
|
shape_ids = ( |
|
output_ids[:, : shape_model.cfg.num_encoder_latents, ...] |
|
.clamp_(0, shape_model.cfg.num_codes - 1) |
|
.view(-1, shape_model.cfg.num_encoder_latents) |
|
) |
|
latents = shape_model.decode_indices(shape_ids) |
|
mesh_v_f, _ = shape_model.extract_geometry( |
|
latents, |
|
resolution_base=resolution_base, |
|
chunk_size=chunk_size, |
|
use_warp=True, |
|
) |
|
return mesh_v_f |
|
|
|
|
|
if __name__ == "__main__": |
|
parser = argparse.ArgumentParser( |
|
description="cube shape encode and decode example script" |
|
) |
|
parser.add_argument( |
|
"--mesh-path", |
|
type=str, |
|
required=True, |
|
help="Path to the input mesh file.", |
|
) |
|
parser.add_argument( |
|
"--config-path", |
|
type=str, |
|
default="cube3d/configs/open_model.yaml", |
|
help="Path to the configuration YAML file.", |
|
) |
|
parser.add_argument( |
|
"--shape-ckpt-path", |
|
type=str, |
|
required=True, |
|
help="Path to the shape encoder/decoder checkpoint file.", |
|
) |
|
parser.add_argument( |
|
"--recovered-mesh-path", |
|
type=str, |
|
default="recovered_mesh.obj", |
|
help="Path to save the recovered mesh file.", |
|
) |
|
args = parser.parse_args() |
|
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") |
|
logging.info(f"Using device: {device}") |
|
|
|
cfg = load_config(args.config_path) |
|
|
|
shape_model = OneDAutoEncoder( |
|
parse_structured(OneDAutoEncoder.Config, cfg.shape_model) |
|
) |
|
load_model_weights( |
|
shape_model, |
|
args.shape_ckpt_path, |
|
) |
|
shape_model = shape_model.eval().to(device) |
|
point_cloud = load_and_process_mesh(args.mesh_path) |
|
output = shape_model.encode(point_cloud.to(device)) |
|
indices = output[3]["indices"] |
|
print("Got the following shape indices:") |
|
print(indices) |
|
print("Indices shape: ", indices.shape) |
|
mesh_v_f = run_shape_decode(shape_model, indices) |
|
vertices, faces = mesh_v_f[0][0], mesh_v_f[0][1] |
|
mesh = trimesh.Trimesh(vertices=vertices, faces=faces) |
|
mesh.export(args.recovered_mesh_path) |
|
|