vedo.pointcloud

Submodule to work with point clouds.

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

Work with point clouds.

Points(inputobj=None, r=4, c=(0.2, 0.2, 0.2), alpha=1)
471    def __init__(self, inputobj=None, r=4, c=(0.2, 0.2, 0.2), alpha=1):
472        """
473        Build an object made of only vertex points for a list of 2D/3D points.
474        Both shapes (N, 3) or (3, N) are accepted as input, if N>3.
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        elif "meshlib" in str(type(inputobj)):
568            from meshlib import mrmeshnumpy as mn
569            self.dataset = utils.buildPolyData(mn.toNumpyArray(inputobj.points))
570
571        else:
572            # try to extract the points from a generic VTK input data object
573            if hasattr(inputobj, "dataset"):
574                inputobj = inputobj.dataset
575            try:
576                vvpts = inputobj.GetPoints()
577                self.dataset = vtki.vtkPolyData()
578                self.dataset.SetPoints(vvpts)
579                for i in range(inputobj.GetPointData().GetNumberOfArrays()):
580                    arr = inputobj.GetPointData().GetArray(i)
581                    self.dataset.GetPointData().AddArray(arr)
582            except:
583                vedo.logger.error(f"cannot build Points from type {type(inputobj)}")
584                raise RuntimeError()
585
586        self.actor.SetMapper(self.mapper)
587        self.mapper.SetInputData(self.dataset)
588
589        self.properties.SetColor(colors.get_color(c))
590        self.properties.SetOpacity(alpha)
591        self.properties.SetRepresentationToPoints()
592        self.properties.SetPointSize(r)
593        self.properties.LightingOff()
594
595        self.pipeline = utils.OperationNode(
596            self, parents=[], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
597        )

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

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

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

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

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

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

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

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

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

Clean pointcloud or mesh by removing coincident points.

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

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

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

Flip all normals orientation.

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

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

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

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

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

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

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

Create a simple point in space.

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

Work with point clouds.

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

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

Check out also cell_centers().

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

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

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

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

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

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

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

Fits a line through points.

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

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

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

Returns the tuple (center, radius, normal_to_circle).

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

References:

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

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

Fits a plane to a set of points.

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

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

Fits a sphere to a set of points.

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

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

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

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

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

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

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

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

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

pca_ellipsoid.py

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

pca_ellipse() for a 2D ellipse.