| | import torch |
| | import io |
| | import numpy as np |
| | from pathlib import Path |
| | import re |
| | import trimesh |
| | import imageio |
| | import os |
| | from scipy.spatial.transform import Rotation as R |
| | 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, |
| | faces, |
| | filename:Path, |
| | colors=None, |
| | ): |
| | filename = Path(filename) |
| |
|
| | bytes_io = io.BytesIO() |
| | if colors is not None: |
| | vertices = torch.cat((vertices, colors),dim=-1) |
| | np.savetxt(bytes_io, vertices.detach().cpu().numpy(), 'v %.4f %.4f %.4f %.4f %.4f %.4f') |
| | else: |
| | 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 save_glb( |
| | filename, |
| | v_pos, |
| | t_pos_idx, |
| | v_nrm=None, |
| | v_tex=None, |
| | t_tex_idx=None, |
| | v_rgb=None, |
| | ) -> str: |
| | |
| | mesh = trimesh.Trimesh( |
| | vertices=v_pos, faces=t_pos_idx, vertex_normals=v_nrm, vertex_colors=v_rgb |
| | ) |
| | |
| | if v_tex is not None: |
| | mesh.visual = trimesh.visual.TextureVisuals(uv=v_tex) |
| | mesh.export(filename) |
| | |
| |
|
| | def load_obj( |
| | filename:Path, |
| | device='cuda', |
| | load_color=False |
| | ) -> 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]+)" |
| | if load_color: |
| | v = re.findall(f"(v {num} {num} {num} {num} {num} {num})",obj_text) |
| | else: |
| | 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.int32).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.int32).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.int32).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.int32).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) |
| | if images.shape[-1]==1: |
| | images = images.repeat(1,1,1,3) |
| | 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_scene(vertices): |
| | bbox_min, bbox_max = vertices.min(axis=0)[0], vertices.max(axis=0)[0] |
| | offset = -(bbox_min + bbox_max) / 2 |
| | vertices = vertices + offset |
| | |
| | |
| | dxyz = bbox_max - bbox_min |
| | dist = torch.sqrt(dxyz[0]**2+ dxyz[1]**2+dxyz[2]**2) |
| | scale = 1. / dist |
| | |
| | vertices *= scale |
| | return vertices |
| | 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, x], |
| | [0, 1, 0, y], |
| | [0, 0, 1, z], |
| | [0, 0, 0, 1]],device=device) |
| |
|
| |
|
| | def make_round_views(view_nums, scale=2., device='cuda'): |
| | w2c = [] |
| | ortho_scale = scale/2 |
| | projection = get_ortho_projection_matrix(-ortho_scale, ortho_scale, -ortho_scale, ortho_scale, 0.1, 100) |
| | for i in reversed(range(view_nums)): |
| | tmp = np.eye(4) |
| | rot = R.from_euler('xyz', [0, 360/view_nums*i-180, 0], degrees=True).as_matrix() |
| | rot[:, 2] *= -1 |
| | tmp[:3, :3] = rot |
| | tmp[2, 3] = -1.8 |
| | w2c.append(tmp) |
| | w2c = torch.from_numpy(np.stack(w2c, 0)).float().to(device=device) |
| | projection = torch.from_numpy(projection).float().to(device=device) |
| | return w2c, projection |
| |
|
| | def make_star_views(az_degs, pol_degs, scale=2., device='cuda'): |
| | w2c = [] |
| | ortho_scale = scale/2 |
| | projection = get_ortho_projection_matrix(-ortho_scale, ortho_scale, -ortho_scale, ortho_scale, 0.1, 100) |
| | for pol in pol_degs: |
| | for az in az_degs: |
| | tmp = np.eye(4) |
| | rot = R.from_euler('xyz', [0, az-180, 0], degrees=True).as_matrix() |
| | rot[:, 2] *= -1 |
| | rot_z = R.from_euler('xyz', [pol, 0, 0], degrees=True).as_matrix() |
| | rot = rot_z @ rot |
| | tmp[:3, :3] = rot |
| | tmp[2, 3] = -1.8 |
| | w2c.append(tmp) |
| | w2c = torch.from_numpy(np.stack(w2c, 0)).float().to(device=device) |
| | projection = torch.from_numpy(projection).float().to(device=device) |
| | return w2c, projection |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | def get_ortho_projection_matrix(left, right, bottom, top, near, far): |
| | projection_matrix = np.zeros((4, 4), dtype=np.float32) |
| |
|
| | projection_matrix[0, 0] = 2.0 / (right - left) |
| | projection_matrix[1, 1] = -2.0 / (top - bottom) |
| | projection_matrix[2, 2] = -2.0 / (far - near) |
| |
|
| | projection_matrix[0, 3] = -(right + left) / (right - left) |
| | projection_matrix[1, 3] = -(top + bottom) / (top - bottom) |
| | projection_matrix[2, 3] = -(far + near) / (far - near) |
| | projection_matrix[3, 3] = 1.0 |
| |
|
| | return projection_matrix |
| |
|
| | 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 get_perspective_projection_matrix(fov, aspect=1.0, near=0.1, far=100.0): |
| | tan_half_fovy = torch.tan(torch.deg2rad(fov/2)) |
| | projection_matrix = torch.zeros(4, 4) |
| | projection_matrix[0, 0] = 1 / (aspect * tan_half_fovy) |
| | projection_matrix[1, 1] = -1 / tan_half_fovy |
| | projection_matrix[2, 2] = -(far + near) / (far - near) |
| | projection_matrix[2, 3] = -2 * far * near / (far - near) |
| | projection_matrix[3, 2] = -1 |
| |
|
| | def make_sparse_camera(cam_path, scale=4., views=None, device='cuda', mode='ortho'): |
| |
|
| | if mode == 'ortho': |
| | ortho_scale = scale/2 |
| | projection = get_ortho_projection_matrix(-ortho_scale, ortho_scale, -ortho_scale, ortho_scale, 0.1, 100) |
| | else: |
| | npy_data = np.load(os.path.join(cam_path, f'{i:03d}.npy'), allow_pickle=True).item() |
| | fov = npy_data['fov'] |
| | projection = get_perspective_projection_matrix(fov, aspect=1.0, near=0.1, far=100.0) |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | ''' |
| | world : |
| | z |
| | | |
| | |____y |
| | / |
| | / |
| | x |
| | camera:(opencv) |
| | z |
| | / |
| | /____x |
| | | |
| | | |
| | y |
| | ''' |
| | if views is None: |
| | views = [0, 1, 2, 4, 6, 7] |
| | w2c = [] |
| | for i in views: |
| | npy_data = np.load(os.path.join(cam_path, f'{i:03d}.npy'), allow_pickle=True).item() |
| | w2c_cv = npy_data['extrinsic'] |
| | w2c_cv = np.concatenate([w2c_cv, np.array([[0, 0, 0, 1]])], axis=0) |
| | c2w_cv = np.linalg.inv(w2c_cv) |
| |
|
| | c2w_gl = c2w_cv[[1, 2, 0, 3], :] |
| | c2w_gl[:3, 1:3] *= -1 |
| | w2c_gl = np.linalg.inv(c2w_gl) |
| | w2c.append(w2c_gl) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | w2c = torch.from_numpy(np.stack(w2c, 0)).float().to(device=device) |
| | projection = torch.from_numpy(projection).float().to(device=device) |
| | return w2c, projection |
| | |
| | def make_sphere(level:int=2,radius=1.,device='cuda') -> tuple[torch.Tensor,torch.Tensor]: |
| | sphere = trimesh.creation.icosphere(subdivisions=level, radius=radius, color=np.array([0.5, 0.5, 0.5])) |
| | vertices = torch.tensor(sphere.vertices, device=device, dtype=torch.float32) * radius |
| | |
| | |
| | |
| | faces = torch.tensor(sphere.faces, device=device, dtype=torch.long) |
| | colors = torch.tensor(sphere.visual.vertex_colors[..., :3], device=device, dtype=torch.float32) |
| | return vertices, faces, colors |