Source code for mosaic.tabs.model

from functools import partial

import numpy as np
from qtpy.QtWidgets import QWidget, QVBoxLayout, QFileDialog

from ..parallel import run_in_background
from ..widgets.ribbon import create_button
from .. import meshing


def on_fit_complete(self, *args, **kwargs):
    self.cdata.data.render()
    self.cdata.models.render()


[docs] class ModelTab(QWidget):
[docs] def __init__(self, cdata, ribbon, legend, **kwargs): super().__init__() self.cdata = cdata self.ribbon = ribbon self.legend = legend layout = QVBoxLayout(self) layout.setSpacing(5) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.ribbon)
[docs] def show_ribbon(self): self.ribbon.clear() func = self._fit fitting_actions = [ create_button("Sphere", "mdi.circle", self, partial(func, "sphere")), create_button("Ellipse", "mdi.ellipse", self, partial(func, "ellipsoid")), create_button("Cylinder", "mdi.hexagon", self, partial(func, "cylinder")), create_button( "RBF", "mdi.grid", self, partial(func, "rbf"), "Fit RBF", RBF_SETTINGS ), create_button( "Mesh", "mdi.triangle-outline", self, func, "Fit Mesh", MESH_SETTINGS ), create_button( "Curve", "mdi.chart-bell-curve", self, partial(func, "spline"), "Fit Spline", SPLINE_SETTINGS, ), ] self.ribbon.add_section("Parametrization", fitting_actions) mesh_actions = [ create_button( "Sample", "mdi.chart-scatter-plot", self, self._sample_fit, "Sample from Fit", SAMPLE_SETTINGS, ), create_button("To Cluster", "mdi.plus", self, self._to_cluster), create_button( "Remove", "fa5s.trash", self, self.cdata.models.remove_cluster ), ] self.ribbon.add_section("Sampling", mesh_actions) mesh_actions = [ create_button( "Merge", "mdi.merge", self, self._merge_meshes, "Merge Meshes" ), create_button( "Volume", "mdi.cube-outline", self, self._mesh_volume, "Mesh Volume", MESHVOLUME_SETTINGS, ), create_button( "Repair", "mdi.auto-fix", self, self._repair_mesh, "Repair Mesh", REPAIR_SETTINGS, ), create_button( "Remesh", "mdi.repeat", self, self._remesh_meshes, "Remesh Mesh", REMESH_SETTINGS, ), create_button( "Project", "mdi.vector-curve", self, self._project_on_mesh, "Project on Mesh", PROJECTION_SETTINGS, ), # create_button("Skeleton", "mdi.vector-line", self, self._sceleton), ] self.ribbon.add_section("Mesh Operations", mesh_actions)
@run_in_background("Parametrization", callback=on_fit_complete) def _fit(self, method: str, *args, **kwargs): _conversion = { "Alpha Shape": "convexhull", "Ball Pivoting": "mesh", "Poisson": "poissonmesh", "Cluster Ball Pivoting": "clusterballpivoting", } method = _conversion.get(method, method) if method == "mesh": radii = kwargs.get("radii", None) try: kwargs["radii"] = [float(x) for x in radii.split(",")] except Exception as e: raise ValueError(f"Incorrect radius specification {radii}.") from e return self.cdata.add_fit(method, **kwargs) @run_in_background("Sample Fit", callback=on_fit_complete) def _sample_fit(self, *args, **kwargs): return self.cdata.sample_fit(*args, **kwargs) def _to_cluster(self, *args, **kwargs): indices = self.cdata.models._get_selected_indices() for index in indices: if not self.cdata._models._index_ok(index): continue geometry = self.cdata._models.data[index] fit = geometry._meta.get("fit", None) normals, sampling = None, geometry._sampling_rate if hasattr(fit, "mesh"): points = fit.vertices normals = fit.compute_vertex_normals() else: points = geometry.points if fit is not None: normals = fit.compute_normal(points) self.cdata._data.new(points, normals=normals, sampling_rate=sampling) self.cdata.data.data_changed.emit() self.cdata.data.render() return None def _get_selected_meshes(self): from ..parametrization import TriangularMesh ret = [] for index in self.cdata.models._get_selected_indices(): fit = self.cdata._models.data[index]._meta.get("fit", None) if not isinstance(fit, TriangularMesh): continue ret.append(index) return ret def _repair_mesh( self, max_hole_size=-1, elastic_weight=0, curvature_weight=0, volume_weight=0, boundary_ring=0, **kwargs, ): from ..parametrization import TriangularMesh for index in self._get_selected_meshes(): fit = self.cdata._models.data[index]._meta.get("fit", None) if not hasattr(fit, "vertices"): continue fit.mesh.remove_non_manifold_edges() fit.mesh.remove_degenerate_triangles() fit.mesh.remove_duplicated_triangles() fit.mesh.remove_unreferenced_vertices() fit.mesh.remove_duplicated_vertices() vs, fs = meshing.triangulate_refine_fair( vs=fit.vertices, fs=fit.triangles, alpha=elastic_weight, beta=curvature_weight, gamma=volume_weight, hole_len_thr=max_hole_size, n_ring=boundary_ring, ) self.cdata._add_fit( fit=TriangularMesh(meshing.to_open3d(vs, fs)), sampling_rate=self.cdata._models.data[index].sampling_rate, ) return self.cdata.models.render() @run_in_background("Project", callback=on_fit_complete) def _project_on_mesh( self, use_normals: bool = False, invert_normals: bool = False, update_normals: bool = False, **kwargs, ): selected_meshes = self._get_selected_meshes() if len(selected_meshes) != 1: raise ValueError("Please select one mesh for projection.") selected_meshes = self._get_selected_meshes() mesh = self.cdata._models.data[selected_meshes[0]]._meta.get("fit", None) if mesh is None: return None projections, triangles = [], [] for index in self.cdata.data._get_selected_indices(): geometry = self.cdata._data.data[index] normals = geometry.normals if use_normals else None if normals is not None: normals = normals * (-1 if invert_normals else 1) kwargs = { "points": geometry.points, "normals": normals, "return_projection": True, "return_indices": False, "return_triangles": True, } _, projection, triangle = mesh.compute_distance(**kwargs) normals = geometry.normals if update_normals: normals = mesh.compute_normal(projection) projections.append(projection) triangles.append(triangle) self.cdata._data.add( points=projection, normals=normals, sampling_rate=geometry.sampling_rate, ) if not len(projections): return None projections = np.concatenate(projections) triangles = np.concatenate(triangles) new_mesh = mesh.add_projections(projections, triangles, return_indices=False) self.cdata._add_fit( fit=new_mesh, sampling_rate=self.cdata._models.data[selected_meshes[0]].sampling_rate, ) self.cdata.data.data_changed.emit() return self.cdata.models.data_changed.emit() @run_in_background("Remesh", callback=on_fit_complete) def _remesh_meshes(self, method, **kwargs): from ..parametrization import TriangularMesh selected_meshes = self._get_selected_meshes() if len(selected_meshes) == 0: return None method = method.lower() supported = ( "edge length", "vertex clustering", "quadratic decimation", "subdivide", ) if method not in (supported): raise ValueError(f"{method} is not supported, chose one of {supported}.") if method == "subdivide": smooth = kwargs.pop("smooth", False) for index in selected_meshes: mesh = self.cdata._models.data[index]._meta.get("fit", None) mesh = meshing.to_open3d(mesh.vertices.copy(), mesh.triangles.copy()) if method == "edge length": mesh = meshing.remesh(mesh=mesh, **kwargs) elif method == "vertex clustering": mesh = mesh.simplify_vertex_clustering(**kwargs) elif method == "subdivide": func = mesh.subdivide_midpoint if smooth: func = mesh.subdivide_loop mesh = func(**kwargs) else: mesh = mesh.simplify_quadric_decimation(**kwargs) self.cdata._add_fit( fit=TriangularMesh(mesh), sampling_rate=self.cdata._models.data[index].sampling_rate, ) return self.cdata.models.data_changed.emit() def _merge_meshes(self): from ..parametrization import TriangularMesh meshes, selected_meshes = [], self._get_selected_meshes() if len(selected_meshes) < 2: return None for index in selected_meshes: meshes.append(self.cdata._models.data[index]._meta.get("fit")) vertices, faces = meshing.merge_meshes( vertices=[x.vertices for x in meshes], faces=[x.triangles for x in meshes], ) self.cdata._add_fit( fit=TriangularMesh(meshing.to_open3d(vertices, faces)), sampling_rate=self.cdata._models.data[index].sampling_rate, ) self.cdata._models.remove(selected_meshes) self.cdata.models.data_changed.emit() return self.cdata.models.render() def _sceleton(self): selected_meshes = self._get_selected_meshes() if len(selected_meshes) == 0: return None for index in selected_meshes: import trimesh import skeletor as sk from ..utils import com_cluster_points mesh = self.cdata._models.data[index]._meta.get("fit", None) mesh = trimesh.Trimesh(mesh.vertices, mesh.triangles) mesh = sk.pre.fix_mesh(mesh) skel = sk.skeletonize.by_wavefront(mesh, waves=5, step_size=1) vertices = com_cluster_points(skel.vertices, 100) vertices = skel.vertices self.cdata._data.add(vertices) self.cdata.data.data_changed.emit() return self.cdata.data.render() def _mesh_volume(self, **kwargs): filename, _ = QFileDialog.getOpenFileName(self, "Select Meshes") if not filename: return -1 return self._run_marching_cubes( filename, max_simplification_error=None, **kwargs ) @run_in_background("Mesh Volume", callback=on_fit_complete) def _run_marching_cubes(self, filename, **kwargs): from ..meshing import mesh_volume from ..formats.parser import load_density from ..parametrization import TriangularMesh mesh_paths = mesh_volume(filename, **kwargs) sampling = load_density(filename, use_memmap=True).sampling_rate for mesh_path in mesh_paths: self.cdata._add_fit( fit=TriangularMesh.from_file(mesh_path), sampling_rate=sampling, ) return self.cdata.models.data_changed.emit()
SAMPLE_SETTINGS = { "title": "Sample Fit", "settings": [ { "label": "Sampling Method", "parameter": "sampling_method", "type": "select", "options": ["Points", "Distance"], "default": "Points", "notes": "Number of points or average distance between points.", }, { "label": "Sampling", "parameter": "sampling", "type": "float", "min": 1, "default": 1000, "notes": "Numerical value for sampling method.", }, { "label": "Offset", "parameter": "normal_offset", "type": "float", "default": 0, "min": -1e32, "notes": "Points are shifted by n times normal vector for particle picking.", }, ], } RBF_SETTINGS = { "title": "RBF Settings", "settings": [ { "label": "Direction", "parameter": "direction", "type": "select", "options": ["xy", "xz", "yz"], "default": "xy", "description": "Coordinate plane to fit RBF in.", }, ], } SPLINE_SETTINGS = { "title": "Curve Settings", "settings": [ { "label": "Order", "parameter": "order", "type": "number", "default": 3, "min": 1, "max": 5, "description": "Spline order to fit to control points.", }, ], } REPAIR_SETTINGS = { "title": "Repair Settings", "settings": [ { "label": "Elastic Weight", "parameter": "elastic_weight", "type": "float", "default": 0.0, "min": -(2**28), "description": "Control mesh smoothness and elasticity.", "notes": "0 - strong anchoring, 1 - no anchoring, > 1 repulsion.", }, { "label": "Curvature Weight", "parameter": "curvature_weight", "type": "float", "default": 0.0, "min": -(2**28), "description": "Controls propagation of mesh curvature.", }, { "label": "Volume Weight", "parameter": "volume_weight", "type": "float", "default": 0.0, "min": -(2**28), "description": "Controls internal pressure of mesh.", }, { "label": "Boundary Ring", "parameter": "boundary_ring", "type": "number", "default": 0, "description": "Also optimize n-ring vertices for ill-defined boundaries.", }, { "label": "Hole Size", "parameter": "max_hole_size", "type": "float", "min": -1.0, "default": -1.0, "description": "Maximum surface area of holes considered for triangulation.", }, ], } REMESH_SETTINGS = { "title": "Remesh Settings", "settings": [ { "label": "Method", "parameter": "method", "type": "select", "options": [ "Edge Length", "Vertex Clustering", "Quadratic Decimation", "Subdivide", ], "default": "Edge Length", }, ], "method_settings": { "Edge Length": [ { "label": "Edge Length", "parameter": "target_edge_length", "type": "float", "default": 40.0, "min": 1e-6, "description": "Average edge length to remesh to.", }, { "label": "Iterations", "parameter": "n_iter", "type": "number", "default": 100, "min": 1, "description": "Number of remeshing operations to repeat on the mesh.", }, { "label": "Mesh Angle", "parameter": "featuredeg", "type": "float", "default": 30.0, "min": 0.0, "description": "Minimum angle between faces to preserve the edge feature.", }, ], "Vertex Clustering": [ { "label": "Radius", "parameter": "voxel_size", "type": "float", "default": 40.0, "min": 1e-6, "description": "Radius within which vertices are clustered.", }, ], "Quadratic Decimation": [ { "label": "Triangles", "parameter": "target_number_of_triangles", "type": "number", "default": 1000, "min": 1, "description": "Target number of triangles.", }, ], "Subdivide": [ { "label": "Iterations", "parameter": "number_of_iterations", "type": "number", "default": 1, "min": 1, "description": "Number of iterations.", "notes": "A single iteration splits each triangle into four triangles.", }, { "label": "Smooth", "parameter": "smooth", "type": "boolean", "default": True, "description": "Perform smooth midpoint division.", }, ], }, } MESH_SETTINGS = { "title": "Mesh Settings", "settings": [ { "label": "Method", "parameter": "method", "type": "select", "options": [ "Alpha Shape", "Ball Pivoting", "Cluster Ball Pivoting", "Poisson", ], "default": "Alpha Shape", }, *REPAIR_SETTINGS["settings"][:4], { "label": "Neighbors", "parameter": "k_neighbors", "type": "number", "min": 1, "default": 15, "description": "Number of neighbors for normal estimations.", "notes": "Consider decreasing this value for small point clouds.", }, ], "method_settings": { "Alpha Shape": [ { "label": "Alpha", "parameter": "alpha", "type": "float", "default": 1.0, "description": "Alpha-shape parameter.", "notes": "Large values yield coarser features.", }, { "label": "Scaling Factor", "parameter": "resampling_factor", "type": "float", "default": 12.0, "description": "Resample mesh to scaling factor times sampling rate.", "notes": "Decrease for creating smoother pressurized meshes.", }, { "label": "Distance", "parameter": "distance_cutoff", "type": "float", "default": 2.0, "description": "Vertices further than distance time sampling rate are " "labled as inferred for subsequent optimization.", }, ], "Ball Pivoting": [ { "label": "Radii", "parameter": "radii", "type": "text", "default": "50", "description": "Ball radii used for surface reconstruction.", "notes": "Use commas to specify multiple radii, e.g. '50,30.5,10.0'.", }, REPAIR_SETTINGS["settings"][-1], { "label": "Downsample", "parameter": "downsample_input", "type": "boolean", "default": True, "description": "Thin input point cloud to core.", }, { "label": "Smoothing Steps", "parameter": "n_smoothing", "type": "number", "default": 5, "description": "Pre-smoothing steps before fairing.", "notes": "Improves repair but less impactful for topolgoy than weights.", }, ], "Cluster Ball Pivoting": [ { "label": "Radius", "parameter": "radius", "type": "float", "default": 0.0, "max": 100, "min": 0.0, "description": "Ball radius compared to point cloud box size.", "notes": "Default 0 corresponds to an automatically determined radius.", }, { "label": "Mesh Angle", "parameter": "creasethr", "type": "float", "min": 0, "default": 90.0, "description": "Maximum crease angle before stoping ball pivoting.", }, { "label": "Smooth Iter", "parameter": "smooth_iter", "type": "number", "min": 1, "default": 1, "description": "Number of smoothing iterations for normal estimation.", }, { "label": "Distance", "parameter": "deldist", "type": "float", "min": -1.0, "default": -1.0, "description": "Drop vertices distant from input sample points.", "notes": "This is post-normalization by the sampling rate.", }, ], "Poisson": [ { "label": "Depth", "parameter": "depth", "type": "number", "min": 1, "default": 9, "description": "Depth of the Octree for surface reconstruction.", }, { "label": "Samples", "parameter": "samplespernode", "type": "float", "min": 0, "default": 5.0, "description": "Minimum number of points per octree node.", }, { "label": "Smooth Iter", "parameter": "smooth_iter", "type": "number", "min": 1, "default": 1, "description": "Number of smoothing iterations for normal estimation.", }, { "label": "Pointweight", "parameter": "pointweight", "type": "float", "min": 0, "default": 0.1, "description": "Interpolation weight of point samples.", }, { "label": "Scale", "parameter": "scale", "type": "float", "min": 0, "default": 1.2, "description": "Ratio between reconstruction and sample cube.", }, { "label": "Distance", "parameter": "deldist", "type": "float", "min": -1.0, "default": -1.0, "description": "Drop vertices distant from input sample points.", "notes": "This is post-normalization by the sampling rate.", }, ], }, } MESHVOLUME_SETTINGS = { "title": "Meshing Settings", "settings": [ { "label": "Simplifcation Factor", "parameter": "simplification_factor", "type": "number", "default": 100, "min": 1, "description": "Reduce initial mesh by x times the number of triangles.", }, { "label": "Workers", "parameter": "num_workers", "type": "number", "default": 8, "min": 1, "description": "Number of parallel workers to use.", }, { "label": "Close Dataset Edges", "parameter": "closed_dataset_edges", "type": "boolean", "default": True, "description": "Close mesh at at dataset edges.", }, ], } PROJECTION_SETTINGS = { "title": "Projection Settings", "settings": [ { "label": "Cast Normals", "parameter": "use_normals", "type": "boolean", "default": True, "description": "Include normal vectors in raycasting.", }, { "label": "Invert Normals", "parameter": "invert_normals", "type": "boolean", "default": False, "description": "Invert direction of normal vectors.", }, { "label": "Update Normals", "parameter": "update_normals", "type": "boolean", "default": False, "description": "Update normal vectors of projection based on the mesh.", }, ], }