vedo.pointcloud

Submodule to work with point clouds.

   1#!/usr/bin/env python3
   2# -*- coding: utf-8 -*-
   3import time
   4from typing import Union, List
   5from typing_extensions import Self
   6from weakref import ref as weak_ref_to
   7import numpy as np
   8
   9import vedo.vtkclasses as vtki
  10
  11import vedo
  12from vedo import colors
  13from vedo import utils
  14from vedo.transformations import LinearTransform, NonLinearTransform
  15from vedo.core import PointAlgorithms
  16from vedo.visual import PointsVisual
  17
  18__docformat__ = "google"
  19
  20__doc__ = """
  21Submodule to work with point clouds.
  22
  23![](https://vedo.embl.es/images/basic/pca.png)
  24"""
  25
  26__all__ = [
  27    "Points",
  28    "Point",
  29    "CellCenters",
  30    "merge",
  31    "delaunay2d",  # deprecated, use .generate_delaunay2d()
  32    "fit_line",
  33    "fit_circle",
  34    "fit_plane",
  35    "fit_sphere",
  36    "pca_ellipse",
  37    "pca_ellipsoid",
  38]
  39
  40
  41####################################################
  42def merge(*meshs, flag=False) -> Union["vedo.Mesh", "vedo.Points", None]:
  43    """
  44    Build a new Mesh (or Points) formed by the fusion of the inputs.
  45
  46    Similar to Assembly, but in this case the input objects become a single entity.
  47
  48    To keep track of the original identities of the inputs you can set `flag=True`.
  49    In this case a `pointdata` array of ids is added to the output with name "OriginalMeshID".
  50
  51    Examples:
  52        - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py)
  53
  54            ![](https://vedo.embl.es/images/advanced/warp1.png)
  55
  56        - [value_iteration.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/value_iteration.py)
  57
  58    """
  59    objs = [a for a in utils.flatten(meshs) if a]
  60
  61    if not objs:
  62        return None
  63
  64    idarr = []
  65    polyapp = vtki.new("AppendPolyData")
  66    for i, ob in enumerate(objs):
  67        polyapp.AddInputData(ob.dataset)
  68        if flag:
  69            idarr += [i] * ob.dataset.GetNumberOfPoints()
  70    polyapp.Update()
  71    mpoly = polyapp.GetOutput()
  72
  73    if flag:
  74        varr = utils.numpy2vtk(idarr, dtype=np.uint16, name="OriginalMeshID")
  75        mpoly.GetPointData().AddArray(varr)
  76
  77    has_mesh = False
  78    for ob in objs:
  79        if isinstance(ob, vedo.Mesh):
  80            has_mesh = True
  81            break
  82
  83    if has_mesh:
  84        msh = vedo.Mesh(mpoly)
  85    else:
  86        msh = Points(mpoly) # type: ignore
  87
  88    msh.copy_properties_from(objs[0])
  89
  90    msh.pipeline = utils.OperationNode(
  91        "merge", parents=objs, comment=f"#pts {msh.dataset.GetNumberOfPoints()}"
  92    )
  93    return msh
  94
  95
  96def delaunay2d(plist, **kwargs) -> Self:
  97    """delaunay2d() is deprecated, use Points().generate_delaunay2d() instead."""
  98    if isinstance(plist, Points):
  99        plist = plist.vertices
 100    else:
 101        plist = np.ascontiguousarray(plist)
 102        plist = utils.make3d(plist)
 103    pp = Points(plist).generate_delaunay2d(**kwargs)
 104    print("WARNING: delaunay2d() is deprecated, use Points().generate_delaunay2d() instead")
 105    return pp
 106
 107
 108def _rotate_points(points, n0=None, n1=(0, 0, 1)) -> Union[np.ndarray, tuple]:
 109    # Rotate a set of 3D points from direction n0 to direction n1.
 110    # Return the rotated points and the normal to the fitting plane (if n0 is None).
 111    # The pointing direction of the normal in this case is arbitrary.
 112    points = np.asarray(points)
 113
 114    if points.ndim == 1:
 115        points = points[np.newaxis, :]
 116
 117    if len(points[0]) == 2:
 118        return points, (0, 0, 1)
 119
 120    if n0 is None:  # fit plane
 121        datamean = points.mean(axis=0)
 122        vv = np.linalg.svd(points - datamean)[2]
 123        n0 = np.cross(vv[0], vv[1])
 124
 125    n0 = n0 / np.linalg.norm(n0)
 126    n1 = n1 / np.linalg.norm(n1)
 127    k = np.cross(n0, n1)
 128    l = np.linalg.norm(k)
 129    if not l:
 130        k = n0
 131    k /= np.linalg.norm(k)
 132
 133    ct = np.dot(n0, n1)
 134    theta = np.arccos(ct)
 135    st = np.sin(theta)
 136    v = k * (1 - ct)
 137
 138    rpoints = []
 139    for p in points:
 140        a = p * ct
 141        b = np.cross(k, p) * st
 142        c = v * np.dot(k, p)
 143        rpoints.append(a + b + c)
 144
 145    return np.array(rpoints), n0
 146
 147
 148def fit_line(points: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Line":
 149    """
 150    Fits a line through points.
 151
 152    Extra info is stored in `Line.slope`, `Line.center`, `Line.variances`.
 153
 154    Examples:
 155        - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py)
 156
 157            ![](https://vedo.embl.es/images/advanced/fitline.png)
 158    """
 159    if isinstance(points, Points):
 160        points = points.vertices
 161    data = np.asarray(points)
 162    datamean = data.mean(axis=0)
 163    _, dd, vv = np.linalg.svd(data - datamean)
 164    vv = vv[0] / np.linalg.norm(vv[0])
 165    # vv contains the first principal component, i.e. the direction
 166    # vector of the best fit line in the least squares sense.
 167    xyz_min = data.min(axis=0)
 168    xyz_max = data.max(axis=0)
 169    a = np.linalg.norm(xyz_min - datamean)
 170    b = np.linalg.norm(xyz_max - datamean)
 171    p1 = datamean - a * vv
 172    p2 = datamean + b * vv
 173    line = vedo.shapes.Line(p1, p2, lw=1)
 174    line.slope = vv
 175    line.center = datamean
 176    line.variances = dd
 177    return line
 178
 179
 180def fit_circle(points: Union[np.ndarray, "vedo.Points"]) -> tuple:
 181    """
 182    Fits a circle through a set of 3D points, with a very fast non-iterative method.
 183
 184    Returns the tuple `(center, radius, normal_to_circle)`.
 185
 186    .. warning::
 187        trying to fit s-shaped points will inevitably lead to instabilities and
 188        circles of small radius.
 189
 190    References:
 191        *J.F. Crawford, Nucl. Instr. Meth. 211, 1983, 223-225.*
 192    """
 193    if isinstance(points, Points):
 194        points = points.vertices
 195    data = np.asarray(points)
 196
 197    offs = data.mean(axis=0)
 198    data, n0 = _rotate_points(data - offs)
 199
 200    xi = data[:, 0]
 201    yi = data[:, 1]
 202
 203    x = sum(xi)
 204    xi2 = xi * xi
 205    xx = sum(xi2)
 206    xxx = sum(xi2 * xi)
 207
 208    y = sum(yi)
 209    yi2 = yi * yi
 210    yy = sum(yi2)
 211    yyy = sum(yi2 * yi)
 212
 213    xiyi = xi * yi
 214    xy = sum(xiyi)
 215    xyy = sum(xiyi * yi)
 216    xxy = sum(xi * xiyi)
 217
 218    N = len(xi)
 219    k = (xx + yy) / N
 220
 221    a1 = xx - x * x / N
 222    b1 = xy - x * y / N
 223    c1 = 0.5 * (xxx + xyy - x * k)
 224
 225    a2 = xy - x * y / N
 226    b2 = yy - y * y / N
 227    c2 = 0.5 * (xxy + yyy - y * k)
 228
 229    d = a2 * b1 - a1 * b2
 230    if not d:
 231        return offs, 0, n0
 232    x0 = (b1 * c2 - b2 * c1) / d
 233    y0 = (c1 - a1 * x0) / b1
 234
 235    R = np.sqrt(x0 * x0 + y0 * y0 - 1 / N * (2 * x0 * x + 2 * y0 * y - xx - yy))
 236
 237    c, _ = _rotate_points([x0, y0, 0], (0, 0, 1), n0)
 238
 239    return c[0] + offs, R, n0
 240
 241
 242def fit_plane(points: Union[np.ndarray, "vedo.Points"], signed=False) -> "vedo.shapes.Plane":
 243    """
 244    Fits a plane to a set of points.
 245
 246    Extra info is stored in `Plane.normal`, `Plane.center`, `Plane.variance`.
 247
 248    Arguments:
 249        signed : (bool)
 250            if True flip sign of the normal based on the ordering of the points
 251
 252    Examples:
 253        - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py)
 254
 255            ![](https://vedo.embl.es/images/advanced/fitline.png)
 256    """
 257    if isinstance(points, Points):
 258        points = points.vertices
 259    data = np.asarray(points)
 260    datamean = data.mean(axis=0)
 261    pts = data - datamean
 262    res = np.linalg.svd(pts)
 263    dd, vv = res[1], res[2]
 264    n = np.cross(vv[0], vv[1])
 265    if signed:
 266        v = np.zeros_like(pts)
 267        for i in range(len(pts) - 1):
 268            vi = np.cross(pts[i], pts[i + 1])
 269            v[i] = vi / np.linalg.norm(vi)
 270        ns = np.mean(v, axis=0)  # normal to the points plane
 271        if np.dot(n, ns) < 0:
 272            n = -n
 273    xyz_min = data.min(axis=0)
 274    xyz_max = data.max(axis=0)
 275    s = np.linalg.norm(xyz_max - xyz_min)
 276    pla = vedo.shapes.Plane(datamean, n, s=[s, s])
 277    pla.variance = dd[2]
 278    pla.name = "FitPlane"
 279    return pla
 280
 281
 282def fit_sphere(coords: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Sphere":
 283    """
 284    Fits a sphere to a set of points.
 285
 286    Extra info is stored in `Sphere.radius`, `Sphere.center`, `Sphere.residue`.
 287
 288    Examples:
 289        - [fitspheres1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitspheres1.py)
 290
 291            ![](https://vedo.embl.es/images/advanced/fitspheres1.jpg)
 292    """
 293    if isinstance(coords, Points):
 294        coords = coords.vertices
 295    coords = np.array(coords)
 296    n = len(coords)
 297    A = np.zeros((n, 4))
 298    A[:, :-1] = coords * 2
 299    A[:, 3] = 1
 300    f = np.zeros((n, 1))
 301    x = coords[:, 0]
 302    y = coords[:, 1]
 303    z = coords[:, 2]
 304    f[:, 0] = x * x + y * y + z * z
 305    try:
 306        C, residue, rank, _ = np.linalg.lstsq(A, f, rcond=-1)  # solve AC=f
 307    except:
 308        C, residue, rank, _ = np.linalg.lstsq(A, f)  # solve AC=f
 309    if rank < 4:
 310        return None
 311    t = (C[0] * C[0]) + (C[1] * C[1]) + (C[2] * C[2]) + C[3]
 312    radius = np.sqrt(t)[0]
 313    center = np.array([C[0][0], C[1][0], C[2][0]])
 314    if len(residue) > 0:
 315        residue = np.sqrt(residue[0]) / n
 316    else:
 317        residue = 0
 318    sph = vedo.shapes.Sphere(center, radius, c=(1, 0, 0)).wireframe(1)
 319    sph.radius = radius
 320    sph.center = center
 321    sph.residue = residue
 322    sph.name = "FitSphere"
 323    return sph
 324
 325
 326def pca_ellipse(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=60) -> Union["vedo.shapes.Circle", None]:
 327    """
 328    Create the oriented 2D ellipse that contains the fraction `pvalue` of points.
 329    PCA (Principal Component Analysis) is used to compute the ellipse orientation.
 330
 331    Parameter `pvalue` sets the specified fraction of points inside the ellipse.
 332    Normalized directions are stored in `ellipse.axis1`, `ellipse.axis2`.
 333    Axes sizes are stored in `ellipse.va`, `ellipse.vb`
 334
 335    Arguments:
 336        pvalue : (float)
 337            ellipse will include this fraction of points
 338        res : (int)
 339            resolution of the ellipse
 340
 341    Examples:
 342        - [pca_ellipse.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipse.py)
 343        - [histo_pca.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_pca.py)
 344
 345            ![](https://vedo.embl.es/images/pyplot/histo_pca.png)
 346    """
 347    from scipy.stats import f
 348
 349    if isinstance(points, Points):
 350        coords = points.vertices
 351    else:
 352        coords = points
 353    if len(coords) < 4:
 354        vedo.logger.warning("in pca_ellipse(), there are not enough points!")
 355        return None
 356
 357    P = np.array(coords, dtype=float)[:, (0, 1)]
 358    cov = np.cov(P, rowvar=0)      # type: ignore
 359    _, s, R = np.linalg.svd(cov)   # singular value decomposition
 360    p, n = s.size, P.shape[0]
 361    fppf = f.ppf(pvalue, p, n - p) # f % point function
 362    u = np.sqrt(s * fppf / 2) * 2  # semi-axes (largest first)
 363    ua, ub = u
 364    center = utils.make3d(np.mean(P, axis=0)) # centroid of the ellipse
 365
 366    t = LinearTransform(R.T * u).translate(center)
 367    elli = vedo.shapes.Circle(alpha=0.75, res=res)
 368    elli.apply_transform(t)
 369    elli.properties.LightingOff()
 370
 371    elli.pvalue = pvalue
 372    elli.center = np.array([center[0], center[1], 0])
 373    elli.nr_of_points = n
 374    elli.va = ua
 375    elli.vb = ub
 376    
 377    # we subtract center because it's in t
 378    elli.axis1 = t.move([1, 0, 0]) - center
 379    elli.axis2 = t.move([0, 1, 0]) - center
 380
 381    elli.axis1 /= np.linalg.norm(elli.axis1)
 382    elli.axis2 /= np.linalg.norm(elli.axis2)
 383    elli.name = "PCAEllipse"
 384    return elli
 385
 386
 387def pca_ellipsoid(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=24) -> Union["vedo.shapes.Ellipsoid", None]:
 388    """
 389    Create the oriented ellipsoid that contains the fraction `pvalue` of points.
 390    PCA (Principal Component Analysis) is used to compute the ellipsoid orientation.
 391
 392    Axes sizes can be accessed in `ellips.va`, `ellips.vb`, `ellips.vc`,
 393    normalized directions are stored in `ellips.axis1`, `ellips.axis2` and `ellips.axis3`.
 394    Center of mass is stored in `ellips.center`.
 395
 396    Asphericity can be accessed in `ellips.asphericity()` and ellips.asphericity_error().
 397    A value of 0 means a perfect sphere.
 398
 399    Arguments:
 400        pvalue : (float)
 401            ellipsoid will include this fraction of points
 402   
 403    Examples:
 404        [pca_ellipsoid.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipsoid.py)
 405
 406            ![](https://vedo.embl.es/images/basic/pca.png)
 407    
 408    See also:
 409        `pca_ellipse()` for a 2D ellipse.
 410    """
 411    from scipy.stats import f
 412
 413    if isinstance(points, Points):
 414        coords = points.vertices
 415    else:
 416        coords = points
 417    if len(coords) < 4:
 418        vedo.logger.warning("in pca_ellipsoid(), not enough input points!")
 419        return None
 420
 421    P = np.array(coords, ndmin=2, dtype=float)
 422    cov = np.cov(P, rowvar=0)     # type: ignore
 423    _, s, R = np.linalg.svd(cov)  # singular value decomposition
 424    p, n = s.size, P.shape[0]
 425    fppf = f.ppf(pvalue, p, n-p)*(n-1)*p*(n+1)/n/(n-p)  # f % point function
 426    u = np.sqrt(s*fppf)
 427    ua, ub, uc = u                # semi-axes (largest first)
 428    center = np.mean(P, axis=0)   # centroid of the hyperellipsoid
 429
 430    t = LinearTransform(R.T * u).translate(center)
 431    elli = vedo.shapes.Ellipsoid((0,0,0), (1,0,0), (0,1,0), (0,0,1), res=res)
 432    elli.apply_transform(t)
 433    elli.alpha(0.25)
 434    elli.properties.LightingOff()
 435
 436    elli.pvalue = pvalue
 437    elli.nr_of_points = n
 438    elli.center = center
 439    elli.va = ua
 440    elli.vb = ub
 441    elli.vc = uc
 442    # we subtract center because it's in t
 443    elli.axis1 = np.array(t.move([1, 0, 0])) - center
 444    elli.axis2 = np.array(t.move([0, 1, 0])) - center
 445    elli.axis3 = np.array(t.move([0, 0, 1])) - center
 446    elli.axis1 /= np.linalg.norm(elli.axis1)
 447    elli.axis2 /= np.linalg.norm(elli.axis2)
 448    elli.axis3 /= np.linalg.norm(elli.axis3)
 449    elli.name = "PCAEllipsoid"
 450    return elli
 451
 452
 453###################################################
 454def Point(pos=(0, 0, 0), r=12, c="red", alpha=1.0) -> Self:
 455    """
 456    Create a simple point in space.
 457
 458    .. note:: if you are creating many points you should use class `Points` instead!
 459    """
 460    pt = Points([[0,0,0]], r, c, alpha).pos(pos)
 461    pt.name = "Point"
 462    return pt
 463
 464
 465###################################################
 466class Points(PointsVisual, PointAlgorithms):
 467    """Work with point clouds."""
 468
 469    def __init__(self, inputobj=None, r=4, c=(0.2, 0.2, 0.2), alpha=1):
 470        """
 471        Build an object made of only vertex points for a list of 2D/3D points.
 472        Both shapes (N, 3) or (3, N) are accepted as input, if N>3.
 473
 474        Arguments:
 475            inputobj : (list, tuple)
 476            r : (int)
 477                Point radius in units of pixels.
 478            c : (str, list)
 479                Color name or rgb tuple.
 480            alpha : (float)
 481                Transparency in range [0,1].
 482
 483        Example:
 484            ```python
 485            from vedo import *
 486
 487            def fibonacci_sphere(n):
 488                s = np.linspace(0, n, num=n, endpoint=False)
 489                theta = s * 2.399963229728653
 490                y = 1 - s * (2/(n-1))
 491                r = np.sqrt(1 - y * y)
 492                x = np.cos(theta) * r
 493                z = np.sin(theta) * r
 494                return np._c[x,y,z]
 495
 496            Points(fibonacci_sphere(1000)).show(axes=1).close()
 497            ```
 498            ![](https://vedo.embl.es/images/feats/fibonacci.png)
 499        """
 500        # print("INIT POINTS")
 501        super().__init__()
 502
 503        self.name = ""
 504        self.filename = ""
 505        self.file_size = ""
 506
 507        self.info = {}
 508        self.time = time.time()
 509        
 510        self.transform = LinearTransform()
 511        self.point_locator = None
 512        self.cell_locator = None
 513        self.line_locator = None
 514
 515        self.actor = vtki.vtkActor()
 516        self.properties = self.actor.GetProperty()
 517        self.properties_backface = self.actor.GetBackfaceProperty()
 518        self.mapper = vtki.new("PolyDataMapper")
 519        self.dataset = vtki.vtkPolyData()
 520        
 521        # Create weakref so actor can access this object (eg to pick/remove):
 522        self.actor.retrieve_object = weak_ref_to(self)
 523
 524        try:
 525            self.properties.RenderPointsAsSpheresOn()
 526        except AttributeError:
 527            pass
 528
 529        if inputobj is None:  ####################
 530            return
 531        ##########################################
 532
 533        self.name = "Points"
 534
 535        ######
 536        if isinstance(inputobj, vtki.vtkActor):
 537            self.dataset.DeepCopy(inputobj.GetMapper().GetInput())
 538            pr = vtki.vtkProperty()
 539            pr.DeepCopy(inputobj.GetProperty())
 540            self.actor.SetProperty(pr)
 541            self.properties = pr
 542            self.mapper.SetScalarVisibility(inputobj.GetMapper().GetScalarVisibility())
 543
 544        elif isinstance(inputobj, vtki.vtkPolyData):
 545            self.dataset = inputobj
 546            if self.dataset.GetNumberOfCells() == 0:
 547                carr = vtki.vtkCellArray()
 548                for i in range(self.dataset.GetNumberOfPoints()):
 549                    carr.InsertNextCell(1)
 550                    carr.InsertCellPoint(i)
 551                self.dataset.SetVerts(carr)
 552
 553        elif isinstance(inputobj, Points):
 554            self.dataset = inputobj.dataset
 555            self.copy_properties_from(inputobj)
 556
 557        elif utils.is_sequence(inputobj):  # passing point coords
 558            self.dataset = utils.buildPolyData(utils.make3d(inputobj))
 559
 560        elif isinstance(inputobj, str):
 561            verts = vedo.file_io.load(inputobj)
 562            self.filename = inputobj
 563            self.dataset = verts.dataset
 564
 565        elif "meshlib" in str(type(inputobj)):
 566            from meshlib import mrmeshnumpy as mn
 567            self.dataset = utils.buildPolyData(mn.toNumpyArray(inputobj.points))
 568
 569        else:
 570            # try to extract the points from a generic VTK input data object
 571            if hasattr(inputobj, "dataset"):
 572                inputobj = inputobj.dataset
 573            try:
 574                vvpts = inputobj.GetPoints()
 575                self.dataset = vtki.vtkPolyData()
 576                self.dataset.SetPoints(vvpts)
 577                for i in range(inputobj.GetPointData().GetNumberOfArrays()):
 578                    arr = inputobj.GetPointData().GetArray(i)
 579                    self.dataset.GetPointData().AddArray(arr)
 580            except:
 581                vedo.logger.error(f"cannot build Points from type {type(inputobj)}")
 582                raise RuntimeError()
 583
 584        self.actor.SetMapper(self.mapper)
 585        self.mapper.SetInputData(self.dataset)
 586
 587        self.properties.SetColor(colors.get_color(c))
 588        self.properties.SetOpacity(alpha)
 589        self.properties.SetRepresentationToPoints()
 590        self.properties.SetPointSize(r)
 591        self.properties.LightingOff()
 592
 593        self.pipeline = utils.OperationNode(
 594            self, parents=[], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
 595        )
 596
 597    def _update(self, polydata, reset_locators=True) -> Self:
 598        """Overwrite the polygonal dataset with a new vtkPolyData."""
 599        self.dataset = polydata
 600        self.mapper.SetInputData(self.dataset)
 601        self.mapper.Modified()
 602        if reset_locators:
 603            self.point_locator = None
 604            self.line_locator = None
 605            self.cell_locator = None
 606        return self
 607
 608    def __str__(self):
 609        """Print a description of the Points/Mesh."""
 610        module = self.__class__.__module__
 611        name = self.__class__.__name__
 612        out = vedo.printc(
 613            f"{module}.{name} at ({hex(self.memory_address())})".ljust(75),
 614            c="g", bold=True, invert=True, return_string=True,
 615        )
 616        out += "\x1b[0m\x1b[32;1m"
 617
 618        if self.name:
 619            out += "name".ljust(14) + ": " + self.name
 620            if "legend" in self.info.keys() and self.info["legend"]:
 621                out+= f", legend='{self.info['legend']}'"
 622            out += "\n"
 623 
 624        if self.filename:
 625            out+= "file name".ljust(14) + ": " + self.filename + "\n"
 626
 627        if not self.mapper.GetScalarVisibility():
 628            col = utils.precision(self.properties.GetColor(), 3)
 629            cname = vedo.colors.get_color_name(self.properties.GetColor())
 630            out+= "color".ljust(14) + ": " + cname 
 631            out+= f", rgb={col}, alpha={self.properties.GetOpacity()}\n"
 632            if self.actor.GetBackfaceProperty():
 633                bcol = self.actor.GetBackfaceProperty().GetDiffuseColor()
 634                cname = vedo.colors.get_color_name(bcol)
 635                out+= "backface color".ljust(14) + ": " 
 636                out+= f"{cname}, rgb={utils.precision(bcol,3)}\n"
 637
 638        npt = self.dataset.GetNumberOfPoints()
 639        npo, nln = self.dataset.GetNumberOfPolys(), self.dataset.GetNumberOfLines()
 640        out+= "elements".ljust(14) + f": vertices={npt:,} polygons={npo:,} lines={nln:,}"
 641        if self.dataset.GetNumberOfStrips():
 642            out+= f", strips={self.dataset.GetNumberOfStrips():,}"
 643        out+= "\n"
 644        if self.dataset.GetNumberOfPieces() > 1:
 645            out+= "pieces".ljust(14) + ": " + str(self.dataset.GetNumberOfPieces()) + "\n"
 646
 647        out+= "position".ljust(14) + ": " + f"{utils.precision(self.pos(), 6)}\n"
 648        try:
 649            sc = self.transform.get_scale()
 650            out+= "scaling".ljust(14)  + ": "
 651            out+= utils.precision(sc, 6) + "\n"
 652        except AttributeError:
 653            pass
 654
 655        if self.npoints:
 656            out+="size".ljust(14)+ ": average=" + utils.precision(self.average_size(),6)
 657            out+=", diagonal="+ utils.precision(self.diagonal_size(), 6)+ "\n"
 658            out+="center of mass".ljust(14) + ": " + utils.precision(self.center_of_mass(),6)+"\n"
 659
 660        bnds = self.bounds()
 661        bx1, bx2 = utils.precision(bnds[0], 3), utils.precision(bnds[1], 3)
 662        by1, by2 = utils.precision(bnds[2], 3), utils.precision(bnds[3], 3)
 663        bz1, bz2 = utils.precision(bnds[4], 3), utils.precision(bnds[5], 3)
 664        out+= "bounds".ljust(14) + ":"
 665        out+= " x=(" + bx1 + ", " + bx2 + "),"
 666        out+= " y=(" + by1 + ", " + by2 + "),"
 667        out+= " z=(" + bz1 + ", " + bz2 + ")\n"
 668
 669        for key in self.pointdata.keys():
 670            arr = self.pointdata[key]
 671            dim = arr.shape[1] if arr.ndim > 1 else 1
 672            mark_active = "pointdata"
 673            a_scalars = self.dataset.GetPointData().GetScalars()
 674            a_vectors = self.dataset.GetPointData().GetVectors()
 675            a_tensors = self.dataset.GetPointData().GetTensors()
 676            if   a_scalars and a_scalars.GetName() == key:
 677                mark_active += " *"
 678            elif a_vectors and a_vectors.GetName() == key:
 679                mark_active += " **"
 680            elif a_tensors and a_tensors.GetName() == key:
 681                mark_active += " ***"
 682            out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), dim={dim}'
 683            if dim == 1 and len(arr):
 684                rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3)
 685                out += f", range=({rng})\n"
 686            else:
 687                out += "\n"
 688
 689        for key in self.celldata.keys():
 690            arr = self.celldata[key]
 691            dim = arr.shape[1] if arr.ndim > 1 else 1
 692            mark_active = "celldata"
 693            a_scalars = self.dataset.GetCellData().GetScalars()
 694            a_vectors = self.dataset.GetCellData().GetVectors()
 695            a_tensors = self.dataset.GetCellData().GetTensors()
 696            if   a_scalars and a_scalars.GetName() == key:
 697                mark_active += " *"
 698            elif a_vectors and a_vectors.GetName() == key:
 699                mark_active += " **"
 700            elif a_tensors and a_tensors.GetName() == key:
 701                mark_active += " ***"
 702            out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), dim={dim}'
 703            if dim == 1 and len(arr):
 704                rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3)
 705                out += f", range=({rng})\n"
 706            else:
 707                out += "\n"
 708
 709        for key in self.metadata.keys():
 710            arr = self.metadata[key]
 711            if len(arr) > 3:
 712                out+= "metadata".ljust(14) + ": " + f'"{key}" ({len(arr)} values)\n'
 713            else:
 714                out+= "metadata".ljust(14) + ": " + f'"{key}" = {arr}\n'
 715
 716        if self.picked3d is not None:
 717            idp = self.closest_point(self.picked3d, return_point_id=True)
 718            idc = self.closest_point(self.picked3d, return_cell_id=True)
 719            out+= "clicked point".ljust(14) + ": " + utils.precision(self.picked3d, 6)
 720            out+= f", pointID={idp}, cellID={idc}\n"
 721
 722        return out.rstrip() + "\x1b[0m"
 723
 724    def _repr_html_(self):
 725        """
 726        HTML representation of the Point cloud object for Jupyter Notebooks.
 727
 728        Returns:
 729            HTML text with the image and some properties.
 730        """
 731        import io
 732        import base64
 733        from PIL import Image
 734
 735        library_name = "vedo.pointcloud.Points"
 736        help_url = "https://vedo.embl.es/docs/vedo/pointcloud.html#Points"
 737
 738        arr = self.thumbnail()
 739        im = Image.fromarray(arr)
 740        buffered = io.BytesIO()
 741        im.save(buffered, format="PNG", quality=100)
 742        encoded = base64.b64encode(buffered.getvalue()).decode("utf-8")
 743        url = "data:image/png;base64," + encoded
 744        image = f"<img src='{url}'></img>"
 745
 746        bounds = "<br/>".join(
 747            [
 748                utils.precision(min_x, 4) + " ... " + utils.precision(max_x, 4)
 749                for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2])
 750            ]
 751        )
 752        average_size = "{size:.3f}".format(size=self.average_size())
 753
 754        help_text = ""
 755        if self.name:
 756            help_text += f"<b> {self.name}: &nbsp&nbsp</b>"
 757        help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>"
 758        if self.filename:
 759            dots = ""
 760            if len(self.filename) > 30:
 761                dots = "..."
 762            help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>"
 763
 764        pdata = ""
 765        if self.dataset.GetPointData().GetScalars():
 766            if self.dataset.GetPointData().GetScalars().GetName():
 767                name = self.dataset.GetPointData().GetScalars().GetName()
 768                pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>"
 769
 770        cdata = ""
 771        if self.dataset.GetCellData().GetScalars():
 772            if self.dataset.GetCellData().GetScalars().GetName():
 773                name = self.dataset.GetCellData().GetScalars().GetName()
 774                cdata = "<tr><td><b> cell data array </b></td><td>" + name + "</td></tr>"
 775
 776        allt = [
 777            "<table>",
 778            "<tr>",
 779            "<td>",
 780            image,
 781            "</td>",
 782            "<td style='text-align: center; vertical-align: center;'><br/>",
 783            help_text,
 784            "<table>",
 785            "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>",
 786            "<tr><td><b> center of mass </b></td><td>"
 787            + utils.precision(self.center_of_mass(), 3)
 788            + "</td></tr>",
 789            "<tr><td><b> average size </b></td><td>" + str(average_size) + "</td></tr>",
 790            "<tr><td><b> nr. points </b></td><td>" + str(self.npoints) + "</td></tr>",
 791            pdata,
 792            cdata,
 793            "</table>",
 794            "</table>",
 795        ]
 796        return "\n".join(allt)
 797
 798    ##################################################################################
 799    def __add__(self, meshs):
 800        """
 801        Add two meshes or a list of meshes together to form an `Assembly` object.
 802        """
 803        if isinstance(meshs, list):
 804            alist = [self]
 805            for l in meshs:
 806                if isinstance(l, vedo.Assembly):
 807                    alist += l.unpack()
 808                else:
 809                    alist += l
 810            return vedo.assembly.Assembly(alist)
 811
 812        if isinstance(meshs, vedo.Assembly):
 813            return meshs + self  # use Assembly.__add__
 814
 815        return vedo.assembly.Assembly([self, meshs])
 816
 817    def polydata(self, **kwargs):
 818        """
 819        Obsolete. Use property `.dataset` instead.
 820        Returns the underlying `vtkPolyData` object.
 821        """
 822        colors.printc(
 823            "WARNING: call to .polydata() is obsolete, use property .dataset instead.",
 824            c="y")
 825        return self.dataset
 826
 827    def __copy__(self):
 828        return self.clone(deep=False)
 829
 830    def __deepcopy__(self, memo):
 831        return self.clone(deep=memo)
 832    
 833    def copy(self, deep=True) -> Self:
 834        """Return a copy of the object. Alias of `clone()`."""
 835        return self.clone(deep=deep)
 836
 837    def clone(self, deep=True) -> Self:
 838        """
 839        Clone a `PointCloud` or `Mesh` object to make an exact copy of it.
 840        Alias of `copy()`.
 841
 842        Arguments:
 843            deep : (bool)
 844                if False return a shallow copy of the mesh without copying the points array.
 845
 846        Examples:
 847            - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py)
 848
 849               ![](https://vedo.embl.es/images/basic/mirror.png)
 850        """
 851        poly = vtki.vtkPolyData()
 852        if deep or isinstance(deep, dict): # if a memo object is passed this checks as True
 853            poly.DeepCopy(self.dataset)
 854        else:
 855            poly.ShallowCopy(self.dataset)
 856
 857        if isinstance(self, vedo.Mesh):
 858            cloned = vedo.Mesh(poly)
 859        else:
 860            cloned = Points(poly)
 861        # print([self], self.__class__)
 862        # cloned = self.__class__(poly)
 863
 864        cloned.transform = self.transform.clone()
 865
 866        cloned.copy_properties_from(self)
 867
 868        cloned.name = str(self.name)
 869        cloned.filename = str(self.filename)
 870        cloned.info = dict(self.info)
 871        cloned.pipeline = utils.OperationNode("clone", parents=[self], shape="diamond", c="#edede9")
 872
 873        if isinstance(deep, dict):
 874            deep[id(self)] = cloned
 875
 876        return cloned
 877
 878    def compute_normals_with_pca(self, n=20, orientation_point=None, invert=False) -> Self:
 879        """
 880        Generate point normals using PCA (principal component analysis).
 881        This algorithm estimates a local tangent plane around each sample point p
 882        by considering a small neighborhood of points around p, and fitting a plane
 883        to the neighborhood (via PCA).
 884
 885        Arguments:
 886            n : (int)
 887                neighborhood size to calculate the normal
 888            orientation_point : (list)
 889                adjust the +/- sign of the normals so that
 890                the normals all point towards a specified point. If None, perform a traversal
 891                of the point cloud and flip neighboring normals so that they are mutually consistent.
 892            invert : (bool)
 893                flip all normals
 894        """
 895        poly = self.dataset
 896        pcan = vtki.new("PCANormalEstimation")
 897        pcan.SetInputData(poly)
 898        pcan.SetSampleSize(n)
 899
 900        if orientation_point is not None:
 901            pcan.SetNormalOrientationToPoint()
 902            pcan.SetOrientationPoint(orientation_point)
 903        else:
 904            pcan.SetNormalOrientationToGraphTraversal()
 905
 906        if invert:
 907            pcan.FlipNormalsOn()
 908        pcan.Update()
 909
 910        varr = pcan.GetOutput().GetPointData().GetNormals()
 911        varr.SetName("Normals")
 912        self.dataset.GetPointData().SetNormals(varr)
 913        self.dataset.GetPointData().Modified()
 914        return self
 915
 916    def compute_acoplanarity(self, n=25, radius=None, on="points") -> Self:
 917        """
 918        Compute acoplanarity which is a measure of how much a local region of the mesh
 919        differs from a plane.
 920        
 921        The information is stored in a `pointdata` or `celldata` array with name 'Acoplanarity'.
 922        
 923        Either `n` (number of neighbour points) or `radius` (radius of local search) can be specified.
 924        If a radius value is given and not enough points fall inside it, then a -1 is stored.
 925
 926        Example:
 927            ```python
 928            from vedo import *
 929            msh = ParametricShape('RandomHills')
 930            msh.compute_acoplanarity(radius=0.1, on='cells')
 931            msh.cmap("coolwarm", on='cells').add_scalarbar()
 932            msh.show(axes=1).close()
 933            ```
 934            ![](https://vedo.embl.es/images/feats/acoplanarity.jpg)
 935        """
 936        acoplanarities = []
 937        if "point" in on:
 938            pts = self.vertices
 939        elif "cell" in on:
 940            pts = self.cell_centers
 941        else:
 942            raise ValueError(f"In compute_acoplanarity() set on to either 'cells' or 'points', not {on}")
 943
 944        for p in utils.progressbar(pts, delay=5, width=15, title=f"{on} acoplanarity"):
 945            if n:
 946                data = self.closest_point(p, n=n)
 947                npts = n
 948            elif radius:
 949                data = self.closest_point(p, radius=radius)
 950                npts = len(data)
 951
 952            try:
 953                center = data.mean(axis=0)
 954                res = np.linalg.svd(data - center)
 955                acoplanarities.append(res[1][2] / npts)
 956            except:
 957                acoplanarities.append(-1.0)
 958
 959        if "point" in on:
 960            self.pointdata["Acoplanarity"] = np.array(acoplanarities, dtype=float)
 961        else:
 962            self.celldata["Acoplanarity"] = np.array(acoplanarities, dtype=float)
 963        return self
 964
 965    def distance_to(self, pcloud, signed=False, invert=False, name="Distance") -> np.ndarray:
 966        """
 967        Computes the distance from one point cloud or mesh to another point cloud or mesh.
 968        This new `pointdata` array is saved with default name "Distance".
 969
 970        Keywords `signed` and `invert` are used to compute signed distance,
 971        but the mesh in that case must have polygonal faces (not a simple point cloud),
 972        and normals must also be computed.
 973
 974        Examples:
 975            - [distance2mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/distance2mesh.py)
 976
 977                ![](https://vedo.embl.es/images/basic/distance2mesh.png)
 978        """
 979        if pcloud.dataset.GetNumberOfPolys():
 980
 981            poly1 = self.dataset
 982            poly2 = pcloud.dataset
 983            df = vtki.new("DistancePolyDataFilter")
 984            df.ComputeSecondDistanceOff()
 985            df.SetInputData(0, poly1)
 986            df.SetInputData(1, poly2)
 987            df.SetSignedDistance(signed)
 988            df.SetNegateDistance(invert)
 989            df.Update()
 990            scals = df.GetOutput().GetPointData().GetScalars()
 991            dists = utils.vtk2numpy(scals)
 992
 993        else:  # has no polygons
 994
 995            if signed:
 996                vedo.logger.warning("distance_to() called with signed=True but input object has no polygons")
 997
 998            if not pcloud.point_locator:
 999                pcloud.point_locator = vtki.new("PointLocator")
1000                pcloud.point_locator.SetDataSet(pcloud.dataset)
1001                pcloud.point_locator.BuildLocator()
1002
1003            ids = []
1004            ps1 = self.vertices
1005            ps2 = pcloud.vertices
1006            for p in ps1:
1007                pid = pcloud.point_locator.FindClosestPoint(p)
1008                ids.append(pid)
1009
1010            deltas = ps2[ids] - ps1
1011            dists = np.linalg.norm(deltas, axis=1).astype(np.float32)
1012            scals = utils.numpy2vtk(dists)
1013
1014        scals.SetName(name)
1015        self.dataset.GetPointData().AddArray(scals)
1016        self.dataset.GetPointData().SetActiveScalars(scals.GetName())
1017        rng = scals.GetRange()
1018        self.mapper.SetScalarRange(rng[0], rng[1])
1019        self.mapper.ScalarVisibilityOn()
1020
1021        self.pipeline = utils.OperationNode(
1022            "distance_to",
1023            parents=[self, pcloud],
1024            shape="cylinder",
1025            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1026        )
1027        return dists
1028
1029    def clean(self) -> Self:
1030        """Clean pointcloud or mesh by removing coincident points."""
1031        cpd = vtki.new("CleanPolyData")
1032        cpd.PointMergingOn()
1033        cpd.ConvertLinesToPointsOff()
1034        cpd.ConvertPolysToLinesOff()
1035        cpd.ConvertStripsToPolysOff()
1036        cpd.SetInputData(self.dataset)
1037        cpd.Update()
1038        self._update(cpd.GetOutput())
1039        self.pipeline = utils.OperationNode(
1040            "clean", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
1041        )
1042        return self
1043
1044    def subsample(self, fraction: float, absolute=False) -> Self:
1045        """
1046        Subsample a point cloud by requiring that the points
1047        or vertices are far apart at least by the specified fraction of the object size.
1048        If a Mesh is passed the polygonal faces are not removed
1049        but holes can appear as their vertices are removed.
1050
1051        Examples:
1052            - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py)
1053
1054                ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png)
1055
1056            - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py)
1057
1058                ![](https://vedo.embl.es/images/advanced/recosurface.png)
1059        """
1060        if not absolute:
1061            if fraction > 1:
1062                vedo.logger.warning(
1063                    f"subsample(fraction=...), fraction must be < 1, but is {fraction}"
1064                )
1065            if fraction <= 0:
1066                return self
1067
1068        cpd = vtki.new("CleanPolyData")
1069        cpd.PointMergingOn()
1070        cpd.ConvertLinesToPointsOn()
1071        cpd.ConvertPolysToLinesOn()
1072        cpd.ConvertStripsToPolysOn()
1073        cpd.SetInputData(self.dataset)
1074        if absolute:
1075            cpd.SetTolerance(fraction / self.diagonal_size())
1076            # cpd.SetToleranceIsAbsolute(absolute)
1077        else:
1078            cpd.SetTolerance(fraction)
1079        cpd.Update()
1080
1081        ps = 2
1082        if self.properties.GetRepresentation() == 0:
1083            ps = self.properties.GetPointSize()
1084
1085        self._update(cpd.GetOutput())
1086        self.ps(ps)
1087
1088        self.pipeline = utils.OperationNode(
1089            "subsample", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
1090        )
1091        return self
1092
1093    def threshold(self, scalars: str, above=None, below=None, on="points") -> Self:
1094        """
1095        Extracts cells where scalar value satisfies threshold criterion.
1096
1097        Arguments:
1098            scalars : (str)
1099                name of the scalars array.
1100            above : (float)
1101                minimum value of the scalar
1102            below : (float)
1103                maximum value of the scalar
1104            on : (str)
1105                if 'cells' assume array of scalars refers to cell data.
1106
1107        Examples:
1108            - [mesh_threshold.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_threshold.py)
1109        """
1110        thres = vtki.new("Threshold")
1111        thres.SetInputData(self.dataset)
1112
1113        if on.startswith("c"):
1114            asso = vtki.vtkDataObject.FIELD_ASSOCIATION_CELLS
1115        else:
1116            asso = vtki.vtkDataObject.FIELD_ASSOCIATION_POINTS
1117
1118        thres.SetInputArrayToProcess(0, 0, 0, asso, scalars)
1119
1120        if above is None and below is not None:
1121            try:  # vtk 9.2
1122                thres.ThresholdByLower(below)
1123            except AttributeError:  # vtk 9.3
1124                thres.SetUpperThreshold(below)
1125
1126        elif below is None and above is not None:
1127            try:
1128                thres.ThresholdByUpper(above)
1129            except AttributeError:
1130                thres.SetLowerThreshold(above)
1131        else:
1132            try:
1133                thres.ThresholdBetween(above, below)
1134            except AttributeError:
1135                thres.SetUpperThreshold(below)
1136                thres.SetLowerThreshold(above)
1137
1138        thres.Update()
1139
1140        gf = vtki.new("GeometryFilter")
1141        gf.SetInputData(thres.GetOutput())
1142        gf.Update()
1143        self._update(gf.GetOutput())
1144        self.pipeline = utils.OperationNode("threshold", parents=[self])
1145        return self
1146
1147    def quantize(self, value: float) -> Self:
1148        """
1149        The user should input a value and all {x,y,z} coordinates
1150        will be quantized to that absolute grain size.
1151        """
1152        qp = vtki.new("QuantizePolyDataPoints")
1153        qp.SetInputData(self.dataset)
1154        qp.SetQFactor(value)
1155        qp.Update()
1156        self._update(qp.GetOutput())
1157        self.pipeline = utils.OperationNode("quantize", parents=[self])
1158        return self
1159
1160    @property
1161    def vertex_normals(self) -> np.ndarray:
1162        """
1163        Retrieve vertex normals as a numpy array. Same as `point_normals`.
1164        Check out also `compute_normals()` and `compute_normals_with_pca()`.
1165        """
1166        vtknormals = self.dataset.GetPointData().GetNormals()
1167        return utils.vtk2numpy(vtknormals)
1168
1169    @property
1170    def point_normals(self) -> np.ndarray:
1171        """
1172        Retrieve vertex normals as a numpy array. Same as `vertex_normals`.
1173        Check out also `compute_normals()` and `compute_normals_with_pca()`.
1174        """
1175        vtknormals = self.dataset.GetPointData().GetNormals()
1176        return utils.vtk2numpy(vtknormals)
1177
1178    def align_to(self, target, iters=100, rigid=False, invert=False, use_centroids=False) -> Self:
1179        """
1180        Aligned to target mesh through the `Iterative Closest Point` algorithm.
1181
1182        The core of the algorithm is to match each vertex in one surface with
1183        the closest surface point on the other, then apply the transformation
1184        that modify one surface to best match the other (in the least-square sense).
1185
1186        Arguments:
1187            rigid : (bool)
1188                if True do not allow scaling
1189            invert : (bool)
1190                if True start by aligning the target to the source but
1191                invert the transformation finally. Useful when the target is smaller
1192                than the source.
1193            use_centroids : (bool)
1194                start by matching the centroids of the two objects.
1195
1196        Examples:
1197            - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py)
1198
1199                ![](https://vedo.embl.es/images/basic/align1.png)
1200
1201            - [align2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align2.py)
1202
1203                ![](https://vedo.embl.es/images/basic/align2.png)
1204        """
1205        icp = vtki.new("IterativeClosestPointTransform")
1206        icp.SetSource(self.dataset)
1207        icp.SetTarget(target.dataset)
1208        if invert:
1209            icp.Inverse()
1210        icp.SetMaximumNumberOfIterations(iters)
1211        if rigid:
1212            icp.GetLandmarkTransform().SetModeToRigidBody()
1213        icp.SetStartByMatchingCentroids(use_centroids)
1214        icp.Update()
1215
1216        self.apply_transform(icp.GetMatrix())
1217
1218        self.pipeline = utils.OperationNode(
1219            "align_to", parents=[self, target], comment=f"rigid = {rigid}"
1220        )
1221        return self
1222
1223    def align_to_bounding_box(self, msh, rigid=False) -> Self:
1224        """
1225        Align the current object's bounding box to the bounding box
1226        of the input object.
1227
1228        Use `rigid=True` to disable scaling.
1229
1230        Example:
1231            [align6.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align6.py)
1232        """
1233        lmt = vtki.vtkLandmarkTransform()
1234        ss = vtki.vtkPoints()
1235        xss0, xss1, yss0, yss1, zss0, zss1 = self.bounds()
1236        for p in [
1237            [xss0, yss0, zss0],
1238            [xss1, yss0, zss0],
1239            [xss1, yss1, zss0],
1240            [xss0, yss1, zss0],
1241            [xss0, yss0, zss1],
1242            [xss1, yss0, zss1],
1243            [xss1, yss1, zss1],
1244            [xss0, yss1, zss1],
1245        ]:
1246            ss.InsertNextPoint(p)
1247        st = vtki.vtkPoints()
1248        xst0, xst1, yst0, yst1, zst0, zst1 = msh.bounds()
1249        for p in [
1250            [xst0, yst0, zst0],
1251            [xst1, yst0, zst0],
1252            [xst1, yst1, zst0],
1253            [xst0, yst1, zst0],
1254            [xst0, yst0, zst1],
1255            [xst1, yst0, zst1],
1256            [xst1, yst1, zst1],
1257            [xst0, yst1, zst1],
1258        ]:
1259            st.InsertNextPoint(p)
1260
1261        lmt.SetSourceLandmarks(ss)
1262        lmt.SetTargetLandmarks(st)
1263        lmt.SetModeToAffine()
1264        if rigid:
1265            lmt.SetModeToRigidBody()
1266        lmt.Update()
1267
1268        LT = LinearTransform(lmt)
1269        self.apply_transform(LT)
1270        return self
1271
1272    def align_with_landmarks(
1273        self,
1274        source_landmarks,
1275        target_landmarks,
1276        rigid=False,
1277        affine=False,
1278        least_squares=False,
1279    ) -> Self:
1280        """
1281        Transform mesh orientation and position based on a set of landmarks points.
1282        The algorithm finds the best matching of source points to target points
1283        in the mean least square sense, in one single step.
1284
1285        If `affine` is True the x, y and z axes can scale independently but stay collinear.
1286        With least_squares they can vary orientation.
1287
1288        Examples:
1289            - [align5.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align5.py)
1290
1291                ![](https://vedo.embl.es/images/basic/align5.png)
1292        """
1293
1294        if utils.is_sequence(source_landmarks):
1295            ss = vtki.vtkPoints()
1296            for p in source_landmarks:
1297                ss.InsertNextPoint(p)
1298        else:
1299            ss = source_landmarks.dataset.GetPoints()
1300            if least_squares:
1301                source_landmarks = source_landmarks.vertices
1302
1303        if utils.is_sequence(target_landmarks):
1304            st = vtki.vtkPoints()
1305            for p in target_landmarks:
1306                st.InsertNextPoint(p)
1307        else:
1308            st = target_landmarks.GetPoints()
1309            if least_squares:
1310                target_landmarks = target_landmarks.vertices
1311
1312        if ss.GetNumberOfPoints() != st.GetNumberOfPoints():
1313            n1 = ss.GetNumberOfPoints()
1314            n2 = st.GetNumberOfPoints()
1315            vedo.logger.error(f"source and target have different nr of points {n1} vs {n2}")
1316            raise RuntimeError()
1317
1318        if int(rigid) + int(affine) + int(least_squares) > 1:
1319            vedo.logger.error(
1320                "only one of rigid, affine, least_squares can be True at a time"
1321            )
1322            raise RuntimeError()
1323
1324        lmt = vtki.vtkLandmarkTransform()
1325        lmt.SetSourceLandmarks(ss)
1326        lmt.SetTargetLandmarks(st)
1327        lmt.SetModeToSimilarity()
1328
1329        if rigid:
1330            lmt.SetModeToRigidBody()
1331            lmt.Update()
1332
1333        elif affine:
1334            lmt.SetModeToAffine()
1335            lmt.Update()
1336
1337        elif least_squares:
1338            cms = source_landmarks.mean(axis=0)
1339            cmt = target_landmarks.mean(axis=0)
1340            m = np.linalg.lstsq(source_landmarks - cms, target_landmarks - cmt, rcond=None)[0]
1341            M = vtki.vtkMatrix4x4()
1342            for i in range(3):
1343                for j in range(3):
1344                    M.SetElement(j, i, m[i][j])
1345            lmt = vtki.vtkTransform()
1346            lmt.Translate(cmt)
1347            lmt.Concatenate(M)
1348            lmt.Translate(-cms)
1349
1350        else:
1351            lmt.Update()
1352
1353        self.apply_transform(lmt)
1354        self.pipeline = utils.OperationNode("transform_with_landmarks", parents=[self])
1355        return self
1356
1357    def normalize(self) -> Self:
1358        """Scale average size to unit. The scaling is performed around the center of mass."""
1359        coords = self.vertices
1360        if not coords.shape[0]:
1361            return self
1362        cm = np.mean(coords, axis=0)
1363        pts = coords - cm
1364        xyz2 = np.sum(pts * pts, axis=0)
1365        scale = 1 / np.sqrt(np.sum(xyz2) / len(pts))
1366        self.scale(scale, origin=cm)
1367        self.pipeline = utils.OperationNode("normalize", parents=[self])
1368        return self
1369
1370    def mirror(self, axis="x", origin=True) -> Self:
1371        """
1372        Mirror reflect along one of the cartesian axes
1373
1374        Arguments:
1375            axis : (str)
1376                axis to use for mirroring, must be set to `x, y, z`.
1377                Or any combination of those.
1378            origin : (list)
1379                use this point as the origin of the mirroring transformation.
1380
1381        Examples:
1382            - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py)
1383
1384                ![](https://vedo.embl.es/images/basic/mirror.png)
1385        """
1386        sx, sy, sz = 1, 1, 1
1387        if "x" in axis.lower(): sx = -1
1388        if "y" in axis.lower(): sy = -1
1389        if "z" in axis.lower(): sz = -1
1390
1391        self.scale([sx, sy, sz], origin=origin)
1392
1393        self.pipeline = utils.OperationNode(
1394            "mirror", comment=f"axis = {axis}", parents=[self])
1395
1396        if sx * sy * sz < 0:
1397            if hasattr(self, "reverse"):
1398                self.reverse()
1399        return self
1400
1401    def flip_normals(self) -> Self:
1402        """Flip all normals orientation."""
1403        rs = vtki.new("ReverseSense")
1404        rs.SetInputData(self.dataset)
1405        rs.ReverseCellsOff()
1406        rs.ReverseNormalsOn()
1407        rs.Update()
1408        self._update(rs.GetOutput())
1409        self.pipeline = utils.OperationNode("flip_normals", parents=[self])
1410        return self
1411
1412    def add_gaussian_noise(self, sigma=1.0) -> Self:
1413        """
1414        Add gaussian noise to point positions.
1415        An extra array is added named "GaussianNoise" with the displacements.
1416
1417        Arguments:
1418            sigma : (float)
1419                nr. of standard deviations, expressed in percent of the diagonal size of mesh.
1420                Can also be a list `[sigma_x, sigma_y, sigma_z]`.
1421
1422        Example:
1423            ```python
1424            from vedo import Sphere
1425            Sphere().add_gaussian_noise(1.0).point_size(8).show().close()
1426            ```
1427        """
1428        sz = self.diagonal_size()
1429        pts = self.vertices
1430        n = len(pts)
1431        ns = (np.random.randn(n, 3) * sigma) * (sz / 100)
1432        vpts = vtki.vtkPoints()
1433        vpts.SetNumberOfPoints(n)
1434        vpts.SetData(utils.numpy2vtk(pts + ns, dtype=np.float32))
1435        self.dataset.SetPoints(vpts)
1436        self.dataset.GetPoints().Modified()
1437        self.pointdata["GaussianNoise"] = -ns
1438        self.pipeline = utils.OperationNode(
1439            "gaussian_noise", parents=[self], shape="egg", comment=f"sigma = {sigma}"
1440        )
1441        return self
1442
1443    def closest_point(
1444        self, pt, n=1, radius=None, return_point_id=False, return_cell_id=False
1445    ) -> Union[List[int], int, np.ndarray]:
1446        """
1447        Find the closest point(s) on a mesh given from the input point `pt`.
1448
1449        Arguments:
1450            n : (int)
1451                if greater than 1, return a list of n ordered closest points
1452            radius : (float)
1453                if given, get all points within that radius. Then n is ignored.
1454            return_point_id : (bool)
1455                return point ID instead of coordinates
1456            return_cell_id : (bool)
1457                return cell ID in which the closest point sits
1458
1459        Examples:
1460            - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py)
1461            - [fitplanes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitplanes.py)
1462            - [quadratic_morphing.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/quadratic_morphing.py)
1463
1464        .. note::
1465            The appropriate tree search locator is built on the fly and cached for speed.
1466
1467            If you want to reset it use `mymesh.point_locator=None`
1468            and / or `mymesh.cell_locator=None`.
1469        """
1470        if len(pt) != 3:
1471            pt = [pt[0], pt[1], 0]
1472
1473        # NB: every time the mesh moves or is warped the locators are set to None
1474        if ((n > 1 or radius) or (n == 1 and return_point_id)) and not return_cell_id:
1475            poly = None
1476            if not self.point_locator:
1477                poly = self.dataset
1478                self.point_locator = vtki.new("StaticPointLocator")
1479                self.point_locator.SetDataSet(poly)
1480                self.point_locator.BuildLocator()
1481
1482            ##########
1483            if radius:
1484                vtklist = vtki.vtkIdList()
1485                self.point_locator.FindPointsWithinRadius(radius, pt, vtklist)
1486            elif n > 1:
1487                vtklist = vtki.vtkIdList()
1488                self.point_locator.FindClosestNPoints(n, pt, vtklist)
1489            else:  # n==1 hence return_point_id==True
1490                ########
1491                return self.point_locator.FindClosestPoint(pt)
1492                ########
1493
1494            if return_point_id:
1495                ########
1496                return utils.vtk2numpy(vtklist)
1497                ########
1498
1499            if not poly:
1500                poly = self.dataset
1501            trgp = []
1502            for i in range(vtklist.GetNumberOfIds()):
1503                trgp_ = [0, 0, 0]
1504                vi = vtklist.GetId(i)
1505                poly.GetPoints().GetPoint(vi, trgp_)
1506                trgp.append(trgp_)
1507            ########
1508            return np.array(trgp)
1509            ########
1510
1511        else:
1512
1513            if not self.cell_locator:
1514                poly = self.dataset
1515
1516                # As per Miquel example with limbs the vtkStaticCellLocator doesnt work !!
1517                # https://discourse.vtk.org/t/vtkstaticcelllocator-problem-vtk9-0-3/7854/4
1518                if vedo.vtk_version[0] >= 9 and vedo.vtk_version[1] > 0:
1519                    self.cell_locator = vtki.new("StaticCellLocator")
1520                else:
1521                    self.cell_locator = vtki.new("CellLocator")
1522
1523                self.cell_locator.SetDataSet(poly)
1524                self.cell_locator.BuildLocator()
1525
1526            if radius is not None:
1527                vedo.printc("Warning: closest_point() with radius is not implemented for cells.", c='r')   
1528 
1529            if n != 1:
1530                vedo.printc("Warning: closest_point() with n>1 is not implemented for cells.", c='r')   
1531 
1532            trgp = [0, 0, 0]
1533            cid = vtki.mutable(0)
1534            dist2 = vtki.mutable(0)
1535            subid = vtki.mutable(0)
1536            self.cell_locator.FindClosestPoint(pt, trgp, cid, subid, dist2)
1537
1538            if return_cell_id:
1539                return int(cid)
1540
1541            return np.array(trgp)
1542
1543    def auto_distance(self) -> np.ndarray:
1544        """
1545        Calculate the distance to the closest point in the same cloud of points.
1546        The output is stored in a new pointdata array called "AutoDistance",
1547        and it is also returned by the function.
1548        """
1549        points = self.vertices
1550        if not self.point_locator:
1551            self.point_locator = vtki.new("StaticPointLocator")
1552            self.point_locator.SetDataSet(self.dataset)
1553            self.point_locator.BuildLocator()
1554        qs = []
1555        vtklist = vtki.vtkIdList()
1556        vtkpoints = self.dataset.GetPoints()
1557        for p in points:
1558            self.point_locator.FindClosestNPoints(2, p, vtklist)
1559            q = [0, 0, 0]
1560            pid = vtklist.GetId(1)
1561            vtkpoints.GetPoint(pid, q)
1562            qs.append(q)
1563        dists = np.linalg.norm(points - np.array(qs), axis=1)
1564        self.pointdata["AutoDistance"] = dists
1565        return dists
1566
1567    def hausdorff_distance(self, points) -> float:
1568        """
1569        Compute the Hausdorff distance to the input point set.
1570        Returns a single `float`.
1571
1572        Example:
1573            ```python
1574            from vedo import *
1575            t = np.linspace(0, 2*np.pi, 100)
1576            x = 4/3 * sin(t)**3
1577            y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12
1578            pol1 = Line(np.c_[x,y], closed=True).triangulate()
1579            pol2 = Polygon(nsides=5).pos(2,2)
1580            d12 = pol1.distance_to(pol2)
1581            d21 = pol2.distance_to(pol1)
1582            pol1.lw(0).cmap("viridis")
1583            pol2.lw(0).cmap("viridis")
1584            print("distance d12, d21 :", min(d12), min(d21))
1585            print("hausdorff distance:", pol1.hausdorff_distance(pol2))
1586            print("chamfer distance  :", pol1.chamfer_distance(pol2))
1587            show(pol1, pol2, axes=1)
1588            ```
1589            ![](https://vedo.embl.es/images/feats/heart.png)
1590        """
1591        hp = vtki.new("HausdorffDistancePointSetFilter")
1592        hp.SetInputData(0, self.dataset)
1593        hp.SetInputData(1, points.dataset)
1594        hp.SetTargetDistanceMethodToPointToCell()
1595        hp.Update()
1596        return hp.GetHausdorffDistance()
1597
1598    def chamfer_distance(self, pcloud) -> float:
1599        """
1600        Compute the Chamfer distance to the input point set.
1601
1602        Example:
1603            ```python
1604            from vedo import *
1605            cloud1 = np.random.randn(1000, 3)
1606            cloud2 = np.random.randn(1000, 3) + [1, 2, 3]
1607            c1 = Points(cloud1, r=5, c="red")
1608            c2 = Points(cloud2, r=5, c="green")
1609            d = c1.chamfer_distance(c2)
1610            show(f"Chamfer distance = {d}", c1, c2, axes=1).close()
1611            ```
1612        """
1613        # Definition of Chamfer distance may vary, here we use the average
1614        if not pcloud.point_locator:
1615            pcloud.point_locator = vtki.new("PointLocator")
1616            pcloud.point_locator.SetDataSet(pcloud.dataset)
1617            pcloud.point_locator.BuildLocator()
1618        if not self.point_locator:
1619            self.point_locator = vtki.new("PointLocator")
1620            self.point_locator.SetDataSet(self.dataset)
1621            self.point_locator.BuildLocator()
1622
1623        ps1 = self.vertices
1624        ps2 = pcloud.vertices
1625
1626        ids12 = []
1627        for p in ps1:
1628            pid12 = pcloud.point_locator.FindClosestPoint(p)
1629            ids12.append(pid12)
1630        deltav = ps2[ids12] - ps1
1631        da = np.mean(np.linalg.norm(deltav, axis=1))
1632
1633        ids21 = []
1634        for p in ps2:
1635            pid21 = self.point_locator.FindClosestPoint(p)
1636            ids21.append(pid21)
1637        deltav = ps1[ids21] - ps2
1638        db = np.mean(np.linalg.norm(deltav, axis=1))
1639        return (da + db) / 2
1640
1641    def remove_outliers(self, radius: float, neighbors=5) -> Self:
1642        """
1643        Remove outliers from a cloud of points within the specified `radius` search.
1644
1645        Arguments:
1646            radius : (float)
1647                Specify the local search radius.
1648            neighbors : (int)
1649                Specify the number of neighbors that a point must have,
1650                within the specified radius, for the point to not be considered isolated.
1651
1652        Examples:
1653            - [clustering.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/clustering.py)
1654
1655                ![](https://vedo.embl.es/images/basic/clustering.png)
1656        """
1657        removal = vtki.new("RadiusOutlierRemoval")
1658        removal.SetInputData(self.dataset)
1659        removal.SetRadius(radius)
1660        removal.SetNumberOfNeighbors(neighbors)
1661        removal.GenerateOutliersOff()
1662        removal.Update()
1663        inputobj = removal.GetOutput()
1664        if inputobj.GetNumberOfCells() == 0:
1665            carr = vtki.vtkCellArray()
1666            for i in range(inputobj.GetNumberOfPoints()):
1667                carr.InsertNextCell(1)
1668                carr.InsertCellPoint(i)
1669            inputobj.SetVerts(carr)
1670        self._update(removal.GetOutput())
1671        self.pipeline = utils.OperationNode("remove_outliers", parents=[self])
1672        return self
1673
1674    def relax_point_positions(
1675            self, 
1676            n=10,
1677            iters=10,
1678            sub_iters=10,
1679            packing_factor=1,
1680            max_step=0,
1681            constraints=(),
1682        ) -> Self:
1683        """
1684        Smooth mesh or points with a 
1685        [Laplacian algorithm](https://vtk.org/doc/nightly/html/classvtkPointSmoothingFilter.html)
1686        variant. This modifies the coordinates of the input points by adjusting their positions
1687        to create a smooth distribution (and thereby form a pleasing packing of the points).
1688        Smoothing is performed by considering the effects of neighboring points on one another
1689        it uses a cubic cutoff function to produce repulsive forces between close points
1690        and attractive forces that are a little further away.
1691        
1692        In general, the larger the neighborhood size, the greater the reduction in high frequency
1693        information. The memory and computational requirements of the algorithm may also
1694        significantly increase.
1695
1696        The algorithm incrementally adjusts the point positions through an iterative process.
1697        Basically points are moved due to the influence of neighboring points. 
1698        
1699        As points move, both the local connectivity and data attributes associated with each point
1700        must be updated. Rather than performing these expensive operations after every iteration,
1701        a number of sub-iterations can be specified. If so, then the neighborhood and attribute
1702        value updates occur only every sub iteration, which can improve performance significantly.
1703        
1704        Arguments:
1705            n : (int)
1706                neighborhood size to calculate the Laplacian.
1707            iters : (int)
1708                number of iterations.
1709            sub_iters : (int)
1710                number of sub-iterations, i.e. the number of times the neighborhood and attribute
1711                value updates occur during each iteration.
1712            packing_factor : (float)
1713                adjust convergence speed.
1714            max_step : (float)
1715                Specify the maximum smoothing step size for each smoothing iteration.
1716                This limits the the distance over which a point can move in each iteration.
1717                As in all iterative methods, the stability of the process is sensitive to this parameter.
1718                In general, small step size and large numbers of iterations are more stable than a larger
1719                step size and a smaller numbers of iterations.
1720            constraints : (dict)
1721                dictionary of constraints.
1722                Point constraints are used to prevent points from moving,
1723                or to move only on a plane. This can prevent shrinking or growing point clouds.
1724                If enabled, a local topological analysis is performed to determine whether a point
1725                should be marked as fixed" i.e., never moves, or the point only moves on a plane,
1726                or the point can move freely.
1727                If all points in the neighborhood surrounding a point are in the cone defined by
1728                `fixed_angle`, then the point is classified as fixed.
1729                If all points in the neighborhood surrounding a point are in the cone defined by
1730                `boundary_angle`, then the point is classified as lying on a plane.
1731                Angles are expressed in degrees.
1732        
1733        Example:
1734            ```py
1735            import numpy as np
1736            from vedo import Points, show
1737            from vedo.pyplot import histogram
1738
1739            vpts1 = Points(np.random.rand(10_000, 3))
1740            dists = vpts1.auto_distance()
1741            h1 = histogram(dists, xlim=(0,0.08)).clone2d()
1742
1743            vpts2 = vpts1.clone().relax_point_positions(n=100, iters=20, sub_iters=10)
1744            dists = vpts2.auto_distance()
1745            h2 = histogram(dists, xlim=(0,0.08)).clone2d()
1746
1747            show([[vpts1, h1], [vpts2, h2]], N=2).close()
1748            ```
1749        """
1750        smooth = vtki.new("PointSmoothingFilter")
1751        smooth.SetInputData(self.dataset)
1752        smooth.SetSmoothingModeToUniform()
1753        smooth.SetNumberOfIterations(iters)
1754        smooth.SetNumberOfSubIterations(sub_iters)
1755        smooth.SetPackingFactor(packing_factor)
1756        if self.point_locator:
1757            smooth.SetLocator(self.point_locator)
1758        if not max_step:
1759            max_step = self.diagonal_size() / 100
1760        smooth.SetMaximumStepSize(max_step)
1761        smooth.SetNeighborhoodSize(n)
1762        if constraints:
1763            fixed_angle = constraints.get("fixed_angle", 45)
1764            boundary_angle = constraints.get("boundary_angle", 110)
1765            smooth.EnableConstraintsOn()
1766            smooth.SetFixedAngle(fixed_angle)
1767            smooth.SetBoundaryAngle(boundary_angle)
1768            smooth.GenerateConstraintScalarsOn()
1769            smooth.GenerateConstraintNormalsOn()
1770        smooth.Update()
1771        self._update(smooth.GetOutput())
1772        self.metadata["PackingRadius"] = smooth.GetPackingRadius()
1773        self.pipeline = utils.OperationNode("relax_point_positions", parents=[self])
1774        return self
1775
1776    def smooth_mls_1d(self, f=0.2, radius=None, n=0) -> Self:
1777        """
1778        Smooth mesh or points with a `Moving Least Squares` variant.
1779        The point data array "Variances" will contain the residue calculated for each point.
1780
1781        Arguments:
1782            f : (float)
1783                smoothing factor - typical range is [0,2].
1784            radius : (float)
1785                radius search in absolute units.
1786                If set then `f` is ignored.
1787            n : (int)
1788                number of neighbours to be used for the fit.
1789                If set then `f` and `radius` are ignored.
1790
1791        Examples:
1792            - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py)
1793            - [skeletonize.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/skeletonize.py)
1794
1795            ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png)
1796        """
1797        coords = self.vertices
1798        ncoords = len(coords)
1799
1800        if n:
1801            Ncp = n
1802        elif radius:
1803            Ncp = 1
1804        else:
1805            Ncp = int(ncoords * f / 10)
1806            if Ncp < 5:
1807                vedo.logger.warning(f"Please choose a fraction higher than {f}")
1808                Ncp = 5
1809
1810        variances, newline = [], []
1811        for p in coords:
1812            points = self.closest_point(p, n=Ncp, radius=radius)
1813            if len(points) < 4:
1814                continue
1815
1816            points = np.array(points)
1817            pointsmean = points.mean(axis=0)  # plane center
1818            _, dd, vv = np.linalg.svd(points - pointsmean)
1819            newp = np.dot(p - pointsmean, vv[0]) * vv[0] + pointsmean
1820            variances.append(dd[1] + dd[2])
1821            newline.append(newp)
1822
1823        self.pointdata["Variances"] = np.array(variances).astype(np.float32)
1824        self.vertices = newline
1825        self.pipeline = utils.OperationNode("smooth_mls_1d", parents=[self])
1826        return self
1827
1828    def smooth_mls_2d(self, f=0.2, radius=None, n=0) -> Self:
1829        """
1830        Smooth mesh or points with a `Moving Least Squares` algorithm variant.
1831
1832        The `mesh.pointdata['MLSVariance']` array will contain the residue calculated for each point.
1833        When a radius is specified, points that are isolated will not be moved and will get
1834        a 0 entry in array `mesh.pointdata['MLSValidPoint']`.
1835
1836        Arguments:
1837            f : (float)
1838                smoothing factor - typical range is [0, 2].
1839            radius : (float | array)
1840                radius search in absolute units. Can be single value (float) or sequence
1841                for adaptive smoothing. If set then `f` is ignored.
1842            n : (int)
1843                number of neighbours to be used for the fit.
1844                If set then `f` and `radius` are ignored.
1845
1846        Examples:
1847            - [moving_least_squares2D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares2D.py)
1848            - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py)
1849
1850                ![](https://vedo.embl.es/images/advanced/recosurface.png)
1851        """
1852        coords = self.vertices
1853        ncoords = len(coords)
1854
1855        if n:
1856            Ncp = n
1857            radius = None
1858        elif radius is not None:
1859            Ncp = 1
1860        else:
1861            Ncp = int(ncoords * f / 100)
1862            if Ncp < 4:
1863                vedo.logger.error(f"please choose a f-value higher than {f}")
1864                Ncp = 4
1865
1866        variances, newpts, valid = [], [], []
1867        radius_is_sequence = utils.is_sequence(radius)
1868
1869        pb = None
1870        if ncoords > 10000:
1871            pb = utils.ProgressBar(0, ncoords, delay=3)
1872
1873        for i, p in enumerate(coords):
1874            if pb:
1875                pb.print("smooth_mls_2d working ...")
1876            
1877            # if a radius was provided for each point
1878            if radius_is_sequence:
1879                pts = self.closest_point(p, n=Ncp, radius=radius[i])
1880            else:
1881                pts = self.closest_point(p, n=Ncp, radius=radius)
1882
1883            if len(pts) > 3:
1884                ptsmean = pts.mean(axis=0)  # plane center
1885                _, dd, vv = np.linalg.svd(pts - ptsmean)
1886                cv = np.cross(vv[0], vv[1])
1887                t = (np.dot(cv, ptsmean) - np.dot(cv, p)) / np.dot(cv, cv)
1888                newpts.append(p + cv * t)
1889                variances.append(dd[2])
1890                if radius is not None:
1891                    valid.append(1)
1892            else:
1893                newpts.append(p)
1894                variances.append(0)
1895                if radius is not None:
1896                    valid.append(0)
1897
1898        if radius is not None:
1899            self.pointdata["MLSValidPoint"] = np.array(valid).astype(np.uint8)
1900        self.pointdata["MLSVariance"] = np.array(variances).astype(np.float32)
1901
1902        self.vertices = newpts
1903
1904        self.pipeline = utils.OperationNode("smooth_mls_2d", parents=[self])
1905        return self
1906
1907    def smooth_lloyd_2d(self, iterations=2, bounds=None, options="Qbb Qc Qx") -> Self:
1908        """
1909        Lloyd relaxation of a 2D pointcloud.
1910        
1911        Arguments:
1912            iterations : (int)
1913                number of iterations.
1914            bounds : (list)
1915                bounding box of the domain.
1916            options : (str)
1917                options for the Qhull algorithm.
1918        """
1919        # Credits: https://hatarilabs.com/ih-en/
1920        # tutorial-to-create-a-geospatial-voronoi-sh-mesh-with-python-scipy-and-geopandas
1921        from scipy.spatial import Voronoi as scipy_voronoi
1922
1923        def _constrain_points(points):
1924            # Update any points that have drifted beyond the boundaries of this space
1925            if bounds is not None:
1926                for point in points:
1927                    if point[0] < bounds[0]: point[0] = bounds[0]
1928                    if point[0] > bounds[1]: point[0] = bounds[1]
1929                    if point[1] < bounds[2]: point[1] = bounds[2]
1930                    if point[1] > bounds[3]: point[1] = bounds[3]
1931            return points
1932
1933        def _find_centroid(vertices):
1934            # The equation for the method used here to find the centroid of a
1935            # 2D polygon is given here: https://en.wikipedia.org/wiki/Centroid#Of_a_polygon
1936            area = 0
1937            centroid_x = 0
1938            centroid_y = 0
1939            for i in range(len(vertices) - 1):
1940                step = (vertices[i, 0] * vertices[i + 1, 1]) - (vertices[i + 1, 0] * vertices[i, 1])
1941                centroid_x += (vertices[i, 0] + vertices[i + 1, 0]) * step
1942                centroid_y += (vertices[i, 1] + vertices[i + 1, 1]) * step
1943                area += step
1944            if area:
1945                centroid_x = (1.0 / (3.0 * area)) * centroid_x
1946                centroid_y = (1.0 / (3.0 * area)) * centroid_y
1947            # prevent centroids from escaping bounding box
1948            return _constrain_points([[centroid_x, centroid_y]])[0]
1949
1950        def _relax(voron):
1951            # Moves each point to the centroid of its cell in the voronoi
1952            # map to "relax" the points (i.e. jitter the points so as
1953            # to spread them out within the space).
1954            centroids = []
1955            for idx in voron.point_region:
1956                # the region is a series of indices into voronoi.vertices
1957                # remove point at infinity, designated by index -1
1958                region = [i for i in voron.regions[idx] if i != -1]
1959                # enclose the polygon
1960                region = region + [region[0]]
1961                verts = voron.vertices[region]
1962                # find the centroid of those vertices
1963                centroids.append(_find_centroid(verts))
1964            return _constrain_points(centroids)
1965
1966        if bounds is None:
1967            bounds = self.bounds()
1968
1969        pts = self.vertices[:, (0, 1)]
1970        for i in range(iterations):
1971            vor = scipy_voronoi(pts, qhull_options=options)
1972            _constrain_points(vor.vertices)
1973            pts = _relax(vor)
1974        out = Points(pts)
1975        out.name = "MeshSmoothLloyd2D"
1976        out.pipeline = utils.OperationNode("smooth_lloyd", parents=[self])
1977        return out
1978
1979    def project_on_plane(self, plane="z", point=None, direction=None) -> Self:
1980        """
1981        Project the mesh on one of the Cartesian planes.
1982
1983        Arguments:
1984            plane : (str, Plane)
1985                if plane is `str`, plane can be one of ['x', 'y', 'z'],
1986                represents x-plane, y-plane and z-plane, respectively.
1987                Otherwise, plane should be an instance of `vedo.shapes.Plane`.
1988            point : (float, array)
1989                if plane is `str`, point should be a float represents the intercept.
1990                Otherwise, point is the camera point of perspective projection
1991            direction : (array)
1992                direction of oblique projection
1993
1994        Note:
1995            Parameters `point` and `direction` are only used if the given plane
1996            is an instance of `vedo.shapes.Plane`. And one of these two params
1997            should be left as `None` to specify the projection type.
1998
1999        Example:
2000            ```python
2001            s.project_on_plane(plane='z') # project to z-plane
2002            plane = Plane(pos=(4, 8, -4), normal=(-1, 0, 1), s=(5,5))
2003            s.project_on_plane(plane=plane)                       # orthogonal projection
2004            s.project_on_plane(plane=plane, point=(6, 6, 6))      # perspective projection
2005            s.project_on_plane(plane=plane, direction=(1, 2, -1)) # oblique projection
2006            ```
2007
2008        Examples:
2009            - [silhouette2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette2.py)
2010
2011                ![](https://vedo.embl.es/images/basic/silhouette2.png)
2012        """
2013        coords = self.vertices
2014
2015        if plane == "x":
2016            coords[:, 0] = self.transform.position[0]
2017            intercept = self.xbounds()[0] if point is None else point
2018            self.x(intercept)
2019        elif plane == "y":
2020            coords[:, 1] = self.transform.position[1]
2021            intercept = self.ybounds()[0] if point is None else point
2022            self.y(intercept)
2023        elif plane == "z":
2024            coords[:, 2] = self.transform.position[2]
2025            intercept = self.zbounds()[0] if point is None else point
2026            self.z(intercept)
2027
2028        elif isinstance(plane, vedo.shapes.Plane):
2029            normal = plane.normal / np.linalg.norm(plane.normal)
2030            pl = np.hstack((normal, -np.dot(plane.pos(), normal))).reshape(4, 1)
2031            if direction is None and point is None:
2032                # orthogonal projection
2033                pt = np.hstack((normal, [0])).reshape(4, 1)
2034                # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T # python3 only
2035                proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T)
2036
2037            elif direction is None:
2038                # perspective projection
2039                pt = np.hstack((np.array(point), [1])).reshape(4, 1)
2040                # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T
2041                proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T)
2042
2043            elif point is None:
2044                # oblique projection
2045                pt = np.hstack((np.array(direction), [0])).reshape(4, 1)
2046                # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T
2047                proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T)
2048
2049            coords = np.concatenate([coords, np.ones((coords.shape[:-1] + (1,)))], axis=-1)
2050            # coords = coords @ proj_mat.T
2051            coords = np.matmul(coords, proj_mat.T)
2052            coords = coords[:, :3] / coords[:, 3:]
2053
2054        else:
2055            vedo.logger.error(f"unknown plane {plane}")
2056            raise RuntimeError()
2057
2058        self.alpha(0.1)
2059        self.vertices = coords
2060        return self
2061
2062    def warp(self, source, target, sigma=1.0, mode="3d") -> Self:
2063        """
2064        "Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set
2065        of source and target landmarks. Any point on the mesh close to a source landmark will
2066        be moved to a place close to the corresponding target landmark.
2067        The points in between are interpolated smoothly using
2068        Bookstein's Thin Plate Spline algorithm.
2069
2070        Transformation object can be accessed with `mesh.transform`.
2071
2072        Arguments:
2073            sigma : (float)
2074                specify the 'stiffness' of the spline.
2075            mode : (str)
2076                set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes)
2077
2078        Examples:
2079            - [interpolate_field.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/interpolate_field.py)
2080            - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py)
2081            - [warp2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp2.py)
2082            - [warp3.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp3.py)
2083            - [warp4a.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4a.py)
2084            - [warp4b.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4b.py)
2085            - [warp6.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp6.py)
2086
2087            ![](https://vedo.embl.es/images/advanced/warp2.png)
2088        """
2089        parents = [self]
2090
2091        try:
2092            source = source.vertices
2093            parents.append(source)
2094        except AttributeError:
2095            source = utils.make3d(source)
2096        
2097        try:
2098            target = target.vertices
2099            parents.append(target)
2100        except AttributeError:
2101            target = utils.make3d(target)
2102
2103        ns = len(source)
2104        nt = len(target)
2105        if ns != nt:
2106            vedo.logger.error(f"#source {ns} != {nt} #target points")
2107            raise RuntimeError()
2108
2109        NLT = NonLinearTransform()
2110        NLT.source_points = source
2111        NLT.target_points = target
2112        self.apply_transform(NLT)
2113
2114        self.pipeline = utils.OperationNode("warp", parents=parents)
2115        return self
2116
2117    def cut_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0), invert=False) -> Self:
2118        """
2119        Cut the mesh with the plane defined by a point and a normal.
2120
2121        Arguments:
2122            origin : (array)
2123                the cutting plane goes through this point
2124            normal : (array)
2125                normal of the cutting plane
2126
2127        Example:
2128            ```python
2129            from vedo import Cube
2130            cube = Cube().cut_with_plane(normal=(1,1,1))
2131            cube.back_color('pink').show().close()
2132            ```
2133            ![](https://vedo.embl.es/images/feats/cut_with_plane_cube.png)
2134
2135        Examples:
2136            - [trail.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/trail.py)
2137
2138                ![](https://vedo.embl.es/images/simulations/trail.gif)
2139
2140        Check out also:
2141            `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`.
2142        """
2143        s = str(normal)
2144        if "x" in s:
2145            normal = (1, 0, 0)
2146            if "-" in s:
2147                normal = -np.array(normal)
2148        elif "y" in s:
2149            normal = (0, 1, 0)
2150            if "-" in s:
2151                normal = -np.array(normal)
2152        elif "z" in s:
2153            normal = (0, 0, 1)
2154            if "-" in s:
2155                normal = -np.array(normal)
2156        plane = vtki.vtkPlane()
2157        plane.SetOrigin(origin)
2158        plane.SetNormal(normal)
2159
2160        clipper = vtki.new("ClipPolyData")
2161        clipper.SetInputData(self.dataset)
2162        clipper.SetClipFunction(plane)
2163        clipper.GenerateClippedOutputOff()
2164        clipper.GenerateClipScalarsOff()
2165        clipper.SetInsideOut(invert)
2166        clipper.SetValue(0)
2167        clipper.Update()
2168
2169        self._update(clipper.GetOutput())
2170
2171        self.pipeline = utils.OperationNode("cut_with_plane", parents=[self])
2172        return self
2173
2174    def cut_with_planes(self, origins, normals, invert=False) -> Self:
2175        """
2176        Cut the mesh with a convex set of planes defined by points and normals.
2177
2178        Arguments:
2179            origins : (array)
2180                each cutting plane goes through this point
2181            normals : (array)
2182                normal of each of the cutting planes
2183            invert : (bool)
2184                if True, cut outside instead of inside
2185
2186        Check out also:
2187            `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`
2188        """
2189
2190        vpoints = vtki.vtkPoints()
2191        for p in utils.make3d(origins):
2192            vpoints.InsertNextPoint(p)
2193        normals = utils.make3d(normals)
2194
2195        planes = vtki.vtkPlanes()
2196        planes.SetPoints(vpoints)
2197        planes.SetNormals(utils.numpy2vtk(normals, dtype=float))
2198
2199        clipper = vtki.new("ClipPolyData")
2200        clipper.SetInputData(self.dataset)
2201        clipper.SetInsideOut(invert)
2202        clipper.SetClipFunction(planes)
2203        clipper.GenerateClippedOutputOff()
2204        clipper.GenerateClipScalarsOff()
2205        clipper.SetValue(0)
2206        clipper.Update()
2207
2208        self._update(clipper.GetOutput())
2209
2210        self.pipeline = utils.OperationNode("cut_with_planes", parents=[self])
2211        return self
2212
2213    def cut_with_box(self, bounds, invert=False) -> Self:
2214        """
2215        Cut the current mesh with a box or a set of boxes.
2216        This is much faster than `cut_with_mesh()`.
2217
2218        Input `bounds` can be either:
2219        - a Mesh or Points object
2220        - a list of 6 number representing a bounding box `[xmin,xmax, ymin,ymax, zmin,zmax]`
2221        - a list of bounding boxes like the above: `[[xmin1,...], [xmin2,...], ...]`
2222
2223        Example:
2224            ```python
2225            from vedo import Sphere, Cube, show
2226            mesh = Sphere(r=1, res=50)
2227            box  = Cube(side=1.5).wireframe()
2228            mesh.cut_with_box(box)
2229            show(mesh, box, axes=1).close()
2230            ```
2231            ![](https://vedo.embl.es/images/feats/cut_with_box_cube.png)
2232
2233        Check out also:
2234            `cut_with_line()`, `cut_with_plane()`, `cut_with_cylinder()`
2235        """
2236        if isinstance(bounds, Points):
2237            bounds = bounds.bounds()
2238
2239        box = vtki.new("Box")
2240        if utils.is_sequence(bounds[0]):
2241            for bs in bounds:
2242                box.AddBounds(bs)
2243        else:
2244            box.SetBounds(bounds)
2245
2246        clipper = vtki.new("ClipPolyData")
2247        clipper.SetInputData(self.dataset)
2248        clipper.SetClipFunction(box)
2249        clipper.SetInsideOut(not invert)
2250        clipper.GenerateClippedOutputOff()
2251        clipper.GenerateClipScalarsOff()
2252        clipper.SetValue(0)
2253        clipper.Update()
2254        self._update(clipper.GetOutput())
2255
2256        self.pipeline = utils.OperationNode("cut_with_box", parents=[self])
2257        return self
2258
2259    def cut_with_line(self, points, invert=False, closed=True) -> Self:
2260        """
2261        Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter.
2262        The polyline is defined by a set of points (z-coordinates are ignored).
2263        This is much faster than `cut_with_mesh()`.
2264
2265        Check out also:
2266            `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()`
2267        """
2268        pplane = vtki.new("PolyPlane")
2269        if isinstance(points, Points):
2270            points = points.vertices.tolist()
2271
2272        if closed:
2273            if isinstance(points, np.ndarray):
2274                points = points.tolist()
2275            points.append(points[0])
2276
2277        vpoints = vtki.vtkPoints()
2278        for p in points:
2279            if len(p) == 2:
2280                p = [p[0], p[1], 0.0]
2281            vpoints.InsertNextPoint(p)
2282
2283        n = len(points)
2284        polyline = vtki.new("PolyLine")
2285        polyline.Initialize(n, vpoints)
2286        polyline.GetPointIds().SetNumberOfIds(n)
2287        for i in range(n):
2288            polyline.GetPointIds().SetId(i, i)
2289        pplane.SetPolyLine(polyline)
2290
2291        clipper = vtki.new("ClipPolyData")
2292        clipper.SetInputData(self.dataset)
2293        clipper.SetClipFunction(pplane)
2294        clipper.SetInsideOut(invert)
2295        clipper.GenerateClippedOutputOff()
2296        clipper.GenerateClipScalarsOff()
2297        clipper.SetValue(0)
2298        clipper.Update()
2299        self._update(clipper.GetOutput())
2300
2301        self.pipeline = utils.OperationNode("cut_with_line", parents=[self])
2302        return self
2303
2304    def cut_with_cookiecutter(self, lines) -> Self:
2305        """
2306        Cut the current mesh with a single line or a set of lines.
2307
2308        Input `lines` can be either:
2309        - a `Mesh` or `Points` object
2310        - a list of 3D points: `[(x1,y1,z1), (x2,y2,z2), ...]`
2311        - a list of 2D points: `[(x1,y1), (x2,y2), ...]`
2312
2313        Example:
2314            ```python
2315            from vedo import *
2316            grid = Mesh(dataurl + "dolfin_fine.vtk")
2317            grid.compute_quality().cmap("Greens")
2318            pols = merge(
2319                Polygon(nsides=10, r=0.3).pos(0.7, 0.3),
2320                Polygon(nsides=10, r=0.2).pos(0.3, 0.7),
2321            )
2322            lines = pols.boundaries()
2323            cgrid = grid.clone().cut_with_cookiecutter(lines)
2324            grid.alpha(0.1).wireframe()
2325            show(grid, cgrid, lines, axes=8, bg='blackboard').close()
2326            ```
2327            ![](https://vedo.embl.es/images/feats/cookiecutter.png)
2328
2329        Check out also:
2330            `cut_with_line()` and `cut_with_point_loop()`
2331
2332        Note:
2333            In case of a warning message like:
2334                "Mesh and trim loop point data attributes are different"
2335            consider interpolating the mesh point data to the loop points,
2336            Eg. (in the above example):
2337            ```python
2338            lines = pols.boundaries().interpolate_data_from(grid, n=2)
2339            ```
2340
2341        Note:
2342            trying to invert the selection by reversing the loop order
2343            will have no effect in this method, hence it does not have
2344            the `invert` option.
2345        """
2346        if utils.is_sequence(lines):
2347            lines = utils.make3d(lines)
2348            iline = list(range(len(lines))) + [0]
2349            poly = utils.buildPolyData(lines, lines=[iline])
2350        else:
2351            poly = lines.dataset
2352
2353        # if invert: # not working
2354        #     rev = vtki.new("ReverseSense")
2355        #     rev.ReverseCellsOn()
2356        #     rev.SetInputData(poly)
2357        #     rev.Update()
2358        #     poly = rev.GetOutput()
2359
2360        # Build loops from the polyline
2361        build_loops = vtki.new("ContourLoopExtraction")
2362        build_loops.SetGlobalWarningDisplay(0)
2363        build_loops.SetInputData(poly)
2364        build_loops.Update()
2365        boundary_poly = build_loops.GetOutput()
2366
2367        ccut = vtki.new("CookieCutter")
2368        ccut.SetInputData(self.dataset)
2369        ccut.SetLoopsData(boundary_poly)
2370        ccut.SetPointInterpolationToMeshEdges()
2371        # ccut.SetPointInterpolationToLoopEdges()
2372        ccut.PassCellDataOn()
2373        ccut.PassPointDataOn()
2374        ccut.Update()
2375        self._update(ccut.GetOutput())
2376
2377        self.pipeline = utils.OperationNode("cut_with_cookiecutter", parents=[self])
2378        return self
2379
2380    def cut_with_cylinder(self, center=(0, 0, 0), axis=(0, 0, 1), r=1, invert=False) -> Self:
2381        """
2382        Cut the current mesh with an infinite cylinder.
2383        This is much faster than `cut_with_mesh()`.
2384
2385        Arguments:
2386            center : (array)
2387                the center of the cylinder
2388            normal : (array)
2389                direction of the cylinder axis
2390            r : (float)
2391                radius of the cylinder
2392
2393        Example:
2394            ```python
2395            from vedo import Disc, show
2396            disc = Disc(r1=1, r2=1.2)
2397            mesh = disc.extrude(3, res=50).linewidth(1)
2398            mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True)
2399            show(mesh, axes=1).close()
2400            ```
2401            ![](https://vedo.embl.es/images/feats/cut_with_cylinder.png)
2402
2403        Examples:
2404            - [optics_main1.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/optics_main1.py)
2405
2406        Check out also:
2407            `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()`
2408        """
2409        s = str(axis)
2410        if "x" in s:
2411            axis = (1, 0, 0)
2412        elif "y" in s:
2413            axis = (0, 1, 0)
2414        elif "z" in s:
2415            axis = (0, 0, 1)
2416        cyl = vtki.new("Cylinder")
2417        cyl.SetCenter(center)
2418        cyl.SetAxis(axis[0], axis[1], axis[2])
2419        cyl.SetRadius(r)
2420
2421        clipper = vtki.new("ClipPolyData")
2422        clipper.SetInputData(self.dataset)
2423        clipper.SetClipFunction(cyl)
2424        clipper.SetInsideOut(not invert)
2425        clipper.GenerateClippedOutputOff()
2426        clipper.GenerateClipScalarsOff()
2427        clipper.SetValue(0)
2428        clipper.Update()
2429        self._update(clipper.GetOutput())
2430
2431        self.pipeline = utils.OperationNode("cut_with_cylinder", parents=[self])
2432        return self
2433
2434    def cut_with_sphere(self, center=(0, 0, 0), r=1.0, invert=False) -> Self:
2435        """
2436        Cut the current mesh with an sphere.
2437        This is much faster than `cut_with_mesh()`.
2438
2439        Arguments:
2440            center : (array)
2441                the center of the sphere
2442            r : (float)
2443                radius of the sphere
2444
2445        Example:
2446            ```python
2447            from vedo import Disc, show
2448            disc = Disc(r1=1, r2=1.2)
2449            mesh = disc.extrude(3, res=50).linewidth(1)
2450            mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True)
2451            show(mesh, axes=1).close()
2452            ```
2453            ![](https://vedo.embl.es/images/feats/cut_with_sphere.png)
2454
2455        Check out also:
2456            `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()`
2457        """
2458        sph = vtki.new("Sphere")
2459        sph.SetCenter(center)
2460        sph.SetRadius(r)
2461
2462        clipper = vtki.new("ClipPolyData")
2463        clipper.SetInputData(self.dataset)
2464        clipper.SetClipFunction(sph)
2465        clipper.SetInsideOut(not invert)
2466        clipper.GenerateClippedOutputOff()
2467        clipper.GenerateClipScalarsOff()
2468        clipper.SetValue(0)
2469        clipper.Update()
2470        self._update(clipper.GetOutput())
2471        self.pipeline = utils.OperationNode("cut_with_sphere", parents=[self])
2472        return self
2473
2474    def cut_with_mesh(self, mesh, invert=False, keep=False) -> Union[Self, "vedo.Assembly"]:
2475        """
2476        Cut an `Mesh` mesh with another `Mesh`.
2477
2478        Use `invert` to invert the selection.
2479
2480        Use `keep` to keep the cutoff part, in this case an `Assembly` is returned:
2481        the "cut" object and the "discarded" part of the original object.
2482        You can access both via `assembly.unpack()` method.
2483
2484        Example:
2485        ```python
2486        from vedo import *
2487        arr = np.random.randn(100000, 3)/2
2488        pts = Points(arr).c('red3').pos(5,0,0)
2489        cube = Cube().pos(4,0.5,0)
2490        assem = pts.cut_with_mesh(cube, keep=True)
2491        show(assem.unpack(), axes=1).close()
2492        ```
2493        ![](https://vedo.embl.es/images/feats/cut_with_mesh.png)
2494
2495       Check out also:
2496            `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()`
2497       """
2498        polymesh = mesh.dataset
2499        poly = self.dataset
2500
2501        # Create an array to hold distance information
2502        signed_distances = vtki.vtkFloatArray()
2503        signed_distances.SetNumberOfComponents(1)
2504        signed_distances.SetName("SignedDistances")
2505
2506        # implicit function that will be used to slice the mesh
2507        ippd = vtki.new("ImplicitPolyDataDistance")
2508        ippd.SetInput(polymesh)
2509
2510        # Evaluate the signed distance function at all of the grid points
2511        for pointId in range(poly.GetNumberOfPoints()):
2512            p = poly.GetPoint(pointId)
2513            signed_distance = ippd.EvaluateFunction(p)
2514            signed_distances.InsertNextValue(signed_distance)
2515
2516        currentscals = poly.GetPointData().GetScalars()
2517        if currentscals:
2518            currentscals = currentscals.GetName()
2519
2520        poly.GetPointData().AddArray(signed_distances)
2521        poly.GetPointData().SetActiveScalars("SignedDistances")
2522
2523        clipper = vtki.new("ClipPolyData")
2524        clipper.SetInputData(poly)
2525        clipper.SetInsideOut(not invert)
2526        clipper.SetGenerateClippedOutput(keep)
2527        clipper.SetValue(0.0)
2528        clipper.Update()
2529        cpoly = clipper.GetOutput()
2530
2531        if keep:
2532            kpoly = clipper.GetOutput(1)
2533
2534        vis = False
2535        if currentscals:
2536            cpoly.GetPointData().SetActiveScalars(currentscals)
2537            vis = self.mapper.GetScalarVisibility()
2538
2539        self._update(cpoly)
2540
2541        self.pointdata.remove("SignedDistances")
2542        self.mapper.SetScalarVisibility(vis)
2543        if keep:
2544            if isinstance(self, vedo.Mesh):
2545                cutoff = vedo.Mesh(kpoly)
2546            else:
2547                cutoff = vedo.Points(kpoly)
2548            # cutoff = self.__class__(kpoly) # this does not work properly
2549            cutoff.properties = vtki.vtkProperty()
2550            cutoff.properties.DeepCopy(self.properties)
2551            cutoff.actor.SetProperty(cutoff.properties)
2552            cutoff.c("k5").alpha(0.2)
2553            return vedo.Assembly([self, cutoff])
2554
2555        self.pipeline = utils.OperationNode("cut_with_mesh", parents=[self, mesh])
2556        return self
2557
2558    def cut_with_point_loop(
2559        self, points, invert=False, on="points", include_boundary=False
2560    ) -> Self:
2561        """
2562        Cut an `Mesh` object with a set of points forming a closed loop.
2563
2564        Arguments:
2565            invert : (bool)
2566                invert selection (inside-out)
2567            on : (str)
2568                if 'cells' will extract the whole cells lying inside (or outside) the point loop
2569            include_boundary : (bool)
2570                include cells lying exactly on the boundary line. Only relevant on 'cells' mode
2571
2572        Examples:
2573            - [cut_with_points1.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points1.py)
2574
2575                ![](https://vedo.embl.es/images/advanced/cutWithPoints1.png)
2576
2577            - [cut_with_points2.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points2.py)
2578
2579                ![](https://vedo.embl.es/images/advanced/cutWithPoints2.png)
2580        """
2581        if isinstance(points, Points):
2582            parents = [points]
2583            vpts = points.dataset.GetPoints()
2584            points = points.vertices
2585        else:
2586            parents = [self]
2587            vpts = vtki.vtkPoints()
2588            points = utils.make3d(points)
2589            for p in points:
2590                vpts.InsertNextPoint(p)
2591
2592        if "cell" in on:
2593            ippd = vtki.new("ImplicitSelectionLoop")
2594            ippd.SetLoop(vpts)
2595            ippd.AutomaticNormalGenerationOn()
2596            clipper = vtki.new("ExtractPolyDataGeometry")
2597            clipper.SetInputData(self.dataset)
2598            clipper.SetImplicitFunction(ippd)
2599            clipper.SetExtractInside(not invert)
2600            clipper.SetExtractBoundaryCells(include_boundary)
2601        else:
2602            spol = vtki.new("SelectPolyData")
2603            spol.SetLoop(vpts)
2604            spol.GenerateSelectionScalarsOn()
2605            spol.GenerateUnselectedOutputOff()
2606            spol.SetInputData(self.dataset)
2607            spol.Update()
2608            clipper = vtki.new("ClipPolyData")
2609            clipper.SetInputData(spol.GetOutput())
2610            clipper.SetInsideOut(not invert)
2611            clipper.SetValue(0.0)
2612        clipper.Update()
2613        self._update(clipper.GetOutput())
2614
2615        self.pipeline = utils.OperationNode("cut_with_pointloop", parents=parents)
2616        return self
2617
2618    def cut_with_scalar(self, value: float, name="", invert=False) -> Self:
2619        """
2620        Cut a mesh or point cloud with some input scalar point-data.
2621
2622        Arguments:
2623            value : (float)
2624                cutting value
2625            name : (str)
2626                array name of the scalars to be used
2627            invert : (bool)
2628                flip selection
2629
2630        Example:
2631            ```python
2632            from vedo import *
2633            s = Sphere().lw(1)
2634            pts = s.vertices
2635            scalars = np.sin(3*pts[:,2]) + pts[:,0]
2636            s.pointdata["somevalues"] = scalars
2637            s.cut_with_scalar(0.3)
2638            s.cmap("Spectral", "somevalues").add_scalarbar()
2639            s.show(axes=1).close()
2640            ```
2641            ![](https://vedo.embl.es/images/feats/cut_with_scalars.png)
2642        """
2643        if name:
2644            self.pointdata.select(name)
2645        clipper = vtki.new("ClipPolyData")
2646        clipper.SetInputData(self.dataset)
2647        clipper.SetValue(value)
2648        clipper.GenerateClippedOutputOff()
2649        clipper.SetInsideOut(not invert)
2650        clipper.Update()
2651        self._update(clipper.GetOutput())
2652        self.pipeline = utils.OperationNode("cut_with_scalar", parents=[self])
2653        return self
2654
2655    def crop(self,
2656             top=None, bottom=None, right=None, left=None, front=None, back=None,
2657             bounds=()) -> Self:
2658        """
2659        Crop an `Mesh` object.
2660
2661        Arguments:
2662            top : (float)
2663                fraction to crop from the top plane (positive z)
2664            bottom : (float)
2665                fraction to crop from the bottom plane (negative z)
2666            front : (float)
2667                fraction to crop from the front plane (positive y)
2668            back : (float)
2669                fraction to crop from the back plane (negative y)
2670            right : (float)
2671                fraction to crop from the right plane (positive x)
2672            left : (float)
2673                fraction to crop from the left plane (negative x)
2674            bounds : (list)
2675                bounding box of the crop region as `[x0,x1, y0,y1, z0,z1]`
2676
2677        Example:
2678            ```python
2679            from vedo import Sphere
2680            Sphere().crop(right=0.3, left=0.1).show()
2681            ```
2682            ![](https://user-images.githubusercontent.com/32848391/57081955-0ef1e800-6cf6-11e9-99de-b45220939bc9.png)
2683        """
2684        if not len(bounds):
2685            pos = np.array(self.pos())
2686            x0, x1, y0, y1, z0, z1 = self.bounds()
2687            x0, y0, z0 = [x0, y0, z0] - pos
2688            x1, y1, z1 = [x1, y1, z1] - pos
2689
2690            dx, dy, dz = x1 - x0, y1 - y0, z1 - z0
2691            if top:
2692                z1 = z1 - top * dz
2693            if bottom:
2694                z0 = z0 + bottom * dz
2695            if front:
2696                y1 = y1 - front * dy
2697            if back:
2698                y0 = y0 + back * dy
2699            if right:
2700                x1 = x1 - right * dx
2701            if left:
2702                x0 = x0 + left * dx
2703            bounds = (x0, x1, y0, y1, z0, z1)
2704
2705        cu = vtki.new("Box")
2706        cu.SetBounds(bounds)
2707
2708        clipper = vtki.new("ClipPolyData")
2709        clipper.SetInputData(self.dataset)
2710        clipper.SetClipFunction(cu)
2711        clipper.InsideOutOn()
2712        clipper.GenerateClippedOutputOff()
2713        clipper.GenerateClipScalarsOff()
2714        clipper.SetValue(0)
2715        clipper.Update()
2716        self._update(clipper.GetOutput())
2717
2718        self.pipeline = utils.OperationNode(
2719            "crop", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
2720        )
2721        return self
2722
2723    def generate_surface_halo(
2724            self, 
2725            distance=0.05,
2726            res=(50, 50, 50),
2727            bounds=(),
2728            maxdist=None,
2729    ) -> "vedo.Mesh":
2730        """
2731        Generate the surface halo which sits at the specified distance from the input one.
2732
2733        Arguments:
2734            distance : (float)
2735                distance from the input surface
2736            res : (int)
2737                resolution of the surface
2738            bounds : (list)
2739                bounding box of the surface
2740            maxdist : (float)
2741                maximum distance to generate the surface
2742        """
2743        if not bounds:
2744            bounds = self.bounds()
2745
2746        if not maxdist:
2747            maxdist = self.diagonal_size() / 2
2748
2749        imp = vtki.new("ImplicitModeller")
2750        imp.SetInputData(self.dataset)
2751        imp.SetSampleDimensions(res)
2752        if maxdist:
2753            imp.SetMaximumDistance(maxdist)
2754        if len(bounds) == 6:
2755            imp.SetModelBounds(bounds)
2756        contour = vtki.new("ContourFilter")
2757        contour.SetInputConnection(imp.GetOutputPort())
2758        contour.SetValue(0, distance)
2759        contour.Update()
2760        out = vedo.Mesh(contour.GetOutput())
2761        out.c("lightblue").alpha(0.25).lighting("off")
2762        out.pipeline = utils.OperationNode("generate_surface_halo", parents=[self])
2763        return out
2764
2765    def generate_mesh(
2766        self,
2767        line_resolution=None,
2768        mesh_resolution=None,
2769        smooth=0.0,
2770        jitter=0.001,
2771        grid=None,
2772        quads=False,
2773        invert=False,
2774    ) -> Self:
2775        """
2776        Generate a polygonal Mesh from a closed contour line.
2777        If line is not closed it will be closed with a straight segment.
2778
2779        Check also `generate_delaunay2d()`.
2780
2781        Arguments:
2782            line_resolution : (int)
2783                resolution of the contour line. The default is None, in this case
2784                the contour is not resampled.
2785            mesh_resolution : (int)
2786                resolution of the internal triangles not touching the boundary.
2787            smooth : (float)
2788                smoothing of the contour before meshing.
2789            jitter : (float)
2790                add a small noise to the internal points.
2791            grid : (Grid)
2792                manually pass a Grid object. The default is True.
2793            quads : (bool)
2794                generate a mesh of quads instead of triangles.
2795            invert : (bool)
2796                flip the line orientation. The default is False.
2797
2798        Examples:
2799            - [line2mesh_tri.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_tri.py)
2800
2801                ![](https://vedo.embl.es/images/advanced/line2mesh_tri.jpg)
2802
2803            - [line2mesh_quads.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_quads.py)
2804
2805                ![](https://vedo.embl.es/images/advanced/line2mesh_quads.png)
2806        """
2807        if line_resolution is None:
2808            contour = vedo.shapes.Line(self.vertices)
2809        else:
2810            contour = vedo.shapes.Spline(self.vertices, smooth=smooth, res=line_resolution)
2811        contour.clean()
2812
2813        length = contour.length()
2814        density = length / contour.npoints
2815        # print(f"tomesh():\n\tline length = {length}")
2816        # print(f"\tdensity = {density} length/pt_separation")
2817
2818        x0, x1 = contour.xbounds()
2819        y0, y1 = contour.ybounds()
2820
2821        if grid is None:
2822            if mesh_resolution is None:
2823                resx = int((x1 - x0) / density + 0.5)
2824                resy = int((y1 - y0) / density + 0.5)
2825                # print(f"tmesh_resolution = {[resx, resy]}")
2826            else:
2827                if utils.is_sequence(mesh_resolution):
2828                    resx, resy = mesh_resolution
2829                else:
2830                    resx, resy = mesh_resolution, mesh_resolution
2831            grid = vedo.shapes.Grid(
2832                [(x0 + x1) / 2, (y0 + y1) / 2, 0],
2833                s=((x1 - x0) * 1.025, (y1 - y0) * 1.025),
2834                res=(resx, resy),
2835            )
2836        else:
2837            grid = grid.clone()
2838
2839        cpts = contour.vertices
2840
2841        # make sure it's closed
2842        p0, p1 = cpts[0], cpts[-1]
2843        nj = max(2, int(utils.mag(p1 - p0) / density + 0.5))
2844        joinline = vedo.shapes.Line(p1, p0, res=nj)
2845        contour = vedo.merge(contour, joinline).subsample(0.0001)
2846
2847        ####################################### quads
2848        if quads:
2849            cmesh = grid.clone().cut_with_point_loop(contour, on="cells", invert=invert)
2850            cmesh.wireframe(False).lw(0.5)
2851            cmesh.pipeline = utils.OperationNode(
2852                "generate_mesh",
2853                parents=[self, contour],
2854                comment=f"#quads {cmesh.dataset.GetNumberOfCells()}",
2855            )
2856            return cmesh
2857        #############################################
2858
2859        grid_tmp = grid.vertices.copy()
2860
2861        if jitter:
2862            np.random.seed(0)
2863            sigma = 1.0 / np.sqrt(grid.npoints) * grid.diagonal_size() * jitter
2864            # print(f"\tsigma jittering = {sigma}")
2865            grid_tmp += np.random.rand(grid.npoints, 3) * sigma
2866            grid_tmp[:, 2] = 0.0
2867
2868        todel = []
2869        density /= np.sqrt(3)
2870        vgrid_tmp = Points(grid_tmp)
2871
2872        for p in contour.vertices:
2873            out = vgrid_tmp.closest_point(p, radius=density, return_point_id=True)
2874            todel += out.tolist()
2875
2876        grid_tmp = grid_tmp.tolist()
2877        for index in sorted(list(set(todel)), reverse=True):
2878            del grid_tmp[index]
2879
2880        points = contour.vertices.tolist() + grid_tmp
2881        if invert:
2882            boundary = list(reversed(range(contour.npoints)))
2883        else:
2884            boundary = list(range(contour.npoints))
2885
2886        dln = Points(points).generate_delaunay2d(mode="xy", boundaries=[boundary])
2887        dln.compute_normals(points=False)  # fixes reversd faces
2888        dln.lw(1)
2889
2890        dln.pipeline = utils.OperationNode(
2891            "generate_mesh",
2892            parents=[self, contour],
2893            comment=f"#cells {dln.dataset.GetNumberOfCells()}",
2894        )
2895        return dln
2896
2897    def reconstruct_surface(
2898        self,
2899        dims=(100, 100, 100),
2900        radius=None,
2901        sample_size=None,
2902        hole_filling=True,
2903        bounds=(),
2904        padding=0.05,
2905    ) -> "vedo.Mesh":
2906        """
2907        Surface reconstruction from a scattered cloud of points.
2908
2909        Arguments:
2910            dims : (int)
2911                number of voxels in x, y and z to control precision.
2912            radius : (float)
2913                radius of influence of each point.
2914                Smaller values generally improve performance markedly.
2915                Note that after the signed distance function is computed,
2916                any voxel taking on the value >= radius
2917                is presumed to be "unseen" or uninitialized.
2918            sample_size : (int)
2919                if normals are not present
2920                they will be calculated using this sample size per point.
2921            hole_filling : (bool)
2922                enables hole filling, this generates
2923                separating surfaces between the empty and unseen portions of the volume.
2924            bounds : (list)
2925                region in space in which to perform the sampling
2926                in format (xmin,xmax, ymin,ymax, zim, zmax)
2927            padding : (float)
2928                increase by this fraction the bounding box
2929
2930        Examples:
2931            - [recosurface.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/recosurface.py)
2932
2933                ![](https://vedo.embl.es/images/advanced/recosurface.png)
2934        """
2935        if not utils.is_sequence(dims):
2936            dims = (dims, dims, dims)
2937
2938        sdf = vtki.new("SignedDistance")
2939
2940        if len(bounds) == 6:
2941            sdf.SetBounds(bounds)
2942        else:
2943            x0, x1, y0, y1, z0, z1 = self.bounds()
2944            sdf.SetBounds(
2945                x0 - (x1 - x0) * padding,
2946                x1 + (x1 - x0) * padding,
2947                y0 - (y1 - y0) * padding,
2948                y1 + (y1 - y0) * padding,
2949                z0 - (z1 - z0) * padding,
2950                z1 + (z1 - z0) * padding,
2951            )
2952        
2953        bb = sdf.GetBounds()
2954        if bb[0]==bb[1]:
2955            vedo.logger.warning("reconstruct_surface(): zero x-range")
2956        if bb[2]==bb[3]:
2957            vedo.logger.warning("reconstruct_surface(): zero y-range")
2958        if bb[4]==bb[5]:
2959            vedo.logger.warning("reconstruct_surface(): zero z-range")
2960
2961        pd = self.dataset
2962
2963        if pd.GetPointData().GetNormals():
2964            sdf.SetInputData(pd)
2965        else:
2966            normals = vtki.new("PCANormalEstimation")
2967            normals.SetInputData(pd)
2968            if not sample_size:
2969                sample_size = int(pd.GetNumberOfPoints() / 50)
2970            normals.SetSampleSize(sample_size)
2971            normals.SetNormalOrientationToGraphTraversal()
2972            sdf.SetInputConnection(normals.GetOutputPort())
2973            # print("Recalculating normals with sample size =", sample_size)
2974
2975        if radius is None:
2976            radius = self.diagonal_size() / (sum(dims) / 3) * 5
2977            # print("Calculating mesh from points with radius =", radius)
2978
2979        sdf.SetRadius(radius)
2980        sdf.SetDimensions(dims)
2981        sdf.Update()
2982
2983        surface = vtki.new("ExtractSurface")
2984        surface.SetRadius(radius * 0.99)
2985        surface.SetHoleFilling(hole_filling)
2986        surface.ComputeNormalsOff()
2987        surface.ComputeGradientsOff()
2988        surface.SetInputConnection(sdf.GetOutputPort())
2989        surface.Update()
2990        m = vedo.mesh.Mesh(surface.GetOutput(), c=self.color())
2991
2992        m.pipeline = utils.OperationNode(
2993            "reconstruct_surface",
2994            parents=[self],
2995            comment=f"#pts {m.dataset.GetNumberOfPoints()}",
2996        )
2997        return m
2998
2999    def compute_clustering(self, radius: float) -> Self:
3000        """
3001        Cluster points in space. The `radius` is the radius of local search.
3002        
3003        An array named "ClusterId" is added to `pointdata`.
3004
3005        Examples:
3006            - [clustering.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/clustering.py)
3007
3008                ![](https://vedo.embl.es/images/basic/clustering.png)
3009        """
3010        cluster = vtki.new("EuclideanClusterExtraction")
3011        cluster.SetInputData(self.dataset)
3012        cluster.SetExtractionModeToAllClusters()
3013        cluster.SetRadius(radius)
3014        cluster.ColorClustersOn()
3015        cluster.Update()
3016        idsarr = cluster.GetOutput().GetPointData().GetArray("ClusterId")
3017        self.dataset.GetPointData().AddArray(idsarr)
3018        self.pipeline = utils.OperationNode(
3019            "compute_clustering", parents=[self], comment=f"radius = {radius}"
3020        )
3021        return self
3022
3023    def compute_connections(self, radius, mode=0, regions=(), vrange=(0, 1), seeds=(), angle=0.0) -> Self:
3024        """
3025        Extracts and/or segments points from a point cloud based on geometric distance measures
3026        (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range.
3027        The default operation is to segment the points into "connected" regions where the connection
3028        is determined by an appropriate distance measure. Each region is given a region id.
3029
3030        Optionally, the filter can output the largest connected region of points; a particular region
3031        (via id specification); those regions that are seeded using a list of input point ids;
3032        or the region of points closest to a specified position.
3033
3034        The key parameter of this filter is the radius defining a sphere around each point which defines
3035        a local neighborhood: any other points in the local neighborhood are assumed connected to the point.
3036        Note that the radius is defined in absolute terms.
3037
3038        Other parameters are used to further qualify what it means to be a neighboring point.
3039        For example, scalar range and/or point normals can be used to further constrain the neighborhood.
3040        Also the extraction mode defines how the filter operates.
3041        By default, all regions are extracted but it is possible to extract particular regions;
3042        the region closest to a seed point; seeded regions; or the largest region found while processing.
3043        By default, all regions are extracted.
3044
3045        On output, all points are labeled with a region number.
3046        However note that the number of input and output points may not be the same:
3047        if not extracting all regions then the output size may be less than the input size.
3048
3049        Arguments:
3050            radius : (float)
3051                variable specifying a local sphere used to define local point neighborhood
3052            mode : (int)
3053                - 0,  Extract all regions
3054                - 1,  Extract point seeded regions
3055                - 2,  Extract largest region
3056                - 3,  Test specified regions
3057                - 4,  Extract all regions with scalar connectivity
3058                - 5,  Extract point seeded regions
3059            regions : (list)
3060                a list of non-negative regions id to extract
3061            vrange : (list)
3062                scalar range to use to extract points based on scalar connectivity
3063            seeds : (list)
3064                a list of non-negative point seed ids
3065            angle : (list)
3066                points are connected if the angle between their normals is
3067                within this angle threshold (expressed in degrees).
3068        """
3069        # https://vtk.org/doc/nightly/html/classvtkConnectedPointsFilter.html
3070        cpf = vtki.new("ConnectedPointsFilter")
3071        cpf.SetInputData(self.dataset)
3072        cpf.SetRadius(radius)
3073        if mode == 0:  # Extract all regions
3074            pass
3075
3076        elif mode == 1:  # Extract point seeded regions
3077            cpf.SetExtractionModeToPointSeededRegions()
3078            for s in seeds:
3079                cpf.AddSeed(s)
3080
3081        elif mode == 2:  # Test largest region
3082            cpf.SetExtractionModeToLargestRegion()
3083
3084        elif mode == 3:  # Test specified regions
3085            cpf.SetExtractionModeToSpecifiedRegions()
3086            for r in regions:
3087                cpf.AddSpecifiedRegion(r)
3088
3089        elif mode == 4:  # Extract all regions with scalar connectivity
3090            cpf.SetExtractionModeToLargestRegion()
3091            cpf.ScalarConnectivityOn()
3092            cpf.SetScalarRange(vrange[0], vrange[1])
3093
3094        elif mode == 5:  # Extract point seeded regions
3095            cpf.SetExtractionModeToLargestRegion()
3096            cpf.ScalarConnectivityOn()
3097            cpf.SetScalarRange(vrange[0], vrange[1])
3098            cpf.AlignedNormalsOn()
3099            cpf.SetNormalAngle(angle)
3100
3101        cpf.Update()
3102        self._update(cpf.GetOutput(), reset_locators=False)
3103        return self
3104
3105    def compute_camera_distance(self) -> np.ndarray:
3106        """
3107        Calculate the distance from points to the camera.
3108        
3109        A pointdata array is created with name 'DistanceToCamera' and returned.
3110        """
3111        if vedo.plotter_instance and vedo.plotter_instance.renderer:
3112            poly = self.dataset
3113            dc = vtki.new("DistanceToCamera")
3114            dc.SetInputData(poly)
3115            dc.SetRenderer(vedo.plotter_instance.renderer)
3116            dc.Update()
3117            self._update(dc.GetOutput(), reset_locators=False)
3118            return self.pointdata["DistanceToCamera"]
3119        return np.array([])
3120
3121    def densify(self, target_distance=0.1, nclosest=6, radius=None, niter=1, nmax=None) -> Self:
3122        """
3123        Return a copy of the cloud with new added points.
3124        The new points are created in such a way that all points in any local neighborhood are
3125        within a target distance of one another.
3126
3127        For each input point, the distance to all points in its neighborhood is computed.
3128        If any of its neighbors is further than the target distance,
3129        the edge connecting the point and its neighbor is bisected and
3130        a new point is inserted at the bisection point.
3131        A single pass is completed once all the input points are visited.
3132        Then the process repeats to the number of iterations.
3133
3134        Examples:
3135            - [densifycloud.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/densifycloud.py)
3136
3137                ![](https://vedo.embl.es/images/volumetric/densifycloud.png)
3138
3139        .. note::
3140            Points will be created in an iterative fashion until all points in their
3141            local neighborhood are the target distance apart or less.
3142            Note that the process may terminate early due to the
3143            number of iterations. By default the target distance is set to 0.5.
3144            Note that the target_distance should be less than the radius
3145            or nothing will change on output.
3146
3147        .. warning::
3148            This class can generate a lot of points very quickly.
3149            The maximum number of iterations is by default set to =1.0 for this reason.
3150            Increase the number of iterations very carefully.
3151            Also, `nmax` can be set to limit the explosion of points.
3152            It is also recommended that a N closest neighborhood is used.
3153
3154        """
3155        src = vtki.new("ProgrammableSource")
3156        opts = self.vertices
3157        # zeros = np.zeros(3)
3158
3159        def _read_points():
3160            output = src.GetPolyDataOutput()
3161            points = vtki.vtkPoints()
3162            for p in opts:
3163                # print(p)
3164                # if not np.array_equal(p, zeros):
3165                points.InsertNextPoint(p)
3166            output.SetPoints(points)
3167
3168        src.SetExecuteMethod(_read_points)
3169
3170        dens = vtki.new("DensifyPointCloudFilter")
3171        dens.SetInputConnection(src.GetOutputPort())
3172        # dens.SetInputData(self.dataset) # this does not work
3173        dens.InterpolateAttributeDataOn()
3174        dens.SetTargetDistance(target_distance)
3175        dens.SetMaximumNumberOfIterations(niter)
3176        if nmax:
3177            dens.SetMaximumNumberOfPoints(nmax)
3178
3179        if radius:
3180            dens.SetNeighborhoodTypeToRadius()
3181            dens.SetRadius(radius)
3182        elif nclosest:
3183            dens.SetNeighborhoodTypeToNClosest()
3184            dens.SetNumberOfClosestPoints(nclosest)
3185        else:
3186            vedo.logger.error("set either radius or nclosest")
3187            raise RuntimeError()
3188        dens.Update()
3189
3190        cld = Points(dens.GetOutput())
3191        cld.copy_properties_from(self)
3192        cld.interpolate_data_from(self, n=nclosest, radius=radius)
3193        cld.name = "DensifiedCloud"
3194        cld.pipeline = utils.OperationNode(
3195            "densify",
3196            parents=[self],
3197            c="#e9c46a:",
3198            comment=f"#pts {cld.dataset.GetNumberOfPoints()}",
3199        )
3200        return cld
3201
3202    ###############################################################################
3203    ## stuff returning a Volume
3204    ###############################################################################
3205
3206    def density(
3207        self, dims=(40, 40, 40), bounds=None, radius=None, compute_gradient=False, locator=None
3208    ) -> "vedo.Volume":
3209        """
3210        Generate a density field from a point cloud. Input can also be a set of 3D coordinates.
3211        Output is a `Volume`.
3212
3213        The local neighborhood is specified as the `radius` around each sample position (each voxel).
3214        If left to None, the radius is automatically computed as the diagonal of the bounding box
3215        and can be accessed via `vol.metadata["radius"]`.
3216        The density is expressed as the number of counts in the radius search.
3217
3218        Arguments:
3219            dims : (int, list)
3220                number of voxels in x, y and z of the output Volume.
3221            compute_gradient : (bool)
3222                Turn on/off the generation of the gradient vector,
3223                gradient magnitude scalar, and function classification scalar.
3224                By default this is off. Note that this will increase execution time
3225                and the size of the output. (The names of these point data arrays are:
3226                "Gradient", "Gradient Magnitude", and "Classification")
3227            locator : (vtkPointLocator)
3228                can be assigned from a previous call for speed (access it via `object.point_locator`).
3229
3230        Examples:
3231            - [plot_density3d.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/plot_density3d.py)
3232
3233                ![](https://vedo.embl.es/images/pyplot/plot_density3d.png)
3234        """
3235        pdf = vtki.new("PointDensityFilter")
3236        pdf.SetInputData(self.dataset)
3237
3238        if not utils.is_sequence(dims):
3239            dims = [dims, dims, dims]
3240
3241        if bounds is None:
3242            bounds = list(self.bounds())
3243        elif len(bounds) == 4:
3244            bounds = [*bounds, 0, 0]
3245
3246        if bounds[5] - bounds[4] == 0 or len(dims) == 2:  # its 2D
3247            dims = list(dims)
3248            dims = [dims[0], dims[1], 2]
3249            diag = self.diagonal_size()
3250            bounds[5] = bounds[4] + diag / 1000
3251        pdf.SetModelBounds(bounds)
3252
3253        pdf.SetSampleDimensions(dims)
3254
3255        if locator:
3256            pdf.SetLocator(locator)
3257
3258        pdf.SetDensityEstimateToFixedRadius()
3259        if radius is None:
3260            radius = self.diagonal_size() / 20
3261        pdf.SetRadius(radius)
3262        pdf.SetComputeGradient(compute_gradient)
3263        pdf.Update()
3264
3265        vol = vedo.Volume(pdf.GetOutput()).mode(1)
3266        vol.name = "PointDensity"
3267        vol.metadata["radius"] = radius
3268        vol.locator = pdf.GetLocator()
3269        vol.pipeline = utils.OperationNode(
3270            "density", parents=[self], comment=f"dims={tuple(vol.dimensions())}"
3271        )
3272        return vol
3273
3274
3275    def tovolume(
3276        self,
3277        kernel="shepard",
3278        radius=None,
3279        n=None,
3280        bounds=None,
3281        null_value=None,
3282        dims=(25, 25, 25),
3283    ) -> "vedo.Volume":
3284        """
3285        Generate a `Volume` by interpolating a scalar
3286        or vector field which is only known on a scattered set of points or mesh.
3287        Available interpolation kernels are: shepard, gaussian, or linear.
3288
3289        Arguments:
3290            kernel : (str)
3291                interpolation kernel type [shepard]
3292            radius : (float)
3293                radius of the local search
3294            n : (int)
3295                number of point to use for interpolation
3296            bounds : (list)
3297                bounding box of the output Volume object
3298            dims : (list)
3299                dimensions of the output Volume object
3300            null_value : (float)
3301                value to be assigned to invalid points
3302
3303        Examples:
3304            - [interpolate_volume.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/interpolate_volume.py)
3305
3306                ![](https://vedo.embl.es/images/volumetric/59095175-1ec5a300-8918-11e9-8bc0-fd35c8981e2b.jpg)
3307        """
3308        if radius is None and not n:
3309            vedo.logger.error("please set either radius or n")
3310            raise RuntimeError
3311
3312        poly = self.dataset
3313
3314        # Create a probe volume
3315        probe = vtki.vtkImageData()
3316        probe.SetDimensions(dims)
3317        if bounds is None:
3318            bounds = self.bounds()
3319        probe.SetOrigin(bounds[0], bounds[2], bounds[4])
3320        probe.SetSpacing(
3321            (bounds[1] - bounds[0]) / dims[0],
3322            (bounds[3] - bounds[2]) / dims[1],
3323            (bounds[5] - bounds[4]) / dims[2],
3324        )
3325
3326        if not self.point_locator:
3327            self.point_locator = vtki.new("PointLocator")
3328            self.point_locator.SetDataSet(poly)
3329            self.point_locator.BuildLocator()
3330
3331        if kernel == "shepard":
3332            kern = vtki.new("ShepardKernel")
3333            kern.SetPowerParameter(2)
3334        elif kernel == "gaussian":
3335            kern = vtki.new("GaussianKernel")
3336        elif kernel == "linear":
3337            kern = vtki.new("LinearKernel")
3338        else:
3339            vedo.logger.error("Error in tovolume(), available kernels are:")
3340            vedo.logger.error(" [shepard, gaussian, linear]")
3341            raise RuntimeError()
3342
3343        if radius:
3344            kern.SetRadius(radius)
3345
3346        interpolator = vtki.new("PointInterpolator")
3347        interpolator.SetInputData(probe)
3348        interpolator.SetSourceData(poly)
3349        interpolator.SetKernel(kern)
3350        interpolator.SetLocator(self.point_locator)
3351
3352        if n:
3353            kern.SetNumberOfPoints(n)
3354            kern.SetKernelFootprintToNClosest()
3355        else:
3356            kern.SetRadius(radius)
3357
3358        if null_value is not None:
3359            interpolator.SetNullValue(null_value)
3360        else:
3361            interpolator.SetNullPointsStrategyToClosestPoint()
3362        interpolator.Update()
3363
3364        vol = vedo.Volume(interpolator.GetOutput())
3365
3366        vol.pipeline = utils.OperationNode(
3367            "signed_distance",
3368            parents=[self],
3369            comment=f"dims={tuple(vol.dimensions())}",
3370            c="#e9c46a:#0096c7",
3371        )
3372        return vol
3373
3374    #################################################################################    
3375    def generate_segments(self, istart=0, rmax=1e30, niter=3) -> "vedo.shapes.Lines":
3376        """
3377        Generate a line segments from a set of points.
3378        The algorithm is based on the closest point search.
3379
3380        Returns a `Line` object.
3381        This object contains the a metadata array of used vertex counts in "UsedVertexCount"
3382        and the sum of the length of the segments in "SegmentsLengthSum".
3383
3384        Arguments:
3385            istart : (int)
3386                index of the starting point
3387            rmax : (float)
3388                maximum length of a segment
3389            niter : (int)
3390                number of iterations or passes through the points
3391
3392        Examples:
3393            - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py)
3394        """
3395        points = self.vertices
3396        segments = []
3397        dists = []
3398        n = len(points)
3399        used = np.zeros(n, dtype=int)
3400        for _ in range(niter):
3401            i = istart
3402            for _ in range(n):
3403                p = points[i]
3404                ids = self.closest_point(p, n=4, return_point_id=True)
3405                j = ids[1]
3406                if used[j] > 1 or [j, i] in segments:
3407                    j = ids[2]
3408                if used[j] > 1:
3409                    j = ids[3]
3410                d = np.linalg.norm(p - points[j])
3411                if used[j] > 1 or used[i] > 1 or d > rmax:
3412                    i += 1
3413                    if i >= n:
3414                        i = 0
3415                    continue
3416                used[i] += 1
3417                used[j] += 1
3418                segments.append([i, j])
3419                dists.append(d)
3420                i = j
3421        segments = np.array(segments, dtype=int)
3422
3423        lines = vedo.shapes.Lines(points[segments], c="k", lw=3)
3424        lines.metadata["UsedVertexCount"] = used
3425        lines.metadata["SegmentsLengthSum"] = np.sum(dists)
3426        lines.pipeline = utils.OperationNode("generate_segments", parents=[self])
3427        lines.name = "Segments"
3428        return lines
3429
3430    def generate_delaunay2d(
3431        self,
3432        mode="scipy",
3433        boundaries=(),
3434        tol=None,
3435        alpha=0.0,
3436        offset=0.0,
3437        transform=None,
3438    ) -> "vedo.mesh.Mesh":
3439        """
3440        Create a mesh from points in the XY plane.
3441        If `mode='fit'` then the filter computes a best fitting
3442        plane and projects the points onto it.
3443
3444        Check also `generate_mesh()`.
3445
3446        Arguments:
3447            tol : (float)
3448                specify a tolerance to control discarding of closely spaced points.
3449                This tolerance is specified as a fraction of the diagonal length of the bounding box of the points.
3450            alpha : (float)
3451                for a non-zero alpha value, only edges or triangles contained
3452                within a sphere centered at mesh vertices will be output.
3453                Otherwise, only triangles will be output.
3454            offset : (float)
3455                multiplier to control the size of the initial, bounding Delaunay triangulation.
3456            transform: (LinearTransform, NonLinearTransform)
3457                a transformation which is applied to points to generate a 2D problem.
3458                This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane.
3459                The points are transformed and triangulated.
3460                The topology of triangulated points is used as the output topology.
3461
3462        Examples:
3463            - [delaunay2d.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delaunay2d.py)
3464
3465                ![](https://vedo.embl.es/images/basic/delaunay2d.png)
3466        """
3467        plist = self.vertices.copy()
3468
3469        #########################################################
3470        if mode == "scipy":
3471            from scipy.spatial import Delaunay as scipy_delaunay
3472
3473            tri = scipy_delaunay(plist[:, 0:2])
3474            return vedo.mesh.Mesh([plist, tri.simplices])
3475        ##########################################################
3476
3477        pd = vtki.vtkPolyData()
3478        vpts = vtki.vtkPoints()
3479        vpts.SetData(utils.numpy2vtk(plist, dtype=np.float32))
3480        pd.SetPoints(vpts)
3481
3482        delny = vtki.new("Delaunay2D")
3483        delny.SetInputData(pd)
3484        if tol:
3485            delny.SetTolerance(tol)
3486        delny.SetAlpha(alpha)
3487        delny.SetOffset(offset)
3488
3489        if transform:
3490            delny.SetTransform(transform.T)
3491        elif mode == "fit":
3492            delny.SetProjectionPlaneMode(vtki.get_class("VTK_BEST_FITTING_PLANE"))
3493        elif mode == "xy" and boundaries:
3494            boundary = vtki.vtkPolyData()
3495            boundary.SetPoints(vpts)
3496            cell_array = vtki.vtkCellArray()
3497            for b in boundaries:
3498                cpolygon = vtki.vtkPolygon()
3499                for idd in b:
3500                    cpolygon.GetPointIds().InsertNextId(idd)
3501                cell_array.InsertNextCell(cpolygon)
3502            boundary.SetPolys(cell_array)
3503            delny.SetSourceData(boundary)
3504
3505        delny.Update()
3506
3507        msh = vedo.mesh.Mesh(delny.GetOutput())
3508        msh.name = "Delaunay2D"
3509        msh.clean().lighting("off")
3510        msh.pipeline = utils.OperationNode(
3511            "delaunay2d",
3512            parents=[self],
3513            comment=f"#cells {msh.dataset.GetNumberOfCells()}",
3514        )
3515        return msh
3516
3517    def generate_voronoi(self, padding=0.0, fit=False, method="vtk") -> "vedo.Mesh":
3518        """
3519        Generate the 2D Voronoi convex tiling of the input points (z is ignored).
3520        The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon.
3521
3522        A cell array named "VoronoiID" is added to the output Mesh.
3523
3524        The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest
3525        to one of the input points. Voronoi tessellations are important in computational geometry
3526        (and many other fields), and are the dual of Delaunay triangulations.
3527
3528        Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored
3529        (although carried through to the output).
3530        If you desire to triangulate in a different plane, you can use fit=True.
3531
3532        A brief summary is as follows. Each (generating) input point is associated with
3533        an initial Voronoi tile, which is simply the bounding box of the point set.
3534        A locator is then used to identify nearby points: each neighbor in turn generates a
3535        clipping line positioned halfway between the generating point and the neighboring point,
3536        and orthogonal to the line connecting them. Clips are readily performed by evaluationg the
3537        vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line.
3538        If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip
3539        line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs,
3540        the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region
3541        containing the neighboring clip points. The clip region (along with the points contained in it) is grown
3542        by careful expansion (e.g., outward spiraling iterator over all candidate clip points).
3543        When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi
3544        tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi
3545        tessellation. Note that topological and geometric information is used to generate a valid triangulation
3546        (e.g., merging points and validating topology).
3547
3548        Arguments:
3549            pts : (list)
3550                list of input points.
3551            padding : (float)
3552                padding distance. The default is 0.
3553            fit : (bool)
3554                detect automatically the best fitting plane. The default is False.
3555
3556        Examples:
3557            - [voronoi1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi1.py)
3558
3559                ![](https://vedo.embl.es/images/basic/voronoi1.png)
3560
3561            - [voronoi2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi2.py)
3562
3563                ![](https://vedo.embl.es/images/advanced/voronoi2.png)
3564        """
3565        pts = self.vertices
3566
3567        if method == "scipy":
3568            from scipy.spatial import Voronoi as scipy_voronoi
3569
3570            pts = np.asarray(pts)[:, (0, 1)]
3571            vor = scipy_voronoi(pts)
3572            regs = []  # filter out invalid indices
3573            for r in vor.regions:
3574                flag = True
3575                for x in r:
3576                    if x < 0:
3577                        flag = False
3578                        break
3579                if flag and len(r) > 0:
3580                    regs.append(r)
3581
3582            m = vedo.Mesh([vor.vertices, regs])
3583            m.celldata["VoronoiID"] = np.array(list(range(len(regs)))).astype(int)
3584
3585        elif method == "vtk":
3586            vor = vtki.new("Voronoi2D")
3587            if isinstance(pts, Points):
3588                vor.SetInputData(pts)
3589            else:
3590                pts = np.asarray(pts)
3591                if pts.shape[1] == 2:
3592                    pts = np.c_[pts, np.zeros(len(pts))]
3593                pd = vtki.vtkPolyData()
3594                vpts = vtki.vtkPoints()
3595                vpts.SetData(utils.numpy2vtk(pts, dtype=np.float32))
3596                pd.SetPoints(vpts)
3597                vor.SetInputData(pd)
3598            vor.SetPadding(padding)
3599            vor.SetGenerateScalarsToPointIds()
3600            if fit:
3601                vor.SetProjectionPlaneModeToBestFittingPlane()
3602            else:
3603                vor.SetProjectionPlaneModeToXYPlane()
3604            vor.Update()
3605            poly = vor.GetOutput()
3606            arr = poly.GetCellData().GetArray(0)
3607            if arr:
3608                arr.SetName("VoronoiID")
3609            m = vedo.Mesh(poly, c="orange5")
3610
3611        else:
3612            vedo.logger.error(f"Unknown method {method} in voronoi()")
3613            raise RuntimeError
3614
3615        m.lw(2).lighting("off").wireframe()
3616        m.name = "Voronoi"
3617        return m
3618
3619    ##########################################################################
3620    def generate_delaunay3d(self, radius=0, tol=None) -> "vedo.TetMesh":
3621        """
3622        Create 3D Delaunay triangulation of input points.
3623
3624        Arguments:
3625            radius : (float)
3626                specify distance (or "alpha") value to control output.
3627                For a non-zero values, only tetra contained within the circumsphere
3628                will be output.
3629            tol : (float)
3630                Specify a tolerance to control discarding of closely spaced points.
3631                This tolerance is specified as a fraction of the diagonal length of
3632                the bounding box of the points.
3633        """
3634        deln = vtki.new("Delaunay3D")
3635        deln.SetInputData(self.dataset)
3636        deln.SetAlpha(radius)
3637        deln.AlphaTetsOn()
3638        deln.AlphaTrisOff()
3639        deln.AlphaLinesOff()
3640        deln.AlphaVertsOff()
3641        deln.BoundingTriangulationOff()
3642        if tol:
3643            deln.SetTolerance(tol)
3644        deln.Update()
3645        m = vedo.TetMesh(deln.GetOutput())
3646        m.pipeline = utils.OperationNode(
3647            "generate_delaunay3d", c="#e9c46a:#edabab", parents=[self],
3648        )
3649        m.name = "Delaunay3D"
3650        return m
3651
3652    ####################################################
3653    def visible_points(self, area=(), tol=None, invert=False) -> Union[Self, None]:
3654        """
3655        Extract points based on whether they are visible or not.
3656        Visibility is determined by accessing the z-buffer of a rendering window.
3657        The position of each input point is converted into display coordinates,
3658        and then the z-value at that point is obtained.
3659        If within the user-specified tolerance, the point is considered visible.
3660        Associated data attributes are passed to the output as well.
3661
3662        This filter also allows you to specify a rectangular window in display (pixel)
3663        coordinates in which the visible points must lie.
3664
3665        Arguments:
3666            area : (list)
3667                specify a rectangular region as (xmin,xmax,ymin,ymax)
3668            tol : (float)
3669                a tolerance in normalized display coordinate system
3670            invert : (bool)
3671                select invisible points instead.
3672
3673        Example:
3674            ```python
3675            from vedo import Ellipsoid, show
3676            s = Ellipsoid().rotate_y(30)
3677
3678            # Camera options: pos, focal_point, viewup, distance
3679            camopts = dict(pos=(0,0,25), focal_point=(0,0,0))
3680            show(s, camera=camopts, offscreen=True)
3681
3682            m = s.visible_points()
3683            # print('visible pts:', m.vertices)  # numpy array
3684            show(m, new=True, axes=1).close() # optionally draw result in a new window
3685            ```
3686            ![](https://vedo.embl.es/images/feats/visible_points.png)
3687        """
3688        svp = vtki.new("SelectVisiblePoints")
3689        svp.SetInputData(self.dataset)
3690
3691        ren = None
3692        if vedo.plotter_instance:
3693            if vedo.plotter_instance.renderer:
3694                ren = vedo.plotter_instance.renderer
3695                svp.SetRenderer(ren)
3696        if not ren:
3697            vedo.logger.warning(
3698                "visible_points() can only be used after a rendering step"
3699            )
3700            return None
3701
3702        if len(area) == 2:
3703            area = utils.flatten(area)
3704        if len(area) == 4:
3705            # specify a rectangular region
3706            svp.SetSelection(area[0], area[1], area[2], area[3])
3707        if tol is not None:
3708            svp.SetTolerance(tol)
3709        if invert:
3710            svp.SelectInvisibleOn()
3711        svp.Update()
3712
3713        m = Points(svp.GetOutput())
3714        m.name = "VisiblePoints"
3715        return m
3716
3717####################################################
3718class CellCenters(Points):
3719    def __init__(self, pcloud):
3720        """
3721        Generate `Points` at the center of the cells of any type of object.
3722
3723        Check out also `cell_centers()`.
3724        """
3725        vcen = vtki.new("CellCenters")
3726        vcen.CopyArraysOn()
3727        vcen.VertexCellsOn()
3728        # vcen.ConvertGhostCellsToGhostPointsOn()
3729        try:
3730            vcen.SetInputData(pcloud.dataset)
3731        except AttributeError:
3732            vcen.SetInputData(pcloud)
3733        vcen.Update()
3734        super().__init__(vcen.GetOutput())
3735        self.name = "CellCenters"
 467class Points(PointsVisual, PointAlgorithms):
 468    """Work with point clouds."""
 469
 470    def __init__(self, inputobj=None, r=4, c=(0.2, 0.2, 0.2), alpha=1):
 471        """
 472        Build an object made of only vertex points for a list of 2D/3D points.
 473        Both shapes (N, 3) or (3, N) are accepted as input, if N>3.
 474
 475        Arguments:
 476            inputobj : (list, tuple)
 477            r : (int)
 478                Point radius in units of pixels.
 479            c : (str, list)
 480                Color name or rgb tuple.
 481            alpha : (float)
 482                Transparency in range [0,1].
 483
 484        Example:
 485            ```python
 486            from vedo import *
 487
 488            def fibonacci_sphere(n):
 489                s = np.linspace(0, n, num=n, endpoint=False)
 490                theta = s * 2.399963229728653
 491                y = 1 - s * (2/(n-1))
 492                r = np.sqrt(1 - y * y)
 493                x = np.cos(theta) * r
 494                z = np.sin(theta) * r
 495                return np._c[x,y,z]
 496
 497            Points(fibonacci_sphere(1000)).show(axes=1).close()
 498            ```
 499            ![](https://vedo.embl.es/images/feats/fibonacci.png)
 500        """
 501        # print("INIT POINTS")
 502        super().__init__()
 503
 504        self.name = ""
 505        self.filename = ""
 506        self.file_size = ""
 507
 508        self.info = {}
 509        self.time = time.time()
 510        
 511        self.transform = LinearTransform()
 512        self.point_locator = None
 513        self.cell_locator = None
 514        self.line_locator = None
 515
 516        self.actor = vtki.vtkActor()
 517        self.properties = self.actor.GetProperty()
 518        self.properties_backface = self.actor.GetBackfaceProperty()
 519        self.mapper = vtki.new("PolyDataMapper")
 520        self.dataset = vtki.vtkPolyData()
 521        
 522        # Create weakref so actor can access this object (eg to pick/remove):
 523        self.actor.retrieve_object = weak_ref_to(self)
 524
 525        try:
 526            self.properties.RenderPointsAsSpheresOn()
 527        except AttributeError:
 528            pass
 529
 530        if inputobj is None:  ####################
 531            return
 532        ##########################################
 533
 534        self.name = "Points"
 535
 536        ######
 537        if isinstance(inputobj, vtki.vtkActor):
 538            self.dataset.DeepCopy(inputobj.GetMapper().GetInput())
 539            pr = vtki.vtkProperty()
 540            pr.DeepCopy(inputobj.GetProperty())
 541            self.actor.SetProperty(pr)
 542            self.properties = pr
 543            self.mapper.SetScalarVisibility(inputobj.GetMapper().GetScalarVisibility())
 544
 545        elif isinstance(inputobj, vtki.vtkPolyData):
 546            self.dataset = inputobj
 547            if self.dataset.GetNumberOfCells() == 0:
 548                carr = vtki.vtkCellArray()
 549                for i in range(self.dataset.GetNumberOfPoints()):
 550                    carr.InsertNextCell(1)
 551                    carr.InsertCellPoint(i)
 552                self.dataset.SetVerts(carr)
 553
 554        elif isinstance(inputobj, Points):
 555            self.dataset = inputobj.dataset
 556            self.copy_properties_from(inputobj)
 557
 558        elif utils.is_sequence(inputobj):  # passing point coords
 559            self.dataset = utils.buildPolyData(utils.make3d(inputobj))
 560
 561        elif isinstance(inputobj, str):
 562            verts = vedo.file_io.load(inputobj)
 563            self.filename = inputobj
 564            self.dataset = verts.dataset
 565
 566        elif "meshlib" in str(type(inputobj)):
 567            from meshlib import mrmeshnumpy as mn
 568            self.dataset = utils.buildPolyData(mn.toNumpyArray(inputobj.points))
 569
 570        else:
 571            # try to extract the points from a generic VTK input data object
 572            if hasattr(inputobj, "dataset"):
 573                inputobj = inputobj.dataset
 574            try:
 575                vvpts = inputobj.GetPoints()
 576                self.dataset = vtki.vtkPolyData()
 577                self.dataset.SetPoints(vvpts)
 578                for i in range(inputobj.GetPointData().GetNumberOfArrays()):
 579                    arr = inputobj.GetPointData().GetArray(i)
 580                    self.dataset.GetPointData().AddArray(arr)
 581            except:
 582                vedo.logger.error(f"cannot build Points from type {type(inputobj)}")
 583                raise RuntimeError()
 584
 585        self.actor.SetMapper(self.mapper)
 586        self.mapper.SetInputData(self.dataset)
 587
 588        self.properties.SetColor(colors.get_color(c))
 589        self.properties.SetOpacity(alpha)
 590        self.properties.SetRepresentationToPoints()
 591        self.properties.SetPointSize(r)
 592        self.properties.LightingOff()
 593
 594        self.pipeline = utils.OperationNode(
 595            self, parents=[], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
 596        )
 597
 598    def _update(self, polydata, reset_locators=True) -> Self:
 599        """Overwrite the polygonal dataset with a new vtkPolyData."""
 600        self.dataset = polydata
 601        self.mapper.SetInputData(self.dataset)
 602        self.mapper.Modified()
 603        if reset_locators:
 604            self.point_locator = None
 605            self.line_locator = None
 606            self.cell_locator = None
 607        return self
 608
 609    def __str__(self):
 610        """Print a description of the Points/Mesh."""
 611        module = self.__class__.__module__
 612        name = self.__class__.__name__
 613        out = vedo.printc(
 614            f"{module}.{name} at ({hex(self.memory_address())})".ljust(75),
 615            c="g", bold=True, invert=True, return_string=True,
 616        )
 617        out += "\x1b[0m\x1b[32;1m"
 618
 619        if self.name:
 620            out += "name".ljust(14) + ": " + self.name
 621            if "legend" in self.info.keys() and self.info["legend"]:
 622                out+= f", legend='{self.info['legend']}'"
 623            out += "\n"
 624 
 625        if self.filename:
 626            out+= "file name".ljust(14) + ": " + self.filename + "\n"
 627
 628        if not self.mapper.GetScalarVisibility():
 629            col = utils.precision(self.properties.GetColor(), 3)
 630            cname = vedo.colors.get_color_name(self.properties.GetColor())
 631            out+= "color".ljust(14) + ": " + cname 
 632            out+= f", rgb={col}, alpha={self.properties.GetOpacity()}\n"
 633            if self.actor.GetBackfaceProperty():
 634                bcol = self.actor.GetBackfaceProperty().GetDiffuseColor()
 635                cname = vedo.colors.get_color_name(bcol)
 636                out+= "backface color".ljust(14) + ": " 
 637                out+= f"{cname}, rgb={utils.precision(bcol,3)}\n"
 638
 639        npt = self.dataset.GetNumberOfPoints()
 640        npo, nln = self.dataset.GetNumberOfPolys(), self.dataset.GetNumberOfLines()
 641        out+= "elements".ljust(14) + f": vertices={npt:,} polygons={npo:,} lines={nln:,}"
 642        if self.dataset.GetNumberOfStrips():
 643            out+= f", strips={self.dataset.GetNumberOfStrips():,}"
 644        out+= "\n"
 645        if self.dataset.GetNumberOfPieces() > 1:
 646            out+= "pieces".ljust(14) + ": " + str(self.dataset.GetNumberOfPieces()) + "\n"
 647
 648        out+= "position".ljust(14) + ": " + f"{utils.precision(self.pos(), 6)}\n"
 649        try:
 650            sc = self.transform.get_scale()
 651            out+= "scaling".ljust(14)  + ": "
 652            out+= utils.precision(sc, 6) + "\n"
 653        except AttributeError:
 654            pass
 655
 656        if self.npoints:
 657            out+="size".ljust(14)+ ": average=" + utils.precision(self.average_size(),6)
 658            out+=", diagonal="+ utils.precision(self.diagonal_size(), 6)+ "\n"
 659            out+="center of mass".ljust(14) + ": " + utils.precision(self.center_of_mass(),6)+"\n"
 660
 661        bnds = self.bounds()
 662        bx1, bx2 = utils.precision(bnds[0], 3), utils.precision(bnds[1], 3)
 663        by1, by2 = utils.precision(bnds[2], 3), utils.precision(bnds[3], 3)
 664        bz1, bz2 = utils.precision(bnds[4], 3), utils.precision(bnds[5], 3)
 665        out+= "bounds".ljust(14) + ":"
 666        out+= " x=(" + bx1 + ", " + bx2 + "),"
 667        out+= " y=(" + by1 + ", " + by2 + "),"
 668        out+= " z=(" + bz1 + ", " + bz2 + ")\n"
 669
 670        for key in self.pointdata.keys():
 671            arr = self.pointdata[key]
 672            dim = arr.shape[1] if arr.ndim > 1 else 1
 673            mark_active = "pointdata"
 674            a_scalars = self.dataset.GetPointData().GetScalars()
 675            a_vectors = self.dataset.GetPointData().GetVectors()
 676            a_tensors = self.dataset.GetPointData().GetTensors()
 677            if   a_scalars and a_scalars.GetName() == key:
 678                mark_active += " *"
 679            elif a_vectors and a_vectors.GetName() == key:
 680                mark_active += " **"
 681            elif a_tensors and a_tensors.GetName() == key:
 682                mark_active += " ***"
 683            out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), dim={dim}'
 684            if dim == 1 and len(arr):
 685                rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3)
 686                out += f", range=({rng})\n"
 687            else:
 688                out += "\n"
 689
 690        for key in self.celldata.keys():
 691            arr = self.celldata[key]
 692            dim = arr.shape[1] if arr.ndim > 1 else 1
 693            mark_active = "celldata"
 694            a_scalars = self.dataset.GetCellData().GetScalars()
 695            a_vectors = self.dataset.GetCellData().GetVectors()
 696            a_tensors = self.dataset.GetCellData().GetTensors()
 697            if   a_scalars and a_scalars.GetName() == key:
 698                mark_active += " *"
 699            elif a_vectors and a_vectors.GetName() == key:
 700                mark_active += " **"
 701            elif a_tensors and a_tensors.GetName() == key:
 702                mark_active += " ***"
 703            out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), dim={dim}'
 704            if dim == 1 and len(arr):
 705                rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3)
 706                out += f", range=({rng})\n"
 707            else:
 708                out += "\n"
 709
 710        for key in self.metadata.keys():
 711            arr = self.metadata[key]
 712            if len(arr) > 3:
 713                out+= "metadata".ljust(14) + ": " + f'"{key}" ({len(arr)} values)\n'
 714            else:
 715                out+= "metadata".ljust(14) + ": " + f'"{key}" = {arr}\n'
 716
 717        if self.picked3d is not None:
 718            idp = self.closest_point(self.picked3d, return_point_id=True)
 719            idc = self.closest_point(self.picked3d, return_cell_id=True)
 720            out+= "clicked point".ljust(14) + ": " + utils.precision(self.picked3d, 6)
 721            out+= f", pointID={idp}, cellID={idc}\n"
 722
 723        return out.rstrip() + "\x1b[0m"
 724
 725    def _repr_html_(self):
 726        """
 727        HTML representation of the Point cloud object for Jupyter Notebooks.
 728
 729        Returns:
 730            HTML text with the image and some properties.
 731        """
 732        import io
 733        import base64
 734        from PIL import Image
 735
 736        library_name = "vedo.pointcloud.Points"
 737        help_url = "https://vedo.embl.es/docs/vedo/pointcloud.html#Points"
 738
 739        arr = self.thumbnail()
 740        im = Image.fromarray(arr)
 741        buffered = io.BytesIO()
 742        im.save(buffered, format="PNG", quality=100)
 743        encoded = base64.b64encode(buffered.getvalue()).decode("utf-8")
 744        url = "data:image/png;base64," + encoded
 745        image = f"<img src='{url}'></img>"
 746
 747        bounds = "<br/>".join(
 748            [
 749                utils.precision(min_x, 4) + " ... " + utils.precision(max_x, 4)
 750                for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2])
 751            ]
 752        )
 753        average_size = "{size:.3f}".format(size=self.average_size())
 754
 755        help_text = ""
 756        if self.name:
 757            help_text += f"<b> {self.name}: &nbsp&nbsp</b>"
 758        help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>"
 759        if self.filename:
 760            dots = ""
 761            if len(self.filename) > 30:
 762                dots = "..."
 763            help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>"
 764
 765        pdata = ""
 766        if self.dataset.GetPointData().GetScalars():
 767            if self.dataset.GetPointData().GetScalars().GetName():
 768                name = self.dataset.GetPointData().GetScalars().GetName()
 769                pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>"
 770
 771        cdata = ""
 772        if self.dataset.GetCellData().GetScalars():
 773            if self.dataset.GetCellData().GetScalars().GetName():
 774                name = self.dataset.GetCellData().GetScalars().GetName()
 775                cdata = "<tr><td><b> cell data array </b></td><td>" + name + "</td></tr>"
 776
 777        allt = [
 778            "<table>",
 779            "<tr>",
 780            "<td>",
 781            image,
 782            "</td>",
 783            "<td style='text-align: center; vertical-align: center;'><br/>",
 784            help_text,
 785            "<table>",
 786            "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>",
 787            "<tr><td><b> center of mass </b></td><td>"
 788            + utils.precision(self.center_of_mass(), 3)
 789            + "</td></tr>",
 790            "<tr><td><b> average size </b></td><td>" + str(average_size) + "</td></tr>",
 791            "<tr><td><b> nr. points </b></td><td>" + str(self.npoints) + "</td></tr>",
 792            pdata,
 793            cdata,
 794            "</table>",
 795            "</table>",
 796        ]
 797        return "\n".join(allt)
 798
 799    ##################################################################################
 800    def __add__(self, meshs):
 801        """
 802        Add two meshes or a list of meshes together to form an `Assembly` object.
 803        """
 804        if isinstance(meshs, list):
 805            alist = [self]
 806            for l in meshs:
 807                if isinstance(l, vedo.Assembly):
 808                    alist += l.unpack()
 809                else:
 810                    alist += l
 811            return vedo.assembly.Assembly(alist)
 812
 813        if isinstance(meshs, vedo.Assembly):
 814            return meshs + self  # use Assembly.__add__
 815
 816        return vedo.assembly.Assembly([self, meshs])
 817
 818    def polydata(self, **kwargs):
 819        """
 820        Obsolete. Use property `.dataset` instead.
 821        Returns the underlying `vtkPolyData` object.
 822        """
 823        colors.printc(
 824            "WARNING: call to .polydata() is obsolete, use property .dataset instead.",
 825            c="y")
 826        return self.dataset
 827
 828    def __copy__(self):
 829        return self.clone(deep=False)
 830
 831    def __deepcopy__(self, memo):
 832        return self.clone(deep=memo)
 833    
 834    def copy(self, deep=True) -> Self:
 835        """Return a copy of the object. Alias of `clone()`."""
 836        return self.clone(deep=deep)
 837
 838    def clone(self, deep=True) -> Self:
 839        """
 840        Clone a `PointCloud` or `Mesh` object to make an exact copy of it.
 841        Alias of `copy()`.
 842
 843        Arguments:
 844            deep : (bool)
 845                if False return a shallow copy of the mesh without copying the points array.
 846
 847        Examples:
 848            - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py)
 849
 850               ![](https://vedo.embl.es/images/basic/mirror.png)
 851        """
 852        poly = vtki.vtkPolyData()
 853        if deep or isinstance(deep, dict): # if a memo object is passed this checks as True
 854            poly.DeepCopy(self.dataset)
 855        else:
 856            poly.ShallowCopy(self.dataset)
 857
 858        if isinstance(self, vedo.Mesh):
 859            cloned = vedo.Mesh(poly)
 860        else:
 861            cloned = Points(poly)
 862        # print([self], self.__class__)
 863        # cloned = self.__class__(poly)
 864
 865        cloned.transform = self.transform.clone()
 866
 867        cloned.copy_properties_from(self)
 868
 869        cloned.name = str(self.name)
 870        cloned.filename = str(self.filename)
 871        cloned.info = dict(self.info)
 872        cloned.pipeline = utils.OperationNode("clone", parents=[self], shape="diamond", c="#edede9")
 873
 874        if isinstance(deep, dict):
 875            deep[id(self)] = cloned
 876
 877        return cloned
 878
 879    def compute_normals_with_pca(self, n=20, orientation_point=None, invert=False) -> Self:
 880        """
 881        Generate point normals using PCA (principal component analysis).
 882        This algorithm estimates a local tangent plane around each sample point p
 883        by considering a small neighborhood of points around p, and fitting a plane
 884        to the neighborhood (via PCA).
 885
 886        Arguments:
 887            n : (int)
 888                neighborhood size to calculate the normal
 889            orientation_point : (list)
 890                adjust the +/- sign of the normals so that
 891                the normals all point towards a specified point. If None, perform a traversal
 892                of the point cloud and flip neighboring normals so that they are mutually consistent.
 893            invert : (bool)
 894                flip all normals
 895        """
 896        poly = self.dataset
 897        pcan = vtki.new("PCANormalEstimation")
 898        pcan.SetInputData(poly)
 899        pcan.SetSampleSize(n)
 900
 901        if orientation_point is not None:
 902            pcan.SetNormalOrientationToPoint()
 903            pcan.SetOrientationPoint(orientation_point)
 904        else:
 905            pcan.SetNormalOrientationToGraphTraversal()
 906
 907        if invert:
 908            pcan.FlipNormalsOn()
 909        pcan.Update()
 910
 911        varr = pcan.GetOutput().GetPointData().GetNormals()
 912        varr.SetName("Normals")
 913        self.dataset.GetPointData().SetNormals(varr)
 914        self.dataset.GetPointData().Modified()
 915        return self
 916
 917    def compute_acoplanarity(self, n=25, radius=None, on="points") -> Self:
 918        """
 919        Compute acoplanarity which is a measure of how much a local region of the mesh
 920        differs from a plane.
 921        
 922        The information is stored in a `pointdata` or `celldata` array with name 'Acoplanarity'.
 923        
 924        Either `n` (number of neighbour points) or `radius` (radius of local search) can be specified.
 925        If a radius value is given and not enough points fall inside it, then a -1 is stored.
 926
 927        Example:
 928            ```python
 929            from vedo import *
 930            msh = ParametricShape('RandomHills')
 931            msh.compute_acoplanarity(radius=0.1, on='cells')
 932            msh.cmap("coolwarm", on='cells').add_scalarbar()
 933            msh.show(axes=1).close()
 934            ```
 935            ![](https://vedo.embl.es/images/feats/acoplanarity.jpg)
 936        """
 937        acoplanarities = []
 938        if "point" in on:
 939            pts = self.vertices
 940        elif "cell" in on:
 941            pts = self.cell_centers
 942        else:
 943            raise ValueError(f"In compute_acoplanarity() set on to either 'cells' or 'points', not {on}")
 944
 945        for p in utils.progressbar(pts, delay=5, width=15, title=f"{on} acoplanarity"):
 946            if n:
 947                data = self.closest_point(p, n=n)
 948                npts = n
 949            elif radius:
 950                data = self.closest_point(p, radius=radius)
 951                npts = len(data)
 952
 953            try:
 954                center = data.mean(axis=0)
 955                res = np.linalg.svd(data - center)
 956                acoplanarities.append(res[1][2] / npts)
 957            except:
 958                acoplanarities.append(-1.0)
 959
 960        if "point" in on:
 961            self.pointdata["Acoplanarity"] = np.array(acoplanarities, dtype=float)
 962        else:
 963            self.celldata["Acoplanarity"] = np.array(acoplanarities, dtype=float)
 964        return self
 965
 966    def distance_to(self, pcloud, signed=False, invert=False, name="Distance") -> np.ndarray:
 967        """
 968        Computes the distance from one point cloud or mesh to another point cloud or mesh.
 969        This new `pointdata` array is saved with default name "Distance".
 970
 971        Keywords `signed` and `invert` are used to compute signed distance,
 972        but the mesh in that case must have polygonal faces (not a simple point cloud),
 973        and normals must also be computed.
 974
 975        Examples:
 976            - [distance2mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/distance2mesh.py)
 977
 978                ![](https://vedo.embl.es/images/basic/distance2mesh.png)
 979        """
 980        if pcloud.dataset.GetNumberOfPolys():
 981
 982            poly1 = self.dataset
 983            poly2 = pcloud.dataset
 984            df = vtki.new("DistancePolyDataFilter")
 985            df.ComputeSecondDistanceOff()
 986            df.SetInputData(0, poly1)
 987            df.SetInputData(1, poly2)
 988            df.SetSignedDistance(signed)
 989            df.SetNegateDistance(invert)
 990            df.Update()
 991            scals = df.GetOutput().GetPointData().GetScalars()
 992            dists = utils.vtk2numpy(scals)
 993
 994        else:  # has no polygons
 995
 996            if signed:
 997                vedo.logger.warning("distance_to() called with signed=True but input object has no polygons")
 998
 999            if not pcloud.point_locator:
1000                pcloud.point_locator = vtki.new("PointLocator")
1001                pcloud.point_locator.SetDataSet(pcloud.dataset)
1002                pcloud.point_locator.BuildLocator()
1003
1004            ids = []
1005            ps1 = self.vertices
1006            ps2 = pcloud.vertices
1007            for p in ps1:
1008                pid = pcloud.point_locator.FindClosestPoint(p)
1009                ids.append(pid)
1010
1011            deltas = ps2[ids] - ps1
1012            dists = np.linalg.norm(deltas, axis=1).astype(np.float32)
1013            scals = utils.numpy2vtk(dists)
1014
1015        scals.SetName(name)
1016        self.dataset.GetPointData().AddArray(scals)
1017        self.dataset.GetPointData().SetActiveScalars(scals.GetName())
1018        rng = scals.GetRange()
1019        self.mapper.SetScalarRange(rng[0], rng[1])
1020        self.mapper.ScalarVisibilityOn()
1021
1022        self.pipeline = utils.OperationNode(
1023            "distance_to",
1024            parents=[self, pcloud],
1025            shape="cylinder",
1026            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1027        )
1028        return dists
1029
1030    def clean(self) -> Self:
1031        """Clean pointcloud or mesh by removing coincident points."""
1032        cpd = vtki.new("CleanPolyData")
1033        cpd.PointMergingOn()
1034        cpd.ConvertLinesToPointsOff()
1035        cpd.ConvertPolysToLinesOff()
1036        cpd.ConvertStripsToPolysOff()
1037        cpd.SetInputData(self.dataset)
1038        cpd.Update()
1039        self._update(cpd.GetOutput())
1040        self.pipeline = utils.OperationNode(
1041            "clean", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
1042        )
1043        return self
1044
1045    def subsample(self, fraction: float, absolute=False) -> Self:
1046        """
1047        Subsample a point cloud by requiring that the points
1048        or vertices are far apart at least by the specified fraction of the object size.
1049        If a Mesh is passed the polygonal faces are not removed
1050        but holes can appear as their vertices are removed.
1051
1052        Examples:
1053            - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py)
1054
1055                ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png)
1056
1057            - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py)
1058
1059                ![](https://vedo.embl.es/images/advanced/recosurface.png)
1060        """
1061        if not absolute:
1062            if fraction > 1:
1063                vedo.logger.warning(
1064                    f"subsample(fraction=...), fraction must be < 1, but is {fraction}"
1065                )
1066            if fraction <= 0:
1067                return self
1068
1069        cpd = vtki.new("CleanPolyData")
1070        cpd.PointMergingOn()
1071        cpd.ConvertLinesToPointsOn()
1072        cpd.ConvertPolysToLinesOn()
1073        cpd.ConvertStripsToPolysOn()
1074        cpd.SetInputData(self.dataset)
1075        if absolute:
1076            cpd.SetTolerance(fraction / self.diagonal_size())
1077            # cpd.SetToleranceIsAbsolute(absolute)
1078        else:
1079            cpd.SetTolerance(fraction)
1080        cpd.Update()
1081
1082        ps = 2
1083        if self.properties.GetRepresentation() == 0:
1084            ps = self.properties.GetPointSize()
1085
1086        self._update(cpd.GetOutput())
1087        self.ps(ps)
1088
1089        self.pipeline = utils.OperationNode(
1090            "subsample", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
1091        )
1092        return self
1093
1094    def threshold(self, scalars: str, above=None, below=None, on="points") -> Self:
1095        """
1096        Extracts cells where scalar value satisfies threshold criterion.
1097
1098        Arguments:
1099            scalars : (str)
1100                name of the scalars array.
1101            above : (float)
1102                minimum value of the scalar
1103            below : (float)
1104                maximum value of the scalar
1105            on : (str)
1106                if 'cells' assume array of scalars refers to cell data.
1107
1108        Examples:
1109            - [mesh_threshold.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_threshold.py)
1110        """
1111        thres = vtki.new("Threshold")
1112        thres.SetInputData(self.dataset)
1113
1114        if on.startswith("c"):
1115            asso = vtki.vtkDataObject.FIELD_ASSOCIATION_CELLS
1116        else:
1117            asso = vtki.vtkDataObject.FIELD_ASSOCIATION_POINTS
1118
1119        thres.SetInputArrayToProcess(0, 0, 0, asso, scalars)
1120
1121        if above is None and below is not None:
1122            try:  # vtk 9.2
1123                thres.ThresholdByLower(below)
1124            except AttributeError:  # vtk 9.3
1125                thres.SetUpperThreshold(below)
1126
1127        elif below is None and above is not None:
1128            try:
1129                thres.ThresholdByUpper(above)
1130            except AttributeError:
1131                thres.SetLowerThreshold(above)
1132        else:
1133            try:
1134                thres.ThresholdBetween(above, below)
1135            except AttributeError:
1136                thres.SetUpperThreshold(below)
1137                thres.SetLowerThreshold(above)
1138
1139        thres.Update()
1140
1141        gf = vtki.new("GeometryFilter")
1142        gf.SetInputData(thres.GetOutput())
1143        gf.Update()
1144        self._update(gf.GetOutput())
1145        self.pipeline = utils.OperationNode("threshold", parents=[self])
1146        return self
1147
1148    def quantize(self, value: float) -> Self:
1149        """
1150        The user should input a value and all {x,y,z} coordinates
1151        will be quantized to that absolute grain size.
1152        """
1153        qp = vtki.new("QuantizePolyDataPoints")
1154        qp.SetInputData(self.dataset)
1155        qp.SetQFactor(value)
1156        qp.Update()
1157        self._update(qp.GetOutput())
1158        self.pipeline = utils.OperationNode("quantize", parents=[self])
1159        return self
1160
1161    @property
1162    def vertex_normals(self) -> np.ndarray:
1163        """
1164        Retrieve vertex normals as a numpy array. Same as `point_normals`.
1165        Check out also `compute_normals()` and `compute_normals_with_pca()`.
1166        """
1167        vtknormals = self.dataset.GetPointData().GetNormals()
1168        return utils.vtk2numpy(vtknormals)
1169
1170    @property
1171    def point_normals(self) -> np.ndarray:
1172        """
1173        Retrieve vertex normals as a numpy array. Same as `vertex_normals`.
1174        Check out also `compute_normals()` and `compute_normals_with_pca()`.
1175        """
1176        vtknormals = self.dataset.GetPointData().GetNormals()
1177        return utils.vtk2numpy(vtknormals)
1178
1179    def align_to(self, target, iters=100, rigid=False, invert=False, use_centroids=False) -> Self:
1180        """
1181        Aligned to target mesh through the `Iterative Closest Point` algorithm.
1182
1183        The core of the algorithm is to match each vertex in one surface with
1184        the closest surface point on the other, then apply the transformation
1185        that modify one surface to best match the other (in the least-square sense).
1186
1187        Arguments:
1188            rigid : (bool)
1189                if True do not allow scaling
1190            invert : (bool)
1191                if True start by aligning the target to the source but
1192                invert the transformation finally. Useful when the target is smaller
1193                than the source.
1194            use_centroids : (bool)
1195                start by matching the centroids of the two objects.
1196
1197        Examples:
1198            - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py)
1199
1200                ![](https://vedo.embl.es/images/basic/align1.png)
1201
1202            - [align2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align2.py)
1203
1204                ![](https://vedo.embl.es/images/basic/align2.png)
1205        """
1206        icp = vtki.new("IterativeClosestPointTransform")
1207        icp.SetSource(self.dataset)
1208        icp.SetTarget(target.dataset)
1209        if invert:
1210            icp.Inverse()
1211        icp.SetMaximumNumberOfIterations(iters)
1212        if rigid:
1213            icp.GetLandmarkTransform().SetModeToRigidBody()
1214        icp.SetStartByMatchingCentroids(use_centroids)
1215        icp.Update()
1216
1217        self.apply_transform(icp.GetMatrix())
1218
1219        self.pipeline = utils.OperationNode(
1220            "align_to", parents=[self, target], comment=f"rigid = {rigid}"
1221        )
1222        return self
1223
1224    def align_to_bounding_box(self, msh, rigid=False) -> Self:
1225        """
1226        Align the current object's bounding box to the bounding box
1227        of the input object.
1228
1229        Use `rigid=True` to disable scaling.
1230
1231        Example:
1232            [align6.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align6.py)
1233        """
1234        lmt = vtki.vtkLandmarkTransform()
1235        ss = vtki.vtkPoints()
1236        xss0, xss1, yss0, yss1, zss0, zss1 = self.bounds()
1237        for p in [
1238            [xss0, yss0, zss0],
1239            [xss1, yss0, zss0],
1240            [xss1, yss1, zss0],
1241            [xss0, yss1, zss0],
1242            [xss0, yss0, zss1],
1243            [xss1, yss0, zss1],
1244            [xss1, yss1, zss1],
1245            [xss0, yss1, zss1],
1246        ]:
1247            ss.InsertNextPoint(p)
1248        st = vtki.vtkPoints()
1249        xst0, xst1, yst0, yst1, zst0, zst1 = msh.bounds()
1250        for p in [
1251            [xst0, yst0, zst0],
1252            [xst1, yst0, zst0],
1253            [xst1, yst1, zst0],
1254            [xst0, yst1, zst0],
1255            [xst0, yst0, zst1],
1256            [xst1, yst0, zst1],
1257            [xst1, yst1, zst1],
1258            [xst0, yst1, zst1],
1259        ]:
1260            st.InsertNextPoint(p)
1261
1262        lmt.SetSourceLandmarks(ss)
1263        lmt.SetTargetLandmarks(st)
1264        lmt.SetModeToAffine()
1265        if rigid:
1266            lmt.SetModeToRigidBody()
1267        lmt.Update()
1268
1269        LT = LinearTransform(lmt)
1270        self.apply_transform(LT)
1271        return self
1272
1273    def align_with_landmarks(
1274        self,
1275        source_landmarks,
1276        target_landmarks,
1277        rigid=False,
1278        affine=False,
1279        least_squares=False,
1280    ) -> Self:
1281        """
1282        Transform mesh orientation and position based on a set of landmarks points.
1283        The algorithm finds the best matching of source points to target points
1284        in the mean least square sense, in one single step.
1285
1286        If `affine` is True the x, y and z axes can scale independently but stay collinear.
1287        With least_squares they can vary orientation.
1288
1289        Examples:
1290            - [align5.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align5.py)
1291
1292                ![](https://vedo.embl.es/images/basic/align5.png)
1293        """
1294
1295        if utils.is_sequence(source_landmarks):
1296            ss = vtki.vtkPoints()
1297            for p in source_landmarks:
1298                ss.InsertNextPoint(p)
1299        else:
1300            ss = source_landmarks.dataset.GetPoints()
1301            if least_squares:
1302                source_landmarks = source_landmarks.vertices
1303
1304        if utils.is_sequence(target_landmarks):
1305            st = vtki.vtkPoints()
1306            for p in target_landmarks:
1307                st.InsertNextPoint(p)
1308        else:
1309            st = target_landmarks.GetPoints()
1310            if least_squares:
1311                target_landmarks = target_landmarks.vertices
1312
1313        if ss.GetNumberOfPoints() != st.GetNumberOfPoints():
1314            n1 = ss.GetNumberOfPoints()
1315            n2 = st.GetNumberOfPoints()
1316            vedo.logger.error(f"source and target have different nr of points {n1} vs {n2}")
1317            raise RuntimeError()
1318
1319        if int(rigid) + int(affine) + int(least_squares) > 1:
1320            vedo.logger.error(
1321                "only one of rigid, affine, least_squares can be True at a time"
1322            )
1323            raise RuntimeError()
1324
1325        lmt = vtki.vtkLandmarkTransform()
1326        lmt.SetSourceLandmarks(ss)
1327        lmt.SetTargetLandmarks(st)
1328        lmt.SetModeToSimilarity()
1329
1330        if rigid:
1331            lmt.SetModeToRigidBody()
1332            lmt.Update()
1333
1334        elif affine:
1335            lmt.SetModeToAffine()
1336            lmt.Update()
1337
1338        elif least_squares:
1339            cms = source_landmarks.mean(axis=0)
1340            cmt = target_landmarks.mean(axis=0)
1341            m = np.linalg.lstsq(source_landmarks - cms, target_landmarks - cmt, rcond=None)[0]
1342            M = vtki.vtkMatrix4x4()
1343            for i in range(3):
1344                for j in range(3):
1345                    M.SetElement(j, i, m[i][j])
1346            lmt = vtki.vtkTransform()
1347            lmt.Translate(cmt)
1348            lmt.Concatenate(M)
1349            lmt.Translate(-cms)
1350
1351        else:
1352            lmt.Update()
1353
1354        self.apply_transform(lmt)
1355        self.pipeline = utils.OperationNode("transform_with_landmarks", parents=[self])
1356        return self
1357
1358    def normalize(self) -> Self:
1359        """Scale average size to unit. The scaling is performed around the center of mass."""
1360        coords = self.vertices
1361        if not coords.shape[0]:
1362            return self
1363        cm = np.mean(coords, axis=0)
1364        pts = coords - cm
1365        xyz2 = np.sum(pts * pts, axis=0)
1366        scale = 1 / np.sqrt(np.sum(xyz2) / len(pts))
1367        self.scale(scale, origin=cm)
1368        self.pipeline = utils.OperationNode("normalize", parents=[self])
1369        return self
1370
1371    def mirror(self, axis="x", origin=True) -> Self:
1372        """
1373        Mirror reflect along one of the cartesian axes
1374
1375        Arguments:
1376            axis : (str)
1377                axis to use for mirroring, must be set to `x, y, z`.
1378                Or any combination of those.
1379            origin : (list)
1380                use this point as the origin of the mirroring transformation.
1381
1382        Examples:
1383            - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py)
1384
1385                ![](https://vedo.embl.es/images/basic/mirror.png)
1386        """
1387        sx, sy, sz = 1, 1, 1
1388        if "x" in axis.lower(): sx = -1
1389        if "y" in axis.lower(): sy = -1
1390        if "z" in axis.lower(): sz = -1
1391
1392        self.scale([sx, sy, sz], origin=origin)
1393
1394        self.pipeline = utils.OperationNode(
1395            "mirror", comment=f"axis = {axis}", parents=[self])
1396
1397        if sx * sy * sz < 0:
1398            if hasattr(self, "reverse"):
1399                self.reverse()
1400        return self
1401
1402    def flip_normals(self) -> Self:
1403        """Flip all normals orientation."""
1404        rs = vtki.new("ReverseSense")
1405        rs.SetInputData(self.dataset)
1406        rs.ReverseCellsOff()
1407        rs.ReverseNormalsOn()
1408        rs.Update()
1409        self._update(rs.GetOutput())
1410        self.pipeline = utils.OperationNode("flip_normals", parents=[self])
1411        return self
1412
1413    def add_gaussian_noise(self, sigma=1.0) -> Self:
1414        """
1415        Add gaussian noise to point positions.
1416        An extra array is added named "GaussianNoise" with the displacements.
1417
1418        Arguments:
1419            sigma : (float)
1420                nr. of standard deviations, expressed in percent of the diagonal size of mesh.
1421                Can also be a list `[sigma_x, sigma_y, sigma_z]`.
1422
1423        Example:
1424            ```python
1425            from vedo import Sphere
1426            Sphere().add_gaussian_noise(1.0).point_size(8).show().close()
1427            ```
1428        """
1429        sz = self.diagonal_size()
1430        pts = self.vertices
1431        n = len(pts)
1432        ns = (np.random.randn(n, 3) * sigma) * (sz / 100)
1433        vpts = vtki.vtkPoints()
1434        vpts.SetNumberOfPoints(n)
1435        vpts.SetData(utils.numpy2vtk(pts + ns, dtype=np.float32))
1436        self.dataset.SetPoints(vpts)
1437        self.dataset.GetPoints().Modified()
1438        self.pointdata["GaussianNoise"] = -ns
1439        self.pipeline = utils.OperationNode(
1440            "gaussian_noise", parents=[self], shape="egg", comment=f"sigma = {sigma}"
1441        )
1442        return self
1443
1444    def closest_point(
1445        self, pt, n=1, radius=None, return_point_id=False, return_cell_id=False
1446    ) -> Union[List[int], int, np.ndarray]:
1447        """
1448        Find the closest point(s) on a mesh given from the input point `pt`.
1449
1450        Arguments:
1451            n : (int)
1452                if greater than 1, return a list of n ordered closest points
1453            radius : (float)
1454                if given, get all points within that radius. Then n is ignored.
1455            return_point_id : (bool)
1456                return point ID instead of coordinates
1457            return_cell_id : (bool)
1458                return cell ID in which the closest point sits
1459
1460        Examples:
1461            - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py)
1462            - [fitplanes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitplanes.py)
1463            - [quadratic_morphing.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/quadratic_morphing.py)
1464
1465        .. note::
1466            The appropriate tree search locator is built on the fly and cached for speed.
1467
1468            If you want to reset it use `mymesh.point_locator=None`
1469            and / or `mymesh.cell_locator=None`.
1470        """
1471        if len(pt) != 3:
1472            pt = [pt[0], pt[1], 0]
1473
1474        # NB: every time the mesh moves or is warped the locators are set to None
1475        if ((n > 1 or radius) or (n == 1 and return_point_id)) and not return_cell_id:
1476            poly = None
1477            if not self.point_locator:
1478                poly = self.dataset
1479                self.point_locator = vtki.new("StaticPointLocator")
1480                self.point_locator.SetDataSet(poly)
1481                self.point_locator.BuildLocator()
1482
1483            ##########
1484            if radius:
1485                vtklist = vtki.vtkIdList()
1486                self.point_locator.FindPointsWithinRadius(radius, pt, vtklist)
1487            elif n > 1:
1488                vtklist = vtki.vtkIdList()
1489                self.point_locator.FindClosestNPoints(n, pt, vtklist)
1490            else:  # n==1 hence return_point_id==True
1491                ########
1492                return self.point_locator.FindClosestPoint(pt)
1493                ########
1494
1495            if return_point_id:
1496                ########
1497                return utils.vtk2numpy(vtklist)
1498                ########
1499
1500            if not poly:
1501                poly = self.dataset
1502            trgp = []
1503            for i in range(vtklist.GetNumberOfIds()):
1504                trgp_ = [0, 0, 0]
1505                vi = vtklist.GetId(i)
1506                poly.GetPoints().GetPoint(vi, trgp_)
1507                trgp.append(trgp_)
1508            ########
1509            return np.array(trgp)
1510            ########
1511
1512        else:
1513
1514            if not self.cell_locator:
1515                poly = self.dataset
1516
1517                # As per Miquel example with limbs the vtkStaticCellLocator doesnt work !!
1518                # https://discourse.vtk.org/t/vtkstaticcelllocator-problem-vtk9-0-3/7854/4
1519                if vedo.vtk_version[0] >= 9 and vedo.vtk_version[1] > 0:
1520                    self.cell_locator = vtki.new("StaticCellLocator")
1521                else:
1522                    self.cell_locator = vtki.new("CellLocator")
1523
1524                self.cell_locator.SetDataSet(poly)
1525                self.cell_locator.BuildLocator()
1526
1527            if radius is not None:
1528                vedo.printc("Warning: closest_point() with radius is not implemented for cells.", c='r')   
1529 
1530            if n != 1:
1531                vedo.printc("Warning: closest_point() with n>1 is not implemented for cells.", c='r')   
1532 
1533            trgp = [0, 0, 0]
1534            cid = vtki.mutable(0)
1535            dist2 = vtki.mutable(0)
1536            subid = vtki.mutable(0)
1537            self.cell_locator.FindClosestPoint(pt, trgp, cid, subid, dist2)
1538
1539            if return_cell_id:
1540                return int(cid)
1541
1542            return np.array(trgp)
1543
1544    def auto_distance(self) -> np.ndarray:
1545        """
1546        Calculate the distance to the closest point in the same cloud of points.
1547        The output is stored in a new pointdata array called "AutoDistance",
1548        and it is also returned by the function.
1549        """
1550        points = self.vertices
1551        if not self.point_locator:
1552            self.point_locator = vtki.new("StaticPointLocator")
1553            self.point_locator.SetDataSet(self.dataset)
1554            self.point_locator.BuildLocator()
1555        qs = []
1556        vtklist = vtki.vtkIdList()
1557        vtkpoints = self.dataset.GetPoints()
1558        for p in points:
1559            self.point_locator.FindClosestNPoints(2, p, vtklist)
1560            q = [0, 0, 0]
1561            pid = vtklist.GetId(1)
1562            vtkpoints.GetPoint(pid, q)
1563            qs.append(q)
1564        dists = np.linalg.norm(points - np.array(qs), axis=1)
1565        self.pointdata["AutoDistance"] = dists
1566        return dists
1567
1568    def hausdorff_distance(self, points) -> float:
1569        """
1570        Compute the Hausdorff distance to the input point set.
1571        Returns a single `float`.
1572
1573        Example:
1574            ```python
1575            from vedo import *
1576            t = np.linspace(0, 2*np.pi, 100)
1577            x = 4/3 * sin(t)**3
1578            y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12
1579            pol1 = Line(np.c_[x,y], closed=True).triangulate()
1580            pol2 = Polygon(nsides=5).pos(2,2)
1581            d12 = pol1.distance_to(pol2)
1582            d21 = pol2.distance_to(pol1)
1583            pol1.lw(0).cmap("viridis")
1584            pol2.lw(0).cmap("viridis")
1585            print("distance d12, d21 :", min(d12), min(d21))
1586            print("hausdorff distance:", pol1.hausdorff_distance(pol2))
1587            print("chamfer distance  :", pol1.chamfer_distance(pol2))
1588            show(pol1, pol2, axes=1)
1589            ```
1590            ![](https://vedo.embl.es/images/feats/heart.png)
1591        """
1592        hp = vtki.new("HausdorffDistancePointSetFilter")
1593        hp.SetInputData(0, self.dataset)
1594        hp.SetInputData(1, points.dataset)
1595        hp.SetTargetDistanceMethodToPointToCell()
1596        hp.Update()
1597        return hp.GetHausdorffDistance()
1598
1599    def chamfer_distance(self, pcloud) -> float:
1600        """
1601        Compute the Chamfer distance to the input point set.
1602
1603        Example:
1604            ```python
1605            from vedo import *
1606            cloud1 = np.random.randn(1000, 3)
1607            cloud2 = np.random.randn(1000, 3) + [1, 2, 3]
1608            c1 = Points(cloud1, r=5, c="red")
1609            c2 = Points(cloud2, r=5, c="green")
1610            d = c1.chamfer_distance(c2)
1611            show(f"Chamfer distance = {d}", c1, c2, axes=1).close()
1612            ```
1613        """
1614        # Definition of Chamfer distance may vary, here we use the average
1615        if not pcloud.point_locator:
1616            pcloud.point_locator = vtki.new("PointLocator")
1617            pcloud.point_locator.SetDataSet(pcloud.dataset)
1618            pcloud.point_locator.BuildLocator()
1619        if not self.point_locator:
1620            self.point_locator = vtki.new("PointLocator")
1621            self.point_locator.SetDataSet(self.dataset)
1622            self.point_locator.BuildLocator()
1623
1624        ps1 = self.vertices
1625        ps2 = pcloud.vertices
1626
1627        ids12 = []
1628        for p in ps1:
1629            pid12 = pcloud.point_locator.FindClosestPoint(p)
1630            ids12.append(pid12)
1631        deltav = ps2[ids12] - ps1
1632        da = np.mean(np.linalg.norm(deltav, axis=1))
1633
1634        ids21 = []
1635        for p in ps2:
1636            pid21 = self.point_locator.FindClosestPoint(p)
1637            ids21.append(pid21)
1638        deltav = ps1[ids21] - ps2
1639        db = np.mean(np.linalg.norm(deltav, axis=1))
1640        return (da + db) / 2
1641
1642    def remove_outliers(self, radius: float, neighbors=5) -> Self:
1643        """
1644        Remove outliers from a cloud of points within the specified `radius` search.
1645
1646        Arguments:
1647            radius : (float)
1648                Specify the local search radius.
1649            neighbors : (int)
1650                Specify the number of neighbors that a point must have,
1651                within the specified radius, for the point to not be considered isolated.
1652
1653        Examples:
1654            - [clustering.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/clustering.py)
1655
1656                ![](https://vedo.embl.es/images/basic/clustering.png)
1657        """
1658        removal = vtki.new("RadiusOutlierRemoval")
1659        removal.SetInputData(self.dataset)
1660        removal.SetRadius(radius)
1661        removal.SetNumberOfNeighbors(neighbors)
1662        removal.GenerateOutliersOff()
1663        removal.Update()
1664        inputobj = removal.GetOutput()
1665        if inputobj.GetNumberOfCells() == 0:
1666            carr = vtki.vtkCellArray()
1667            for i in range(inputobj.GetNumberOfPoints()):
1668                carr.InsertNextCell(1)
1669                carr.InsertCellPoint(i)
1670            inputobj.SetVerts(carr)
1671        self._update(removal.GetOutput())
1672        self.pipeline = utils.OperationNode("remove_outliers", parents=[self])
1673        return self
1674
1675    def relax_point_positions(
1676            self, 
1677            n=10,
1678            iters=10,
1679            sub_iters=10,
1680            packing_factor=1,
1681            max_step=0,
1682            constraints=(),
1683        ) -> Self:
1684        """
1685        Smooth mesh or points with a 
1686        [Laplacian algorithm](https://vtk.org/doc/nightly/html/classvtkPointSmoothingFilter.html)
1687        variant. This modifies the coordinates of the input points by adjusting their positions
1688        to create a smooth distribution (and thereby form a pleasing packing of the points).
1689        Smoothing is performed by considering the effects of neighboring points on one another
1690        it uses a cubic cutoff function to produce repulsive forces between close points
1691        and attractive forces that are a little further away.
1692        
1693        In general, the larger the neighborhood size, the greater the reduction in high frequency
1694        information. The memory and computational requirements of the algorithm may also
1695        significantly increase.
1696
1697        The algorithm incrementally adjusts the point positions through an iterative process.
1698        Basically points are moved due to the influence of neighboring points. 
1699        
1700        As points move, both the local connectivity and data attributes associated with each point
1701        must be updated. Rather than performing these expensive operations after every iteration,
1702        a number of sub-iterations can be specified. If so, then the neighborhood and attribute
1703        value updates occur only every sub iteration, which can improve performance significantly.
1704        
1705        Arguments:
1706            n : (int)
1707                neighborhood size to calculate the Laplacian.
1708            iters : (int)
1709                number of iterations.
1710            sub_iters : (int)
1711                number of sub-iterations, i.e. the number of times the neighborhood and attribute
1712                value updates occur during each iteration.
1713            packing_factor : (float)
1714                adjust convergence speed.
1715            max_step : (float)
1716                Specify the maximum smoothing step size for each smoothing iteration.
1717                This limits the the distance over which a point can move in each iteration.
1718                As in all iterative methods, the stability of the process is sensitive to this parameter.
1719                In general, small step size and large numbers of iterations are more stable than a larger
1720                step size and a smaller numbers of iterations.
1721            constraints : (dict)
1722                dictionary of constraints.
1723                Point constraints are used to prevent points from moving,
1724                or to move only on a plane. This can prevent shrinking or growing point clouds.
1725                If enabled, a local topological analysis is performed to determine whether a point
1726                should be marked as fixed" i.e., never moves, or the point only moves on a plane,
1727                or the point can move freely.
1728                If all points in the neighborhood surrounding a point are in the cone defined by
1729                `fixed_angle`, then the point is classified as fixed.
1730                If all points in the neighborhood surrounding a point are in the cone defined by
1731                `boundary_angle`, then the point is classified as lying on a plane.
1732                Angles are expressed in degrees.
1733        
1734        Example:
1735            ```py
1736            import numpy as np
1737            from vedo import Points, show
1738            from vedo.pyplot import histogram
1739
1740            vpts1 = Points(np.random.rand(10_000, 3))
1741            dists = vpts1.auto_distance()
1742            h1 = histogram(dists, xlim=(0,0.08)).clone2d()
1743
1744            vpts2 = vpts1.clone().relax_point_positions(n=100, iters=20, sub_iters=10)
1745            dists = vpts2.auto_distance()
1746            h2 = histogram(dists, xlim=(0,0.08)).clone2d()
1747
1748            show([[vpts1, h1], [vpts2, h2]], N=2).close()
1749            ```
1750        """
1751        smooth = vtki.new("PointSmoothingFilter")
1752        smooth.SetInputData(self.dataset)
1753        smooth.SetSmoothingModeToUniform()
1754        smooth.SetNumberOfIterations(iters)
1755        smooth.SetNumberOfSubIterations(sub_iters)
1756        smooth.SetPackingFactor(packing_factor)
1757        if self.point_locator:
1758            smooth.SetLocator(self.point_locator)
1759        if not max_step:
1760            max_step = self.diagonal_size() / 100
1761        smooth.SetMaximumStepSize(max_step)
1762        smooth.SetNeighborhoodSize(n)
1763        if constraints:
1764            fixed_angle = constraints.get("fixed_angle", 45)
1765            boundary_angle = constraints.get("boundary_angle", 110)
1766            smooth.EnableConstraintsOn()
1767            smooth.SetFixedAngle(fixed_angle)
1768            smooth.SetBoundaryAngle(boundary_angle)
1769            smooth.GenerateConstraintScalarsOn()
1770            smooth.GenerateConstraintNormalsOn()
1771        smooth.Update()
1772        self._update(smooth.GetOutput())
1773        self.metadata["PackingRadius"] = smooth.GetPackingRadius()
1774        self.pipeline = utils.OperationNode("relax_point_positions", parents=[self])
1775        return self
1776
1777    def smooth_mls_1d(self, f=0.2, radius=None, n=0) -> Self:
1778        """
1779        Smooth mesh or points with a `Moving Least Squares` variant.
1780        The point data array "Variances" will contain the residue calculated for each point.
1781
1782        Arguments:
1783            f : (float)
1784                smoothing factor - typical range is [0,2].
1785            radius : (float)
1786                radius search in absolute units.
1787                If set then `f` is ignored.
1788            n : (int)
1789                number of neighbours to be used for the fit.
1790                If set then `f` and `radius` are ignored.
1791
1792        Examples:
1793            - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py)
1794            - [skeletonize.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/skeletonize.py)
1795
1796            ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png)
1797        """
1798        coords = self.vertices
1799        ncoords = len(coords)
1800
1801        if n:
1802            Ncp = n
1803        elif radius:
1804            Ncp = 1
1805        else:
1806            Ncp = int(ncoords * f / 10)
1807            if Ncp < 5:
1808                vedo.logger.warning(f"Please choose a fraction higher than {f}")
1809                Ncp = 5
1810
1811        variances, newline = [], []
1812        for p in coords:
1813            points = self.closest_point(p, n=Ncp, radius=radius)
1814            if len(points) < 4:
1815                continue
1816
1817            points = np.array(points)
1818            pointsmean = points.mean(axis=0)  # plane center
1819            _, dd, vv = np.linalg.svd(points - pointsmean)
1820            newp = np.dot(p - pointsmean, vv[0]) * vv[0] + pointsmean
1821            variances.append(dd[1] + dd[2])
1822            newline.append(newp)
1823
1824        self.pointdata["Variances"] = np.array(variances).astype(np.float32)
1825        self.vertices = newline
1826        self.pipeline = utils.OperationNode("smooth_mls_1d", parents=[self])
1827        return self
1828
1829    def smooth_mls_2d(self, f=0.2, radius=None, n=0) -> Self:
1830        """
1831        Smooth mesh or points with a `Moving Least Squares` algorithm variant.
1832
1833        The `mesh.pointdata['MLSVariance']` array will contain the residue calculated for each point.
1834        When a radius is specified, points that are isolated will not be moved and will get
1835        a 0 entry in array `mesh.pointdata['MLSValidPoint']`.
1836
1837        Arguments:
1838            f : (float)
1839                smoothing factor - typical range is [0, 2].
1840            radius : (float | array)
1841                radius search in absolute units. Can be single value (float) or sequence
1842                for adaptive smoothing. If set then `f` is ignored.
1843            n : (int)
1844                number of neighbours to be used for the fit.
1845                If set then `f` and `radius` are ignored.
1846
1847        Examples:
1848            - [moving_least_squares2D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares2D.py)
1849            - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py)
1850
1851                ![](https://vedo.embl.es/images/advanced/recosurface.png)
1852        """
1853        coords = self.vertices
1854        ncoords = len(coords)
1855
1856        if n:
1857            Ncp = n
1858            radius = None
1859        elif radius is not None:
1860            Ncp = 1
1861        else:
1862            Ncp = int(ncoords * f / 100)
1863            if Ncp < 4:
1864                vedo.logger.error(f"please choose a f-value higher than {f}")
1865                Ncp = 4
1866
1867        variances, newpts, valid = [], [], []
1868        radius_is_sequence = utils.is_sequence(radius)
1869
1870        pb = None
1871        if ncoords > 10000:
1872            pb = utils.ProgressBar(0, ncoords, delay=3)
1873
1874        for i, p in enumerate(coords):
1875            if pb:
1876                pb.print("smooth_mls_2d working ...")
1877            
1878            # if a radius was provided for each point
1879            if radius_is_sequence:
1880                pts = self.closest_point(p, n=Ncp, radius=radius[i])
1881            else:
1882                pts = self.closest_point(p, n=Ncp, radius=radius)
1883
1884            if len(pts) > 3:
1885                ptsmean = pts.mean(axis=0)  # plane center
1886                _, dd, vv = np.linalg.svd(pts - ptsmean)
1887                cv = np.cross(vv[0], vv[1])
1888                t = (np.dot(cv, ptsmean) - np.dot(cv, p)) / np.dot(cv, cv)
1889                newpts.append(p + cv * t)
1890                variances.append(dd[2])
1891                if radius is not None:
1892                    valid.append(1)
1893            else:
1894                newpts.append(p)
1895                variances.append(0)
1896                if radius is not None:
1897                    valid.append(0)
1898
1899        if radius is not None:
1900            self.pointdata["MLSValidPoint"] = np.array(valid).astype(np.uint8)
1901        self.pointdata["MLSVariance"] = np.array(variances).astype(np.float32)
1902
1903        self.vertices = newpts
1904
1905        self.pipeline = utils.OperationNode("smooth_mls_2d", parents=[self])
1906        return self
1907
1908    def smooth_lloyd_2d(self, iterations=2, bounds=None, options="Qbb Qc Qx") -> Self:
1909        """
1910        Lloyd relaxation of a 2D pointcloud.
1911        
1912        Arguments:
1913            iterations : (int)
1914                number of iterations.
1915            bounds : (list)
1916                bounding box of the domain.
1917            options : (str)
1918                options for the Qhull algorithm.
1919        """
1920        # Credits: https://hatarilabs.com/ih-en/
1921        # tutorial-to-create-a-geospatial-voronoi-sh-mesh-with-python-scipy-and-geopandas
1922        from scipy.spatial import Voronoi as scipy_voronoi
1923
1924        def _constrain_points(points):
1925            # Update any points that have drifted beyond the boundaries of this space
1926            if bounds is not None:
1927                for point in points:
1928                    if point[0] < bounds[0]: point[0] = bounds[0]
1929                    if point[0] > bounds[1]: point[0] = bounds[1]
1930                    if point[1] < bounds[2]: point[1] = bounds[2]
1931                    if point[1] > bounds[3]: point[1] = bounds[3]
1932            return points
1933
1934        def _find_centroid(vertices):
1935            # The equation for the method used here to find the centroid of a
1936            # 2D polygon is given here: https://en.wikipedia.org/wiki/Centroid#Of_a_polygon
1937            area = 0
1938            centroid_x = 0
1939            centroid_y = 0
1940            for i in range(len(vertices) - 1):
1941                step = (vertices[i, 0] * vertices[i + 1, 1]) - (vertices[i + 1, 0] * vertices[i, 1])
1942                centroid_x += (vertices[i, 0] + vertices[i + 1, 0]) * step
1943                centroid_y += (vertices[i, 1] + vertices[i + 1, 1]) * step
1944                area += step
1945            if area:
1946                centroid_x = (1.0 / (3.0 * area)) * centroid_x
1947                centroid_y = (1.0 / (3.0 * area)) * centroid_y
1948            # prevent centroids from escaping bounding box
1949            return _constrain_points([[centroid_x, centroid_y]])[0]
1950
1951        def _relax(voron):
1952            # Moves each point to the centroid of its cell in the voronoi
1953            # map to "relax" the points (i.e. jitter the points so as
1954            # to spread them out within the space).
1955            centroids = []
1956            for idx in voron.point_region:
1957                # the region is a series of indices into voronoi.vertices
1958                # remove point at infinity, designated by index -1
1959                region = [i for i in voron.regions[idx] if i != -1]
1960                # enclose the polygon
1961                region = region + [region[0]]
1962                verts = voron.vertices[region]
1963                # find the centroid of those vertices
1964                centroids.append(_find_centroid(verts))
1965            return _constrain_points(centroids)
1966
1967        if bounds is None:
1968            bounds = self.bounds()
1969
1970        pts = self.vertices[:, (0, 1)]
1971        for i in range(iterations):
1972            vor = scipy_voronoi(pts, qhull_options=options)
1973            _constrain_points(vor.vertices)
1974            pts = _relax(vor)
1975        out = Points(pts)
1976        out.name = "MeshSmoothLloyd2D"
1977        out.pipeline = utils.OperationNode("smooth_lloyd", parents=[self])
1978        return out
1979
1980    def project_on_plane(self, plane="z", point=None, direction=None) -> Self:
1981        """
1982        Project the mesh on one of the Cartesian planes.
1983
1984        Arguments:
1985            plane : (str, Plane)
1986                if plane is `str`, plane can be one of ['x', 'y', 'z'],
1987                represents x-plane, y-plane and z-plane, respectively.
1988                Otherwise, plane should be an instance of `vedo.shapes.Plane`.
1989            point : (float, array)
1990                if plane is `str`, point should be a float represents the intercept.
1991                Otherwise, point is the camera point of perspective projection
1992            direction : (array)
1993                direction of oblique projection
1994
1995        Note:
1996            Parameters `point` and `direction` are only used if the given plane
1997            is an instance of `vedo.shapes.Plane`. And one of these two params
1998            should be left as `None` to specify the projection type.
1999
2000        Example:
2001            ```python
2002            s.project_on_plane(plane='z') # project to z-plane
2003            plane = Plane(pos=(4, 8, -4), normal=(-1, 0, 1), s=(5,5))
2004            s.project_on_plane(plane=plane)                       # orthogonal projection
2005            s.project_on_plane(plane=plane, point=(6, 6, 6))      # perspective projection
2006            s.project_on_plane(plane=plane, direction=(1, 2, -1)) # oblique projection
2007            ```
2008
2009        Examples:
2010            - [silhouette2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette2.py)
2011
2012                ![](https://vedo.embl.es/images/basic/silhouette2.png)
2013        """
2014        coords = self.vertices
2015
2016        if plane == "x":
2017            coords[:, 0] = self.transform.position[0]
2018            intercept = self.xbounds()[0] if point is None else point
2019            self.x(intercept)
2020        elif plane == "y":
2021            coords[:, 1] = self.transform.position[1]
2022            intercept = self.ybounds()[0] if point is None else point
2023            self.y(intercept)
2024        elif plane == "z":
2025            coords[:, 2] = self.transform.position[2]
2026            intercept = self.zbounds()[0] if point is None else point
2027            self.z(intercept)
2028
2029        elif isinstance(plane, vedo.shapes.Plane):
2030            normal = plane.normal / np.linalg.norm(plane.normal)
2031            pl = np.hstack((normal, -np.dot(plane.pos(), normal))).reshape(4, 1)
2032            if direction is None and point is None:
2033                # orthogonal projection
2034                pt = np.hstack((normal, [0])).reshape(4, 1)
2035                # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T # python3 only
2036                proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T)
2037
2038            elif direction is None:
2039                # perspective projection
2040                pt = np.hstack((np.array(point), [1])).reshape(4, 1)
2041                # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T
2042                proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T)
2043
2044            elif point is None:
2045                # oblique projection
2046                pt = np.hstack((np.array(direction), [0])).reshape(4, 1)
2047                # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T
2048                proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T)
2049
2050            coords = np.concatenate([coords, np.ones((coords.shape[:-1] + (1,)))], axis=-1)
2051            # coords = coords @ proj_mat.T
2052            coords = np.matmul(coords, proj_mat.T)
2053            coords = coords[:, :3] / coords[:, 3:]
2054
2055        else:
2056            vedo.logger.error(f"unknown plane {plane}")
2057            raise RuntimeError()
2058
2059        self.alpha(0.1)
2060        self.vertices = coords
2061        return self
2062
2063    def warp(self, source, target, sigma=1.0, mode="3d") -> Self:
2064        """
2065        "Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set
2066        of source and target landmarks. Any point on the mesh close to a source landmark will
2067        be moved to a place close to the corresponding target landmark.
2068        The points in between are interpolated smoothly using
2069        Bookstein's Thin Plate Spline algorithm.
2070
2071        Transformation object can be accessed with `mesh.transform`.
2072
2073        Arguments:
2074            sigma : (float)
2075                specify the 'stiffness' of the spline.
2076            mode : (str)
2077                set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes)
2078
2079        Examples:
2080            - [interpolate_field.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/interpolate_field.py)
2081            - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py)
2082            - [warp2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp2.py)
2083            - [warp3.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp3.py)
2084            - [warp4a.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4a.py)
2085            - [warp4b.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4b.py)
2086            - [warp6.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp6.py)
2087
2088            ![](https://vedo.embl.es/images/advanced/warp2.png)
2089        """
2090        parents = [self]
2091
2092        try:
2093            source = source.vertices
2094            parents.append(source)
2095        except AttributeError:
2096            source = utils.make3d(source)
2097        
2098        try:
2099            target = target.vertices
2100            parents.append(target)
2101        except AttributeError:
2102            target = utils.make3d(target)
2103
2104        ns = len(source)
2105        nt = len(target)
2106        if ns != nt:
2107            vedo.logger.error(f"#source {ns} != {nt} #target points")
2108            raise RuntimeError()
2109
2110        NLT = NonLinearTransform()
2111        NLT.source_points = source
2112        NLT.target_points = target
2113        self.apply_transform(NLT)
2114
2115        self.pipeline = utils.OperationNode("warp", parents=parents)
2116        return self
2117
2118    def cut_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0), invert=False) -> Self:
2119        """
2120        Cut the mesh with the plane defined by a point and a normal.
2121
2122        Arguments:
2123            origin : (array)
2124                the cutting plane goes through this point
2125            normal : (array)
2126                normal of the cutting plane
2127
2128        Example:
2129            ```python
2130            from vedo import Cube
2131            cube = Cube().cut_with_plane(normal=(1,1,1))
2132            cube.back_color('pink').show().close()
2133            ```
2134            ![](https://vedo.embl.es/images/feats/cut_with_plane_cube.png)
2135
2136        Examples:
2137            - [trail.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/trail.py)
2138
2139                ![](https://vedo.embl.es/images/simulations/trail.gif)
2140
2141        Check out also:
2142            `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`.
2143        """
2144        s = str(normal)
2145        if "x" in s:
2146            normal = (1, 0, 0)
2147            if "-" in s:
2148                normal = -np.array(normal)
2149        elif "y" in s:
2150            normal = (0, 1, 0)
2151            if "-" in s:
2152                normal = -np.array(normal)
2153        elif "z" in s:
2154            normal = (0, 0, 1)
2155            if "-" in s:
2156                normal = -np.array(normal)
2157        plane = vtki.vtkPlane()
2158        plane.SetOrigin(origin)
2159        plane.SetNormal(normal)
2160
2161        clipper = vtki.new("ClipPolyData")
2162        clipper.SetInputData(self.dataset)
2163        clipper.SetClipFunction(plane)
2164        clipper.GenerateClippedOutputOff()
2165        clipper.GenerateClipScalarsOff()
2166        clipper.SetInsideOut(invert)
2167        clipper.SetValue(0)
2168        clipper.Update()
2169
2170        self._update(clipper.GetOutput())
2171
2172        self.pipeline = utils.OperationNode("cut_with_plane", parents=[self])
2173        return self
2174
2175    def cut_with_planes(self, origins, normals, invert=False) -> Self:
2176        """
2177        Cut the mesh with a convex set of planes defined by points and normals.
2178
2179        Arguments:
2180            origins : (array)
2181                each cutting plane goes through this point
2182            normals : (array)
2183                normal of each of the cutting planes
2184            invert : (bool)
2185                if True, cut outside instead of inside
2186
2187        Check out also:
2188            `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`
2189        """
2190
2191        vpoints = vtki.vtkPoints()
2192        for p in utils.make3d(origins):
2193            vpoints.InsertNextPoint(p)
2194        normals = utils.make3d(normals)
2195
2196        planes = vtki.vtkPlanes()
2197        planes.SetPoints(vpoints)
2198        planes.SetNormals(utils.numpy2vtk(normals, dtype=float))
2199
2200        clipper = vtki.new("ClipPolyData")
2201        clipper.SetInputData(self.dataset)
2202        clipper.SetInsideOut(invert)
2203        clipper.SetClipFunction(planes)
2204        clipper.GenerateClippedOutputOff()
2205        clipper.GenerateClipScalarsOff()
2206        clipper.SetValue(0)
2207        clipper.Update()
2208
2209        self._update(clipper.GetOutput())
2210
2211        self.pipeline = utils.OperationNode("cut_with_planes", parents=[self])
2212        return self
2213
2214    def cut_with_box(self, bounds, invert=False) -> Self:
2215        """
2216        Cut the current mesh with a box or a set of boxes.
2217        This is much faster than `cut_with_mesh()`.
2218
2219        Input `bounds` can be either:
2220        - a Mesh or Points object
2221        - a list of 6 number representing a bounding box `[xmin,xmax, ymin,ymax, zmin,zmax]`
2222        - a list of bounding boxes like the above: `[[xmin1,...], [xmin2,...], ...]`
2223
2224        Example:
2225            ```python
2226            from vedo import Sphere, Cube, show
2227            mesh = Sphere(r=1, res=50)
2228            box  = Cube(side=1.5).wireframe()
2229            mesh.cut_with_box(box)
2230            show(mesh, box, axes=1).close()
2231            ```
2232            ![](https://vedo.embl.es/images/feats/cut_with_box_cube.png)
2233
2234        Check out also:
2235            `cut_with_line()`, `cut_with_plane()`, `cut_with_cylinder()`
2236        """
2237        if isinstance(bounds, Points):
2238            bounds = bounds.bounds()
2239
2240        box = vtki.new("Box")
2241        if utils.is_sequence(bounds[0]):
2242            for bs in bounds:
2243                box.AddBounds(bs)
2244        else:
2245            box.SetBounds(bounds)
2246
2247        clipper = vtki.new("ClipPolyData")
2248        clipper.SetInputData(self.dataset)
2249        clipper.SetClipFunction(box)
2250        clipper.SetInsideOut(not invert)
2251        clipper.GenerateClippedOutputOff()
2252        clipper.GenerateClipScalarsOff()
2253        clipper.SetValue(0)
2254        clipper.Update()
2255        self._update(clipper.GetOutput())
2256
2257        self.pipeline = utils.OperationNode("cut_with_box", parents=[self])
2258        return self
2259
2260    def cut_with_line(self, points, invert=False, closed=True) -> Self:
2261        """
2262        Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter.
2263        The polyline is defined by a set of points (z-coordinates are ignored).
2264        This is much faster than `cut_with_mesh()`.
2265
2266        Check out also:
2267            `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()`
2268        """
2269        pplane = vtki.new("PolyPlane")
2270        if isinstance(points, Points):
2271            points = points.vertices.tolist()
2272
2273        if closed:
2274            if isinstance(points, np.ndarray):
2275                points = points.tolist()
2276            points.append(points[0])
2277
2278        vpoints = vtki.vtkPoints()
2279        for p in points:
2280            if len(p) == 2:
2281                p = [p[0], p[1], 0.0]
2282            vpoints.InsertNextPoint(p)
2283
2284        n = len(points)
2285        polyline = vtki.new("PolyLine")
2286        polyline.Initialize(n, vpoints)
2287        polyline.GetPointIds().SetNumberOfIds(n)
2288        for i in range(n):
2289            polyline.GetPointIds().SetId(i, i)
2290        pplane.SetPolyLine(polyline)
2291
2292        clipper = vtki.new("ClipPolyData")
2293        clipper.SetInputData(self.dataset)
2294        clipper.SetClipFunction(pplane)
2295        clipper.SetInsideOut(invert)
2296        clipper.GenerateClippedOutputOff()
2297        clipper.GenerateClipScalarsOff()
2298        clipper.SetValue(0)
2299        clipper.Update()
2300        self._update(clipper.GetOutput())
2301
2302        self.pipeline = utils.OperationNode("cut_with_line", parents=[self])
2303        return self
2304
2305    def cut_with_cookiecutter(self, lines) -> Self:
2306        """
2307        Cut the current mesh with a single line or a set of lines.
2308
2309        Input `lines` can be either:
2310        - a `Mesh` or `Points` object
2311        - a list of 3D points: `[(x1,y1,z1), (x2,y2,z2), ...]`
2312        - a list of 2D points: `[(x1,y1), (x2,y2), ...]`
2313
2314        Example:
2315            ```python
2316            from vedo import *
2317            grid = Mesh(dataurl + "dolfin_fine.vtk")
2318            grid.compute_quality().cmap("Greens")
2319            pols = merge(
2320                Polygon(nsides=10, r=0.3).pos(0.7, 0.3),
2321                Polygon(nsides=10, r=0.2).pos(0.3, 0.7),
2322            )
2323            lines = pols.boundaries()
2324            cgrid = grid.clone().cut_with_cookiecutter(lines)
2325            grid.alpha(0.1).wireframe()
2326            show(grid, cgrid, lines, axes=8, bg='blackboard').close()
2327            ```
2328            ![](https://vedo.embl.es/images/feats/cookiecutter.png)
2329
2330        Check out also:
2331            `cut_with_line()` and `cut_with_point_loop()`
2332
2333        Note:
2334            In case of a warning message like:
2335                "Mesh and trim loop point data attributes are different"
2336            consider interpolating the mesh point data to the loop points,
2337            Eg. (in the above example):
2338            ```python
2339            lines = pols.boundaries().interpolate_data_from(grid, n=2)
2340            ```
2341
2342        Note:
2343            trying to invert the selection by reversing the loop order
2344            will have no effect in this method, hence it does not have
2345            the `invert` option.
2346        """
2347        if utils.is_sequence(lines):
2348            lines = utils.make3d(lines)
2349            iline = list(range(len(lines))) + [0]
2350            poly = utils.buildPolyData(lines, lines=[iline])
2351        else:
2352            poly = lines.dataset
2353
2354        # if invert: # not working
2355        #     rev = vtki.new("ReverseSense")
2356        #     rev.ReverseCellsOn()
2357        #     rev.SetInputData(poly)
2358        #     rev.Update()
2359        #     poly = rev.GetOutput()
2360
2361        # Build loops from the polyline
2362        build_loops = vtki.new("ContourLoopExtraction")
2363        build_loops.SetGlobalWarningDisplay(0)
2364        build_loops.SetInputData(poly)
2365        build_loops.Update()
2366        boundary_poly = build_loops.GetOutput()
2367
2368        ccut = vtki.new("CookieCutter")
2369        ccut.SetInputData(self.dataset)
2370        ccut.SetLoopsData(boundary_poly)
2371        ccut.SetPointInterpolationToMeshEdges()
2372        # ccut.SetPointInterpolationToLoopEdges()
2373        ccut.PassCellDataOn()
2374        ccut.PassPointDataOn()
2375        ccut.Update()
2376        self._update(ccut.GetOutput())
2377
2378        self.pipeline = utils.OperationNode("cut_with_cookiecutter", parents=[self])
2379        return self
2380
2381    def cut_with_cylinder(self, center=(0, 0, 0), axis=(0, 0, 1), r=1, invert=False) -> Self:
2382        """
2383        Cut the current mesh with an infinite cylinder.
2384        This is much faster than `cut_with_mesh()`.
2385
2386        Arguments:
2387            center : (array)
2388                the center of the cylinder
2389            normal : (array)
2390                direction of the cylinder axis
2391            r : (float)
2392                radius of the cylinder
2393
2394        Example:
2395            ```python
2396            from vedo import Disc, show
2397            disc = Disc(r1=1, r2=1.2)
2398            mesh = disc.extrude(3, res=50).linewidth(1)
2399            mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True)
2400            show(mesh, axes=1).close()
2401            ```
2402            ![](https://vedo.embl.es/images/feats/cut_with_cylinder.png)
2403
2404        Examples:
2405            - [optics_main1.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/optics_main1.py)
2406
2407        Check out also:
2408            `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()`
2409        """
2410        s = str(axis)
2411        if "x" in s:
2412            axis = (1, 0, 0)
2413        elif "y" in s:
2414            axis = (0, 1, 0)
2415        elif "z" in s:
2416            axis = (0, 0, 1)
2417        cyl = vtki.new("Cylinder")
2418        cyl.SetCenter(center)
2419        cyl.SetAxis(axis[0], axis[1], axis[2])
2420        cyl.SetRadius(r)
2421
2422        clipper = vtki.new("ClipPolyData")
2423        clipper.SetInputData(self.dataset)
2424        clipper.SetClipFunction(cyl)
2425        clipper.SetInsideOut(not invert)
2426        clipper.GenerateClippedOutputOff()
2427        clipper.GenerateClipScalarsOff()
2428        clipper.SetValue(0)
2429        clipper.Update()
2430        self._update(clipper.GetOutput())
2431
2432        self.pipeline = utils.OperationNode("cut_with_cylinder", parents=[self])
2433        return self
2434
2435    def cut_with_sphere(self, center=(0, 0, 0), r=1.0, invert=False) -> Self:
2436        """
2437        Cut the current mesh with an sphere.
2438        This is much faster than `cut_with_mesh()`.
2439
2440        Arguments:
2441            center : (array)
2442                the center of the sphere
2443            r : (float)
2444                radius of the sphere
2445
2446        Example:
2447            ```python
2448            from vedo import Disc, show
2449            disc = Disc(r1=1, r2=1.2)
2450            mesh = disc.extrude(3, res=50).linewidth(1)
2451            mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True)
2452            show(mesh, axes=1).close()
2453            ```
2454            ![](https://vedo.embl.es/images/feats/cut_with_sphere.png)
2455
2456        Check out also:
2457            `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()`
2458        """
2459        sph = vtki.new("Sphere")
2460        sph.SetCenter(center)
2461        sph.SetRadius(r)
2462
2463        clipper = vtki.new("ClipPolyData")
2464        clipper.SetInputData(self.dataset)
2465        clipper.SetClipFunction(sph)
2466        clipper.SetInsideOut(not invert)
2467        clipper.GenerateClippedOutputOff()
2468        clipper.GenerateClipScalarsOff()
2469        clipper.SetValue(0)
2470        clipper.Update()
2471        self._update(clipper.GetOutput())
2472        self.pipeline = utils.OperationNode("cut_with_sphere", parents=[self])
2473        return self
2474
2475    def cut_with_mesh(self, mesh, invert=False, keep=False) -> Union[Self, "vedo.Assembly"]:
2476        """
2477        Cut an `Mesh` mesh with another `Mesh`.
2478
2479        Use `invert` to invert the selection.
2480
2481        Use `keep` to keep the cutoff part, in this case an `Assembly` is returned:
2482        the "cut" object and the "discarded" part of the original object.
2483        You can access both via `assembly.unpack()` method.
2484
2485        Example:
2486        ```python
2487        from vedo import *
2488        arr = np.random.randn(100000, 3)/2
2489        pts = Points(arr).c('red3').pos(5,0,0)
2490        cube = Cube().pos(4,0.5,0)
2491        assem = pts.cut_with_mesh(cube, keep=True)
2492        show(assem.unpack(), axes=1).close()
2493        ```
2494        ![](https://vedo.embl.es/images/feats/cut_with_mesh.png)
2495
2496       Check out also:
2497            `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()`
2498       """
2499        polymesh = mesh.dataset
2500        poly = self.dataset
2501
2502        # Create an array to hold distance information
2503        signed_distances = vtki.vtkFloatArray()
2504        signed_distances.SetNumberOfComponents(1)
2505        signed_distances.SetName("SignedDistances")
2506
2507        # implicit function that will be used to slice the mesh
2508        ippd = vtki.new("ImplicitPolyDataDistance")
2509        ippd.SetInput(polymesh)
2510
2511        # Evaluate the signed distance function at all of the grid points
2512        for pointId in range(poly.GetNumberOfPoints()):
2513            p = poly.GetPoint(pointId)
2514            signed_distance = ippd.EvaluateFunction(p)
2515            signed_distances.InsertNextValue(signed_distance)
2516
2517        currentscals = poly.GetPointData().GetScalars()
2518        if currentscals:
2519            currentscals = currentscals.GetName()
2520
2521        poly.GetPointData().AddArray(signed_distances)
2522        poly.GetPointData().SetActiveScalars("SignedDistances")
2523
2524        clipper = vtki.new("ClipPolyData")
2525        clipper.SetInputData(poly)
2526        clipper.SetInsideOut(not invert)
2527        clipper.SetGenerateClippedOutput(keep)
2528        clipper.SetValue(0.0)
2529        clipper.Update()
2530        cpoly = clipper.GetOutput()
2531
2532        if keep:
2533            kpoly = clipper.GetOutput(1)
2534
2535        vis = False
2536        if currentscals:
2537            cpoly.GetPointData().SetActiveScalars(currentscals)
2538            vis = self.mapper.GetScalarVisibility()
2539
2540        self._update(cpoly)
2541
2542        self.pointdata.remove("SignedDistances")
2543        self.mapper.SetScalarVisibility(vis)
2544        if keep:
2545            if isinstance(self, vedo.Mesh):
2546                cutoff = vedo.Mesh(kpoly)
2547            else:
2548                cutoff = vedo.Points(kpoly)
2549            # cutoff = self.__class__(kpoly) # this does not work properly
2550            cutoff.properties = vtki.vtkProperty()
2551            cutoff.properties.DeepCopy(self.properties)
2552            cutoff.actor.SetProperty(cutoff.properties)
2553            cutoff.c("k5").alpha(0.2)
2554            return vedo.Assembly([self, cutoff])
2555
2556        self.pipeline = utils.OperationNode("cut_with_mesh", parents=[self, mesh])
2557        return self
2558
2559    def cut_with_point_loop(
2560        self, points, invert=False, on="points", include_boundary=False
2561    ) -> Self:
2562        """
2563        Cut an `Mesh` object with a set of points forming a closed loop.
2564
2565        Arguments:
2566            invert : (bool)
2567                invert selection (inside-out)
2568            on : (str)
2569                if 'cells' will extract the whole cells lying inside (or outside) the point loop
2570            include_boundary : (bool)
2571                include cells lying exactly on the boundary line. Only relevant on 'cells' mode
2572
2573        Examples:
2574            - [cut_with_points1.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points1.py)
2575
2576                ![](https://vedo.embl.es/images/advanced/cutWithPoints1.png)
2577
2578            - [cut_with_points2.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points2.py)
2579
2580                ![](https://vedo.embl.es/images/advanced/cutWithPoints2.png)
2581        """
2582        if isinstance(points, Points):
2583            parents = [points]
2584            vpts = points.dataset.GetPoints()
2585            points = points.vertices
2586        else:
2587            parents = [self]
2588            vpts = vtki.vtkPoints()
2589            points = utils.make3d(points)
2590            for p in points:
2591                vpts.InsertNextPoint(p)
2592
2593        if "cell" in on:
2594            ippd = vtki.new("ImplicitSelectionLoop")
2595            ippd.SetLoop(vpts)
2596            ippd.AutomaticNormalGenerationOn()
2597            clipper = vtki.new("ExtractPolyDataGeometry")
2598            clipper.SetInputData(self.dataset)
2599            clipper.SetImplicitFunction(ippd)
2600            clipper.SetExtractInside(not invert)
2601            clipper.SetExtractBoundaryCells(include_boundary)
2602        else:
2603            spol = vtki.new("SelectPolyData")
2604            spol.SetLoop(vpts)
2605            spol.GenerateSelectionScalarsOn()
2606            spol.GenerateUnselectedOutputOff()
2607            spol.SetInputData(self.dataset)
2608            spol.Update()
2609            clipper = vtki.new("ClipPolyData")
2610            clipper.SetInputData(spol.GetOutput())
2611            clipper.SetInsideOut(not invert)
2612            clipper.SetValue(0.0)
2613        clipper.Update()
2614        self._update(clipper.GetOutput())
2615
2616        self.pipeline = utils.OperationNode("cut_with_pointloop", parents=parents)
2617        return self
2618
2619    def cut_with_scalar(self, value: float, name="", invert=False) -> Self:
2620        """
2621        Cut a mesh or point cloud with some input scalar point-data.
2622
2623        Arguments:
2624            value : (float)
2625                cutting value
2626            name : (str)
2627                array name of the scalars to be used
2628            invert : (bool)
2629                flip selection
2630
2631        Example:
2632            ```python
2633            from vedo import *
2634            s = Sphere().lw(1)
2635            pts = s.vertices
2636            scalars = np.sin(3*pts[:,2]) + pts[:,0]
2637            s.pointdata["somevalues"] = scalars
2638            s.cut_with_scalar(0.3)
2639            s.cmap("Spectral", "somevalues").add_scalarbar()
2640            s.show(axes=1).close()
2641            ```
2642            ![](https://vedo.embl.es/images/feats/cut_with_scalars.png)
2643        """
2644        if name:
2645            self.pointdata.select(name)
2646        clipper = vtki.new("ClipPolyData")
2647        clipper.SetInputData(self.dataset)
2648        clipper.SetValue(value)
2649        clipper.GenerateClippedOutputOff()
2650        clipper.SetInsideOut(not invert)
2651        clipper.Update()
2652        self._update(clipper.GetOutput())
2653        self.pipeline = utils.OperationNode("cut_with_scalar", parents=[self])
2654        return self
2655
2656    def crop(self,
2657             top=None, bottom=None, right=None, left=None, front=None, back=None,
2658             bounds=()) -> Self:
2659        """
2660        Crop an `Mesh` object.
2661
2662        Arguments:
2663            top : (float)
2664                fraction to crop from the top plane (positive z)
2665            bottom : (float)
2666                fraction to crop from the bottom plane (negative z)
2667            front : (float)
2668                fraction to crop from the front plane (positive y)
2669            back : (float)
2670                fraction to crop from the back plane (negative y)
2671            right : (float)
2672                fraction to crop from the right plane (positive x)
2673            left : (float)
2674                fraction to crop from the left plane (negative x)
2675            bounds : (list)
2676                bounding box of the crop region as `[x0,x1, y0,y1, z0,z1]`
2677
2678        Example:
2679            ```python
2680            from vedo import Sphere
2681            Sphere().crop(right=0.3, left=0.1).show()
2682            ```
2683            ![](https://user-images.githubusercontent.com/32848391/57081955-0ef1e800-6cf6-11e9-99de-b45220939bc9.png)
2684        """
2685        if not len(bounds):
2686            pos = np.array(self.pos())
2687            x0, x1, y0, y1, z0, z1 = self.bounds()
2688            x0, y0, z0 = [x0, y0, z0] - pos
2689            x1, y1, z1 = [x1, y1, z1] - pos
2690
2691            dx, dy, dz = x1 - x0, y1 - y0, z1 - z0
2692            if top:
2693                z1 = z1 - top * dz
2694            if bottom:
2695                z0 = z0 + bottom * dz
2696            if front:
2697                y1 = y1 - front * dy
2698            if back:
2699                y0 = y0 + back * dy
2700            if right:
2701                x1 = x1 - right * dx
2702            if left:
2703                x0 = x0 + left * dx
2704            bounds = (x0, x1, y0, y1, z0, z1)
2705
2706        cu = vtki.new("Box")
2707        cu.SetBounds(bounds)
2708
2709        clipper = vtki.new("ClipPolyData")
2710        clipper.SetInputData(self.dataset)
2711        clipper.SetClipFunction(cu)
2712        clipper.InsideOutOn()
2713        clipper.GenerateClippedOutputOff()
2714        clipper.GenerateClipScalarsOff()
2715        clipper.SetValue(0)
2716        clipper.Update()
2717        self._update(clipper.GetOutput())
2718
2719        self.pipeline = utils.OperationNode(
2720            "crop", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
2721        )
2722        return self
2723
2724    def generate_surface_halo(
2725            self, 
2726            distance=0.05,
2727            res=(50, 50, 50),
2728            bounds=(),
2729            maxdist=None,
2730    ) -> "vedo.Mesh":
2731        """
2732        Generate the surface halo which sits at the specified distance from the input one.
2733
2734        Arguments:
2735            distance : (float)
2736                distance from the input surface
2737            res : (int)
2738                resolution of the surface
2739            bounds : (list)
2740                bounding box of the surface
2741            maxdist : (float)
2742                maximum distance to generate the surface
2743        """
2744        if not bounds:
2745            bounds = self.bounds()
2746
2747        if not maxdist:
2748            maxdist = self.diagonal_size() / 2
2749
2750        imp = vtki.new("ImplicitModeller")
2751        imp.SetInputData(self.dataset)
2752        imp.SetSampleDimensions(res)
2753        if maxdist:
2754            imp.SetMaximumDistance(maxdist)
2755        if len(bounds) == 6:
2756            imp.SetModelBounds(bounds)
2757        contour = vtki.new("ContourFilter")
2758        contour.SetInputConnection(imp.GetOutputPort())
2759        contour.SetValue(0, distance)
2760        contour.Update()
2761        out = vedo.Mesh(contour.GetOutput())
2762        out.c("lightblue").alpha(0.25).lighting("off")
2763        out.pipeline = utils.OperationNode("generate_surface_halo", parents=[self])
2764        return out
2765
2766    def generate_mesh(
2767        self,
2768        line_resolution=None,
2769        mesh_resolution=None,
2770        smooth=0.0,
2771        jitter=0.001,
2772        grid=None,
2773        quads=False,
2774        invert=False,
2775    ) -> Self:
2776        """
2777        Generate a polygonal Mesh from a closed contour line.
2778        If line is not closed it will be closed with a straight segment.
2779
2780        Check also `generate_delaunay2d()`.
2781
2782        Arguments:
2783            line_resolution : (int)
2784                resolution of the contour line. The default is None, in this case
2785                the contour is not resampled.
2786            mesh_resolution : (int)
2787                resolution of the internal triangles not touching the boundary.
2788            smooth : (float)
2789                smoothing of the contour before meshing.
2790            jitter : (float)
2791                add a small noise to the internal points.
2792            grid : (Grid)
2793                manually pass a Grid object. The default is True.
2794            quads : (bool)
2795                generate a mesh of quads instead of triangles.
2796            invert : (bool)
2797                flip the line orientation. The default is False.
2798
2799        Examples:
2800            - [line2mesh_tri.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_tri.py)
2801
2802                ![](https://vedo.embl.es/images/advanced/line2mesh_tri.jpg)
2803
2804            - [line2mesh_quads.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_quads.py)
2805
2806                ![](https://vedo.embl.es/images/advanced/line2mesh_quads.png)
2807        """
2808        if line_resolution is None:
2809            contour = vedo.shapes.Line(self.vertices)
2810        else:
2811            contour = vedo.shapes.Spline(self.vertices, smooth=smooth, res=line_resolution)
2812        contour.clean()
2813
2814        length = contour.length()
2815        density = length / contour.npoints
2816        # print(f"tomesh():\n\tline length = {length}")
2817        # print(f"\tdensity = {density} length/pt_separation")
2818
2819        x0, x1 = contour.xbounds()
2820        y0, y1 = contour.ybounds()
2821
2822        if grid is None:
2823            if mesh_resolution is None:
2824                resx = int((x1 - x0) / density + 0.5)
2825                resy = int((y1 - y0) / density + 0.5)
2826                # print(f"tmesh_resolution = {[resx, resy]}")
2827            else:
2828                if utils.is_sequence(mesh_resolution):
2829                    resx, resy = mesh_resolution
2830                else:
2831                    resx, resy = mesh_resolution, mesh_resolution
2832            grid = vedo.shapes.Grid(
2833                [(x0 + x1) / 2, (y0 + y1) / 2, 0],
2834                s=((x1 - x0) * 1.025, (y1 - y0) * 1.025),
2835                res=(resx, resy),
2836            )
2837        else:
2838            grid = grid.clone()
2839
2840        cpts = contour.vertices
2841
2842        # make sure it's closed
2843        p0, p1 = cpts[0], cpts[-1]
2844        nj = max(2, int(utils.mag(p1 - p0) / density + 0.5))
2845        joinline = vedo.shapes.Line(p1, p0, res=nj)
2846        contour = vedo.merge(contour, joinline).subsample(0.0001)
2847
2848        ####################################### quads
2849        if quads:
2850            cmesh = grid.clone().cut_with_point_loop(contour, on="cells", invert=invert)
2851            cmesh.wireframe(False).lw(0.5)
2852            cmesh.pipeline = utils.OperationNode(
2853                "generate_mesh",
2854                parents=[self, contour],
2855                comment=f"#quads {cmesh.dataset.GetNumberOfCells()}",
2856            )
2857            return cmesh
2858        #############################################
2859
2860        grid_tmp = grid.vertices.copy()
2861
2862        if jitter:
2863            np.random.seed(0)
2864            sigma = 1.0 / np.sqrt(grid.npoints) * grid.diagonal_size() * jitter
2865            # print(f"\tsigma jittering = {sigma}")
2866            grid_tmp += np.random.rand(grid.npoints, 3) * sigma
2867            grid_tmp[:, 2] = 0.0
2868
2869        todel = []
2870        density /= np.sqrt(3)
2871        vgrid_tmp = Points(grid_tmp)
2872
2873        for p in contour.vertices:
2874            out = vgrid_tmp.closest_point(p, radius=density, return_point_id=True)
2875            todel += out.tolist()
2876
2877        grid_tmp = grid_tmp.tolist()
2878        for index in sorted(list(set(todel)), reverse=True):
2879            del grid_tmp[index]
2880
2881        points = contour.vertices.tolist() + grid_tmp
2882        if invert:
2883            boundary = list(reversed(range(contour.npoints)))
2884        else:
2885            boundary = list(range(contour.npoints))
2886
2887        dln = Points(points).generate_delaunay2d(mode="xy", boundaries=[boundary])
2888        dln.compute_normals(points=False)  # fixes reversd faces
2889        dln.lw(1)
2890
2891        dln.pipeline = utils.OperationNode(
2892            "generate_mesh",
2893            parents=[self, contour],
2894            comment=f"#cells {dln.dataset.GetNumberOfCells()}",
2895        )
2896        return dln
2897
2898    def reconstruct_surface(
2899        self,
2900        dims=(100, 100, 100),
2901        radius=None,
2902        sample_size=None,
2903        hole_filling=True,
2904        bounds=(),
2905        padding=0.05,
2906    ) -> "vedo.Mesh":
2907        """
2908        Surface reconstruction from a scattered cloud of points.
2909
2910        Arguments:
2911            dims : (int)
2912                number of voxels in x, y and z to control precision.
2913            radius : (float)
2914                radius of influence of each point.
2915                Smaller values generally improve performance markedly.
2916                Note that after the signed distance function is computed,
2917                any voxel taking on the value >= radius
2918                is presumed to be "unseen" or uninitialized.
2919            sample_size : (int)
2920                if normals are not present
2921                they will be calculated using this sample size per point.
2922            hole_filling : (bool)
2923                enables hole filling, this generates
2924                separating surfaces between the empty and unseen portions of the volume.
2925            bounds : (list)
2926                region in space in which to perform the sampling
2927                in format (xmin,xmax, ymin,ymax, zim, zmax)
2928            padding : (float)
2929                increase by this fraction the bounding box
2930
2931        Examples:
2932            - [recosurface.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/recosurface.py)
2933
2934                ![](https://vedo.embl.es/images/advanced/recosurface.png)
2935        """
2936        if not utils.is_sequence(dims):
2937            dims = (dims, dims, dims)
2938
2939        sdf = vtki.new("SignedDistance")
2940
2941        if len(bounds) == 6:
2942            sdf.SetBounds(bounds)
2943        else:
2944            x0, x1, y0, y1, z0, z1 = self.bounds()
2945            sdf.SetBounds(
2946                x0 - (x1 - x0) * padding,
2947                x1 + (x1 - x0) * padding,
2948                y0 - (y1 - y0) * padding,
2949                y1 + (y1 - y0) * padding,
2950                z0 - (z1 - z0) * padding,
2951                z1 + (z1 - z0) * padding,
2952            )
2953        
2954        bb = sdf.GetBounds()
2955        if bb[0]==bb[1]:
2956            vedo.logger.warning("reconstruct_surface(): zero x-range")
2957        if bb[2]==bb[3]:
2958            vedo.logger.warning("reconstruct_surface(): zero y-range")
2959        if bb[4]==bb[5]:
2960            vedo.logger.warning("reconstruct_surface(): zero z-range")
2961
2962        pd = self.dataset
2963
2964        if pd.GetPointData().GetNormals():
2965            sdf.SetInputData(pd)
2966        else:
2967            normals = vtki.new("PCANormalEstimation")
2968            normals.SetInputData(pd)
2969            if not sample_size:
2970                sample_size = int(pd.GetNumberOfPoints() / 50)
2971            normals.SetSampleSize(sample_size)
2972            normals.SetNormalOrientationToGraphTraversal()
2973            sdf.SetInputConnection(normals.GetOutputPort())
2974            # print("Recalculating normals with sample size =", sample_size)
2975
2976        if radius is None:
2977            radius = self.diagonal_size() / (sum(dims) / 3) * 5
2978            # print("Calculating mesh from points with radius =", radius)
2979
2980        sdf.SetRadius(radius)
2981        sdf.SetDimensions(dims)
2982        sdf.Update()
2983
2984        surface = vtki.new("ExtractSurface")
2985        surface.SetRadius(radius * 0.99)
2986        surface.SetHoleFilling(hole_filling)
2987        surface.ComputeNormalsOff()
2988        surface.ComputeGradientsOff()
2989        surface.SetInputConnection(sdf.GetOutputPort())
2990        surface.Update()
2991        m = vedo.mesh.Mesh(surface.GetOutput(), c=self.color())
2992
2993        m.pipeline = utils.OperationNode(
2994            "reconstruct_surface",
2995            parents=[self],
2996            comment=f"#pts {m.dataset.GetNumberOfPoints()}",
2997        )
2998        return m
2999
3000    def compute_clustering(self, radius: float) -> Self:
3001        """
3002        Cluster points in space. The `radius` is the radius of local search.
3003        
3004        An array named "ClusterId" is added to `pointdata`.
3005
3006        Examples:
3007            - [clustering.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/clustering.py)
3008
3009                ![](https://vedo.embl.es/images/basic/clustering.png)
3010        """
3011        cluster = vtki.new("EuclideanClusterExtraction")
3012        cluster.SetInputData(self.dataset)
3013        cluster.SetExtractionModeToAllClusters()
3014        cluster.SetRadius(radius)
3015        cluster.ColorClustersOn()
3016        cluster.Update()
3017        idsarr = cluster.GetOutput().GetPointData().GetArray("ClusterId")
3018        self.dataset.GetPointData().AddArray(idsarr)
3019        self.pipeline = utils.OperationNode(
3020            "compute_clustering", parents=[self], comment=f"radius = {radius}"
3021        )
3022        return self
3023
3024    def compute_connections(self, radius, mode=0, regions=(), vrange=(0, 1), seeds=(), angle=0.0) -> Self:
3025        """
3026        Extracts and/or segments points from a point cloud based on geometric distance measures
3027        (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range.
3028        The default operation is to segment the points into "connected" regions where the connection
3029        is determined by an appropriate distance measure. Each region is given a region id.
3030
3031        Optionally, the filter can output the largest connected region of points; a particular region
3032        (via id specification); those regions that are seeded using a list of input point ids;
3033        or the region of points closest to a specified position.
3034
3035        The key parameter of this filter is the radius defining a sphere around each point which defines
3036        a local neighborhood: any other points in the local neighborhood are assumed connected to the point.
3037        Note that the radius is defined in absolute terms.
3038
3039        Other parameters are used to further qualify what it means to be a neighboring point.
3040        For example, scalar range and/or point normals can be used to further constrain the neighborhood.
3041        Also the extraction mode defines how the filter operates.
3042        By default, all regions are extracted but it is possible to extract particular regions;
3043        the region closest to a seed point; seeded regions; or the largest region found while processing.
3044        By default, all regions are extracted.
3045
3046        On output, all points are labeled with a region number.
3047        However note that the number of input and output points may not be the same:
3048        if not extracting all regions then the output size may be less than the input size.
3049
3050        Arguments:
3051            radius : (float)
3052                variable specifying a local sphere used to define local point neighborhood
3053            mode : (int)
3054                - 0,  Extract all regions
3055                - 1,  Extract point seeded regions
3056                - 2,  Extract largest region
3057                - 3,  Test specified regions
3058                - 4,  Extract all regions with scalar connectivity
3059                - 5,  Extract point seeded regions
3060            regions : (list)
3061                a list of non-negative regions id to extract
3062            vrange : (list)
3063                scalar range to use to extract points based on scalar connectivity
3064            seeds : (list)
3065                a list of non-negative point seed ids
3066            angle : (list)
3067                points are connected if the angle between their normals is
3068                within this angle threshold (expressed in degrees).
3069        """
3070        # https://vtk.org/doc/nightly/html/classvtkConnectedPointsFilter.html
3071        cpf = vtki.new("ConnectedPointsFilter")
3072        cpf.SetInputData(self.dataset)
3073        cpf.SetRadius(radius)
3074        if mode == 0:  # Extract all regions
3075            pass
3076
3077        elif mode == 1:  # Extract point seeded regions
3078            cpf.SetExtractionModeToPointSeededRegions()
3079            for s in seeds:
3080                cpf.AddSeed(s)
3081
3082        elif mode == 2:  # Test largest region
3083            cpf.SetExtractionModeToLargestRegion()
3084
3085        elif mode == 3:  # Test specified regions
3086            cpf.SetExtractionModeToSpecifiedRegions()
3087            for r in regions:
3088                cpf.AddSpecifiedRegion(r)
3089
3090        elif mode == 4:  # Extract all regions with scalar connectivity
3091            cpf.SetExtractionModeToLargestRegion()
3092            cpf.ScalarConnectivityOn()
3093            cpf.SetScalarRange(vrange[0], vrange[1])
3094
3095        elif mode == 5:  # Extract point seeded regions
3096            cpf.SetExtractionModeToLargestRegion()
3097            cpf.ScalarConnectivityOn()
3098            cpf.SetScalarRange(vrange[0], vrange[1])
3099            cpf.AlignedNormalsOn()
3100            cpf.SetNormalAngle(angle)
3101
3102        cpf.Update()
3103        self._update(cpf.GetOutput(), reset_locators=False)
3104        return self
3105
3106    def compute_camera_distance(self) -> np.ndarray:
3107        """
3108        Calculate the distance from points to the camera.
3109        
3110        A pointdata array is created with name 'DistanceToCamera' and returned.
3111        """
3112        if vedo.plotter_instance and vedo.plotter_instance.renderer:
3113            poly = self.dataset
3114            dc = vtki.new("DistanceToCamera")
3115            dc.SetInputData(poly)
3116            dc.SetRenderer(vedo.plotter_instance.renderer)
3117            dc.Update()
3118            self._update(dc.GetOutput(), reset_locators=False)
3119            return self.pointdata["DistanceToCamera"]
3120        return np.array([])
3121
3122    def densify(self, target_distance=0.1, nclosest=6, radius=None, niter=1, nmax=None) -> Self:
3123        """
3124        Return a copy of the cloud with new added points.
3125        The new points are created in such a way that all points in any local neighborhood are
3126        within a target distance of one another.
3127
3128        For each input point, the distance to all points in its neighborhood is computed.
3129        If any of its neighbors is further than the target distance,
3130        the edge connecting the point and its neighbor is bisected and
3131        a new point is inserted at the bisection point.
3132        A single pass is completed once all the input points are visited.
3133        Then the process repeats to the number of iterations.
3134
3135        Examples:
3136            - [densifycloud.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/densifycloud.py)
3137
3138                ![](https://vedo.embl.es/images/volumetric/densifycloud.png)
3139
3140        .. note::
3141            Points will be created in an iterative fashion until all points in their
3142            local neighborhood are the target distance apart or less.
3143            Note that the process may terminate early due to the
3144            number of iterations. By default the target distance is set to 0.5.
3145            Note that the target_distance should be less than the radius
3146            or nothing will change on output.
3147
3148        .. warning::
3149            This class can generate a lot of points very quickly.
3150            The maximum number of iterations is by default set to =1.0 for this reason.
3151            Increase the number of iterations very carefully.
3152            Also, `nmax` can be set to limit the explosion of points.
3153            It is also recommended that a N closest neighborhood is used.
3154
3155        """
3156        src = vtki.new("ProgrammableSource")
3157        opts = self.vertices
3158        # zeros = np.zeros(3)
3159
3160        def _read_points():
3161            output = src.GetPolyDataOutput()
3162            points = vtki.vtkPoints()
3163            for p in opts:
3164                # print(p)
3165                # if not np.array_equal(p, zeros):
3166                points.InsertNextPoint(p)
3167            output.SetPoints(points)
3168
3169        src.SetExecuteMethod(_read_points)
3170
3171        dens = vtki.new("DensifyPointCloudFilter")
3172        dens.SetInputConnection(src.GetOutputPort())
3173        # dens.SetInputData(self.dataset) # this does not work
3174        dens.InterpolateAttributeDataOn()
3175        dens.SetTargetDistance(target_distance)
3176        dens.SetMaximumNumberOfIterations(niter)
3177        if nmax:
3178            dens.SetMaximumNumberOfPoints(nmax)
3179
3180        if radius:
3181            dens.SetNeighborhoodTypeToRadius()
3182            dens.SetRadius(radius)
3183        elif nclosest:
3184            dens.SetNeighborhoodTypeToNClosest()
3185            dens.SetNumberOfClosestPoints(nclosest)
3186        else:
3187            vedo.logger.error("set either radius or nclosest")
3188            raise RuntimeError()
3189        dens.Update()
3190
3191        cld = Points(dens.GetOutput())
3192        cld.copy_properties_from(self)
3193        cld.interpolate_data_from(self, n=nclosest, radius=radius)
3194        cld.name = "DensifiedCloud"
3195        cld.pipeline = utils.OperationNode(
3196            "densify",
3197            parents=[self],
3198            c="#e9c46a:",
3199            comment=f"#pts {cld.dataset.GetNumberOfPoints()}",
3200        )
3201        return cld
3202
3203    ###############################################################################
3204    ## stuff returning a Volume
3205    ###############################################################################
3206
3207    def density(
3208        self, dims=(40, 40, 40), bounds=None, radius=None, compute_gradient=False, locator=None
3209    ) -> "vedo.Volume":
3210        """
3211        Generate a density field from a point cloud. Input can also be a set of 3D coordinates.
3212        Output is a `Volume`.
3213
3214        The local neighborhood is specified as the `radius` around each sample position (each voxel).
3215        If left to None, the radius is automatically computed as the diagonal of the bounding box
3216        and can be accessed via `vol.metadata["radius"]`.
3217        The density is expressed as the number of counts in the radius search.
3218
3219        Arguments:
3220            dims : (int, list)
3221                number of voxels in x, y and z of the output Volume.
3222            compute_gradient : (bool)
3223                Turn on/off the generation of the gradient vector,
3224                gradient magnitude scalar, and function classification scalar.
3225                By default this is off. Note that this will increase execution time
3226                and the size of the output. (The names of these point data arrays are:
3227                "Gradient", "Gradient Magnitude", and "Classification")
3228            locator : (vtkPointLocator)
3229                can be assigned from a previous call for speed (access it via `object.point_locator`).
3230
3231        Examples:
3232            - [plot_density3d.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/plot_density3d.py)
3233
3234                ![](https://vedo.embl.es/images/pyplot/plot_density3d.png)
3235        """
3236        pdf = vtki.new("PointDensityFilter")
3237        pdf.SetInputData(self.dataset)
3238
3239        if not utils.is_sequence(dims):
3240            dims = [dims, dims, dims]
3241
3242        if bounds is None:
3243            bounds = list(self.bounds())
3244        elif len(bounds) == 4:
3245            bounds = [*bounds, 0, 0]
3246
3247        if bounds[5] - bounds[4] == 0 or len(dims) == 2:  # its 2D
3248            dims = list(dims)
3249            dims = [dims[0], dims[1], 2]
3250            diag = self.diagonal_size()
3251            bounds[5] = bounds[4] + diag / 1000
3252        pdf.SetModelBounds(bounds)
3253
3254        pdf.SetSampleDimensions(dims)
3255
3256        if locator:
3257            pdf.SetLocator(locator)
3258
3259        pdf.SetDensityEstimateToFixedRadius()
3260        if radius is None:
3261            radius = self.diagonal_size() / 20
3262        pdf.SetRadius(radius)
3263        pdf.SetComputeGradient(compute_gradient)
3264        pdf.Update()
3265
3266        vol = vedo.Volume(pdf.GetOutput()).mode(1)
3267        vol.name = "PointDensity"
3268        vol.metadata["radius"] = radius
3269        vol.locator = pdf.GetLocator()
3270        vol.pipeline = utils.OperationNode(
3271            "density", parents=[self], comment=f"dims={tuple(vol.dimensions())}"
3272        )
3273        return vol
3274
3275
3276    def tovolume(
3277        self,
3278        kernel="shepard",
3279        radius=None,
3280        n=None,
3281        bounds=None,
3282        null_value=None,
3283        dims=(25, 25, 25),
3284    ) -> "vedo.Volume":
3285        """
3286        Generate a `Volume` by interpolating a scalar
3287        or vector field which is only known on a scattered set of points or mesh.
3288        Available interpolation kernels are: shepard, gaussian, or linear.
3289
3290        Arguments:
3291            kernel : (str)
3292                interpolation kernel type [shepard]
3293            radius : (float)
3294                radius of the local search
3295            n : (int)
3296                number of point to use for interpolation
3297            bounds : (list)
3298                bounding box of the output Volume object
3299            dims : (list)
3300                dimensions of the output Volume object
3301            null_value : (float)
3302                value to be assigned to invalid points
3303
3304        Examples:
3305            - [interpolate_volume.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/interpolate_volume.py)
3306
3307                ![](https://vedo.embl.es/images/volumetric/59095175-1ec5a300-8918-11e9-8bc0-fd35c8981e2b.jpg)
3308        """
3309        if radius is None and not n:
3310            vedo.logger.error("please set either radius or n")
3311            raise RuntimeError
3312
3313        poly = self.dataset
3314
3315        # Create a probe volume
3316        probe = vtki.vtkImageData()
3317        probe.SetDimensions(dims)
3318        if bounds is None:
3319            bounds = self.bounds()
3320        probe.SetOrigin(bounds[0], bounds[2], bounds[4])
3321        probe.SetSpacing(
3322            (bounds[1] - bounds[0]) / dims[0],
3323            (bounds[3] - bounds[2]) / dims[1],
3324            (bounds[5] - bounds[4]) / dims[2],
3325        )
3326
3327        if not self.point_locator:
3328            self.point_locator = vtki.new("PointLocator")
3329            self.point_locator.SetDataSet(poly)
3330            self.point_locator.BuildLocator()
3331
3332        if kernel == "shepard":
3333            kern = vtki.new("ShepardKernel")
3334            kern.SetPowerParameter(2)
3335        elif kernel == "gaussian":
3336            kern = vtki.new("GaussianKernel")
3337        elif kernel == "linear":
3338            kern = vtki.new("LinearKernel")
3339        else:
3340            vedo.logger.error("Error in tovolume(), available kernels are:")
3341            vedo.logger.error(" [shepard, gaussian, linear]")
3342            raise RuntimeError()
3343
3344        if radius:
3345            kern.SetRadius(radius)
3346
3347        interpolator = vtki.new("PointInterpolator")
3348        interpolator.SetInputData(probe)
3349        interpolator.SetSourceData(poly)
3350        interpolator.SetKernel(kern)
3351        interpolator.SetLocator(self.point_locator)
3352
3353        if n:
3354            kern.SetNumberOfPoints(n)
3355            kern.SetKernelFootprintToNClosest()
3356        else:
3357            kern.SetRadius(radius)
3358
3359        if null_value is not None:
3360            interpolator.SetNullValue(null_value)
3361        else:
3362            interpolator.SetNullPointsStrategyToClosestPoint()
3363        interpolator.Update()
3364
3365        vol = vedo.Volume(interpolator.GetOutput())
3366
3367        vol.pipeline = utils.OperationNode(
3368            "signed_distance",
3369            parents=[self],
3370            comment=f"dims={tuple(vol.dimensions())}",
3371            c="#e9c46a:#0096c7",
3372        )
3373        return vol
3374
3375    #################################################################################    
3376    def generate_segments(self, istart=0, rmax=1e30, niter=3) -> "vedo.shapes.Lines":
3377        """
3378        Generate a line segments from a set of points.
3379        The algorithm is based on the closest point search.
3380
3381        Returns a `Line` object.
3382        This object contains the a metadata array of used vertex counts in "UsedVertexCount"
3383        and the sum of the length of the segments in "SegmentsLengthSum".
3384
3385        Arguments:
3386            istart : (int)
3387                index of the starting point
3388            rmax : (float)
3389                maximum length of a segment
3390            niter : (int)
3391                number of iterations or passes through the points
3392
3393        Examples:
3394            - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py)
3395        """
3396        points = self.vertices
3397        segments = []
3398        dists = []
3399        n = len(points)
3400        used = np.zeros(n, dtype=int)
3401        for _ in range(niter):
3402            i = istart
3403            for _ in range(n):
3404                p = points[i]
3405                ids = self.closest_point(p, n=4, return_point_id=True)
3406                j = ids[1]
3407                if used[j] > 1 or [j, i] in segments:
3408                    j = ids[2]
3409                if used[j] > 1:
3410                    j = ids[3]
3411                d = np.linalg.norm(p - points[j])
3412                if used[j] > 1 or used[i] > 1 or d > rmax:
3413                    i += 1
3414                    if i >= n:
3415                        i = 0
3416                    continue
3417                used[i] += 1
3418                used[j] += 1
3419                segments.append([i, j])
3420                dists.append(d)
3421                i = j
3422        segments = np.array(segments, dtype=int)
3423
3424        lines = vedo.shapes.Lines(points[segments], c="k", lw=3)
3425        lines.metadata["UsedVertexCount"] = used
3426        lines.metadata["SegmentsLengthSum"] = np.sum(dists)
3427        lines.pipeline = utils.OperationNode("generate_segments", parents=[self])
3428        lines.name = "Segments"
3429        return lines
3430
3431    def generate_delaunay2d(
3432        self,
3433        mode="scipy",
3434        boundaries=(),
3435        tol=None,
3436        alpha=0.0,
3437        offset=0.0,
3438        transform=None,
3439    ) -> "vedo.mesh.Mesh":
3440        """
3441        Create a mesh from points in the XY plane.
3442        If `mode='fit'` then the filter computes a best fitting
3443        plane and projects the points onto it.
3444
3445        Check also `generate_mesh()`.
3446
3447        Arguments:
3448            tol : (float)
3449                specify a tolerance to control discarding of closely spaced points.
3450                This tolerance is specified as a fraction of the diagonal length of the bounding box of the points.
3451            alpha : (float)
3452                for a non-zero alpha value, only edges or triangles contained
3453                within a sphere centered at mesh vertices will be output.
3454                Otherwise, only triangles will be output.
3455            offset : (float)
3456                multiplier to control the size of the initial, bounding Delaunay triangulation.
3457            transform: (LinearTransform, NonLinearTransform)
3458                a transformation which is applied to points to generate a 2D problem.
3459                This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane.
3460                The points are transformed and triangulated.
3461                The topology of triangulated points is used as the output topology.
3462
3463        Examples:
3464            - [delaunay2d.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delaunay2d.py)
3465
3466                ![](https://vedo.embl.es/images/basic/delaunay2d.png)
3467        """
3468        plist = self.vertices.copy()
3469
3470        #########################################################
3471        if mode == "scipy":
3472            from scipy.spatial import Delaunay as scipy_delaunay
3473
3474            tri = scipy_delaunay(plist[:, 0:2])
3475            return vedo.mesh.Mesh([plist, tri.simplices])
3476        ##########################################################
3477
3478        pd = vtki.vtkPolyData()
3479        vpts = vtki.vtkPoints()
3480        vpts.SetData(utils.numpy2vtk(plist, dtype=np.float32))
3481        pd.SetPoints(vpts)
3482
3483        delny = vtki.new("Delaunay2D")
3484        delny.SetInputData(pd)
3485        if tol:
3486            delny.SetTolerance(tol)
3487        delny.SetAlpha(alpha)
3488        delny.SetOffset(offset)
3489
3490        if transform:
3491            delny.SetTransform(transform.T)
3492        elif mode == "fit":
3493            delny.SetProjectionPlaneMode(vtki.get_class("VTK_BEST_FITTING_PLANE"))
3494        elif mode == "xy" and boundaries:
3495            boundary = vtki.vtkPolyData()
3496            boundary.SetPoints(vpts)
3497            cell_array = vtki.vtkCellArray()
3498            for b in boundaries:
3499                cpolygon = vtki.vtkPolygon()
3500                for idd in b:
3501                    cpolygon.GetPointIds().InsertNextId(idd)
3502                cell_array.InsertNextCell(cpolygon)
3503            boundary.SetPolys(cell_array)
3504            delny.SetSourceData(boundary)
3505
3506        delny.Update()
3507
3508        msh = vedo.mesh.Mesh(delny.GetOutput())
3509        msh.name = "Delaunay2D"
3510        msh.clean().lighting("off")
3511        msh.pipeline = utils.OperationNode(
3512            "delaunay2d",
3513            parents=[self],
3514            comment=f"#cells {msh.dataset.GetNumberOfCells()}",
3515        )
3516        return msh
3517
3518    def generate_voronoi(self, padding=0.0, fit=False, method="vtk") -> "vedo.Mesh":
3519        """
3520        Generate the 2D Voronoi convex tiling of the input points (z is ignored).
3521        The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon.
3522
3523        A cell array named "VoronoiID" is added to the output Mesh.
3524
3525        The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest
3526        to one of the input points. Voronoi tessellations are important in computational geometry
3527        (and many other fields), and are the dual of Delaunay triangulations.
3528
3529        Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored
3530        (although carried through to the output).
3531        If you desire to triangulate in a different plane, you can use fit=True.
3532
3533        A brief summary is as follows. Each (generating) input point is associated with
3534        an initial Voronoi tile, which is simply the bounding box of the point set.
3535        A locator is then used to identify nearby points: each neighbor in turn generates a
3536        clipping line positioned halfway between the generating point and the neighboring point,
3537        and orthogonal to the line connecting them. Clips are readily performed by evaluationg the
3538        vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line.
3539        If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip
3540        line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs,
3541        the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region
3542        containing the neighboring clip points. The clip region (along with the points contained in it) is grown
3543        by careful expansion (e.g., outward spiraling iterator over all candidate clip points).
3544        When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi
3545        tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi
3546        tessellation. Note that topological and geometric information is used to generate a valid triangulation
3547        (e.g., merging points and validating topology).
3548
3549        Arguments:
3550            pts : (list)
3551                list of input points.
3552            padding : (float)
3553                padding distance. The default is 0.
3554            fit : (bool)
3555                detect automatically the best fitting plane. The default is False.
3556
3557        Examples:
3558            - [voronoi1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi1.py)
3559
3560                ![](https://vedo.embl.es/images/basic/voronoi1.png)
3561
3562            - [voronoi2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi2.py)
3563
3564                ![](https://vedo.embl.es/images/advanced/voronoi2.png)
3565        """
3566        pts = self.vertices
3567
3568        if method == "scipy":
3569            from scipy.spatial import Voronoi as scipy_voronoi
3570
3571            pts = np.asarray(pts)[:, (0, 1)]
3572            vor = scipy_voronoi(pts)
3573            regs = []  # filter out invalid indices
3574            for r in vor.regions:
3575                flag = True
3576                for x in r:
3577                    if x < 0:
3578                        flag = False
3579                        break
3580                if flag and len(r) > 0:
3581                    regs.append(r)
3582
3583            m = vedo.Mesh([vor.vertices, regs])
3584            m.celldata["VoronoiID"] = np.array(list(range(len(regs)))).astype(int)
3585
3586        elif method == "vtk":
3587            vor = vtki.new("Voronoi2D")
3588            if isinstance(pts, Points):
3589                vor.SetInputData(pts)
3590            else:
3591                pts = np.asarray(pts)
3592                if pts.shape[1] == 2:
3593                    pts = np.c_[pts, np.zeros(len(pts))]
3594                pd = vtki.vtkPolyData()
3595                vpts = vtki.vtkPoints()
3596                vpts.SetData(utils.numpy2vtk(pts, dtype=np.float32))
3597                pd.SetPoints(vpts)
3598                vor.SetInputData(pd)
3599            vor.SetPadding(padding)
3600            vor.SetGenerateScalarsToPointIds()
3601            if fit:
3602                vor.SetProjectionPlaneModeToBestFittingPlane()
3603            else:
3604                vor.SetProjectionPlaneModeToXYPlane()
3605            vor.Update()
3606            poly = vor.GetOutput()
3607            arr = poly.GetCellData().GetArray(0)
3608            if arr:
3609                arr.SetName("VoronoiID")
3610            m = vedo.Mesh(poly, c="orange5")
3611
3612        else:
3613            vedo.logger.error(f"Unknown method {method} in voronoi()")
3614            raise RuntimeError
3615
3616        m.lw(2).lighting("off").wireframe()
3617        m.name = "Voronoi"
3618        return m
3619
3620    ##########################################################################
3621    def generate_delaunay3d(self, radius=0, tol=None) -> "vedo.TetMesh":
3622        """
3623        Create 3D Delaunay triangulation of input points.
3624
3625        Arguments:
3626            radius : (float)
3627                specify distance (or "alpha") value to control output.
3628                For a non-zero values, only tetra contained within the circumsphere
3629                will be output.
3630            tol : (float)
3631                Specify a tolerance to control discarding of closely spaced points.
3632                This tolerance is specified as a fraction of the diagonal length of
3633                the bounding box of the points.
3634        """
3635        deln = vtki.new("Delaunay3D")
3636        deln.SetInputData(self.dataset)
3637        deln.SetAlpha(radius)
3638        deln.AlphaTetsOn()
3639        deln.AlphaTrisOff()
3640        deln.AlphaLinesOff()
3641        deln.AlphaVertsOff()
3642        deln.BoundingTriangulationOff()
3643        if tol:
3644            deln.SetTolerance(tol)
3645        deln.Update()
3646        m = vedo.TetMesh(deln.GetOutput())
3647        m.pipeline = utils.OperationNode(
3648            "generate_delaunay3d", c="#e9c46a:#edabab", parents=[self],
3649        )
3650        m.name = "Delaunay3D"
3651        return m
3652
3653    ####################################################
3654    def visible_points(self, area=(), tol=None, invert=False) -> Union[Self, None]:
3655        """
3656        Extract points based on whether they are visible or not.
3657        Visibility is determined by accessing the z-buffer of a rendering window.
3658        The position of each input point is converted into display coordinates,
3659        and then the z-value at that point is obtained.
3660        If within the user-specified tolerance, the point is considered visible.
3661        Associated data attributes are passed to the output as well.
3662
3663        This filter also allows you to specify a rectangular window in display (pixel)
3664        coordinates in which the visible points must lie.
3665
3666        Arguments:
3667            area : (list)
3668                specify a rectangular region as (xmin,xmax,ymin,ymax)
3669            tol : (float)
3670                a tolerance in normalized display coordinate system
3671            invert : (bool)
3672                select invisible points instead.
3673
3674        Example:
3675            ```python
3676            from vedo import Ellipsoid, show
3677            s = Ellipsoid().rotate_y(30)
3678
3679            # Camera options: pos, focal_point, viewup, distance
3680            camopts = dict(pos=(0,0,25), focal_point=(0,0,0))
3681            show(s, camera=camopts, offscreen=True)
3682
3683            m = s.visible_points()
3684            # print('visible pts:', m.vertices)  # numpy array
3685            show(m, new=True, axes=1).close() # optionally draw result in a new window
3686            ```
3687            ![](https://vedo.embl.es/images/feats/visible_points.png)
3688        """
3689        svp = vtki.new("SelectVisiblePoints")
3690        svp.SetInputData(self.dataset)
3691
3692        ren = None
3693        if vedo.plotter_instance:
3694            if vedo.plotter_instance.renderer:
3695                ren = vedo.plotter_instance.renderer
3696                svp.SetRenderer(ren)
3697        if not ren:
3698            vedo.logger.warning(
3699                "visible_points() can only be used after a rendering step"
3700            )
3701            return None
3702
3703        if len(area) == 2:
3704            area = utils.flatten(area)
3705        if len(area) == 4:
3706            # specify a rectangular region
3707            svp.SetSelection(area[0], area[1], area[2], area[3])
3708        if tol is not None:
3709            svp.SetTolerance(tol)
3710        if invert:
3711            svp.SelectInvisibleOn()
3712        svp.Update()
3713
3714        m = Points(svp.GetOutput())
3715        m.name = "VisiblePoints"
3716        return m

Work with point clouds.

Points(inputobj=None, r=4, c=(0.2, 0.2, 0.2), alpha=1)
470    def __init__(self, inputobj=None, r=4, c=(0.2, 0.2, 0.2), alpha=1):
471        """
472        Build an object made of only vertex points for a list of 2D/3D points.
473        Both shapes (N, 3) or (3, N) are accepted as input, if N>3.
474
475        Arguments:
476            inputobj : (list, tuple)
477            r : (int)
478                Point radius in units of pixels.
479            c : (str, list)
480                Color name or rgb tuple.
481            alpha : (float)
482                Transparency in range [0,1].
483
484        Example:
485            ```python
486            from vedo import *
487
488            def fibonacci_sphere(n):
489                s = np.linspace(0, n, num=n, endpoint=False)
490                theta = s * 2.399963229728653
491                y = 1 - s * (2/(n-1))
492                r = np.sqrt(1 - y * y)
493                x = np.cos(theta) * r
494                z = np.sin(theta) * r
495                return np._c[x,y,z]
496
497            Points(fibonacci_sphere(1000)).show(axes=1).close()
498            ```
499            ![](https://vedo.embl.es/images/feats/fibonacci.png)
500        """
501        # print("INIT POINTS")
502        super().__init__()
503
504        self.name = ""
505        self.filename = ""
506        self.file_size = ""
507
508        self.info = {}
509        self.time = time.time()
510        
511        self.transform = LinearTransform()
512        self.point_locator = None
513        self.cell_locator = None
514        self.line_locator = None
515
516        self.actor = vtki.vtkActor()
517        self.properties = self.actor.GetProperty()
518        self.properties_backface = self.actor.GetBackfaceProperty()
519        self.mapper = vtki.new("PolyDataMapper")
520        self.dataset = vtki.vtkPolyData()
521        
522        # Create weakref so actor can access this object (eg to pick/remove):
523        self.actor.retrieve_object = weak_ref_to(self)
524
525        try:
526            self.properties.RenderPointsAsSpheresOn()
527        except AttributeError:
528            pass
529
530        if inputobj is None:  ####################
531            return
532        ##########################################
533
534        self.name = "Points"
535
536        ######
537        if isinstance(inputobj, vtki.vtkActor):
538            self.dataset.DeepCopy(inputobj.GetMapper().GetInput())
539            pr = vtki.vtkProperty()
540            pr.DeepCopy(inputobj.GetProperty())
541            self.actor.SetProperty(pr)
542            self.properties = pr
543            self.mapper.SetScalarVisibility(inputobj.GetMapper().GetScalarVisibility())
544
545        elif isinstance(inputobj, vtki.vtkPolyData):
546            self.dataset = inputobj
547            if self.dataset.GetNumberOfCells() == 0:
548                carr = vtki.vtkCellArray()
549                for i in range(self.dataset.GetNumberOfPoints()):
550                    carr.InsertNextCell(1)
551                    carr.InsertCellPoint(i)
552                self.dataset.SetVerts(carr)
553
554        elif isinstance(inputobj, Points):
555            self.dataset = inputobj.dataset
556            self.copy_properties_from(inputobj)
557
558        elif utils.is_sequence(inputobj):  # passing point coords
559            self.dataset = utils.buildPolyData(utils.make3d(inputobj))
560
561        elif isinstance(inputobj, str):
562            verts = vedo.file_io.load(inputobj)
563            self.filename = inputobj
564            self.dataset = verts.dataset
565
566        elif "meshlib" in str(type(inputobj)):
567            from meshlib import mrmeshnumpy as mn
568            self.dataset = utils.buildPolyData(mn.toNumpyArray(inputobj.points))
569
570        else:
571            # try to extract the points from a generic VTK input data object
572            if hasattr(inputobj, "dataset"):
573                inputobj = inputobj.dataset
574            try:
575                vvpts = inputobj.GetPoints()
576                self.dataset = vtki.vtkPolyData()
577                self.dataset.SetPoints(vvpts)
578                for i in range(inputobj.GetPointData().GetNumberOfArrays()):
579                    arr = inputobj.GetPointData().GetArray(i)
580                    self.dataset.GetPointData().AddArray(arr)
581            except:
582                vedo.logger.error(f"cannot build Points from type {type(inputobj)}")
583                raise RuntimeError()
584
585        self.actor.SetMapper(self.mapper)
586        self.mapper.SetInputData(self.dataset)
587
588        self.properties.SetColor(colors.get_color(c))
589        self.properties.SetOpacity(alpha)
590        self.properties.SetRepresentationToPoints()
591        self.properties.SetPointSize(r)
592        self.properties.LightingOff()
593
594        self.pipeline = utils.OperationNode(
595            self, parents=[], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
596        )

Build an object made of only vertex points for a list of 2D/3D points. Both shapes (N, 3) or (3, N) are accepted as input, if N>3.

Arguments:
  • inputobj : (list, tuple)
  • r : (int) Point radius in units of pixels.
  • c : (str, list) Color name or rgb tuple.
  • alpha : (float) Transparency in range [0,1].
Example:
from vedo import *

def fibonacci_sphere(n):
    s = np.linspace(0, n, num=n, endpoint=False)
    theta = s * 2.399963229728653
    y = 1 - s * (2/(n-1))
    r = np.sqrt(1 - y * y)
    x = np.cos(theta) * r
    z = np.sin(theta) * r
    return np._c[x,y,z]

Points(fibonacci_sphere(1000)).show(axes=1).close()

def polydata(self, **kwargs):
818    def polydata(self, **kwargs):
819        """
820        Obsolete. Use property `.dataset` instead.
821        Returns the underlying `vtkPolyData` object.
822        """
823        colors.printc(
824            "WARNING: call to .polydata() is obsolete, use property .dataset instead.",
825            c="y")
826        return self.dataset

Obsolete. Use property .dataset instead. Returns the underlying vtkPolyData object.

def copy(self, deep=True) -> Self:
834    def copy(self, deep=True) -> Self:
835        """Return a copy of the object. Alias of `clone()`."""
836        return self.clone(deep=deep)

Return a copy of the object. Alias of clone().

def clone(self, deep=True) -> Self:
838    def clone(self, deep=True) -> Self:
839        """
840        Clone a `PointCloud` or `Mesh` object to make an exact copy of it.
841        Alias of `copy()`.
842
843        Arguments:
844            deep : (bool)
845                if False return a shallow copy of the mesh without copying the points array.
846
847        Examples:
848            - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py)
849
850               ![](https://vedo.embl.es/images/basic/mirror.png)
851        """
852        poly = vtki.vtkPolyData()
853        if deep or isinstance(deep, dict): # if a memo object is passed this checks as True
854            poly.DeepCopy(self.dataset)
855        else:
856            poly.ShallowCopy(self.dataset)
857
858        if isinstance(self, vedo.Mesh):
859            cloned = vedo.Mesh(poly)
860        else:
861            cloned = Points(poly)
862        # print([self], self.__class__)
863        # cloned = self.__class__(poly)
864
865        cloned.transform = self.transform.clone()
866
867        cloned.copy_properties_from(self)
868
869        cloned.name = str(self.name)
870        cloned.filename = str(self.filename)
871        cloned.info = dict(self.info)
872        cloned.pipeline = utils.OperationNode("clone", parents=[self], shape="diamond", c="#edede9")
873
874        if isinstance(deep, dict):
875            deep[id(self)] = cloned
876
877        return cloned

Clone a PointCloud or Mesh object to make an exact copy of it. Alias of copy().

Arguments:
  • deep : (bool) if False return a shallow copy of the mesh without copying the points array.
Examples:
def compute_normals_with_pca(self, n=20, orientation_point=None, invert=False) -> Self:
879    def compute_normals_with_pca(self, n=20, orientation_point=None, invert=False) -> Self:
880        """
881        Generate point normals using PCA (principal component analysis).
882        This algorithm estimates a local tangent plane around each sample point p
883        by considering a small neighborhood of points around p, and fitting a plane
884        to the neighborhood (via PCA).
885
886        Arguments:
887            n : (int)
888                neighborhood size to calculate the normal
889            orientation_point : (list)
890                adjust the +/- sign of the normals so that
891                the normals all point towards a specified point. If None, perform a traversal
892                of the point cloud and flip neighboring normals so that they are mutually consistent.
893            invert : (bool)
894                flip all normals
895        """
896        poly = self.dataset
897        pcan = vtki.new("PCANormalEstimation")
898        pcan.SetInputData(poly)
899        pcan.SetSampleSize(n)
900
901        if orientation_point is not None:
902            pcan.SetNormalOrientationToPoint()
903            pcan.SetOrientationPoint(orientation_point)
904        else:
905            pcan.SetNormalOrientationToGraphTraversal()
906
907        if invert:
908            pcan.FlipNormalsOn()
909        pcan.Update()
910
911        varr = pcan.GetOutput().GetPointData().GetNormals()
912        varr.SetName("Normals")
913        self.dataset.GetPointData().SetNormals(varr)
914        self.dataset.GetPointData().Modified()
915        return self

Generate point normals using PCA (principal component analysis). This algorithm estimates a local tangent plane around each sample point p by considering a small neighborhood of points around p, and fitting a plane to the neighborhood (via PCA).

Arguments:
  • n : (int) neighborhood size to calculate the normal
  • orientation_point : (list) adjust the +/- sign of the normals so that the normals all point towards a specified point. If None, perform a traversal of the point cloud and flip neighboring normals so that they are mutually consistent.
  • invert : (bool) flip all normals
def compute_acoplanarity(self, n=25, radius=None, on='points') -> Self:
917    def compute_acoplanarity(self, n=25, radius=None, on="points") -> Self:
918        """
919        Compute acoplanarity which is a measure of how much a local region of the mesh
920        differs from a plane.
921        
922        The information is stored in a `pointdata` or `celldata` array with name 'Acoplanarity'.
923        
924        Either `n` (number of neighbour points) or `radius` (radius of local search) can be specified.
925        If a radius value is given and not enough points fall inside it, then a -1 is stored.
926
927        Example:
928            ```python
929            from vedo import *
930            msh = ParametricShape('RandomHills')
931            msh.compute_acoplanarity(radius=0.1, on='cells')
932            msh.cmap("coolwarm", on='cells').add_scalarbar()
933            msh.show(axes=1).close()
934            ```
935            ![](https://vedo.embl.es/images/feats/acoplanarity.jpg)
936        """
937        acoplanarities = []
938        if "point" in on:
939            pts = self.vertices
940        elif "cell" in on:
941            pts = self.cell_centers
942        else:
943            raise ValueError(f"In compute_acoplanarity() set on to either 'cells' or 'points', not {on}")
944
945        for p in utils.progressbar(pts, delay=5, width=15, title=f"{on} acoplanarity"):
946            if n:
947                data = self.closest_point(p, n=n)
948                npts = n
949            elif radius:
950                data = self.closest_point(p, radius=radius)
951                npts = len(data)
952
953            try:
954                center = data.mean(axis=0)
955                res = np.linalg.svd(data - center)
956                acoplanarities.append(res[1][2] / npts)
957            except:
958                acoplanarities.append(-1.0)
959
960        if "point" in on:
961            self.pointdata["Acoplanarity"] = np.array(acoplanarities, dtype=float)
962        else:
963            self.celldata["Acoplanarity"] = np.array(acoplanarities, dtype=float)
964        return self

Compute acoplanarity which is a measure of how much a local region of the mesh differs from a plane.

The information is stored in a pointdata or celldata array with name 'Acoplanarity'.

Either n (number of neighbour points) or radius (radius of local search) can be specified. If a radius value is given and not enough points fall inside it, then a -1 is stored.

Example:
from vedo import *
msh = ParametricShape('RandomHills')
msh.compute_acoplanarity(radius=0.1, on='cells')
msh.cmap("coolwarm", on='cells').add_scalarbar()
msh.show(axes=1).close()

def distance_to( self, pcloud, signed=False, invert=False, name='Distance') -> numpy.ndarray:
 966    def distance_to(self, pcloud, signed=False, invert=False, name="Distance") -> np.ndarray:
 967        """
 968        Computes the distance from one point cloud or mesh to another point cloud or mesh.
 969        This new `pointdata` array is saved with default name "Distance".
 970
 971        Keywords `signed` and `invert` are used to compute signed distance,
 972        but the mesh in that case must have polygonal faces (not a simple point cloud),
 973        and normals must also be computed.
 974
 975        Examples:
 976            - [distance2mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/distance2mesh.py)
 977
 978                ![](https://vedo.embl.es/images/basic/distance2mesh.png)
 979        """
 980        if pcloud.dataset.GetNumberOfPolys():
 981
 982            poly1 = self.dataset
 983            poly2 = pcloud.dataset
 984            df = vtki.new("DistancePolyDataFilter")
 985            df.ComputeSecondDistanceOff()
 986            df.SetInputData(0, poly1)
 987            df.SetInputData(1, poly2)
 988            df.SetSignedDistance(signed)
 989            df.SetNegateDistance(invert)
 990            df.Update()
 991            scals = df.GetOutput().GetPointData().GetScalars()
 992            dists = utils.vtk2numpy(scals)
 993
 994        else:  # has no polygons
 995
 996            if signed:
 997                vedo.logger.warning("distance_to() called with signed=True but input object has no polygons")
 998
 999            if not pcloud.point_locator:
1000                pcloud.point_locator = vtki.new("PointLocator")
1001                pcloud.point_locator.SetDataSet(pcloud.dataset)
1002                pcloud.point_locator.BuildLocator()
1003
1004            ids = []
1005            ps1 = self.vertices
1006            ps2 = pcloud.vertices
1007            for p in ps1:
1008                pid = pcloud.point_locator.FindClosestPoint(p)
1009                ids.append(pid)
1010
1011            deltas = ps2[ids] - ps1
1012            dists = np.linalg.norm(deltas, axis=1).astype(np.float32)
1013            scals = utils.numpy2vtk(dists)
1014
1015        scals.SetName(name)
1016        self.dataset.GetPointData().AddArray(scals)
1017        self.dataset.GetPointData().SetActiveScalars(scals.GetName())
1018        rng = scals.GetRange()
1019        self.mapper.SetScalarRange(rng[0], rng[1])
1020        self.mapper.ScalarVisibilityOn()
1021
1022        self.pipeline = utils.OperationNode(
1023            "distance_to",
1024            parents=[self, pcloud],
1025            shape="cylinder",
1026            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1027        )
1028        return dists

Computes the distance from one point cloud or mesh to another point cloud or mesh. This new pointdata array is saved with default name "Distance".

Keywords signed and invert are used to compute signed distance, but the mesh in that case must have polygonal faces (not a simple point cloud), and normals must also be computed.

Examples:
def clean(self) -> Self:
1030    def clean(self) -> Self:
1031        """Clean pointcloud or mesh by removing coincident points."""
1032        cpd = vtki.new("CleanPolyData")
1033        cpd.PointMergingOn()
1034        cpd.ConvertLinesToPointsOff()
1035        cpd.ConvertPolysToLinesOff()
1036        cpd.ConvertStripsToPolysOff()
1037        cpd.SetInputData(self.dataset)
1038        cpd.Update()
1039        self._update(cpd.GetOutput())
1040        self.pipeline = utils.OperationNode(
1041            "clean", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
1042        )
1043        return self

Clean pointcloud or mesh by removing coincident points.

def subsample(self, fraction: float, absolute=False) -> Self:
1045    def subsample(self, fraction: float, absolute=False) -> Self:
1046        """
1047        Subsample a point cloud by requiring that the points
1048        or vertices are far apart at least by the specified fraction of the object size.
1049        If a Mesh is passed the polygonal faces are not removed
1050        but holes can appear as their vertices are removed.
1051
1052        Examples:
1053            - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py)
1054
1055                ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png)
1056
1057            - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py)
1058
1059                ![](https://vedo.embl.es/images/advanced/recosurface.png)
1060        """
1061        if not absolute:
1062            if fraction > 1:
1063                vedo.logger.warning(
1064                    f"subsample(fraction=...), fraction must be < 1, but is {fraction}"
1065                )
1066            if fraction <= 0:
1067                return self
1068
1069        cpd = vtki.new("CleanPolyData")
1070        cpd.PointMergingOn()
1071        cpd.ConvertLinesToPointsOn()
1072        cpd.ConvertPolysToLinesOn()
1073        cpd.ConvertStripsToPolysOn()
1074        cpd.SetInputData(self.dataset)
1075        if absolute:
1076            cpd.SetTolerance(fraction / self.diagonal_size())
1077            # cpd.SetToleranceIsAbsolute(absolute)
1078        else:
1079            cpd.SetTolerance(fraction)
1080        cpd.Update()
1081
1082        ps = 2
1083        if self.properties.GetRepresentation() == 0:
1084            ps = self.properties.GetPointSize()
1085
1086        self._update(cpd.GetOutput())
1087        self.ps(ps)
1088
1089        self.pipeline = utils.OperationNode(
1090            "subsample", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
1091        )
1092        return self

Subsample a point cloud by requiring that the points or vertices are far apart at least by the specified fraction of the object size. If a Mesh is passed the polygonal faces are not removed but holes can appear as their vertices are removed.

Examples:
def threshold(self, scalars: str, above=None, below=None, on='points') -> Self:
1094    def threshold(self, scalars: str, above=None, below=None, on="points") -> Self:
1095        """
1096        Extracts cells where scalar value satisfies threshold criterion.
1097
1098        Arguments:
1099            scalars : (str)
1100                name of the scalars array.
1101            above : (float)
1102                minimum value of the scalar
1103            below : (float)
1104                maximum value of the scalar
1105            on : (str)
1106                if 'cells' assume array of scalars refers to cell data.
1107
1108        Examples:
1109            - [mesh_threshold.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_threshold.py)
1110        """
1111        thres = vtki.new("Threshold")
1112        thres.SetInputData(self.dataset)
1113
1114        if on.startswith("c"):
1115            asso = vtki.vtkDataObject.FIELD_ASSOCIATION_CELLS
1116        else:
1117            asso = vtki.vtkDataObject.FIELD_ASSOCIATION_POINTS
1118
1119        thres.SetInputArrayToProcess(0, 0, 0, asso, scalars)
1120
1121        if above is None and below is not None:
1122            try:  # vtk 9.2
1123                thres.ThresholdByLower(below)
1124            except AttributeError:  # vtk 9.3
1125                thres.SetUpperThreshold(below)
1126
1127        elif below is None and above is not None:
1128            try:
1129                thres.ThresholdByUpper(above)
1130            except AttributeError:
1131                thres.SetLowerThreshold(above)
1132        else:
1133            try:
1134                thres.ThresholdBetween(above, below)
1135            except AttributeError:
1136                thres.SetUpperThreshold(below)
1137                thres.SetLowerThreshold(above)
1138
1139        thres.Update()
1140
1141        gf = vtki.new("GeometryFilter")
1142        gf.SetInputData(thres.GetOutput())
1143        gf.Update()
1144        self._update(gf.GetOutput())
1145        self.pipeline = utils.OperationNode("threshold", parents=[self])
1146        return self

Extracts cells where scalar value satisfies threshold criterion.

Arguments:
  • scalars : (str) name of the scalars array.
  • above : (float) minimum value of the scalar
  • below : (float) maximum value of the scalar
  • on : (str) if 'cells' assume array of scalars refers to cell data.
Examples:
def quantize(self, value: float) -> Self:
1148    def quantize(self, value: float) -> Self:
1149        """
1150        The user should input a value and all {x,y,z} coordinates
1151        will be quantized to that absolute grain size.
1152        """
1153        qp = vtki.new("QuantizePolyDataPoints")
1154        qp.SetInputData(self.dataset)
1155        qp.SetQFactor(value)
1156        qp.Update()
1157        self._update(qp.GetOutput())
1158        self.pipeline = utils.OperationNode("quantize", parents=[self])
1159        return self

The user should input a value and all {x,y,z} coordinates will be quantized to that absolute grain size.

vertex_normals: numpy.ndarray
1161    @property
1162    def vertex_normals(self) -> np.ndarray:
1163        """
1164        Retrieve vertex normals as a numpy array. Same as `point_normals`.
1165        Check out also `compute_normals()` and `compute_normals_with_pca()`.
1166        """
1167        vtknormals = self.dataset.GetPointData().GetNormals()
1168        return utils.vtk2numpy(vtknormals)

Retrieve vertex normals as a numpy array. Same as point_normals. Check out also compute_normals() and compute_normals_with_pca().

point_normals: numpy.ndarray
1170    @property
1171    def point_normals(self) -> np.ndarray:
1172        """
1173        Retrieve vertex normals as a numpy array. Same as `vertex_normals`.
1174        Check out also `compute_normals()` and `compute_normals_with_pca()`.
1175        """
1176        vtknormals = self.dataset.GetPointData().GetNormals()
1177        return utils.vtk2numpy(vtknormals)

Retrieve vertex normals as a numpy array. Same as vertex_normals. Check out also compute_normals() and compute_normals_with_pca().

def align_to( self, target, iters=100, rigid=False, invert=False, use_centroids=False) -> Self:
1179    def align_to(self, target, iters=100, rigid=False, invert=False, use_centroids=False) -> Self:
1180        """
1181        Aligned to target mesh through the `Iterative Closest Point` algorithm.
1182
1183        The core of the algorithm is to match each vertex in one surface with
1184        the closest surface point on the other, then apply the transformation
1185        that modify one surface to best match the other (in the least-square sense).
1186
1187        Arguments:
1188            rigid : (bool)
1189                if True do not allow scaling
1190            invert : (bool)
1191                if True start by aligning the target to the source but
1192                invert the transformation finally. Useful when the target is smaller
1193                than the source.
1194            use_centroids : (bool)
1195                start by matching the centroids of the two objects.
1196
1197        Examples:
1198            - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py)
1199
1200                ![](https://vedo.embl.es/images/basic/align1.png)
1201
1202            - [align2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align2.py)
1203
1204                ![](https://vedo.embl.es/images/basic/align2.png)
1205        """
1206        icp = vtki.new("IterativeClosestPointTransform")
1207        icp.SetSource(self.dataset)
1208        icp.SetTarget(target.dataset)
1209        if invert:
1210            icp.Inverse()
1211        icp.SetMaximumNumberOfIterations(iters)
1212        if rigid:
1213            icp.GetLandmarkTransform().SetModeToRigidBody()
1214        icp.SetStartByMatchingCentroids(use_centroids)
1215        icp.Update()
1216
1217        self.apply_transform(icp.GetMatrix())
1218
1219        self.pipeline = utils.OperationNode(
1220            "align_to", parents=[self, target], comment=f"rigid = {rigid}"
1221        )
1222        return self

Aligned to target mesh through the Iterative Closest Point algorithm.

The core of the algorithm is to match each vertex in one surface with the closest surface point on the other, then apply the transformation that modify one surface to best match the other (in the least-square sense).

Arguments:
  • rigid : (bool) if True do not allow scaling
  • invert : (bool) if True start by aligning the target to the source but invert the transformation finally. Useful when the target is smaller than the source.
  • use_centroids : (bool) start by matching the centroids of the two objects.
Examples:
def align_to_bounding_box(self, msh, rigid=False) -> Self:
1224    def align_to_bounding_box(self, msh, rigid=False) -> Self:
1225        """
1226        Align the current object's bounding box to the bounding box
1227        of the input object.
1228
1229        Use `rigid=True` to disable scaling.
1230
1231        Example:
1232            [align6.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align6.py)
1233        """
1234        lmt = vtki.vtkLandmarkTransform()
1235        ss = vtki.vtkPoints()
1236        xss0, xss1, yss0, yss1, zss0, zss1 = self.bounds()
1237        for p in [
1238            [xss0, yss0, zss0],
1239            [xss1, yss0, zss0],
1240            [xss1, yss1, zss0],
1241            [xss0, yss1, zss0],
1242            [xss0, yss0, zss1],
1243            [xss1, yss0, zss1],
1244            [xss1, yss1, zss1],
1245            [xss0, yss1, zss1],
1246        ]:
1247            ss.InsertNextPoint(p)
1248        st = vtki.vtkPoints()
1249        xst0, xst1, yst0, yst1, zst0, zst1 = msh.bounds()
1250        for p in [
1251            [xst0, yst0, zst0],
1252            [xst1, yst0, zst0],
1253            [xst1, yst1, zst0],
1254            [xst0, yst1, zst0],
1255            [xst0, yst0, zst1],
1256            [xst1, yst0, zst1],
1257            [xst1, yst1, zst1],
1258            [xst0, yst1, zst1],
1259        ]:
1260            st.InsertNextPoint(p)
1261
1262        lmt.SetSourceLandmarks(ss)
1263        lmt.SetTargetLandmarks(st)
1264        lmt.SetModeToAffine()
1265        if rigid:
1266            lmt.SetModeToRigidBody()
1267        lmt.Update()
1268
1269        LT = LinearTransform(lmt)
1270        self.apply_transform(LT)
1271        return self

Align the current object's bounding box to the bounding box of the input object.

Use rigid=True to disable scaling.

Example:

align6.py

def align_with_landmarks( self, source_landmarks, target_landmarks, rigid=False, affine=False, least_squares=False) -> Self:
1273    def align_with_landmarks(
1274        self,
1275        source_landmarks,
1276        target_landmarks,
1277        rigid=False,
1278        affine=False,
1279        least_squares=False,
1280    ) -> Self:
1281        """
1282        Transform mesh orientation and position based on a set of landmarks points.
1283        The algorithm finds the best matching of source points to target points
1284        in the mean least square sense, in one single step.
1285
1286        If `affine` is True the x, y and z axes can scale independently but stay collinear.
1287        With least_squares they can vary orientation.
1288
1289        Examples:
1290            - [align5.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align5.py)
1291
1292                ![](https://vedo.embl.es/images/basic/align5.png)
1293        """
1294
1295        if utils.is_sequence(source_landmarks):
1296            ss = vtki.vtkPoints()
1297            for p in source_landmarks:
1298                ss.InsertNextPoint(p)
1299        else:
1300            ss = source_landmarks.dataset.GetPoints()
1301            if least_squares:
1302                source_landmarks = source_landmarks.vertices
1303
1304        if utils.is_sequence(target_landmarks):
1305            st = vtki.vtkPoints()
1306            for p in target_landmarks:
1307                st.InsertNextPoint(p)
1308        else:
1309            st = target_landmarks.GetPoints()
1310            if least_squares:
1311                target_landmarks = target_landmarks.vertices
1312
1313        if ss.GetNumberOfPoints() != st.GetNumberOfPoints():
1314            n1 = ss.GetNumberOfPoints()
1315            n2 = st.GetNumberOfPoints()
1316            vedo.logger.error(f"source and target have different nr of points {n1} vs {n2}")
1317            raise RuntimeError()
1318
1319        if int(rigid) + int(affine) + int(least_squares) > 1:
1320            vedo.logger.error(
1321                "only one of rigid, affine, least_squares can be True at a time"
1322            )
1323            raise RuntimeError()
1324
1325        lmt = vtki.vtkLandmarkTransform()
1326        lmt.SetSourceLandmarks(ss)
1327        lmt.SetTargetLandmarks(st)
1328        lmt.SetModeToSimilarity()
1329
1330        if rigid:
1331            lmt.SetModeToRigidBody()
1332            lmt.Update()
1333
1334        elif affine:
1335            lmt.SetModeToAffine()
1336            lmt.Update()
1337
1338        elif least_squares:
1339            cms = source_landmarks.mean(axis=0)
1340            cmt = target_landmarks.mean(axis=0)
1341            m = np.linalg.lstsq(source_landmarks - cms, target_landmarks - cmt, rcond=None)[0]
1342            M = vtki.vtkMatrix4x4()
1343            for i in range(3):
1344                for j in range(3):
1345                    M.SetElement(j, i, m[i][j])
1346            lmt = vtki.vtkTransform()
1347            lmt.Translate(cmt)
1348            lmt.Concatenate(M)
1349            lmt.Translate(-cms)
1350
1351        else:
1352            lmt.Update()
1353
1354        self.apply_transform(lmt)
1355        self.pipeline = utils.OperationNode("transform_with_landmarks", parents=[self])
1356        return self

Transform mesh orientation and position based on a set of landmarks points. The algorithm finds the best matching of source points to target points in the mean least square sense, in one single step.

If affine is True the x, y and z axes can scale independently but stay collinear. With least_squares they can vary orientation.

Examples:
def normalize(self) -> Self:
1358    def normalize(self) -> Self:
1359        """Scale average size to unit. The scaling is performed around the center of mass."""
1360        coords = self.vertices
1361        if not coords.shape[0]:
1362            return self
1363        cm = np.mean(coords, axis=0)
1364        pts = coords - cm
1365        xyz2 = np.sum(pts * pts, axis=0)
1366        scale = 1 / np.sqrt(np.sum(xyz2) / len(pts))
1367        self.scale(scale, origin=cm)
1368        self.pipeline = utils.OperationNode("normalize", parents=[self])
1369        return self

Scale average size to unit. The scaling is performed around the center of mass.

def mirror(self, axis='x', origin=True) -> Self:
1371    def mirror(self, axis="x", origin=True) -> Self:
1372        """
1373        Mirror reflect along one of the cartesian axes
1374
1375        Arguments:
1376            axis : (str)
1377                axis to use for mirroring, must be set to `x, y, z`.
1378                Or any combination of those.
1379            origin : (list)
1380                use this point as the origin of the mirroring transformation.
1381
1382        Examples:
1383            - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py)
1384
1385                ![](https://vedo.embl.es/images/basic/mirror.png)
1386        """
1387        sx, sy, sz = 1, 1, 1
1388        if "x" in axis.lower(): sx = -1
1389        if "y" in axis.lower(): sy = -1
1390        if "z" in axis.lower(): sz = -1
1391
1392        self.scale([sx, sy, sz], origin=origin)
1393
1394        self.pipeline = utils.OperationNode(
1395            "mirror", comment=f"axis = {axis}", parents=[self])
1396
1397        if sx * sy * sz < 0:
1398            if hasattr(self, "reverse"):
1399                self.reverse()
1400        return self

Mirror reflect along one of the cartesian axes

Arguments:
  • axis : (str) axis to use for mirroring, must be set to x, y, z. Or any combination of those.
  • origin : (list) use this point as the origin of the mirroring transformation.
Examples:
def flip_normals(self) -> Self:
1402    def flip_normals(self) -> Self:
1403        """Flip all normals orientation."""
1404        rs = vtki.new("ReverseSense")
1405        rs.SetInputData(self.dataset)
1406        rs.ReverseCellsOff()
1407        rs.ReverseNormalsOn()
1408        rs.Update()
1409        self._update(rs.GetOutput())
1410        self.pipeline = utils.OperationNode("flip_normals", parents=[self])
1411        return self

Flip all normals orientation.

def add_gaussian_noise(self, sigma=1.0) -> Self:
1413    def add_gaussian_noise(self, sigma=1.0) -> Self:
1414        """
1415        Add gaussian noise to point positions.
1416        An extra array is added named "GaussianNoise" with the displacements.
1417
1418        Arguments:
1419            sigma : (float)
1420                nr. of standard deviations, expressed in percent of the diagonal size of mesh.
1421                Can also be a list `[sigma_x, sigma_y, sigma_z]`.
1422
1423        Example:
1424            ```python
1425            from vedo import Sphere
1426            Sphere().add_gaussian_noise(1.0).point_size(8).show().close()
1427            ```
1428        """
1429        sz = self.diagonal_size()
1430        pts = self.vertices
1431        n = len(pts)
1432        ns = (np.random.randn(n, 3) * sigma) * (sz / 100)
1433        vpts = vtki.vtkPoints()
1434        vpts.SetNumberOfPoints(n)
1435        vpts.SetData(utils.numpy2vtk(pts + ns, dtype=np.float32))
1436        self.dataset.SetPoints(vpts)
1437        self.dataset.GetPoints().Modified()
1438        self.pointdata["GaussianNoise"] = -ns
1439        self.pipeline = utils.OperationNode(
1440            "gaussian_noise", parents=[self], shape="egg", comment=f"sigma = {sigma}"
1441        )
1442        return self

Add gaussian noise to point positions. An extra array is added named "GaussianNoise" with the displacements.

Arguments:
  • sigma : (float) nr. of standard deviations, expressed in percent of the diagonal size of mesh. Can also be a list [sigma_x, sigma_y, sigma_z].
Example:
from vedo import Sphere
Sphere().add_gaussian_noise(1.0).point_size(8).show().close()
def closest_point( self, pt, n=1, radius=None, return_point_id=False, return_cell_id=False) -> Union[List[int], int, numpy.ndarray]:
1444    def closest_point(
1445        self, pt, n=1, radius=None, return_point_id=False, return_cell_id=False
1446    ) -> Union[List[int], int, np.ndarray]:
1447        """
1448        Find the closest point(s) on a mesh given from the input point `pt`.
1449
1450        Arguments:
1451            n : (int)
1452                if greater than 1, return a list of n ordered closest points
1453            radius : (float)
1454                if given, get all points within that radius. Then n is ignored.
1455            return_point_id : (bool)
1456                return point ID instead of coordinates
1457            return_cell_id : (bool)
1458                return cell ID in which the closest point sits
1459
1460        Examples:
1461            - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py)
1462            - [fitplanes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitplanes.py)
1463            - [quadratic_morphing.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/quadratic_morphing.py)
1464
1465        .. note::
1466            The appropriate tree search locator is built on the fly and cached for speed.
1467
1468            If you want to reset it use `mymesh.point_locator=None`
1469            and / or `mymesh.cell_locator=None`.
1470        """
1471        if len(pt) != 3:
1472            pt = [pt[0], pt[1], 0]
1473
1474        # NB: every time the mesh moves or is warped the locators are set to None
1475        if ((n > 1 or radius) or (n == 1 and return_point_id)) and not return_cell_id:
1476            poly = None
1477            if not self.point_locator:
1478                poly = self.dataset
1479                self.point_locator = vtki.new("StaticPointLocator")
1480                self.point_locator.SetDataSet(poly)
1481                self.point_locator.BuildLocator()
1482
1483            ##########
1484            if radius:
1485                vtklist = vtki.vtkIdList()
1486                self.point_locator.FindPointsWithinRadius(radius, pt, vtklist)
1487            elif n > 1:
1488                vtklist = vtki.vtkIdList()
1489                self.point_locator.FindClosestNPoints(n, pt, vtklist)
1490            else:  # n==1 hence return_point_id==True
1491                ########
1492                return self.point_locator.FindClosestPoint(pt)
1493                ########
1494
1495            if return_point_id:
1496                ########
1497                return utils.vtk2numpy(vtklist)
1498                ########
1499
1500            if not poly:
1501                poly = self.dataset
1502            trgp = []
1503            for i in range(vtklist.GetNumberOfIds()):
1504                trgp_ = [0, 0, 0]
1505                vi = vtklist.GetId(i)
1506                poly.GetPoints().GetPoint(vi, trgp_)
1507                trgp.append(trgp_)
1508            ########
1509            return np.array(trgp)
1510            ########
1511
1512        else:
1513
1514            if not self.cell_locator:
1515                poly = self.dataset
1516
1517                # As per Miquel example with limbs the vtkStaticCellLocator doesnt work !!
1518                # https://discourse.vtk.org/t/vtkstaticcelllocator-problem-vtk9-0-3/7854/4
1519                if vedo.vtk_version[0] >= 9 and vedo.vtk_version[1] > 0:
1520                    self.cell_locator = vtki.new("StaticCellLocator")
1521                else:
1522                    self.cell_locator = vtki.new("CellLocator")
1523
1524                self.cell_locator.SetDataSet(poly)
1525                self.cell_locator.BuildLocator()
1526
1527            if radius is not None:
1528                vedo.printc("Warning: closest_point() with radius is not implemented for cells.", c='r')   
1529 
1530            if n != 1:
1531                vedo.printc("Warning: closest_point() with n>1 is not implemented for cells.", c='r')   
1532 
1533            trgp = [0, 0, 0]
1534            cid = vtki.mutable(0)
1535            dist2 = vtki.mutable(0)
1536            subid = vtki.mutable(0)
1537            self.cell_locator.FindClosestPoint(pt, trgp, cid, subid, dist2)
1538
1539            if return_cell_id:
1540                return int(cid)
1541
1542            return np.array(trgp)

Find the closest point(s) on a mesh given from the input point pt.

Arguments:
  • n : (int) if greater than 1, return a list of n ordered closest points
  • radius : (float) if given, get all points within that radius. Then n is ignored.
  • return_point_id : (bool) return point ID instead of coordinates
  • return_cell_id : (bool) return cell ID in which the closest point sits
Examples:

The appropriate tree search locator is built on the fly and cached for speed.

If you want to reset it use mymesh.point_locator=None and / or mymesh.cell_locator=None.

def auto_distance(self) -> numpy.ndarray:
1544    def auto_distance(self) -> np.ndarray:
1545        """
1546        Calculate the distance to the closest point in the same cloud of points.
1547        The output is stored in a new pointdata array called "AutoDistance",
1548        and it is also returned by the function.
1549        """
1550        points = self.vertices
1551        if not self.point_locator:
1552            self.point_locator = vtki.new("StaticPointLocator")
1553            self.point_locator.SetDataSet(self.dataset)
1554            self.point_locator.BuildLocator()
1555        qs = []
1556        vtklist = vtki.vtkIdList()
1557        vtkpoints = self.dataset.GetPoints()
1558        for p in points:
1559            self.point_locator.FindClosestNPoints(2, p, vtklist)
1560            q = [0, 0, 0]
1561            pid = vtklist.GetId(1)
1562            vtkpoints.GetPoint(pid, q)
1563            qs.append(q)
1564        dists = np.linalg.norm(points - np.array(qs), axis=1)
1565        self.pointdata["AutoDistance"] = dists
1566        return dists

Calculate the distance to the closest point in the same cloud of points. The output is stored in a new pointdata array called "AutoDistance", and it is also returned by the function.

def hausdorff_distance(self, points) -> float:
1568    def hausdorff_distance(self, points) -> float:
1569        """
1570        Compute the Hausdorff distance to the input point set.
1571        Returns a single `float`.
1572
1573        Example:
1574            ```python
1575            from vedo import *
1576            t = np.linspace(0, 2*np.pi, 100)
1577            x = 4/3 * sin(t)**3
1578            y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12
1579            pol1 = Line(np.c_[x,y], closed=True).triangulate()
1580            pol2 = Polygon(nsides=5).pos(2,2)
1581            d12 = pol1.distance_to(pol2)
1582            d21 = pol2.distance_to(pol1)
1583            pol1.lw(0).cmap("viridis")
1584            pol2.lw(0).cmap("viridis")
1585            print("distance d12, d21 :", min(d12), min(d21))
1586            print("hausdorff distance:", pol1.hausdorff_distance(pol2))
1587            print("chamfer distance  :", pol1.chamfer_distance(pol2))
1588            show(pol1, pol2, axes=1)
1589            ```
1590            ![](https://vedo.embl.es/images/feats/heart.png)
1591        """
1592        hp = vtki.new("HausdorffDistancePointSetFilter")
1593        hp.SetInputData(0, self.dataset)
1594        hp.SetInputData(1, points.dataset)
1595        hp.SetTargetDistanceMethodToPointToCell()
1596        hp.Update()
1597        return hp.GetHausdorffDistance()

Compute the Hausdorff distance to the input point set. Returns a single float.

Example:
from vedo import *
t = np.linspace(0, 2*np.pi, 100)
x = 4/3 * sin(t)**3
y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12
pol1 = Line(np.c_[x,y], closed=True).triangulate()
pol2 = Polygon(nsides=5).pos(2,2)
d12 = pol1.distance_to(pol2)
d21 = pol2.distance_to(pol1)
pol1.lw(0).cmap("viridis")
pol2.lw(0).cmap("viridis")
print("distance d12, d21 :", min(d12), min(d21))
print("hausdorff distance:", pol1.hausdorff_distance(pol2))
print("chamfer distance  :", pol1.chamfer_distance(pol2))
show(pol1, pol2, axes=1)

def chamfer_distance(self, pcloud) -> float:
1599    def chamfer_distance(self, pcloud) -> float:
1600        """
1601        Compute the Chamfer distance to the input point set.
1602
1603        Example:
1604            ```python
1605            from vedo import *
1606            cloud1 = np.random.randn(1000, 3)
1607            cloud2 = np.random.randn(1000, 3) + [1, 2, 3]
1608            c1 = Points(cloud1, r=5, c="red")
1609            c2 = Points(cloud2, r=5, c="green")
1610            d = c1.chamfer_distance(c2)
1611            show(f"Chamfer distance = {d}", c1, c2, axes=1).close()
1612            ```
1613        """
1614        # Definition of Chamfer distance may vary, here we use the average
1615        if not pcloud.point_locator:
1616            pcloud.point_locator = vtki.new("PointLocator")
1617            pcloud.point_locator.SetDataSet(pcloud.dataset)
1618            pcloud.point_locator.BuildLocator()
1619        if not self.point_locator:
1620            self.point_locator = vtki.new("PointLocator")
1621            self.point_locator.SetDataSet(self.dataset)
1622            self.point_locator.BuildLocator()
1623
1624        ps1 = self.vertices
1625        ps2 = pcloud.vertices
1626
1627        ids12 = []
1628        for p in ps1:
1629            pid12 = pcloud.point_locator.FindClosestPoint(p)
1630            ids12.append(pid12)
1631        deltav = ps2[ids12] - ps1
1632        da = np.mean(np.linalg.norm(deltav, axis=1))
1633
1634        ids21 = []
1635        for p in ps2:
1636            pid21 = self.point_locator.FindClosestPoint(p)
1637            ids21.append(pid21)
1638        deltav = ps1[ids21] - ps2
1639        db = np.mean(np.linalg.norm(deltav, axis=1))
1640        return (da + db) / 2

Compute the Chamfer distance to the input point set.

Example:
from vedo import *
cloud1 = np.random.randn(1000, 3)
cloud2 = np.random.randn(1000, 3) + [1, 2, 3]
c1 = Points(cloud1, r=5, c="red")
c2 = Points(cloud2, r=5, c="green")
d = c1.chamfer_distance(c2)
show(f"Chamfer distance = {d}", c1, c2, axes=1).close()
def remove_outliers(self, radius: float, neighbors=5) -> Self:
1642    def remove_outliers(self, radius: float, neighbors=5) -> Self:
1643        """
1644        Remove outliers from a cloud of points within the specified `radius` search.
1645
1646        Arguments:
1647            radius : (float)
1648                Specify the local search radius.
1649            neighbors : (int)
1650                Specify the number of neighbors that a point must have,
1651                within the specified radius, for the point to not be considered isolated.
1652
1653        Examples:
1654            - [clustering.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/clustering.py)
1655
1656                ![](https://vedo.embl.es/images/basic/clustering.png)
1657        """
1658        removal = vtki.new("RadiusOutlierRemoval")
1659        removal.SetInputData(self.dataset)
1660        removal.SetRadius(radius)
1661        removal.SetNumberOfNeighbors(neighbors)
1662        removal.GenerateOutliersOff()
1663        removal.Update()
1664        inputobj = removal.GetOutput()
1665        if inputobj.GetNumberOfCells() == 0:
1666            carr = vtki.vtkCellArray()
1667            for i in range(inputobj.GetNumberOfPoints()):
1668                carr.InsertNextCell(1)
1669                carr.InsertCellPoint(i)
1670            inputobj.SetVerts(carr)
1671        self._update(removal.GetOutput())
1672        self.pipeline = utils.OperationNode("remove_outliers", parents=[self])
1673        return self

Remove outliers from a cloud of points within the specified radius search.

Arguments:
  • radius : (float) Specify the local search radius.
  • neighbors : (int) Specify the number of neighbors that a point must have, within the specified radius, for the point to not be considered isolated.
Examples:
def relax_point_positions( self, n=10, iters=10, sub_iters=10, packing_factor=1, max_step=0, constraints=()) -> Self:
1675    def relax_point_positions(
1676            self, 
1677            n=10,
1678            iters=10,
1679            sub_iters=10,
1680            packing_factor=1,
1681            max_step=0,
1682            constraints=(),
1683        ) -> Self:
1684        """
1685        Smooth mesh or points with a 
1686        [Laplacian algorithm](https://vtk.org/doc/nightly/html/classvtkPointSmoothingFilter.html)
1687        variant. This modifies the coordinates of the input points by adjusting their positions
1688        to create a smooth distribution (and thereby form a pleasing packing of the points).
1689        Smoothing is performed by considering the effects of neighboring points on one another
1690        it uses a cubic cutoff function to produce repulsive forces between close points
1691        and attractive forces that are a little further away.
1692        
1693        In general, the larger the neighborhood size, the greater the reduction in high frequency
1694        information. The memory and computational requirements of the algorithm may also
1695        significantly increase.
1696
1697        The algorithm incrementally adjusts the point positions through an iterative process.
1698        Basically points are moved due to the influence of neighboring points. 
1699        
1700        As points move, both the local connectivity and data attributes associated with each point
1701        must be updated. Rather than performing these expensive operations after every iteration,
1702        a number of sub-iterations can be specified. If so, then the neighborhood and attribute
1703        value updates occur only every sub iteration, which can improve performance significantly.
1704        
1705        Arguments:
1706            n : (int)
1707                neighborhood size to calculate the Laplacian.
1708            iters : (int)
1709                number of iterations.
1710            sub_iters : (int)
1711                number of sub-iterations, i.e. the number of times the neighborhood and attribute
1712                value updates occur during each iteration.
1713            packing_factor : (float)
1714                adjust convergence speed.
1715            max_step : (float)
1716                Specify the maximum smoothing step size for each smoothing iteration.
1717                This limits the the distance over which a point can move in each iteration.
1718                As in all iterative methods, the stability of the process is sensitive to this parameter.
1719                In general, small step size and large numbers of iterations are more stable than a larger
1720                step size and a smaller numbers of iterations.
1721            constraints : (dict)
1722                dictionary of constraints.
1723                Point constraints are used to prevent points from moving,
1724                or to move only on a plane. This can prevent shrinking or growing point clouds.
1725                If enabled, a local topological analysis is performed to determine whether a point
1726                should be marked as fixed" i.e., never moves, or the point only moves on a plane,
1727                or the point can move freely.
1728                If all points in the neighborhood surrounding a point are in the cone defined by
1729                `fixed_angle`, then the point is classified as fixed.
1730                If all points in the neighborhood surrounding a point are in the cone defined by
1731                `boundary_angle`, then the point is classified as lying on a plane.
1732                Angles are expressed in degrees.
1733        
1734        Example:
1735            ```py
1736            import numpy as np
1737            from vedo import Points, show
1738            from vedo.pyplot import histogram
1739
1740            vpts1 = Points(np.random.rand(10_000, 3))
1741            dists = vpts1.auto_distance()
1742            h1 = histogram(dists, xlim=(0,0.08)).clone2d()
1743
1744            vpts2 = vpts1.clone().relax_point_positions(n=100, iters=20, sub_iters=10)
1745            dists = vpts2.auto_distance()
1746            h2 = histogram(dists, xlim=(0,0.08)).clone2d()
1747
1748            show([[vpts1, h1], [vpts2, h2]], N=2).close()
1749            ```
1750        """
1751        smooth = vtki.new("PointSmoothingFilter")
1752        smooth.SetInputData(self.dataset)
1753        smooth.SetSmoothingModeToUniform()
1754        smooth.SetNumberOfIterations(iters)
1755        smooth.SetNumberOfSubIterations(sub_iters)
1756        smooth.SetPackingFactor(packing_factor)
1757        if self.point_locator:
1758            smooth.SetLocator(self.point_locator)
1759        if not max_step:
1760            max_step = self.diagonal_size() / 100
1761        smooth.SetMaximumStepSize(max_step)
1762        smooth.SetNeighborhoodSize(n)
1763        if constraints:
1764            fixed_angle = constraints.get("fixed_angle", 45)
1765            boundary_angle = constraints.get("boundary_angle", 110)
1766            smooth.EnableConstraintsOn()
1767            smooth.SetFixedAngle(fixed_angle)
1768            smooth.SetBoundaryAngle(boundary_angle)
1769            smooth.GenerateConstraintScalarsOn()
1770            smooth.GenerateConstraintNormalsOn()
1771        smooth.Update()
1772        self._update(smooth.GetOutput())
1773        self.metadata["PackingRadius"] = smooth.GetPackingRadius()
1774        self.pipeline = utils.OperationNode("relax_point_positions", parents=[self])
1775        return self

Smooth mesh or points with a Laplacian algorithm variant. This modifies the coordinates of the input points by adjusting their positions to create a smooth distribution (and thereby form a pleasing packing of the points). Smoothing is performed by considering the effects of neighboring points on one another it uses a cubic cutoff function to produce repulsive forces between close points and attractive forces that are a little further away.

In general, the larger the neighborhood size, the greater the reduction in high frequency information. The memory and computational requirements of the algorithm may also significantly increase.

The algorithm incrementally adjusts the point positions through an iterative process. Basically points are moved due to the influence of neighboring points.

As points move, both the local connectivity and data attributes associated with each point must be updated. Rather than performing these expensive operations after every iteration, a number of sub-iterations can be specified. If so, then the neighborhood and attribute value updates occur only every sub iteration, which can improve performance significantly.

Arguments:
  • n : (int) neighborhood size to calculate the Laplacian.
  • iters : (int) number of iterations.
  • sub_iters : (int) number of sub-iterations, i.e. the number of times the neighborhood and attribute value updates occur during each iteration.
  • packing_factor : (float) adjust convergence speed.
  • max_step : (float) Specify the maximum smoothing step size for each smoothing iteration. This limits the the distance over which a point can move in each iteration. As in all iterative methods, the stability of the process is sensitive to this parameter. In general, small step size and large numbers of iterations are more stable than a larger step size and a smaller numbers of iterations.
  • constraints : (dict) dictionary of constraints. Point constraints are used to prevent points from moving, or to move only on a plane. This can prevent shrinking or growing point clouds. If enabled, a local topological analysis is performed to determine whether a point should be marked as fixed" i.e., never moves, or the point only moves on a plane, or the point can move freely. If all points in the neighborhood surrounding a point are in the cone defined by fixed_angle, then the point is classified as fixed. If all points in the neighborhood surrounding a point are in the cone defined by boundary_angle, then the point is classified as lying on a plane. Angles are expressed in degrees.
Example:
import numpy as np
from vedo import Points, show
from vedo.pyplot import histogram

vpts1 = Points(np.random.rand(10_000, 3))
dists = vpts1.auto_distance()
h1 = histogram(dists, xlim=(0,0.08)).clone2d()

vpts2 = vpts1.clone().relax_point_positions(n=100, iters=20, sub_iters=10)
dists = vpts2.auto_distance()
h2 = histogram(dists, xlim=(0,0.08)).clone2d()

show([[vpts1, h1], [vpts2, h2]], N=2).close()
def smooth_mls_1d(self, f=0.2, radius=None, n=0) -> Self:
1777    def smooth_mls_1d(self, f=0.2, radius=None, n=0) -> Self:
1778        """
1779        Smooth mesh or points with a `Moving Least Squares` variant.
1780        The point data array "Variances" will contain the residue calculated for each point.
1781
1782        Arguments:
1783            f : (float)
1784                smoothing factor - typical range is [0,2].
1785            radius : (float)
1786                radius search in absolute units.
1787                If set then `f` is ignored.
1788            n : (int)
1789                number of neighbours to be used for the fit.
1790                If set then `f` and `radius` are ignored.
1791
1792        Examples:
1793            - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py)
1794            - [skeletonize.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/skeletonize.py)
1795
1796            ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png)
1797        """
1798        coords = self.vertices
1799        ncoords = len(coords)
1800
1801        if n:
1802            Ncp = n
1803        elif radius:
1804            Ncp = 1
1805        else:
1806            Ncp = int(ncoords * f / 10)
1807            if Ncp < 5:
1808                vedo.logger.warning(f"Please choose a fraction higher than {f}")
1809                Ncp = 5
1810
1811        variances, newline = [], []
1812        for p in coords:
1813            points = self.closest_point(p, n=Ncp, radius=radius)
1814            if len(points) < 4:
1815                continue
1816
1817            points = np.array(points)
1818            pointsmean = points.mean(axis=0)  # plane center
1819            _, dd, vv = np.linalg.svd(points - pointsmean)
1820            newp = np.dot(p - pointsmean, vv[0]) * vv[0] + pointsmean
1821            variances.append(dd[1] + dd[2])
1822            newline.append(newp)
1823
1824        self.pointdata["Variances"] = np.array(variances).astype(np.float32)
1825        self.vertices = newline
1826        self.pipeline = utils.OperationNode("smooth_mls_1d", parents=[self])
1827        return self

Smooth mesh or points with a Moving Least Squares variant. The point data array "Variances" will contain the residue calculated for each point.

Arguments:
  • f : (float) smoothing factor - typical range is [0,2].
  • radius : (float) radius search in absolute units. If set then f is ignored.
  • n : (int) number of neighbours to be used for the fit. If set then f and radius are ignored.
Examples:

def smooth_mls_2d(self, f=0.2, radius=None, n=0) -> Self:
1829    def smooth_mls_2d(self, f=0.2, radius=None, n=0) -> Self:
1830        """
1831        Smooth mesh or points with a `Moving Least Squares` algorithm variant.
1832
1833        The `mesh.pointdata['MLSVariance']` array will contain the residue calculated for each point.
1834        When a radius is specified, points that are isolated will not be moved and will get
1835        a 0 entry in array `mesh.pointdata['MLSValidPoint']`.
1836
1837        Arguments:
1838            f : (float)
1839                smoothing factor - typical range is [0, 2].
1840            radius : (float | array)
1841                radius search in absolute units. Can be single value (float) or sequence
1842                for adaptive smoothing. If set then `f` is ignored.
1843            n : (int)
1844                number of neighbours to be used for the fit.
1845                If set then `f` and `radius` are ignored.
1846
1847        Examples:
1848            - [moving_least_squares2D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares2D.py)
1849            - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py)
1850
1851                ![](https://vedo.embl.es/images/advanced/recosurface.png)
1852        """
1853        coords = self.vertices
1854        ncoords = len(coords)
1855
1856        if n:
1857            Ncp = n
1858            radius = None
1859        elif radius is not None:
1860            Ncp = 1
1861        else:
1862            Ncp = int(ncoords * f / 100)
1863            if Ncp < 4:
1864                vedo.logger.error(f"please choose a f-value higher than {f}")
1865                Ncp = 4
1866
1867        variances, newpts, valid = [], [], []
1868        radius_is_sequence = utils.is_sequence(radius)
1869
1870        pb = None
1871        if ncoords > 10000:
1872            pb = utils.ProgressBar(0, ncoords, delay=3)
1873
1874        for i, p in enumerate(coords):
1875            if pb:
1876                pb.print("smooth_mls_2d working ...")
1877            
1878            # if a radius was provided for each point
1879            if radius_is_sequence:
1880                pts = self.closest_point(p, n=Ncp, radius=radius[i])
1881            else:
1882                pts = self.closest_point(p, n=Ncp, radius=radius)
1883
1884            if len(pts) > 3:
1885                ptsmean = pts.mean(axis=0)  # plane center
1886                _, dd, vv = np.linalg.svd(pts - ptsmean)
1887                cv = np.cross(vv[0], vv[1])
1888                t = (np.dot(cv, ptsmean) - np.dot(cv, p)) / np.dot(cv, cv)
1889                newpts.append(p + cv * t)
1890                variances.append(dd[2])
1891                if radius is not None:
1892                    valid.append(1)
1893            else:
1894                newpts.append(p)
1895                variances.append(0)
1896                if radius is not None:
1897                    valid.append(0)
1898
1899        if radius is not None:
1900            self.pointdata["MLSValidPoint"] = np.array(valid).astype(np.uint8)
1901        self.pointdata["MLSVariance"] = np.array(variances).astype(np.float32)
1902
1903        self.vertices = newpts
1904
1905        self.pipeline = utils.OperationNode("smooth_mls_2d", parents=[self])
1906        return self

Smooth mesh or points with a Moving Least Squares algorithm variant.

The mesh.pointdata['MLSVariance'] array will contain the residue calculated for each point. When a radius is specified, points that are isolated will not be moved and will get a 0 entry in array mesh.pointdata['MLSValidPoint'].

Arguments:
  • f : (float) smoothing factor - typical range is [0, 2].
  • radius : (float | array) radius search in absolute units. Can be single value (float) or sequence for adaptive smoothing. If set then f is ignored.
  • n : (int) number of neighbours to be used for the fit. If set then f and radius are ignored.
Examples:
def smooth_lloyd_2d(self, iterations=2, bounds=None, options='Qbb Qc Qx') -> Self:
1908    def smooth_lloyd_2d(self, iterations=2, bounds=None, options="Qbb Qc Qx") -> Self:
1909        """
1910        Lloyd relaxation of a 2D pointcloud.
1911        
1912        Arguments:
1913            iterations : (int)
1914                number of iterations.
1915            bounds : (list)
1916                bounding box of the domain.
1917            options : (str)
1918                options for the Qhull algorithm.
1919        """
1920        # Credits: https://hatarilabs.com/ih-en/
1921        # tutorial-to-create-a-geospatial-voronoi-sh-mesh-with-python-scipy-and-geopandas
1922        from scipy.spatial import Voronoi as scipy_voronoi
1923
1924        def _constrain_points(points):
1925            # Update any points that have drifted beyond the boundaries of this space
1926            if bounds is not None:
1927                for point in points:
1928                    if point[0] < bounds[0]: point[0] = bounds[0]
1929                    if point[0] > bounds[1]: point[0] = bounds[1]
1930                    if point[1] < bounds[2]: point[1] = bounds[2]
1931                    if point[1] > bounds[3]: point[1] = bounds[3]
1932            return points
1933
1934        def _find_centroid(vertices):
1935            # The equation for the method used here to find the centroid of a
1936            # 2D polygon is given here: https://en.wikipedia.org/wiki/Centroid#Of_a_polygon
1937            area = 0
1938            centroid_x = 0
1939            centroid_y = 0
1940            for i in range(len(vertices) - 1):
1941                step = (vertices[i, 0] * vertices[i + 1, 1]) - (vertices[i + 1, 0] * vertices[i, 1])
1942                centroid_x += (vertices[i, 0] + vertices[i + 1, 0]) * step
1943                centroid_y += (vertices[i, 1] + vertices[i + 1, 1]) * step
1944                area += step
1945            if area:
1946                centroid_x = (1.0 / (3.0 * area)) * centroid_x
1947                centroid_y = (1.0 / (3.0 * area)) * centroid_y
1948            # prevent centroids from escaping bounding box
1949            return _constrain_points([[centroid_x, centroid_y]])[0]
1950
1951        def _relax(voron):
1952            # Moves each point to the centroid of its cell in the voronoi
1953            # map to "relax" the points (i.e. jitter the points so as
1954            # to spread them out within the space).
1955            centroids = []
1956            for idx in voron.point_region:
1957                # the region is a series of indices into voronoi.vertices
1958                # remove point at infinity, designated by index -1
1959                region = [i for i in voron.regions[idx] if i != -1]
1960                # enclose the polygon
1961                region = region + [region[0]]
1962                verts = voron.vertices[region]
1963                # find the centroid of those vertices
1964                centroids.append(_find_centroid(verts))
1965            return _constrain_points(centroids)
1966
1967        if bounds is None:
1968            bounds = self.bounds()
1969
1970        pts = self.vertices[:, (0, 1)]
1971        for i in range(iterations):
1972            vor = scipy_voronoi(pts, qhull_options=options)
1973            _constrain_points(vor.vertices)
1974            pts = _relax(vor)
1975        out = Points(pts)
1976        out.name = "MeshSmoothLloyd2D"
1977        out.pipeline = utils.OperationNode("smooth_lloyd", parents=[self])
1978        return out

Lloyd relaxation of a 2D pointcloud.

Arguments:
  • iterations : (int) number of iterations.
  • bounds : (list) bounding box of the domain.
  • options : (str) options for the Qhull algorithm.
def project_on_plane(self, plane='z', point=None, direction=None) -> Self:
1980    def project_on_plane(self, plane="z", point=None, direction=None) -> Self:
1981        """
1982        Project the mesh on one of the Cartesian planes.
1983
1984        Arguments:
1985            plane : (str, Plane)
1986                if plane is `str`, plane can be one of ['x', 'y', 'z'],
1987                represents x-plane, y-plane and z-plane, respectively.
1988                Otherwise, plane should be an instance of `vedo.shapes.Plane`.
1989            point : (float, array)
1990                if plane is `str`, point should be a float represents the intercept.
1991                Otherwise, point is the camera point of perspective projection
1992            direction : (array)
1993                direction of oblique projection
1994
1995        Note:
1996            Parameters `point` and `direction` are only used if the given plane
1997            is an instance of `vedo.shapes.Plane`. And one of these two params
1998            should be left as `None` to specify the projection type.
1999
2000        Example:
2001            ```python
2002            s.project_on_plane(plane='z') # project to z-plane
2003            plane = Plane(pos=(4, 8, -4), normal=(-1, 0, 1), s=(5,5))
2004            s.project_on_plane(plane=plane)                       # orthogonal projection
2005            s.project_on_plane(plane=plane, point=(6, 6, 6))      # perspective projection
2006            s.project_on_plane(plane=plane, direction=(1, 2, -1)) # oblique projection
2007            ```
2008
2009        Examples:
2010            - [silhouette2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette2.py)
2011
2012                ![](https://vedo.embl.es/images/basic/silhouette2.png)
2013        """
2014        coords = self.vertices
2015
2016        if plane == "x":
2017            coords[:, 0] = self.transform.position[0]
2018            intercept = self.xbounds()[0] if point is None else point
2019            self.x(intercept)
2020        elif plane == "y":
2021            coords[:, 1] = self.transform.position[1]
2022            intercept = self.ybounds()[0] if point is None else point
2023            self.y(intercept)
2024        elif plane == "z":
2025            coords[:, 2] = self.transform.position[2]
2026            intercept = self.zbounds()[0] if point is None else point
2027            self.z(intercept)
2028
2029        elif isinstance(plane, vedo.shapes.Plane):
2030            normal = plane.normal / np.linalg.norm(plane.normal)
2031            pl = np.hstack((normal, -np.dot(plane.pos(), normal))).reshape(4, 1)
2032            if direction is None and point is None:
2033                # orthogonal projection
2034                pt = np.hstack((normal, [0])).reshape(4, 1)
2035                # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T # python3 only
2036                proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T)
2037
2038            elif direction is None:
2039                # perspective projection
2040                pt = np.hstack((np.array(point), [1])).reshape(4, 1)
2041                # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T
2042                proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T)
2043
2044            elif point is None:
2045                # oblique projection
2046                pt = np.hstack((np.array(direction), [0])).reshape(4, 1)
2047                # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T
2048                proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T)
2049
2050            coords = np.concatenate([coords, np.ones((coords.shape[:-1] + (1,)))], axis=-1)
2051            # coords = coords @ proj_mat.T
2052            coords = np.matmul(coords, proj_mat.T)
2053            coords = coords[:, :3] / coords[:, 3:]
2054
2055        else:
2056            vedo.logger.error(f"unknown plane {plane}")
2057            raise RuntimeError()
2058
2059        self.alpha(0.1)
2060        self.vertices = coords
2061        return self

Project the mesh on one of the Cartesian planes.

Arguments:
  • plane : (str, Plane) if plane is str, plane can be one of ['x', 'y', 'z'], represents x-plane, y-plane and z-plane, respectively. Otherwise, plane should be an instance of vedo.shapes.Plane.
  • point : (float, array) if plane is str, point should be a float represents the intercept. Otherwise, point is the camera point of perspective projection
  • direction : (array) direction of oblique projection
Note:

Parameters point and direction are only used if the given plane is an instance of vedo.shapes.Plane. And one of these two params should be left as None to specify the projection type.

Example:
s.project_on_plane(plane='z') # project to z-plane
plane = Plane(pos=(4, 8, -4), normal=(-1, 0, 1), s=(5,5))
s.project_on_plane(plane=plane)                       # orthogonal projection
s.project_on_plane(plane=plane, point=(6, 6, 6))      # perspective projection
s.project_on_plane(plane=plane, direction=(1, 2, -1)) # oblique projection
Examples:
def warp(self, source, target, sigma=1.0, mode='3d') -> Self:
2063    def warp(self, source, target, sigma=1.0, mode="3d") -> Self:
2064        """
2065        "Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set
2066        of source and target landmarks. Any point on the mesh close to a source landmark will
2067        be moved to a place close to the corresponding target landmark.
2068        The points in between are interpolated smoothly using
2069        Bookstein's Thin Plate Spline algorithm.
2070
2071        Transformation object can be accessed with `mesh.transform`.
2072
2073        Arguments:
2074            sigma : (float)
2075                specify the 'stiffness' of the spline.
2076            mode : (str)
2077                set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes)
2078
2079        Examples:
2080            - [interpolate_field.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/interpolate_field.py)
2081            - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py)
2082            - [warp2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp2.py)
2083            - [warp3.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp3.py)
2084            - [warp4a.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4a.py)
2085            - [warp4b.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4b.py)
2086            - [warp6.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp6.py)
2087
2088            ![](https://vedo.embl.es/images/advanced/warp2.png)
2089        """
2090        parents = [self]
2091
2092        try:
2093            source = source.vertices
2094            parents.append(source)
2095        except AttributeError:
2096            source = utils.make3d(source)
2097        
2098        try:
2099            target = target.vertices
2100            parents.append(target)
2101        except AttributeError:
2102            target = utils.make3d(target)
2103
2104        ns = len(source)
2105        nt = len(target)
2106        if ns != nt:
2107            vedo.logger.error(f"#source {ns} != {nt} #target points")
2108            raise RuntimeError()
2109
2110        NLT = NonLinearTransform()
2111        NLT.source_points = source
2112        NLT.target_points = target
2113        self.apply_transform(NLT)
2114
2115        self.pipeline = utils.OperationNode("warp", parents=parents)
2116        return self

"Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set of source and target landmarks. Any point on the mesh close to a source landmark will be moved to a place close to the corresponding target landmark. The points in between are interpolated smoothly using Bookstein's Thin Plate Spline algorithm.

Transformation object can be accessed with mesh.transform.

Arguments:
  • sigma : (float) specify the 'stiffness' of the spline.
  • mode : (str) set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes)
Examples:

def cut_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0), invert=False) -> Self:
2118    def cut_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0), invert=False) -> Self:
2119        """
2120        Cut the mesh with the plane defined by a point and a normal.
2121
2122        Arguments:
2123            origin : (array)
2124                the cutting plane goes through this point
2125            normal : (array)
2126                normal of the cutting plane
2127
2128        Example:
2129            ```python
2130            from vedo import Cube
2131            cube = Cube().cut_with_plane(normal=(1,1,1))
2132            cube.back_color('pink').show().close()
2133            ```
2134            ![](https://vedo.embl.es/images/feats/cut_with_plane_cube.png)
2135
2136        Examples:
2137            - [trail.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/trail.py)
2138
2139                ![](https://vedo.embl.es/images/simulations/trail.gif)
2140
2141        Check out also:
2142            `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`.
2143        """
2144        s = str(normal)
2145        if "x" in s:
2146            normal = (1, 0, 0)
2147            if "-" in s:
2148                normal = -np.array(normal)
2149        elif "y" in s:
2150            normal = (0, 1, 0)
2151            if "-" in s:
2152                normal = -np.array(normal)
2153        elif "z" in s:
2154            normal = (0, 0, 1)
2155            if "-" in s:
2156                normal = -np.array(normal)
2157        plane = vtki.vtkPlane()
2158        plane.SetOrigin(origin)
2159        plane.SetNormal(normal)
2160
2161        clipper = vtki.new("ClipPolyData")
2162        clipper.SetInputData(self.dataset)
2163        clipper.SetClipFunction(plane)
2164        clipper.GenerateClippedOutputOff()
2165        clipper.GenerateClipScalarsOff()
2166        clipper.SetInsideOut(invert)
2167        clipper.SetValue(0)
2168        clipper.Update()
2169
2170        self._update(clipper.GetOutput())
2171
2172        self.pipeline = utils.OperationNode("cut_with_plane", parents=[self])
2173        return self

Cut the mesh with the plane defined by a point and a normal.

Arguments:
  • origin : (array) the cutting plane goes through this point
  • normal : (array) normal of the cutting plane
Example:
from vedo import Cube
cube = Cube().cut_with_plane(normal=(1,1,1))
cube.back_color('pink').show().close()

Examples:
Check out also:

cut_with_box(), cut_with_cylinder(), cut_with_sphere().

def cut_with_planes(self, origins, normals, invert=False) -> Self:
2175    def cut_with_planes(self, origins, normals, invert=False) -> Self:
2176        """
2177        Cut the mesh with a convex set of planes defined by points and normals.
2178
2179        Arguments:
2180            origins : (array)
2181                each cutting plane goes through this point
2182            normals : (array)
2183                normal of each of the cutting planes
2184            invert : (bool)
2185                if True, cut outside instead of inside
2186
2187        Check out also:
2188            `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`
2189        """
2190
2191        vpoints = vtki.vtkPoints()
2192        for p in utils.make3d(origins):
2193            vpoints.InsertNextPoint(p)
2194        normals = utils.make3d(normals)
2195
2196        planes = vtki.vtkPlanes()
2197        planes.SetPoints(vpoints)
2198        planes.SetNormals(utils.numpy2vtk(normals, dtype=float))
2199
2200        clipper = vtki.new("ClipPolyData")
2201        clipper.SetInputData(self.dataset)
2202        clipper.SetInsideOut(invert)
2203        clipper.SetClipFunction(planes)
2204        clipper.GenerateClippedOutputOff()
2205        clipper.GenerateClipScalarsOff()
2206        clipper.SetValue(0)
2207        clipper.Update()
2208
2209        self._update(clipper.GetOutput())
2210
2211        self.pipeline = utils.OperationNode("cut_with_planes", parents=[self])
2212        return self

Cut the mesh with a convex set of planes defined by points and normals.

Arguments:
  • origins : (array) each cutting plane goes through this point
  • normals : (array) normal of each of the cutting planes
  • invert : (bool) if True, cut outside instead of inside
Check out also:

cut_with_box(), cut_with_cylinder(), cut_with_sphere()

def cut_with_box(self, bounds, invert=False) -> Self:
2214    def cut_with_box(self, bounds, invert=False) -> Self:
2215        """
2216        Cut the current mesh with a box or a set of boxes.
2217        This is much faster than `cut_with_mesh()`.
2218
2219        Input `bounds` can be either:
2220        - a Mesh or Points object
2221        - a list of 6 number representing a bounding box `[xmin,xmax, ymin,ymax, zmin,zmax]`
2222        - a list of bounding boxes like the above: `[[xmin1,...], [xmin2,...], ...]`
2223
2224        Example:
2225            ```python
2226            from vedo import Sphere, Cube, show
2227            mesh = Sphere(r=1, res=50)
2228            box  = Cube(side=1.5).wireframe()
2229            mesh.cut_with_box(box)
2230            show(mesh, box, axes=1).close()
2231            ```
2232            ![](https://vedo.embl.es/images/feats/cut_with_box_cube.png)
2233
2234        Check out also:
2235            `cut_with_line()`, `cut_with_plane()`, `cut_with_cylinder()`
2236        """
2237        if isinstance(bounds, Points):
2238            bounds = bounds.bounds()
2239
2240        box = vtki.new("Box")
2241        if utils.is_sequence(bounds[0]):
2242            for bs in bounds:
2243                box.AddBounds(bs)
2244        else:
2245            box.SetBounds(bounds)
2246
2247        clipper = vtki.new("ClipPolyData")
2248        clipper.SetInputData(self.dataset)
2249        clipper.SetClipFunction(box)
2250        clipper.SetInsideOut(not invert)
2251        clipper.GenerateClippedOutputOff()
2252        clipper.GenerateClipScalarsOff()
2253        clipper.SetValue(0)
2254        clipper.Update()
2255        self._update(clipper.GetOutput())
2256
2257        self.pipeline = utils.OperationNode("cut_with_box", parents=[self])
2258        return self

Cut the current mesh with a box or a set of boxes. This is much faster than cut_with_mesh().

Input bounds can be either:

  • a Mesh or Points object
  • a list of 6 number representing a bounding box [xmin,xmax, ymin,ymax, zmin,zmax]
  • a list of bounding boxes like the above: [[xmin1,...], [xmin2,...], ...]
Example:
from vedo import Sphere, Cube, show
mesh = Sphere(r=1, res=50)
box  = Cube(side=1.5).wireframe()
mesh.cut_with_box(box)
show(mesh, box, axes=1).close()

Check out also:

cut_with_line(), cut_with_plane(), cut_with_cylinder()

def cut_with_line(self, points, invert=False, closed=True) -> Self:
2260    def cut_with_line(self, points, invert=False, closed=True) -> Self:
2261        """
2262        Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter.
2263        The polyline is defined by a set of points (z-coordinates are ignored).
2264        This is much faster than `cut_with_mesh()`.
2265
2266        Check out also:
2267            `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()`
2268        """
2269        pplane = vtki.new("PolyPlane")
2270        if isinstance(points, Points):
2271            points = points.vertices.tolist()
2272
2273        if closed:
2274            if isinstance(points, np.ndarray):
2275                points = points.tolist()
2276            points.append(points[0])
2277
2278        vpoints = vtki.vtkPoints()
2279        for p in points:
2280            if len(p) == 2:
2281                p = [p[0], p[1], 0.0]
2282            vpoints.InsertNextPoint(p)
2283
2284        n = len(points)
2285        polyline = vtki.new("PolyLine")
2286        polyline.Initialize(n, vpoints)
2287        polyline.GetPointIds().SetNumberOfIds(n)
2288        for i in range(n):
2289            polyline.GetPointIds().SetId(i, i)
2290        pplane.SetPolyLine(polyline)
2291
2292        clipper = vtki.new("ClipPolyData")
2293        clipper.SetInputData(self.dataset)
2294        clipper.SetClipFunction(pplane)
2295        clipper.SetInsideOut(invert)
2296        clipper.GenerateClippedOutputOff()
2297        clipper.GenerateClipScalarsOff()
2298        clipper.SetValue(0)
2299        clipper.Update()
2300        self._update(clipper.GetOutput())
2301
2302        self.pipeline = utils.OperationNode("cut_with_line", parents=[self])
2303        return self

Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter. The polyline is defined by a set of points (z-coordinates are ignored). This is much faster than cut_with_mesh().

Check out also:

cut_with_box(), cut_with_plane(), cut_with_sphere()

def cut_with_cookiecutter(self, lines) -> Self:
2305    def cut_with_cookiecutter(self, lines) -> Self:
2306        """
2307        Cut the current mesh with a single line or a set of lines.
2308
2309        Input `lines` can be either:
2310        - a `Mesh` or `Points` object
2311        - a list of 3D points: `[(x1,y1,z1), (x2,y2,z2), ...]`
2312        - a list of 2D points: `[(x1,y1), (x2,y2), ...]`
2313
2314        Example:
2315            ```python
2316            from vedo import *
2317            grid = Mesh(dataurl + "dolfin_fine.vtk")
2318            grid.compute_quality().cmap("Greens")
2319            pols = merge(
2320                Polygon(nsides=10, r=0.3).pos(0.7, 0.3),
2321                Polygon(nsides=10, r=0.2).pos(0.3, 0.7),
2322            )
2323            lines = pols.boundaries()
2324            cgrid = grid.clone().cut_with_cookiecutter(lines)
2325            grid.alpha(0.1).wireframe()
2326            show(grid, cgrid, lines, axes=8, bg='blackboard').close()
2327            ```
2328            ![](https://vedo.embl.es/images/feats/cookiecutter.png)
2329
2330        Check out also:
2331            `cut_with_line()` and `cut_with_point_loop()`
2332
2333        Note:
2334            In case of a warning message like:
2335                "Mesh and trim loop point data attributes are different"
2336            consider interpolating the mesh point data to the loop points,
2337            Eg. (in the above example):
2338            ```python
2339            lines = pols.boundaries().interpolate_data_from(grid, n=2)
2340            ```
2341
2342        Note:
2343            trying to invert the selection by reversing the loop order
2344            will have no effect in this method, hence it does not have
2345            the `invert` option.
2346        """
2347        if utils.is_sequence(lines):
2348            lines = utils.make3d(lines)
2349            iline = list(range(len(lines))) + [0]
2350            poly = utils.buildPolyData(lines, lines=[iline])
2351        else:
2352            poly = lines.dataset
2353
2354        # if invert: # not working
2355        #     rev = vtki.new("ReverseSense")
2356        #     rev.ReverseCellsOn()
2357        #     rev.SetInputData(poly)
2358        #     rev.Update()
2359        #     poly = rev.GetOutput()
2360
2361        # Build loops from the polyline
2362        build_loops = vtki.new("ContourLoopExtraction")
2363        build_loops.SetGlobalWarningDisplay(0)
2364        build_loops.SetInputData(poly)
2365        build_loops.Update()
2366        boundary_poly = build_loops.GetOutput()
2367
2368        ccut = vtki.new("CookieCutter")
2369        ccut.SetInputData(self.dataset)
2370        ccut.SetLoopsData(boundary_poly)
2371        ccut.SetPointInterpolationToMeshEdges()
2372        # ccut.SetPointInterpolationToLoopEdges()
2373        ccut.PassCellDataOn()
2374        ccut.PassPointDataOn()
2375        ccut.Update()
2376        self._update(ccut.GetOutput())
2377
2378        self.pipeline = utils.OperationNode("cut_with_cookiecutter", parents=[self])
2379        return self

Cut the current mesh with a single line or a set of lines.

Input lines can be either:

  • a Mesh or Points object
  • a list of 3D points: [(x1,y1,z1), (x2,y2,z2), ...]
  • a list of 2D points: [(x1,y1), (x2,y2), ...]
Example:
from vedo import *
grid = Mesh(dataurl + "dolfin_fine.vtk")
grid.compute_quality().cmap("Greens")
pols = merge(
    Polygon(nsides=10, r=0.3).pos(0.7, 0.3),
    Polygon(nsides=10, r=0.2).pos(0.3, 0.7),
)
lines = pols.boundaries()
cgrid = grid.clone().cut_with_cookiecutter(lines)
grid.alpha(0.1).wireframe()
show(grid, cgrid, lines, axes=8, bg='blackboard').close()

Check out also:

cut_with_line() and cut_with_point_loop()

Note:

In case of a warning message like: "Mesh and trim loop point data attributes are different" consider interpolating the mesh point data to the loop points, Eg. (in the above example):

lines = pols.boundaries().interpolate_data_from(grid, n=2)
Note:

trying to invert the selection by reversing the loop order will have no effect in this method, hence it does not have the invert option.

def cut_with_cylinder(self, center=(0, 0, 0), axis=(0, 0, 1), r=1, invert=False) -> Self:
2381    def cut_with_cylinder(self, center=(0, 0, 0), axis=(0, 0, 1), r=1, invert=False) -> Self:
2382        """
2383        Cut the current mesh with an infinite cylinder.
2384        This is much faster than `cut_with_mesh()`.
2385
2386        Arguments:
2387            center : (array)
2388                the center of the cylinder
2389            normal : (array)
2390                direction of the cylinder axis
2391            r : (float)
2392                radius of the cylinder
2393
2394        Example:
2395            ```python
2396            from vedo import Disc, show
2397            disc = Disc(r1=1, r2=1.2)
2398            mesh = disc.extrude(3, res=50).linewidth(1)
2399            mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True)
2400            show(mesh, axes=1).close()
2401            ```
2402            ![](https://vedo.embl.es/images/feats/cut_with_cylinder.png)
2403
2404        Examples:
2405            - [optics_main1.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/optics_main1.py)
2406
2407        Check out also:
2408            `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()`
2409        """
2410        s = str(axis)
2411        if "x" in s:
2412            axis = (1, 0, 0)
2413        elif "y" in s:
2414            axis = (0, 1, 0)
2415        elif "z" in s:
2416            axis = (0, 0, 1)
2417        cyl = vtki.new("Cylinder")
2418        cyl.SetCenter(center)
2419        cyl.SetAxis(axis[0], axis[1], axis[2])
2420        cyl.SetRadius(r)
2421
2422        clipper = vtki.new("ClipPolyData")
2423        clipper.SetInputData(self.dataset)
2424        clipper.SetClipFunction(cyl)
2425        clipper.SetInsideOut(not invert)
2426        clipper.GenerateClippedOutputOff()
2427        clipper.GenerateClipScalarsOff()
2428        clipper.SetValue(0)
2429        clipper.Update()
2430        self._update(clipper.GetOutput())
2431
2432        self.pipeline = utils.OperationNode("cut_with_cylinder", parents=[self])
2433        return self

Cut the current mesh with an infinite cylinder. This is much faster than cut_with_mesh().

Arguments:
  • center : (array) the center of the cylinder
  • normal : (array) direction of the cylinder axis
  • r : (float) radius of the cylinder
Example:
from vedo import Disc, show
disc = Disc(r1=1, r2=1.2)
mesh = disc.extrude(3, res=50).linewidth(1)
mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True)
show(mesh, axes=1).close()

Examples:
Check out also:

cut_with_box(), cut_with_plane(), cut_with_sphere()

def cut_with_sphere(self, center=(0, 0, 0), r=1.0, invert=False) -> Self:
2435    def cut_with_sphere(self, center=(0, 0, 0), r=1.0, invert=False) -> Self:
2436        """
2437        Cut the current mesh with an sphere.
2438        This is much faster than `cut_with_mesh()`.
2439
2440        Arguments:
2441            center : (array)
2442                the center of the sphere
2443            r : (float)
2444                radius of the sphere
2445
2446        Example:
2447            ```python
2448            from vedo import Disc, show
2449            disc = Disc(r1=1, r2=1.2)
2450            mesh = disc.extrude(3, res=50).linewidth(1)
2451            mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True)
2452            show(mesh, axes=1).close()
2453            ```
2454            ![](https://vedo.embl.es/images/feats/cut_with_sphere.png)
2455
2456        Check out also:
2457            `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()`
2458        """
2459        sph = vtki.new("Sphere")
2460        sph.SetCenter(center)
2461        sph.SetRadius(r)
2462
2463        clipper = vtki.new("ClipPolyData")
2464        clipper.SetInputData(self.dataset)
2465        clipper.SetClipFunction(sph)
2466        clipper.SetInsideOut(not invert)
2467        clipper.GenerateClippedOutputOff()
2468        clipper.GenerateClipScalarsOff()
2469        clipper.SetValue(0)
2470        clipper.Update()
2471        self._update(clipper.GetOutput())
2472        self.pipeline = utils.OperationNode("cut_with_sphere", parents=[self])
2473        return self

Cut the current mesh with an sphere. This is much faster than cut_with_mesh().

Arguments:
  • center : (array) the center of the sphere
  • r : (float) radius of the sphere
Example:
from vedo import Disc, show
disc = Disc(r1=1, r2=1.2)
mesh = disc.extrude(3, res=50).linewidth(1)
mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True)
show(mesh, axes=1).close()

Check out also:

cut_with_box(), cut_with_plane(), cut_with_cylinder()

def cut_with_mesh( self, mesh, invert=False, keep=False) -> Union[Self, vedo.assembly.Assembly]:
2475    def cut_with_mesh(self, mesh, invert=False, keep=False) -> Union[Self, "vedo.Assembly"]:
2476        """
2477        Cut an `Mesh` mesh with another `Mesh`.
2478
2479        Use `invert` to invert the selection.
2480
2481        Use `keep` to keep the cutoff part, in this case an `Assembly` is returned:
2482        the "cut" object and the "discarded" part of the original object.
2483        You can access both via `assembly.unpack()` method.
2484
2485        Example:
2486        ```python
2487        from vedo import *
2488        arr = np.random.randn(100000, 3)/2
2489        pts = Points(arr).c('red3').pos(5,0,0)
2490        cube = Cube().pos(4,0.5,0)
2491        assem = pts.cut_with_mesh(cube, keep=True)
2492        show(assem.unpack(), axes=1).close()
2493        ```
2494        ![](https://vedo.embl.es/images/feats/cut_with_mesh.png)
2495
2496       Check out also:
2497            `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()`
2498       """
2499        polymesh = mesh.dataset
2500        poly = self.dataset
2501
2502        # Create an array to hold distance information
2503        signed_distances = vtki.vtkFloatArray()
2504        signed_distances.SetNumberOfComponents(1)
2505        signed_distances.SetName("SignedDistances")
2506
2507        # implicit function that will be used to slice the mesh
2508        ippd = vtki.new("ImplicitPolyDataDistance")
2509        ippd.SetInput(polymesh)
2510
2511        # Evaluate the signed distance function at all of the grid points
2512        for pointId in range(poly.GetNumberOfPoints()):
2513            p = poly.GetPoint(pointId)
2514            signed_distance = ippd.EvaluateFunction(p)
2515            signed_distances.InsertNextValue(signed_distance)
2516
2517        currentscals = poly.GetPointData().GetScalars()
2518        if currentscals:
2519            currentscals = currentscals.GetName()
2520
2521        poly.GetPointData().AddArray(signed_distances)
2522        poly.GetPointData().SetActiveScalars("SignedDistances")
2523
2524        clipper = vtki.new("ClipPolyData")
2525        clipper.SetInputData(poly)
2526        clipper.SetInsideOut(not invert)
2527        clipper.SetGenerateClippedOutput(keep)
2528        clipper.SetValue(0.0)
2529        clipper.Update()
2530        cpoly = clipper.GetOutput()
2531
2532        if keep:
2533            kpoly = clipper.GetOutput(1)
2534
2535        vis = False
2536        if currentscals:
2537            cpoly.GetPointData().SetActiveScalars(currentscals)
2538            vis = self.mapper.GetScalarVisibility()
2539
2540        self._update(cpoly)
2541
2542        self.pointdata.remove("SignedDistances")
2543        self.mapper.SetScalarVisibility(vis)
2544        if keep:
2545            if isinstance(self, vedo.Mesh):
2546                cutoff = vedo.Mesh(kpoly)
2547            else:
2548                cutoff = vedo.Points(kpoly)
2549            # cutoff = self.__class__(kpoly) # this does not work properly
2550            cutoff.properties = vtki.vtkProperty()
2551            cutoff.properties.DeepCopy(self.properties)
2552            cutoff.actor.SetProperty(cutoff.properties)
2553            cutoff.c("k5").alpha(0.2)
2554            return vedo.Assembly([self, cutoff])
2555
2556        self.pipeline = utils.OperationNode("cut_with_mesh", parents=[self, mesh])
2557        return self

Cut an Mesh mesh with another Mesh.

Use invert to invert the selection.

Use keep to keep the cutoff part, in this case an Assembly is returned: the "cut" object and the "discarded" part of the original object. You can access both via assembly.unpack() method.

Example:

from vedo import *
 arr = np.random.randn(100000, 3)/2
 pts = Points(arr).c('red3').pos(5,0,0)
 cube = Cube().pos(4,0.5,0)
 assem = pts.cut_with_mesh(cube, keep=True)
 show(assem.unpack(), axes=1).close()
 

Check out also:

cut_with_box(), cut_with_plane(), cut_with_cylinder()

def cut_with_point_loop(self, points, invert=False, on='points', include_boundary=False) -> Self:
2559    def cut_with_point_loop(
2560        self, points, invert=False, on="points", include_boundary=False
2561    ) -> Self:
2562        """
2563        Cut an `Mesh` object with a set of points forming a closed loop.
2564
2565        Arguments:
2566            invert : (bool)
2567                invert selection (inside-out)
2568            on : (str)
2569                if 'cells' will extract the whole cells lying inside (or outside) the point loop
2570            include_boundary : (bool)
2571                include cells lying exactly on the boundary line. Only relevant on 'cells' mode
2572
2573        Examples:
2574            - [cut_with_points1.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points1.py)
2575
2576                ![](https://vedo.embl.es/images/advanced/cutWithPoints1.png)
2577
2578            - [cut_with_points2.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points2.py)
2579
2580                ![](https://vedo.embl.es/images/advanced/cutWithPoints2.png)
2581        """
2582        if isinstance(points, Points):
2583            parents = [points]
2584            vpts = points.dataset.GetPoints()
2585            points = points.vertices
2586        else:
2587            parents = [self]
2588            vpts = vtki.vtkPoints()
2589            points = utils.make3d(points)
2590            for p in points:
2591                vpts.InsertNextPoint(p)
2592
2593        if "cell" in on:
2594            ippd = vtki.new("ImplicitSelectionLoop")
2595            ippd.SetLoop(vpts)
2596            ippd.AutomaticNormalGenerationOn()
2597            clipper = vtki.new("ExtractPolyDataGeometry")
2598            clipper.SetInputData(self.dataset)
2599            clipper.SetImplicitFunction(ippd)
2600            clipper.SetExtractInside(not invert)
2601            clipper.SetExtractBoundaryCells(include_boundary)
2602        else:
2603            spol = vtki.new("SelectPolyData")
2604            spol.SetLoop(vpts)
2605            spol.GenerateSelectionScalarsOn()
2606            spol.GenerateUnselectedOutputOff()
2607            spol.SetInputData(self.dataset)
2608            spol.Update()
2609            clipper = vtki.new("ClipPolyData")
2610            clipper.SetInputData(spol.GetOutput())
2611            clipper.SetInsideOut(not invert)
2612            clipper.SetValue(0.0)
2613        clipper.Update()
2614        self._update(clipper.GetOutput())
2615
2616        self.pipeline = utils.OperationNode("cut_with_pointloop", parents=parents)
2617        return self

Cut an Mesh object with a set of points forming a closed loop.

Arguments:
  • invert : (bool) invert selection (inside-out)
  • on : (str) if 'cells' will extract the whole cells lying inside (or outside) the point loop
  • include_boundary : (bool) include cells lying exactly on the boundary line. Only relevant on 'cells' mode
Examples:
def cut_with_scalar(self, value: float, name='', invert=False) -> Self:
2619    def cut_with_scalar(self, value: float, name="", invert=False) -> Self:
2620        """
2621        Cut a mesh or point cloud with some input scalar point-data.
2622
2623        Arguments:
2624            value : (float)
2625                cutting value
2626            name : (str)
2627                array name of the scalars to be used
2628            invert : (bool)
2629                flip selection
2630
2631        Example:
2632            ```python
2633            from vedo import *
2634            s = Sphere().lw(1)
2635            pts = s.vertices
2636            scalars = np.sin(3*pts[:,2]) + pts[:,0]
2637            s.pointdata["somevalues"] = scalars
2638            s.cut_with_scalar(0.3)
2639            s.cmap("Spectral", "somevalues").add_scalarbar()
2640            s.show(axes=1).close()
2641            ```
2642            ![](https://vedo.embl.es/images/feats/cut_with_scalars.png)
2643        """
2644        if name:
2645            self.pointdata.select(name)
2646        clipper = vtki.new("ClipPolyData")
2647        clipper.SetInputData(self.dataset)
2648        clipper.SetValue(value)
2649        clipper.GenerateClippedOutputOff()
2650        clipper.SetInsideOut(not invert)
2651        clipper.Update()
2652        self._update(clipper.GetOutput())
2653        self.pipeline = utils.OperationNode("cut_with_scalar", parents=[self])
2654        return self

Cut a mesh or point cloud with some input scalar point-data.

Arguments:
  • value : (float) cutting value
  • name : (str) array name of the scalars to be used
  • invert : (bool) flip selection
Example:
from vedo import *
s = Sphere().lw(1)
pts = s.vertices
scalars = np.sin(3*pts[:,2]) + pts[:,0]
s.pointdata["somevalues"] = scalars
s.cut_with_scalar(0.3)
s.cmap("Spectral", "somevalues").add_scalarbar()
s.show(axes=1).close()

def crop( self, top=None, bottom=None, right=None, left=None, front=None, back=None, bounds=()) -> Self:
2656    def crop(self,
2657             top=None, bottom=None, right=None, left=None, front=None, back=None,
2658             bounds=()) -> Self:
2659        """
2660        Crop an `Mesh` object.
2661
2662        Arguments:
2663            top : (float)
2664                fraction to crop from the top plane (positive z)
2665            bottom : (float)
2666                fraction to crop from the bottom plane (negative z)
2667            front : (float)
2668                fraction to crop from the front plane (positive y)
2669            back : (float)
2670                fraction to crop from the back plane (negative y)
2671            right : (float)
2672                fraction to crop from the right plane (positive x)
2673            left : (float)
2674                fraction to crop from the left plane (negative x)
2675            bounds : (list)
2676                bounding box of the crop region as `[x0,x1, y0,y1, z0,z1]`
2677
2678        Example:
2679            ```python
2680            from vedo import Sphere
2681            Sphere().crop(right=0.3, left=0.1).show()
2682            ```
2683            ![](https://user-images.githubusercontent.com/32848391/57081955-0ef1e800-6cf6-11e9-99de-b45220939bc9.png)
2684        """
2685        if not len(bounds):
2686            pos = np.array(self.pos())
2687            x0, x1, y0, y1, z0, z1 = self.bounds()
2688            x0, y0, z0 = [x0, y0, z0] - pos
2689            x1, y1, z1 = [x1, y1, z1] - pos
2690
2691            dx, dy, dz = x1 - x0, y1 - y0, z1 - z0
2692            if top:
2693                z1 = z1 - top * dz
2694            if bottom:
2695                z0 = z0 + bottom * dz
2696            if front:
2697                y1 = y1 - front * dy
2698            if back:
2699                y0 = y0 + back * dy
2700            if right:
2701                x1 = x1 - right * dx
2702            if left:
2703                x0 = x0 + left * dx
2704            bounds = (x0, x1, y0, y1, z0, z1)
2705
2706        cu = vtki.new("Box")
2707        cu.SetBounds(bounds)
2708
2709        clipper = vtki.new("ClipPolyData")
2710        clipper.SetInputData(self.dataset)
2711        clipper.SetClipFunction(cu)
2712        clipper.InsideOutOn()
2713        clipper.GenerateClippedOutputOff()
2714        clipper.GenerateClipScalarsOff()
2715        clipper.SetValue(0)
2716        clipper.Update()
2717        self._update(clipper.GetOutput())
2718
2719        self.pipeline = utils.OperationNode(
2720            "crop", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
2721        )
2722        return self

Crop an Mesh object.

Arguments:
  • top : (float) fraction to crop from the top plane (positive z)
  • bottom : (float) fraction to crop from the bottom plane (negative z)
  • front : (float) fraction to crop from the front plane (positive y)
  • back : (float) fraction to crop from the back plane (negative y)
  • right : (float) fraction to crop from the right plane (positive x)
  • left : (float) fraction to crop from the left plane (negative x)
  • bounds : (list) bounding box of the crop region as [x0,x1, y0,y1, z0,z1]
Example:
from vedo import Sphere
Sphere().crop(right=0.3, left=0.1).show()

def generate_surface_halo( self, distance=0.05, res=(50, 50, 50), bounds=(), maxdist=None) -> vedo.mesh.Mesh:
2724    def generate_surface_halo(
2725            self, 
2726            distance=0.05,
2727            res=(50, 50, 50),
2728            bounds=(),
2729            maxdist=None,
2730    ) -> "vedo.Mesh":
2731        """
2732        Generate the surface halo which sits at the specified distance from the input one.
2733
2734        Arguments:
2735            distance : (float)
2736                distance from the input surface
2737            res : (int)
2738                resolution of the surface
2739            bounds : (list)
2740                bounding box of the surface
2741            maxdist : (float)
2742                maximum distance to generate the surface
2743        """
2744        if not bounds:
2745            bounds = self.bounds()
2746
2747        if not maxdist:
2748            maxdist = self.diagonal_size() / 2
2749
2750        imp = vtki.new("ImplicitModeller")
2751        imp.SetInputData(self.dataset)
2752        imp.SetSampleDimensions(res)
2753        if maxdist:
2754            imp.SetMaximumDistance(maxdist)
2755        if len(bounds) == 6:
2756            imp.SetModelBounds(bounds)
2757        contour = vtki.new("ContourFilter")
2758        contour.SetInputConnection(imp.GetOutputPort())
2759        contour.SetValue(0, distance)
2760        contour.Update()
2761        out = vedo.Mesh(contour.GetOutput())
2762        out.c("lightblue").alpha(0.25).lighting("off")
2763        out.pipeline = utils.OperationNode("generate_surface_halo", parents=[self])
2764        return out

Generate the surface halo which sits at the specified distance from the input one.

Arguments:
  • distance : (float) distance from the input surface
  • res : (int) resolution of the surface
  • bounds : (list) bounding box of the surface
  • maxdist : (float) maximum distance to generate the surface
def generate_mesh( self, line_resolution=None, mesh_resolution=None, smooth=0.0, jitter=0.001, grid=None, quads=False, invert=False) -> Self:
2766    def generate_mesh(
2767        self,
2768        line_resolution=None,
2769        mesh_resolution=None,
2770        smooth=0.0,
2771        jitter=0.001,
2772        grid=None,
2773        quads=False,
2774        invert=False,
2775    ) -> Self:
2776        """
2777        Generate a polygonal Mesh from a closed contour line.
2778        If line is not closed it will be closed with a straight segment.
2779
2780        Check also `generate_delaunay2d()`.
2781
2782        Arguments:
2783            line_resolution : (int)
2784                resolution of the contour line. The default is None, in this case
2785                the contour is not resampled.
2786            mesh_resolution : (int)
2787                resolution of the internal triangles not touching the boundary.
2788            smooth : (float)
2789                smoothing of the contour before meshing.
2790            jitter : (float)
2791                add a small noise to the internal points.
2792            grid : (Grid)
2793                manually pass a Grid object. The default is True.
2794            quads : (bool)
2795                generate a mesh of quads instead of triangles.
2796            invert : (bool)
2797                flip the line orientation. The default is False.
2798
2799        Examples:
2800            - [line2mesh_tri.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_tri.py)
2801
2802                ![](https://vedo.embl.es/images/advanced/line2mesh_tri.jpg)
2803
2804            - [line2mesh_quads.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_quads.py)
2805
2806                ![](https://vedo.embl.es/images/advanced/line2mesh_quads.png)
2807        """
2808        if line_resolution is None:
2809            contour = vedo.shapes.Line(self.vertices)
2810        else:
2811            contour = vedo.shapes.Spline(self.vertices, smooth=smooth, res=line_resolution)
2812        contour.clean()
2813
2814        length = contour.length()
2815        density = length / contour.npoints
2816        # print(f"tomesh():\n\tline length = {length}")
2817        # print(f"\tdensity = {density} length/pt_separation")
2818
2819        x0, x1 = contour.xbounds()
2820        y0, y1 = contour.ybounds()
2821
2822        if grid is None:
2823            if mesh_resolution is None:
2824                resx = int((x1 - x0) / density + 0.5)
2825                resy = int((y1 - y0) / density + 0.5)
2826                # print(f"tmesh_resolution = {[resx, resy]}")
2827            else:
2828                if utils.is_sequence(mesh_resolution):
2829                    resx, resy = mesh_resolution
2830                else:
2831                    resx, resy = mesh_resolution, mesh_resolution
2832            grid = vedo.shapes.Grid(
2833                [(x0 + x1) / 2, (y0 + y1) / 2, 0],
2834                s=((x1 - x0) * 1.025, (y1 - y0) * 1.025),
2835                res=(resx, resy),
2836            )
2837        else:
2838            grid = grid.clone()
2839
2840        cpts = contour.vertices
2841
2842        # make sure it's closed
2843        p0, p1 = cpts[0], cpts[-1]
2844        nj = max(2, int(utils.mag(p1 - p0) / density + 0.5))
2845        joinline = vedo.shapes.Line(p1, p0, res=nj)
2846        contour = vedo.merge(contour, joinline).subsample(0.0001)
2847
2848        ####################################### quads
2849        if quads:
2850            cmesh = grid.clone().cut_with_point_loop(contour, on="cells", invert=invert)
2851            cmesh.wireframe(False).lw(0.5)
2852            cmesh.pipeline = utils.OperationNode(
2853                "generate_mesh",
2854                parents=[self, contour],
2855                comment=f"#quads {cmesh.dataset.GetNumberOfCells()}",
2856            )
2857            return cmesh
2858        #############################################
2859
2860        grid_tmp = grid.vertices.copy()
2861
2862        if jitter:
2863            np.random.seed(0)
2864            sigma = 1.0 / np.sqrt(grid.npoints) * grid.diagonal_size() * jitter
2865            # print(f"\tsigma jittering = {sigma}")
2866            grid_tmp += np.random.rand(grid.npoints, 3) * sigma
2867            grid_tmp[:, 2] = 0.0
2868
2869        todel = []
2870        density /= np.sqrt(3)
2871        vgrid_tmp = Points(grid_tmp)
2872
2873        for p in contour.vertices:
2874            out = vgrid_tmp.closest_point(p, radius=density, return_point_id=True)
2875            todel += out.tolist()
2876
2877        grid_tmp = grid_tmp.tolist()
2878        for index in sorted(list(set(todel)), reverse=True):
2879            del grid_tmp[index]
2880
2881        points = contour.vertices.tolist() + grid_tmp
2882        if invert:
2883            boundary = list(reversed(range(contour.npoints)))
2884        else:
2885            boundary = list(range(contour.npoints))
2886
2887        dln = Points(points).generate_delaunay2d(mode="xy", boundaries=[boundary])
2888        dln.compute_normals(points=False)  # fixes reversd faces
2889        dln.lw(1)
2890
2891        dln.pipeline = utils.OperationNode(
2892            "generate_mesh",
2893            parents=[self, contour],
2894            comment=f"#cells {dln.dataset.GetNumberOfCells()}",
2895        )
2896        return dln

Generate a polygonal Mesh from a closed contour line. If line is not closed it will be closed with a straight segment.

Check also generate_delaunay2d().

Arguments:
  • line_resolution : (int) resolution of the contour line. The default is None, in this case the contour is not resampled.
  • mesh_resolution : (int) resolution of the internal triangles not touching the boundary.
  • smooth : (float) smoothing of the contour before meshing.
  • jitter : (float) add a small noise to the internal points.
  • grid : (Grid) manually pass a Grid object. The default is True.
  • quads : (bool) generate a mesh of quads instead of triangles.
  • invert : (bool) flip the line orientation. The default is False.
Examples:
def reconstruct_surface( self, dims=(100, 100, 100), radius=None, sample_size=None, hole_filling=True, bounds=(), padding=0.05) -> vedo.mesh.Mesh:
2898    def reconstruct_surface(
2899        self,
2900        dims=(100, 100, 100),
2901        radius=None,
2902        sample_size=None,
2903        hole_filling=True,
2904        bounds=(),
2905        padding=0.05,
2906    ) -> "vedo.Mesh":
2907        """
2908        Surface reconstruction from a scattered cloud of points.
2909
2910        Arguments:
2911            dims : (int)
2912                number of voxels in x, y and z to control precision.
2913            radius : (float)
2914                radius of influence of each point.
2915                Smaller values generally improve performance markedly.
2916                Note that after the signed distance function is computed,
2917                any voxel taking on the value >= radius
2918                is presumed to be "unseen" or uninitialized.
2919            sample_size : (int)
2920                if normals are not present
2921                they will be calculated using this sample size per point.
2922            hole_filling : (bool)
2923                enables hole filling, this generates
2924                separating surfaces between the empty and unseen portions of the volume.
2925            bounds : (list)
2926                region in space in which to perform the sampling
2927                in format (xmin,xmax, ymin,ymax, zim, zmax)
2928            padding : (float)
2929                increase by this fraction the bounding box
2930
2931        Examples:
2932            - [recosurface.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/recosurface.py)
2933
2934                ![](https://vedo.embl.es/images/advanced/recosurface.png)
2935        """
2936        if not utils.is_sequence(dims):
2937            dims = (dims, dims, dims)
2938
2939        sdf = vtki.new("SignedDistance")
2940
2941        if len(bounds) == 6:
2942            sdf.SetBounds(bounds)
2943        else:
2944            x0, x1, y0, y1, z0, z1 = self.bounds()
2945            sdf.SetBounds(
2946                x0 - (x1 - x0) * padding,
2947                x1 + (x1 - x0) * padding,
2948                y0 - (y1 - y0) * padding,
2949                y1 + (y1 - y0) * padding,
2950                z0 - (z1 - z0) * padding,
2951                z1 + (z1 - z0) * padding,
2952            )
2953        
2954        bb = sdf.GetBounds()
2955        if bb[0]==bb[1]:
2956            vedo.logger.warning("reconstruct_surface(): zero x-range")
2957        if bb[2]==bb[3]:
2958            vedo.logger.warning("reconstruct_surface(): zero y-range")
2959        if bb[4]==bb[5]:
2960            vedo.logger.warning("reconstruct_surface(): zero z-range")
2961
2962        pd = self.dataset
2963
2964        if pd.GetPointData().GetNormals():
2965            sdf.SetInputData(pd)
2966        else:
2967            normals = vtki.new("PCANormalEstimation")
2968            normals.SetInputData(pd)
2969            if not sample_size:
2970                sample_size = int(pd.GetNumberOfPoints() / 50)
2971            normals.SetSampleSize(sample_size)
2972            normals.SetNormalOrientationToGraphTraversal()
2973            sdf.SetInputConnection(normals.GetOutputPort())
2974            # print("Recalculating normals with sample size =", sample_size)
2975
2976        if radius is None:
2977            radius = self.diagonal_size() / (sum(dims) / 3) * 5
2978            # print("Calculating mesh from points with radius =", radius)
2979
2980        sdf.SetRadius(radius)
2981        sdf.SetDimensions(dims)
2982        sdf.Update()
2983
2984        surface = vtki.new("ExtractSurface")
2985        surface.SetRadius(radius * 0.99)
2986        surface.SetHoleFilling(hole_filling)
2987        surface.ComputeNormalsOff()
2988        surface.ComputeGradientsOff()
2989        surface.SetInputConnection(sdf.GetOutputPort())
2990        surface.Update()
2991        m = vedo.mesh.Mesh(surface.GetOutput(), c=self.color())
2992
2993        m.pipeline = utils.OperationNode(
2994            "reconstruct_surface",
2995            parents=[self],
2996            comment=f"#pts {m.dataset.GetNumberOfPoints()}",
2997        )
2998        return m

Surface reconstruction from a scattered cloud of points.

Arguments:
  • dims : (int) number of voxels in x, y and z to control precision.
  • radius : (float) radius of influence of each point. Smaller values generally improve performance markedly. Note that after the signed distance function is computed, any voxel taking on the value >= radius is presumed to be "unseen" or uninitialized.
  • sample_size : (int) if normals are not present they will be calculated using this sample size per point.
  • hole_filling : (bool) enables hole filling, this generates separating surfaces between the empty and unseen portions of the volume.
  • bounds : (list) region in space in which to perform the sampling in format (xmin,xmax, ymin,ymax, zim, zmax)
  • padding : (float) increase by this fraction the bounding box
Examples:
def compute_clustering(self, radius: float) -> Self:
3000    def compute_clustering(self, radius: float) -> Self:
3001        """
3002        Cluster points in space. The `radius` is the radius of local search.
3003        
3004        An array named "ClusterId" is added to `pointdata`.
3005
3006        Examples:
3007            - [clustering.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/clustering.py)
3008
3009                ![](https://vedo.embl.es/images/basic/clustering.png)
3010        """
3011        cluster = vtki.new("EuclideanClusterExtraction")
3012        cluster.SetInputData(self.dataset)
3013        cluster.SetExtractionModeToAllClusters()
3014        cluster.SetRadius(radius)
3015        cluster.ColorClustersOn()
3016        cluster.Update()
3017        idsarr = cluster.GetOutput().GetPointData().GetArray("ClusterId")
3018        self.dataset.GetPointData().AddArray(idsarr)
3019        self.pipeline = utils.OperationNode(
3020            "compute_clustering", parents=[self], comment=f"radius = {radius}"
3021        )
3022        return self

Cluster points in space. The radius is the radius of local search.

An array named "ClusterId" is added to pointdata.

Examples:
def compute_connections( self, radius, mode=0, regions=(), vrange=(0, 1), seeds=(), angle=0.0) -> Self:
3024    def compute_connections(self, radius, mode=0, regions=(), vrange=(0, 1), seeds=(), angle=0.0) -> Self:
3025        """
3026        Extracts and/or segments points from a point cloud based on geometric distance measures
3027        (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range.
3028        The default operation is to segment the points into "connected" regions where the connection
3029        is determined by an appropriate distance measure. Each region is given a region id.
3030
3031        Optionally, the filter can output the largest connected region of points; a particular region
3032        (via id specification); those regions that are seeded using a list of input point ids;
3033        or the region of points closest to a specified position.
3034
3035        The key parameter of this filter is the radius defining a sphere around each point which defines
3036        a local neighborhood: any other points in the local neighborhood are assumed connected to the point.
3037        Note that the radius is defined in absolute terms.
3038
3039        Other parameters are used to further qualify what it means to be a neighboring point.
3040        For example, scalar range and/or point normals can be used to further constrain the neighborhood.
3041        Also the extraction mode defines how the filter operates.
3042        By default, all regions are extracted but it is possible to extract particular regions;
3043        the region closest to a seed point; seeded regions; or the largest region found while processing.
3044        By default, all regions are extracted.
3045
3046        On output, all points are labeled with a region number.
3047        However note that the number of input and output points may not be the same:
3048        if not extracting all regions then the output size may be less than the input size.
3049
3050        Arguments:
3051            radius : (float)
3052                variable specifying a local sphere used to define local point neighborhood
3053            mode : (int)
3054                - 0,  Extract all regions
3055                - 1,  Extract point seeded regions
3056                - 2,  Extract largest region
3057                - 3,  Test specified regions
3058                - 4,  Extract all regions with scalar connectivity
3059                - 5,  Extract point seeded regions
3060            regions : (list)
3061                a list of non-negative regions id to extract
3062            vrange : (list)
3063                scalar range to use to extract points based on scalar connectivity
3064            seeds : (list)
3065                a list of non-negative point seed ids
3066            angle : (list)
3067                points are connected if the angle between their normals is
3068                within this angle threshold (expressed in degrees).
3069        """
3070        # https://vtk.org/doc/nightly/html/classvtkConnectedPointsFilter.html
3071        cpf = vtki.new("ConnectedPointsFilter")
3072        cpf.SetInputData(self.dataset)
3073        cpf.SetRadius(radius)
3074        if mode == 0:  # Extract all regions
3075            pass
3076
3077        elif mode == 1:  # Extract point seeded regions
3078            cpf.SetExtractionModeToPointSeededRegions()
3079            for s in seeds:
3080                cpf.AddSeed(s)
3081
3082        elif mode == 2:  # Test largest region
3083            cpf.SetExtractionModeToLargestRegion()
3084
3085        elif mode == 3:  # Test specified regions
3086            cpf.SetExtractionModeToSpecifiedRegions()
3087            for r in regions:
3088                cpf.AddSpecifiedRegion(r)
3089
3090        elif mode == 4:  # Extract all regions with scalar connectivity
3091            cpf.SetExtractionModeToLargestRegion()
3092            cpf.ScalarConnectivityOn()
3093            cpf.SetScalarRange(vrange[0], vrange[1])
3094
3095        elif mode == 5:  # Extract point seeded regions
3096            cpf.SetExtractionModeToLargestRegion()
3097            cpf.ScalarConnectivityOn()
3098            cpf.SetScalarRange(vrange[0], vrange[1])
3099            cpf.AlignedNormalsOn()
3100            cpf.SetNormalAngle(angle)
3101
3102        cpf.Update()
3103        self._update(cpf.GetOutput(), reset_locators=False)
3104        return self

Extracts and/or segments points from a point cloud based on geometric distance measures (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range. The default operation is to segment the points into "connected" regions where the connection is determined by an appropriate distance measure. Each region is given a region id.

Optionally, the filter can output the largest connected region of points; a particular region (via id specification); those regions that are seeded using a list of input point ids; or the region of points closest to a specified position.

The key parameter of this filter is the radius defining a sphere around each point which defines a local neighborhood: any other points in the local neighborhood are assumed connected to the point. Note that the radius is defined in absolute terms.

Other parameters are used to further qualify what it means to be a neighboring point. For example, scalar range and/or point normals can be used to further constrain the neighborhood. Also the extraction mode defines how the filter operates. By default, all regions are extracted but it is possible to extract particular regions; the region closest to a seed point; seeded regions; or the largest region found while processing. By default, all regions are extracted.

On output, all points are labeled with a region number. However note that the number of input and output points may not be the same: if not extracting all regions then the output size may be less than the input size.

Arguments:
  • radius : (float) variable specifying a local sphere used to define local point neighborhood
  • mode : (int)
    • 0, Extract all regions
    • 1, Extract point seeded regions
    • 2, Extract largest region
    • 3, Test specified regions
    • 4, Extract all regions with scalar connectivity
    • 5, Extract point seeded regions
  • regions : (list) a list of non-negative regions id to extract
  • vrange : (list) scalar range to use to extract points based on scalar connectivity
  • seeds : (list) a list of non-negative point seed ids
  • angle : (list) points are connected if the angle between their normals is within this angle threshold (expressed in degrees).
def compute_camera_distance(self) -> numpy.ndarray:
3106    def compute_camera_distance(self) -> np.ndarray:
3107        """
3108        Calculate the distance from points to the camera.
3109        
3110        A pointdata array is created with name 'DistanceToCamera' and returned.
3111        """
3112        if vedo.plotter_instance and vedo.plotter_instance.renderer:
3113            poly = self.dataset
3114            dc = vtki.new("DistanceToCamera")
3115            dc.SetInputData(poly)
3116            dc.SetRenderer(vedo.plotter_instance.renderer)
3117            dc.Update()
3118            self._update(dc.GetOutput(), reset_locators=False)
3119            return self.pointdata["DistanceToCamera"]
3120        return np.array([])

Calculate the distance from points to the camera.

A pointdata array is created with name 'DistanceToCamera' and returned.

def densify( self, target_distance=0.1, nclosest=6, radius=None, niter=1, nmax=None) -> Self:
3122    def densify(self, target_distance=0.1, nclosest=6, radius=None, niter=1, nmax=None) -> Self:
3123        """
3124        Return a copy of the cloud with new added points.
3125        The new points are created in such a way that all points in any local neighborhood are
3126        within a target distance of one another.
3127
3128        For each input point, the distance to all points in its neighborhood is computed.
3129        If any of its neighbors is further than the target distance,
3130        the edge connecting the point and its neighbor is bisected and
3131        a new point is inserted at the bisection point.
3132        A single pass is completed once all the input points are visited.
3133        Then the process repeats to the number of iterations.
3134
3135        Examples:
3136            - [densifycloud.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/densifycloud.py)
3137
3138                ![](https://vedo.embl.es/images/volumetric/densifycloud.png)
3139
3140        .. note::
3141            Points will be created in an iterative fashion until all points in their
3142            local neighborhood are the target distance apart or less.
3143            Note that the process may terminate early due to the
3144            number of iterations. By default the target distance is set to 0.5.
3145            Note that the target_distance should be less than the radius
3146            or nothing will change on output.
3147
3148        .. warning::
3149            This class can generate a lot of points very quickly.
3150            The maximum number of iterations is by default set to =1.0 for this reason.
3151            Increase the number of iterations very carefully.
3152            Also, `nmax` can be set to limit the explosion of points.
3153            It is also recommended that a N closest neighborhood is used.
3154
3155        """
3156        src = vtki.new("ProgrammableSource")
3157        opts = self.vertices
3158        # zeros = np.zeros(3)
3159
3160        def _read_points():
3161            output = src.GetPolyDataOutput()
3162            points = vtki.vtkPoints()
3163            for p in opts:
3164                # print(p)
3165                # if not np.array_equal(p, zeros):
3166                points.InsertNextPoint(p)
3167            output.SetPoints(points)
3168
3169        src.SetExecuteMethod(_read_points)
3170
3171        dens = vtki.new("DensifyPointCloudFilter")
3172        dens.SetInputConnection(src.GetOutputPort())
3173        # dens.SetInputData(self.dataset) # this does not work
3174        dens.InterpolateAttributeDataOn()
3175        dens.SetTargetDistance(target_distance)
3176        dens.SetMaximumNumberOfIterations(niter)
3177        if nmax:
3178            dens.SetMaximumNumberOfPoints(nmax)
3179
3180        if radius:
3181            dens.SetNeighborhoodTypeToRadius()
3182            dens.SetRadius(radius)
3183        elif nclosest:
3184            dens.SetNeighborhoodTypeToNClosest()
3185            dens.SetNumberOfClosestPoints(nclosest)
3186        else:
3187            vedo.logger.error("set either radius or nclosest")
3188            raise RuntimeError()
3189        dens.Update()
3190
3191        cld = Points(dens.GetOutput())
3192        cld.copy_properties_from(self)
3193        cld.interpolate_data_from(self, n=nclosest, radius=radius)
3194        cld.name = "DensifiedCloud"
3195        cld.pipeline = utils.OperationNode(
3196            "densify",
3197            parents=[self],
3198            c="#e9c46a:",
3199            comment=f"#pts {cld.dataset.GetNumberOfPoints()}",
3200        )
3201        return cld

Return a copy of the cloud with new added points. The new points are created in such a way that all points in any local neighborhood are within a target distance of one another.

For each input point, the distance to all points in its neighborhood is computed. If any of its neighbors is further than the target distance, the edge connecting the point and its neighbor is bisected and a new point is inserted at the bisection point. A single pass is completed once all the input points are visited. Then the process repeats to the number of iterations.

Examples:

Points will be created in an iterative fashion until all points in their local neighborhood are the target distance apart or less. Note that the process may terminate early due to the number of iterations. By default the target distance is set to 0.5. Note that the target_distance should be less than the radius or nothing will change on output.

This class can generate a lot of points very quickly. The maximum number of iterations is by default set to =1.0 for this reason. Increase the number of iterations very carefully. Also, nmax can be set to limit the explosion of points. It is also recommended that a N closest neighborhood is used.

def density( self, dims=(40, 40, 40), bounds=None, radius=None, compute_gradient=False, locator=None) -> vedo.volume.Volume:
3207    def density(
3208        self, dims=(40, 40, 40), bounds=None, radius=None, compute_gradient=False, locator=None
3209    ) -> "vedo.Volume":
3210        """
3211        Generate a density field from a point cloud. Input can also be a set of 3D coordinates.
3212        Output is a `Volume`.
3213
3214        The local neighborhood is specified as the `radius` around each sample position (each voxel).
3215        If left to None, the radius is automatically computed as the diagonal of the bounding box
3216        and can be accessed via `vol.metadata["radius"]`.
3217        The density is expressed as the number of counts in the radius search.
3218
3219        Arguments:
3220            dims : (int, list)
3221                number of voxels in x, y and z of the output Volume.
3222            compute_gradient : (bool)
3223                Turn on/off the generation of the gradient vector,
3224                gradient magnitude scalar, and function classification scalar.
3225                By default this is off. Note that this will increase execution time
3226                and the size of the output. (The names of these point data arrays are:
3227                "Gradient", "Gradient Magnitude", and "Classification")
3228            locator : (vtkPointLocator)
3229                can be assigned from a previous call for speed (access it via `object.point_locator`).
3230
3231        Examples:
3232            - [plot_density3d.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/plot_density3d.py)
3233
3234                ![](https://vedo.embl.es/images/pyplot/plot_density3d.png)
3235        """
3236        pdf = vtki.new("PointDensityFilter")
3237        pdf.SetInputData(self.dataset)
3238
3239        if not utils.is_sequence(dims):
3240            dims = [dims, dims, dims]
3241
3242        if bounds is None:
3243            bounds = list(self.bounds())
3244        elif len(bounds) == 4:
3245            bounds = [*bounds, 0, 0]
3246
3247        if bounds[5] - bounds[4] == 0 or len(dims) == 2:  # its 2D
3248            dims = list(dims)
3249            dims = [dims[0], dims[1], 2]
3250            diag = self.diagonal_size()
3251            bounds[5] = bounds[4] + diag / 1000
3252        pdf.SetModelBounds(bounds)
3253
3254        pdf.SetSampleDimensions(dims)
3255
3256        if locator:
3257            pdf.SetLocator(locator)
3258
3259        pdf.SetDensityEstimateToFixedRadius()
3260        if radius is None:
3261            radius = self.diagonal_size() / 20
3262        pdf.SetRadius(radius)
3263        pdf.SetComputeGradient(compute_gradient)
3264        pdf.Update()
3265
3266        vol = vedo.Volume(pdf.GetOutput()).mode(1)
3267        vol.name = "PointDensity"
3268        vol.metadata["radius"] = radius
3269        vol.locator = pdf.GetLocator()
3270        vol.pipeline = utils.OperationNode(
3271            "density", parents=[self], comment=f"dims={tuple(vol.dimensions())}"
3272        )
3273        return vol

Generate a density field from a point cloud. Input can also be a set of 3D coordinates. Output is a Volume.

The local neighborhood is specified as the radius around each sample position (each voxel). If left to None, the radius is automatically computed as the diagonal of the bounding box and can be accessed via vol.metadata["radius"]. The density is expressed as the number of counts in the radius search.

Arguments:
  • dims : (int, list) number of voxels in x, y and z of the output Volume.
  • compute_gradient : (bool) Turn on/off the generation of the gradient vector, gradient magnitude scalar, and function classification scalar. By default this is off. Note that this will increase execution time and the size of the output. (The names of these point data arrays are: "Gradient", "Gradient Magnitude", and "Classification")
  • locator : (vtkPointLocator) can be assigned from a previous call for speed (access it via object.point_locator).
Examples:
def tovolume( self, kernel='shepard', radius=None, n=None, bounds=None, null_value=None, dims=(25, 25, 25)) -> vedo.volume.Volume:
3276    def tovolume(
3277        self,
3278        kernel="shepard",
3279        radius=None,
3280        n=None,
3281        bounds=None,
3282        null_value=None,
3283        dims=(25, 25, 25),
3284    ) -> "vedo.Volume":
3285        """
3286        Generate a `Volume` by interpolating a scalar
3287        or vector field which is only known on a scattered set of points or mesh.
3288        Available interpolation kernels are: shepard, gaussian, or linear.
3289
3290        Arguments:
3291            kernel : (str)
3292                interpolation kernel type [shepard]
3293            radius : (float)
3294                radius of the local search
3295            n : (int)
3296                number of point to use for interpolation
3297            bounds : (list)
3298                bounding box of the output Volume object
3299            dims : (list)
3300                dimensions of the output Volume object
3301            null_value : (float)
3302                value to be assigned to invalid points
3303
3304        Examples:
3305            - [interpolate_volume.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/interpolate_volume.py)
3306
3307                ![](https://vedo.embl.es/images/volumetric/59095175-1ec5a300-8918-11e9-8bc0-fd35c8981e2b.jpg)
3308        """
3309        if radius is None and not n:
3310            vedo.logger.error("please set either radius or n")
3311            raise RuntimeError
3312
3313        poly = self.dataset
3314
3315        # Create a probe volume
3316        probe = vtki.vtkImageData()
3317        probe.SetDimensions(dims)
3318        if bounds is None:
3319            bounds = self.bounds()
3320        probe.SetOrigin(bounds[0], bounds[2], bounds[4])
3321        probe.SetSpacing(
3322            (bounds[1] - bounds[0]) / dims[0],
3323            (bounds[3] - bounds[2]) / dims[1],
3324            (bounds[5] - bounds[4]) / dims[2],
3325        )
3326
3327        if not self.point_locator:
3328            self.point_locator = vtki.new("PointLocator")
3329            self.point_locator.SetDataSet(poly)
3330            self.point_locator.BuildLocator()
3331
3332        if kernel == "shepard":
3333            kern = vtki.new("ShepardKernel")
3334            kern.SetPowerParameter(2)
3335        elif kernel == "gaussian":
3336            kern = vtki.new("GaussianKernel")
3337        elif kernel == "linear":
3338            kern = vtki.new("LinearKernel")
3339        else:
3340            vedo.logger.error("Error in tovolume(), available kernels are:")
3341            vedo.logger.error(" [shepard, gaussian, linear]")
3342            raise RuntimeError()
3343
3344        if radius:
3345            kern.SetRadius(radius)
3346
3347        interpolator = vtki.new("PointInterpolator")
3348        interpolator.SetInputData(probe)
3349        interpolator.SetSourceData(poly)
3350        interpolator.SetKernel(kern)
3351        interpolator.SetLocator(self.point_locator)
3352
3353        if n:
3354            kern.SetNumberOfPoints(n)
3355            kern.SetKernelFootprintToNClosest()
3356        else:
3357            kern.SetRadius(radius)
3358
3359        if null_value is not None:
3360            interpolator.SetNullValue(null_value)
3361        else:
3362            interpolator.SetNullPointsStrategyToClosestPoint()
3363        interpolator.Update()
3364
3365        vol = vedo.Volume(interpolator.GetOutput())
3366
3367        vol.pipeline = utils.OperationNode(
3368            "signed_distance",
3369            parents=[self],
3370            comment=f"dims={tuple(vol.dimensions())}",
3371            c="#e9c46a:#0096c7",
3372        )
3373        return vol

Generate a Volume by interpolating a scalar or vector field which is only known on a scattered set of points or mesh. Available interpolation kernels are: shepard, gaussian, or linear.

Arguments:
  • kernel : (str) interpolation kernel type [shepard]
  • radius : (float) radius of the local search
  • n : (int) number of point to use for interpolation
  • bounds : (list) bounding box of the output Volume object
  • dims : (list) dimensions of the output Volume object
  • null_value : (float) value to be assigned to invalid points
Examples:
def generate_segments(self, istart=0, rmax=1e+30, niter=3) -> vedo.shapes.Lines:
3376    def generate_segments(self, istart=0, rmax=1e30, niter=3) -> "vedo.shapes.Lines":
3377        """
3378        Generate a line segments from a set of points.
3379        The algorithm is based on the closest point search.
3380
3381        Returns a `Line` object.
3382        This object contains the a metadata array of used vertex counts in "UsedVertexCount"
3383        and the sum of the length of the segments in "SegmentsLengthSum".
3384
3385        Arguments:
3386            istart : (int)
3387                index of the starting point
3388            rmax : (float)
3389                maximum length of a segment
3390            niter : (int)
3391                number of iterations or passes through the points
3392
3393        Examples:
3394            - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py)
3395        """
3396        points = self.vertices
3397        segments = []
3398        dists = []
3399        n = len(points)
3400        used = np.zeros(n, dtype=int)
3401        for _ in range(niter):
3402            i = istart
3403            for _ in range(n):
3404                p = points[i]
3405                ids = self.closest_point(p, n=4, return_point_id=True)
3406                j = ids[1]
3407                if used[j] > 1 or [j, i] in segments:
3408                    j = ids[2]
3409                if used[j] > 1:
3410                    j = ids[3]
3411                d = np.linalg.norm(p - points[j])
3412                if used[j] > 1 or used[i] > 1 or d > rmax:
3413                    i += 1
3414                    if i >= n:
3415                        i = 0
3416                    continue
3417                used[i] += 1
3418                used[j] += 1
3419                segments.append([i, j])
3420                dists.append(d)
3421                i = j
3422        segments = np.array(segments, dtype=int)
3423
3424        lines = vedo.shapes.Lines(points[segments], c="k", lw=3)
3425        lines.metadata["UsedVertexCount"] = used
3426        lines.metadata["SegmentsLengthSum"] = np.sum(dists)
3427        lines.pipeline = utils.OperationNode("generate_segments", parents=[self])
3428        lines.name = "Segments"
3429        return lines

Generate a line segments from a set of points. The algorithm is based on the closest point search.

Returns a Line object. This object contains the a metadata array of used vertex counts in "UsedVertexCount" and the sum of the length of the segments in "SegmentsLengthSum".

Arguments:
  • istart : (int) index of the starting point
  • rmax : (float) maximum length of a segment
  • niter : (int) number of iterations or passes through the points
Examples:
def generate_delaunay2d( self, mode='scipy', boundaries=(), tol=None, alpha=0.0, offset=0.0, transform=None) -> vedo.mesh.Mesh:
3431    def generate_delaunay2d(
3432        self,
3433        mode="scipy",
3434        boundaries=(),
3435        tol=None,
3436        alpha=0.0,
3437        offset=0.0,
3438        transform=None,
3439    ) -> "vedo.mesh.Mesh":
3440        """
3441        Create a mesh from points in the XY plane.
3442        If `mode='fit'` then the filter computes a best fitting
3443        plane and projects the points onto it.
3444
3445        Check also `generate_mesh()`.
3446
3447        Arguments:
3448            tol : (float)
3449                specify a tolerance to control discarding of closely spaced points.
3450                This tolerance is specified as a fraction of the diagonal length of the bounding box of the points.
3451            alpha : (float)
3452                for a non-zero alpha value, only edges or triangles contained
3453                within a sphere centered at mesh vertices will be output.
3454                Otherwise, only triangles will be output.
3455            offset : (float)
3456                multiplier to control the size of the initial, bounding Delaunay triangulation.
3457            transform: (LinearTransform, NonLinearTransform)
3458                a transformation which is applied to points to generate a 2D problem.
3459                This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane.
3460                The points are transformed and triangulated.
3461                The topology of triangulated points is used as the output topology.
3462
3463        Examples:
3464            - [delaunay2d.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delaunay2d.py)
3465
3466                ![](https://vedo.embl.es/images/basic/delaunay2d.png)
3467        """
3468        plist = self.vertices.copy()
3469
3470        #########################################################
3471        if mode == "scipy":
3472            from scipy.spatial import Delaunay as scipy_delaunay
3473
3474            tri = scipy_delaunay(plist[:, 0:2])
3475            return vedo.mesh.Mesh([plist, tri.simplices])
3476        ##########################################################
3477
3478        pd = vtki.vtkPolyData()
3479        vpts = vtki.vtkPoints()
3480        vpts.SetData(utils.numpy2vtk(plist, dtype=np.float32))
3481        pd.SetPoints(vpts)
3482
3483        delny = vtki.new("Delaunay2D")
3484        delny.SetInputData(pd)
3485        if tol:
3486            delny.SetTolerance(tol)
3487        delny.SetAlpha(alpha)
3488        delny.SetOffset(offset)
3489
3490        if transform:
3491            delny.SetTransform(transform.T)
3492        elif mode == "fit":
3493            delny.SetProjectionPlaneMode(vtki.get_class("VTK_BEST_FITTING_PLANE"))
3494        elif mode == "xy" and boundaries:
3495            boundary = vtki.vtkPolyData()
3496            boundary.SetPoints(vpts)
3497            cell_array = vtki.vtkCellArray()
3498            for b in boundaries:
3499                cpolygon = vtki.vtkPolygon()
3500                for idd in b:
3501                    cpolygon.GetPointIds().InsertNextId(idd)
3502                cell_array.InsertNextCell(cpolygon)
3503            boundary.SetPolys(cell_array)
3504            delny.SetSourceData(boundary)
3505
3506        delny.Update()
3507
3508        msh = vedo.mesh.Mesh(delny.GetOutput())
3509        msh.name = "Delaunay2D"
3510        msh.clean().lighting("off")
3511        msh.pipeline = utils.OperationNode(
3512            "delaunay2d",
3513            parents=[self],
3514            comment=f"#cells {msh.dataset.GetNumberOfCells()}",
3515        )
3516        return msh

Create a mesh from points in the XY plane. If mode='fit' then the filter computes a best fitting plane and projects the points onto it.

Check also generate_mesh().

Arguments:
  • tol : (float) specify a tolerance to control discarding of closely spaced points. This tolerance is specified as a fraction of the diagonal length of the bounding box of the points.
  • alpha : (float) for a non-zero alpha value, only edges or triangles contained within a sphere centered at mesh vertices will be output. Otherwise, only triangles will be output.
  • offset : (float) multiplier to control the size of the initial, bounding Delaunay triangulation.
  • transform: (LinearTransform, NonLinearTransform) a transformation which is applied to points to generate a 2D problem. This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane. The points are transformed and triangulated. The topology of triangulated points is used as the output topology.
Examples:
def generate_voronoi(self, padding=0.0, fit=False, method='vtk') -> vedo.mesh.Mesh:
3518    def generate_voronoi(self, padding=0.0, fit=False, method="vtk") -> "vedo.Mesh":
3519        """
3520        Generate the 2D Voronoi convex tiling of the input points (z is ignored).
3521        The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon.
3522
3523        A cell array named "VoronoiID" is added to the output Mesh.
3524
3525        The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest
3526        to one of the input points. Voronoi tessellations are important in computational geometry
3527        (and many other fields), and are the dual of Delaunay triangulations.
3528
3529        Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored
3530        (although carried through to the output).
3531        If you desire to triangulate in a different plane, you can use fit=True.
3532
3533        A brief summary is as follows. Each (generating) input point is associated with
3534        an initial Voronoi tile, which is simply the bounding box of the point set.
3535        A locator is then used to identify nearby points: each neighbor in turn generates a
3536        clipping line positioned halfway between the generating point and the neighboring point,
3537        and orthogonal to the line connecting them. Clips are readily performed by evaluationg the
3538        vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line.
3539        If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip
3540        line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs,
3541        the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region
3542        containing the neighboring clip points. The clip region (along with the points contained in it) is grown
3543        by careful expansion (e.g., outward spiraling iterator over all candidate clip points).
3544        When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi
3545        tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi
3546        tessellation. Note that topological and geometric information is used to generate a valid triangulation
3547        (e.g., merging points and validating topology).
3548
3549        Arguments:
3550            pts : (list)
3551                list of input points.
3552            padding : (float)
3553                padding distance. The default is 0.
3554            fit : (bool)
3555                detect automatically the best fitting plane. The default is False.
3556
3557        Examples:
3558            - [voronoi1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi1.py)
3559
3560                ![](https://vedo.embl.es/images/basic/voronoi1.png)
3561
3562            - [voronoi2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi2.py)
3563
3564                ![](https://vedo.embl.es/images/advanced/voronoi2.png)
3565        """
3566        pts = self.vertices
3567
3568        if method == "scipy":
3569            from scipy.spatial import Voronoi as scipy_voronoi
3570
3571            pts = np.asarray(pts)[:, (0, 1)]
3572            vor = scipy_voronoi(pts)
3573            regs = []  # filter out invalid indices
3574            for r in vor.regions:
3575                flag = True
3576                for x in r:
3577                    if x < 0:
3578                        flag = False
3579                        break
3580                if flag and len(r) > 0:
3581                    regs.append(r)
3582
3583            m = vedo.Mesh([vor.vertices, regs])
3584            m.celldata["VoronoiID"] = np.array(list(range(len(regs)))).astype(int)
3585
3586        elif method == "vtk":
3587            vor = vtki.new("Voronoi2D")
3588            if isinstance(pts, Points):
3589                vor.SetInputData(pts)
3590            else:
3591                pts = np.asarray(pts)
3592                if pts.shape[1] == 2:
3593                    pts = np.c_[pts, np.zeros(len(pts))]
3594                pd = vtki.vtkPolyData()
3595                vpts = vtki.vtkPoints()
3596                vpts.SetData(utils.numpy2vtk(pts, dtype=np.float32))
3597                pd.SetPoints(vpts)
3598                vor.SetInputData(pd)
3599            vor.SetPadding(padding)
3600            vor.SetGenerateScalarsToPointIds()
3601            if fit:
3602                vor.SetProjectionPlaneModeToBestFittingPlane()
3603            else:
3604                vor.SetProjectionPlaneModeToXYPlane()
3605            vor.Update()
3606            poly = vor.GetOutput()
3607            arr = poly.GetCellData().GetArray(0)
3608            if arr:
3609                arr.SetName("VoronoiID")
3610            m = vedo.Mesh(poly, c="orange5")
3611
3612        else:
3613            vedo.logger.error(f"Unknown method {method} in voronoi()")
3614            raise RuntimeError
3615
3616        m.lw(2).lighting("off").wireframe()
3617        m.name = "Voronoi"
3618        return m

Generate the 2D Voronoi convex tiling of the input points (z is ignored). The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon.

A cell array named "VoronoiID" is added to the output Mesh.

The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest to one of the input points. Voronoi tessellations are important in computational geometry (and many other fields), and are the dual of Delaunay triangulations.

Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored (although carried through to the output). If you desire to triangulate in a different plane, you can use fit=True.

A brief summary is as follows. Each (generating) input point is associated with an initial Voronoi tile, which is simply the bounding box of the point set. A locator is then used to identify nearby points: each neighbor in turn generates a clipping line positioned halfway between the generating point and the neighboring point, and orthogonal to the line connecting them. Clips are readily performed by evaluationg the vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line. If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs, the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region containing the neighboring clip points. The clip region (along with the points contained in it) is grown by careful expansion (e.g., outward spiraling iterator over all candidate clip points). When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi tessellation. Note that topological and geometric information is used to generate a valid triangulation (e.g., merging points and validating topology).

Arguments:
  • pts : (list) list of input points.
  • padding : (float) padding distance. The default is 0.
  • fit : (bool) detect automatically the best fitting plane. The default is False.
Examples:
def generate_delaunay3d(self, radius=0, tol=None) -> vedo.grids.TetMesh:
3621    def generate_delaunay3d(self, radius=0, tol=None) -> "vedo.TetMesh":
3622        """
3623        Create 3D Delaunay triangulation of input points.
3624
3625        Arguments:
3626            radius : (float)
3627                specify distance (or "alpha") value to control output.
3628                For a non-zero values, only tetra contained within the circumsphere
3629                will be output.
3630            tol : (float)
3631                Specify a tolerance to control discarding of closely spaced points.
3632                This tolerance is specified as a fraction of the diagonal length of
3633                the bounding box of the points.
3634        """
3635        deln = vtki.new("Delaunay3D")
3636        deln.SetInputData(self.dataset)
3637        deln.SetAlpha(radius)
3638        deln.AlphaTetsOn()
3639        deln.AlphaTrisOff()
3640        deln.AlphaLinesOff()
3641        deln.AlphaVertsOff()
3642        deln.BoundingTriangulationOff()
3643        if tol:
3644            deln.SetTolerance(tol)
3645        deln.Update()
3646        m = vedo.TetMesh(deln.GetOutput())
3647        m.pipeline = utils.OperationNode(
3648            "generate_delaunay3d", c="#e9c46a:#edabab", parents=[self],
3649        )
3650        m.name = "Delaunay3D"
3651        return m

Create 3D Delaunay triangulation of input points.

Arguments:
  • radius : (float) specify distance (or "alpha") value to control output. For a non-zero values, only tetra contained within the circumsphere will be output.
  • tol : (float) Specify a tolerance to control discarding of closely spaced points. This tolerance is specified as a fraction of the diagonal length of the bounding box of the points.
def visible_points(self, area=(), tol=None, invert=False) -> Optional[Self]:
3654    def visible_points(self, area=(), tol=None, invert=False) -> Union[Self, None]:
3655        """
3656        Extract points based on whether they are visible or not.
3657        Visibility is determined by accessing the z-buffer of a rendering window.
3658        The position of each input point is converted into display coordinates,
3659        and then the z-value at that point is obtained.
3660        If within the user-specified tolerance, the point is considered visible.
3661        Associated data attributes are passed to the output as well.
3662
3663        This filter also allows you to specify a rectangular window in display (pixel)
3664        coordinates in which the visible points must lie.
3665
3666        Arguments:
3667            area : (list)
3668                specify a rectangular region as (xmin,xmax,ymin,ymax)
3669            tol : (float)
3670                a tolerance in normalized display coordinate system
3671            invert : (bool)
3672                select invisible points instead.
3673
3674        Example:
3675            ```python
3676            from vedo import Ellipsoid, show
3677            s = Ellipsoid().rotate_y(30)
3678
3679            # Camera options: pos, focal_point, viewup, distance
3680            camopts = dict(pos=(0,0,25), focal_point=(0,0,0))
3681            show(s, camera=camopts, offscreen=True)
3682
3683            m = s.visible_points()
3684            # print('visible pts:', m.vertices)  # numpy array
3685            show(m, new=True, axes=1).close() # optionally draw result in a new window
3686            ```
3687            ![](https://vedo.embl.es/images/feats/visible_points.png)
3688        """
3689        svp = vtki.new("SelectVisiblePoints")
3690        svp.SetInputData(self.dataset)
3691
3692        ren = None
3693        if vedo.plotter_instance:
3694            if vedo.plotter_instance.renderer:
3695                ren = vedo.plotter_instance.renderer
3696                svp.SetRenderer(ren)
3697        if not ren:
3698            vedo.logger.warning(
3699                "visible_points() can only be used after a rendering step"
3700            )
3701            return None
3702
3703        if len(area) == 2:
3704            area = utils.flatten(area)
3705        if len(area) == 4:
3706            # specify a rectangular region
3707            svp.SetSelection(area[0], area[1], area[2], area[3])
3708        if tol is not None:
3709            svp.SetTolerance(tol)
3710        if invert:
3711            svp.SelectInvisibleOn()
3712        svp.Update()
3713
3714        m = Points(svp.GetOutput())
3715        m.name = "VisiblePoints"
3716        return m

Extract points based on whether they are visible or not. Visibility is determined by accessing the z-buffer of a rendering window. The position of each input point is converted into display coordinates, and then the z-value at that point is obtained. If within the user-specified tolerance, the point is considered visible. Associated data attributes are passed to the output as well.

This filter also allows you to specify a rectangular window in display (pixel) coordinates in which the visible points must lie.

Arguments:
  • area : (list) specify a rectangular region as (xmin,xmax,ymin,ymax)
  • tol : (float) a tolerance in normalized display coordinate system
  • invert : (bool) select invisible points instead.
Example:
from vedo import Ellipsoid, show
s = Ellipsoid().rotate_y(30)

# Camera options: pos, focal_point, viewup, distance
camopts = dict(pos=(0,0,25), focal_point=(0,0,0))
show(s, camera=camopts, offscreen=True)

m = s.visible_points()
# print('visible pts:', m.vertices)  # numpy array
show(m, new=True, axes=1).close() # optionally draw result in a new window

def Point(pos=(0, 0, 0), r=12, c='red', alpha=1.0) -> Self:
455def Point(pos=(0, 0, 0), r=12, c="red", alpha=1.0) -> Self:
456    """
457    Create a simple point in space.
458
459    .. note:: if you are creating many points you should use class `Points` instead!
460    """
461    pt = Points([[0,0,0]], r, c, alpha).pos(pos)
462    pt.name = "Point"
463    return pt

Create a simple point in space.

if you are creating many points you should use class Points instead!
class CellCenters(Points):
3719class CellCenters(Points):
3720    def __init__(self, pcloud):
3721        """
3722        Generate `Points` at the center of the cells of any type of object.
3723
3724        Check out also `cell_centers()`.
3725        """
3726        vcen = vtki.new("CellCenters")
3727        vcen.CopyArraysOn()
3728        vcen.VertexCellsOn()
3729        # vcen.ConvertGhostCellsToGhostPointsOn()
3730        try:
3731            vcen.SetInputData(pcloud.dataset)
3732        except AttributeError:
3733            vcen.SetInputData(pcloud)
3734        vcen.Update()
3735        super().__init__(vcen.GetOutput())
3736        self.name = "CellCenters"

Work with point clouds.

CellCenters(pcloud)
3720    def __init__(self, pcloud):
3721        """
3722        Generate `Points` at the center of the cells of any type of object.
3723
3724        Check out also `cell_centers()`.
3725        """
3726        vcen = vtki.new("CellCenters")
3727        vcen.CopyArraysOn()
3728        vcen.VertexCellsOn()
3729        # vcen.ConvertGhostCellsToGhostPointsOn()
3730        try:
3731            vcen.SetInputData(pcloud.dataset)
3732        except AttributeError:
3733            vcen.SetInputData(pcloud)
3734        vcen.Update()
3735        super().__init__(vcen.GetOutput())
3736        self.name = "CellCenters"

Generate Points at the center of the cells of any type of object.

Check out also cell_centers().

Inherited Members
Points
polydata
copy
clone
compute_normals_with_pca
compute_acoplanarity
distance_to
clean
subsample
threshold
quantize
vertex_normals
point_normals
align_to
align_to_bounding_box
align_with_landmarks
normalize
mirror
flip_normals
add_gaussian_noise
closest_point
auto_distance
hausdorff_distance
chamfer_distance
remove_outliers
relax_point_positions
smooth_mls_1d
smooth_mls_2d
smooth_lloyd_2d
project_on_plane
warp
cut_with_plane
cut_with_planes
cut_with_box
cut_with_line
cut_with_cookiecutter
cut_with_cylinder
cut_with_sphere
cut_with_mesh
cut_with_point_loop
cut_with_scalar
crop
generate_surface_halo
generate_mesh
reconstruct_surface
compute_clustering
compute_connections
compute_camera_distance
densify
density
tovolume
generate_segments
generate_delaunay2d
generate_voronoi
generate_delaunay3d
visible_points
vedo.visual.PointsVisual
clone2d
copy_properties_from
color
c
alpha
lut_color_at
opacity
force_opaque
force_translucent
point_size
ps
render_points_as_spheres
lighting
point_blurring
cellcolors
pointcolors
cmap
add_trail
update_trail
add_shadow
update_shadows
labels
labels2d
legend
flagpole
flagpost
caption
vedo.visual.CommonVisual
print
LUT
scalar_range
add_observer
invoke_event
show
thumbnail
pickable
use_bounds
draggable
on
off
toggle
add_scalarbar
add_scalarbar3d
vedo.core.PointAlgorithms
apply_transform
apply_transform_from_actor
pos
shift
x
y
z
rotate
rotate_x
rotate_y
rotate_z
reorient
scale
vedo.core.CommonAlgorithms
pointdata
celldata
metadata
memory_address
memory_size
modified
box
update_dataset
bounds
xbounds
ybounds
zbounds
diagonal_size
average_size
center_of_mass
copy_data_from
inputdata
npoints
nvertices
ncells
points
cell_centers
lines
lines_as_flat_array
mark_boundaries
find_cells_in_bounds
find_cells_along_line
find_cells_along_plane
keep_cell_types
map_cells_to_points
vertices
coordinates
cells_as_flat_array
cells
map_points_to_cells
resample_data_from
interpolate_data_from
add_ids
gradient
divergence
vorticity
probe
compute_cell_size
generate_random_data
integrate_data
write
tomesh
signed_distance
unsigned_distance
smooth_data
compute_streamlines
def merge( *meshs, flag=False) -> Union[vedo.mesh.Mesh, Points, NoneType]:
43def merge(*meshs, flag=False) -> Union["vedo.Mesh", "vedo.Points", None]:
44    """
45    Build a new Mesh (or Points) formed by the fusion of the inputs.
46
47    Similar to Assembly, but in this case the input objects become a single entity.
48
49    To keep track of the original identities of the inputs you can set `flag=True`.
50    In this case a `pointdata` array of ids is added to the output with name "OriginalMeshID".
51
52    Examples:
53        - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py)
54
55            ![](https://vedo.embl.es/images/advanced/warp1.png)
56
57        - [value_iteration.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/value_iteration.py)
58
59    """
60    objs = [a for a in utils.flatten(meshs) if a]
61
62    if not objs:
63        return None
64
65    idarr = []
66    polyapp = vtki.new("AppendPolyData")
67    for i, ob in enumerate(objs):
68        polyapp.AddInputData(ob.dataset)
69        if flag:
70            idarr += [i] * ob.dataset.GetNumberOfPoints()
71    polyapp.Update()
72    mpoly = polyapp.GetOutput()
73
74    if flag:
75        varr = utils.numpy2vtk(idarr, dtype=np.uint16, name="OriginalMeshID")
76        mpoly.GetPointData().AddArray(varr)
77
78    has_mesh = False
79    for ob in objs:
80        if isinstance(ob, vedo.Mesh):
81            has_mesh = True
82            break
83
84    if has_mesh:
85        msh = vedo.Mesh(mpoly)
86    else:
87        msh = Points(mpoly) # type: ignore
88
89    msh.copy_properties_from(objs[0])
90
91    msh.pipeline = utils.OperationNode(
92        "merge", parents=objs, comment=f"#pts {msh.dataset.GetNumberOfPoints()}"
93    )
94    return msh

Build a new Mesh (or Points) formed by the fusion of the inputs.

Similar to Assembly, but in this case the input objects become a single entity.

To keep track of the original identities of the inputs you can set flag=True. In this case a pointdata array of ids is added to the output with name "OriginalMeshID".

Examples:
def delaunay2d(plist, **kwargs) -> Self:
 97def delaunay2d(plist, **kwargs) -> Self:
 98    """delaunay2d() is deprecated, use Points().generate_delaunay2d() instead."""
 99    if isinstance(plist, Points):
100        plist = plist.vertices
101    else:
102        plist = np.ascontiguousarray(plist)
103        plist = utils.make3d(plist)
104    pp = Points(plist).generate_delaunay2d(**kwargs)
105    print("WARNING: delaunay2d() is deprecated, use Points().generate_delaunay2d() instead")
106    return pp

delaunay2d() is deprecated, use Points().generate_delaunay2d() instead.

def fit_line(points: Union[numpy.ndarray, Points]) -> vedo.shapes.Line:
149def fit_line(points: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Line":
150    """
151    Fits a line through points.
152
153    Extra info is stored in `Line.slope`, `Line.center`, `Line.variances`.
154
155    Examples:
156        - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py)
157
158            ![](https://vedo.embl.es/images/advanced/fitline.png)
159    """
160    if isinstance(points, Points):
161        points = points.vertices
162    data = np.asarray(points)
163    datamean = data.mean(axis=0)
164    _, dd, vv = np.linalg.svd(data - datamean)
165    vv = vv[0] / np.linalg.norm(vv[0])
166    # vv contains the first principal component, i.e. the direction
167    # vector of the best fit line in the least squares sense.
168    xyz_min = data.min(axis=0)
169    xyz_max = data.max(axis=0)
170    a = np.linalg.norm(xyz_min - datamean)
171    b = np.linalg.norm(xyz_max - datamean)
172    p1 = datamean - a * vv
173    p2 = datamean + b * vv
174    line = vedo.shapes.Line(p1, p2, lw=1)
175    line.slope = vv
176    line.center = datamean
177    line.variances = dd
178    return line

Fits a line through points.

Extra info is stored in Line.slope, Line.center, Line.variances.

Examples:
def fit_circle(points: Union[numpy.ndarray, Points]) -> tuple:
181def fit_circle(points: Union[np.ndarray, "vedo.Points"]) -> tuple:
182    """
183    Fits a circle through a set of 3D points, with a very fast non-iterative method.
184
185    Returns the tuple `(center, radius, normal_to_circle)`.
186
187    .. warning::
188        trying to fit s-shaped points will inevitably lead to instabilities and
189        circles of small radius.
190
191    References:
192        *J.F. Crawford, Nucl. Instr. Meth. 211, 1983, 223-225.*
193    """
194    if isinstance(points, Points):
195        points = points.vertices
196    data = np.asarray(points)
197
198    offs = data.mean(axis=0)
199    data, n0 = _rotate_points(data - offs)
200
201    xi = data[:, 0]
202    yi = data[:, 1]
203
204    x = sum(xi)
205    xi2 = xi * xi
206    xx = sum(xi2)
207    xxx = sum(xi2 * xi)
208
209    y = sum(yi)
210    yi2 = yi * yi
211    yy = sum(yi2)
212    yyy = sum(yi2 * yi)
213
214    xiyi = xi * yi
215    xy = sum(xiyi)
216    xyy = sum(xiyi * yi)
217    xxy = sum(xi * xiyi)
218
219    N = len(xi)
220    k = (xx + yy) / N
221
222    a1 = xx - x * x / N
223    b1 = xy - x * y / N
224    c1 = 0.5 * (xxx + xyy - x * k)
225
226    a2 = xy - x * y / N
227    b2 = yy - y * y / N
228    c2 = 0.5 * (xxy + yyy - y * k)
229
230    d = a2 * b1 - a1 * b2
231    if not d:
232        return offs, 0, n0
233    x0 = (b1 * c2 - b2 * c1) / d
234    y0 = (c1 - a1 * x0) / b1
235
236    R = np.sqrt(x0 * x0 + y0 * y0 - 1 / N * (2 * x0 * x + 2 * y0 * y - xx - yy))
237
238    c, _ = _rotate_points([x0, y0, 0], (0, 0, 1), n0)
239
240    return c[0] + offs, R, n0

Fits a circle through a set of 3D points, with a very fast non-iterative method.

Returns the tuple (center, radius, normal_to_circle).

trying to fit s-shaped points will inevitably lead to instabilities and circles of small radius.

References:

J.F. Crawford, Nucl. Instr. Meth. 211, 1983, 223-225.

def fit_plane( points: Union[numpy.ndarray, Points], signed=False) -> vedo.shapes.Plane:
243def fit_plane(points: Union[np.ndarray, "vedo.Points"], signed=False) -> "vedo.shapes.Plane":
244    """
245    Fits a plane to a set of points.
246
247    Extra info is stored in `Plane.normal`, `Plane.center`, `Plane.variance`.
248
249    Arguments:
250        signed : (bool)
251            if True flip sign of the normal based on the ordering of the points
252
253    Examples:
254        - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py)
255
256            ![](https://vedo.embl.es/images/advanced/fitline.png)
257    """
258    if isinstance(points, Points):
259        points = points.vertices
260    data = np.asarray(points)
261    datamean = data.mean(axis=0)
262    pts = data - datamean
263    res = np.linalg.svd(pts)
264    dd, vv = res[1], res[2]
265    n = np.cross(vv[0], vv[1])
266    if signed:
267        v = np.zeros_like(pts)
268        for i in range(len(pts) - 1):
269            vi = np.cross(pts[i], pts[i + 1])
270            v[i] = vi / np.linalg.norm(vi)
271        ns = np.mean(v, axis=0)  # normal to the points plane
272        if np.dot(n, ns) < 0:
273            n = -n
274    xyz_min = data.min(axis=0)
275    xyz_max = data.max(axis=0)
276    s = np.linalg.norm(xyz_max - xyz_min)
277    pla = vedo.shapes.Plane(datamean, n, s=[s, s])
278    pla.variance = dd[2]
279    pla.name = "FitPlane"
280    return pla

Fits a plane to a set of points.

Extra info is stored in Plane.normal, Plane.center, Plane.variance.

Arguments:
  • signed : (bool) if True flip sign of the normal based on the ordering of the points
Examples:
def fit_sphere( coords: Union[numpy.ndarray, Points]) -> vedo.shapes.Sphere:
283def fit_sphere(coords: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Sphere":
284    """
285    Fits a sphere to a set of points.
286
287    Extra info is stored in `Sphere.radius`, `Sphere.center`, `Sphere.residue`.
288
289    Examples:
290        - [fitspheres1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitspheres1.py)
291
292            ![](https://vedo.embl.es/images/advanced/fitspheres1.jpg)
293    """
294    if isinstance(coords, Points):
295        coords = coords.vertices
296    coords = np.array(coords)
297    n = len(coords)
298    A = np.zeros((n, 4))
299    A[:, :-1] = coords * 2
300    A[:, 3] = 1
301    f = np.zeros((n, 1))
302    x = coords[:, 0]
303    y = coords[:, 1]
304    z = coords[:, 2]
305    f[:, 0] = x * x + y * y + z * z
306    try:
307        C, residue, rank, _ = np.linalg.lstsq(A, f, rcond=-1)  # solve AC=f
308    except:
309        C, residue, rank, _ = np.linalg.lstsq(A, f)  # solve AC=f
310    if rank < 4:
311        return None
312    t = (C[0] * C[0]) + (C[1] * C[1]) + (C[2] * C[2]) + C[3]
313    radius = np.sqrt(t)[0]
314    center = np.array([C[0][0], C[1][0], C[2][0]])
315    if len(residue) > 0:
316        residue = np.sqrt(residue[0]) / n
317    else:
318        residue = 0
319    sph = vedo.shapes.Sphere(center, radius, c=(1, 0, 0)).wireframe(1)
320    sph.radius = radius
321    sph.center = center
322    sph.residue = residue
323    sph.name = "FitSphere"
324    return sph

Fits a sphere to a set of points.

Extra info is stored in Sphere.radius, Sphere.center, Sphere.residue.

Examples:
def pca_ellipse( points: Union[numpy.ndarray, Points], pvalue=0.673, res=60) -> Optional[vedo.shapes.Circle]:
327def pca_ellipse(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=60) -> Union["vedo.shapes.Circle", None]:
328    """
329    Create the oriented 2D ellipse that contains the fraction `pvalue` of points.
330    PCA (Principal Component Analysis) is used to compute the ellipse orientation.
331
332    Parameter `pvalue` sets the specified fraction of points inside the ellipse.
333    Normalized directions are stored in `ellipse.axis1`, `ellipse.axis2`.
334    Axes sizes are stored in `ellipse.va`, `ellipse.vb`
335
336    Arguments:
337        pvalue : (float)
338            ellipse will include this fraction of points
339        res : (int)
340            resolution of the ellipse
341
342    Examples:
343        - [pca_ellipse.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipse.py)
344        - [histo_pca.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_pca.py)
345
346            ![](https://vedo.embl.es/images/pyplot/histo_pca.png)
347    """
348    from scipy.stats import f
349
350    if isinstance(points, Points):
351        coords = points.vertices
352    else:
353        coords = points
354    if len(coords) < 4:
355        vedo.logger.warning("in pca_ellipse(), there are not enough points!")
356        return None
357
358    P = np.array(coords, dtype=float)[:, (0, 1)]
359    cov = np.cov(P, rowvar=0)      # type: ignore
360    _, s, R = np.linalg.svd(cov)   # singular value decomposition
361    p, n = s.size, P.shape[0]
362    fppf = f.ppf(pvalue, p, n - p) # f % point function
363    u = np.sqrt(s * fppf / 2) * 2  # semi-axes (largest first)
364    ua, ub = u
365    center = utils.make3d(np.mean(P, axis=0)) # centroid of the ellipse
366
367    t = LinearTransform(R.T * u).translate(center)
368    elli = vedo.shapes.Circle(alpha=0.75, res=res)
369    elli.apply_transform(t)
370    elli.properties.LightingOff()
371
372    elli.pvalue = pvalue
373    elli.center = np.array([center[0], center[1], 0])
374    elli.nr_of_points = n
375    elli.va = ua
376    elli.vb = ub
377    
378    # we subtract center because it's in t
379    elli.axis1 = t.move([1, 0, 0]) - center
380    elli.axis2 = t.move([0, 1, 0]) - center
381
382    elli.axis1 /= np.linalg.norm(elli.axis1)
383    elli.axis2 /= np.linalg.norm(elli.axis2)
384    elli.name = "PCAEllipse"
385    return elli

Create the oriented 2D ellipse that contains the fraction pvalue of points. PCA (Principal Component Analysis) is used to compute the ellipse orientation.

Parameter pvalue sets the specified fraction of points inside the ellipse. Normalized directions are stored in ellipse.axis1, ellipse.axis2. Axes sizes are stored in ellipse.va, ellipse.vb

Arguments:
  • pvalue : (float) ellipse will include this fraction of points
  • res : (int) resolution of the ellipse
Examples:
def pca_ellipsoid( points: Union[numpy.ndarray, Points], pvalue=0.673, res=24) -> Optional[vedo.shapes.Ellipsoid]:
388def pca_ellipsoid(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=24) -> Union["vedo.shapes.Ellipsoid", None]:
389    """
390    Create the oriented ellipsoid that contains the fraction `pvalue` of points.
391    PCA (Principal Component Analysis) is used to compute the ellipsoid orientation.
392
393    Axes sizes can be accessed in `ellips.va`, `ellips.vb`, `ellips.vc`,
394    normalized directions are stored in `ellips.axis1`, `ellips.axis2` and `ellips.axis3`.
395    Center of mass is stored in `ellips.center`.
396
397    Asphericity can be accessed in `ellips.asphericity()` and ellips.asphericity_error().
398    A value of 0 means a perfect sphere.
399
400    Arguments:
401        pvalue : (float)
402            ellipsoid will include this fraction of points
403   
404    Examples:
405        [pca_ellipsoid.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipsoid.py)
406
407            ![](https://vedo.embl.es/images/basic/pca.png)
408    
409    See also:
410        `pca_ellipse()` for a 2D ellipse.
411    """
412    from scipy.stats import f
413
414    if isinstance(points, Points):
415        coords = points.vertices
416    else:
417        coords = points
418    if len(coords) < 4:
419        vedo.logger.warning("in pca_ellipsoid(), not enough input points!")
420        return None
421
422    P = np.array(coords, ndmin=2, dtype=float)
423    cov = np.cov(P, rowvar=0)     # type: ignore
424    _, s, R = np.linalg.svd(cov)  # singular value decomposition
425    p, n = s.size, P.shape[0]
426    fppf = f.ppf(pvalue, p, n-p)*(n-1)*p*(n+1)/n/(n-p)  # f % point function
427    u = np.sqrt(s*fppf)
428    ua, ub, uc = u                # semi-axes (largest first)
429    center = np.mean(P, axis=0)   # centroid of the hyperellipsoid
430
431    t = LinearTransform(R.T * u).translate(center)
432    elli = vedo.shapes.Ellipsoid((0,0,0), (1,0,0), (0,1,0), (0,0,1), res=res)
433    elli.apply_transform(t)
434    elli.alpha(0.25)
435    elli.properties.LightingOff()
436
437    elli.pvalue = pvalue
438    elli.nr_of_points = n
439    elli.center = center
440    elli.va = ua
441    elli.vb = ub
442    elli.vc = uc
443    # we subtract center because it's in t
444    elli.axis1 = np.array(t.move([1, 0, 0])) - center
445    elli.axis2 = np.array(t.move([0, 1, 0])) - center
446    elli.axis3 = np.array(t.move([0, 0, 1])) - center
447    elli.axis1 /= np.linalg.norm(elli.axis1)
448    elli.axis2 /= np.linalg.norm(elli.axis2)
449    elli.axis3 /= np.linalg.norm(elli.axis3)
450    elli.name = "PCAEllipsoid"
451    return elli

Create the oriented ellipsoid that contains the fraction pvalue of points. PCA (Principal Component Analysis) is used to compute the ellipsoid orientation.

Axes sizes can be accessed in ellips.va, ellips.vb, ellips.vc, normalized directions are stored in ellips.axis1, ellips.axis2 and ellips.axis3. Center of mass is stored in ellips.center.

Asphericity can be accessed in ellips.asphericity() and ellips.asphericity_error(). A value of 0 means a perfect sphere.

Arguments:
  • pvalue : (float) ellipsoid will include this fraction of points
Examples:

pca_ellipsoid.py

![](https://vedo.embl.es/images/basic/pca.png)
See also:

pca_ellipse() for a 2D ellipse.