vedo.pointcloud

Submodule to work with point clouds

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

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. For very large point clouds a list of colors and alpha can be assigned to each point in the form c=[(R,G,B,A), ... ] where 0<=R<256, ... 0<=A<256.

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):
816    def polydata(self, **kwargs):
817        """
818        Obsolete. Use property `.dataset` instead.
819        Returns the underlying `vtkPolyData` object.
820        """
821        colors.printc(
822            "WARNING: call to .polydata() is obsolete, use property .dataset instead.",
823            c="y")
824        return self.dataset

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

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

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

def clone(self, deep=True) -> Self:
836    def clone(self, deep=True) -> Self:
837        """
838        Clone a `PointCloud` or `Mesh` object to make an exact copy of it.
839        Alias of `copy()`.
840
841        Arguments:
842            deep : (bool)
843                if False return a shallow copy of the mesh without copying the points array.
844
845        Examples:
846            - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py)
847
848               ![](https://vedo.embl.es/images/basic/mirror.png)
849        """
850        poly = vtki.vtkPolyData()
851        if deep or isinstance(deep, dict): # if a memo object is passed this checks as True
852            poly.DeepCopy(self.dataset)
853        else:
854            poly.ShallowCopy(self.dataset)
855
856        if isinstance(self, vedo.Mesh):
857            cloned = vedo.Mesh(poly)
858        else:
859            cloned = Points(poly)
860        # print([self], self.__class__)
861        # cloned = self.__class__(poly)
862
863        cloned.transform = self.transform.clone()
864
865        cloned.copy_properties_from(self)
866
867        cloned.name = str(self.name)
868        cloned.filename = str(self.filename)
869        cloned.info = dict(self.info)
870        cloned.pipeline = utils.OperationNode("clone", parents=[self], shape="diamond", c="#edede9")
871
872        if isinstance(deep, dict):
873            deep[id(self)] = cloned
874
875        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:
877    def compute_normals_with_pca(self, n=20, orientation_point=None, invert=False) -> Self:
878        """
879        Generate point normals using PCA (principal component analysis).
880        This algorithm estimates a local tangent plane around each sample point p
881        by considering a small neighborhood of points around p, and fitting a plane
882        to the neighborhood (via PCA).
883
884        Arguments:
885            n : (int)
886                neighborhood size to calculate the normal
887            orientation_point : (list)
888                adjust the +/- sign of the normals so that
889                the normals all point towards a specified point. If None, perform a traversal
890                of the point cloud and flip neighboring normals so that they are mutually consistent.
891            invert : (bool)
892                flip all normals
893        """
894        poly = self.dataset
895        pcan = vtki.new("PCANormalEstimation")
896        pcan.SetInputData(poly)
897        pcan.SetSampleSize(n)
898
899        if orientation_point is not None:
900            pcan.SetNormalOrientationToPoint()
901            pcan.SetOrientationPoint(orientation_point)
902        else:
903            pcan.SetNormalOrientationToGraphTraversal()
904
905        if invert:
906            pcan.FlipNormalsOn()
907        pcan.Update()
908
909        varr = pcan.GetOutput().GetPointData().GetNormals()
910        varr.SetName("Normals")
911        self.dataset.GetPointData().SetNormals(varr)
912        self.dataset.GetPointData().Modified()
913        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:
915    def compute_acoplanarity(self, n=25, radius=None, on="points") -> Self:
916        """
917        Compute acoplanarity which is a measure of how much a local region of the mesh
918        differs from a plane.
919        
920        The information is stored in a `pointdata` or `celldata` array with name 'Acoplanarity'.
921        
922        Either `n` (number of neighbour points) or `radius` (radius of local search) can be specified.
923        If a radius value is given and not enough points fall inside it, then a -1 is stored.
924
925        Example:
926            ```python
927            from vedo import *
928            msh = ParametricShape('RandomHills')
929            msh.compute_acoplanarity(radius=0.1, on='cells')
930            msh.cmap("coolwarm", on='cells').add_scalarbar()
931            msh.show(axes=1).close()
932            ```
933            ![](https://vedo.embl.es/images/feats/acoplanarity.jpg)
934        """
935        acoplanarities = []
936        if "point" in on:
937            pts = self.vertices
938        elif "cell" in on:
939            pts = self.cell_centers
940        else:
941            raise ValueError(f"In compute_acoplanarity() set on to either 'cells' or 'points', not {on}")
942
943        for p in utils.progressbar(pts, delay=5, width=15, title=f"{on} acoplanarity"):
944            if n:
945                data = self.closest_point(p, n=n)
946                npts = n
947            elif radius:
948                data = self.closest_point(p, radius=radius)
949                npts = len(data)
950
951            try:
952                center = data.mean(axis=0)
953                res = np.linalg.svd(data - center)
954                acoplanarities.append(res[1][2] / npts)
955            except:
956                acoplanarities.append(-1.0)
957
958        if "point" in on:
959            self.pointdata["Acoplanarity"] = np.array(acoplanarities, dtype=float)
960        else:
961            self.celldata["Acoplanarity"] = np.array(acoplanarities, dtype=float)
962        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:
 964    def distance_to(self, pcloud, signed=False, invert=False, name="Distance") -> np.ndarray:
 965        """
 966        Computes the distance from one point cloud or mesh to another point cloud or mesh.
 967        This new `pointdata` array is saved with default name "Distance".
 968
 969        Keywords `signed` and `invert` are used to compute signed distance,
 970        but the mesh in that case must have polygonal faces (not a simple point cloud),
 971        and normals must also be computed.
 972
 973        Examples:
 974            - [distance2mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/distance2mesh.py)
 975
 976                ![](https://vedo.embl.es/images/basic/distance2mesh.png)
 977        """
 978        if pcloud.dataset.GetNumberOfPolys():
 979
 980            poly1 = self.dataset
 981            poly2 = pcloud.dataset
 982            df = vtki.new("DistancePolyDataFilter")
 983            df.ComputeSecondDistanceOff()
 984            df.SetInputData(0, poly1)
 985            df.SetInputData(1, poly2)
 986            df.SetSignedDistance(signed)
 987            df.SetNegateDistance(invert)
 988            df.Update()
 989            scals = df.GetOutput().GetPointData().GetScalars()
 990            dists = utils.vtk2numpy(scals)
 991
 992        else:  # has no polygons
 993
 994            if signed:
 995                vedo.logger.warning("distance_to() called with signed=True but input object has no polygons")
 996
 997            if not pcloud.point_locator:
 998                pcloud.point_locator = vtki.new("PointLocator")
 999                pcloud.point_locator.SetDataSet(pcloud.dataset)
1000                pcloud.point_locator.BuildLocator()
1001
1002            ids = []
1003            ps1 = self.vertices
1004            ps2 = pcloud.vertices
1005            for p in ps1:
1006                pid = pcloud.point_locator.FindClosestPoint(p)
1007                ids.append(pid)
1008
1009            deltas = ps2[ids] - ps1
1010            dists = np.linalg.norm(deltas, axis=1).astype(np.float32)
1011            scals = utils.numpy2vtk(dists)
1012
1013        scals.SetName(name)
1014        self.dataset.GetPointData().AddArray(scals)
1015        self.dataset.GetPointData().SetActiveScalars(scals.GetName())
1016        rng = scals.GetRange()
1017        self.mapper.SetScalarRange(rng[0], rng[1])
1018        self.mapper.ScalarVisibilityOn()
1019
1020        self.pipeline = utils.OperationNode(
1021            "distance_to",
1022            parents=[self, pcloud],
1023            shape="cylinder",
1024            comment=f"#pts {self.dataset.GetNumberOfPoints()}",
1025        )
1026        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:
1028    def clean(self) -> Self:
1029        """Clean pointcloud or mesh by removing coincident points."""
1030        cpd = vtki.new("CleanPolyData")
1031        cpd.PointMergingOn()
1032        cpd.ConvertLinesToPointsOff()
1033        cpd.ConvertPolysToLinesOff()
1034        cpd.ConvertStripsToPolysOff()
1035        cpd.SetInputData(self.dataset)
1036        cpd.Update()
1037        self._update(cpd.GetOutput())
1038        self.pipeline = utils.OperationNode(
1039            "clean", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
1040        )
1041        return self

Clean pointcloud or mesh by removing coincident points.

def subsample(self, fraction: float, absolute=False) -> Self:
1043    def subsample(self, fraction: float, absolute=False) -> Self:
1044        """
1045        Subsample a point cloud by requiring that the points
1046        or vertices are far apart at least by the specified fraction of the object size.
1047        If a Mesh is passed the polygonal faces are not removed
1048        but holes can appear as their vertices are removed.
1049
1050        Examples:
1051            - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py)
1052
1053                ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png)
1054
1055            - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py)
1056
1057                ![](https://vedo.embl.es/images/advanced/recosurface.png)
1058        """
1059        if not absolute:
1060            if fraction > 1:
1061                vedo.logger.warning(
1062                    f"subsample(fraction=...), fraction must be < 1, but is {fraction}"
1063                )
1064            if fraction <= 0:
1065                return self
1066
1067        cpd = vtki.new("CleanPolyData")
1068        cpd.PointMergingOn()
1069        cpd.ConvertLinesToPointsOn()
1070        cpd.ConvertPolysToLinesOn()
1071        cpd.ConvertStripsToPolysOn()
1072        cpd.SetInputData(self.dataset)
1073        if absolute:
1074            cpd.SetTolerance(fraction / self.diagonal_size())
1075            # cpd.SetToleranceIsAbsolute(absolute)
1076        else:
1077            cpd.SetTolerance(fraction)
1078        cpd.Update()
1079
1080        ps = 2
1081        if self.properties.GetRepresentation() == 0:
1082            ps = self.properties.GetPointSize()
1083
1084        self._update(cpd.GetOutput())
1085        self.ps(ps)
1086
1087        self.pipeline = utils.OperationNode(
1088            "subsample", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
1089        )
1090        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:
1092    def threshold(self, scalars: str, above=None, below=None, on="points") -> Self:
1093        """
1094        Extracts cells where scalar value satisfies threshold criterion.
1095
1096        Arguments:
1097            scalars : (str)
1098                name of the scalars array.
1099            above : (float)
1100                minimum value of the scalar
1101            below : (float)
1102                maximum value of the scalar
1103            on : (str)
1104                if 'cells' assume array of scalars refers to cell data.
1105
1106        Examples:
1107            - [mesh_threshold.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_threshold.py)
1108        """
1109        thres = vtki.new("Threshold")
1110        thres.SetInputData(self.dataset)
1111
1112        if on.startswith("c"):
1113            asso = vtki.vtkDataObject.FIELD_ASSOCIATION_CELLS
1114        else:
1115            asso = vtki.vtkDataObject.FIELD_ASSOCIATION_POINTS
1116
1117        thres.SetInputArrayToProcess(0, 0, 0, asso, scalars)
1118
1119        if above is None and below is not None:
1120            try:  # vtk 9.2
1121                thres.ThresholdByLower(below)
1122            except AttributeError:  # vtk 9.3
1123                thres.SetUpperThreshold(below)
1124
1125        elif below is None and above is not None:
1126            try:
1127                thres.ThresholdByUpper(above)
1128            except AttributeError:
1129                thres.SetLowerThreshold(above)
1130        else:
1131            try:
1132                thres.ThresholdBetween(above, below)
1133            except AttributeError:
1134                thres.SetUpperThreshold(below)
1135                thres.SetLowerThreshold(above)
1136
1137        thres.Update()
1138
1139        gf = vtki.new("GeometryFilter")
1140        gf.SetInputData(thres.GetOutput())
1141        gf.Update()
1142        self._update(gf.GetOutput())
1143        self.pipeline = utils.OperationNode("threshold", parents=[self])
1144        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:
1146    def quantize(self, value: float) -> Self:
1147        """
1148        The user should input a value and all {x,y,z} coordinates
1149        will be quantized to that absolute grain size.
1150        """
1151        qp = vtki.new("QuantizePolyDataPoints")
1152        qp.SetInputData(self.dataset)
1153        qp.SetQFactor(value)
1154        qp.Update()
1155        self._update(qp.GetOutput())
1156        self.pipeline = utils.OperationNode("quantize", parents=[self])
1157        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
1159    @property
1160    def vertex_normals(self) -> np.ndarray:
1161        """
1162        Retrieve vertex normals as a numpy array. Same as `point_normals`.
1163        Check out also `compute_normals()` and `compute_normals_with_pca()`.
1164        """
1165        vtknormals = self.dataset.GetPointData().GetNormals()
1166        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
1168    @property
1169    def point_normals(self) -> np.ndarray:
1170        """
1171        Retrieve vertex normals as a numpy array. Same as `vertex_normals`.
1172        Check out also `compute_normals()` and `compute_normals_with_pca()`.
1173        """
1174        vtknormals = self.dataset.GetPointData().GetNormals()
1175        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:
1177    def align_to(self, target, iters=100, rigid=False, invert=False, use_centroids=False) -> Self:
1178        """
1179        Aligned to target mesh through the `Iterative Closest Point` algorithm.
1180
1181        The core of the algorithm is to match each vertex in one surface with
1182        the closest surface point on the other, then apply the transformation
1183        that modify one surface to best match the other (in the least-square sense).
1184
1185        Arguments:
1186            rigid : (bool)
1187                if True do not allow scaling
1188            invert : (bool)
1189                if True start by aligning the target to the source but
1190                invert the transformation finally. Useful when the target is smaller
1191                than the source.
1192            use_centroids : (bool)
1193                start by matching the centroids of the two objects.
1194
1195        Examples:
1196            - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py)
1197
1198                ![](https://vedo.embl.es/images/basic/align1.png)
1199
1200            - [align2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align2.py)
1201
1202                ![](https://vedo.embl.es/images/basic/align2.png)
1203        """
1204        icp = vtki.new("IterativeClosestPointTransform")
1205        icp.SetSource(self.dataset)
1206        icp.SetTarget(target.dataset)
1207        if invert:
1208            icp.Inverse()
1209        icp.SetMaximumNumberOfIterations(iters)
1210        if rigid:
1211            icp.GetLandmarkTransform().SetModeToRigidBody()
1212        icp.SetStartByMatchingCentroids(use_centroids)
1213        icp.Update()
1214
1215        self.apply_transform(icp.GetMatrix())
1216
1217        self.pipeline = utils.OperationNode(
1218            "align_to", parents=[self, target], comment=f"rigid = {rigid}"
1219        )
1220        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:
1222    def align_to_bounding_box(self, msh, rigid=False) -> Self:
1223        """
1224        Align the current object's bounding box to the bounding box
1225        of the input object.
1226
1227        Use `rigid=True` to disable scaling.
1228
1229        Example:
1230            [align6.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align6.py)
1231        """
1232        lmt = vtki.vtkLandmarkTransform()
1233        ss = vtki.vtkPoints()
1234        xss0, xss1, yss0, yss1, zss0, zss1 = self.bounds()
1235        for p in [
1236            [xss0, yss0, zss0],
1237            [xss1, yss0, zss0],
1238            [xss1, yss1, zss0],
1239            [xss0, yss1, zss0],
1240            [xss0, yss0, zss1],
1241            [xss1, yss0, zss1],
1242            [xss1, yss1, zss1],
1243            [xss0, yss1, zss1],
1244        ]:
1245            ss.InsertNextPoint(p)
1246        st = vtki.vtkPoints()
1247        xst0, xst1, yst0, yst1, zst0, zst1 = msh.bounds()
1248        for p in [
1249            [xst0, yst0, zst0],
1250            [xst1, yst0, zst0],
1251            [xst1, yst1, zst0],
1252            [xst0, yst1, zst0],
1253            [xst0, yst0, zst1],
1254            [xst1, yst0, zst1],
1255            [xst1, yst1, zst1],
1256            [xst0, yst1, zst1],
1257        ]:
1258            st.InsertNextPoint(p)
1259
1260        lmt.SetSourceLandmarks(ss)
1261        lmt.SetTargetLandmarks(st)
1262        lmt.SetModeToAffine()
1263        if rigid:
1264            lmt.SetModeToRigidBody()
1265        lmt.Update()
1266
1267        LT = LinearTransform(lmt)
1268        self.apply_transform(LT)
1269        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:
1271    def align_with_landmarks(
1272        self,
1273        source_landmarks,
1274        target_landmarks,
1275        rigid=False,
1276        affine=False,
1277        least_squares=False,
1278    ) -> Self:
1279        """
1280        Transform mesh orientation and position based on a set of landmarks points.
1281        The algorithm finds the best matching of source points to target points
1282        in the mean least square sense, in one single step.
1283
1284        If `affine` is True the x, y and z axes can scale independently but stay collinear.
1285        With least_squares they can vary orientation.
1286
1287        Examples:
1288            - [align5.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align5.py)
1289
1290                ![](https://vedo.embl.es/images/basic/align5.png)
1291        """
1292
1293        if utils.is_sequence(source_landmarks):
1294            ss = vtki.vtkPoints()
1295            for p in source_landmarks:
1296                ss.InsertNextPoint(p)
1297        else:
1298            ss = source_landmarks.dataset.GetPoints()
1299            if least_squares:
1300                source_landmarks = source_landmarks.vertices
1301
1302        if utils.is_sequence(target_landmarks):
1303            st = vtki.vtkPoints()
1304            for p in target_landmarks:
1305                st.InsertNextPoint(p)
1306        else:
1307            st = target_landmarks.GetPoints()
1308            if least_squares:
1309                target_landmarks = target_landmarks.vertices
1310
1311        if ss.GetNumberOfPoints() != st.GetNumberOfPoints():
1312            n1 = ss.GetNumberOfPoints()
1313            n2 = st.GetNumberOfPoints()
1314            vedo.logger.error(f"source and target have different nr of points {n1} vs {n2}")
1315            raise RuntimeError()
1316
1317        if int(rigid) + int(affine) + int(least_squares) > 1:
1318            vedo.logger.error(
1319                "only one of rigid, affine, least_squares can be True at a time"
1320            )
1321            raise RuntimeError()
1322
1323        lmt = vtki.vtkLandmarkTransform()
1324        lmt.SetSourceLandmarks(ss)
1325        lmt.SetTargetLandmarks(st)
1326        lmt.SetModeToSimilarity()
1327
1328        if rigid:
1329            lmt.SetModeToRigidBody()
1330            lmt.Update()
1331
1332        elif affine:
1333            lmt.SetModeToAffine()
1334            lmt.Update()
1335
1336        elif least_squares:
1337            cms = source_landmarks.mean(axis=0)
1338            cmt = target_landmarks.mean(axis=0)
1339            m = np.linalg.lstsq(source_landmarks - cms, target_landmarks - cmt, rcond=None)[0]
1340            M = vtki.vtkMatrix4x4()
1341            for i in range(3):
1342                for j in range(3):
1343                    M.SetElement(j, i, m[i][j])
1344            lmt = vtki.vtkTransform()
1345            lmt.Translate(cmt)
1346            lmt.Concatenate(M)
1347            lmt.Translate(-cms)
1348
1349        else:
1350            lmt.Update()
1351
1352        self.apply_transform(lmt)
1353        self.pipeline = utils.OperationNode("transform_with_landmarks", parents=[self])
1354        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:
1356    def normalize(self) -> Self:
1357        """Scale average size to unit. The scaling is performed around the center of mass."""
1358        coords = self.vertices
1359        if not coords.shape[0]:
1360            return self
1361        cm = np.mean(coords, axis=0)
1362        pts = coords - cm
1363        xyz2 = np.sum(pts * pts, axis=0)
1364        scale = 1 / np.sqrt(np.sum(xyz2) / len(pts))
1365        self.scale(scale, origin=cm)
1366        self.pipeline = utils.OperationNode("normalize", parents=[self])
1367        return self

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

def mirror(self, axis='x', origin=True) -> Self:
1369    def mirror(self, axis="x", origin=True) -> Self:
1370        """
1371        Mirror reflect along one of the cartesian axes
1372
1373        Arguments:
1374            axis : (str)
1375                axis to use for mirroring, must be set to `x, y, z`.
1376                Or any combination of those.
1377            origin : (list)
1378                use this point as the origin of the mirroring transformation.
1379
1380        Examples:
1381            - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py)
1382
1383                ![](https://vedo.embl.es/images/basic/mirror.png)
1384        """
1385        sx, sy, sz = 1, 1, 1
1386        if "x" in axis.lower(): sx = -1
1387        if "y" in axis.lower(): sy = -1
1388        if "z" in axis.lower(): sz = -1
1389
1390        self.scale([sx, sy, sz], origin=origin)
1391
1392        self.pipeline = utils.OperationNode(
1393            "mirror", comment=f"axis = {axis}", parents=[self])
1394
1395        if sx * sy * sz < 0:
1396            if hasattr(self, "reverse"):
1397                self.reverse()
1398        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:
1400    def flip_normals(self) -> Self:
1401        """Flip all normals orientation."""
1402        rs = vtki.new("ReverseSense")
1403        rs.SetInputData(self.dataset)
1404        rs.ReverseCellsOff()
1405        rs.ReverseNormalsOn()
1406        rs.Update()
1407        self._update(rs.GetOutput())
1408        self.pipeline = utils.OperationNode("flip_normals", parents=[self])
1409        return self

Flip all normals orientation.

def add_gaussian_noise(self, sigma=1.0) -> Self:
1411    def add_gaussian_noise(self, sigma=1.0) -> Self:
1412        """
1413        Add gaussian noise to point positions.
1414        An extra array is added named "GaussianNoise" with the displacements.
1415
1416        Arguments:
1417            sigma : (float)
1418                nr. of standard deviations, expressed in percent of the diagonal size of mesh.
1419                Can also be a list `[sigma_x, sigma_y, sigma_z]`.
1420
1421        Example:
1422            ```python
1423            from vedo import Sphere
1424            Sphere().add_gaussian_noise(1.0).point_size(8).show().close()
1425            ```
1426        """
1427        sz = self.diagonal_size()
1428        pts = self.vertices
1429        n = len(pts)
1430        ns = (np.random.randn(n, 3) * sigma) * (sz / 100)
1431        vpts = vtki.vtkPoints()
1432        vpts.SetNumberOfPoints(n)
1433        vpts.SetData(utils.numpy2vtk(pts + ns, dtype=np.float32))
1434        self.dataset.SetPoints(vpts)
1435        self.dataset.GetPoints().Modified()
1436        self.pointdata["GaussianNoise"] = -ns
1437        self.pipeline = utils.OperationNode(
1438            "gaussian_noise", parents=[self], shape="egg", comment=f"sigma = {sigma}"
1439        )
1440        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]:
1442    def closest_point(
1443        self, pt, n=1, radius=None, return_point_id=False, return_cell_id=False
1444    ) -> Union[List[int], int, np.ndarray]:
1445        """
1446        Find the closest point(s) on a mesh given from the input point `pt`.
1447
1448        Arguments:
1449            n : (int)
1450                if greater than 1, return a list of n ordered closest points
1451            radius : (float)
1452                if given, get all points within that radius. Then n is ignored.
1453            return_point_id : (bool)
1454                return point ID instead of coordinates
1455            return_cell_id : (bool)
1456                return cell ID in which the closest point sits
1457
1458        Examples:
1459            - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py)
1460            - [fitplanes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitplanes.py)
1461            - [quadratic_morphing.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/quadratic_morphing.py)
1462
1463        .. note::
1464            The appropriate tree search locator is built on the fly and cached for speed.
1465
1466            If you want to reset it use `mymesh.point_locator=None`
1467            and / or `mymesh.cell_locator=None`.
1468        """
1469        if len(pt) != 3:
1470            pt = [pt[0], pt[1], 0]
1471
1472        # NB: every time the mesh moves or is warped the locators are set to None
1473        if ((n > 1 or radius) or (n == 1 and return_point_id)) and not return_cell_id:
1474            poly = None
1475            if not self.point_locator:
1476                poly = self.dataset
1477                self.point_locator = vtki.new("StaticPointLocator")
1478                self.point_locator.SetDataSet(poly)
1479                self.point_locator.BuildLocator()
1480
1481            ##########
1482            if radius:
1483                vtklist = vtki.vtkIdList()
1484                self.point_locator.FindPointsWithinRadius(radius, pt, vtklist)
1485            elif n > 1:
1486                vtklist = vtki.vtkIdList()
1487                self.point_locator.FindClosestNPoints(n, pt, vtklist)
1488            else:  # n==1 hence return_point_id==True
1489                ########
1490                return self.point_locator.FindClosestPoint(pt)
1491                ########
1492
1493            if return_point_id:
1494                ########
1495                return utils.vtk2numpy(vtklist)
1496                ########
1497
1498            if not poly:
1499                poly = self.dataset
1500            trgp = []
1501            for i in range(vtklist.GetNumberOfIds()):
1502                trgp_ = [0, 0, 0]
1503                vi = vtklist.GetId(i)
1504                poly.GetPoints().GetPoint(vi, trgp_)
1505                trgp.append(trgp_)
1506            ########
1507            return np.array(trgp)
1508            ########
1509
1510        else:
1511
1512            if not self.cell_locator:
1513                poly = self.dataset
1514
1515                # As per Miquel example with limbs the vtkStaticCellLocator doesnt work !!
1516                # https://discourse.vtk.org/t/vtkstaticcelllocator-problem-vtk9-0-3/7854/4
1517                if vedo.vtk_version[0] >= 9 and vedo.vtk_version[1] > 0:
1518                    self.cell_locator = vtki.new("StaticCellLocator")
1519                else:
1520                    self.cell_locator = vtki.new("CellLocator")
1521
1522                self.cell_locator.SetDataSet(poly)
1523                self.cell_locator.BuildLocator()
1524
1525            if radius is not None:
1526                vedo.printc("Warning: closest_point() with radius is not implemented for cells.", c='r')   
1527 
1528            if n != 1:
1529                vedo.printc("Warning: closest_point() with n>1 is not implemented for cells.", c='r')   
1530 
1531            trgp = [0, 0, 0]
1532            cid = vtki.mutable(0)
1533            dist2 = vtki.mutable(0)
1534            subid = vtki.mutable(0)
1535            self.cell_locator.FindClosestPoint(pt, trgp, cid, subid, dist2)
1536
1537            if return_cell_id:
1538                return int(cid)
1539
1540            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:
1542    def auto_distance(self) -> np.ndarray:
1543        """
1544        Calculate the distance to the closest point in the same cloud of points.
1545        The output is stored in a new pointdata array called "AutoDistance",
1546        and it is also returned by the function.
1547        """
1548        points = self.vertices
1549        if not self.point_locator:
1550            self.point_locator = vtki.new("StaticPointLocator")
1551            self.point_locator.SetDataSet(self.dataset)
1552            self.point_locator.BuildLocator()
1553        qs = []
1554        vtklist = vtki.vtkIdList()
1555        vtkpoints = self.dataset.GetPoints()
1556        for p in points:
1557            self.point_locator.FindClosestNPoints(2, p, vtklist)
1558            q = [0, 0, 0]
1559            pid = vtklist.GetId(1)
1560            vtkpoints.GetPoint(pid, q)
1561            qs.append(q)
1562        dists = np.linalg.norm(points - np.array(qs), axis=1)
1563        self.pointdata["AutoDistance"] = dists
1564        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:
1566    def hausdorff_distance(self, points) -> float:
1567        """
1568        Compute the Hausdorff distance to the input point set.
1569        Returns a single `float`.
1570
1571        Example:
1572            ```python
1573            from vedo import *
1574            t = np.linspace(0, 2*np.pi, 100)
1575            x = 4/3 * sin(t)**3
1576            y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12
1577            pol1 = Line(np.c_[x,y], closed=True).triangulate()
1578            pol2 = Polygon(nsides=5).pos(2,2)
1579            d12 = pol1.distance_to(pol2)
1580            d21 = pol2.distance_to(pol1)
1581            pol1.lw(0).cmap("viridis")
1582            pol2.lw(0).cmap("viridis")
1583            print("distance d12, d21 :", min(d12), min(d21))
1584            print("hausdorff distance:", pol1.hausdorff_distance(pol2))
1585            print("chamfer distance  :", pol1.chamfer_distance(pol2))
1586            show(pol1, pol2, axes=1)
1587            ```
1588            ![](https://vedo.embl.es/images/feats/heart.png)
1589        """
1590        hp = vtki.new("HausdorffDistancePointSetFilter")
1591        hp.SetInputData(0, self.dataset)
1592        hp.SetInputData(1, points.dataset)
1593        hp.SetTargetDistanceMethodToPointToCell()
1594        hp.Update()
1595        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:
1597    def chamfer_distance(self, pcloud) -> float:
1598        """
1599        Compute the Chamfer distance to the input point set.
1600
1601        Example:
1602            ```python
1603            from vedo import *
1604            cloud1 = np.random.randn(1000, 3)
1605            cloud2 = np.random.randn(1000, 3) + [1, 2, 3]
1606            c1 = Points(cloud1, r=5, c="red")
1607            c2 = Points(cloud2, r=5, c="green")
1608            d = c1.chamfer_distance(c2)
1609            show(f"Chamfer distance = {d}", c1, c2, axes=1).close()
1610            ```
1611        """
1612        # Definition of Chamfer distance may vary, here we use the average
1613        if not pcloud.point_locator:
1614            pcloud.point_locator = vtki.new("PointLocator")
1615            pcloud.point_locator.SetDataSet(pcloud.dataset)
1616            pcloud.point_locator.BuildLocator()
1617        if not self.point_locator:
1618            self.point_locator = vtki.new("PointLocator")
1619            self.point_locator.SetDataSet(self.dataset)
1620            self.point_locator.BuildLocator()
1621
1622        ps1 = self.vertices
1623        ps2 = pcloud.vertices
1624
1625        ids12 = []
1626        for p in ps1:
1627            pid12 = pcloud.point_locator.FindClosestPoint(p)
1628            ids12.append(pid12)
1629        deltav = ps2[ids12] - ps1
1630        da = np.mean(np.linalg.norm(deltav, axis=1))
1631
1632        ids21 = []
1633        for p in ps2:
1634            pid21 = self.point_locator.FindClosestPoint(p)
1635            ids21.append(pid21)
1636        deltav = ps1[ids21] - ps2
1637        db = np.mean(np.linalg.norm(deltav, axis=1))
1638        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:
1640    def remove_outliers(self, radius: float, neighbors=5) -> Self:
1641        """
1642        Remove outliers from a cloud of points within the specified `radius` search.
1643
1644        Arguments:
1645            radius : (float)
1646                Specify the local search radius.
1647            neighbors : (int)
1648                Specify the number of neighbors that a point must have,
1649                within the specified radius, for the point to not be considered isolated.
1650
1651        Examples:
1652            - [clustering.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/clustering.py)
1653
1654                ![](https://vedo.embl.es/images/basic/clustering.png)
1655        """
1656        removal = vtki.new("RadiusOutlierRemoval")
1657        removal.SetInputData(self.dataset)
1658        removal.SetRadius(radius)
1659        removal.SetNumberOfNeighbors(neighbors)
1660        removal.GenerateOutliersOff()
1661        removal.Update()
1662        inputobj = removal.GetOutput()
1663        if inputobj.GetNumberOfCells() == 0:
1664            carr = vtki.vtkCellArray()
1665            for i in range(inputobj.GetNumberOfPoints()):
1666                carr.InsertNextCell(1)
1667                carr.InsertCellPoint(i)
1668            inputobj.SetVerts(carr)
1669        self._update(removal.GetOutput())
1670        self.pipeline = utils.OperationNode("remove_outliers", parents=[self])
1671        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:
1673    def relax_point_positions(
1674            self, 
1675            n=10,
1676            iters=10,
1677            sub_iters=10,
1678            packing_factor=1,
1679            max_step=0,
1680            constraints=(),
1681        ) -> Self:
1682        """
1683        Smooth mesh or points with a 
1684        [Laplacian algorithm](https://vtk.org/doc/nightly/html/classvtkPointSmoothingFilter.html)
1685        variant. This modifies the coordinates of the input points by adjusting their positions
1686        to create a smooth distribution (and thereby form a pleasing packing of the points).
1687        Smoothing is performed by considering the effects of neighboring points on one another
1688        it uses a cubic cutoff function to produce repulsive forces between close points
1689        and attractive forces that are a little further away.
1690        
1691        In general, the larger the neighborhood size, the greater the reduction in high frequency
1692        information. The memory and computational requirements of the algorithm may also
1693        significantly increase.
1694
1695        The algorithm incrementally adjusts the point positions through an iterative process.
1696        Basically points are moved due to the influence of neighboring points. 
1697        
1698        As points move, both the local connectivity and data attributes associated with each point
1699        must be updated. Rather than performing these expensive operations after every iteration,
1700        a number of sub-iterations can be specified. If so, then the neighborhood and attribute
1701        value updates occur only every sub iteration, which can improve performance significantly.
1702        
1703        Arguments:
1704            n : (int)
1705                neighborhood size to calculate the Laplacian.
1706            iters : (int)
1707                number of iterations.
1708            sub_iters : (int)
1709                number of sub-iterations, i.e. the number of times the neighborhood and attribute
1710                value updates occur during each iteration.
1711            packing_factor : (float)
1712                adjust convergence speed.
1713            max_step : (float)
1714                Specify the maximum smoothing step size for each smoothing iteration.
1715                This limits the the distance over which a point can move in each iteration.
1716                As in all iterative methods, the stability of the process is sensitive to this parameter.
1717                In general, small step size and large numbers of iterations are more stable than a larger
1718                step size and a smaller numbers of iterations.
1719            constraints : (dict)
1720                dictionary of constraints.
1721                Point constraints are used to prevent points from moving,
1722                or to move only on a plane. This can prevent shrinking or growing point clouds.
1723                If enabled, a local topological analysis is performed to determine whether a point
1724                should be marked as fixed" i.e., never moves, or the point only moves on a plane,
1725                or the point can move freely.
1726                If all points in the neighborhood surrounding a point are in the cone defined by
1727                `fixed_angle`, then the point is classified as fixed.
1728                If all points in the neighborhood surrounding a point are in the cone defined by
1729                `boundary_angle`, then the point is classified as lying on a plane.
1730                Angles are expressed in degrees.
1731        
1732        Example:
1733            ```py
1734            import numpy as np
1735            from vedo import Points, show
1736            from vedo.pyplot import histogram
1737
1738            vpts1 = Points(np.random.rand(10_000, 3))
1739            dists = vpts1.auto_distance()
1740            h1 = histogram(dists, xlim=(0,0.08)).clone2d()
1741
1742            vpts2 = vpts1.clone().relax_point_positions(n=100, iters=20, sub_iters=10)
1743            dists = vpts2.auto_distance()
1744            h2 = histogram(dists, xlim=(0,0.08)).clone2d()
1745
1746            show([[vpts1, h1], [vpts2, h2]], N=2).close()
1747            ```
1748        """
1749        smooth = vtki.new("PointSmoothingFilter")
1750        smooth.SetInputData(self.dataset)
1751        smooth.SetSmoothingModeToUniform()
1752        smooth.SetNumberOfIterations(iters)
1753        smooth.SetNumberOfSubIterations(sub_iters)
1754        smooth.SetPackingFactor(packing_factor)
1755        if self.point_locator:
1756            smooth.SetLocator(self.point_locator)
1757        if not max_step:
1758            max_step = self.diagonal_size() / 100
1759        smooth.SetMaximumStepSize(max_step)
1760        smooth.SetNeighborhoodSize(n)
1761        if constraints:
1762            fixed_angle = constraints.get("fixed_angle", 45)
1763            boundary_angle = constraints.get("boundary_angle", 110)
1764            smooth.EnableConstraintsOn()
1765            smooth.SetFixedAngle(fixed_angle)
1766            smooth.SetBoundaryAngle(boundary_angle)
1767            smooth.GenerateConstraintScalarsOn()
1768            smooth.GenerateConstraintNormalsOn()
1769        smooth.Update()
1770        self._update(smooth.GetOutput())
1771        self.metadata["PackingRadius"] = smooth.GetPackingRadius()
1772        self.pipeline = utils.OperationNode("relax_point_positions", parents=[self])
1773        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:
1775    def smooth_mls_1d(self, f=0.2, radius=None, n=0) -> Self:
1776        """
1777        Smooth mesh or points with a `Moving Least Squares` variant.
1778        The point data array "Variances" will contain the residue calculated for each point.
1779
1780        Arguments:
1781            f : (float)
1782                smoothing factor - typical range is [0,2].
1783            radius : (float)
1784                radius search in absolute units.
1785                If set then `f` is ignored.
1786            n : (int)
1787                number of neighbours to be used for the fit.
1788                If set then `f` and `radius` are ignored.
1789
1790        Examples:
1791            - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py)
1792            - [skeletonize.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/skeletonize.py)
1793
1794            ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png)
1795        """
1796        coords = self.vertices
1797        ncoords = len(coords)
1798
1799        if n:
1800            Ncp = n
1801        elif radius:
1802            Ncp = 1
1803        else:
1804            Ncp = int(ncoords * f / 10)
1805            if Ncp < 5:
1806                vedo.logger.warning(f"Please choose a fraction higher than {f}")
1807                Ncp = 5
1808
1809        variances, newline = [], []
1810        for p in coords:
1811            points = self.closest_point(p, n=Ncp, radius=radius)
1812            if len(points) < 4:
1813                continue
1814
1815            points = np.array(points)
1816            pointsmean = points.mean(axis=0)  # plane center
1817            _, dd, vv = np.linalg.svd(points - pointsmean)
1818            newp = np.dot(p - pointsmean, vv[0]) * vv[0] + pointsmean
1819            variances.append(dd[1] + dd[2])
1820            newline.append(newp)
1821
1822        self.pointdata["Variances"] = np.array(variances).astype(np.float32)
1823        self.vertices = newline
1824        self.pipeline = utils.OperationNode("smooth_mls_1d", parents=[self])
1825        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:
1827    def smooth_mls_2d(self, f=0.2, radius=None, n=0) -> Self:
1828        """
1829        Smooth mesh or points with a `Moving Least Squares` algorithm variant.
1830
1831        The `mesh.pointdata['MLSVariance']` array will contain the residue calculated for each point.
1832        When a radius is specified, points that are isolated will not be moved and will get
1833        a 0 entry in array `mesh.pointdata['MLSValidPoint']`.
1834
1835        Arguments:
1836            f : (float)
1837                smoothing factor - typical range is [0, 2].
1838            radius : (float | array)
1839                radius search in absolute units. Can be single value (float) or sequence
1840                for adaptive smoothing. If set then `f` is ignored.
1841            n : (int)
1842                number of neighbours to be used for the fit.
1843                If set then `f` and `radius` are ignored.
1844
1845        Examples:
1846            - [moving_least_squares2D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares2D.py)
1847            - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py)
1848
1849                ![](https://vedo.embl.es/images/advanced/recosurface.png)
1850        """
1851        coords = self.vertices
1852        ncoords = len(coords)
1853
1854        if n:
1855            Ncp = n
1856            radius = None
1857        elif radius is not None:
1858            Ncp = 1
1859        else:
1860            Ncp = int(ncoords * f / 100)
1861            if Ncp < 4:
1862                vedo.logger.error(f"please choose a f-value higher than {f}")
1863                Ncp = 4
1864
1865        variances, newpts, valid = [], [], []
1866        radius_is_sequence = utils.is_sequence(radius)
1867
1868        pb = None
1869        if ncoords > 10000:
1870            pb = utils.ProgressBar(0, ncoords, delay=3)
1871
1872        for i, p in enumerate(coords):
1873            if pb:
1874                pb.print("smooth_mls_2d working ...")
1875            
1876            # if a radius was provided for each point
1877            if radius_is_sequence:
1878                pts = self.closest_point(p, n=Ncp, radius=radius[i])
1879            else:
1880                pts = self.closest_point(p, n=Ncp, radius=radius)
1881
1882            if len(pts) > 3:
1883                ptsmean = pts.mean(axis=0)  # plane center
1884                _, dd, vv = np.linalg.svd(pts - ptsmean)
1885                cv = np.cross(vv[0], vv[1])
1886                t = (np.dot(cv, ptsmean) - np.dot(cv, p)) / np.dot(cv, cv)
1887                newpts.append(p + cv * t)
1888                variances.append(dd[2])
1889                if radius is not None:
1890                    valid.append(1)
1891            else:
1892                newpts.append(p)
1893                variances.append(0)
1894                if radius is not None:
1895                    valid.append(0)
1896
1897        if radius is not None:
1898            self.pointdata["MLSValidPoint"] = np.array(valid).astype(np.uint8)
1899        self.pointdata["MLSVariance"] = np.array(variances).astype(np.float32)
1900
1901        self.vertices = newpts
1902
1903        self.pipeline = utils.OperationNode("smooth_mls_2d", parents=[self])
1904        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:
1906    def smooth_lloyd_2d(self, iterations=2, bounds=None, options="Qbb Qc Qx") -> Self:
1907        """
1908        Lloyd relaxation of a 2D pointcloud.
1909        
1910        Arguments:
1911            iterations : (int)
1912                number of iterations.
1913            bounds : (list)
1914                bounding box of the domain.
1915            options : (str)
1916                options for the Qhull algorithm.
1917        """
1918        # Credits: https://hatarilabs.com/ih-en/
1919        # tutorial-to-create-a-geospatial-voronoi-sh-mesh-with-python-scipy-and-geopandas
1920        from scipy.spatial import Voronoi as scipy_voronoi
1921
1922        def _constrain_points(points):
1923            # Update any points that have drifted beyond the boundaries of this space
1924            if bounds is not None:
1925                for point in points:
1926                    if point[0] < bounds[0]: point[0] = bounds[0]
1927                    if point[0] > bounds[1]: point[0] = bounds[1]
1928                    if point[1] < bounds[2]: point[1] = bounds[2]
1929                    if point[1] > bounds[3]: point[1] = bounds[3]
1930            return points
1931
1932        def _find_centroid(vertices):
1933            # The equation for the method used here to find the centroid of a
1934            # 2D polygon is given here: https://en.wikipedia.org/wiki/Centroid#Of_a_polygon
1935            area = 0
1936            centroid_x = 0
1937            centroid_y = 0
1938            for i in range(len(vertices) - 1):
1939                step = (vertices[i, 0] * vertices[i + 1, 1]) - (vertices[i + 1, 0] * vertices[i, 1])
1940                centroid_x += (vertices[i, 0] + vertices[i + 1, 0]) * step
1941                centroid_y += (vertices[i, 1] + vertices[i + 1, 1]) * step
1942                area += step
1943            if area:
1944                centroid_x = (1.0 / (3.0 * area)) * centroid_x
1945                centroid_y = (1.0 / (3.0 * area)) * centroid_y
1946            # prevent centroids from escaping bounding box
1947            return _constrain_points([[centroid_x, centroid_y]])[0]
1948
1949        def _relax(voron):
1950            # Moves each point to the centroid of its cell in the voronoi
1951            # map to "relax" the points (i.e. jitter the points so as
1952            # to spread them out within the space).
1953            centroids = []
1954            for idx in voron.point_region:
1955                # the region is a series of indices into voronoi.vertices
1956                # remove point at infinity, designated by index -1
1957                region = [i for i in voron.regions[idx] if i != -1]
1958                # enclose the polygon
1959                region = region + [region[0]]
1960                verts = voron.vertices[region]
1961                # find the centroid of those vertices
1962                centroids.append(_find_centroid(verts))
1963            return _constrain_points(centroids)
1964
1965        if bounds is None:
1966            bounds = self.bounds()
1967
1968        pts = self.vertices[:, (0, 1)]
1969        for i in range(iterations):
1970            vor = scipy_voronoi(pts, qhull_options=options)
1971            _constrain_points(vor.vertices)
1972            pts = _relax(vor)
1973        out = Points(pts)
1974        out.name = "MeshSmoothLloyd2D"
1975        out.pipeline = utils.OperationNode("smooth_lloyd", parents=[self])
1976        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:
1978    def project_on_plane(self, plane="z", point=None, direction=None) -> Self:
1979        """
1980        Project the mesh on one of the Cartesian planes.
1981
1982        Arguments:
1983            plane : (str, Plane)
1984                if plane is `str`, plane can be one of ['x', 'y', 'z'],
1985                represents x-plane, y-plane and z-plane, respectively.
1986                Otherwise, plane should be an instance of `vedo.shapes.Plane`.
1987            point : (float, array)
1988                if plane is `str`, point should be a float represents the intercept.
1989                Otherwise, point is the camera point of perspective projection
1990            direction : (array)
1991                direction of oblique projection
1992
1993        Note:
1994            Parameters `point` and `direction` are only used if the given plane
1995            is an instance of `vedo.shapes.Plane`. And one of these two params
1996            should be left as `None` to specify the projection type.
1997
1998        Example:
1999            ```python
2000            s.project_on_plane(plane='z') # project to z-plane
2001            plane = Plane(pos=(4, 8, -4), normal=(-1, 0, 1), s=(5,5))
2002            s.project_on_plane(plane=plane)                       # orthogonal projection
2003            s.project_on_plane(plane=plane, point=(6, 6, 6))      # perspective projection
2004            s.project_on_plane(plane=plane, direction=(1, 2, -1)) # oblique projection
2005            ```
2006
2007        Examples:
2008            - [silhouette2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette2.py)
2009
2010                ![](https://vedo.embl.es/images/basic/silhouette2.png)
2011        """
2012        coords = self.vertices
2013
2014        if plane == "x":
2015            coords[:, 0] = self.transform.position[0]
2016            intercept = self.xbounds()[0] if point is None else point
2017            self.x(intercept)
2018        elif plane == "y":
2019            coords[:, 1] = self.transform.position[1]
2020            intercept = self.ybounds()[0] if point is None else point
2021            self.y(intercept)
2022        elif plane == "z":
2023            coords[:, 2] = self.transform.position[2]
2024            intercept = self.zbounds()[0] if point is None else point
2025            self.z(intercept)
2026
2027        elif isinstance(plane, vedo.shapes.Plane):
2028            normal = plane.normal / np.linalg.norm(plane.normal)
2029            pl = np.hstack((normal, -np.dot(plane.pos(), normal))).reshape(4, 1)
2030            if direction is None and point is None:
2031                # orthogonal projection
2032                pt = np.hstack((normal, [0])).reshape(4, 1)
2033                # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T # python3 only
2034                proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T)
2035
2036            elif direction is None:
2037                # perspective projection
2038                pt = np.hstack((np.array(point), [1])).reshape(4, 1)
2039                # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T
2040                proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T)
2041
2042            elif point is None:
2043                # oblique projection
2044                pt = np.hstack((np.array(direction), [0])).reshape(4, 1)
2045                # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T
2046                proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T)
2047
2048            coords = np.concatenate([coords, np.ones((coords.shape[:-1] + (1,)))], axis=-1)
2049            # coords = coords @ proj_mat.T
2050            coords = np.matmul(coords, proj_mat.T)
2051            coords = coords[:, :3] / coords[:, 3:]
2052
2053        else:
2054            vedo.logger.error(f"unknown plane {plane}")
2055            raise RuntimeError()
2056
2057        self.alpha(0.1)
2058        self.vertices = coords
2059        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:
2061    def warp(self, source, target, sigma=1.0, mode="3d") -> Self:
2062        """
2063        "Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set
2064        of source and target landmarks. Any point on the mesh close to a source landmark will
2065        be moved to a place close to the corresponding target landmark.
2066        The points in between are interpolated smoothly using
2067        Bookstein's Thin Plate Spline algorithm.
2068
2069        Transformation object can be accessed with `mesh.transform`.
2070
2071        Arguments:
2072            sigma : (float)
2073                specify the 'stiffness' of the spline.
2074            mode : (str)
2075                set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes)
2076
2077        Examples:
2078            - [interpolate_field.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/interpolate_field.py)
2079            - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py)
2080            - [warp2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp2.py)
2081            - [warp3.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp3.py)
2082            - [warp4a.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4a.py)
2083            - [warp4b.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4b.py)
2084            - [warp6.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp6.py)
2085
2086            ![](https://vedo.embl.es/images/advanced/warp2.png)
2087        """
2088        parents = [self]
2089
2090        try:
2091            source = source.vertices
2092            parents.append(source)
2093        except AttributeError:
2094            source = utils.make3d(source)
2095        
2096        try:
2097            target = target.vertices
2098            parents.append(target)
2099        except AttributeError:
2100            target = utils.make3d(target)
2101
2102        ns = len(source)
2103        nt = len(target)
2104        if ns != nt:
2105            vedo.logger.error(f"#source {ns} != {nt} #target points")
2106            raise RuntimeError()
2107
2108        NLT = NonLinearTransform()
2109        NLT.source_points = source
2110        NLT.target_points = target
2111        self.apply_transform(NLT)
2112
2113        self.pipeline = utils.OperationNode("warp", parents=parents)
2114        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:
2116    def cut_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0), invert=False) -> Self:
2117        """
2118        Cut the mesh with the plane defined by a point and a normal.
2119
2120        Arguments:
2121            origin : (array)
2122                the cutting plane goes through this point
2123            normal : (array)
2124                normal of the cutting plane
2125
2126        Example:
2127            ```python
2128            from vedo import Cube
2129            cube = Cube().cut_with_plane(normal=(1,1,1))
2130            cube.back_color('pink').show().close()
2131            ```
2132            ![](https://vedo.embl.es/images/feats/cut_with_plane_cube.png)
2133
2134        Examples:
2135            - [trail.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/trail.py)
2136
2137                ![](https://vedo.embl.es/images/simulations/trail.gif)
2138
2139        Check out also:
2140            `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`.
2141        """
2142        s = str(normal)
2143        if "x" in s:
2144            normal = (1, 0, 0)
2145            if "-" in s:
2146                normal = -np.array(normal)
2147        elif "y" in s:
2148            normal = (0, 1, 0)
2149            if "-" in s:
2150                normal = -np.array(normal)
2151        elif "z" in s:
2152            normal = (0, 0, 1)
2153            if "-" in s:
2154                normal = -np.array(normal)
2155        plane = vtki.vtkPlane()
2156        plane.SetOrigin(origin)
2157        plane.SetNormal(normal)
2158
2159        clipper = vtki.new("ClipPolyData")
2160        clipper.SetInputData(self.dataset)
2161        clipper.SetClipFunction(plane)
2162        clipper.GenerateClippedOutputOff()
2163        clipper.GenerateClipScalarsOff()
2164        clipper.SetInsideOut(invert)
2165        clipper.SetValue(0)
2166        clipper.Update()
2167
2168        self._update(clipper.GetOutput())
2169
2170        self.pipeline = utils.OperationNode("cut_with_plane", parents=[self])
2171        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:
2173    def cut_with_planes(self, origins, normals, invert=False) -> Self:
2174        """
2175        Cut the mesh with a convex set of planes defined by points and normals.
2176
2177        Arguments:
2178            origins : (array)
2179                each cutting plane goes through this point
2180            normals : (array)
2181                normal of each of the cutting planes
2182            invert : (bool)
2183                if True, cut outside instead of inside
2184
2185        Check out also:
2186            `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`
2187        """
2188
2189        vpoints = vtki.vtkPoints()
2190        for p in utils.make3d(origins):
2191            vpoints.InsertNextPoint(p)
2192        normals = utils.make3d(normals)
2193
2194        planes = vtki.vtkPlanes()
2195        planes.SetPoints(vpoints)
2196        planes.SetNormals(utils.numpy2vtk(normals, dtype=float))
2197
2198        clipper = vtki.new("ClipPolyData")
2199        clipper.SetInputData(self.dataset)
2200        clipper.SetInsideOut(invert)
2201        clipper.SetClipFunction(planes)
2202        clipper.GenerateClippedOutputOff()
2203        clipper.GenerateClipScalarsOff()
2204        clipper.SetValue(0)
2205        clipper.Update()
2206
2207        self._update(clipper.GetOutput())
2208
2209        self.pipeline = utils.OperationNode("cut_with_planes", parents=[self])
2210        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:
2212    def cut_with_box(self, bounds, invert=False) -> Self:
2213        """
2214        Cut the current mesh with a box or a set of boxes.
2215        This is much faster than `cut_with_mesh()`.
2216
2217        Input `bounds` can be either:
2218        - a Mesh or Points object
2219        - a list of 6 number representing a bounding box `[xmin,xmax, ymin,ymax, zmin,zmax]`
2220        - a list of bounding boxes like the above: `[[xmin1,...], [xmin2,...], ...]`
2221
2222        Example:
2223            ```python
2224            from vedo import Sphere, Cube, show
2225            mesh = Sphere(r=1, res=50)
2226            box  = Cube(side=1.5).wireframe()
2227            mesh.cut_with_box(box)
2228            show(mesh, box, axes=1).close()
2229            ```
2230            ![](https://vedo.embl.es/images/feats/cut_with_box_cube.png)
2231
2232        Check out also:
2233            `cut_with_line()`, `cut_with_plane()`, `cut_with_cylinder()`
2234        """
2235        if isinstance(bounds, Points):
2236            bounds = bounds.bounds()
2237
2238        box = vtki.new("Box")
2239        if utils.is_sequence(bounds[0]):
2240            for bs in bounds:
2241                box.AddBounds(bs)
2242        else:
2243            box.SetBounds(bounds)
2244
2245        clipper = vtki.new("ClipPolyData")
2246        clipper.SetInputData(self.dataset)
2247        clipper.SetClipFunction(box)
2248        clipper.SetInsideOut(not invert)
2249        clipper.GenerateClippedOutputOff()
2250        clipper.GenerateClipScalarsOff()
2251        clipper.SetValue(0)
2252        clipper.Update()
2253        self._update(clipper.GetOutput())
2254
2255        self.pipeline = utils.OperationNode("cut_with_box", parents=[self])
2256        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:
2258    def cut_with_line(self, points, invert=False, closed=True) -> Self:
2259        """
2260        Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter.
2261        The polyline is defined by a set of points (z-coordinates are ignored).
2262        This is much faster than `cut_with_mesh()`.
2263
2264        Check out also:
2265            `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()`
2266        """
2267        pplane = vtki.new("PolyPlane")
2268        if isinstance(points, Points):
2269            points = points.vertices.tolist()
2270
2271        if closed:
2272            if isinstance(points, np.ndarray):
2273                points = points.tolist()
2274            points.append(points[0])
2275
2276        vpoints = vtki.vtkPoints()
2277        for p in points:
2278            if len(p) == 2:
2279                p = [p[0], p[1], 0.0]
2280            vpoints.InsertNextPoint(p)
2281
2282        n = len(points)
2283        polyline = vtki.new("PolyLine")
2284        polyline.Initialize(n, vpoints)
2285        polyline.GetPointIds().SetNumberOfIds(n)
2286        for i in range(n):
2287            polyline.GetPointIds().SetId(i, i)
2288        pplane.SetPolyLine(polyline)
2289
2290        clipper = vtki.new("ClipPolyData")
2291        clipper.SetInputData(self.dataset)
2292        clipper.SetClipFunction(pplane)
2293        clipper.SetInsideOut(invert)
2294        clipper.GenerateClippedOutputOff()
2295        clipper.GenerateClipScalarsOff()
2296        clipper.SetValue(0)
2297        clipper.Update()
2298        self._update(clipper.GetOutput())
2299
2300        self.pipeline = utils.OperationNode("cut_with_line", parents=[self])
2301        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:
2303    def cut_with_cookiecutter(self, lines) -> Self:
2304        """
2305        Cut the current mesh with a single line or a set of lines.
2306
2307        Input `lines` can be either:
2308        - a `Mesh` or `Points` object
2309        - a list of 3D points: `[(x1,y1,z1), (x2,y2,z2), ...]`
2310        - a list of 2D points: `[(x1,y1), (x2,y2), ...]`
2311
2312        Example:
2313            ```python
2314            from vedo import *
2315            grid = Mesh(dataurl + "dolfin_fine.vtk")
2316            grid.compute_quality().cmap("Greens")
2317            pols = merge(
2318                Polygon(nsides=10, r=0.3).pos(0.7, 0.3),
2319                Polygon(nsides=10, r=0.2).pos(0.3, 0.7),
2320            )
2321            lines = pols.boundaries()
2322            cgrid = grid.clone().cut_with_cookiecutter(lines)
2323            grid.alpha(0.1).wireframe()
2324            show(grid, cgrid, lines, axes=8, bg='blackboard').close()
2325            ```
2326            ![](https://vedo.embl.es/images/feats/cookiecutter.png)
2327
2328        Check out also:
2329            `cut_with_line()` and `cut_with_point_loop()`
2330
2331        Note:
2332            In case of a warning message like:
2333                "Mesh and trim loop point data attributes are different"
2334            consider interpolating the mesh point data to the loop points,
2335            Eg. (in the above example):
2336            ```python
2337            lines = pols.boundaries().interpolate_data_from(grid, n=2)
2338            ```
2339
2340        Note:
2341            trying to invert the selection by reversing the loop order
2342            will have no effect in this method, hence it does not have
2343            the `invert` option.
2344        """
2345        if utils.is_sequence(lines):
2346            lines = utils.make3d(lines)
2347            iline = list(range(len(lines))) + [0]
2348            poly = utils.buildPolyData(lines, lines=[iline])
2349        else:
2350            poly = lines.dataset
2351
2352        # if invert: # not working
2353        #     rev = vtki.new("ReverseSense")
2354        #     rev.ReverseCellsOn()
2355        #     rev.SetInputData(poly)
2356        #     rev.Update()
2357        #     poly = rev.GetOutput()
2358
2359        # Build loops from the polyline
2360        build_loops = vtki.new("ContourLoopExtraction")
2361        build_loops.SetGlobalWarningDisplay(0)
2362        build_loops.SetInputData(poly)
2363        build_loops.Update()
2364        boundary_poly = build_loops.GetOutput()
2365
2366        ccut = vtki.new("CookieCutter")
2367        ccut.SetInputData(self.dataset)
2368        ccut.SetLoopsData(boundary_poly)
2369        ccut.SetPointInterpolationToMeshEdges()
2370        # ccut.SetPointInterpolationToLoopEdges()
2371        ccut.PassCellDataOn()
2372        ccut.PassPointDataOn()
2373        ccut.Update()
2374        self._update(ccut.GetOutput())
2375
2376        self.pipeline = utils.OperationNode("cut_with_cookiecutter", parents=[self])
2377        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:
2379    def cut_with_cylinder(self, center=(0, 0, 0), axis=(0, 0, 1), r=1, invert=False) -> Self:
2380        """
2381        Cut the current mesh with an infinite cylinder.
2382        This is much faster than `cut_with_mesh()`.
2383
2384        Arguments:
2385            center : (array)
2386                the center of the cylinder
2387            normal : (array)
2388                direction of the cylinder axis
2389            r : (float)
2390                radius of the cylinder
2391
2392        Example:
2393            ```python
2394            from vedo import Disc, show
2395            disc = Disc(r1=1, r2=1.2)
2396            mesh = disc.extrude(3, res=50).linewidth(1)
2397            mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True)
2398            show(mesh, axes=1).close()
2399            ```
2400            ![](https://vedo.embl.es/images/feats/cut_with_cylinder.png)
2401
2402        Examples:
2403            - [optics_main1.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/optics_main1.py)
2404
2405        Check out also:
2406            `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()`
2407        """
2408        s = str(axis)
2409        if "x" in s:
2410            axis = (1, 0, 0)
2411        elif "y" in s:
2412            axis = (0, 1, 0)
2413        elif "z" in s:
2414            axis = (0, 0, 1)
2415        cyl = vtki.new("Cylinder")
2416        cyl.SetCenter(center)
2417        cyl.SetAxis(axis[0], axis[1], axis[2])
2418        cyl.SetRadius(r)
2419
2420        clipper = vtki.new("ClipPolyData")
2421        clipper.SetInputData(self.dataset)
2422        clipper.SetClipFunction(cyl)
2423        clipper.SetInsideOut(not invert)
2424        clipper.GenerateClippedOutputOff()
2425        clipper.GenerateClipScalarsOff()
2426        clipper.SetValue(0)
2427        clipper.Update()
2428        self._update(clipper.GetOutput())
2429
2430        self.pipeline = utils.OperationNode("cut_with_cylinder", parents=[self])
2431        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:
2433    def cut_with_sphere(self, center=(0, 0, 0), r=1.0, invert=False) -> Self:
2434        """
2435        Cut the current mesh with an sphere.
2436        This is much faster than `cut_with_mesh()`.
2437
2438        Arguments:
2439            center : (array)
2440                the center of the sphere
2441            r : (float)
2442                radius of the sphere
2443
2444        Example:
2445            ```python
2446            from vedo import Disc, show
2447            disc = Disc(r1=1, r2=1.2)
2448            mesh = disc.extrude(3, res=50).linewidth(1)
2449            mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True)
2450            show(mesh, axes=1).close()
2451            ```
2452            ![](https://vedo.embl.es/images/feats/cut_with_sphere.png)
2453
2454        Check out also:
2455            `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()`
2456        """
2457        sph = vtki.new("Sphere")
2458        sph.SetCenter(center)
2459        sph.SetRadius(r)
2460
2461        clipper = vtki.new("ClipPolyData")
2462        clipper.SetInputData(self.dataset)
2463        clipper.SetClipFunction(sph)
2464        clipper.SetInsideOut(not invert)
2465        clipper.GenerateClippedOutputOff()
2466        clipper.GenerateClipScalarsOff()
2467        clipper.SetValue(0)
2468        clipper.Update()
2469        self._update(clipper.GetOutput())
2470        self.pipeline = utils.OperationNode("cut_with_sphere", parents=[self])
2471        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]:
2473    def cut_with_mesh(self, mesh, invert=False, keep=False) -> Union[Self, "vedo.Assembly"]:
2474        """
2475        Cut an `Mesh` mesh with another `Mesh`.
2476
2477        Use `invert` to invert the selection.
2478
2479        Use `keep` to keep the cutoff part, in this case an `Assembly` is returned:
2480        the "cut" object and the "discarded" part of the original object.
2481        You can access both via `assembly.unpack()` method.
2482
2483        Example:
2484        ```python
2485        from vedo import *
2486        arr = np.random.randn(100000, 3)/2
2487        pts = Points(arr).c('red3').pos(5,0,0)
2488        cube = Cube().pos(4,0.5,0)
2489        assem = pts.cut_with_mesh(cube, keep=True)
2490        show(assem.unpack(), axes=1).close()
2491        ```
2492        ![](https://vedo.embl.es/images/feats/cut_with_mesh.png)
2493
2494       Check out also:
2495            `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()`
2496       """
2497        polymesh = mesh.dataset
2498        poly = self.dataset
2499
2500        # Create an array to hold distance information
2501        signed_distances = vtki.vtkFloatArray()
2502        signed_distances.SetNumberOfComponents(1)
2503        signed_distances.SetName("SignedDistances")
2504
2505        # implicit function that will be used to slice the mesh
2506        ippd = vtki.new("ImplicitPolyDataDistance")
2507        ippd.SetInput(polymesh)
2508
2509        # Evaluate the signed distance function at all of the grid points
2510        for pointId in range(poly.GetNumberOfPoints()):
2511            p = poly.GetPoint(pointId)
2512            signed_distance = ippd.EvaluateFunction(p)
2513            signed_distances.InsertNextValue(signed_distance)
2514
2515        currentscals = poly.GetPointData().GetScalars()
2516        if currentscals:
2517            currentscals = currentscals.GetName()
2518
2519        poly.GetPointData().AddArray(signed_distances)
2520        poly.GetPointData().SetActiveScalars("SignedDistances")
2521
2522        clipper = vtki.new("ClipPolyData")
2523        clipper.SetInputData(poly)
2524        clipper.SetInsideOut(not invert)
2525        clipper.SetGenerateClippedOutput(keep)
2526        clipper.SetValue(0.0)
2527        clipper.Update()
2528        cpoly = clipper.GetOutput()
2529
2530        if keep:
2531            kpoly = clipper.GetOutput(1)
2532
2533        vis = False
2534        if currentscals:
2535            cpoly.GetPointData().SetActiveScalars(currentscals)
2536            vis = self.mapper.GetScalarVisibility()
2537
2538        self._update(cpoly)
2539
2540        self.pointdata.remove("SignedDistances")
2541        self.mapper.SetScalarVisibility(vis)
2542        if keep:
2543            if isinstance(self, vedo.Mesh):
2544                cutoff = vedo.Mesh(kpoly)
2545            else:
2546                cutoff = vedo.Points(kpoly)
2547            # cutoff = self.__class__(kpoly) # this does not work properly
2548            cutoff.properties = vtki.vtkProperty()
2549            cutoff.properties.DeepCopy(self.properties)
2550            cutoff.actor.SetProperty(cutoff.properties)
2551            cutoff.c("k5").alpha(0.2)
2552            return vedo.Assembly([self, cutoff])
2553
2554        self.pipeline = utils.OperationNode("cut_with_mesh", parents=[self, mesh])
2555        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:
2557    def cut_with_point_loop(
2558        self, points, invert=False, on="points", include_boundary=False
2559    ) -> Self:
2560        """
2561        Cut an `Mesh` object with a set of points forming a closed loop.
2562
2563        Arguments:
2564            invert : (bool)
2565                invert selection (inside-out)
2566            on : (str)
2567                if 'cells' will extract the whole cells lying inside (or outside) the point loop
2568            include_boundary : (bool)
2569                include cells lying exactly on the boundary line. Only relevant on 'cells' mode
2570
2571        Examples:
2572            - [cut_with_points1.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points1.py)
2573
2574                ![](https://vedo.embl.es/images/advanced/cutWithPoints1.png)
2575
2576            - [cut_with_points2.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points2.py)
2577
2578                ![](https://vedo.embl.es/images/advanced/cutWithPoints2.png)
2579        """
2580        if isinstance(points, Points):
2581            parents = [points]
2582            vpts = points.dataset.GetPoints()
2583            points = points.vertices
2584        else:
2585            parents = [self]
2586            vpts = vtki.vtkPoints()
2587            points = utils.make3d(points)
2588            for p in points:
2589                vpts.InsertNextPoint(p)
2590
2591        if "cell" in on:
2592            ippd = vtki.new("ImplicitSelectionLoop")
2593            ippd.SetLoop(vpts)
2594            ippd.AutomaticNormalGenerationOn()
2595            clipper = vtki.new("ExtractPolyDataGeometry")
2596            clipper.SetInputData(self.dataset)
2597            clipper.SetImplicitFunction(ippd)
2598            clipper.SetExtractInside(not invert)
2599            clipper.SetExtractBoundaryCells(include_boundary)
2600        else:
2601            spol = vtki.new("SelectPolyData")
2602            spol.SetLoop(vpts)
2603            spol.GenerateSelectionScalarsOn()
2604            spol.GenerateUnselectedOutputOff()
2605            spol.SetInputData(self.dataset)
2606            spol.Update()
2607            clipper = vtki.new("ClipPolyData")
2608            clipper.SetInputData(spol.GetOutput())
2609            clipper.SetInsideOut(not invert)
2610            clipper.SetValue(0.0)
2611        clipper.Update()
2612        self._update(clipper.GetOutput())
2613
2614        self.pipeline = utils.OperationNode("cut_with_pointloop", parents=parents)
2615        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:
2617    def cut_with_scalar(self, value: float, name="", invert=False) -> Self:
2618        """
2619        Cut a mesh or point cloud with some input scalar point-data.
2620
2621        Arguments:
2622            value : (float)
2623                cutting value
2624            name : (str)
2625                array name of the scalars to be used
2626            invert : (bool)
2627                flip selection
2628
2629        Example:
2630            ```python
2631            from vedo import *
2632            s = Sphere().lw(1)
2633            pts = s.vertices
2634            scalars = np.sin(3*pts[:,2]) + pts[:,0]
2635            s.pointdata["somevalues"] = scalars
2636            s.cut_with_scalar(0.3)
2637            s.cmap("Spectral", "somevalues").add_scalarbar()
2638            s.show(axes=1).close()
2639            ```
2640            ![](https://vedo.embl.es/images/feats/cut_with_scalars.png)
2641        """
2642        if name:
2643            self.pointdata.select(name)
2644        clipper = vtki.new("ClipPolyData")
2645        clipper.SetInputData(self.dataset)
2646        clipper.SetValue(value)
2647        clipper.GenerateClippedOutputOff()
2648        clipper.SetInsideOut(not invert)
2649        clipper.Update()
2650        self._update(clipper.GetOutput())
2651        self.pipeline = utils.OperationNode("cut_with_scalar", parents=[self])
2652        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:
2654    def crop(self,
2655             top=None, bottom=None, right=None, left=None, front=None, back=None,
2656             bounds=()) -> Self:
2657        """
2658        Crop an `Mesh` object.
2659
2660        Arguments:
2661            top : (float)
2662                fraction to crop from the top plane (positive z)
2663            bottom : (float)
2664                fraction to crop from the bottom plane (negative z)
2665            front : (float)
2666                fraction to crop from the front plane (positive y)
2667            back : (float)
2668                fraction to crop from the back plane (negative y)
2669            right : (float)
2670                fraction to crop from the right plane (positive x)
2671            left : (float)
2672                fraction to crop from the left plane (negative x)
2673            bounds : (list)
2674                bounding box of the crop region as `[x0,x1, y0,y1, z0,z1]`
2675
2676        Example:
2677            ```python
2678            from vedo import Sphere
2679            Sphere().crop(right=0.3, left=0.1).show()
2680            ```
2681            ![](https://user-images.githubusercontent.com/32848391/57081955-0ef1e800-6cf6-11e9-99de-b45220939bc9.png)
2682        """
2683        if not len(bounds):
2684            pos = np.array(self.pos())
2685            x0, x1, y0, y1, z0, z1 = self.bounds()
2686            x0, y0, z0 = [x0, y0, z0] - pos
2687            x1, y1, z1 = [x1, y1, z1] - pos
2688
2689            dx, dy, dz = x1 - x0, y1 - y0, z1 - z0
2690            if top:
2691                z1 = z1 - top * dz
2692            if bottom:
2693                z0 = z0 + bottom * dz
2694            if front:
2695                y1 = y1 - front * dy
2696            if back:
2697                y0 = y0 + back * dy
2698            if right:
2699                x1 = x1 - right * dx
2700            if left:
2701                x0 = x0 + left * dx
2702            bounds = (x0, x1, y0, y1, z0, z1)
2703
2704        cu = vtki.new("Box")
2705        cu.SetBounds(bounds)
2706
2707        clipper = vtki.new("ClipPolyData")
2708        clipper.SetInputData(self.dataset)
2709        clipper.SetClipFunction(cu)
2710        clipper.InsideOutOn()
2711        clipper.GenerateClippedOutputOff()
2712        clipper.GenerateClipScalarsOff()
2713        clipper.SetValue(0)
2714        clipper.Update()
2715        self._update(clipper.GetOutput())
2716
2717        self.pipeline = utils.OperationNode(
2718            "crop", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
2719        )
2720        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:
2722    def generate_surface_halo(
2723            self, 
2724            distance=0.05,
2725            res=(50, 50, 50),
2726            bounds=(),
2727            maxdist=None,
2728    ) -> "vedo.Mesh":
2729        """
2730        Generate the surface halo which sits at the specified distance from the input one.
2731
2732        Arguments:
2733            distance : (float)
2734                distance from the input surface
2735            res : (int)
2736                resolution of the surface
2737            bounds : (list)
2738                bounding box of the surface
2739            maxdist : (float)
2740                maximum distance to generate the surface
2741        """
2742        if not bounds:
2743            bounds = self.bounds()
2744
2745        if not maxdist:
2746            maxdist = self.diagonal_size() / 2
2747
2748        imp = vtki.new("ImplicitModeller")
2749        imp.SetInputData(self.dataset)
2750        imp.SetSampleDimensions(res)
2751        if maxdist:
2752            imp.SetMaximumDistance(maxdist)
2753        if len(bounds) == 6:
2754            imp.SetModelBounds(bounds)
2755        contour = vtki.new("ContourFilter")
2756        contour.SetInputConnection(imp.GetOutputPort())
2757        contour.SetValue(0, distance)
2758        contour.Update()
2759        out = vedo.Mesh(contour.GetOutput())
2760        out.c("lightblue").alpha(0.25).lighting("off")
2761        out.pipeline = utils.OperationNode("generate_surface_halo", parents=[self])
2762        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:
2764    def generate_mesh(
2765        self,
2766        line_resolution=None,
2767        mesh_resolution=None,
2768        smooth=0.0,
2769        jitter=0.001,
2770        grid=None,
2771        quads=False,
2772        invert=False,
2773    ) -> Self:
2774        """
2775        Generate a polygonal Mesh from a closed contour line.
2776        If line is not closed it will be closed with a straight segment.
2777
2778        Check also `generate_delaunay2d()`.
2779
2780        Arguments:
2781            line_resolution : (int)
2782                resolution of the contour line. The default is None, in this case
2783                the contour is not resampled.
2784            mesh_resolution : (int)
2785                resolution of the internal triangles not touching the boundary.
2786            smooth : (float)
2787                smoothing of the contour before meshing.
2788            jitter : (float)
2789                add a small noise to the internal points.
2790            grid : (Grid)
2791                manually pass a Grid object. The default is True.
2792            quads : (bool)
2793                generate a mesh of quads instead of triangles.
2794            invert : (bool)
2795                flip the line orientation. The default is False.
2796
2797        Examples:
2798            - [line2mesh_tri.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_tri.py)
2799
2800                ![](https://vedo.embl.es/images/advanced/line2mesh_tri.jpg)
2801
2802            - [line2mesh_quads.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_quads.py)
2803
2804                ![](https://vedo.embl.es/images/advanced/line2mesh_quads.png)
2805        """
2806        if line_resolution is None:
2807            contour = vedo.shapes.Line(self.vertices)
2808        else:
2809            contour = vedo.shapes.Spline(self.vertices, smooth=smooth, res=line_resolution)
2810        contour.clean()
2811
2812        length = contour.length()
2813        density = length / contour.npoints
2814        # print(f"tomesh():\n\tline length = {length}")
2815        # print(f"\tdensity = {density} length/pt_separation")
2816
2817        x0, x1 = contour.xbounds()
2818        y0, y1 = contour.ybounds()
2819
2820        if grid is None:
2821            if mesh_resolution is None:
2822                resx = int((x1 - x0) / density + 0.5)
2823                resy = int((y1 - y0) / density + 0.5)
2824                # print(f"tmesh_resolution = {[resx, resy]}")
2825            else:
2826                if utils.is_sequence(mesh_resolution):
2827                    resx, resy = mesh_resolution
2828                else:
2829                    resx, resy = mesh_resolution, mesh_resolution
2830            grid = vedo.shapes.Grid(
2831                [(x0 + x1) / 2, (y0 + y1) / 2, 0],
2832                s=((x1 - x0) * 1.025, (y1 - y0) * 1.025),
2833                res=(resx, resy),
2834            )
2835        else:
2836            grid = grid.clone()
2837
2838        cpts = contour.vertices
2839
2840        # make sure it's closed
2841        p0, p1 = cpts[0], cpts[-1]
2842        nj = max(2, int(utils.mag(p1 - p0) / density + 0.5))
2843        joinline = vedo.shapes.Line(p1, p0, res=nj)
2844        contour = vedo.merge(contour, joinline).subsample(0.0001)
2845
2846        ####################################### quads
2847        if quads:
2848            cmesh = grid.clone().cut_with_point_loop(contour, on="cells", invert=invert)
2849            cmesh.wireframe(False).lw(0.5)
2850            cmesh.pipeline = utils.OperationNode(
2851                "generate_mesh",
2852                parents=[self, contour],
2853                comment=f"#quads {cmesh.dataset.GetNumberOfCells()}",
2854            )
2855            return cmesh
2856        #############################################
2857
2858        grid_tmp = grid.vertices.copy()
2859
2860        if jitter:
2861            np.random.seed(0)
2862            sigma = 1.0 / np.sqrt(grid.npoints) * grid.diagonal_size() * jitter
2863            # print(f"\tsigma jittering = {sigma}")
2864            grid_tmp += np.random.rand(grid.npoints, 3) * sigma
2865            grid_tmp[:, 2] = 0.0
2866
2867        todel = []
2868        density /= np.sqrt(3)
2869        vgrid_tmp = Points(grid_tmp)
2870
2871        for p in contour.vertices:
2872            out = vgrid_tmp.closest_point(p, radius=density, return_point_id=True)
2873            todel += out.tolist()
2874
2875        grid_tmp = grid_tmp.tolist()
2876        for index in sorted(list(set(todel)), reverse=True):
2877            del grid_tmp[index]
2878
2879        points = contour.vertices.tolist() + grid_tmp
2880        if invert:
2881            boundary = list(reversed(range(contour.npoints)))
2882        else:
2883            boundary = list(range(contour.npoints))
2884
2885        dln = Points(points).generate_delaunay2d(mode="xy", boundaries=[boundary])
2886        dln.compute_normals(points=False)  # fixes reversd faces
2887        dln.lw(1)
2888
2889        dln.pipeline = utils.OperationNode(
2890            "generate_mesh",
2891            parents=[self, contour],
2892            comment=f"#cells {dln.dataset.GetNumberOfCells()}",
2893        )
2894        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:
2896    def reconstruct_surface(
2897        self,
2898        dims=(100, 100, 100),
2899        radius=None,
2900        sample_size=None,
2901        hole_filling=True,
2902        bounds=(),
2903        padding=0.05,
2904    ) -> "vedo.Mesh":
2905        """
2906        Surface reconstruction from a scattered cloud of points.
2907
2908        Arguments:
2909            dims : (int)
2910                number of voxels in x, y and z to control precision.
2911            radius : (float)
2912                radius of influence of each point.
2913                Smaller values generally improve performance markedly.
2914                Note that after the signed distance function is computed,
2915                any voxel taking on the value >= radius
2916                is presumed to be "unseen" or uninitialized.
2917            sample_size : (int)
2918                if normals are not present
2919                they will be calculated using this sample size per point.
2920            hole_filling : (bool)
2921                enables hole filling, this generates
2922                separating surfaces between the empty and unseen portions of the volume.
2923            bounds : (list)
2924                region in space in which to perform the sampling
2925                in format (xmin,xmax, ymin,ymax, zim, zmax)
2926            padding : (float)
2927                increase by this fraction the bounding box
2928
2929        Examples:
2930            - [recosurface.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/recosurface.py)
2931
2932                ![](https://vedo.embl.es/images/advanced/recosurface.png)
2933        """
2934        if not utils.is_sequence(dims):
2935            dims = (dims, dims, dims)
2936
2937        sdf = vtki.new("SignedDistance")
2938
2939        if len(bounds) == 6:
2940            sdf.SetBounds(bounds)
2941        else:
2942            x0, x1, y0, y1, z0, z1 = self.bounds()
2943            sdf.SetBounds(
2944                x0 - (x1 - x0) * padding,
2945                x1 + (x1 - x0) * padding,
2946                y0 - (y1 - y0) * padding,
2947                y1 + (y1 - y0) * padding,
2948                z0 - (z1 - z0) * padding,
2949                z1 + (z1 - z0) * padding,
2950            )
2951        
2952        bb = sdf.GetBounds()
2953        if bb[0]==bb[1]:
2954            vedo.logger.warning("reconstruct_surface(): zero x-range")
2955        if bb[2]==bb[3]:
2956            vedo.logger.warning("reconstruct_surface(): zero y-range")
2957        if bb[4]==bb[5]:
2958            vedo.logger.warning("reconstruct_surface(): zero z-range")
2959
2960        pd = self.dataset
2961
2962        if pd.GetPointData().GetNormals():
2963            sdf.SetInputData(pd)
2964        else:
2965            normals = vtki.new("PCANormalEstimation")
2966            normals.SetInputData(pd)
2967            if not sample_size:
2968                sample_size = int(pd.GetNumberOfPoints() / 50)
2969            normals.SetSampleSize(sample_size)
2970            normals.SetNormalOrientationToGraphTraversal()
2971            sdf.SetInputConnection(normals.GetOutputPort())
2972            # print("Recalculating normals with sample size =", sample_size)
2973
2974        if radius is None:
2975            radius = self.diagonal_size() / (sum(dims) / 3) * 5
2976            # print("Calculating mesh from points with radius =", radius)
2977
2978        sdf.SetRadius(radius)
2979        sdf.SetDimensions(dims)
2980        sdf.Update()
2981
2982        surface = vtki.new("ExtractSurface")
2983        surface.SetRadius(radius * 0.99)
2984        surface.SetHoleFilling(hole_filling)
2985        surface.ComputeNormalsOff()
2986        surface.ComputeGradientsOff()
2987        surface.SetInputConnection(sdf.GetOutputPort())
2988        surface.Update()
2989        m = vedo.mesh.Mesh(surface.GetOutput(), c=self.color())
2990
2991        m.pipeline = utils.OperationNode(
2992            "reconstruct_surface",
2993            parents=[self],
2994            comment=f"#pts {m.dataset.GetNumberOfPoints()}",
2995        )
2996        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:
2998    def compute_clustering(self, radius: float) -> Self:
2999        """
3000        Cluster points in space. The `radius` is the radius of local search.
3001        
3002        An array named "ClusterId" is added to `pointdata`.
3003
3004        Examples:
3005            - [clustering.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/clustering.py)
3006
3007                ![](https://vedo.embl.es/images/basic/clustering.png)
3008        """
3009        cluster = vtki.new("EuclideanClusterExtraction")
3010        cluster.SetInputData(self.dataset)
3011        cluster.SetExtractionModeToAllClusters()
3012        cluster.SetRadius(radius)
3013        cluster.ColorClustersOn()
3014        cluster.Update()
3015        idsarr = cluster.GetOutput().GetPointData().GetArray("ClusterId")
3016        self.dataset.GetPointData().AddArray(idsarr)
3017        self.pipeline = utils.OperationNode(
3018            "compute_clustering", parents=[self], comment=f"radius = {radius}"
3019        )
3020        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:
3022    def compute_connections(self, radius, mode=0, regions=(), vrange=(0, 1), seeds=(), angle=0.0) -> Self:
3023        """
3024        Extracts and/or segments points from a point cloud based on geometric distance measures
3025        (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range.
3026        The default operation is to segment the points into "connected" regions where the connection
3027        is determined by an appropriate distance measure. Each region is given a region id.
3028
3029        Optionally, the filter can output the largest connected region of points; a particular region
3030        (via id specification); those regions that are seeded using a list of input point ids;
3031        or the region of points closest to a specified position.
3032
3033        The key parameter of this filter is the radius defining a sphere around each point which defines
3034        a local neighborhood: any other points in the local neighborhood are assumed connected to the point.
3035        Note that the radius is defined in absolute terms.
3036
3037        Other parameters are used to further qualify what it means to be a neighboring point.
3038        For example, scalar range and/or point normals can be used to further constrain the neighborhood.
3039        Also the extraction mode defines how the filter operates.
3040        By default, all regions are extracted but it is possible to extract particular regions;
3041        the region closest to a seed point; seeded regions; or the largest region found while processing.
3042        By default, all regions are extracted.
3043
3044        On output, all points are labeled with a region number.
3045        However note that the number of input and output points may not be the same:
3046        if not extracting all regions then the output size may be less than the input size.
3047
3048        Arguments:
3049            radius : (float)
3050                variable specifying a local sphere used to define local point neighborhood
3051            mode : (int)
3052                - 0,  Extract all regions
3053                - 1,  Extract point seeded regions
3054                - 2,  Extract largest region
3055                - 3,  Test specified regions
3056                - 4,  Extract all regions with scalar connectivity
3057                - 5,  Extract point seeded regions
3058            regions : (list)
3059                a list of non-negative regions id to extract
3060            vrange : (list)
3061                scalar range to use to extract points based on scalar connectivity
3062            seeds : (list)
3063                a list of non-negative point seed ids
3064            angle : (list)
3065                points are connected if the angle between their normals is
3066                within this angle threshold (expressed in degrees).
3067        """
3068        # https://vtk.org/doc/nightly/html/classvtkConnectedPointsFilter.html
3069        cpf = vtki.new("ConnectedPointsFilter")
3070        cpf.SetInputData(self.dataset)
3071        cpf.SetRadius(radius)
3072        if mode == 0:  # Extract all regions
3073            pass
3074
3075        elif mode == 1:  # Extract point seeded regions
3076            cpf.SetExtractionModeToPointSeededRegions()
3077            for s in seeds:
3078                cpf.AddSeed(s)
3079
3080        elif mode == 2:  # Test largest region
3081            cpf.SetExtractionModeToLargestRegion()
3082
3083        elif mode == 3:  # Test specified regions
3084            cpf.SetExtractionModeToSpecifiedRegions()
3085            for r in regions:
3086                cpf.AddSpecifiedRegion(r)
3087
3088        elif mode == 4:  # Extract all regions with scalar connectivity
3089            cpf.SetExtractionModeToLargestRegion()
3090            cpf.ScalarConnectivityOn()
3091            cpf.SetScalarRange(vrange[0], vrange[1])
3092
3093        elif mode == 5:  # Extract point seeded regions
3094            cpf.SetExtractionModeToLargestRegion()
3095            cpf.ScalarConnectivityOn()
3096            cpf.SetScalarRange(vrange[0], vrange[1])
3097            cpf.AlignedNormalsOn()
3098            cpf.SetNormalAngle(angle)
3099
3100        cpf.Update()
3101        self._update(cpf.GetOutput(), reset_locators=False)
3102        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:
3104    def compute_camera_distance(self) -> np.ndarray:
3105        """
3106        Calculate the distance from points to the camera.
3107        
3108        A pointdata array is created with name 'DistanceToCamera' and returned.
3109        """
3110        if vedo.plotter_instance and vedo.plotter_instance.renderer:
3111            poly = self.dataset
3112            dc = vtki.new("DistanceToCamera")
3113            dc.SetInputData(poly)
3114            dc.SetRenderer(vedo.plotter_instance.renderer)
3115            dc.Update()
3116            self._update(dc.GetOutput(), reset_locators=False)
3117            return self.pointdata["DistanceToCamera"]
3118        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:
3120    def densify(self, target_distance=0.1, nclosest=6, radius=None, niter=1, nmax=None) -> Self:
3121        """
3122        Return a copy of the cloud with new added points.
3123        The new points are created in such a way that all points in any local neighborhood are
3124        within a target distance of one another.
3125
3126        For each input point, the distance to all points in its neighborhood is computed.
3127        If any of its neighbors is further than the target distance,
3128        the edge connecting the point and its neighbor is bisected and
3129        a new point is inserted at the bisection point.
3130        A single pass is completed once all the input points are visited.
3131        Then the process repeats to the number of iterations.
3132
3133        Examples:
3134            - [densifycloud.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/densifycloud.py)
3135
3136                ![](https://vedo.embl.es/images/volumetric/densifycloud.png)
3137
3138        .. note::
3139            Points will be created in an iterative fashion until all points in their
3140            local neighborhood are the target distance apart or less.
3141            Note that the process may terminate early due to the
3142            number of iterations. By default the target distance is set to 0.5.
3143            Note that the target_distance should be less than the radius
3144            or nothing will change on output.
3145
3146        .. warning::
3147            This class can generate a lot of points very quickly.
3148            The maximum number of iterations is by default set to =1.0 for this reason.
3149            Increase the number of iterations very carefully.
3150            Also, `nmax` can be set to limit the explosion of points.
3151            It is also recommended that a N closest neighborhood is used.
3152
3153        """
3154        src = vtki.new("ProgrammableSource")
3155        opts = self.vertices
3156
3157        def _read_points():
3158            output = src.GetPolyDataOutput()
3159            points = vtki.vtkPoints()
3160            for p in opts:
3161                points.InsertNextPoint(p)
3162            output.SetPoints(points)
3163
3164        src.SetExecuteMethod(_read_points)
3165
3166        dens = vtki.new("DensifyPointCloudFilter")
3167        dens.SetInputConnection(src.GetOutputPort())
3168        dens.InterpolateAttributeDataOn()
3169        dens.SetTargetDistance(target_distance)
3170        dens.SetMaximumNumberOfIterations(niter)
3171        if nmax:
3172            dens.SetMaximumNumberOfPoints(nmax)
3173
3174        if radius:
3175            dens.SetNeighborhoodTypeToRadius()
3176            dens.SetRadius(radius)
3177        elif nclosest:
3178            dens.SetNeighborhoodTypeToNClosest()
3179            dens.SetNumberOfClosestPoints(nclosest)
3180        else:
3181            vedo.logger.error("set either radius or nclosest")
3182            raise RuntimeError()
3183        dens.Update()
3184        pts = utils.vtk2numpy(dens.GetOutput().GetPoints().GetData())
3185        cld = Points(pts, c=None).point_size(self.properties.GetPointSize())
3186        cld.interpolate_data_from(self, n=nclosest, radius=radius)
3187        cld.name = "DensifiedCloud"
3188
3189        cld.pipeline = utils.OperationNode(
3190            "densify",
3191            parents=[self],
3192            c="#e9c46a:",
3193            comment=f"#pts {cld.dataset.GetNumberOfPoints()}",
3194        )
3195        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:
3201    def density(
3202        self, dims=(40, 40, 40), bounds=None, radius=None, compute_gradient=False, locator=None
3203    ) -> "vedo.Volume":
3204        """
3205        Generate a density field from a point cloud. Input can also be a set of 3D coordinates.
3206        Output is a `Volume`.
3207
3208        The local neighborhood is specified as the `radius` around each sample position (each voxel).
3209        If left to None, the radius is automatically computed as the diagonal of the bounding box
3210        and can be accessed via `vol.metadata["radius"]`.
3211        The density is expressed as the number of counts in the radius search.
3212
3213        Arguments:
3214            dims : (int, list)
3215                number of voxels in x, y and z of the output Volume.
3216            compute_gradient : (bool)
3217                Turn on/off the generation of the gradient vector,
3218                gradient magnitude scalar, and function classification scalar.
3219                By default this is off. Note that this will increase execution time
3220                and the size of the output. (The names of these point data arrays are:
3221                "Gradient", "Gradient Magnitude", and "Classification")
3222            locator : (vtkPointLocator)
3223                can be assigned from a previous call for speed (access it via `object.point_locator`).
3224
3225        Examples:
3226            - [plot_density3d.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/plot_density3d.py)
3227
3228                ![](https://vedo.embl.es/images/pyplot/plot_density3d.png)
3229        """
3230        pdf = vtki.new("PointDensityFilter")
3231        pdf.SetInputData(self.dataset)
3232
3233        if not utils.is_sequence(dims):
3234            dims = [dims, dims, dims]
3235
3236        if bounds is None:
3237            bounds = list(self.bounds())
3238        elif len(bounds) == 4:
3239            bounds = [*bounds, 0, 0]
3240
3241        if bounds[5] - bounds[4] == 0 or len(dims) == 2:  # its 2D
3242            dims = list(dims)
3243            dims = [dims[0], dims[1], 2]
3244            diag = self.diagonal_size()
3245            bounds[5] = bounds[4] + diag / 1000
3246        pdf.SetModelBounds(bounds)
3247
3248        pdf.SetSampleDimensions(dims)
3249
3250        if locator:
3251            pdf.SetLocator(locator)
3252
3253        pdf.SetDensityEstimateToFixedRadius()
3254        if radius is None:
3255            radius = self.diagonal_size() / 20
3256        pdf.SetRadius(radius)
3257        pdf.SetComputeGradient(compute_gradient)
3258        pdf.Update()
3259
3260        vol = vedo.Volume(pdf.GetOutput()).mode(1)
3261        vol.name = "PointDensity"
3262        vol.metadata["radius"] = radius
3263        vol.locator = pdf.GetLocator()
3264        vol.pipeline = utils.OperationNode(
3265            "density", parents=[self], comment=f"dims={tuple(vol.dimensions())}"
3266        )
3267        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:
3270    def tovolume(
3271        self,
3272        kernel="shepard",
3273        radius=None,
3274        n=None,
3275        bounds=None,
3276        null_value=None,
3277        dims=(25, 25, 25),
3278    ) -> "vedo.Volume":
3279        """
3280        Generate a `Volume` by interpolating a scalar
3281        or vector field which is only known on a scattered set of points or mesh.
3282        Available interpolation kernels are: shepard, gaussian, or linear.
3283
3284        Arguments:
3285            kernel : (str)
3286                interpolation kernel type [shepard]
3287            radius : (float)
3288                radius of the local search
3289            n : (int)
3290                number of point to use for interpolation
3291            bounds : (list)
3292                bounding box of the output Volume object
3293            dims : (list)
3294                dimensions of the output Volume object
3295            null_value : (float)
3296                value to be assigned to invalid points
3297
3298        Examples:
3299            - [interpolate_volume.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/interpolate_volume.py)
3300
3301                ![](https://vedo.embl.es/images/volumetric/59095175-1ec5a300-8918-11e9-8bc0-fd35c8981e2b.jpg)
3302        """
3303        if radius is None and not n:
3304            vedo.logger.error("please set either radius or n")
3305            raise RuntimeError
3306
3307        poly = self.dataset
3308
3309        # Create a probe volume
3310        probe = vtki.vtkImageData()
3311        probe.SetDimensions(dims)
3312        if bounds is None:
3313            bounds = self.bounds()
3314        probe.SetOrigin(bounds[0], bounds[2], bounds[4])
3315        probe.SetSpacing(
3316            (bounds[1] - bounds[0]) / dims[0],
3317            (bounds[3] - bounds[2]) / dims[1],
3318            (bounds[5] - bounds[4]) / dims[2],
3319        )
3320
3321        if not self.point_locator:
3322            self.point_locator = vtki.new("PointLocator")
3323            self.point_locator.SetDataSet(poly)
3324            self.point_locator.BuildLocator()
3325
3326        if kernel == "shepard":
3327            kern = vtki.new("ShepardKernel")
3328            kern.SetPowerParameter(2)
3329        elif kernel == "gaussian":
3330            kern = vtki.new("GaussianKernel")
3331        elif kernel == "linear":
3332            kern = vtki.new("LinearKernel")
3333        else:
3334            vedo.logger.error("Error in tovolume(), available kernels are:")
3335            vedo.logger.error(" [shepard, gaussian, linear]")
3336            raise RuntimeError()
3337
3338        if radius:
3339            kern.SetRadius(radius)
3340
3341        interpolator = vtki.new("PointInterpolator")
3342        interpolator.SetInputData(probe)
3343        interpolator.SetSourceData(poly)
3344        interpolator.SetKernel(kern)
3345        interpolator.SetLocator(self.point_locator)
3346
3347        if n:
3348            kern.SetNumberOfPoints(n)
3349            kern.SetKernelFootprintToNClosest()
3350        else:
3351            kern.SetRadius(radius)
3352
3353        if null_value is not None:
3354            interpolator.SetNullValue(null_value)
3355        else:
3356            interpolator.SetNullPointsStrategyToClosestPoint()
3357        interpolator.Update()
3358
3359        vol = vedo.Volume(interpolator.GetOutput())
3360
3361        vol.pipeline = utils.OperationNode(
3362            "signed_distance",
3363            parents=[self],
3364            comment=f"dims={tuple(vol.dimensions())}",
3365            c="#e9c46a:#0096c7",
3366        )
3367        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:
3370    def generate_segments(self, istart=0, rmax=1e30, niter=3) -> "vedo.shapes.Lines":
3371        """
3372        Generate a line segments from a set of points.
3373        The algorithm is based on the closest point search.
3374
3375        Returns a `Line` object.
3376        This object contains the a metadata array of used vertex counts in "UsedVertexCount"
3377        and the sum of the length of the segments in "SegmentsLengthSum".
3378
3379        Arguments:
3380            istart : (int)
3381                index of the starting point
3382            rmax : (float)
3383                maximum length of a segment
3384            niter : (int)
3385                number of iterations or passes through the points
3386
3387        Examples:
3388            - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py)
3389        """
3390        points = self.vertices
3391        segments = []
3392        dists = []
3393        n = len(points)
3394        used = np.zeros(n, dtype=int)
3395        for _ in range(niter):
3396            i = istart
3397            for _ in range(n):
3398                p = points[i]
3399                ids = self.closest_point(p, n=4, return_point_id=True)
3400                j = ids[1]
3401                if used[j] > 1 or [j, i] in segments:
3402                    j = ids[2]
3403                if used[j] > 1:
3404                    j = ids[3]
3405                d = np.linalg.norm(p - points[j])
3406                if used[j] > 1 or used[i] > 1 or d > rmax:
3407                    i += 1
3408                    if i >= n:
3409                        i = 0
3410                    continue
3411                used[i] += 1
3412                used[j] += 1
3413                segments.append([i, j])
3414                dists.append(d)
3415                i = j
3416        segments = np.array(segments, dtype=int)
3417
3418        lines = vedo.shapes.Lines(points[segments], c="k", lw=3)
3419        lines.metadata["UsedVertexCount"] = used
3420        lines.metadata["SegmentsLengthSum"] = np.sum(dists)
3421        lines.pipeline = utils.OperationNode("generate_segments", parents=[self])
3422        lines.name = "Segments"
3423        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:
3425    def generate_delaunay2d(
3426        self,
3427        mode="scipy",
3428        boundaries=(),
3429        tol=None,
3430        alpha=0.0,
3431        offset=0.0,
3432        transform=None,
3433    ) -> "vedo.mesh.Mesh":
3434        """
3435        Create a mesh from points in the XY plane.
3436        If `mode='fit'` then the filter computes a best fitting
3437        plane and projects the points onto it.
3438
3439        Check also `generate_mesh()`.
3440
3441        Arguments:
3442            tol : (float)
3443                specify a tolerance to control discarding of closely spaced points.
3444                This tolerance is specified as a fraction of the diagonal length of the bounding box of the points.
3445            alpha : (float)
3446                for a non-zero alpha value, only edges or triangles contained
3447                within a sphere centered at mesh vertices will be output.
3448                Otherwise, only triangles will be output.
3449            offset : (float)
3450                multiplier to control the size of the initial, bounding Delaunay triangulation.
3451            transform: (LinearTransform, NonLinearTransform)
3452                a transformation which is applied to points to generate a 2D problem.
3453                This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane.
3454                The points are transformed and triangulated.
3455                The topology of triangulated points is used as the output topology.
3456
3457        Examples:
3458            - [delaunay2d.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delaunay2d.py)
3459
3460                ![](https://vedo.embl.es/images/basic/delaunay2d.png)
3461        """
3462        plist = self.vertices.copy()
3463
3464        #########################################################
3465        if mode == "scipy":
3466            from scipy.spatial import Delaunay as scipy_delaunay
3467
3468            tri = scipy_delaunay(plist[:, 0:2])
3469            return vedo.mesh.Mesh([plist, tri.simplices])
3470        ##########################################################
3471
3472        pd = vtki.vtkPolyData()
3473        vpts = vtki.vtkPoints()
3474        vpts.SetData(utils.numpy2vtk(plist, dtype=np.float32))
3475        pd.SetPoints(vpts)
3476
3477        delny = vtki.new("Delaunay2D")
3478        delny.SetInputData(pd)
3479        if tol:
3480            delny.SetTolerance(tol)
3481        delny.SetAlpha(alpha)
3482        delny.SetOffset(offset)
3483
3484        if transform:
3485            delny.SetTransform(transform.T)
3486        elif mode == "fit":
3487            delny.SetProjectionPlaneMode(vtki.get_class("VTK_BEST_FITTING_PLANE"))
3488        elif mode == "xy" and boundaries:
3489            boundary = vtki.vtkPolyData()
3490            boundary.SetPoints(vpts)
3491            cell_array = vtki.vtkCellArray()
3492            for b in boundaries:
3493                cpolygon = vtki.vtkPolygon()
3494                for idd in b:
3495                    cpolygon.GetPointIds().InsertNextId(idd)
3496                cell_array.InsertNextCell(cpolygon)
3497            boundary.SetPolys(cell_array)
3498            delny.SetSourceData(boundary)
3499
3500        delny.Update()
3501
3502        msh = vedo.mesh.Mesh(delny.GetOutput())
3503        msh.name = "Delaunay2D"
3504        msh.clean().lighting("off")
3505        msh.pipeline = utils.OperationNode(
3506            "delaunay2d",
3507            parents=[self],
3508            comment=f"#cells {msh.dataset.GetNumberOfCells()}",
3509        )
3510        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:
3512    def generate_voronoi(self, padding=0.0, fit=False, method="vtk") -> "vedo.Mesh":
3513        """
3514        Generate the 2D Voronoi convex tiling of the input points (z is ignored).
3515        The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon.
3516
3517        A cell array named "VoronoiID" is added to the output Mesh.
3518
3519        The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest
3520        to one of the input points. Voronoi tessellations are important in computational geometry
3521        (and many other fields), and are the dual of Delaunay triangulations.
3522
3523        Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored
3524        (although carried through to the output).
3525        If you desire to triangulate in a different plane, you can use fit=True.
3526
3527        A brief summary is as follows. Each (generating) input point is associated with
3528        an initial Voronoi tile, which is simply the bounding box of the point set.
3529        A locator is then used to identify nearby points: each neighbor in turn generates a
3530        clipping line positioned halfway between the generating point and the neighboring point,
3531        and orthogonal to the line connecting them. Clips are readily performed by evaluationg the
3532        vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line.
3533        If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip
3534        line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs,
3535        the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region
3536        containing the neighboring clip points. The clip region (along with the points contained in it) is grown
3537        by careful expansion (e.g., outward spiraling iterator over all candidate clip points).
3538        When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi
3539        tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi
3540        tessellation. Note that topological and geometric information is used to generate a valid triangulation
3541        (e.g., merging points and validating topology).
3542
3543        Arguments:
3544            pts : (list)
3545                list of input points.
3546            padding : (float)
3547                padding distance. The default is 0.
3548            fit : (bool)
3549                detect automatically the best fitting plane. The default is False.
3550
3551        Examples:
3552            - [voronoi1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi1.py)
3553
3554                ![](https://vedo.embl.es/images/basic/voronoi1.png)
3555
3556            - [voronoi2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi2.py)
3557
3558                ![](https://vedo.embl.es/images/advanced/voronoi2.png)
3559        """
3560        pts = self.vertices
3561
3562        if method == "scipy":
3563            from scipy.spatial import Voronoi as scipy_voronoi
3564
3565            pts = np.asarray(pts)[:, (0, 1)]
3566            vor = scipy_voronoi(pts)
3567            regs = []  # filter out invalid indices
3568            for r in vor.regions:
3569                flag = True
3570                for x in r:
3571                    if x < 0:
3572                        flag = False
3573                        break
3574                if flag and len(r) > 0:
3575                    regs.append(r)
3576
3577            m = vedo.Mesh([vor.vertices, regs])
3578            m.celldata["VoronoiID"] = np.array(list(range(len(regs)))).astype(int)
3579
3580        elif method == "vtk":
3581            vor = vtki.new("Voronoi2D")
3582            if isinstance(pts, Points):
3583                vor.SetInputData(pts)
3584            else:
3585                pts = np.asarray(pts)
3586                if pts.shape[1] == 2:
3587                    pts = np.c_[pts, np.zeros(len(pts))]
3588                pd = vtki.vtkPolyData()
3589                vpts = vtki.vtkPoints()
3590                vpts.SetData(utils.numpy2vtk(pts, dtype=np.float32))
3591                pd.SetPoints(vpts)
3592                vor.SetInputData(pd)
3593            vor.SetPadding(padding)
3594            vor.SetGenerateScalarsToPointIds()
3595            if fit:
3596                vor.SetProjectionPlaneModeToBestFittingPlane()
3597            else:
3598                vor.SetProjectionPlaneModeToXYPlane()
3599            vor.Update()
3600            poly = vor.GetOutput()
3601            arr = poly.GetCellData().GetArray(0)
3602            if arr:
3603                arr.SetName("VoronoiID")
3604            m = vedo.Mesh(poly, c="orange5")
3605
3606        else:
3607            vedo.logger.error(f"Unknown method {method} in voronoi()")
3608            raise RuntimeError
3609
3610        m.lw(2).lighting("off").wireframe()
3611        m.name = "Voronoi"
3612        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:
3615    def generate_delaunay3d(self, radius=0, tol=None) -> "vedo.TetMesh":
3616        """
3617        Create 3D Delaunay triangulation of input points.
3618
3619        Arguments:
3620            radius : (float)
3621                specify distance (or "alpha") value to control output.
3622                For a non-zero values, only tetra contained within the circumsphere
3623                will be output.
3624            tol : (float)
3625                Specify a tolerance to control discarding of closely spaced points.
3626                This tolerance is specified as a fraction of the diagonal length of
3627                the bounding box of the points.
3628        """
3629        deln = vtki.new("Delaunay3D")
3630        deln.SetInputData(self.dataset)
3631        deln.SetAlpha(radius)
3632        deln.AlphaTetsOn()
3633        deln.AlphaTrisOff()
3634        deln.AlphaLinesOff()
3635        deln.AlphaVertsOff()
3636        deln.BoundingTriangulationOff()
3637        if tol:
3638            deln.SetTolerance(tol)
3639        deln.Update()
3640        m = vedo.TetMesh(deln.GetOutput())
3641        m.pipeline = utils.OperationNode(
3642            "generate_delaunay3d", c="#e9c46a:#edabab", parents=[self],
3643        )
3644        m.name = "Delaunay3D"
3645        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]:
3648    def visible_points(self, area=(), tol=None, invert=False) -> Union[Self, None]:
3649        """
3650        Extract points based on whether they are visible or not.
3651        Visibility is determined by accessing the z-buffer of a rendering window.
3652        The position of each input point is converted into display coordinates,
3653        and then the z-value at that point is obtained.
3654        If within the user-specified tolerance, the point is considered visible.
3655        Associated data attributes are passed to the output as well.
3656
3657        This filter also allows you to specify a rectangular window in display (pixel)
3658        coordinates in which the visible points must lie.
3659
3660        Arguments:
3661            area : (list)
3662                specify a rectangular region as (xmin,xmax,ymin,ymax)
3663            tol : (float)
3664                a tolerance in normalized display coordinate system
3665            invert : (bool)
3666                select invisible points instead.
3667
3668        Example:
3669            ```python
3670            from vedo import Ellipsoid, show
3671            s = Ellipsoid().rotate_y(30)
3672
3673            # Camera options: pos, focal_point, viewup, distance
3674            camopts = dict(pos=(0,0,25), focal_point=(0,0,0))
3675            show(s, camera=camopts, offscreen=True)
3676
3677            m = s.visible_points()
3678            # print('visible pts:', m.vertices)  # numpy array
3679            show(m, new=True, axes=1).close() # optionally draw result in a new window
3680            ```
3681            ![](https://vedo.embl.es/images/feats/visible_points.png)
3682        """
3683        svp = vtki.new("SelectVisiblePoints")
3684        svp.SetInputData(self.dataset)
3685
3686        ren = None
3687        if vedo.plotter_instance:
3688            if vedo.plotter_instance.renderer:
3689                ren = vedo.plotter_instance.renderer
3690                svp.SetRenderer(ren)
3691        if not ren:
3692            vedo.logger.warning(
3693                "visible_points() can only be used after a rendering step"
3694            )
3695            return None
3696
3697        if len(area) == 2:
3698            area = utils.flatten(area)
3699        if len(area) == 4:
3700            # specify a rectangular region
3701            svp.SetSelection(area[0], area[1], area[2], area[3])
3702        if tol is not None:
3703            svp.SetTolerance(tol)
3704        if invert:
3705            svp.SelectInvisibleOn()
3706        svp.Update()
3707
3708        m = Points(svp.GetOutput())
3709        m.name = "VisiblePoints"
3710        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:
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([pos], r, c, alpha)
461    # pt.pos(pos) # dont set position, just the point coords
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):
3713class CellCenters(Points):
3714    def __init__(self, pcloud):
3715        """
3716        Generate `Points` at the center of the cells of any type of object.
3717
3718        Check out also `cell_centers()`.
3719        """
3720        vcen = vtki.new("CellCenters")
3721        vcen.CopyArraysOn()
3722        vcen.VertexCellsOn()
3723        # vcen.ConvertGhostCellsToGhostPointsOn()
3724        try:
3725            vcen.SetInputData(pcloud.dataset)
3726        except AttributeError:
3727            vcen.SetInputData(pcloud)
3728        vcen.Update()
3729        super().__init__(vcen.GetOutput())
3730        self.name = "CellCenters"

Work with point clouds.

CellCenters(pcloud)
3714    def __init__(self, pcloud):
3715        """
3716        Generate `Points` at the center of the cells of any type of object.
3717
3718        Check out also `cell_centers()`.
3719        """
3720        vcen = vtki.new("CellCenters")
3721        vcen.CopyArraysOn()
3722        vcen.VertexCellsOn()
3723        # vcen.ConvertGhostCellsToGhostPointsOn()
3724        try:
3725            vcen.SetInputData(pcloud.dataset)
3726        except AttributeError:
3727            vcen.SetInputData(pcloud)
3728        vcen.Update()
3729        super().__init__(vcen.GetOutput())
3730        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]:
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

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:
 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

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

def fit_line(points: Union[numpy.ndarray, Points]) -> vedo.shapes.Line:
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

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:
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

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:
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

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:
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

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]:
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

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]:
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

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.