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

Work with point clouds.

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

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):
809    def polydata(self, **kwargs):
810        """
811        Obsolete. Use property `.dataset` instead.
812        Returns the underlying `vtkPolyData` object.
813        """
814        colors.printc(
815            "WARNING: call to .polydata() is obsolete, use property .dataset instead.",
816            c="y")
817        return self.dataset

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

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

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

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

Clean pointcloud or mesh by removing coincident points.

def subsample(self, fraction: float, absolute=False) -> Self:
1036    def subsample(self, fraction: float, absolute=False) -> Self:
1037        """
1038        Subsample a point cloud by requiring that the points
1039        or vertices are far apart at least by the specified fraction of the object size.
1040        If a Mesh is passed the polygonal faces are not removed
1041        but holes can appear as their vertices are removed.
1042
1043        Examples:
1044            - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py)
1045
1046                ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png)
1047
1048            - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py)
1049
1050                ![](https://vedo.embl.es/images/advanced/recosurface.png)
1051        """
1052        if not absolute:
1053            if fraction > 1:
1054                vedo.logger.warning(
1055                    f"subsample(fraction=...), fraction must be < 1, but is {fraction}"
1056                )
1057            if fraction <= 0:
1058                return self
1059
1060        cpd = vtki.new("CleanPolyData")
1061        cpd.PointMergingOn()
1062        cpd.ConvertLinesToPointsOn()
1063        cpd.ConvertPolysToLinesOn()
1064        cpd.ConvertStripsToPolysOn()
1065        cpd.SetInputData(self.dataset)
1066        if absolute:
1067            cpd.SetTolerance(fraction / self.diagonal_size())
1068            # cpd.SetToleranceIsAbsolute(absolute)
1069        else:
1070            cpd.SetTolerance(fraction)
1071        cpd.Update()
1072
1073        ps = 2
1074        if self.properties.GetRepresentation() == 0:
1075            ps = self.properties.GetPointSize()
1076
1077        self._update(cpd.GetOutput())
1078        self.ps(ps)
1079
1080        self.pipeline = utils.OperationNode(
1081            "subsample", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
1082        )
1083        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:
1085    def threshold(self, scalars: str, above=None, below=None, on="points") -> Self:
1086        """
1087        Extracts cells where scalar value satisfies threshold criterion.
1088
1089        Arguments:
1090            scalars : (str)
1091                name of the scalars array.
1092            above : (float)
1093                minimum value of the scalar
1094            below : (float)
1095                maximum value of the scalar
1096            on : (str)
1097                if 'cells' assume array of scalars refers to cell data.
1098
1099        Examples:
1100            - [mesh_threshold.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_threshold.py)
1101        """
1102        thres = vtki.new("Threshold")
1103        thres.SetInputData(self.dataset)
1104
1105        if on.startswith("c"):
1106            asso = vtki.vtkDataObject.FIELD_ASSOCIATION_CELLS
1107        else:
1108            asso = vtki.vtkDataObject.FIELD_ASSOCIATION_POINTS
1109
1110        thres.SetInputArrayToProcess(0, 0, 0, asso, scalars)
1111
1112        if above is None and below is not None:
1113            try:  # vtk 9.2
1114                thres.ThresholdByLower(below)
1115            except AttributeError:  # vtk 9.3
1116                thres.SetUpperThreshold(below)
1117
1118        elif below is None and above is not None:
1119            try:
1120                thres.ThresholdByUpper(above)
1121            except AttributeError:
1122                thres.SetLowerThreshold(above)
1123        else:
1124            try:
1125                thres.ThresholdBetween(above, below)
1126            except AttributeError:
1127                thres.SetUpperThreshold(below)
1128                thres.SetLowerThreshold(above)
1129
1130        thres.Update()
1131
1132        gf = vtki.new("GeometryFilter")
1133        gf.SetInputData(thres.GetOutput())
1134        gf.Update()
1135        self._update(gf.GetOutput())
1136        self.pipeline = utils.OperationNode("threshold", parents=[self])
1137        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:
1139    def quantize(self, value: float) -> Self:
1140        """
1141        The user should input a value and all {x,y,z} coordinates
1142        will be quantized to that absolute grain size.
1143        """
1144        qp = vtki.new("QuantizePolyDataPoints")
1145        qp.SetInputData(self.dataset)
1146        qp.SetQFactor(value)
1147        qp.Update()
1148        self._update(qp.GetOutput())
1149        self.pipeline = utils.OperationNode("quantize", parents=[self])
1150        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
1152    @property
1153    def vertex_normals(self) -> np.ndarray:
1154        """
1155        Retrieve vertex normals as a numpy array. Same as `point_normals`.
1156        Check out also `compute_normals()` and `compute_normals_with_pca()`.
1157        """
1158        vtknormals = self.dataset.GetPointData().GetNormals()
1159        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
1161    @property
1162    def point_normals(self) -> np.ndarray:
1163        """
1164        Retrieve vertex normals as a numpy array. Same as `vertex_normals`.
1165        Check out also `compute_normals()` and `compute_normals_with_pca()`.
1166        """
1167        vtknormals = self.dataset.GetPointData().GetNormals()
1168        return utils.vtk2numpy(vtknormals)

Retrieve vertex normals as a numpy array. Same as 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:
1170    def align_to(self, target, iters=100, rigid=False, invert=False, use_centroids=False) -> Self:
1171        """
1172        Aligned to target mesh through the `Iterative Closest Point` algorithm.
1173
1174        The core of the algorithm is to match each vertex in one surface with
1175        the closest surface point on the other, then apply the transformation
1176        that modify one surface to best match the other (in the least-square sense).
1177
1178        Arguments:
1179            rigid : (bool)
1180                if True do not allow scaling
1181            invert : (bool)
1182                if True start by aligning the target to the source but
1183                invert the transformation finally. Useful when the target is smaller
1184                than the source.
1185            use_centroids : (bool)
1186                start by matching the centroids of the two objects.
1187
1188        Examples:
1189            - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py)
1190
1191                ![](https://vedo.embl.es/images/basic/align1.png)
1192
1193            - [align2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align2.py)
1194
1195                ![](https://vedo.embl.es/images/basic/align2.png)
1196        """
1197        icp = vtki.new("IterativeClosestPointTransform")
1198        icp.SetSource(self.dataset)
1199        icp.SetTarget(target.dataset)
1200        if invert:
1201            icp.Inverse()
1202        icp.SetMaximumNumberOfIterations(iters)
1203        if rigid:
1204            icp.GetLandmarkTransform().SetModeToRigidBody()
1205        icp.SetStartByMatchingCentroids(use_centroids)
1206        icp.Update()
1207
1208        self.apply_transform(icp.GetMatrix())
1209
1210        self.pipeline = utils.OperationNode(
1211            "align_to", parents=[self, target], comment=f"rigid = {rigid}"
1212        )
1213        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:
1215    def align_to_bounding_box(self, msh, rigid=False) -> Self:
1216        """
1217        Align the current object's bounding box to the bounding box
1218        of the input object.
1219
1220        Use `rigid=True` to disable scaling.
1221
1222        Example:
1223            [align6.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align6.py)
1224        """
1225        lmt = vtki.vtkLandmarkTransform()
1226        ss = vtki.vtkPoints()
1227        xss0, xss1, yss0, yss1, zss0, zss1 = self.bounds()
1228        for p in [
1229            [xss0, yss0, zss0],
1230            [xss1, yss0, zss0],
1231            [xss1, yss1, zss0],
1232            [xss0, yss1, zss0],
1233            [xss0, yss0, zss1],
1234            [xss1, yss0, zss1],
1235            [xss1, yss1, zss1],
1236            [xss0, yss1, zss1],
1237        ]:
1238            ss.InsertNextPoint(p)
1239        st = vtki.vtkPoints()
1240        xst0, xst1, yst0, yst1, zst0, zst1 = msh.bounds()
1241        for p in [
1242            [xst0, yst0, zst0],
1243            [xst1, yst0, zst0],
1244            [xst1, yst1, zst0],
1245            [xst0, yst1, zst0],
1246            [xst0, yst0, zst1],
1247            [xst1, yst0, zst1],
1248            [xst1, yst1, zst1],
1249            [xst0, yst1, zst1],
1250        ]:
1251            st.InsertNextPoint(p)
1252
1253        lmt.SetSourceLandmarks(ss)
1254        lmt.SetTargetLandmarks(st)
1255        lmt.SetModeToAffine()
1256        if rigid:
1257            lmt.SetModeToRigidBody()
1258        lmt.Update()
1259
1260        LT = LinearTransform(lmt)
1261        self.apply_transform(LT)
1262        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:
1264    def align_with_landmarks(
1265        self,
1266        source_landmarks,
1267        target_landmarks,
1268        rigid=False,
1269        affine=False,
1270        least_squares=False,
1271    ) -> Self:
1272        """
1273        Transform mesh orientation and position based on a set of landmarks points.
1274        The algorithm finds the best matching of source points to target points
1275        in the mean least square sense, in one single step.
1276
1277        If `affine` is True the x, y and z axes can scale independently but stay collinear.
1278        With least_squares they can vary orientation.
1279
1280        Examples:
1281            - [align5.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align5.py)
1282
1283                ![](https://vedo.embl.es/images/basic/align5.png)
1284        """
1285
1286        if utils.is_sequence(source_landmarks):
1287            ss = vtki.vtkPoints()
1288            for p in source_landmarks:
1289                ss.InsertNextPoint(p)
1290        else:
1291            ss = source_landmarks.dataset.GetPoints()
1292            if least_squares:
1293                source_landmarks = source_landmarks.coordinates
1294
1295        if utils.is_sequence(target_landmarks):
1296            st = vtki.vtkPoints()
1297            for p in target_landmarks:
1298                st.InsertNextPoint(p)
1299        else:
1300            st = target_landmarks.GetPoints()
1301            if least_squares:
1302                target_landmarks = target_landmarks.coordinates
1303
1304        if ss.GetNumberOfPoints() != st.GetNumberOfPoints():
1305            n1 = ss.GetNumberOfPoints()
1306            n2 = st.GetNumberOfPoints()
1307            vedo.logger.error(f"source and target have different nr of points {n1} vs {n2}")
1308            raise RuntimeError()
1309
1310        if int(rigid) + int(affine) + int(least_squares) > 1:
1311            vedo.logger.error(
1312                "only one of rigid, affine, least_squares can be True at a time"
1313            )
1314            raise RuntimeError()
1315
1316        lmt = vtki.vtkLandmarkTransform()
1317        lmt.SetSourceLandmarks(ss)
1318        lmt.SetTargetLandmarks(st)
1319        lmt.SetModeToSimilarity()
1320
1321        if rigid:
1322            lmt.SetModeToRigidBody()
1323            lmt.Update()
1324
1325        elif affine:
1326            lmt.SetModeToAffine()
1327            lmt.Update()
1328
1329        elif least_squares:
1330            cms = source_landmarks.mean(axis=0)
1331            cmt = target_landmarks.mean(axis=0)
1332            m = np.linalg.lstsq(source_landmarks - cms, target_landmarks - cmt, rcond=None)[0]
1333            M = vtki.vtkMatrix4x4()
1334            for i in range(3):
1335                for j in range(3):
1336                    M.SetElement(j, i, m[i][j])
1337            lmt = vtki.vtkTransform()
1338            lmt.Translate(cmt)
1339            lmt.Concatenate(M)
1340            lmt.Translate(-cms)
1341
1342        else:
1343            lmt.Update()
1344
1345        self.apply_transform(lmt)
1346        self.pipeline = utils.OperationNode("transform_with_landmarks", parents=[self])
1347        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:
1349    def normalize(self) -> Self:
1350        """Scale average size to unit. The scaling is performed around the center of mass."""
1351        coords = self.coordinates
1352        if not coords.shape[0]:
1353            return self
1354        cm = np.mean(coords, axis=0)
1355        pts = coords - cm
1356        xyz2 = np.sum(pts * pts, axis=0)
1357        scale = 1 / np.sqrt(np.sum(xyz2) / len(pts))
1358        self.scale(scale, origin=cm)
1359        self.pipeline = utils.OperationNode("normalize", parents=[self])
1360        return self

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

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

Flip all normals orientation.

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

Create a simple point in space.

if you are creating many points you should use class Points instead!
def merge( *meshs, flag=False) -> Union[vedo.mesh.Mesh, Points, NoneType]:
41def merge(*meshs, flag=False) -> Union["vedo.Mesh", "vedo.Points", None]:
42    """
43    Build a new Mesh (or Points) formed by the fusion of the inputs.
44
45    Similar to Assembly, but in this case the input objects become a single entity.
46
47    To keep track of the original identities of the inputs you can set `flag=True`.
48    In this case a `pointdata` array of ids is added to the output with name "OriginalMeshID".
49
50    Examples:
51        - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py)
52
53            ![](https://vedo.embl.es/images/advanced/warp1.png)
54
55        - [value_iteration.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/value_iteration.py)
56
57    """
58    objs = [a for a in utils.flatten(meshs) if a]
59
60    if not objs:
61        return None
62
63    idarr = []
64    polyapp = vtki.new("AppendPolyData")
65    for i, ob in enumerate(objs):
66        polyapp.AddInputData(ob.dataset)
67        if flag:
68            idarr += [i] * ob.dataset.GetNumberOfPoints()
69    polyapp.Update()
70    mpoly = polyapp.GetOutput()
71
72    if flag:
73        varr = utils.numpy2vtk(idarr, dtype=np.uint16, name="OriginalMeshID")
74        mpoly.GetPointData().AddArray(varr)
75
76    has_mesh = False
77    for ob in objs:
78        if isinstance(ob, vedo.Mesh):
79            has_mesh = True
80            break
81
82    if has_mesh:
83        msh = vedo.Mesh(mpoly)
84    else:
85        msh = Points(mpoly) # type: ignore
86
87    msh.copy_properties_from(objs[0])
88
89    msh.pipeline = utils.OperationNode(
90        "merge", parents=objs, comment=f"#pts {msh.dataset.GetNumberOfPoints()}"
91    )
92    return msh

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 fit_line(points: Union[numpy.ndarray, Points]) -> vedo.shapes.Line:
135def fit_line(points: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Line":
136    """
137    Fits a line through points.
138
139    Extra info is stored in `Line.slope`, `Line.center`, `Line.variances`.
140
141    Examples:
142        - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py)
143
144            ![](https://vedo.embl.es/images/advanced/fitline.png)
145    """
146    if isinstance(points, Points):
147        points = points.coordinates
148    data = np.asarray(points)
149    datamean = data.mean(axis=0)
150    _, dd, vv = np.linalg.svd(data - datamean)
151    vv = vv[0] / np.linalg.norm(vv[0])
152    # vv contains the first principal component, i.e. the direction
153    # vector of the best fit line in the least squares sense.
154    xyz_min = data.min(axis=0)
155    xyz_max = data.max(axis=0)
156    a = np.linalg.norm(xyz_min - datamean)
157    b = np.linalg.norm(xyz_max - datamean)
158    p1 = datamean - a * vv
159    p2 = datamean + b * vv
160    line = vedo.shapes.Line(p1, p2, lw=1)
161    line.slope = vv
162    line.center = datamean
163    line.variances = dd
164    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:
167def fit_circle(points: Union[np.ndarray, "vedo.Points"]) -> tuple:
168    """
169    Fits a circle through a set of 3D points, with a very fast non-iterative method.
170
171    Returns the tuple `(center, radius, normal_to_circle)`.
172
173    .. warning::
174        trying to fit s-shaped points will inevitably lead to instabilities and
175        circles of small radius.
176
177    References:
178        *J.F. Crawford, Nucl. Instr. Meth. 211, 1983, 223-225.*
179    """
180    if isinstance(points, Points):
181        points = points.coordinates
182    data = np.asarray(points)
183
184    offs = data.mean(axis=0)
185    data, n0 = _rotate_points(data - offs)
186
187    xi = data[:, 0]
188    yi = data[:, 1]
189
190    x = sum(xi)
191    xi2 = xi * xi
192    xx = sum(xi2)
193    xxx = sum(xi2 * xi)
194
195    y = sum(yi)
196    yi2 = yi * yi
197    yy = sum(yi2)
198    yyy = sum(yi2 * yi)
199
200    xiyi = xi * yi
201    xy = sum(xiyi)
202    xyy = sum(xiyi * yi)
203    xxy = sum(xi * xiyi)
204
205    N = len(xi)
206    k = (xx + yy) / N
207
208    a1 = xx - x * x / N
209    b1 = xy - x * y / N
210    c1 = 0.5 * (xxx + xyy - x * k)
211
212    a2 = xy - x * y / N
213    b2 = yy - y * y / N
214    c2 = 0.5 * (xxy + yyy - y * k)
215
216    d = a2 * b1 - a1 * b2
217    if not d:
218        return offs, 0, n0
219    x0 = (b1 * c2 - b2 * c1) / d
220    y0 = (c1 - a1 * x0) / b1
221
222    R = np.sqrt(x0 * x0 + y0 * y0 - 1 / N * (2 * x0 * x + 2 * y0 * y - xx - yy))
223
224    c, _ = _rotate_points([x0, y0, 0], (0, 0, 1), n0)
225
226    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:
229def fit_plane(points: Union[np.ndarray, "vedo.Points"], signed=False) -> "vedo.shapes.Plane":
230    """
231    Fits a plane to a set of points.
232
233    Extra info is stored in `Plane.normal`, `Plane.center`, `Plane.variance`.
234
235    Arguments:
236        signed : (bool)
237            if True flip sign of the normal based on the ordering of the points
238
239    Examples:
240        - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py)
241
242            ![](https://vedo.embl.es/images/advanced/fitline.png)
243    """
244    if isinstance(points, Points):
245        points = points.coordinates
246    data = np.asarray(points)
247    datamean = data.mean(axis=0)
248    pts = data - datamean
249    res = np.linalg.svd(pts)
250    dd, vv = res[1], res[2]
251    n = np.cross(vv[0], vv[1])
252    if signed:
253        v = np.zeros_like(pts)
254        for i in range(len(pts) - 1):
255            vi = np.cross(pts[i], pts[i + 1])
256            v[i] = vi / np.linalg.norm(vi)
257        ns = np.mean(v, axis=0)  # normal to the points plane
258        if np.dot(n, ns) < 0:
259            n = -n
260    xyz_min = data.min(axis=0)
261    xyz_max = data.max(axis=0)
262    s = np.linalg.norm(xyz_max - xyz_min)
263    pla = vedo.shapes.Plane(datamean, n, s=[s, s])
264    pla.variance = dd[2]
265    pla.name = "FitPlane"
266    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:
269def fit_sphere(coords: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Sphere":
270    """
271    Fits a sphere to a set of points.
272
273    Extra info is stored in `Sphere.radius`, `Sphere.center`, `Sphere.residue`.
274
275    Examples:
276        - [fitspheres1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitspheres1.py)
277
278            ![](https://vedo.embl.es/images/advanced/fitspheres1.jpg)
279    """
280    if isinstance(coords, Points):
281        coords = coords.coordinates
282    coords = np.array(coords)
283    n = len(coords)
284    A = np.zeros((n, 4))
285    A[:, :-1] = coords * 2
286    A[:, 3] = 1
287    f = np.zeros((n, 1))
288    x = coords[:, 0]
289    y = coords[:, 1]
290    z = coords[:, 2]
291    f[:, 0] = x * x + y * y + z * z
292    try:
293        C, residue, rank, _ = np.linalg.lstsq(A, f, rcond=-1)  # solve AC=f
294    except:
295        C, residue, rank, _ = np.linalg.lstsq(A, f)  # solve AC=f
296    if rank < 4:
297        return None
298    t = (C[0] * C[0]) + (C[1] * C[1]) + (C[2] * C[2]) + C[3]
299    radius = np.sqrt(t)[0]
300    center = np.array([C[0][0], C[1][0], C[2][0]])
301    if len(residue) > 0:
302        residue = np.sqrt(residue[0]) / n
303    else:
304        residue = 0
305    sph = vedo.shapes.Sphere(center, radius, c=(1, 0, 0)).wireframe(1)
306    sph.radius = radius
307    sph.center = center
308    sph.residue = residue
309    sph.name = "FitSphere"
310    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]:
313def pca_ellipse(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=60) -> Union["vedo.shapes.Circle", None]:
314    """
315    Create the oriented 2D ellipse that contains the fraction `pvalue` of points.
316    PCA (Principal Component Analysis) is used to compute the ellipse orientation.
317
318    Parameter `pvalue` sets the specified fraction of points inside the ellipse.
319    Normalized directions are stored in `ellipse.axis1`, `ellipse.axis2`.
320    Axes sizes are stored in `ellipse.va`, `ellipse.vb`
321
322    Arguments:
323        pvalue : (float)
324            ellipse will include this fraction of points
325        res : (int)
326            resolution of the ellipse
327
328    Examples:
329        - [pca_ellipse.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipse.py)
330        - [histo_pca.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_pca.py)
331
332            ![](https://vedo.embl.es/images/pyplot/histo_pca.png)
333    """
334    from scipy.stats import f
335
336    if isinstance(points, Points):
337        coords = points.coordinates
338    else:
339        coords = points
340    if len(coords) < 4:
341        vedo.logger.warning("in pca_ellipse(), there are not enough points!")
342        return None
343
344    P = np.array(coords, dtype=float)[:, (0, 1)]
345    cov = np.cov(P, rowvar=0)      # type: ignore
346    _, s, R = np.linalg.svd(cov)   # singular value decomposition
347    p, n = s.size, P.shape[0]
348    fppf = f.ppf(pvalue, p, n - p) # f % point function
349    u = np.sqrt(s * fppf / 2) * 2  # semi-axes (largest first)
350    ua, ub = u
351    center = utils.make3d(np.mean(P, axis=0)) # centroid of the ellipse
352
353    t = LinearTransform(R.T * u).translate(center)
354    elli = vedo.shapes.Circle(alpha=0.75, res=res)
355    elli.apply_transform(t)
356    elli.properties.LightingOff()
357
358    elli.pvalue = pvalue
359    elli.center = np.array([center[0], center[1], 0])
360    elli.nr_of_points = n
361    elli.va = ua
362    elli.vb = ub
363    
364    # we subtract center because it's in t
365    elli.axis1 = t.move([1, 0, 0]) - center
366    elli.axis2 = t.move([0, 1, 0]) - center
367
368    elli.axis1 /= np.linalg.norm(elli.axis1)
369    elli.axis2 /= np.linalg.norm(elli.axis2)
370    elli.name = "PCAEllipse"
371    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]:
374def pca_ellipsoid(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=24) -> Union["vedo.shapes.Ellipsoid", None]:
375    """
376    Create the oriented ellipsoid that contains the fraction `pvalue` of points.
377    PCA (Principal Component Analysis) is used to compute the ellipsoid orientation.
378
379    Axes sizes can be accessed in `ellips.va`, `ellips.vb`, `ellips.vc`,
380    normalized directions are stored in `ellips.axis1`, `ellips.axis2` and `ellips.axis3`.
381    Center of mass is stored in `ellips.center`.
382
383    Asphericity can be accessed in `ellips.asphericity()` and ellips.asphericity_error().
384    A value of 0 means a perfect sphere.
385
386    Arguments:
387        pvalue : (float)
388            ellipsoid will include this fraction of points
389   
390    Examples:
391        [pca_ellipsoid.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipsoid.py)
392
393            ![](https://vedo.embl.es/images/basic/pca.png)
394    
395    See also:
396        `pca_ellipse()` for a 2D ellipse.
397    """
398    from scipy.stats import f
399
400    if isinstance(points, Points):
401        coords = points.coordinates
402    else:
403        coords = points
404    if len(coords) < 4:
405        vedo.logger.warning("in pca_ellipsoid(), not enough input points!")
406        return None
407
408    P = np.array(coords, ndmin=2, dtype=float)
409    cov = np.cov(P, rowvar=0)     # type: ignore
410    _, s, R = np.linalg.svd(cov)  # singular value decomposition
411    p, n = s.size, P.shape[0]
412    fppf = f.ppf(pvalue, p, n-p)*(n-1)*p*(n+1)/n/(n-p)  # f % point function
413    u = np.sqrt(s*fppf)
414    ua, ub, uc = u                # semi-axes (largest first)
415    center = np.mean(P, axis=0)   # centroid of the hyperellipsoid
416
417    t = LinearTransform(R.T * u).translate(center)
418    elli = vedo.shapes.Ellipsoid((0,0,0), (1,0,0), (0,1,0), (0,0,1), res=res)
419    elli.apply_transform(t)
420    elli.alpha(0.25)
421    elli.properties.LightingOff()
422
423    elli.pvalue = pvalue
424    elli.nr_of_points = n
425    elli.center = center
426    elli.va = ua
427    elli.vb = ub
428    elli.vc = uc
429    # we subtract center because it's in t
430    elli.axis1 = np.array(t.move([1, 0, 0])) - center
431    elli.axis2 = np.array(t.move([0, 1, 0])) - center
432    elli.axis3 = np.array(t.move([0, 0, 1])) - center
433    elli.axis1 /= np.linalg.norm(elli.axis1)
434    elli.axis2 /= np.linalg.norm(elli.axis2)
435    elli.axis3 /= np.linalg.norm(elli.axis3)
436    elli.name = "PCAEllipsoid"
437    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.