| import torch |
| import io |
| import numpy as np |
| from pathlib import Path |
| import re |
| import trimesh |
| import imageio |
|
|
|
|
| def to_numpy(*args): |
| def convert(a): |
| if isinstance(a, torch.Tensor): |
| return a.detach().cpu().numpy() |
| assert a is None or isinstance(a, np.ndarray) |
| return a |
|
|
| return convert(args[0]) if len(args) == 1 else tuple(convert(a) for a in args) |
|
|
|
|
| def save_obj(vertices: torch.Tensor, faces: torch.Tensor, filename: Path): |
| filename = Path(filename) |
|
|
| bytes_io = io.BytesIO() |
| np.savetxt(bytes_io, vertices.detach().cpu().numpy(), "v %.4f %.4f %.4f") |
| np.savetxt(bytes_io, faces.cpu().numpy() + 1, "f %d %d %d") |
|
|
| obj_path = filename.with_suffix(".obj") |
| with open(obj_path, "w") as file: |
| file.write(bytes_io.getvalue().decode("UTF-8")) |
|
|
|
|
| def load_obj(filename: Path, device="cuda") -> tuple[torch.Tensor, torch.Tensor]: |
| filename = Path(filename) |
| obj_path = filename.with_suffix(".obj") |
| with open(obj_path) as file: |
| obj_text = file.read() |
| num = r"([0-9\.\-eE]+)" |
| v = re.findall(f"(v {num} {num} {num})", obj_text) |
| vertices = np.array(v)[:, 1:].astype(np.float32) |
| all_faces = [] |
| f = re.findall(f"(f {num} {num} {num})", obj_text) |
| if f: |
| all_faces.append(np.array(f)[:, 1:].astype(np.long).reshape(-1, 3, 1)[..., :1]) |
| f = re.findall(f"(f {num}/{num} {num}/{num} {num}/{num})", obj_text) |
| if f: |
| all_faces.append(np.array(f)[:, 1:].astype(np.long).reshape(-1, 3, 2)[..., :2]) |
| f = re.findall( |
| f"(f {num}/{num}/{num} {num}/{num}/{num} {num}/{num}/{num})", obj_text |
| ) |
| if f: |
| all_faces.append(np.array(f)[:, 1:].astype(np.long).reshape(-1, 3, 3)[..., :2]) |
| f = re.findall(f"(f {num}//{num} {num}//{num} {num}//{num})", obj_text) |
| if f: |
| all_faces.append(np.array(f)[:, 1:].astype(np.long).reshape(-1, 3, 2)[..., :1]) |
| all_faces = np.concatenate(all_faces, axis=0) |
| all_faces -= 1 |
| faces = all_faces[:, :, 0] |
|
|
| vertices = torch.tensor(vertices, dtype=torch.float32, device=device) |
| faces = torch.tensor(faces, dtype=torch.long, device=device) |
|
|
| return vertices, faces |
|
|
|
|
| def save_ply( |
| filename: Path, |
| vertices: torch.Tensor, |
| faces: torch.Tensor, |
| vertex_colors: torch.Tensor = None, |
| vertex_normals: torch.Tensor = None, |
| ): |
|
|
| filename = Path(filename).with_suffix(".ply") |
| vertices, faces, vertex_colors = to_numpy(vertices, faces, vertex_colors) |
| assert ( |
| np.all(np.isfinite(vertices)) |
| and faces.min() == 0 |
| and faces.max() == vertices.shape[0] - 1 |
| ) |
|
|
| header = "ply\nformat ascii 1.0\n" |
|
|
| header += "element vertex " + str(vertices.shape[0]) + "\n" |
| header += "property double x\n" |
| header += "property double y\n" |
| header += "property double z\n" |
|
|
| if vertex_normals is not None: |
| header += "property double nx\n" |
| header += "property double ny\n" |
| header += "property double nz\n" |
|
|
| if vertex_colors is not None: |
| assert vertex_colors.shape[0] == vertices.shape[0] |
| color = (vertex_colors * 255).astype(np.uint8) |
| header += "property uchar red\n" |
| header += "property uchar green\n" |
| header += "property uchar blue\n" |
|
|
| header += "element face " + str(faces.shape[0]) + "\n" |
| header += "property list int int vertex_indices\n" |
| header += "end_header\n" |
|
|
| with open(filename, "w") as file: |
| file.write(header) |
|
|
| for i in range(vertices.shape[0]): |
| s = f"{vertices[i,0]} {vertices[i,1]} {vertices[i,2]}" |
| if vertex_normals is not None: |
| s += f" {vertex_normals[i,0]} {vertex_normals[i,1]} {vertex_normals[i,2]}" |
| if vertex_colors is not None: |
| s += f" {color[i,0]:03d} {color[i,1]:03d} {color[i,2]:03d}" |
| file.write(s + "\n") |
|
|
| for i in range(faces.shape[0]): |
| file.write(f"3 {faces[i,0]} {faces[i,1]} {faces[i,2]}\n") |
| full_verts = vertices[faces] |
|
|
|
|
| def save_images( |
| images: torch.Tensor, |
| dir: Path, |
| ): |
| dir = Path(dir) |
| dir.mkdir(parents=True, exist_ok=True) |
| for i in range(images.shape[0]): |
| imageio.imwrite( |
| dir / f"{i:02d}.png", |
| (images.detach()[i, :, :, :3] * 255) |
| .clamp(max=255) |
| .type(torch.uint8) |
| .cpu() |
| .numpy(), |
| ) |
|
|
|
|
| def normalize_vertices( |
| vertices: torch.Tensor, |
| ): |
| """shift and resize mesh to fit into a unit sphere""" |
| vertices -= (vertices.min(dim=0)[0] + vertices.max(dim=0)[0]) / 2 |
| vertices /= torch.norm(vertices, dim=-1).max() |
| return vertices |
|
|
|
|
| def laplacian(num_verts: int, edges: torch.Tensor) -> torch.Tensor: |
| """create sparse Laplacian matrix""" |
| V = num_verts |
| E = edges.shape[0] |
|
|
| |
| idx = torch.cat([edges, edges.fliplr()], dim=0).type(torch.long).T |
| ones = torch.ones(2 * E, dtype=torch.float32, device=edges.device) |
| A = torch.sparse.FloatTensor(idx, ones, (V, V)) |
|
|
| |
| deg = torch.sparse.sum(A, dim=1).to_dense() |
| idx = torch.arange(V, device=edges.device) |
| idx = torch.stack([idx, idx], dim=0) |
| D = torch.sparse.FloatTensor(idx, deg, (V, V)) |
|
|
| return D - A |
|
|
|
|
| def _translation(x, y, z, device): |
| return torch.tensor( |
| [[1.0, 0, 0, x], [0, 1, 0, y], [0, 0, 1, z], [0, 0, 0, 1]], device=device |
| ) |
|
|
|
|
| def _projection(r, device, l=None, t=None, b=None, n=1.0, f=50.0, flip_y=True): |
| if l is None: |
| l = -r |
| if t is None: |
| t = r |
| if b is None: |
| b = -t |
| p = torch.zeros([4, 4], device=device) |
| p[0, 0] = 2 * n / (r - l) |
| p[0, 2] = (r + l) / (r - l) |
| p[1, 1] = 2 * n / (t - b) * (-1 if flip_y else 1) |
| p[1, 2] = (t + b) / (t - b) |
| p[2, 2] = -(f + n) / (f - n) |
| p[2, 3] = -(2 * f * n) / (f - n) |
| p[3, 2] = -1 |
| return p |
|
|
|
|
| def make_star_cameras( |
| az_count, |
| pol_count, |
| distance: float = 10.0, |
| r=None, |
| image_size=[512, 512], |
| device="cuda", |
| ): |
| if r is None: |
| r = 1 / distance |
| A = az_count |
| P = pol_count |
| C = A * P |
|
|
| phi = torch.arange(0, A) * (2 * torch.pi / A) |
| phi_rot = torch.eye(3, device=device)[None, None].expand(A, 1, 3, 3).clone() |
| phi_rot[:, 0, 2, 2] = phi.cos() |
| phi_rot[:, 0, 2, 0] = -phi.sin() |
| phi_rot[:, 0, 0, 2] = phi.sin() |
| phi_rot[:, 0, 0, 0] = phi.cos() |
|
|
| theta = torch.arange(1, P + 1) * (torch.pi / (P + 1)) - torch.pi / 2 |
| theta_rot = torch.eye(3, device=device)[None, None].expand(1, P, 3, 3).clone() |
| theta_rot[0, :, 1, 1] = theta.cos() |
| theta_rot[0, :, 1, 2] = -theta.sin() |
| theta_rot[0, :, 2, 1] = theta.sin() |
| theta_rot[0, :, 2, 2] = theta.cos() |
|
|
| mv = torch.empty((C, 4, 4), device=device) |
| mv[:] = torch.eye(4, device=device) |
| mv[:, :3, :3] = (theta_rot @ phi_rot).reshape(C, 3, 3) |
| mv = _translation(0, 0, -distance, device) @ mv |
|
|
| return mv, _projection(r, device) |
|
|
|
|
| def make_sphere( |
| level: int = 2, radius=1.0, device="cuda" |
| ) -> tuple[torch.Tensor, torch.Tensor]: |
| sphere = trimesh.creation.icosphere(subdivisions=level, radius=1.0, color=None) |
| vertices = ( |
| torch.tensor(sphere.vertices, device=device, dtype=torch.float32) * radius |
| ) |
| faces = torch.tensor(sphere.faces, device=device, dtype=torch.long) |
| return vertices, faces |
|
|