Skip to content

PyVista Mesh Processing

pyvista_mesh_processing: Mesh geometry operations powered by PyVista/VTK: decimation, subdivision, smoothing (Laplacian, Taubin), cleaning, hole filling, normal computation, triangulation, and surface extraction.

Methods pv_decimate, pv_subdivide, pv_smooth_laplacian, pv_smooth_taubin, pv_clean, pv_fill_holes, pv_compute_normals, pv_triangulate, pv_extract_surface
Input files .ply, .obj, .stl, .vtk, .vtu
Bundled datasets to try open3d_bunny, open3d_icp_demo, kitti_raw_2011_09_26_drive_0001, medshapenet, c3vd_endoscopy_splats, hf_objaverse_glb_shard
Example Label map to mesh on MSD hippocampus
CLI pardesline run pyvista_mesh_processing.<method> input.ext [--param value]

How it works

PyVista wraps VTK filters for mesh geometry processing. Operations include:

Decimation reduces triangle count using quadric error metrics. Subdivision refines meshes by splitting triangles (linear, butterfly, loop). Smoothing removes noise via Laplacian \(\mathbf{v}_i' = \mathbf{v}_i + \lambda \Delta \mathbf{v}_i\) or Taubin's \(\lambda|\mu\) scheme. Cleaning merges duplicate points and removes degenerate cells. Triangulation converts polygonal cells to triangles.

Methods

pv_decimate

Reduces mesh complexity by collapsing edges based on quadric error metrics. Target reduction is a fraction \(r \in [0,1)\) of triangles to remove.

import pardesline as pl

obj = pl.load("input.ply")
result = obj.run("pyvista_mesh_processing.pv_decimate")  # catalog defaults
result.provenance[-1]  # the step, every parameter used, duration, version

Output: .ply. Script: python -m pardesline.ops.pyvista_mesh_processing.decimate --input ... --output ....

Defaults validated on: MSD hippocampus surfaces (examples/hippocampus_mask_to_mesh, decimate 0.5); generated meshes in tests/test_ops_smoke.py

Parameter Type Default Range What it does Why this default, when to change it
target_reduction float 0.5 0.01 to 0.99 Fraction of triangles to remove (0.5 = remove 50%). 0.5 halves the triangle count: on marching-cubes output (one triangle per voxel face) that removes the redundant co-planar triangles with no visible change.
volume_preservation (advanced) bool false Constrain decimation to preserve mesh volume (slower, less shrinkage). Off: VTK's default; on when the mesh is used for volume measurements (organs), since plain quadric decimation shrinks convex shapes slightly.
attribute_error (advanced) bool false Include scalar/attribute error in the decimation cost metric. Off: ignore scalar fields in the error metric; on when a label or curvature field must survive decimation.
normals (advanced) bool false Include normals deviation in the decimation cost metric. Off: do not penalise normal deviation; on for CAD meshes with sharp edges.
boundary_constraints (advanced) bool false Use legacy boundary-edge-length weighting for boundary quadric constraints. Off: VTK's current boundary handling; the legacy weighting is for reproducing old results.

pv_subdivide

Refines mesh by splitting triangles. Linear splits at midpoints; butterfly and loop use interpolation for smoother results.

import pardesline as pl

obj = pl.load("input.ply")
result = obj.run("pyvista_mesh_processing.pv_subdivide")  # catalog defaults
result.provenance[-1]  # the step, every parameter used, duration, version

Output: .ply. Script: python -m pardesline.ops.pyvista_mesh_processing.subdivide --input ... --output ....

Defaults validated on: generated meshes in tests/test_ops_smoke.py

Parameter Type Default Range What it does Why this default, when to change it
n_subdivisions int 1 1 to 4 Number of subdivision iterations. 1 iteration quadruples the triangle count, which is as much as a smoothing or sampling step ever needs.
algorithm string linear Subdivision algorithm: linear, butterfly, or loop. linear keeps the surface exactly where it is and only adds vertices; butterfly and loop also smooth, which changes the geometry.

pv_smooth_laplacian

Iteratively moves each vertex toward the centroid of its neighbors: \(\mathbf{v}_i' = \mathbf{v}_i + \lambda \frac{1}{|N_i|} \sum_{j \in N_i}(\mathbf{v}_j - \mathbf{v}_i)\).

import pardesline as pl

obj = pl.load("input.ply")
result = obj.run("pyvista_mesh_processing.pv_smooth_laplacian")  # catalog defaults
result.provenance[-1]  # the step, every parameter used, duration, version

Output: .ply. Script: python -m pardesline.ops.pyvista_mesh_processing.smooth_laplacian --input ... --output ....

Defaults validated on: MSD hippocampus surfaces (examples/hippocampus_mask_to_mesh); generated meshes in tests/test_ops_smoke.py

Parameter Type Default Range What it does Why this default, when to change it
n_iterations int 20 1 to 500 Number of smoothing iterations. 20 iterations remove the voxel staircase of a marching-cubes surface; more than 50 visibly shrinks small structures.
relaxation_factor float 0.01 0.001 to 1.0 Step size per iteration (higher = more smoothing). 0.01 is VTK's default step: small steps over 20 iterations move vertices by less than a voxel in total.
convergence (advanced) float 0.0 0.0 to 1.0 Convergence threshold; smaller = more iterations. 0 disables early stop. 0 disables early stop so the iteration count is the only knob.
edge_angle (advanced) float 15.0 0.0 to 180.0 Edge angle (deg) for interior edge features when feature_smoothing is on. 15 degrees is VTK's edge-feature threshold when feature smoothing is on.
feature_angle (advanced) float 45.0 0.0 to 180.0 Feature angle (deg) for sharp-feature detection when feature_smoothing is on. 45 degrees is VTK's sharp-feature threshold when feature smoothing is on.
boundary_smoothing (advanced) bool true Allow boundary (edge) vertices to move during smoothing. On: boundary vertices of open surfaces move too, otherwise the rim stays jagged.
feature_smoothing (advanced) bool false Smooth along detected sharp features rather than freezing them. Off: organic surfaces have no sharp features to preserve; on for CAD parts.

pv_smooth_taubin

Alternates shrinking (\(\lambda > 0\)) and inflating (\(\mu < 0\)) steps to smooth without volume shrinkage. Pass-band controls the cutoff frequency.

import pardesline as pl

obj = pl.load("input.ply")
result = obj.run("pyvista_mesh_processing.pv_smooth_taubin")  # catalog defaults
# same thing, domain vocabulary: `mesh.smooth()`
result.provenance[-1]  # the step, every parameter used, duration, version

Output: .ply. Script: python -m pardesline.ops.pyvista_mesh_processing.smooth_taubin --input ... --output ....

Defaults validated on: MSD hippocampus surfaces: the default mesh.smooth() (examples/hippocampus_mask_to_mesh); generated meshes in tests/test_ops_smoke.py

Parameter Type Default Range What it does Why this default, when to change it
n_iterations int 20 1 to 500 Number of smoothing iterations. 20 iterations remove the voxel staircase; Taubin's pass band keeps the volume, so the count can be higher than for Laplacian smoothing without shrinkage.
pass_band float 0.1 0.001 to 2.0 Pass-band value controlling cutoff frequency. 0.1 is VTK's default: frequencies above it are attenuated (staircase), below it kept (anatomy); lower it for a smoother result.
edge_angle (advanced) float 15.0 0.0 to 180.0 Edge angle (deg) for interior edge features when feature_smoothing is on. 15 degrees, VTK's edge-feature threshold.
feature_angle (advanced) float 45.0 0.0 to 180.0 Feature angle (deg) for sharp-feature detection when feature_smoothing is on. 45 degrees, VTK's sharp-feature threshold.
boundary_smoothing (advanced) bool true Allow boundary (edge) vertices to move during smoothing. On so open rims are smoothed too.
feature_smoothing (advanced) bool false Smooth along detected sharp features rather than freezing them. Off for organic surfaces.
non_manifold_smoothing (advanced) bool false Also smooth non-manifold vertices. Off: non-manifold vertices (two sheets touching) are left in place to avoid tearing.
normalize_coordinates (advanced) bool false Normalize coordinates before smoothing for better numerical stability. Off: VTK's default; on for coordinates with very large magnitudes (UTM metres) to keep the filter numerically stable.

pv_clean

Merges coincident points, removes degenerate cells, and strips unused points within the specified tolerance.

import pardesline as pl

obj = pl.load("input.ply")
result = obj.run("pyvista_mesh_processing.pv_clean")  # catalog defaults
# same thing, domain vocabulary: `mesh.clean()`
result.provenance[-1]  # the step, every parameter used, duration, version

Output: .ply. Script: python -m pardesline.ops.pyvista_mesh_processing.clean --input ... --output ....

Defaults validated on: every Mesh.clean() in the examples; generated meshes in tests/test_ops_smoke.py

Parameter Type Default Range What it does Why this default, when to change it
tolerance float 0.0 0.0 to 1.0 Merge tolerance (0 = exact duplicates only). 0 merges exact duplicates only: safe on any mesh; a positive tolerance welds nearby vertices and can close scan seams.
point_merging (advanced) bool true Merge coincident points within tolerance. On: duplicate vertices from STL files and marching cubes are merged.
absolute (advanced) bool true Interpret tolerance as an absolute distance (else fraction of bounding box). On: the tolerance is in file units, not a fraction of the bounding box, so it means the same thing on every mesh.
lines_to_points (advanced) bool true Convert degenerate lines (zero length) to points. On: zero-length lines become points instead of invalid cells.
polys_to_lines (advanced) bool true Convert degenerate polygons to lines. On: degenerate polygons become lines.
strips_to_polys (advanced) bool true Convert degenerate triangle strips to polygons. On: degenerate strips become polygons.

pv_fill_holes

Identifies boundary edges and fills holes smaller than the specified size by generating new triangular faces.

import pardesline as pl

obj = pl.load("input.ply")
result = obj.run("pyvista_mesh_processing.pv_fill_holes")  # catalog defaults
result.provenance[-1]  # the step, every parameter used, duration, version

Output: .ply. Script: python -m pardesline.ops.pyvista_mesh_processing.fill_holes --input ... --output ....

Defaults validated on: generated meshes in tests/test_ops_smoke.py

Parameter Type Default Range What it does Why this default, when to change it
hole_size float 100.0 1.0 to 10000.0 Maximum hole size to fill. 100 in file units is the largest hole (by boundary radius) that is filled: scan holes, not the open bottom of a bust.

pv_compute_normals

Computes point and cell normals from triangle geometry. Consistency flips normals to face outward.

import pardesline as pl

obj = pl.load("input.ply")
result = obj.run("pyvista_mesh_processing.pv_compute_normals")  # catalog defaults
result.provenance[-1]  # the step, every parameter used, duration, version

Output: .ply. Script: python -m pardesline.ops.pyvista_mesh_processing.compute_normals --input ... --output ....

Defaults validated on: generated meshes in tests/test_ops_smoke.py

Parameter Type Default Range What it does Why this default, when to change it
flip_normals int 0 0 to 1 Flip all normals (0=no, 1=yes). 0: keep the file's orientation.
consistency int 1 0 to 1 Enforce consistent normal orientation (0=no, 1=yes). 1: make neighbouring faces agree, which STL files from scanners often do not.
cell_normals (advanced) bool true Compute per-cell (face) normals. On: per-face normals for rendering and sampling.
point_normals (advanced) bool true Compute per-point (vertex) normals. On: per-vertex normals for smooth shading and registration.
split_vertices (advanced) bool false Split vertices along sharp edges (creates duplicate points at hard edges). Off: splitting duplicates vertices along sharp edges and breaks the connectivity other steps rely on.
auto_orient_normals (advanced) bool false Automatically orient normals outward (assumes a closed manifold). Off: it assumes a closed manifold and inverts open scans.
non_manifold_traversal (advanced) bool true Prevent orientation propagation across non-manifold edges. On: orientation does not propagate across non-manifold edges, where it would flip.
feature_angle (advanced) float 30.0 0.0 to 180.0 Feature angle (deg) used when split_vertices is on. 30 degrees when split_vertices is on, VTK's default.

pv_triangulate

Converts all polygonal cells (quads, n-gons) into triangles for uniform processing.

import pardesline as pl

obj = pl.load("input.ply")
result = obj.run("pyvista_mesh_processing.pv_triangulate")  # catalog defaults
result.provenance[-1]  # the step, every parameter used, duration, version

Output: .ply. Script: python -m pardesline.ops.pyvista_mesh_processing.triangulate --input ... --output ....

Defaults validated on: every Volume.to_mesh() surface is triangulated before saving (examples/hippocampus_mask_to_mesh); generated meshes in tests/test_ops_smoke.py

No tunable parameters.

pv_extract_surface

Extracts the outer surface of a volumetric or unstructured grid as a polygonal mesh.

import pardesline as pl

obj = pl.load("input.ply")
result = obj.run("pyvista_mesh_processing.pv_extract_surface")  # catalog defaults
result.provenance[-1]  # the step, every parameter used, duration, version

Output: .ply. Script: python -m pardesline.ops.pyvista_mesh_processing.extract_surface --input ... --output ....

Defaults validated on: every Volume.to_mesh() result goes through surface extraction (examples/hippocampus_mask_to_mesh); generated grids in tests/test_ops_smoke.py

Parameter Type Default Range What it does Why this default, when to change it
pass_pointid (advanced) bool true Keep original point ids as a 'vtkOriginalPointIds' array on the surface. On: keep the original point ids so results can be mapped back to the volume.
pass_cellid (advanced) bool true Keep original cell ids as a 'vtkOriginalCellIds' array on the surface. On: keep the original cell ids for the same reason.
nonlinear_subdivision (advanced) int 1 0 to 4 Subdivision level for nonlinear (higher-order) cells; 1 matches the default. 1 matches VTK's default for higher-order cells; the grids here are linear.

Generated from src/pardesline/catalog/data/pyvista_mesh_processing.json and _defaults.json on 2026-10-10.