.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "example_gallery/auto_examples/advanced/target_fitting.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_example_gallery_auto_examples_advanced_target_fitting.py: Target fitting: minimize Chamfer distance to a point cloud ========================================================== Deform mesh points with gradient descent so that :func:`graphlow.TensorMesh.geometry.chamfer_distance` to a target point set decreases. The target is a **dense** point cloud on an **ellipsoid**; the mesh (an icosphere) is centered, scaled, then updated in place to fit the target. Using an ellipsoid (different radii per axis) and many target points makes the optimization non-trivial and shows the mesh deforming toward the target. The same topology (sphere-like) allows the loss to converge to a low value. Soft-min Chamfer ensures gradients reach all vertices for stable convergence. This example walks through: sampling target points, loading the mesh, defining the loss, and running the optimization with visualization. .. GENERATED FROM PYTHON SOURCE LINES 19-21 Imports and constants --------------------- .. GENERATED FROM PYTHON SOURCE LINES 21-44 .. code-block:: Python import pathlib import numpy as np import pyvista as pv import torch import graphlow LEARNING_RATE = 0.06 N_STEPS = 2000 LOG_INTERVAL = 100 # Soft-min temperature for Chamfer: gradient flows to all vertices. SOFTMIN_TEMPERATURE = 0.08 # Target shape: ellipsoid semi-axes (a, b, c). Non-uniform = non-trivial fit. ELLIPSOID_AXES = (1.4, 0.8, 0.5) # Dense target point cloud (independent of mesh vertex count). N_TARGET_POINTS = 2000 # Icosphere subdivision level (higher = more vertices, smoother fit). ICOSPHERE_NSUB = 2 # Fixed RNG seed for reproducible target sampling (not part of the algorithm). _TARGET_SAMPLE_SEED = 0 .. GENERATED FROM PYTHON SOURCE LINES 45-50 Step 1: Target point cloud -------------------------- Sample points on an ellipsoid (uniform in solid angle, then scale by axes). A dense cloud with many more points than mesh vertices gives a clear target shape and stable Chamfer gradients. .. GENERATED FROM PYTHON SOURCE LINES 50-67 .. code-block:: Python def points_on_ellipsoid( n: int, axes: tuple[float, float, float] = (1.0, 1.0, 1.0), ) -> np.ndarray: """ Sample n points on an ellipsoid with semi-axes (a, b, c). Returns (n, 3). """ rng = np.random.default_rng(_TARGET_SAMPLE_SEED) phi = np.arccos(2.0 * rng.uniform(0, 1, n) - 1.0) theta = rng.uniform(0, 2 * np.pi, n) a, b, c = axes x = a * np.sin(phi) * np.cos(theta) y = b * np.sin(phi) * np.sin(theta) z = c * np.cos(phi) return np.stack([x, y, z], axis=1).astype(np.float32) .. GENERATED FROM PYTHON SOURCE LINES 68-73 Step 2: Load and prepare mesh ----------------------------- Use an icosphere (many vertices) so the mesh can deform into the target shape. Center and scale so initial points sit in a comparable range to the target (e.g. ellipsoid extent). .. GENERATED FROM PYTHON SOURCE LINES 73-103 .. code-block:: Python def load_and_center_mesh( mesh_path: pathlib.Path | None, icosphere_nsub: int = ICOSPHERE_NSUB, target_scale: float | None = None, ) -> graphlow.TensorMesh[torch.Tensor]: """Load mesh, require_grad, center and scale to sit near target extent.""" if mesh_path is not None and mesh_path.exists(): pv_mesh: pv.DataSet = pv.read(mesh_path) pv_mesh: pv.UnstructuredGrid = pv_mesh.cast_to_unstructured_grid() else: pv_mesh = pv.Icosphere(radius=1.0, nsub=icosphere_nsub) pv_mesh = pv_mesh.cast_to_unstructured_grid() pv_mesh.points = pv_mesh.points.astype(np.float32) mesh = graphlow.from_pyvista(pv_mesh, "torch") mesh.requires_grad(True) with torch.no_grad(): torch.sub( mesh.points, mesh.points.mean(dim=0, keepdim=True), out=mesh.points ) r = torch.linalg.norm(mesh.points, dim=1).mean().item() if r > 1e-8: torch.mul(mesh.points, 1.0 / r, out=mesh.points) if target_scale is not None and target_scale > 0: torch.mul(mesh.points, target_scale, out=mesh.points) return mesh .. GENERATED FROM PYTHON SOURCE LINES 104-108 Step 3: Loss and optimizer step ------------------------------- Loss is Chamfer distance. We update mesh.points in place with gradient descent (no separate parameter tensor). .. GENERATED FROM PYTHON SOURCE LINES 108-120 .. code-block:: Python def loss_fn( mesh: graphlow.TensorMesh[torch.Tensor], target_points: torch.Tensor, softmin_temperature: float | None = SOFTMIN_TEMPERATURE, ) -> torch.Tensor: """Chamfer distance; soft min lets gradient flow to all vertices.""" cd = mesh.geometry.chamfer_distance( target_points, softmin_temperature=softmin_temperature ) return cd.squeeze() .. GENERATED FROM PYTHON SOURCE LINES 121-125 Step 4: Visualization --------------------- Color by per-vertex distance to the target (nearest point in the cloud). Dark = close, bright = far; shows fitting quality over the mesh. .. GENERATED FROM PYTHON SOURCE LINES 125-167 .. code-block:: Python def _distance_to_target( mesh_pts: np.ndarray, target_pts: np.ndarray ) -> np.ndarray: """Per-vertex min distance to target point cloud. Shape (n_points,).""" diff = mesh_pts[:, None, :] - target_pts[None, :, :] return np.linalg.norm(diff, axis=2).min(axis=1).astype(np.float32) def create_plotter( mesh_grid: pv.UnstructuredGrid, target_pts: np.ndarray, ) -> pv.Plotter: """Build a plotter for GIF: initial mesh + target point cloud.""" plotter = pv.Plotter(window_size=[800, 600]) init_grid = mesh_grid.copy() mesh_grid["distance_to_target"] = _distance_to_target( mesh_grid.points, target_pts ) plotter.add_mesh(init_grid, show_edges=True, color="white", opacity=0.1) plotter.add_mesh( mesh_grid, scalars="distance_to_target", show_edges=True, lighting=False, cmap="viridis", opacity=0.8, ) target_poly = pv.PolyData(target_pts) plotter.add_mesh( target_poly, color="white", point_size=4, opacity=0.4, render_points_as_spheres=True, ) plotter.open_gif("target_fitting.gif") plotter.show_bounds(mesh=init_grid, location="outer") plotter.camera_position = "iso" return plotter .. GENERATED FROM PYTHON SOURCE LINES 168-171 Step 5: Run optimization ------------------------ Gradient descent on mesh.points; log and write frame every log_interval. .. GENERATED FROM PYTHON SOURCE LINES 171-249 .. code-block:: Python def main( mesh_path: pathlib.Path | None = None, n_steps: int = N_STEPS, lr: float = LEARNING_RATE, log_interval: int = LOG_INTERVAL, softmin_temperature: float | None = SOFTMIN_TEMPERATURE, n_target_points: int = N_TARGET_POINTS, ellipsoid_axes: tuple[float, float, float] | None = None, icosphere_nsub: int = ICOSPHERE_NSUB, ) -> None: """ Fit mesh to target point cloud by minimizing Chamfer distance. Target is a dense point cloud on an ellipsoid; mesh is an icosphere so it has enough vertices to deform into the target shape. Same topology allows the loss to converge to a low value. softmin_temperature > 0 makes gradient flow to all vertices (recommended). Frames are written to target_fitting.gif every log_interval steps. """ if ellipsoid_axes is None: ellipsoid_axes = ELLIPSOID_AXES # Scale initial mesh to sit inside the target envelope. target_scale = sum(ellipsoid_axes) / 3.0 mesh = load_and_center_mesh( mesh_path, icosphere_nsub=icosphere_nsub, target_scale=target_scale, ) device = mesh.points.device dtype = mesh.points.dtype target_np = points_on_ellipsoid(n_target_points, axes=ellipsoid_axes) target_points = torch.tensor( target_np, device=device, dtype=dtype, requires_grad=False ) plotter = create_plotter(mesh.pvmesh, target_np) plotter.write_frame() initial_loss = ( loss_fn(mesh, target_points, softmin_temperature=softmin_temperature) .detach() .item() ) print("Mesh vertices:", mesh.n_points, "| Target points:", n_target_points) print("Initial Chamfer distance:", f"{initial_loss:.6e}") print("Step | Chamfer distance") print("------|------------------") for step in range(1, n_steps + 1): loss = loss_fn( mesh, target_points, softmin_temperature=softmin_temperature ) loss.backward() with torch.no_grad(): torch.sub(mesh.points, mesh.points.grad * lr, out=mesh.points) torch.zero_(mesh.points.grad) if step % log_interval == 0: loss_val = loss.detach().item() print(f"{step:5d} | {loss_val:.6e}") pts = mesh.points.detach().numpy() mesh.pvmesh.points = pts mesh.pvmesh["distance_to_target"] = _distance_to_target( pts, target_np ) plotter.write_frame() plotter.close() final_loss = ( loss_fn(mesh, target_points, softmin_temperature=softmin_temperature) .detach() .item() ) print(f"\nFinal Chamfer distance: {final_loss:.6e}") print(f"Relative reduction: {(1 - final_loss / initial_loss) * 100:.1f}%") .. GENERATED FROM PYTHON SOURCE LINES 250-252 Step 6: Run the example ----------------------- .. GENERATED FROM PYTHON SOURCE LINES 252-254 .. code-block:: Python if __name__ == "__main__": main(n_steps=N_STEPS, log_interval=LOG_INTERVAL) .. image-sg:: /example_gallery/auto_examples/advanced/images/sphx_glr_target_fitting_001.gif :alt: target fitting :srcset: /example_gallery/auto_examples/advanced/images/sphx_glr_target_fitting_001.gif :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none Mesh vertices: 162 | Target points: 2000 Initial Chamfer distance: 7.075465e-01 Step | Chamfer distance ------|------------------ 100 | 6.819631e-01 200 | 6.622591e-01 300 | 6.474187e-01 400 | 6.359413e-01 500 | 6.268204e-01 600 | 6.194415e-01 700 | 6.134008e-01 800 | 6.084111e-01 900 | 6.042575e-01 1000 | 6.007739e-01 1100 | 5.978305e-01 1200 | 5.953248e-01 1300 | 5.931746e-01 1400 | 5.913145e-01 1500 | 5.896921e-01 1600 | 5.882651e-01 1700 | 5.869994e-01 1800 | 5.858676e-01 1900 | 5.848473e-01 2000 | 5.839207e-01 Final Chamfer distance: 5.839118e-01 Relative reduction: 17.5% .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 6.411 seconds) .. _sphx_glr_download_example_gallery_auto_examples_advanced_target_fitting.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: target_fitting.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: target_fitting.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: target_fitting.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_