Source code for mosaic.tabs.model

from functools import partial

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

from .. import meshing
from ..widgets.ribbon import create_button
from ..parallel import submit_task, submit_task_batch


def _project(
    mesh_geometry,
    geometries,
    use_normals: bool = False,
    invert_normals: bool = False,
    update_normals: bool = False,
):
    from ..geometry import Geometry

    mesh = mesh_geometry.model
    new_geometries, projections, triangles = [], [], []
    for geometry in geometries:
        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)
        new_geometries.append(
            Geometry(
                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)
    new_mesh = Geometry(model=new_mesh, sampling_rate=mesh_geometry.sampling_rate)

    return new_mesh, new_geometries


[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_parallel fitting_actions = [ create_button( "Sphere", "ph.circle", self, partial(func, "sphere"), "Fit to selected clusters", ), create_button( "Ellipse", "ph.link-simple-horizontal-break", self, partial(func, "ellipsoid"), "Fit to selected clusters", ), create_button( "Cylinder", "ph.hexagon", self, partial(func, "cylinder"), "Fit to selected clusters", ), create_button( "RBF", "ph.dots-nine", self, partial(func, "rbf"), "Fit to selected clusters", RBF_SETTINGS, ), create_button( "Mesh", "ph.triangle", self, func, "Fit to selected clusters", MESH_SETTINGS, ), create_button( "Curve", "ph.line-segments", self, partial(func, "spline"), "Fit to selected clusters", SPLINE_SETTINGS, ), ] self.ribbon.add_section("Parametrization", fitting_actions) mesh_actions = [ create_button( "Sample", "ph.broadcast", self, self._sample_parallel, "Generate points from fitted model", SAMPLE_SETTINGS, ), ] self.ribbon.add_section("Sampling", mesh_actions) mesh_actions = [ create_button( "Repair", "ph.wrench", self, self._repair_mesh, "Fix holes and topology issues", REPAIR_SETTINGS, ), create_button( "Remesh", "ph.arrows-clockwise", self, self._remesh_parallel, "Adjust resolution and quality", REMESH_SETTINGS, ), create_button( "Smooth", "ph.drop", self, self._smooth_parallel, "Reduce surface noise", SMOOTH_SETTINGS, ), create_button( "Project", "ph.arrow-line-down", self, self._project_on_mesh, "Project points onto mesh", PROJECTION_SETTINGS, ), ] self.ribbon.add_section("Mesh Operations", mesh_actions)
def _default_callback(self, geom): from ..parametrization import TriangularMesh if isinstance(geom.model, TriangularMesh): geom.change_representation("surface") self.cdata.models.add(geom) self.cdata.models.render() def _get_selected_meshes(self): from ..parametrization import TriangularMesh ret = [] for geometry in self.cdata.models.get_selected_geometries(): fit = geometry.model if not isinstance(fit, TriangularMesh): continue ret.append(geometry) 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 geometry in self._get_selected_meshes(): fit = geometry.model 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, ) geom = geometry[...] geom._model = TriangularMesh(meshing.to_open3d(vs, fs)) geom.change_representation("surface") self.cdata.models.add(geom) return self.cdata.models.render() def _fit_parallel(self, method: str, *args, **kwargs): from ..operations import GeometryOperations # These methods are parallelize and would mess with the worker pool tasks, max_concurrent = [], None if method in ("Poisson", "Marching Cubes"): max_concurrent = 1 for geometry in self.cdata.data.get_selected_geometries(): tasks.append( { "name": "Parametrization", "func": GeometryOperations.fit, "callback": self._default_callback, "kwargs": {"geometry": geometry, "method": method} | kwargs, } ) submit_task_batch(tasks, max_concurrent=max_concurrent) def _smooth_parallel(self, method, **kwargs): from ..operations import GeometryOperations for geometry in self._get_selected_meshes(): submit_task( "Smooth", GeometryOperations.smooth, self._default_callback, geometry, method, **kwargs, ) def _sample_parallel(self, sampling, method, normal_offset=0.0, **kwargs): from ..operations import GeometryOperations def _callback(*args, **kwargs): self.cdata.data.add(*args, **kwargs) self.cdata.data.render() for geometry in self.cdata.models.get_selected_geometries(): submit_task( "Sample Fit", GeometryOperations.sample, _callback, geometry, method=method, sampling=sampling, normal_offset=normal_offset, **kwargs, ) def _remesh_parallel(self, method, **kwargs): from ..operations import GeometryOperations for geometry in self._get_selected_meshes(): submit_task( "Remesh", GeometryOperations.remesh, self._default_callback, geometry, method, **kwargs, ) 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.") mesh = selected_meshes[0] if mesh.model is None: return None def _callback(ret): new_mesh, new_geometries = ret for new_geometry in new_geometries: self.cdata.data.add(new_geometry) new_mesh.change_representation("surface") self.cdata.models.add(new_mesh) self.cdata.data.render() self.cdata.models.render() submit_task( "Project", _project, _callback, mesh, self.cdata.data.get_selected_geometries(), use_normals, invert_normals, update_normals, )
SAMPLE_SETTINGS = { "title": "Settings", "settings": [ { "label": "Sampling Method", "parameter": "method", "type": "select", "options": ["Points", "Distance"], "default": "Distance", "notes": "Number of points or average distance between points.", }, { "label": "Sampling", "parameter": "sampling", "type": "float", "min": 1, "default": 40, "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.", }, { "label": "Bidirectional", "parameter": "bidirectional", "type": "boolean", "default": False, "notes": "Draw inward and outward facing points at the same time. This " "doubles the total number of points compared to running sample without " "this option set.", }, ], } RBF_SETTINGS = { "title": "Settings", "settings": [ { "label": "Direction", "parameter": "direction", "type": "select", "options": ["xy", "xz", "yz"], "default": "xy", "description": "Coordinate plane to fit RBF in.", }, ], } SPLINE_SETTINGS = { "title": "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": "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": "Flexibility", "parameter": "anchoring", "type": "float_list", "default": "1", "min": "0", "max": "1", "description": "Flexibility of inferred vertices. 1 is maximum. Can be " "specified for all axes, e.g., 1, or per-axis, e.g., 1;1;0.5.", }, { "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": "Settings", "settings": [ { "label": "Method", "parameter": "method", "type": "select", "options": [ "Decimation", "Edge Length", "Subdivide", "Vertex Clustering", ], "default": "Decimation", }, ], "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.", }, ], "Decimation": [ { "label": "Method", "parameter": "decimation_method", "type": "select", "options": ["Triangle Count", "Reduction Factor"], "default": "Reduction Factor", "description": "Choose how to specify the decimation target.", }, { "label": "Sampling", "parameter": "sampling", "type": "float", "default": 10, "min": 0, "description": "Numerical value for reduction method.", }, { "label": "Smooth", "parameter": "smooth", "type": "boolean", "default": True, "description": "Use quadratic decimation instead of pyfqmr.", }, ], "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.", }, ], }, } SMOOTH_SETTINGS = { "title": "Settings", "settings": [ { "label": "Method", "parameter": "method", "type": "select", "options": [ "Taubin", "Laplacian", "Average", ], "default": "Taubin", }, ], "method_settings": { "Taubin": [ { "label": "Iterations", "parameter": "number_of_iterations", "type": "number", "default": 10, "min": 1, "description": "Number of smoothing iterations.", "notes": "Taubin filter prevents mesh shrinkage by applying two Laplacian filters with different parameters.", }, ], "Laplacian": [ { "label": "Iterations", "parameter": "number_of_iterations", "type": "number", "default": 10, "min": 1, "description": "Number of smoothing iterations.", "notes": "May lead to mesh shrinkage with high iteration counts.", }, ], "Average": [ { "label": "Iterations", "parameter": "number_of_iterations", "type": "number", "default": 5, "min": 1, "description": "Number of smoothing iterations.", "notes": "Simplest filter - vertices are replaced by the average of adjacent vertices.", }, ], }, } MESH_SETTINGS = { "title": "Settings", "settings": [ { "label": "Method", "parameter": "method", "type": "select", "options": [ "Alpha Shape", "Ball Pivoting", "Cluster Ball Pivoting", "Poisson", "Flying Edges", "Marching Cubes", ], "default": "Alpha Shape", }, *REPAIR_SETTINGS["settings"][:5], ], "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.", }, { "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.", }, ], "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.", }, { "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.", }, ], "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 further than distance from input.", }, { "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.", }, ], "Flying Edges": [ { "label": "Distance", "parameter": "distance", "type": "float", "description": "Distance between points to be considered connected.", "default": -1.0, "min": -1.0, "max": 1e32, "notes": "Defaults to the sampling rate of the object.", }, ], "Marching Cubes": [ { "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": "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.", }, ], }