vedo.pointcloud

Submodule to work with point clouds.

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

Work with point clouds.

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

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

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

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

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

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

Clean pointcloud or mesh by removing coincident points.

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

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

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

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

Flip all normals orientation.

def add_gaussian_noise(self, sigma=1.0) -> Self:
1405    def add_gaussian_noise(self, sigma=1.0) -> Self:
1406        """
1407        Add gaussian noise to point positions.
1408        An extra array is added named "GaussianNoise" with the displacements.
1409
1410        Arguments:
1411            sigma : (float)
1412                nr. of standard deviations, expressed in percent of the diagonal size of mesh.
1413                Can also be a list `[sigma_x, sigma_y, sigma_z]`.
1414
1415        Example:
1416            ```python
1417            from vedo import Sphere
1418            Sphere().add_gaussian_noise(1.0).point_size(8).show().close()
1419            ```
1420        """
1421        sz = self.diagonal_size()
1422        pts = self.vertices
1423        n = len(pts)
1424        ns = (np.random.randn(n, 3) * sigma) * (sz / 100)
1425        vpts = vtki.vtkPoints()
1426        vpts.SetNumberOfPoints(n)
1427        vpts.SetData(utils.numpy2vtk(pts + ns, dtype=np.float32))
1428        self.dataset.SetPoints(vpts)
1429        self.dataset.GetPoints().Modified()
1430        self.pointdata["GaussianNoise"] = -ns
1431        self.pipeline = utils.OperationNode(
1432            "gaussian_noise", parents=[self], shape="egg", comment=f"sigma = {sigma}"
1433        )
1434        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]:
1436    def closest_point(
1437        self, pt, n=1, radius=None, return_point_id=False, return_cell_id=False
1438    ) -> Union[List[int], int, np.ndarray]:
1439        """
1440        Find the closest point(s) on a mesh given from the input point `pt`.
1441
1442        Arguments:
1443            n : (int)
1444                if greater than 1, return a list of n ordered closest points
1445            radius : (float)
1446                if given, get all points within that radius. Then n is ignored.
1447            return_point_id : (bool)
1448                return point ID instead of coordinates
1449            return_cell_id : (bool)
1450                return cell ID in which the closest point sits
1451
1452        Examples:
1453            - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py)
1454            - [fitplanes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitplanes.py)
1455            - [quadratic_morphing.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/quadratic_morphing.py)
1456
1457        .. note::
1458            The appropriate tree search locator is built on the fly and cached for speed.
1459
1460            If you want to reset it use `mymesh.point_locator=None`
1461            and / or `mymesh.cell_locator=None`.
1462        """
1463        if len(pt) != 3:
1464            pt = [pt[0], pt[1], 0]
1465
1466        # NB: every time the mesh moves or is warped the locators are set to None
1467        if ((n > 1 or radius) or (n == 1 and return_point_id)) and not return_cell_id:
1468            poly = None
1469            if not self.point_locator:
1470                poly = self.dataset
1471                self.point_locator = vtki.new("StaticPointLocator")
1472                self.point_locator.SetDataSet(poly)
1473                self.point_locator.BuildLocator()
1474
1475            ##########
1476            if radius:
1477                vtklist = vtki.vtkIdList()
1478                self.point_locator.FindPointsWithinRadius(radius, pt, vtklist)
1479            elif n > 1:
1480                vtklist = vtki.vtkIdList()
1481                self.point_locator.FindClosestNPoints(n, pt, vtklist)
1482            else:  # n==1 hence return_point_id==True
1483                ########
1484                return self.point_locator.FindClosestPoint(pt)
1485                ########
1486
1487            if return_point_id:
1488                ########
1489                return utils.vtk2numpy(vtklist)
1490                ########
1491
1492            if not poly:
1493                poly = self.dataset
1494            trgp = []
1495            for i in range(vtklist.GetNumberOfIds()):
1496                trgp_ = [0, 0, 0]
1497                vi = vtklist.GetId(i)
1498                poly.GetPoints().GetPoint(vi, trgp_)
1499                trgp.append(trgp_)
1500            ########
1501            return np.array(trgp)
1502            ########
1503
1504        else:
1505
1506            if not self.cell_locator:
1507                poly = self.dataset
1508
1509                # As per Miquel example with limbs the vtkStaticCellLocator doesnt work !!
1510                # https://discourse.vtk.org/t/vtkstaticcelllocator-problem-vtk9-0-3/7854/4
1511                if vedo.vtk_version[0] >= 9 and vedo.vtk_version[1] > 0:
1512                    self.cell_locator = vtki.new("StaticCellLocator")
1513                else:
1514                    self.cell_locator = vtki.new("CellLocator")
1515
1516                self.cell_locator.SetDataSet(poly)
1517                self.cell_locator.BuildLocator()
1518
1519            if radius is not None:
1520                vedo.printc("Warning: closest_point() with radius is not implemented for cells.", c='r')   
1521 
1522            if n != 1:
1523                vedo.printc("Warning: closest_point() with n>1 is not implemented for cells.", c='r')   
1524 
1525            trgp = [0, 0, 0]
1526            cid = vtki.mutable(0)
1527            dist2 = vtki.mutable(0)
1528            subid = vtki.mutable(0)
1529            self.cell_locator.FindClosestPoint(pt, trgp, cid, subid, dist2)
1530
1531            if return_cell_id:
1532                return int(cid)
1533
1534            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:
1536    def auto_distance(self) -> np.ndarray:
1537        """
1538        Calculate the distance to the closest point in the same cloud of points.
1539        The output is stored in a new pointdata array called "AutoDistance",
1540        and it is also returned by the function.
1541        """
1542        points = self.vertices
1543        if not self.point_locator:
1544            self.point_locator = vtki.new("StaticPointLocator")
1545            self.point_locator.SetDataSet(self.dataset)
1546            self.point_locator.BuildLocator()
1547        qs = []
1548        vtklist = vtki.vtkIdList()
1549        vtkpoints = self.dataset.GetPoints()
1550        for p in points:
1551            self.point_locator.FindClosestNPoints(2, p, vtklist)
1552            q = [0, 0, 0]
1553            pid = vtklist.GetId(1)
1554            vtkpoints.GetPoint(pid, q)
1555            qs.append(q)
1556        dists = np.linalg.norm(points - np.array(qs), axis=1)
1557        self.pointdata["AutoDistance"] = dists
1558        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:
1560    def hausdorff_distance(self, points) -> float:
1561        """
1562        Compute the Hausdorff distance to the input point set.
1563        Returns a single `float`.
1564
1565        Example:
1566            ```python
1567            from vedo import *
1568            t = np.linspace(0, 2*np.pi, 100)
1569            x = 4/3 * sin(t)**3
1570            y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12
1571            pol1 = Line(np.c_[x,y], closed=True).triangulate()
1572            pol2 = Polygon(nsides=5).pos(2,2)
1573            d12 = pol1.distance_to(pol2)
1574            d21 = pol2.distance_to(pol1)
1575            pol1.lw(0).cmap("viridis")
1576            pol2.lw(0).cmap("viridis")
1577            print("distance d12, d21 :", min(d12), min(d21))
1578            print("hausdorff distance:", pol1.hausdorff_distance(pol2))
1579            print("chamfer distance  :", pol1.chamfer_distance(pol2))
1580            show(pol1, pol2, axes=1)
1581            ```
1582            ![](https://vedo.embl.es/images/feats/heart.png)
1583        """
1584        hp = vtki.new("HausdorffDistancePointSetFilter")
1585        hp.SetInputData(0, self.dataset)
1586        hp.SetInputData(1, points.dataset)
1587        hp.SetTargetDistanceMethodToPointToCell()
1588        hp.Update()
1589        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:
1591    def chamfer_distance(self, pcloud) -> float:
1592        """
1593        Compute the Chamfer distance to the input point set.
1594
1595        Example:
1596            ```python
1597            from vedo import *
1598            cloud1 = np.random.randn(1000, 3)
1599            cloud2 = np.random.randn(1000, 3) + [1, 2, 3]
1600            c1 = Points(cloud1, r=5, c="red")
1601            c2 = Points(cloud2, r=5, c="green")
1602            d = c1.chamfer_distance(c2)
1603            show(f"Chamfer distance = {d}", c1, c2, axes=1).close()
1604            ```
1605        """
1606        # Definition of Chamfer distance may vary, here we use the average
1607        if not pcloud.point_locator:
1608            pcloud.point_locator = vtki.new("PointLocator")
1609            pcloud.point_locator.SetDataSet(pcloud.dataset)
1610            pcloud.point_locator.BuildLocator()
1611        if not self.point_locator:
1612            self.point_locator = vtki.new("PointLocator")
1613            self.point_locator.SetDataSet(self.dataset)
1614            self.point_locator.BuildLocator()
1615
1616        ps1 = self.vertices
1617        ps2 = pcloud.vertices
1618
1619        ids12 = []
1620        for p in ps1:
1621            pid12 = pcloud.point_locator.FindClosestPoint(p)
1622            ids12.append(pid12)
1623        deltav = ps2[ids12] - ps1
1624        da = np.mean(np.linalg.norm(deltav, axis=1))
1625
1626        ids21 = []
1627        for p in ps2:
1628            pid21 = self.point_locator.FindClosestPoint(p)
1629            ids21.append(pid21)
1630        deltav = ps1[ids21] - ps2
1631        db = np.mean(np.linalg.norm(deltav, axis=1))
1632        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:
1634    def remove_outliers(self, radius: float, neighbors=5) -> Self:
1635        """
1636        Remove outliers from a cloud of points within the specified `radius` search.
1637
1638        Arguments:
1639            radius : (float)
1640                Specify the local search radius.
1641            neighbors : (int)
1642                Specify the number of neighbors that a point must have,
1643                within the specified radius, for the point to not be considered isolated.
1644
1645        Examples:
1646            - [clustering.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/clustering.py)
1647
1648                ![](https://vedo.embl.es/images/basic/clustering.png)
1649        """
1650        removal = vtki.new("RadiusOutlierRemoval")
1651        removal.SetInputData(self.dataset)
1652        removal.SetRadius(radius)
1653        removal.SetNumberOfNeighbors(neighbors)
1654        removal.GenerateOutliersOff()
1655        removal.Update()
1656        inputobj = removal.GetOutput()
1657        if inputobj.GetNumberOfCells() == 0:
1658            carr = vtki.vtkCellArray()
1659            for i in range(inputobj.GetNumberOfPoints()):
1660                carr.InsertNextCell(1)
1661                carr.InsertCellPoint(i)
1662            inputobj.SetVerts(carr)
1663        self._update(removal.GetOutput())
1664        self.pipeline = utils.OperationNode("remove_outliers", parents=[self])
1665        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:
1667    def relax_point_positions(
1668            self, 
1669            n=10,
1670            iters=10,
1671            sub_iters=10,
1672            packing_factor=1,
1673            max_step=0,
1674            constraints=(),
1675        ) -> Self:
1676        """
1677        Smooth mesh or points with a 
1678        [Laplacian algorithm](https://vtk.org/doc/nightly/html/classvtkPointSmoothingFilter.html)
1679        variant. This modifies the coordinates of the input points by adjusting their positions
1680        to create a smooth distribution (and thereby form a pleasing packing of the points).
1681        Smoothing is performed by considering the effects of neighboring points on one another
1682        it uses a cubic cutoff function to produce repulsive forces between close points
1683        and attractive forces that are a little further away.
1684        
1685        In general, the larger the neighborhood size, the greater the reduction in high frequency
1686        information. The memory and computational requirements of the algorithm may also
1687        significantly increase.
1688
1689        The algorithm incrementally adjusts the point positions through an iterative process.
1690        Basically points are moved due to the influence of neighboring points. 
1691        
1692        As points move, both the local connectivity and data attributes associated with each point
1693        must be updated. Rather than performing these expensive operations after every iteration,
1694        a number of sub-iterations can be specified. If so, then the neighborhood and attribute
1695        value updates occur only every sub iteration, which can improve performance significantly.
1696        
1697        Arguments:
1698            n : (int)
1699                neighborhood size to calculate the Laplacian.
1700            iters : (int)
1701                number of iterations.
1702            sub_iters : (int)
1703                number of sub-iterations, i.e. the number of times the neighborhood and attribute
1704                value updates occur during each iteration.
1705            packing_factor : (float)
1706                adjust convergence speed.
1707            max_step : (float)
1708                Specify the maximum smoothing step size for each smoothing iteration.
1709                This limits the the distance over which a point can move in each iteration.
1710                As in all iterative methods, the stability of the process is sensitive to this parameter.
1711                In general, small step size and large numbers of iterations are more stable than a larger
1712                step size and a smaller numbers of iterations.
1713            constraints : (dict)
1714                dictionary of constraints.
1715                Point constraints are used to prevent points from moving,
1716                or to move only on a plane. This can prevent shrinking or growing point clouds.
1717                If enabled, a local topological analysis is performed to determine whether a point
1718                should be marked as fixed" i.e., never moves, or the point only moves on a plane,
1719                or the point can move freely.
1720                If all points in the neighborhood surrounding a point are in the cone defined by
1721                `fixed_angle`, then the point is classified as fixed.
1722                If all points in the neighborhood surrounding a point are in the cone defined by
1723                `boundary_angle`, then the point is classified as lying on a plane.
1724                Angles are expressed in degrees.
1725        
1726        Example:
1727            ```py
1728            import numpy as np
1729            from vedo import Points, show
1730            from vedo.pyplot import histogram
1731
1732            vpts1 = Points(np.random.rand(10_000, 3))
1733            dists = vpts1.auto_distance()
1734            h1 = histogram(dists, xlim=(0,0.08)).clone2d()
1735
1736            vpts2 = vpts1.clone().relax_point_positions(n=100, iters=20, sub_iters=10)
1737            dists = vpts2.auto_distance()
1738            h2 = histogram(dists, xlim=(0,0.08)).clone2d()
1739
1740            show([[vpts1, h1], [vpts2, h2]], N=2).close()
1741            ```
1742        """
1743        smooth = vtki.new("PointSmoothingFilter")
1744        smooth.SetInputData(self.dataset)
1745        smooth.SetSmoothingModeToUniform()
1746        smooth.SetNumberOfIterations(iters)
1747        smooth.SetNumberOfSubIterations(sub_iters)
1748        smooth.SetPackingFactor(packing_factor)
1749        if self.point_locator:
1750            smooth.SetLocator(self.point_locator)
1751        if not max_step:
1752            max_step = self.diagonal_size() / 100
1753        smooth.SetMaximumStepSize(max_step)
1754        smooth.SetNeighborhoodSize(n)
1755        if constraints:
1756            fixed_angle = constraints.get("fixed_angle", 45)
1757            boundary_angle = constraints.get("boundary_angle", 110)
1758            smooth.EnableConstraintsOn()
1759            smooth.SetFixedAngle(fixed_angle)
1760            smooth.SetBoundaryAngle(boundary_angle)
1761            smooth.GenerateConstraintScalarsOn()
1762            smooth.GenerateConstraintNormalsOn()
1763        smooth.Update()
1764        self._update(smooth.GetOutput())
1765        self.metadata["PackingRadius"] = smooth.GetPackingRadius()
1766        self.pipeline = utils.OperationNode("relax_point_positions", parents=[self])
1767        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:
1769    def smooth_mls_1d(self, f=0.2, radius=None, n=0) -> Self:
1770        """
1771        Smooth mesh or points with a `Moving Least Squares` variant.
1772        The point data array "Variances" will contain the residue calculated for each point.
1773
1774        Arguments:
1775            f : (float)
1776                smoothing factor - typical range is [0,2].
1777            radius : (float)
1778                radius search in absolute units.
1779                If set then `f` is ignored.
1780            n : (int)
1781                number of neighbours to be used for the fit.
1782                If set then `f` and `radius` are ignored.
1783
1784        Examples:
1785            - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py)
1786            - [skeletonize.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/skeletonize.py)
1787
1788            ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png)
1789        """
1790        coords = self.vertices
1791        ncoords = len(coords)
1792
1793        if n:
1794            Ncp = n
1795        elif radius:
1796            Ncp = 1
1797        else:
1798            Ncp = int(ncoords * f / 10)
1799            if Ncp < 5:
1800                vedo.logger.warning(f"Please choose a fraction higher than {f}")
1801                Ncp = 5
1802
1803        variances, newline = [], []
1804        for p in coords:
1805            points = self.closest_point(p, n=Ncp, radius=radius)
1806            if len(points) < 4:
1807                continue
1808
1809            points = np.array(points)
1810            pointsmean = points.mean(axis=0)  # plane center
1811            _, dd, vv = np.linalg.svd(points - pointsmean)
1812            newp = np.dot(p - pointsmean, vv[0]) * vv[0] + pointsmean
1813            variances.append(dd[1] + dd[2])
1814            newline.append(newp)
1815
1816        self.pointdata["Variances"] = np.array(variances).astype(np.float32)
1817        self.vertices = newline
1818        self.pipeline = utils.OperationNode("smooth_mls_1d", parents=[self])
1819        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:
1821    def smooth_mls_2d(self, f=0.2, radius=None, n=0) -> Self:
1822        """
1823        Smooth mesh or points with a `Moving Least Squares` algorithm variant.
1824
1825        The `mesh.pointdata['MLSVariance']` array will contain the residue calculated for each point.
1826        When a radius is specified, points that are isolated will not be moved and will get
1827        a 0 entry in array `mesh.pointdata['MLSValidPoint']`.
1828
1829        Arguments:
1830            f : (float)
1831                smoothing factor - typical range is [0, 2].
1832            radius : (float | array)
1833                radius search in absolute units. Can be single value (float) or sequence
1834                for adaptive smoothing. If set then `f` is ignored.
1835            n : (int)
1836                number of neighbours to be used for the fit.
1837                If set then `f` and `radius` are ignored.
1838
1839        Examples:
1840            - [moving_least_squares2D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares2D.py)
1841            - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py)
1842
1843                ![](https://vedo.embl.es/images/advanced/recosurface.png)
1844        """
1845        coords = self.vertices
1846        ncoords = len(coords)
1847
1848        if n:
1849            Ncp = n
1850            radius = None
1851        elif radius is not None:
1852            Ncp = 1
1853        else:
1854            Ncp = int(ncoords * f / 100)
1855            if Ncp < 4:
1856                vedo.logger.error(f"please choose a f-value higher than {f}")
1857                Ncp = 4
1858
1859        variances, newpts, valid = [], [], []
1860        radius_is_sequence = utils.is_sequence(radius)
1861
1862        pb = None
1863        if ncoords > 10000:
1864            pb = utils.ProgressBar(0, ncoords, delay=3)
1865
1866        for i, p in enumerate(coords):
1867            if pb:
1868                pb.print("smooth_mls_2d working ...")
1869            
1870            # if a radius was provided for each point
1871            if radius_is_sequence:
1872                pts = self.closest_point(p, n=Ncp, radius=radius[i])
1873            else:
1874                pts = self.closest_point(p, n=Ncp, radius=radius)
1875
1876            if len(pts) > 3:
1877                ptsmean = pts.mean(axis=0)  # plane center
1878                _, dd, vv = np.linalg.svd(pts - ptsmean)
1879                cv = np.cross(vv[0], vv[1])
1880                t = (np.dot(cv, ptsmean) - np.dot(cv, p)) / np.dot(cv, cv)
1881                newpts.append(p + cv * t)
1882                variances.append(dd[2])
1883                if radius is not None:
1884                    valid.append(1)
1885            else:
1886                newpts.append(p)
1887                variances.append(0)
1888                if radius is not None:
1889                    valid.append(0)
1890
1891        if radius is not None:
1892            self.pointdata["MLSValidPoint"] = np.array(valid).astype(np.uint8)
1893        self.pointdata["MLSVariance"] = np.array(variances).astype(np.float32)
1894
1895        self.vertices = newpts
1896
1897        self.pipeline = utils.OperationNode("smooth_mls_2d", parents=[self])
1898        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:
1900    def smooth_lloyd_2d(self, iterations=2, bounds=None, options="Qbb Qc Qx") -> Self:
1901        """
1902        Lloyd relaxation of a 2D pointcloud.
1903        
1904        Arguments:
1905            iterations : (int)
1906                number of iterations.
1907            bounds : (list)
1908                bounding box of the domain.
1909            options : (str)
1910                options for the Qhull algorithm.
1911        """
1912        # Credits: https://hatarilabs.com/ih-en/
1913        # tutorial-to-create-a-geospatial-voronoi-sh-mesh-with-python-scipy-and-geopandas
1914        from scipy.spatial import Voronoi as scipy_voronoi
1915
1916        def _constrain_points(points):
1917            # Update any points that have drifted beyond the boundaries of this space
1918            if bounds is not None:
1919                for point in points:
1920                    if point[0] < bounds[0]: point[0] = bounds[0]
1921                    if point[0] > bounds[1]: point[0] = bounds[1]
1922                    if point[1] < bounds[2]: point[1] = bounds[2]
1923                    if point[1] > bounds[3]: point[1] = bounds[3]
1924            return points
1925
1926        def _find_centroid(vertices):
1927            # The equation for the method used here to find the centroid of a
1928            # 2D polygon is given here: https://en.wikipedia.org/wiki/Centroid#Of_a_polygon
1929            area = 0
1930            centroid_x = 0
1931            centroid_y = 0
1932            for i in range(len(vertices) - 1):
1933                step = (vertices[i, 0] * vertices[i + 1, 1]) - (vertices[i + 1, 0] * vertices[i, 1])
1934                centroid_x += (vertices[i, 0] + vertices[i + 1, 0]) * step
1935                centroid_y += (vertices[i, 1] + vertices[i + 1, 1]) * step
1936                area += step
1937            if area:
1938                centroid_x = (1.0 / (3.0 * area)) * centroid_x
1939                centroid_y = (1.0 / (3.0 * area)) * centroid_y
1940            # prevent centroids from escaping bounding box
1941            return _constrain_points([[centroid_x, centroid_y]])[0]
1942
1943        def _relax(voron):
1944            # Moves each point to the centroid of its cell in the voronoi
1945            # map to "relax" the points (i.e. jitter the points so as
1946            # to spread them out within the space).
1947            centroids = []
1948            for idx in voron.point_region:
1949                # the region is a series of indices into voronoi.vertices
1950                # remove point at infinity, designated by index -1
1951                region = [i for i in voron.regions[idx] if i != -1]
1952                # enclose the polygon
1953                region = region + [region[0]]
1954                verts = voron.vertices[region]
1955                # find the centroid of those vertices
1956                centroids.append(_find_centroid(verts))
1957            return _constrain_points(centroids)
1958
1959        if bounds is None:
1960            bounds = self.bounds()
1961
1962        pts = self.vertices[:, (0, 1)]
1963        for i in range(iterations):
1964            vor = scipy_voronoi(pts, qhull_options=options)
1965            _constrain_points(vor.vertices)
1966            pts = _relax(vor)
1967        out = Points(pts)
1968        out.name = "MeshSmoothLloyd2D"
1969        out.pipeline = utils.OperationNode("smooth_lloyd", parents=[self])
1970        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:
1972    def project_on_plane(self, plane="z", point=None, direction=None) -> Self:
1973        """
1974        Project the mesh on one of the Cartesian planes.
1975
1976        Arguments:
1977            plane : (str, Plane)
1978                if plane is `str`, plane can be one of ['x', 'y', 'z'],
1979                represents x-plane, y-plane and z-plane, respectively.
1980                Otherwise, plane should be an instance of `vedo.shapes.Plane`.
1981            point : (float, array)
1982                if plane is `str`, point should be a float represents the intercept.
1983                Otherwise, point is the camera point of perspective projection
1984            direction : (array)
1985                direction of oblique projection
1986
1987        Note:
1988            Parameters `point` and `direction` are only used if the given plane
1989            is an instance of `vedo.shapes.Plane`. And one of these two params
1990            should be left as `None` to specify the projection type.
1991
1992        Example:
1993            ```python
1994            s.project_on_plane(plane='z') # project to z-plane
1995            plane = Plane(pos=(4, 8, -4), normal=(-1, 0, 1), s=(5,5))
1996            s.project_on_plane(plane=plane)                       # orthogonal projection
1997            s.project_on_plane(plane=plane, point=(6, 6, 6))      # perspective projection
1998            s.project_on_plane(plane=plane, direction=(1, 2, -1)) # oblique projection
1999            ```
2000
2001        Examples:
2002            - [silhouette2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette2.py)
2003
2004                ![](https://vedo.embl.es/images/basic/silhouette2.png)
2005        """
2006        coords = self.vertices
2007
2008        if plane == "x":
2009            coords[:, 0] = self.transform.position[0]
2010            intercept = self.xbounds()[0] if point is None else point
2011            self.x(intercept)
2012        elif plane == "y":
2013            coords[:, 1] = self.transform.position[1]
2014            intercept = self.ybounds()[0] if point is None else point
2015            self.y(intercept)
2016        elif plane == "z":
2017            coords[:, 2] = self.transform.position[2]
2018            intercept = self.zbounds()[0] if point is None else point
2019            self.z(intercept)
2020
2021        elif isinstance(plane, vedo.shapes.Plane):
2022            normal = plane.normal / np.linalg.norm(plane.normal)
2023            pl = np.hstack((normal, -np.dot(plane.pos(), normal))).reshape(4, 1)
2024            if direction is None and point is None:
2025                # orthogonal projection
2026                pt = np.hstack((normal, [0])).reshape(4, 1)
2027                # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T # python3 only
2028                proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T)
2029
2030            elif direction is None:
2031                # perspective projection
2032                pt = np.hstack((np.array(point), [1])).reshape(4, 1)
2033                # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T
2034                proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T)
2035
2036            elif point is None:
2037                # oblique projection
2038                pt = np.hstack((np.array(direction), [0])).reshape(4, 1)
2039                # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T
2040                proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T)
2041
2042            coords = np.concatenate([coords, np.ones((coords.shape[:-1] + (1,)))], axis=-1)
2043            # coords = coords @ proj_mat.T
2044            coords = np.matmul(coords, proj_mat.T)
2045            coords = coords[:, :3] / coords[:, 3:]
2046
2047        else:
2048            vedo.logger.error(f"unknown plane {plane}")
2049            raise RuntimeError()
2050
2051        self.alpha(0.1)
2052        self.vertices = coords
2053        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:
2055    def warp(self, source, target, sigma=1.0, mode="3d") -> Self:
2056        """
2057        "Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set
2058        of source and target landmarks. Any point on the mesh close to a source landmark will
2059        be moved to a place close to the corresponding target landmark.
2060        The points in between are interpolated smoothly using
2061        Bookstein's Thin Plate Spline algorithm.
2062
2063        Transformation object can be accessed with `mesh.transform`.
2064
2065        Arguments:
2066            sigma : (float)
2067                specify the 'stiffness' of the spline.
2068            mode : (str)
2069                set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes)
2070
2071        Examples:
2072            - [interpolate_field.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/interpolate_field.py)
2073            - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py)
2074            - [warp2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp2.py)
2075            - [warp3.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp3.py)
2076            - [warp4a.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4a.py)
2077            - [warp4b.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4b.py)
2078            - [warp6.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp6.py)
2079
2080            ![](https://vedo.embl.es/images/advanced/warp2.png)
2081        """
2082        parents = [self]
2083
2084        try:
2085            source = source.vertices
2086            parents.append(source)
2087        except AttributeError:
2088            source = utils.make3d(source)
2089        
2090        try:
2091            target = target.vertices
2092            parents.append(target)
2093        except AttributeError:
2094            target = utils.make3d(target)
2095
2096        ns = len(source)
2097        nt = len(target)
2098        if ns != nt:
2099            vedo.logger.error(f"#source {ns} != {nt} #target points")
2100            raise RuntimeError()
2101
2102        NLT = NonLinearTransform()
2103        NLT.source_points = source
2104        NLT.target_points = target
2105        self.apply_transform(NLT)
2106
2107        self.pipeline = utils.OperationNode("warp", parents=parents)
2108        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:
2110    def cut_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0), invert=False) -> Self:
2111        """
2112        Cut the mesh with the plane defined by a point and a normal.
2113
2114        Arguments:
2115            origin : (array)
2116                the cutting plane goes through this point
2117            normal : (array)
2118                normal of the cutting plane
2119
2120        Example:
2121            ```python
2122            from vedo import Cube
2123            cube = Cube().cut_with_plane(normal=(1,1,1))
2124            cube.back_color('pink').show().close()
2125            ```
2126            ![](https://vedo.embl.es/images/feats/cut_with_plane_cube.png)
2127
2128        Examples:
2129            - [trail.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/trail.py)
2130
2131                ![](https://vedo.embl.es/images/simulations/trail.gif)
2132
2133        Check out also:
2134            `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`.
2135        """
2136        s = str(normal)
2137        if "x" in s:
2138            normal = (1, 0, 0)
2139            if "-" in s:
2140                normal = -np.array(normal)
2141        elif "y" in s:
2142            normal = (0, 1, 0)
2143            if "-" in s:
2144                normal = -np.array(normal)
2145        elif "z" in s:
2146            normal = (0, 0, 1)
2147            if "-" in s:
2148                normal = -np.array(normal)
2149        plane = vtki.vtkPlane()
2150        plane.SetOrigin(origin)
2151        plane.SetNormal(normal)
2152
2153        clipper = vtki.new("ClipPolyData")
2154        clipper.SetInputData(self.dataset)
2155        clipper.SetClipFunction(plane)
2156        clipper.GenerateClippedOutputOff()
2157        clipper.GenerateClipScalarsOff()
2158        clipper.SetInsideOut(invert)
2159        clipper.SetValue(0)
2160        clipper.Update()
2161
2162        self._update(clipper.GetOutput())
2163
2164        self.pipeline = utils.OperationNode("cut_with_plane", parents=[self])
2165        return self

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

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

Examples:
Check out also:

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

def cut_with_planes(self, origins, normals, invert=False) -> Self:
2167    def cut_with_planes(self, origins, normals, invert=False) -> Self:
2168        """
2169        Cut the mesh with a convex set of planes defined by points and normals.
2170
2171        Arguments:
2172            origins : (array)
2173                each cutting plane goes through this point
2174            normals : (array)
2175                normal of each of the cutting planes
2176            invert : (bool)
2177                if True, cut outside instead of inside
2178
2179        Check out also:
2180            `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`
2181        """
2182
2183        vpoints = vtki.vtkPoints()
2184        for p in utils.make3d(origins):
2185            vpoints.InsertNextPoint(p)
2186        normals = utils.make3d(normals)
2187
2188        planes = vtki.vtkPlanes()
2189        planes.SetPoints(vpoints)
2190        planes.SetNormals(utils.numpy2vtk(normals, dtype=float))
2191
2192        clipper = vtki.new("ClipPolyData")
2193        clipper.SetInputData(self.dataset)
2194        clipper.SetInsideOut(invert)
2195        clipper.SetClipFunction(planes)
2196        clipper.GenerateClippedOutputOff()
2197        clipper.GenerateClipScalarsOff()
2198        clipper.SetValue(0)
2199        clipper.Update()
2200
2201        self._update(clipper.GetOutput())
2202
2203        self.pipeline = utils.OperationNode("cut_with_planes", parents=[self])
2204        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:
2206    def cut_with_box(self, bounds, invert=False) -> Self:
2207        """
2208        Cut the current mesh with a box or a set of boxes.
2209        This is much faster than `cut_with_mesh()`.
2210
2211        Input `bounds` can be either:
2212        - a Mesh or Points object
2213        - a list of 6 number representing a bounding box `[xmin,xmax, ymin,ymax, zmin,zmax]`
2214        - a list of bounding boxes like the above: `[[xmin1,...], [xmin2,...], ...]`
2215
2216        Example:
2217            ```python
2218            from vedo import Sphere, Cube, show
2219            mesh = Sphere(r=1, res=50)
2220            box  = Cube(side=1.5).wireframe()
2221            mesh.cut_with_box(box)
2222            show(mesh, box, axes=1).close()
2223            ```
2224            ![](https://vedo.embl.es/images/feats/cut_with_box_cube.png)
2225
2226        Check out also:
2227            `cut_with_line()`, `cut_with_plane()`, `cut_with_cylinder()`
2228        """
2229        if isinstance(bounds, Points):
2230            bounds = bounds.bounds()
2231
2232        box = vtki.new("Box")
2233        if utils.is_sequence(bounds[0]):
2234            for bs in bounds:
2235                box.AddBounds(bs)
2236        else:
2237            box.SetBounds(bounds)
2238
2239        clipper = vtki.new("ClipPolyData")
2240        clipper.SetInputData(self.dataset)
2241        clipper.SetClipFunction(box)
2242        clipper.SetInsideOut(not invert)
2243        clipper.GenerateClippedOutputOff()
2244        clipper.GenerateClipScalarsOff()
2245        clipper.SetValue(0)
2246        clipper.Update()
2247        self._update(clipper.GetOutput())
2248
2249        self.pipeline = utils.OperationNode("cut_with_box", parents=[self])
2250        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:
2252    def cut_with_line(self, points, invert=False, closed=True) -> Self:
2253        """
2254        Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter.
2255        The polyline is defined by a set of points (z-coordinates are ignored).
2256        This is much faster than `cut_with_mesh()`.
2257
2258        Check out also:
2259            `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()`
2260        """
2261        pplane = vtki.new("PolyPlane")
2262        if isinstance(points, Points):
2263            points = points.vertices.tolist()
2264
2265        if closed:
2266            if isinstance(points, np.ndarray):
2267                points = points.tolist()
2268            points.append(points[0])
2269
2270        vpoints = vtki.vtkPoints()
2271        for p in points:
2272            if len(p) == 2:
2273                p = [p[0], p[1], 0.0]
2274            vpoints.InsertNextPoint(p)
2275
2276        n = len(points)
2277        polyline = vtki.new("PolyLine")
2278        polyline.Initialize(n, vpoints)
2279        polyline.GetPointIds().SetNumberOfIds(n)
2280        for i in range(n):
2281            polyline.GetPointIds().SetId(i, i)
2282        pplane.SetPolyLine(polyline)
2283
2284        clipper = vtki.new("ClipPolyData")
2285        clipper.SetInputData(self.dataset)
2286        clipper.SetClipFunction(pplane)
2287        clipper.SetInsideOut(invert)
2288        clipper.GenerateClippedOutputOff()
2289        clipper.GenerateClipScalarsOff()
2290        clipper.SetValue(0)
2291        clipper.Update()
2292        self._update(clipper.GetOutput())
2293
2294        self.pipeline = utils.OperationNode("cut_with_line", parents=[self])
2295        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:
2297    def cut_with_cookiecutter(self, lines) -> Self:
2298        """
2299        Cut the current mesh with a single line or a set of lines.
2300
2301        Input `lines` can be either:
2302        - a `Mesh` or `Points` object
2303        - a list of 3D points: `[(x1,y1,z1), (x2,y2,z2), ...]`
2304        - a list of 2D points: `[(x1,y1), (x2,y2), ...]`
2305
2306        Example:
2307            ```python
2308            from vedo import *
2309            grid = Mesh(dataurl + "dolfin_fine.vtk")
2310            grid.compute_quality().cmap("Greens")
2311            pols = merge(
2312                Polygon(nsides=10, r=0.3).pos(0.7, 0.3),
2313                Polygon(nsides=10, r=0.2).pos(0.3, 0.7),
2314            )
2315            lines = pols.boundaries()
2316            cgrid = grid.clone().cut_with_cookiecutter(lines)
2317            grid.alpha(0.1).wireframe()
2318            show(grid, cgrid, lines, axes=8, bg='blackboard').close()
2319            ```
2320            ![](https://vedo.embl.es/images/feats/cookiecutter.png)
2321
2322        Check out also:
2323            `cut_with_line()` and `cut_with_point_loop()`
2324
2325        Note:
2326            In case of a warning message like:
2327                "Mesh and trim loop point data attributes are different"
2328            consider interpolating the mesh point data to the loop points,
2329            Eg. (in the above example):
2330            ```python
2331            lines = pols.boundaries().interpolate_data_from(grid, n=2)
2332            ```
2333
2334        Note:
2335            trying to invert the selection by reversing the loop order
2336            will have no effect in this method, hence it does not have
2337            the `invert` option.
2338        """
2339        if utils.is_sequence(lines):
2340            lines = utils.make3d(lines)
2341            iline = list(range(len(lines))) + [0]
2342            poly = utils.buildPolyData(lines, lines=[iline])
2343        else:
2344            poly = lines.dataset
2345
2346        # if invert: # not working
2347        #     rev = vtki.new("ReverseSense")
2348        #     rev.ReverseCellsOn()
2349        #     rev.SetInputData(poly)
2350        #     rev.Update()
2351        #     poly = rev.GetOutput()
2352
2353        # Build loops from the polyline
2354        build_loops = vtki.new("ContourLoopExtraction")
2355        build_loops.SetGlobalWarningDisplay(0)
2356        build_loops.SetInputData(poly)
2357        build_loops.Update()
2358        boundary_poly = build_loops.GetOutput()
2359
2360        ccut = vtki.new("CookieCutter")
2361        ccut.SetInputData(self.dataset)
2362        ccut.SetLoopsData(boundary_poly)
2363        ccut.SetPointInterpolationToMeshEdges()
2364        # ccut.SetPointInterpolationToLoopEdges()
2365        ccut.PassCellDataOn()
2366        ccut.PassPointDataOn()
2367        ccut.Update()
2368        self._update(ccut.GetOutput())
2369
2370        self.pipeline = utils.OperationNode("cut_with_cookiecutter", parents=[self])
2371        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:
2373    def cut_with_cylinder(self, center=(0, 0, 0), axis=(0, 0, 1), r=1, invert=False) -> Self:
2374        """
2375        Cut the current mesh with an infinite cylinder.
2376        This is much faster than `cut_with_mesh()`.
2377
2378        Arguments:
2379            center : (array)
2380                the center of the cylinder
2381            normal : (array)
2382                direction of the cylinder axis
2383            r : (float)
2384                radius of the cylinder
2385
2386        Example:
2387            ```python
2388            from vedo import Disc, show
2389            disc = Disc(r1=1, r2=1.2)
2390            mesh = disc.extrude(3, res=50).linewidth(1)
2391            mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True)
2392            show(mesh, axes=1).close()
2393            ```
2394            ![](https://vedo.embl.es/images/feats/cut_with_cylinder.png)
2395
2396        Examples:
2397            - [optics_main1.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/optics_main1.py)
2398
2399        Check out also:
2400            `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()`
2401        """
2402        s = str(axis)
2403        if "x" in s:
2404            axis = (1, 0, 0)
2405        elif "y" in s:
2406            axis = (0, 1, 0)
2407        elif "z" in s:
2408            axis = (0, 0, 1)
2409        cyl = vtki.new("Cylinder")
2410        cyl.SetCenter(center)
2411        cyl.SetAxis(axis[0], axis[1], axis[2])
2412        cyl.SetRadius(r)
2413
2414        clipper = vtki.new("ClipPolyData")
2415        clipper.SetInputData(self.dataset)
2416        clipper.SetClipFunction(cyl)
2417        clipper.SetInsideOut(not invert)
2418        clipper.GenerateClippedOutputOff()
2419        clipper.GenerateClipScalarsOff()
2420        clipper.SetValue(0)
2421        clipper.Update()
2422        self._update(clipper.GetOutput())
2423
2424        self.pipeline = utils.OperationNode("cut_with_cylinder", parents=[self])
2425        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:
2427    def cut_with_sphere(self, center=(0, 0, 0), r=1.0, invert=False) -> Self:
2428        """
2429        Cut the current mesh with an sphere.
2430        This is much faster than `cut_with_mesh()`.
2431
2432        Arguments:
2433            center : (array)
2434                the center of the sphere
2435            r : (float)
2436                radius of the sphere
2437
2438        Example:
2439            ```python
2440            from vedo import Disc, show
2441            disc = Disc(r1=1, r2=1.2)
2442            mesh = disc.extrude(3, res=50).linewidth(1)
2443            mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True)
2444            show(mesh, axes=1).close()
2445            ```
2446            ![](https://vedo.embl.es/images/feats/cut_with_sphere.png)
2447
2448        Check out also:
2449            `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()`
2450        """
2451        sph = vtki.new("Sphere")
2452        sph.SetCenter(center)
2453        sph.SetRadius(r)
2454
2455        clipper = vtki.new("ClipPolyData")
2456        clipper.SetInputData(self.dataset)
2457        clipper.SetClipFunction(sph)
2458        clipper.SetInsideOut(not invert)
2459        clipper.GenerateClippedOutputOff()
2460        clipper.GenerateClipScalarsOff()
2461        clipper.SetValue(0)
2462        clipper.Update()
2463        self._update(clipper.GetOutput())
2464        self.pipeline = utils.OperationNode("cut_with_sphere", parents=[self])
2465        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]:
2467    def cut_with_mesh(self, mesh, invert=False, keep=False) -> Union[Self, "vedo.Assembly"]:
2468        """
2469        Cut an `Mesh` mesh with another `Mesh`.
2470
2471        Use `invert` to invert the selection.
2472
2473        Use `keep` to keep the cutoff part, in this case an `Assembly` is returned:
2474        the "cut" object and the "discarded" part of the original object.
2475        You can access both via `assembly.unpack()` method.
2476
2477        Example:
2478        ```python
2479        from vedo import *
2480        arr = np.random.randn(100000, 3)/2
2481        pts = Points(arr).c('red3').pos(5,0,0)
2482        cube = Cube().pos(4,0.5,0)
2483        assem = pts.cut_with_mesh(cube, keep=True)
2484        show(assem.unpack(), axes=1).close()
2485        ```
2486        ![](https://vedo.embl.es/images/feats/cut_with_mesh.png)
2487
2488       Check out also:
2489            `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()`
2490       """
2491        polymesh = mesh.dataset
2492        poly = self.dataset
2493
2494        # Create an array to hold distance information
2495        signed_distances = vtki.vtkFloatArray()
2496        signed_distances.SetNumberOfComponents(1)
2497        signed_distances.SetName("SignedDistances")
2498
2499        # implicit function that will be used to slice the mesh
2500        ippd = vtki.new("ImplicitPolyDataDistance")
2501        ippd.SetInput(polymesh)
2502
2503        # Evaluate the signed distance function at all of the grid points
2504        for pointId in range(poly.GetNumberOfPoints()):
2505            p = poly.GetPoint(pointId)
2506            signed_distance = ippd.EvaluateFunction(p)
2507            signed_distances.InsertNextValue(signed_distance)
2508
2509        currentscals = poly.GetPointData().GetScalars()
2510        if currentscals:
2511            currentscals = currentscals.GetName()
2512
2513        poly.GetPointData().AddArray(signed_distances)
2514        poly.GetPointData().SetActiveScalars("SignedDistances")
2515
2516        clipper = vtki.new("ClipPolyData")
2517        clipper.SetInputData(poly)
2518        clipper.SetInsideOut(not invert)
2519        clipper.SetGenerateClippedOutput(keep)
2520        clipper.SetValue(0.0)
2521        clipper.Update()
2522        cpoly = clipper.GetOutput()
2523
2524        if keep:
2525            kpoly = clipper.GetOutput(1)
2526
2527        vis = False
2528        if currentscals:
2529            cpoly.GetPointData().SetActiveScalars(currentscals)
2530            vis = self.mapper.GetScalarVisibility()
2531
2532        self._update(cpoly)
2533
2534        self.pointdata.remove("SignedDistances")
2535        self.mapper.SetScalarVisibility(vis)
2536        if keep:
2537            if isinstance(self, vedo.Mesh):
2538                cutoff = vedo.Mesh(kpoly)
2539            else:
2540                cutoff = vedo.Points(kpoly)
2541            # cutoff = self.__class__(kpoly) # this does not work properly
2542            cutoff.properties = vtki.vtkProperty()
2543            cutoff.properties.DeepCopy(self.properties)
2544            cutoff.actor.SetProperty(cutoff.properties)
2545            cutoff.c("k5").alpha(0.2)
2546            return vedo.Assembly([self, cutoff])
2547
2548        self.pipeline = utils.OperationNode("cut_with_mesh", parents=[self, mesh])
2549        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:
2551    def cut_with_point_loop(
2552        self, points, invert=False, on="points", include_boundary=False
2553    ) -> Self:
2554        """
2555        Cut an `Mesh` object with a set of points forming a closed loop.
2556
2557        Arguments:
2558            invert : (bool)
2559                invert selection (inside-out)
2560            on : (str)
2561                if 'cells' will extract the whole cells lying inside (or outside) the point loop
2562            include_boundary : (bool)
2563                include cells lying exactly on the boundary line. Only relevant on 'cells' mode
2564
2565        Examples:
2566            - [cut_with_points1.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points1.py)
2567
2568                ![](https://vedo.embl.es/images/advanced/cutWithPoints1.png)
2569
2570            - [cut_with_points2.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points2.py)
2571
2572                ![](https://vedo.embl.es/images/advanced/cutWithPoints2.png)
2573        """
2574        if isinstance(points, Points):
2575            parents = [points]
2576            vpts = points.dataset.GetPoints()
2577            points = points.vertices
2578        else:
2579            parents = [self]
2580            vpts = vtki.vtkPoints()
2581            points = utils.make3d(points)
2582            for p in points:
2583                vpts.InsertNextPoint(p)
2584
2585        if "cell" in on:
2586            ippd = vtki.new("ImplicitSelectionLoop")
2587            ippd.SetLoop(vpts)
2588            ippd.AutomaticNormalGenerationOn()
2589            clipper = vtki.new("ExtractPolyDataGeometry")
2590            clipper.SetInputData(self.dataset)
2591            clipper.SetImplicitFunction(ippd)
2592            clipper.SetExtractInside(not invert)
2593            clipper.SetExtractBoundaryCells(include_boundary)
2594        else:
2595            spol = vtki.new("SelectPolyData")
2596            spol.SetLoop(vpts)
2597            spol.GenerateSelectionScalarsOn()
2598            spol.GenerateUnselectedOutputOff()
2599            spol.SetInputData(self.dataset)
2600            spol.Update()
2601            clipper = vtki.new("ClipPolyData")
2602            clipper.SetInputData(spol.GetOutput())
2603            clipper.SetInsideOut(not invert)
2604            clipper.SetValue(0.0)
2605        clipper.Update()
2606        self._update(clipper.GetOutput())
2607
2608        self.pipeline = utils.OperationNode("cut_with_pointloop", parents=parents)
2609        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:
2611    def cut_with_scalar(self, value: float, name="", invert=False) -> Self:
2612        """
2613        Cut a mesh or point cloud with some input scalar point-data.
2614
2615        Arguments:
2616            value : (float)
2617                cutting value
2618            name : (str)
2619                array name of the scalars to be used
2620            invert : (bool)
2621                flip selection
2622
2623        Example:
2624            ```python
2625            from vedo import *
2626            s = Sphere().lw(1)
2627            pts = s.vertices
2628            scalars = np.sin(3*pts[:,2]) + pts[:,0]
2629            s.pointdata["somevalues"] = scalars
2630            s.cut_with_scalar(0.3)
2631            s.cmap("Spectral", "somevalues").add_scalarbar()
2632            s.show(axes=1).close()
2633            ```
2634            ![](https://vedo.embl.es/images/feats/cut_with_scalars.png)
2635        """
2636        if name:
2637            self.pointdata.select(name)
2638        clipper = vtki.new("ClipPolyData")
2639        clipper.SetInputData(self.dataset)
2640        clipper.SetValue(value)
2641        clipper.GenerateClippedOutputOff()
2642        clipper.SetInsideOut(not invert)
2643        clipper.Update()
2644        self._update(clipper.GetOutput())
2645        self.pipeline = utils.OperationNode("cut_with_scalar", parents=[self])
2646        return self

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

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

def crop( self, top=None, bottom=None, right=None, left=None, front=None, back=None, bounds=()) -> Self:
2648    def crop(self,
2649             top=None, bottom=None, right=None, left=None, front=None, back=None,
2650             bounds=()) -> Self:
2651        """
2652        Crop an `Mesh` object.
2653
2654        Arguments:
2655            top : (float)
2656                fraction to crop from the top plane (positive z)
2657            bottom : (float)
2658                fraction to crop from the bottom plane (negative z)
2659            front : (float)
2660                fraction to crop from the front plane (positive y)
2661            back : (float)
2662                fraction to crop from the back plane (negative y)
2663            right : (float)
2664                fraction to crop from the right plane (positive x)
2665            left : (float)
2666                fraction to crop from the left plane (negative x)
2667            bounds : (list)
2668                bounding box of the crop region as `[x0,x1, y0,y1, z0,z1]`
2669
2670        Example:
2671            ```python
2672            from vedo import Sphere
2673            Sphere().crop(right=0.3, left=0.1).show()
2674            ```
2675            ![](https://user-images.githubusercontent.com/32848391/57081955-0ef1e800-6cf6-11e9-99de-b45220939bc9.png)
2676        """
2677        if not len(bounds):
2678            pos = np.array(self.pos())
2679            x0, x1, y0, y1, z0, z1 = self.bounds()
2680            x0, y0, z0 = [x0, y0, z0] - pos
2681            x1, y1, z1 = [x1, y1, z1] - pos
2682
2683            dx, dy, dz = x1 - x0, y1 - y0, z1 - z0
2684            if top:
2685                z1 = z1 - top * dz
2686            if bottom:
2687                z0 = z0 + bottom * dz
2688            if front:
2689                y1 = y1 - front * dy
2690            if back:
2691                y0 = y0 + back * dy
2692            if right:
2693                x1 = x1 - right * dx
2694            if left:
2695                x0 = x0 + left * dx
2696            bounds = (x0, x1, y0, y1, z0, z1)
2697
2698        cu = vtki.new("Box")
2699        cu.SetBounds(bounds)
2700
2701        clipper = vtki.new("ClipPolyData")
2702        clipper.SetInputData(self.dataset)
2703        clipper.SetClipFunction(cu)
2704        clipper.InsideOutOn()
2705        clipper.GenerateClippedOutputOff()
2706        clipper.GenerateClipScalarsOff()
2707        clipper.SetValue(0)
2708        clipper.Update()
2709        self._update(clipper.GetOutput())
2710
2711        self.pipeline = utils.OperationNode(
2712            "crop", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}"
2713        )
2714        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:
2716    def generate_surface_halo(
2717            self, 
2718            distance=0.05,
2719            res=(50, 50, 50),
2720            bounds=(),
2721            maxdist=None,
2722    ) -> "vedo.Mesh":
2723        """
2724        Generate the surface halo which sits at the specified distance from the input one.
2725
2726        Arguments:
2727            distance : (float)
2728                distance from the input surface
2729            res : (int)
2730                resolution of the surface
2731            bounds : (list)
2732                bounding box of the surface
2733            maxdist : (float)
2734                maximum distance to generate the surface
2735        """
2736        if not bounds:
2737            bounds = self.bounds()
2738
2739        if not maxdist:
2740            maxdist = self.diagonal_size() / 2
2741
2742        imp = vtki.new("ImplicitModeller")
2743        imp.SetInputData(self.dataset)
2744        imp.SetSampleDimensions(res)
2745        if maxdist:
2746            imp.SetMaximumDistance(maxdist)
2747        if len(bounds) == 6:
2748            imp.SetModelBounds(bounds)
2749        contour = vtki.new("ContourFilter")
2750        contour.SetInputConnection(imp.GetOutputPort())
2751        contour.SetValue(0, distance)
2752        contour.Update()
2753        out = vedo.Mesh(contour.GetOutput())
2754        out.c("lightblue").alpha(0.25).lighting("off")
2755        out.pipeline = utils.OperationNode("generate_surface_halo", parents=[self])
2756        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:
2758    def generate_mesh(
2759        self,
2760        line_resolution=None,
2761        mesh_resolution=None,
2762        smooth=0.0,
2763        jitter=0.001,
2764        grid=None,
2765        quads=False,
2766        invert=False,
2767    ) -> Self:
2768        """
2769        Generate a polygonal Mesh from a closed contour line.
2770        If line is not closed it will be closed with a straight segment.
2771
2772        Check also `generate_delaunay2d()`.
2773
2774        Arguments:
2775            line_resolution : (int)
2776                resolution of the contour line. The default is None, in this case
2777                the contour is not resampled.
2778            mesh_resolution : (int)
2779                resolution of the internal triangles not touching the boundary.
2780            smooth : (float)
2781                smoothing of the contour before meshing.
2782            jitter : (float)
2783                add a small noise to the internal points.
2784            grid : (Grid)
2785                manually pass a Grid object. The default is True.
2786            quads : (bool)
2787                generate a mesh of quads instead of triangles.
2788            invert : (bool)
2789                flip the line orientation. The default is False.
2790
2791        Examples:
2792            - [line2mesh_tri.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_tri.py)
2793
2794                ![](https://vedo.embl.es/images/advanced/line2mesh_tri.jpg)
2795
2796            - [line2mesh_quads.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_quads.py)
2797
2798                ![](https://vedo.embl.es/images/advanced/line2mesh_quads.png)
2799        """
2800        if line_resolution is None:
2801            contour = vedo.shapes.Line(self.vertices)
2802        else:
2803            contour = vedo.shapes.Spline(self.vertices, smooth=smooth, res=line_resolution)
2804        contour.clean()
2805
2806        length = contour.length()
2807        density = length / contour.npoints
2808        # print(f"tomesh():\n\tline length = {length}")
2809        # print(f"\tdensity = {density} length/pt_separation")
2810
2811        x0, x1 = contour.xbounds()
2812        y0, y1 = contour.ybounds()
2813
2814        if grid is None:
2815            if mesh_resolution is None:
2816                resx = int((x1 - x0) / density + 0.5)
2817                resy = int((y1 - y0) / density + 0.5)
2818                # print(f"tmesh_resolution = {[resx, resy]}")
2819            else:
2820                if utils.is_sequence(mesh_resolution):
2821                    resx, resy = mesh_resolution
2822                else:
2823                    resx, resy = mesh_resolution, mesh_resolution
2824            grid = vedo.shapes.Grid(
2825                [(x0 + x1) / 2, (y0 + y1) / 2, 0],
2826                s=((x1 - x0) * 1.025, (y1 - y0) * 1.025),
2827                res=(resx, resy),
2828            )
2829        else:
2830            grid = grid.clone()
2831
2832        cpts = contour.vertices
2833
2834        # make sure it's closed
2835        p0, p1 = cpts[0], cpts[-1]
2836        nj = max(2, int(utils.mag(p1 - p0) / density + 0.5))
2837        joinline = vedo.shapes.Line(p1, p0, res=nj)
2838        contour = vedo.merge(contour, joinline).subsample(0.0001)
2839
2840        ####################################### quads
2841        if quads:
2842            cmesh = grid.clone().cut_with_point_loop(contour, on="cells", invert=invert)
2843            cmesh.wireframe(False).lw(0.5)
2844            cmesh.pipeline = utils.OperationNode(
2845                "generate_mesh",
2846                parents=[self, contour],
2847                comment=f"#quads {cmesh.dataset.GetNumberOfCells()}",
2848            )
2849            return cmesh
2850        #############################################
2851
2852        grid_tmp = grid.vertices.copy()
2853
2854        if jitter:
2855            np.random.seed(0)
2856            sigma = 1.0 / np.sqrt(grid.npoints) * grid.diagonal_size() * jitter
2857            # print(f"\tsigma jittering = {sigma}")
2858            grid_tmp += np.random.rand(grid.npoints, 3) * sigma
2859            grid_tmp[:, 2] = 0.0
2860
2861        todel = []
2862        density /= np.sqrt(3)
2863        vgrid_tmp = Points(grid_tmp)
2864
2865        for p in contour.vertices:
2866            out = vgrid_tmp.closest_point(p, radius=density, return_point_id=True)
2867            todel += out.tolist()
2868
2869        grid_tmp = grid_tmp.tolist()
2870        for index in sorted(list(set(todel)), reverse=True):
2871            del grid_tmp[index]
2872
2873        points = contour.vertices.tolist() + grid_tmp
2874        if invert:
2875            boundary = list(reversed(range(contour.npoints)))
2876        else:
2877            boundary = list(range(contour.npoints))
2878
2879        dln = Points(points).generate_delaunay2d(mode="xy", boundaries=[boundary])
2880        dln.compute_normals(points=False)  # fixes reversd faces
2881        dln.lw(1)
2882
2883        dln.pipeline = utils.OperationNode(
2884            "generate_mesh",
2885            parents=[self, contour],
2886            comment=f"#cells {dln.dataset.GetNumberOfCells()}",
2887        )
2888        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:
2890    def reconstruct_surface(
2891        self,
2892        dims=(100, 100, 100),
2893        radius=None,
2894        sample_size=None,
2895        hole_filling=True,
2896        bounds=(),
2897        padding=0.05,
2898    ) -> "vedo.Mesh":
2899        """
2900        Surface reconstruction from a scattered cloud of points.
2901
2902        Arguments:
2903            dims : (int)
2904                number of voxels in x, y and z to control precision.
2905            radius : (float)
2906                radius of influence of each point.
2907                Smaller values generally improve performance markedly.
2908                Note that after the signed distance function is computed,
2909                any voxel taking on the value >= radius
2910                is presumed to be "unseen" or uninitialized.
2911            sample_size : (int)
2912                if normals are not present
2913                they will be calculated using this sample size per point.
2914            hole_filling : (bool)
2915                enables hole filling, this generates
2916                separating surfaces between the empty and unseen portions of the volume.
2917            bounds : (list)
2918                region in space in which to perform the sampling
2919                in format (xmin,xmax, ymin,ymax, zim, zmax)
2920            padding : (float)
2921                increase by this fraction the bounding box
2922
2923        Examples:
2924            - [recosurface.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/recosurface.py)
2925
2926                ![](https://vedo.embl.es/images/advanced/recosurface.png)
2927        """
2928        if not utils.is_sequence(dims):
2929            dims = (dims, dims, dims)
2930
2931        sdf = vtki.new("SignedDistance")
2932
2933        if len(bounds) == 6:
2934            sdf.SetBounds(bounds)
2935        else:
2936            x0, x1, y0, y1, z0, z1 = self.bounds()
2937            sdf.SetBounds(
2938                x0 - (x1 - x0) * padding,
2939                x1 + (x1 - x0) * padding,
2940                y0 - (y1 - y0) * padding,
2941                y1 + (y1 - y0) * padding,
2942                z0 - (z1 - z0) * padding,
2943                z1 + (z1 - z0) * padding,
2944            )
2945        
2946        bb = sdf.GetBounds()
2947        if bb[0]==bb[1]:
2948            vedo.logger.warning("reconstruct_surface(): zero x-range")
2949        if bb[2]==bb[3]:
2950            vedo.logger.warning("reconstruct_surface(): zero y-range")
2951        if bb[4]==bb[5]:
2952            vedo.logger.warning("reconstruct_surface(): zero z-range")
2953
2954        pd = self.dataset
2955
2956        if pd.GetPointData().GetNormals():
2957            sdf.SetInputData(pd)
2958        else:
2959            normals = vtki.new("PCANormalEstimation")
2960            normals.SetInputData(pd)
2961            if not sample_size:
2962                sample_size = int(pd.GetNumberOfPoints() / 50)
2963            normals.SetSampleSize(sample_size)
2964            normals.SetNormalOrientationToGraphTraversal()
2965            sdf.SetInputConnection(normals.GetOutputPort())
2966            # print("Recalculating normals with sample size =", sample_size)
2967
2968        if radius is None:
2969            radius = self.diagonal_size() / (sum(dims) / 3) * 5
2970            # print("Calculating mesh from points with radius =", radius)
2971
2972        sdf.SetRadius(radius)
2973        sdf.SetDimensions(dims)
2974        sdf.Update()
2975
2976        surface = vtki.new("ExtractSurface")
2977        surface.SetRadius(radius * 0.99)
2978        surface.SetHoleFilling(hole_filling)
2979        surface.ComputeNormalsOff()
2980        surface.ComputeGradientsOff()
2981        surface.SetInputConnection(sdf.GetOutputPort())
2982        surface.Update()
2983        m = vedo.mesh.Mesh(surface.GetOutput(), c=self.color())
2984
2985        m.pipeline = utils.OperationNode(
2986            "reconstruct_surface",
2987            parents=[self],
2988            comment=f"#pts {m.dataset.GetNumberOfPoints()}",
2989        )
2990        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:
2992    def compute_clustering(self, radius: float) -> Self:
2993        """
2994        Cluster points in space. The `radius` is the radius of local search.
2995        
2996        An array named "ClusterId" is added to `pointdata`.
2997
2998        Examples:
2999            - [clustering.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/clustering.py)
3000
3001                ![](https://vedo.embl.es/images/basic/clustering.png)
3002        """
3003        cluster = vtki.new("EuclideanClusterExtraction")
3004        cluster.SetInputData(self.dataset)
3005        cluster.SetExtractionModeToAllClusters()
3006        cluster.SetRadius(radius)
3007        cluster.ColorClustersOn()
3008        cluster.Update()
3009        idsarr = cluster.GetOutput().GetPointData().GetArray("ClusterId")
3010        self.dataset.GetPointData().AddArray(idsarr)
3011        self.pipeline = utils.OperationNode(
3012            "compute_clustering", parents=[self], comment=f"radius = {radius}"
3013        )
3014        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:
3016    def compute_connections(self, radius, mode=0, regions=(), vrange=(0, 1), seeds=(), angle=0.0) -> Self:
3017        """
3018        Extracts and/or segments points from a point cloud based on geometric distance measures
3019        (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range.
3020        The default operation is to segment the points into "connected" regions where the connection
3021        is determined by an appropriate distance measure. Each region is given a region id.
3022
3023        Optionally, the filter can output the largest connected region of points; a particular region
3024        (via id specification); those regions that are seeded using a list of input point ids;
3025        or the region of points closest to a specified position.
3026
3027        The key parameter of this filter is the radius defining a sphere around each point which defines
3028        a local neighborhood: any other points in the local neighborhood are assumed connected to the point.
3029        Note that the radius is defined in absolute terms.
3030
3031        Other parameters are used to further qualify what it means to be a neighboring point.
3032        For example, scalar range and/or point normals can be used to further constrain the neighborhood.
3033        Also the extraction mode defines how the filter operates.
3034        By default, all regions are extracted but it is possible to extract particular regions;
3035        the region closest to a seed point; seeded regions; or the largest region found while processing.
3036        By default, all regions are extracted.
3037
3038        On output, all points are labeled with a region number.
3039        However note that the number of input and output points may not be the same:
3040        if not extracting all regions then the output size may be less than the input size.
3041
3042        Arguments:
3043            radius : (float)
3044                variable specifying a local sphere used to define local point neighborhood
3045            mode : (int)
3046                - 0,  Extract all regions
3047                - 1,  Extract point seeded regions
3048                - 2,  Extract largest region
3049                - 3,  Test specified regions
3050                - 4,  Extract all regions with scalar connectivity
3051                - 5,  Extract point seeded regions
3052            regions : (list)
3053                a list of non-negative regions id to extract
3054            vrange : (list)
3055                scalar range to use to extract points based on scalar connectivity
3056            seeds : (list)
3057                a list of non-negative point seed ids
3058            angle : (list)
3059                points are connected if the angle between their normals is
3060                within this angle threshold (expressed in degrees).
3061        """
3062        # https://vtk.org/doc/nightly/html/classvtkConnectedPointsFilter.html
3063        cpf = vtki.new("ConnectedPointsFilter")
3064        cpf.SetInputData(self.dataset)
3065        cpf.SetRadius(radius)
3066        if mode == 0:  # Extract all regions
3067            pass
3068
3069        elif mode == 1:  # Extract point seeded regions
3070            cpf.SetExtractionModeToPointSeededRegions()
3071            for s in seeds:
3072                cpf.AddSeed(s)
3073
3074        elif mode == 2:  # Test largest region
3075            cpf.SetExtractionModeToLargestRegion()
3076
3077        elif mode == 3:  # Test specified regions
3078            cpf.SetExtractionModeToSpecifiedRegions()
3079            for r in regions:
3080                cpf.AddSpecifiedRegion(r)
3081
3082        elif mode == 4:  # Extract all regions with scalar connectivity
3083            cpf.SetExtractionModeToLargestRegion()
3084            cpf.ScalarConnectivityOn()
3085            cpf.SetScalarRange(vrange[0], vrange[1])
3086
3087        elif mode == 5:  # Extract point seeded regions
3088            cpf.SetExtractionModeToLargestRegion()
3089            cpf.ScalarConnectivityOn()
3090            cpf.SetScalarRange(vrange[0], vrange[1])
3091            cpf.AlignedNormalsOn()
3092            cpf.SetNormalAngle(angle)
3093
3094        cpf.Update()
3095        self._update(cpf.GetOutput(), reset_locators=False)
3096        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:
3098    def compute_camera_distance(self) -> np.ndarray:
3099        """
3100        Calculate the distance from points to the camera.
3101        
3102        A pointdata array is created with name 'DistanceToCamera' and returned.
3103        """
3104        if vedo.plotter_instance and vedo.plotter_instance.renderer:
3105            poly = self.dataset
3106            dc = vtki.new("DistanceToCamera")
3107            dc.SetInputData(poly)
3108            dc.SetRenderer(vedo.plotter_instance.renderer)
3109            dc.Update()
3110            self._update(dc.GetOutput(), reset_locators=False)
3111            return self.pointdata["DistanceToCamera"]
3112        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:
3114    def densify(self, target_distance=0.1, nclosest=6, radius=None, niter=1, nmax=None) -> Self:
3115        """
3116        Return a copy of the cloud with new added points.
3117        The new points are created in such a way that all points in any local neighborhood are
3118        within a target distance of one another.
3119
3120        For each input point, the distance to all points in its neighborhood is computed.
3121        If any of its neighbors is further than the target distance,
3122        the edge connecting the point and its neighbor is bisected and
3123        a new point is inserted at the bisection point.
3124        A single pass is completed once all the input points are visited.
3125        Then the process repeats to the number of iterations.
3126
3127        Examples:
3128            - [densifycloud.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/densifycloud.py)
3129
3130                ![](https://vedo.embl.es/images/volumetric/densifycloud.png)
3131
3132        .. note::
3133            Points will be created in an iterative fashion until all points in their
3134            local neighborhood are the target distance apart or less.
3135            Note that the process may terminate early due to the
3136            number of iterations. By default the target distance is set to 0.5.
3137            Note that the target_distance should be less than the radius
3138            or nothing will change on output.
3139
3140        .. warning::
3141            This class can generate a lot of points very quickly.
3142            The maximum number of iterations is by default set to =1.0 for this reason.
3143            Increase the number of iterations very carefully.
3144            Also, `nmax` can be set to limit the explosion of points.
3145            It is also recommended that a N closest neighborhood is used.
3146
3147        """
3148        src = vtki.new("ProgrammableSource")
3149        opts = self.vertices
3150        # zeros = np.zeros(3)
3151
3152        def _read_points():
3153            output = src.GetPolyDataOutput()
3154            points = vtki.vtkPoints()
3155            for p in opts:
3156                # print(p)
3157                # if not np.array_equal(p, zeros):
3158                points.InsertNextPoint(p)
3159            output.SetPoints(points)
3160
3161        src.SetExecuteMethod(_read_points)
3162
3163        dens = vtki.new("DensifyPointCloudFilter")
3164        dens.SetInputConnection(src.GetOutputPort())
3165        # dens.SetInputData(self.dataset) # this does not work
3166        dens.InterpolateAttributeDataOn()
3167        dens.SetTargetDistance(target_distance)
3168        dens.SetMaximumNumberOfIterations(niter)
3169        if nmax:
3170            dens.SetMaximumNumberOfPoints(nmax)
3171
3172        if radius:
3173            dens.SetNeighborhoodTypeToRadius()
3174            dens.SetRadius(radius)
3175        elif nclosest:
3176            dens.SetNeighborhoodTypeToNClosest()
3177            dens.SetNumberOfClosestPoints(nclosest)
3178        else:
3179            vedo.logger.error("set either radius or nclosest")
3180            raise RuntimeError()
3181        dens.Update()
3182
3183        cld = Points(dens.GetOutput())
3184        cld.copy_properties_from(self)
3185        cld.interpolate_data_from(self, n=nclosest, radius=radius)
3186        cld.name = "DensifiedCloud"
3187        cld.pipeline = utils.OperationNode(
3188            "densify",
3189            parents=[self],
3190            c="#e9c46a:",
3191            comment=f"#pts {cld.dataset.GetNumberOfPoints()}",
3192        )
3193        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:
3199    def density(
3200        self, dims=(40, 40, 40), bounds=None, radius=None, compute_gradient=False, locator=None
3201    ) -> "vedo.Volume":
3202        """
3203        Generate a density field from a point cloud. Input can also be a set of 3D coordinates.
3204        Output is a `Volume`.
3205
3206        The local neighborhood is specified as the `radius` around each sample position (each voxel).
3207        If left to None, the radius is automatically computed as the diagonal of the bounding box
3208        and can be accessed via `vol.metadata["radius"]`.
3209        The density is expressed as the number of counts in the radius search.
3210
3211        Arguments:
3212            dims : (int, list)
3213                number of voxels in x, y and z of the output Volume.
3214            compute_gradient : (bool)
3215                Turn on/off the generation of the gradient vector,
3216                gradient magnitude scalar, and function classification scalar.
3217                By default this is off. Note that this will increase execution time
3218                and the size of the output. (The names of these point data arrays are:
3219                "Gradient", "Gradient Magnitude", and "Classification")
3220            locator : (vtkPointLocator)
3221                can be assigned from a previous call for speed (access it via `object.point_locator`).
3222
3223        Examples:
3224            - [plot_density3d.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/plot_density3d.py)
3225
3226                ![](https://vedo.embl.es/images/pyplot/plot_density3d.png)
3227        """
3228        pdf = vtki.new("PointDensityFilter")
3229        pdf.SetInputData(self.dataset)
3230
3231        if not utils.is_sequence(dims):
3232            dims = [dims, dims, dims]
3233
3234        if bounds is None:
3235            bounds = list(self.bounds())
3236        elif len(bounds) == 4:
3237            bounds = [*bounds, 0, 0]
3238
3239        if bounds[5] - bounds[4] == 0 or len(dims) == 2:  # its 2D
3240            dims = list(dims)
3241            dims = [dims[0], dims[1], 2]
3242            diag = self.diagonal_size()
3243            bounds[5] = bounds[4] + diag / 1000
3244        pdf.SetModelBounds(bounds)
3245
3246        pdf.SetSampleDimensions(dims)
3247
3248        if locator:
3249            pdf.SetLocator(locator)
3250
3251        pdf.SetDensityEstimateToFixedRadius()
3252        if radius is None:
3253            radius = self.diagonal_size() / 20
3254        pdf.SetRadius(radius)
3255        pdf.SetComputeGradient(compute_gradient)
3256        pdf.Update()
3257
3258        vol = vedo.Volume(pdf.GetOutput()).mode(1)
3259        vol.name = "PointDensity"
3260        vol.metadata["radius"] = radius
3261        vol.locator = pdf.GetLocator()
3262        vol.pipeline = utils.OperationNode(
3263            "density", parents=[self], comment=f"dims={tuple(vol.dimensions())}"
3264        )
3265        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:
3268    def tovolume(
3269        self,
3270        kernel="shepard",
3271        radius=None,
3272        n=None,
3273        bounds=None,
3274        null_value=None,
3275        dims=(25, 25, 25),
3276    ) -> "vedo.Volume":
3277        """
3278        Generate a `Volume` by interpolating a scalar
3279        or vector field which is only known on a scattered set of points or mesh.
3280        Available interpolation kernels are: shepard, gaussian, or linear.
3281
3282        Arguments:
3283            kernel : (str)
3284                interpolation kernel type [shepard]
3285            radius : (float)
3286                radius of the local search
3287            n : (int)
3288                number of point to use for interpolation
3289            bounds : (list)
3290                bounding box of the output Volume object
3291            dims : (list)
3292                dimensions of the output Volume object
3293            null_value : (float)
3294                value to be assigned to invalid points
3295
3296        Examples:
3297            - [interpolate_volume.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/interpolate_volume.py)
3298
3299                ![](https://vedo.embl.es/images/volumetric/59095175-1ec5a300-8918-11e9-8bc0-fd35c8981e2b.jpg)
3300        """
3301        if radius is None and not n:
3302            vedo.logger.error("please set either radius or n")
3303            raise RuntimeError
3304
3305        poly = self.dataset
3306
3307        # Create a probe volume
3308        probe = vtki.vtkImageData()
3309        probe.SetDimensions(dims)
3310        if bounds is None:
3311            bounds = self.bounds()
3312        probe.SetOrigin(bounds[0], bounds[2], bounds[4])
3313        probe.SetSpacing(
3314            (bounds[1] - bounds[0]) / dims[0],
3315            (bounds[3] - bounds[2]) / dims[1],
3316            (bounds[5] - bounds[4]) / dims[2],
3317        )
3318
3319        if not self.point_locator:
3320            self.point_locator = vtki.new("PointLocator")
3321            self.point_locator.SetDataSet(poly)
3322            self.point_locator.BuildLocator()
3323
3324        if kernel == "shepard":
3325            kern = vtki.new("ShepardKernel")
3326            kern.SetPowerParameter(2)
3327        elif kernel == "gaussian":
3328            kern = vtki.new("GaussianKernel")
3329        elif kernel == "linear":
3330            kern = vtki.new("LinearKernel")
3331        else:
3332            vedo.logger.error("Error in tovolume(), available kernels are:")
3333            vedo.logger.error(" [shepard, gaussian, linear]")
3334            raise RuntimeError()
3335
3336        if radius:
3337            kern.SetRadius(radius)
3338
3339        interpolator = vtki.new("PointInterpolator")
3340        interpolator.SetInputData(probe)
3341        interpolator.SetSourceData(poly)
3342        interpolator.SetKernel(kern)
3343        interpolator.SetLocator(self.point_locator)
3344
3345        if n:
3346            kern.SetNumberOfPoints(n)
3347            kern.SetKernelFootprintToNClosest()
3348        else:
3349            kern.SetRadius(radius)
3350
3351        if null_value is not None:
3352            interpolator.SetNullValue(null_value)
3353        else:
3354            interpolator.SetNullPointsStrategyToClosestPoint()
3355        interpolator.Update()
3356
3357        vol = vedo.Volume(interpolator.GetOutput())
3358
3359        vol.pipeline = utils.OperationNode(
3360            "signed_distance",
3361            parents=[self],
3362            comment=f"dims={tuple(vol.dimensions())}",
3363            c="#e9c46a:#0096c7",
3364        )
3365        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:
3368    def generate_segments(self, istart=0, rmax=1e30, niter=3) -> "vedo.shapes.Lines":
3369        """
3370        Generate a line segments from a set of points.
3371        The algorithm is based on the closest point search.
3372
3373        Returns a `Line` object.
3374        This object contains the a metadata array of used vertex counts in "UsedVertexCount"
3375        and the sum of the length of the segments in "SegmentsLengthSum".
3376
3377        Arguments:
3378            istart : (int)
3379                index of the starting point
3380            rmax : (float)
3381                maximum length of a segment
3382            niter : (int)
3383                number of iterations or passes through the points
3384
3385        Examples:
3386            - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py)
3387        """
3388        points = self.vertices
3389        segments = []
3390        dists = []
3391        n = len(points)
3392        used = np.zeros(n, dtype=int)
3393        for _ in range(niter):
3394            i = istart
3395            for _ in range(n):
3396                p = points[i]
3397                ids = self.closest_point(p, n=4, return_point_id=True)
3398                j = ids[1]
3399                if used[j] > 1 or [j, i] in segments:
3400                    j = ids[2]
3401                if used[j] > 1:
3402                    j = ids[3]
3403                d = np.linalg.norm(p - points[j])
3404                if used[j] > 1 or used[i] > 1 or d > rmax:
3405                    i += 1
3406                    if i >= n:
3407                        i = 0
3408                    continue
3409                used[i] += 1
3410                used[j] += 1
3411                segments.append([i, j])
3412                dists.append(d)
3413                i = j
3414        segments = np.array(segments, dtype=int)
3415
3416        lines = vedo.shapes.Lines(points[segments], c="k", lw=3)
3417        lines.metadata["UsedVertexCount"] = used
3418        lines.metadata["SegmentsLengthSum"] = np.sum(dists)
3419        lines.pipeline = utils.OperationNode("generate_segments", parents=[self])
3420        lines.name = "Segments"
3421        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:
3423    def generate_delaunay2d(
3424        self,
3425        mode="scipy",
3426        boundaries=(),
3427        tol=None,
3428        alpha=0.0,
3429        offset=0.0,
3430        transform=None,
3431    ) -> "vedo.mesh.Mesh":
3432        """
3433        Create a mesh from points in the XY plane.
3434        If `mode='fit'` then the filter computes a best fitting
3435        plane and projects the points onto it.
3436
3437        Check also `generate_mesh()`.
3438
3439        Arguments:
3440            tol : (float)
3441                specify a tolerance to control discarding of closely spaced points.
3442                This tolerance is specified as a fraction of the diagonal length of the bounding box of the points.
3443            alpha : (float)
3444                for a non-zero alpha value, only edges or triangles contained
3445                within a sphere centered at mesh vertices will be output.
3446                Otherwise, only triangles will be output.
3447            offset : (float)
3448                multiplier to control the size of the initial, bounding Delaunay triangulation.
3449            transform: (LinearTransform, NonLinearTransform)
3450                a transformation which is applied to points to generate a 2D problem.
3451                This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane.
3452                The points are transformed and triangulated.
3453                The topology of triangulated points is used as the output topology.
3454
3455        Examples:
3456            - [delaunay2d.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delaunay2d.py)
3457
3458                ![](https://vedo.embl.es/images/basic/delaunay2d.png)
3459        """
3460        plist = self.vertices.copy()
3461
3462        #########################################################
3463        if mode == "scipy":
3464            from scipy.spatial import Delaunay as scipy_delaunay
3465
3466            tri = scipy_delaunay(plist[:, 0:2])
3467            return vedo.mesh.Mesh([plist, tri.simplices])
3468        ##########################################################
3469
3470        pd = vtki.vtkPolyData()
3471        vpts = vtki.vtkPoints()
3472        vpts.SetData(utils.numpy2vtk(plist, dtype=np.float32))
3473        pd.SetPoints(vpts)
3474
3475        delny = vtki.new("Delaunay2D")
3476        delny.SetInputData(pd)
3477        if tol:
3478            delny.SetTolerance(tol)
3479        delny.SetAlpha(alpha)
3480        delny.SetOffset(offset)
3481
3482        if transform:
3483            delny.SetTransform(transform.T)
3484        elif mode == "fit":
3485            delny.SetProjectionPlaneMode(vtki.get_class("VTK_BEST_FITTING_PLANE"))
3486        elif mode == "xy" and boundaries:
3487            boundary = vtki.vtkPolyData()
3488            boundary.SetPoints(vpts)
3489            cell_array = vtki.vtkCellArray()
3490            for b in boundaries:
3491                cpolygon = vtki.vtkPolygon()
3492                for idd in b:
3493                    cpolygon.GetPointIds().InsertNextId(idd)
3494                cell_array.InsertNextCell(cpolygon)
3495            boundary.SetPolys(cell_array)
3496            delny.SetSourceData(boundary)
3497
3498        delny.Update()
3499
3500        msh = vedo.mesh.Mesh(delny.GetOutput())
3501        msh.name = "Delaunay2D"
3502        msh.clean().lighting("off")
3503        msh.pipeline = utils.OperationNode(
3504            "delaunay2d",
3505            parents=[self],
3506            comment=f"#cells {msh.dataset.GetNumberOfCells()}",
3507        )
3508        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:
3510    def generate_voronoi(self, padding=0.0, fit=False, method="vtk") -> "vedo.Mesh":
3511        """
3512        Generate the 2D Voronoi convex tiling of the input points (z is ignored).
3513        The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon.
3514
3515        A cell array named "VoronoiID" is added to the output Mesh.
3516
3517        The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest
3518        to one of the input points. Voronoi tessellations are important in computational geometry
3519        (and many other fields), and are the dual of Delaunay triangulations.
3520
3521        Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored
3522        (although carried through to the output).
3523        If you desire to triangulate in a different plane, you can use fit=True.
3524
3525        A brief summary is as follows. Each (generating) input point is associated with
3526        an initial Voronoi tile, which is simply the bounding box of the point set.
3527        A locator is then used to identify nearby points: each neighbor in turn generates a
3528        clipping line positioned halfway between the generating point and the neighboring point,
3529        and orthogonal to the line connecting them. Clips are readily performed by evaluationg the
3530        vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line.
3531        If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip
3532        line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs,
3533        the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region
3534        containing the neighboring clip points. The clip region (along with the points contained in it) is grown
3535        by careful expansion (e.g., outward spiraling iterator over all candidate clip points).
3536        When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi
3537        tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi
3538        tessellation. Note that topological and geometric information is used to generate a valid triangulation
3539        (e.g., merging points and validating topology).
3540
3541        Arguments:
3542            pts : (list)
3543                list of input points.
3544            padding : (float)
3545                padding distance. The default is 0.
3546            fit : (bool)
3547                detect automatically the best fitting plane. The default is False.
3548
3549        Examples:
3550            - [voronoi1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi1.py)
3551
3552                ![](https://vedo.embl.es/images/basic/voronoi1.png)
3553
3554            - [voronoi2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi2.py)
3555
3556                ![](https://vedo.embl.es/images/advanced/voronoi2.png)
3557        """
3558        pts = self.vertices
3559
3560        if method == "scipy":
3561            from scipy.spatial import Voronoi as scipy_voronoi
3562
3563            pts = np.asarray(pts)[:, (0, 1)]
3564            vor = scipy_voronoi(pts)
3565            regs = []  # filter out invalid indices
3566            for r in vor.regions:
3567                flag = True
3568                for x in r:
3569                    if x < 0:
3570                        flag = False
3571                        break
3572                if flag and len(r) > 0:
3573                    regs.append(r)
3574
3575            m = vedo.Mesh([vor.vertices, regs])
3576            m.celldata["VoronoiID"] = np.array(list(range(len(regs)))).astype(int)
3577
3578        elif method == "vtk":
3579            vor = vtki.new("Voronoi2D")
3580            if isinstance(pts, Points):
3581                vor.SetInputData(pts)
3582            else:
3583                pts = np.asarray(pts)
3584                if pts.shape[1] == 2:
3585                    pts = np.c_[pts, np.zeros(len(pts))]
3586                pd = vtki.vtkPolyData()
3587                vpts = vtki.vtkPoints()
3588                vpts.SetData(utils.numpy2vtk(pts, dtype=np.float32))
3589                pd.SetPoints(vpts)
3590                vor.SetInputData(pd)
3591            vor.SetPadding(padding)
3592            vor.SetGenerateScalarsToPointIds()
3593            if fit:
3594                vor.SetProjectionPlaneModeToBestFittingPlane()
3595            else:
3596                vor.SetProjectionPlaneModeToXYPlane()
3597            vor.Update()
3598            poly = vor.GetOutput()
3599            arr = poly.GetCellData().GetArray(0)
3600            if arr:
3601                arr.SetName("VoronoiID")
3602            m = vedo.Mesh(poly, c="orange5")
3603
3604        else:
3605            vedo.logger.error(f"Unknown method {method} in voronoi()")
3606            raise RuntimeError
3607
3608        m.lw(2).lighting("off").wireframe()
3609        m.name = "Voronoi"
3610        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:
3613    def generate_delaunay3d(self, radius=0, tol=None) -> "vedo.TetMesh":
3614        """
3615        Create 3D Delaunay triangulation of input points.
3616
3617        Arguments:
3618            radius : (float)
3619                specify distance (or "alpha") value to control output.
3620                For a non-zero values, only tetra contained within the circumsphere
3621                will be output.
3622            tol : (float)
3623                Specify a tolerance to control discarding of closely spaced points.
3624                This tolerance is specified as a fraction of the diagonal length of
3625                the bounding box of the points.
3626        """
3627        deln = vtki.new("Delaunay3D")
3628        deln.SetInputData(self.dataset)
3629        deln.SetAlpha(radius)
3630        deln.AlphaTetsOn()
3631        deln.AlphaTrisOff()
3632        deln.AlphaLinesOff()
3633        deln.AlphaVertsOff()
3634        deln.BoundingTriangulationOff()
3635        if tol:
3636            deln.SetTolerance(tol)
3637        deln.Update()
3638        m = vedo.TetMesh(deln.GetOutput())
3639        m.pipeline = utils.OperationNode(
3640            "generate_delaunay3d", c="#e9c46a:#edabab", parents=[self],
3641        )
3642        m.name = "Delaunay3D"
3643        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]:
3646    def visible_points(self, area=(), tol=None, invert=False) -> Union[Self, None]:
3647        """
3648        Extract points based on whether they are visible or not.
3649        Visibility is determined by accessing the z-buffer of a rendering window.
3650        The position of each input point is converted into display coordinates,
3651        and then the z-value at that point is obtained.
3652        If within the user-specified tolerance, the point is considered visible.
3653        Associated data attributes are passed to the output as well.
3654
3655        This filter also allows you to specify a rectangular window in display (pixel)
3656        coordinates in which the visible points must lie.
3657
3658        Arguments:
3659            area : (list)
3660                specify a rectangular region as (xmin,xmax,ymin,ymax)
3661            tol : (float)
3662                a tolerance in normalized display coordinate system
3663            invert : (bool)
3664                select invisible points instead.
3665
3666        Example:
3667            ```python
3668            from vedo import Ellipsoid, show
3669            s = Ellipsoid().rotate_y(30)
3670
3671            # Camera options: pos, focal_point, viewup, distance
3672            camopts = dict(pos=(0,0,25), focal_point=(0,0,0))
3673            show(s, camera=camopts, offscreen=True)
3674
3675            m = s.visible_points()
3676            # print('visible pts:', m.vertices)  # numpy array
3677            show(m, new=True, axes=1).close() # optionally draw result in a new window
3678            ```
3679            ![](https://vedo.embl.es/images/feats/visible_points.png)
3680        """
3681        svp = vtki.new("SelectVisiblePoints")
3682        svp.SetInputData(self.dataset)
3683
3684        ren = None
3685        if vedo.plotter_instance:
3686            if vedo.plotter_instance.renderer:
3687                ren = vedo.plotter_instance.renderer
3688                svp.SetRenderer(ren)
3689        if not ren:
3690            vedo.logger.warning(
3691                "visible_points() can only be used after a rendering step"
3692            )
3693            return None
3694
3695        if len(area) == 2:
3696            area = utils.flatten(area)
3697        if len(area) == 4:
3698            # specify a rectangular region
3699            svp.SetSelection(area[0], area[1], area[2], area[3])
3700        if tol is not None:
3701            svp.SetTolerance(tol)
3702        if invert:
3703            svp.SelectInvisibleOn()
3704        svp.Update()
3705
3706        m = Points(svp.GetOutput())
3707        m.name = "VisiblePoints"
3708        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:
442def Point(pos=(0, 0, 0), r=12, c="red", alpha=1.0) -> Self:
443    """
444    Create a simple point in space.
445
446    .. note:: if you are creating many points you should use class `Points` instead!
447    """
448    pt = Points([[0,0,0]], r, c, alpha).pos(pos)
449    pt.name = "Point"
450    return pt

Create a simple point in space.

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

Work with point clouds.

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

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

Check out also cell_centers().

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

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

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

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

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