Source code for Mesher

import shutil
import pymeshlab
import os
import sys

# VirtualRocks source is released under GPL-3.0-only or GPL-3.0-or-later

# Default Parameters
OVERLAP = 0.1 # Overlap ammount between tiles
TEXTURE_RES = 4096
CELL_SIZE = 0.0001 # Clustering decimation cell size
TILE_SIZE = 25000 # Will subdivide tiles until they are below this number of verts
VERTEX_LIMIT = 2000000
VERBOSE = False

[docs] class Mesher(): def __init__(self, projdir): """ `Mesher` is a python class designed to be run as a subprocess. It uses the :ref:`pymeshlab <meshlab>` library to create the mesh, subdivide the mesh into tiles until each tile has less than 50k verticies, generate textures, and create a low polygon mesh for the entire model. Args: projdir (pathlib.Path): Project directory containing .vrp file. """ self.projdir = projdir self.dense2mesh()
[docs] def dense2mesh(self): """ Function to generate tiles with textures as well as a low poly mesh. After being started in main, it runs different :ref:`pymeshlab <meshlab>` functions while updating the progress bar for each one. """ print("$$", flush=True) # Path to Colmap dense folder base_path = self.projdir + r"\dense" # Create a new MeshSet object print("$Mesher.Loading pymeshlab.0$", flush=True) self.ms = pymeshlab.MeshSet() self.ms.set_verbosity(VERBOSE) # Open Colmap project from sparse as well as dense recon (fused.ply) print("$Mesher.Importing project files.1$", flush=True) self.ms.load_project([base_path+r"\images\project.bundle.out", base_path+r'\images\project.list.txt']) if self.ms.mesh_number() != 1: raise Exception("Failed to load sparse point cloud with rasters") # Import the fused.ply mesh print("$Mesher.Loading dense point cloud.3$", flush=True) self.ms.load_new_mesh(base_path+r"\fused.ply") if self.ms.mesh_number() != 2: raise Exception("Failed to load dense point cloud") # Update bounding box for dense poit cloud self.bounds = self.ms.current_mesh().bounding_box() # Point cloud simplification print("$Mesher.Optimizing Point Cloud.5$", flush=True) self.ms.meshing_decimation_clustering(threshold = pymeshlab.PureValue(CELL_SIZE)) # Mesher (this will retry with lower res if the user runs out of memory) print("$Mesher.Poisson Mesher.7$", flush=True) vertnum = 0 depth = 12 while vertnum < 100: self.ms.generate_surface_reconstruction_screened_poisson(depth = depth, samplespernode = 20, pointweight = 4) vertnum = self.ms.current_mesh().vertex_number() depth -= 1 # Crop skirt from model self._crop() # Wipe verticies colors print("$Mesher.Setting vertex colors.18$", flush=True) self.ms.set_color_per_vertex(color1 = pymeshlab.Color(255, 255, 255)) self.outdir = self.projdir + r"\out" if os.path.exists(self.outdir): shutil.rmtree(self.outdir) os.makedirs(self.outdir) # Get model bounds min=self.ms.current_mesh().bounding_box().min() max=self.ms.current_mesh().bounding_box().max() minx = min[0] maxx = max[0] miny = min[1] maxy = max[1] print("$Mesher.Starting tiling.20$", flush=True) self.totalverts = self.ms.current_mesh().vertex_number() if self.totalverts > VERTEX_LIMIT: print(f"Reducing Mesh from {self.totalverts} to {VERTEX_LIMIT}", flush=True) self.ms.meshing_decimation_quadric_edge_collapse(targetfacenum = VERTEX_LIMIT, preserveboundary = True, preservenormal = True) print("Removing non-manifold edges", flush=True) self.ms.meshing_repair_non_manifold_edges() self.totalverts = self.ms.current_mesh().vertex_number() self.percentdone = 0.0 self.tile = 0 self.fullmodel = self.ms.current_mesh_id() self.ms.set_verbosity(False) self._quad_slice(minx, maxx, miny, maxy) print(f"Created {self.tile} tiles", flush=True) self.ms.set_current_mesh(self.fullmodel) self.ms.set_verbosity(VERBOSE) #Mesh simplification print("$Mesher.Creating low poly mesh.90$", flush=True) self.ms.meshing_decimation_quadric_edge_collapse(targetfacenum = 100000, preserveboundary = True, preservenormal = True) # Remove non-manifold edges print("Removing non-manifold edges", flush=True) nonman = self.ms.compute_selection_by_non_manifold_per_vertex() print(f"Found {nonman} non-manifold edges out of 100000 total vertices", flush=True) self.ms.meshing_repair_non_manifold_edges() print("Building texture for low poly mesh", flush=True) self.ms.compute_texcoord_parametrization_and_texture_from_registered_rasters(texturesize = TEXTURE_RES, texturename = "low_poly.jpg", usedistanceweight=False) # Export mesh print(fr"Exporting mesh to {self.outdir}\low_poly.obj", flush=True) self.ms.save_current_mesh(fr"{self.outdir}\low_poly.obj") print("$Mesher..100$", flush=True) print("$$", flush=True) return True
[docs] def _quad_slice(self, minx, maxx, miny, maxy): """ Recursive helper method for subdividing mesh into tiles. Once a tile is below 50k verticies, it generates a texture and export. The passed values allow the method to recurse on smaller portions of the mesh. Bounds are inclusive. .. note:: Exported tiles save to the out folder in the current project directory. Their names follow the `"tile_#.obj"` format, where **#** is a number that distinguishes the tiles from each other. Args: minx (float): minimum x axis bound. maxx (float): maximum x axis bound. miny (float): minimum y axis bound. maxy (float): maximum y axis bound. """ # Select verts in bounds self.ms.set_current_mesh(self.fullmodel) self.ms.set_selection_none() self.ms.compute_selection_by_condition_per_vertex(condselect=f"(x < {maxx} && x > {minx}) && (y < {maxy} && y > {miny})") numverts = self.ms.current_mesh().selected_vertex_number() # Base Case (cut and export) if(numverts < TILE_SIZE): # Create new mesh with vertex within bounds + overlap self.ms.add_mesh(self.ms.mesh(self.fullmodel)) self.ms.compute_selection_by_condition_per_vertex(condselect=f"(x < {maxx + OVERLAP} && x > {minx - OVERLAP}) && (y < {maxy + OVERLAP} && y > {miny - OVERLAP})") self.ms.apply_selection_inverse() self.ms.meshing_remove_selected_vertices() # Build texture self.ms.compute_texcoord_parametrization_and_texture_from_registered_rasters(texturesize = TEXTURE_RES, texturename = f"tile_{self.tile}.jpg") # Export mesh self.ms.save_current_mesh(fr"{self.outdir}\tile_{self.tile}.obj") self.ms.set_selection_all() self.ms.meshing_remove_selected_vertices() # scaled_value = ((self.percentdone) * (70) / (100)) + 20 self.tile += 1 self.percentdone += numverts / self.totalverts * 100.0 print(f"-----{round(self.percentdone, 2)}%", flush=True) print(f"$Mesher.Tiling.{int(((self.percentdone) * (70) / (100)) + 20)}$", flush=True) return """ maxy ----------- | | | minx ----------- maxx | | | ----------- miny """ midx = (maxx + minx) / 2.0 midy = (maxy + miny) / 2.0 # topleft self._quad_slice(minx, midx, midy, maxy) # topright self._quad_slice(midx, maxx, midy, maxy) # bottomleft self._quad_slice(minx, midx, miny, midy) # bottomright self._quad_slice(midx, maxx, miny, midy)
[docs] def _crop(self): """ Helper function for cropping any extra points added to the mesh which lie outside the bounds of the original point cloud. """ min=self.bounds.min() max=self.bounds.max() minx = min[0] maxx = max[0] miny = min[1] maxy = max[1] self.ms.set_selection_none() self.ms.compute_selection_by_condition_per_vertex(condselect=f"(x < {minx} || x > {maxx}) || (y < {miny} || y > {maxy})") self.ms.meshing_remove_selected_vertices()
# Get args from caller (recon manager) and start mesher code projdir = sys.argv[1] try: Mesher(projdir) except Exception as e: print(e, flush=True)