4 minute read

Open3D์—์„œ TriangleMesh ์ง์ ‘ ์ƒ์„ฑํ•˜๊ธฐ

TriangleMesh ๊ฐ์ฒด๋ฅผ ์ง์ ‘ ์ƒ์„ฑํ•˜์—ฌ 3D ๋ฐ์ดํ„ฐ๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Open3D์—์„œ TriangleMesh๋ฅผ ์ง์ ‘ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์„ ๋‹จ๊ณ„๋ณ„๋กœ ์„ค๋ช…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์ด ๊ณผ์ •์—์„œ๋Š” ์ •์ (vertices)๊ณผ ์‚ผ๊ฐํ˜•(triangles)์„ ๋ช…์‹œ์ ์œผ๋กœ ์ •์˜ํ•˜์—ฌ ๋ฉ”์‰ฌ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

Open3D ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜

๋จผ์ € Open3D ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์„ค์น˜๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์„ค์น˜ํ•˜์ง€ ์•Š์•˜๋‹ค๋ฉด ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„ค์น˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

pip install open3d

TriangleMesh ๊ฐ์ฒด ์ƒ์„ฑ

๋‹ค์Œ์€ ์ •์ ๊ณผ ์‚ผ๊ฐํ˜•์„ ์ง์ ‘ ์ •์˜ํ•˜์—ฌ TriangleMesh ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์‹œ๊ฐํ™”ํ•˜๋Š” ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

import open3d as o3d
import numpy as np

# ์ •์  (vertices) ์ •์˜
vertices = np.array([
    [0, 0, 0],  # Vertex 0
    [1, 0, 0],  # Vertex 1
    [0, 1, 0],  # Vertex 2
    [0, 0, 1]   # Vertex 3
])

# ์‚ผ๊ฐํ˜• (triangles) ์ •์˜
triangles = np.array([
    [0, 1, 2],  # Triangle 0
    [0, 1, 3],  # Triangle 1
    [0, 2, 3],  # Triangle 2
    [1, 2, 3]   # Triangle 3
])

# TriangleMesh ๊ฐ์ฒด ์ƒ์„ฑ
mesh = o3d.geometry.TriangleMesh()
mesh.vertices = o3d.utility.Vector3dVector(vertices)
mesh.triangles = o3d.utility.Vector3iVector(triangles)

# ๋ฉ”์‰ฌ์— ์ƒ‰์ƒ ์ถ”๊ฐ€ (์„ ํƒ ์‚ฌํ•ญ)
mesh.vertex_colors = o3d.utility.Vector3dVector(np.array([
    [1, 0, 0],  # Color for Vertex 0
    [0, 1, 0],  # Color for Vertex 1
    [0, 0, 1],  # Color for Vertex 2
    [1, 1, 0]   # Color for Vertex 3
]))

# ๋ฉ”์‰ฌ ์‹œ๊ฐํ™”
o3d.visualization.draw_geometries([mesh])
  • ์ •์  ์ •์˜:์ •์ ์€ 3์ฐจ์› ๊ณต๊ฐ„์˜ ์ขŒํ‘œ๋กœ ์ •์˜๋ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” ๋„ค ๊ฐœ์˜ ์ •์ ์„ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ์‚ผ๊ฐํ˜• ์ •์˜:์‚ผ๊ฐํ˜•์€ ์ •์ ์˜ ์ธ๋ฑ์Šค๋กœ ์ •์˜๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, [0, 1, 2]๋Š” ์ฒซ ๋ฒˆ์งธ, ๋‘ ๋ฒˆ์งธ, ์„ธ ๋ฒˆ์งธ ์ •์ ์„ ์—ฐ๊ฒฐํ•˜๋Š” ์‚ผ๊ฐํ˜•์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.
  • TriangleMesh ๊ฐ์ฒด ์ƒ์„ฑ:TriangleMesh ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ ํ›„, ์ •์ ๊ณผ ์‚ผ๊ฐํ˜• ๋ฐ์ดํ„ฐ๋ฅผ Vector3dVector์™€ Vector3iVector๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
  • ์ƒ‰์ƒ ์ถ”๊ฐ€ (์„ ํƒ ์‚ฌํ•ญ):๊ฐ ์ •์ ์— ์ƒ‰์ƒ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์‹œ๊ฐํ™”ํ•  ๋•Œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • ์‹œ๊ฐํ™”:draw_geometries ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒ์„ฑํ•œ ๋ฉ”์‰ฌ๋ฅผ ์‹œ๊ฐํ™”ํ•ฉ๋‹ˆ๋‹ค.

์•„๋ž˜์˜ open3d official document ์˜ˆ์ œ์˜ ๊ทธ๋ฆผ์„ ๋ณด๋ฉด vertices์™€ triangle์ด ์–ด๋–ป๊ฒŒ ์ •์˜๋˜์–ด TriangleMesh ๊ฐ์ฒด๋ฅผ ๊ตฌ์„ฑํ•˜๊ฒŒ ๋˜๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

https://www.open3d.org/docs/release/python_api/open3d.utility.Vector3iVector.html

image image

์ด์ œ SuGaR์—์„œ TriangleMesh๋ฅผ ์–ด๋–ป๊ฒŒ ๋ถˆ๋Ÿฌ์˜ค๋Š”์ง€์™€ ์–ด๋–ป๊ฒŒ ์ƒˆ๋กญ๊ฒŒ ์ •์˜ํ•ด์„œ ๋„˜๊ธฐ๋Š”์ง€ ๋ด…์‹œ๋‹ค.

  • ์ €์žฅ๋œ TriangleMesh ๊ฐ์ฒด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฒ•์€ o3d.io.read_triangle_mesh๋กœ ํ•ฉ๋‹ˆ๋‹ค.
# sugar_extractors/refined_mesh.py

# --- Loading coarse mesh ---
o3d_mesh = o3d.io.read_triangle_mesh(sugar_mesh_path)
    
# --- Loading refined SuGaR model ---
checkpoint = torch.load(refined_model_path, map_location=nerfmodel.device)

refined_sugar = SuGaR(
        nerfmodel=nerfmodel,
        points=checkpoint['state_dict']['_points'],
        colors=SH2RGB(checkpoint['state_dict']['_sh_coordinates_dc'][:, 0, :]),
        initialize=False,
        sh_levels=nerfmodel.gaussians.active_sh_degree+1,
        keep_track_of_knn=False,
        knn_to_track=0,
        beta_mode='average',
        surface_mesh_to_bind=o3d_mesh,
        n_gaussians_per_surface_triangle=n_gaussians_per_surface_triangle,
)
refined_sugar.load_state_dict(checkpoint['state_dict'])
refined_sugar.eval()
  • ์ƒˆ๋กœ์šด TriangleMesh ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๋•Œ๋Š”,o3d.geometry.TriangleMesh()๋กœ ์ •์˜ํ•˜๊ณ , ์ด์— vertices, triangles, vertex_normals, vertex_colors๋ฅผ ๋„˜๊ฒจ์ค๋‹ˆ๋‹ค.
# sugar_extractors/refined_mesh.py

new_o3d_mesh = o3d.geometry.TriangleMesh()
new_o3d_mesh.vertices = o3d.utility.Vector3dVector(new_verts.cpu().numpy())
new_o3d_mesh.triangles = o3d.utility.Vector3iVector(new_faces.cpu().numpy())
new_o3d_mesh.vertex_normals = o3d.utility.Vector3dVector(new_normals.cpu().numpy())
new_o3d_mesh.vertex_colors = o3d.utility.Vector3dVector(torch.ones_like(new_verts).cpu().numpy())
            
refined_sugar = SuGaR(
                nerfmodel=nerfmodel,
                points=None,
                colors=None,
                initialize=False,
                sh_levels=nerfmodel.gaussians.active_sh_degree+1,
                keep_track_of_knn=False,
                knn_to_track=0,
                beta_mode='average',
                surface_mesh_to_bind=new_o3d_mesh,
                n_gaussians_per_surface_triangle=refined_sugar.n_gaussians_per_surface_triangle,
                )
  • ์ด๋•Œ, new TriangleMesh์ธ new_o3d_mesh์— ์ €์žฅํ•˜๋Š” vertices, triangles, vertex_normals, vertex_colors๋Š” ์•ž์—์„œ ๋ฏธ๋ฆฌ mesh์—์„œ verts_list(), face_list(), faces_normals_list()๋กœ ๋ถˆ๋Ÿฌ์™€์„œ postprocess๋ฅผ ํ•œ ์ •๋ณด์ž…๋‹ˆ๋‹ค.

image image image

  • ์•„๋ž˜์™€ ๊ฐ™์ด 0๋ฒˆ์งธ mesh๋งŒ verts_list()[0], faces_list()[0], faces_normals_list()์—์„œ ์ธ๋ฑ์‹ฑํ•˜์—ฌ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • new_verts = refined_sugar.surface_mesh.verts_list()[0].detach().clone()
  • new_faces = refined_sugar.surface_mesh.faces_list()[0].detach().clone()
  • new_normals = refined_sugar.surface_mesh.faces_normals_list()[0].detach().clone()
  • postprocess๋ฅผ ํ•˜๋ฉด ์ตœ์ข…์ ์œผ๋กœ face_mask๋Š” ๋‚ด๋ถ€ ์‚ผ๊ฐํ˜•๊ณผ ๊ฒฝ๊ณ„ ์‚ผ๊ฐํ˜•์„ ๊ตฌ๋ถ„ํ•˜๋ฉฐ, ๊ฒฝ๊ณ„ ์‚ผ๊ฐํ˜• ์ค‘์—์„œ ๋ฐ€๋„๊ฐ€ ๋†’์€ ์‚ผ๊ฐํ˜•์„ ๋‹ค์‹œ ํฌํ•จ์‹œํ‚ต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ํ›„์ฒ˜๋ฆฌ๋œ ๋ฉ”์‰ฌ๋Š” ๋ถˆํ•„์š”ํ•œ ๊ฒฝ๊ณ„ ์‚ผ๊ฐํ˜•์ด ์ œ๊ฑฐ๋˜๊ณ , ์ค‘์š”ํ•œ ๊ฒฝ๊ณ„ ์‚ผ๊ฐํ˜•์€ ๋ณต๊ตฌ๋œ ํ˜•ํƒœ๋กœ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

         if postprocess_mesh:
          CONSOLE.print("Postprocessing mesh by removing border triangles with low-opacity gaussians...")
          with torch.no_grad():
              new_verts = refined_sugar.surface_mesh.verts_list()[0].detach().clone()
              new_faces = refined_sugar.surface_mesh.faces_list()[0].detach().clone()
              new_normals = refined_sugar.surface_mesh.faces_normals_list()[0].detach().clone()
                
              # For each face, get the 3 edges
              edges0 = new_faces[..., None, (0,1)].sort(dim=-1)[0]
              edges1 = new_faces[..., None, (1,2)].sort(dim=-1)[0]
              edges2 = new_faces[..., None, (2,0)].sort(dim=-1)[0]
              all_edges = torch.cat([edges0, edges1, edges2], dim=-2)
                
              # We start by identifying the inside faces and border faces
              face_mask = refined_sugar.strengths[..., 0] > -1.
              for i in range(postprocess_iterations):
                  CONSOLE.print("\nStarting postprocessing iteration", i)
                  # We look for edges that appear in the list at least twice (their NN is themselves)
                  edges_neighbors = knn_points(all_edges[face_mask].view(1, -1, 2).float(), all_edges[face_mask].view(1, -1, 2).float(), K=2)
                  # If all edges of a face appear in the list at least twice, then the face is inside the mesh
                  is_inside = (edges_neighbors.dists[0][..., 1].view(-1, 3) < 0.01).all(-1)
                  # We update the mask by removing border faces
                  face_mask[face_mask.clone()] = is_inside
    
              # We then add back border faces with high-density
              face_centers = new_verts[new_faces].mean(-2)
              face_densities = refined_sugar.compute_density(face_centers[~face_mask])
              face_mask[~face_mask.clone()] = face_densities > postprocess_density_threshold
    
              # And we create the new mesh and SuGaR model
              new_faces = new_faces[face_mask]
              new_normals = new_normals[face_mask]
    
              new_scales = refined_sugar._scales.reshape(len(face_mask), -1, 2)[face_mask].view(-1, 2)
              new_quaternions = refined_sugar._quaternions.reshape(len(face_mask), -1, 2)[face_mask].view(-1, 2)
              new_densities = refined_sugar.all_densities.reshape(len(face_mask), -1, 1)[face_mask].view(-1, 1)
              new_sh_coordinates_dc = refined_sugar._sh_coordinates_dc.reshape(len(face_mask), -1, 1, 3)[face_mask].view(-1, 1, 3)
              new_sh_coordinates_rest = refined_sugar._sh_coordinates_rest.reshape(len(face_mask), -1, 15, 3)[face_mask].view(-1, 15, 3)
    
  • density๊ฐ€ ๋†’์€ faces(์‚ผ๊ฐํ˜•)์€ ์–ด๋–ป๊ฒŒ ์ฐพ์„๊นŒ์š”?
  • sugar_scene/sugar_model.py์—์„œ compute_density๋ฅผ ๋ณด๋ฉด ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
# sugar_scene/sugar_model.py

class SuGaR(nn.Module):

...

    def get_gaussians_closest_to_samples(self, x, n_closest_gaussian=None):
        if n_closest_gaussian is None:
            if not hasattr(self, 'knn_to_track'):
                print("Variable knn_to_track not found. Setting it to 16.")
                self.knn_to_track = 16
            n_closest_gaussian = self.knn_to_track
        
        closest_gaussians_idx = knn_points(x[None], self.points[None], K=n_closest_gaussian).idx[0]
        return closest_gaussians_idx
    
    def compute_density(self, x, closest_gaussians_idx=None, density_factor=1., 
                        return_closest_gaussian_opacities=False):
        
        if closest_gaussians_idx is None:
            closest_gaussians_idx = self.get_gaussians_closest_to_samples(x)
        
        # Gather gaussian parameters
        close_gaussian_centers = self.points[closest_gaussians_idx]
        close_gaussian_inv_scaled_rotation = self.get_covariance(
            return_full_matrix=True, return_sqrt=True, inverse_scales=True
            )[closest_gaussians_idx]
        close_gaussian_strengths = self.strengths[closest_gaussians_idx]
        
        # Compute the density field as a sum of local gaussian opacities
        shift = (x[:, None] - close_gaussian_centers)
        warped_shift = close_gaussian_inv_scaled_rotation.transpose(-1, -2) @ shift[..., None]
        neighbor_opacities = (warped_shift[..., 0] * warped_shift[..., 0]).sum(dim=-1).clamp(min=0., max=1e8)
        neighbor_opacities = density_factor * close_gaussian_strengths[..., 0] * torch.exp(-1. / 2 * neighbor_opacities)
        densities = neighbor_opacities.sum(dim=-1)
        
        if return_closest_gaussian_opacities:
            return densities, neighbor_opacities
        else:
            return densities  # Shape is (n_points, )
        
  • ์ตœ์ข…์ ์œผ๋กœ ์ €์žฅํ•  mesh์—๋Š” verts_list(), face_list(), faces_normals_list(), textures.verts_uvs_list(), textures.faces_uvs_list(), textures.maps_padded()๋ฅผ ํฌํ•จ์‹œ์ผœ ์ €์žฅํ•ด์ค๋‹ˆ๋‹ค.
  # Compute texture
  with torch.no_grad():
      verts_uv, faces_uv, texture_img = extract_texture_image_and_uv_from_gaussians(
          refined_sugar, square_size=square_size, n_sh=1, texture_with_gaussian_renders=True)
      
      textures_uv = TexturesUV(
          maps=texture_img[None], #texture_img[None]),
          verts_uvs=verts_uv[None],
          faces_uvs=faces_uv[None],
          sampling_mode='nearest',
          )
      textured_mesh = Meshes(
          verts=[refined_sugar.surface_mesh.verts_list()[0]],   
          faces=[refined_sugar.surface_mesh.faces_list()[0]],
          textures=textures_uv,
          )
  
  CONSOLE.print("Texture extracted.")
  CONSOLE.print("Texture shape:", texture_img.shape)
  
  CONSOLE.print("Saving textured mesh...")
  
  with torch.no_grad():
      save_obj(  
          mesh_save_path,
          verts=textured_mesh.verts_list()[0],
          faces=textured_mesh.faces_list()[0],
          verts_uvs=textured_mesh.textures.verts_uvs_list()[0],
          faces_uvs=textured_mesh.textures.faces_uvs_list()[0],
          texture_map=textured_mesh.textures.maps_padded()[0].clamp(0., 1.),
          )
      
  CONSOLE.print("Texture saved at:", mesh_save_path)
  return mesh_save_path

Leave a comment