vedo.pointcloud
Submodule to work with point clouds.
1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3import time 4from typing import Union, List 5from typing_extensions import Self 6from weakref import ref as weak_ref_to 7import numpy as np 8 9import vedo.vtkclasses as vtki 10 11import vedo 12from vedo import colors 13from vedo import utils 14from vedo.transformations import LinearTransform, NonLinearTransform 15from vedo.core import PointAlgorithms 16from vedo.visual import PointsVisual 17 18__docformat__ = "google" 19 20__doc__ = """ 21Submodule to work with point clouds. 22 23![](https://vedo.embl.es/images/basic/pca.png) 24""" 25 26__all__ = [ 27 "Points", 28 "Point", 29 "CellCenters", 30 "merge", 31 "delaunay2d", # deprecated, use .generate_delaunay2d() 32 "fit_line", 33 "fit_circle", 34 "fit_plane", 35 "fit_sphere", 36 "pca_ellipse", 37 "pca_ellipsoid", 38] 39 40 41#################################################### 42def merge(*meshs, flag=False) -> Union["vedo.Mesh", "vedo.Points", None]: 43 """ 44 Build a new Mesh (or Points) formed by the fusion of the inputs. 45 46 Similar to Assembly, but in this case the input objects become a single entity. 47 48 To keep track of the original identities of the inputs you can set `flag=True`. 49 In this case a `pointdata` array of ids is added to the output with name "OriginalMeshID". 50 51 Examples: 52 - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py) 53 54 ![](https://vedo.embl.es/images/advanced/warp1.png) 55 56 - [value_iteration.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/value_iteration.py) 57 58 """ 59 objs = [a for a in utils.flatten(meshs) if a] 60 61 if not objs: 62 return None 63 64 idarr = [] 65 polyapp = vtki.new("AppendPolyData") 66 for i, ob in enumerate(objs): 67 polyapp.AddInputData(ob.dataset) 68 if flag: 69 idarr += [i] * ob.dataset.GetNumberOfPoints() 70 polyapp.Update() 71 mpoly = polyapp.GetOutput() 72 73 if flag: 74 varr = utils.numpy2vtk(idarr, dtype=np.uint16, name="OriginalMeshID") 75 mpoly.GetPointData().AddArray(varr) 76 77 has_mesh = False 78 for ob in objs: 79 if isinstance(ob, vedo.Mesh): 80 has_mesh = True 81 break 82 83 if has_mesh: 84 msh = vedo.Mesh(mpoly) 85 else: 86 msh = Points(mpoly) # type: ignore 87 88 msh.copy_properties_from(objs[0]) 89 90 msh.pipeline = utils.OperationNode( 91 "merge", parents=objs, comment=f"#pts {msh.dataset.GetNumberOfPoints()}" 92 ) 93 return msh 94 95 96def delaunay2d(plist, **kwargs) -> Self: 97 """delaunay2d() is deprecated, use Points().generate_delaunay2d() instead.""" 98 if isinstance(plist, Points): 99 plist = plist.vertices 100 else: 101 plist = np.ascontiguousarray(plist) 102 plist = utils.make3d(plist) 103 pp = Points(plist).generate_delaunay2d(**kwargs) 104 print("WARNING: delaunay2d() is deprecated, use Points().generate_delaunay2d() instead") 105 return pp 106 107 108def _rotate_points(points, n0=None, n1=(0, 0, 1)) -> Union[np.ndarray, tuple]: 109 # Rotate a set of 3D points from direction n0 to direction n1. 110 # Return the rotated points and the normal to the fitting plane (if n0 is None). 111 # The pointing direction of the normal in this case is arbitrary. 112 points = np.asarray(points) 113 114 if points.ndim == 1: 115 points = points[np.newaxis, :] 116 117 if len(points[0]) == 2: 118 return points, (0, 0, 1) 119 120 if n0 is None: # fit plane 121 datamean = points.mean(axis=0) 122 vv = np.linalg.svd(points - datamean)[2] 123 n0 = np.cross(vv[0], vv[1]) 124 125 n0 = n0 / np.linalg.norm(n0) 126 n1 = n1 / np.linalg.norm(n1) 127 k = np.cross(n0, n1) 128 l = np.linalg.norm(k) 129 if not l: 130 k = n0 131 k /= np.linalg.norm(k) 132 133 ct = np.dot(n0, n1) 134 theta = np.arccos(ct) 135 st = np.sin(theta) 136 v = k * (1 - ct) 137 138 rpoints = [] 139 for p in points: 140 a = p * ct 141 b = np.cross(k, p) * st 142 c = v * np.dot(k, p) 143 rpoints.append(a + b + c) 144 145 return np.array(rpoints), n0 146 147 148def fit_line(points: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Line": 149 """ 150 Fits a line through points. 151 152 Extra info is stored in `Line.slope`, `Line.center`, `Line.variances`. 153 154 Examples: 155 - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py) 156 157 ![](https://vedo.embl.es/images/advanced/fitline.png) 158 """ 159 if isinstance(points, Points): 160 points = points.vertices 161 data = np.asarray(points) 162 datamean = data.mean(axis=0) 163 _, dd, vv = np.linalg.svd(data - datamean) 164 vv = vv[0] / np.linalg.norm(vv[0]) 165 # vv contains the first principal component, i.e. the direction 166 # vector of the best fit line in the least squares sense. 167 xyz_min = data.min(axis=0) 168 xyz_max = data.max(axis=0) 169 a = np.linalg.norm(xyz_min - datamean) 170 b = np.linalg.norm(xyz_max - datamean) 171 p1 = datamean - a * vv 172 p2 = datamean + b * vv 173 line = vedo.shapes.Line(p1, p2, lw=1) 174 line.slope = vv 175 line.center = datamean 176 line.variances = dd 177 return line 178 179 180def fit_circle(points: Union[np.ndarray, "vedo.Points"]) -> tuple: 181 """ 182 Fits a circle through a set of 3D points, with a very fast non-iterative method. 183 184 Returns the tuple `(center, radius, normal_to_circle)`. 185 186 .. warning:: 187 trying to fit s-shaped points will inevitably lead to instabilities and 188 circles of small radius. 189 190 References: 191 *J.F. Crawford, Nucl. Instr. Meth. 211, 1983, 223-225.* 192 """ 193 if isinstance(points, Points): 194 points = points.vertices 195 data = np.asarray(points) 196 197 offs = data.mean(axis=0) 198 data, n0 = _rotate_points(data - offs) 199 200 xi = data[:, 0] 201 yi = data[:, 1] 202 203 x = sum(xi) 204 xi2 = xi * xi 205 xx = sum(xi2) 206 xxx = sum(xi2 * xi) 207 208 y = sum(yi) 209 yi2 = yi * yi 210 yy = sum(yi2) 211 yyy = sum(yi2 * yi) 212 213 xiyi = xi * yi 214 xy = sum(xiyi) 215 xyy = sum(xiyi * yi) 216 xxy = sum(xi * xiyi) 217 218 N = len(xi) 219 k = (xx + yy) / N 220 221 a1 = xx - x * x / N 222 b1 = xy - x * y / N 223 c1 = 0.5 * (xxx + xyy - x * k) 224 225 a2 = xy - x * y / N 226 b2 = yy - y * y / N 227 c2 = 0.5 * (xxy + yyy - y * k) 228 229 d = a2 * b1 - a1 * b2 230 if not d: 231 return offs, 0, n0 232 x0 = (b1 * c2 - b2 * c1) / d 233 y0 = (c1 - a1 * x0) / b1 234 235 R = np.sqrt(x0 * x0 + y0 * y0 - 1 / N * (2 * x0 * x + 2 * y0 * y - xx - yy)) 236 237 c, _ = _rotate_points([x0, y0, 0], (0, 0, 1), n0) 238 239 return c[0] + offs, R, n0 240 241 242def fit_plane(points: Union[np.ndarray, "vedo.Points"], signed=False) -> "vedo.shapes.Plane": 243 """ 244 Fits a plane to a set of points. 245 246 Extra info is stored in `Plane.normal`, `Plane.center`, `Plane.variance`. 247 248 Arguments: 249 signed : (bool) 250 if True flip sign of the normal based on the ordering of the points 251 252 Examples: 253 - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py) 254 255 ![](https://vedo.embl.es/images/advanced/fitline.png) 256 """ 257 if isinstance(points, Points): 258 points = points.vertices 259 data = np.asarray(points) 260 datamean = data.mean(axis=0) 261 pts = data - datamean 262 res = np.linalg.svd(pts) 263 dd, vv = res[1], res[2] 264 n = np.cross(vv[0], vv[1]) 265 if signed: 266 v = np.zeros_like(pts) 267 for i in range(len(pts) - 1): 268 vi = np.cross(pts[i], pts[i + 1]) 269 v[i] = vi / np.linalg.norm(vi) 270 ns = np.mean(v, axis=0) # normal to the points plane 271 if np.dot(n, ns) < 0: 272 n = -n 273 xyz_min = data.min(axis=0) 274 xyz_max = data.max(axis=0) 275 s = np.linalg.norm(xyz_max - xyz_min) 276 pla = vedo.shapes.Plane(datamean, n, s=[s, s]) 277 pla.variance = dd[2] 278 pla.name = "FitPlane" 279 return pla 280 281 282def fit_sphere(coords: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Sphere": 283 """ 284 Fits a sphere to a set of points. 285 286 Extra info is stored in `Sphere.radius`, `Sphere.center`, `Sphere.residue`. 287 288 Examples: 289 - [fitspheres1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitspheres1.py) 290 291 ![](https://vedo.embl.es/images/advanced/fitspheres1.jpg) 292 """ 293 if isinstance(coords, Points): 294 coords = coords.vertices 295 coords = np.array(coords) 296 n = len(coords) 297 A = np.zeros((n, 4)) 298 A[:, :-1] = coords * 2 299 A[:, 3] = 1 300 f = np.zeros((n, 1)) 301 x = coords[:, 0] 302 y = coords[:, 1] 303 z = coords[:, 2] 304 f[:, 0] = x * x + y * y + z * z 305 try: 306 C, residue, rank, _ = np.linalg.lstsq(A, f, rcond=-1) # solve AC=f 307 except: 308 C, residue, rank, _ = np.linalg.lstsq(A, f) # solve AC=f 309 if rank < 4: 310 return None 311 t = (C[0] * C[0]) + (C[1] * C[1]) + (C[2] * C[2]) + C[3] 312 radius = np.sqrt(t)[0] 313 center = np.array([C[0][0], C[1][0], C[2][0]]) 314 if len(residue) > 0: 315 residue = np.sqrt(residue[0]) / n 316 else: 317 residue = 0 318 sph = vedo.shapes.Sphere(center, radius, c=(1, 0, 0)).wireframe(1) 319 sph.radius = radius 320 sph.center = center 321 sph.residue = residue 322 sph.name = "FitSphere" 323 return sph 324 325 326def pca_ellipse(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=60) -> Union["vedo.shapes.Circle", None]: 327 """ 328 Create the oriented 2D ellipse that contains the fraction `pvalue` of points. 329 PCA (Principal Component Analysis) is used to compute the ellipse orientation. 330 331 Parameter `pvalue` sets the specified fraction of points inside the ellipse. 332 Normalized directions are stored in `ellipse.axis1`, `ellipse.axis2`. 333 Axes sizes are stored in `ellipse.va`, `ellipse.vb` 334 335 Arguments: 336 pvalue : (float) 337 ellipse will include this fraction of points 338 res : (int) 339 resolution of the ellipse 340 341 Examples: 342 - [pca_ellipse.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipse.py) 343 - [histo_pca.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_pca.py) 344 345 ![](https://vedo.embl.es/images/pyplot/histo_pca.png) 346 """ 347 from scipy.stats import f 348 349 if isinstance(points, Points): 350 coords = points.vertices 351 else: 352 coords = points 353 if len(coords) < 4: 354 vedo.logger.warning("in pca_ellipse(), there are not enough points!") 355 return None 356 357 P = np.array(coords, dtype=float)[:, (0, 1)] 358 cov = np.cov(P, rowvar=0) # type: ignore 359 _, s, R = np.linalg.svd(cov) # singular value decomposition 360 p, n = s.size, P.shape[0] 361 fppf = f.ppf(pvalue, p, n - p) # f % point function 362 u = np.sqrt(s * fppf / 2) * 2 # semi-axes (largest first) 363 ua, ub = u 364 center = utils.make3d(np.mean(P, axis=0)) # centroid of the ellipse 365 366 t = LinearTransform(R.T * u).translate(center) 367 elli = vedo.shapes.Circle(alpha=0.75, res=res) 368 elli.apply_transform(t) 369 elli.properties.LightingOff() 370 371 elli.pvalue = pvalue 372 elli.center = np.array([center[0], center[1], 0]) 373 elli.nr_of_points = n 374 elli.va = ua 375 elli.vb = ub 376 377 # we subtract center because it's in t 378 elli.axis1 = t.move([1, 0, 0]) - center 379 elli.axis2 = t.move([0, 1, 0]) - center 380 381 elli.axis1 /= np.linalg.norm(elli.axis1) 382 elli.axis2 /= np.linalg.norm(elli.axis2) 383 elli.name = "PCAEllipse" 384 return elli 385 386 387def pca_ellipsoid(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=24) -> Union["vedo.shapes.Ellipsoid", None]: 388 """ 389 Create the oriented ellipsoid that contains the fraction `pvalue` of points. 390 PCA (Principal Component Analysis) is used to compute the ellipsoid orientation. 391 392 Axes sizes can be accessed in `ellips.va`, `ellips.vb`, `ellips.vc`, 393 normalized directions are stored in `ellips.axis1`, `ellips.axis2` and `ellips.axis3`. 394 Center of mass is stored in `ellips.center`. 395 396 Asphericity can be accessed in `ellips.asphericity()` and ellips.asphericity_error(). 397 A value of 0 means a perfect sphere. 398 399 Arguments: 400 pvalue : (float) 401 ellipsoid will include this fraction of points 402 403 Examples: 404 [pca_ellipsoid.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipsoid.py) 405 406 ![](https://vedo.embl.es/images/basic/pca.png) 407 408 See also: 409 `pca_ellipse()` for a 2D ellipse. 410 """ 411 from scipy.stats import f 412 413 if isinstance(points, Points): 414 coords = points.vertices 415 else: 416 coords = points 417 if len(coords) < 4: 418 vedo.logger.warning("in pca_ellipsoid(), not enough input points!") 419 return None 420 421 P = np.array(coords, ndmin=2, dtype=float) 422 cov = np.cov(P, rowvar=0) # type: ignore 423 _, s, R = np.linalg.svd(cov) # singular value decomposition 424 p, n = s.size, P.shape[0] 425 fppf = f.ppf(pvalue, p, n-p)*(n-1)*p*(n+1)/n/(n-p) # f % point function 426 u = np.sqrt(s*fppf) 427 ua, ub, uc = u # semi-axes (largest first) 428 center = np.mean(P, axis=0) # centroid of the hyperellipsoid 429 430 t = LinearTransform(R.T * u).translate(center) 431 elli = vedo.shapes.Ellipsoid((0,0,0), (1,0,0), (0,1,0), (0,0,1), res=res) 432 elli.apply_transform(t) 433 elli.alpha(0.25) 434 elli.properties.LightingOff() 435 436 elli.pvalue = pvalue 437 elli.nr_of_points = n 438 elli.center = center 439 elli.va = ua 440 elli.vb = ub 441 elli.vc = uc 442 # we subtract center because it's in t 443 elli.axis1 = np.array(t.move([1, 0, 0])) - center 444 elli.axis2 = np.array(t.move([0, 1, 0])) - center 445 elli.axis3 = np.array(t.move([0, 0, 1])) - center 446 elli.axis1 /= np.linalg.norm(elli.axis1) 447 elli.axis2 /= np.linalg.norm(elli.axis2) 448 elli.axis3 /= np.linalg.norm(elli.axis3) 449 elli.name = "PCAEllipsoid" 450 return elli 451 452 453################################################### 454def Point(pos=(0, 0, 0), r=12, c="red", alpha=1.0) -> Self: 455 """ 456 Create a simple point in space. 457 458 .. note:: if you are creating many points you should use class `Points` instead! 459 """ 460 pt = Points([[0,0,0]], r, c, alpha).pos(pos) 461 pt.name = "Point" 462 return pt 463 464 465################################################### 466class Points(PointsVisual, PointAlgorithms): 467 """Work with point clouds.""" 468 469 def __init__(self, inputobj=None, r=4, c=(0.2, 0.2, 0.2), alpha=1): 470 """ 471 Build an object made of only vertex points for a list of 2D/3D points. 472 Both shapes (N, 3) or (3, N) are accepted as input, if N>3. 473 474 Arguments: 475 inputobj : (list, tuple) 476 r : (int) 477 Point radius in units of pixels. 478 c : (str, list) 479 Color name or rgb tuple. 480 alpha : (float) 481 Transparency in range [0,1]. 482 483 Example: 484 ```python 485 from vedo import * 486 487 def fibonacci_sphere(n): 488 s = np.linspace(0, n, num=n, endpoint=False) 489 theta = s * 2.399963229728653 490 y = 1 - s * (2/(n-1)) 491 r = np.sqrt(1 - y * y) 492 x = np.cos(theta) * r 493 z = np.sin(theta) * r 494 return np._c[x,y,z] 495 496 Points(fibonacci_sphere(1000)).show(axes=1).close() 497 ``` 498 ![](https://vedo.embl.es/images/feats/fibonacci.png) 499 """ 500 # print("INIT POINTS") 501 super().__init__() 502 503 self.name = "" 504 self.filename = "" 505 self.file_size = "" 506 507 self.info = {} 508 self.time = time.time() 509 510 self.transform = LinearTransform() 511 self.point_locator = None 512 self.cell_locator = None 513 self.line_locator = None 514 515 self.actor = vtki.vtkActor() 516 self.properties = self.actor.GetProperty() 517 self.properties_backface = self.actor.GetBackfaceProperty() 518 self.mapper = vtki.new("PolyDataMapper") 519 self.dataset = vtki.vtkPolyData() 520 521 # Create weakref so actor can access this object (eg to pick/remove): 522 self.actor.retrieve_object = weak_ref_to(self) 523 524 try: 525 self.properties.RenderPointsAsSpheresOn() 526 except AttributeError: 527 pass 528 529 if inputobj is None: #################### 530 return 531 ########################################## 532 533 self.name = "Points" 534 535 ###### 536 if isinstance(inputobj, vtki.vtkActor): 537 self.dataset.DeepCopy(inputobj.GetMapper().GetInput()) 538 pr = vtki.vtkProperty() 539 pr.DeepCopy(inputobj.GetProperty()) 540 self.actor.SetProperty(pr) 541 self.properties = pr 542 self.mapper.SetScalarVisibility(inputobj.GetMapper().GetScalarVisibility()) 543 544 elif isinstance(inputobj, vtki.vtkPolyData): 545 self.dataset = inputobj 546 if self.dataset.GetNumberOfCells() == 0: 547 carr = vtki.vtkCellArray() 548 for i in range(self.dataset.GetNumberOfPoints()): 549 carr.InsertNextCell(1) 550 carr.InsertCellPoint(i) 551 self.dataset.SetVerts(carr) 552 553 elif isinstance(inputobj, Points): 554 self.dataset = inputobj.dataset 555 self.copy_properties_from(inputobj) 556 557 elif utils.is_sequence(inputobj): # passing point coords 558 self.dataset = utils.buildPolyData(utils.make3d(inputobj)) 559 560 elif isinstance(inputobj, str): 561 verts = vedo.file_io.load(inputobj) 562 self.filename = inputobj 563 self.dataset = verts.dataset 564 565 elif "meshlib" in str(type(inputobj)): 566 from meshlib import mrmeshnumpy as mn 567 self.dataset = utils.buildPolyData(mn.toNumpyArray(inputobj.points)) 568 569 else: 570 # try to extract the points from a generic VTK input data object 571 if hasattr(inputobj, "dataset"): 572 inputobj = inputobj.dataset 573 try: 574 vvpts = inputobj.GetPoints() 575 self.dataset = vtki.vtkPolyData() 576 self.dataset.SetPoints(vvpts) 577 for i in range(inputobj.GetPointData().GetNumberOfArrays()): 578 arr = inputobj.GetPointData().GetArray(i) 579 self.dataset.GetPointData().AddArray(arr) 580 except: 581 vedo.logger.error(f"cannot build Points from type {type(inputobj)}") 582 raise RuntimeError() 583 584 self.actor.SetMapper(self.mapper) 585 self.mapper.SetInputData(self.dataset) 586 587 self.properties.SetColor(colors.get_color(c)) 588 self.properties.SetOpacity(alpha) 589 self.properties.SetRepresentationToPoints() 590 self.properties.SetPointSize(r) 591 self.properties.LightingOff() 592 593 self.pipeline = utils.OperationNode( 594 self, parents=[], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 595 ) 596 597 def _update(self, polydata, reset_locators=True) -> Self: 598 """Overwrite the polygonal dataset with a new vtkPolyData.""" 599 self.dataset = polydata 600 self.mapper.SetInputData(self.dataset) 601 self.mapper.Modified() 602 if reset_locators: 603 self.point_locator = None 604 self.line_locator = None 605 self.cell_locator = None 606 return self 607 608 def __str__(self): 609 """Print a description of the Points/Mesh.""" 610 module = self.__class__.__module__ 611 name = self.__class__.__name__ 612 out = vedo.printc( 613 f"{module}.{name} at ({hex(self.memory_address())})".ljust(75), 614 c="g", bold=True, invert=True, return_string=True, 615 ) 616 out += "\x1b[0m\x1b[32;1m" 617 618 if self.name: 619 out += "name".ljust(14) + ": " + self.name 620 if "legend" in self.info.keys() and self.info["legend"]: 621 out+= f", legend='{self.info['legend']}'" 622 out += "\n" 623 624 if self.filename: 625 out+= "file name".ljust(14) + ": " + self.filename + "\n" 626 627 if not self.mapper.GetScalarVisibility(): 628 col = utils.precision(self.properties.GetColor(), 3) 629 cname = vedo.colors.get_color_name(self.properties.GetColor()) 630 out+= "color".ljust(14) + ": " + cname 631 out+= f", rgb={col}, alpha={self.properties.GetOpacity()}\n" 632 if self.actor.GetBackfaceProperty(): 633 bcol = self.actor.GetBackfaceProperty().GetDiffuseColor() 634 cname = vedo.colors.get_color_name(bcol) 635 out+= "backface color".ljust(14) + ": " 636 out+= f"{cname}, rgb={utils.precision(bcol,3)}\n" 637 638 npt = self.dataset.GetNumberOfPoints() 639 npo, nln = self.dataset.GetNumberOfPolys(), self.dataset.GetNumberOfLines() 640 out+= "elements".ljust(14) + f": vertices={npt:,} polygons={npo:,} lines={nln:,}" 641 if self.dataset.GetNumberOfStrips(): 642 out+= f", strips={self.dataset.GetNumberOfStrips():,}" 643 out+= "\n" 644 if self.dataset.GetNumberOfPieces() > 1: 645 out+= "pieces".ljust(14) + ": " + str(self.dataset.GetNumberOfPieces()) + "\n" 646 647 out+= "position".ljust(14) + ": " + f"{utils.precision(self.pos(), 6)}\n" 648 try: 649 sc = self.transform.get_scale() 650 out+= "scaling".ljust(14) + ": " 651 out+= utils.precision(sc, 6) + "\n" 652 except AttributeError: 653 pass 654 655 if self.npoints: 656 out+="size".ljust(14)+ ": average=" + utils.precision(self.average_size(),6) 657 out+=", diagonal="+ utils.precision(self.diagonal_size(), 6)+ "\n" 658 out+="center of mass".ljust(14) + ": " + utils.precision(self.center_of_mass(),6)+"\n" 659 660 bnds = self.bounds() 661 bx1, bx2 = utils.precision(bnds[0], 3), utils.precision(bnds[1], 3) 662 by1, by2 = utils.precision(bnds[2], 3), utils.precision(bnds[3], 3) 663 bz1, bz2 = utils.precision(bnds[4], 3), utils.precision(bnds[5], 3) 664 out+= "bounds".ljust(14) + ":" 665 out+= " x=(" + bx1 + ", " + bx2 + ")," 666 out+= " y=(" + by1 + ", " + by2 + ")," 667 out+= " z=(" + bz1 + ", " + bz2 + ")\n" 668 669 for key in self.pointdata.keys(): 670 arr = self.pointdata[key] 671 dim = arr.shape[1] if arr.ndim > 1 else 1 672 mark_active = "pointdata" 673 a_scalars = self.dataset.GetPointData().GetScalars() 674 a_vectors = self.dataset.GetPointData().GetVectors() 675 a_tensors = self.dataset.GetPointData().GetTensors() 676 if a_scalars and a_scalars.GetName() == key: 677 mark_active += " *" 678 elif a_vectors and a_vectors.GetName() == key: 679 mark_active += " **" 680 elif a_tensors and a_tensors.GetName() == key: 681 mark_active += " ***" 682 out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), dim={dim}' 683 if dim == 1 and len(arr): 684 rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3) 685 out += f", range=({rng})\n" 686 else: 687 out += "\n" 688 689 for key in self.celldata.keys(): 690 arr = self.celldata[key] 691 dim = arr.shape[1] if arr.ndim > 1 else 1 692 mark_active = "celldata" 693 a_scalars = self.dataset.GetCellData().GetScalars() 694 a_vectors = self.dataset.GetCellData().GetVectors() 695 a_tensors = self.dataset.GetCellData().GetTensors() 696 if a_scalars and a_scalars.GetName() == key: 697 mark_active += " *" 698 elif a_vectors and a_vectors.GetName() == key: 699 mark_active += " **" 700 elif a_tensors and a_tensors.GetName() == key: 701 mark_active += " ***" 702 out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), dim={dim}' 703 if dim == 1 and len(arr): 704 rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3) 705 out += f", range=({rng})\n" 706 else: 707 out += "\n" 708 709 for key in self.metadata.keys(): 710 arr = self.metadata[key] 711 if len(arr) > 3: 712 out+= "metadata".ljust(14) + ": " + f'"{key}" ({len(arr)} values)\n' 713 else: 714 out+= "metadata".ljust(14) + ": " + f'"{key}" = {arr}\n' 715 716 if self.picked3d is not None: 717 idp = self.closest_point(self.picked3d, return_point_id=True) 718 idc = self.closest_point(self.picked3d, return_cell_id=True) 719 out+= "clicked point".ljust(14) + ": " + utils.precision(self.picked3d, 6) 720 out+= f", pointID={idp}, cellID={idc}\n" 721 722 return out.rstrip() + "\x1b[0m" 723 724 def _repr_html_(self): 725 """ 726 HTML representation of the Point cloud object for Jupyter Notebooks. 727 728 Returns: 729 HTML text with the image and some properties. 730 """ 731 import io 732 import base64 733 from PIL import Image 734 735 library_name = "vedo.pointcloud.Points" 736 help_url = "https://vedo.embl.es/docs/vedo/pointcloud.html#Points" 737 738 arr = self.thumbnail() 739 im = Image.fromarray(arr) 740 buffered = io.BytesIO() 741 im.save(buffered, format="PNG", quality=100) 742 encoded = base64.b64encode(buffered.getvalue()).decode("utf-8") 743 url = "data:image/png;base64," + encoded 744 image = f"<img src='{url}'></img>" 745 746 bounds = "<br/>".join( 747 [ 748 utils.precision(min_x, 4) + " ... " + utils.precision(max_x, 4) 749 for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2]) 750 ] 751 ) 752 average_size = "{size:.3f}".format(size=self.average_size()) 753 754 help_text = "" 755 if self.name: 756 help_text += f"<b> {self.name}:   </b>" 757 help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>" 758 if self.filename: 759 dots = "" 760 if len(self.filename) > 30: 761 dots = "..." 762 help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>" 763 764 pdata = "" 765 if self.dataset.GetPointData().GetScalars(): 766 if self.dataset.GetPointData().GetScalars().GetName(): 767 name = self.dataset.GetPointData().GetScalars().GetName() 768 pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>" 769 770 cdata = "" 771 if self.dataset.GetCellData().GetScalars(): 772 if self.dataset.GetCellData().GetScalars().GetName(): 773 name = self.dataset.GetCellData().GetScalars().GetName() 774 cdata = "<tr><td><b> cell data array </b></td><td>" + name + "</td></tr>" 775 776 allt = [ 777 "<table>", 778 "<tr>", 779 "<td>", 780 image, 781 "</td>", 782 "<td style='text-align: center; vertical-align: center;'><br/>", 783 help_text, 784 "<table>", 785 "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>", 786 "<tr><td><b> center of mass </b></td><td>" 787 + utils.precision(self.center_of_mass(), 3) 788 + "</td></tr>", 789 "<tr><td><b> average size </b></td><td>" + str(average_size) + "</td></tr>", 790 "<tr><td><b> nr. points </b></td><td>" + str(self.npoints) + "</td></tr>", 791 pdata, 792 cdata, 793 "</table>", 794 "</table>", 795 ] 796 return "\n".join(allt) 797 798 ################################################################################## 799 def __add__(self, meshs): 800 """ 801 Add two meshes or a list of meshes together to form an `Assembly` object. 802 """ 803 if isinstance(meshs, list): 804 alist = [self] 805 for l in meshs: 806 if isinstance(l, vedo.Assembly): 807 alist += l.unpack() 808 else: 809 alist += l 810 return vedo.assembly.Assembly(alist) 811 812 if isinstance(meshs, vedo.Assembly): 813 return meshs + self # use Assembly.__add__ 814 815 return vedo.assembly.Assembly([self, meshs]) 816 817 def polydata(self, **kwargs): 818 """ 819 Obsolete. Use property `.dataset` instead. 820 Returns the underlying `vtkPolyData` object. 821 """ 822 colors.printc( 823 "WARNING: call to .polydata() is obsolete, use property .dataset instead.", 824 c="y") 825 return self.dataset 826 827 def __copy__(self): 828 return self.clone(deep=False) 829 830 def __deepcopy__(self, memo): 831 return self.clone(deep=memo) 832 833 def copy(self, deep=True) -> Self: 834 """Return a copy of the object. Alias of `clone()`.""" 835 return self.clone(deep=deep) 836 837 def clone(self, deep=True) -> Self: 838 """ 839 Clone a `PointCloud` or `Mesh` object to make an exact copy of it. 840 Alias of `copy()`. 841 842 Arguments: 843 deep : (bool) 844 if False return a shallow copy of the mesh without copying the points array. 845 846 Examples: 847 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 848 849 ![](https://vedo.embl.es/images/basic/mirror.png) 850 """ 851 poly = vtki.vtkPolyData() 852 if deep or isinstance(deep, dict): # if a memo object is passed this checks as True 853 poly.DeepCopy(self.dataset) 854 else: 855 poly.ShallowCopy(self.dataset) 856 857 if isinstance(self, vedo.Mesh): 858 cloned = vedo.Mesh(poly) 859 else: 860 cloned = Points(poly) 861 # print([self], self.__class__) 862 # cloned = self.__class__(poly) 863 864 cloned.transform = self.transform.clone() 865 866 cloned.copy_properties_from(self) 867 868 cloned.name = str(self.name) 869 cloned.filename = str(self.filename) 870 cloned.info = dict(self.info) 871 cloned.pipeline = utils.OperationNode("clone", parents=[self], shape="diamond", c="#edede9") 872 873 if isinstance(deep, dict): 874 deep[id(self)] = cloned 875 876 return cloned 877 878 def compute_normals_with_pca(self, n=20, orientation_point=None, invert=False) -> Self: 879 """ 880 Generate point normals using PCA (principal component analysis). 881 This algorithm estimates a local tangent plane around each sample point p 882 by considering a small neighborhood of points around p, and fitting a plane 883 to the neighborhood (via PCA). 884 885 Arguments: 886 n : (int) 887 neighborhood size to calculate the normal 888 orientation_point : (list) 889 adjust the +/- sign of the normals so that 890 the normals all point towards a specified point. If None, perform a traversal 891 of the point cloud and flip neighboring normals so that they are mutually consistent. 892 invert : (bool) 893 flip all normals 894 """ 895 poly = self.dataset 896 pcan = vtki.new("PCANormalEstimation") 897 pcan.SetInputData(poly) 898 pcan.SetSampleSize(n) 899 900 if orientation_point is not None: 901 pcan.SetNormalOrientationToPoint() 902 pcan.SetOrientationPoint(orientation_point) 903 else: 904 pcan.SetNormalOrientationToGraphTraversal() 905 906 if invert: 907 pcan.FlipNormalsOn() 908 pcan.Update() 909 910 varr = pcan.GetOutput().GetPointData().GetNormals() 911 varr.SetName("Normals") 912 self.dataset.GetPointData().SetNormals(varr) 913 self.dataset.GetPointData().Modified() 914 return self 915 916 def compute_acoplanarity(self, n=25, radius=None, on="points") -> Self: 917 """ 918 Compute acoplanarity which is a measure of how much a local region of the mesh 919 differs from a plane. 920 921 The information is stored in a `pointdata` or `celldata` array with name 'Acoplanarity'. 922 923 Either `n` (number of neighbour points) or `radius` (radius of local search) can be specified. 924 If a radius value is given and not enough points fall inside it, then a -1 is stored. 925 926 Example: 927 ```python 928 from vedo import * 929 msh = ParametricShape('RandomHills') 930 msh.compute_acoplanarity(radius=0.1, on='cells') 931 msh.cmap("coolwarm", on='cells').add_scalarbar() 932 msh.show(axes=1).close() 933 ``` 934 ![](https://vedo.embl.es/images/feats/acoplanarity.jpg) 935 """ 936 acoplanarities = [] 937 if "point" in on: 938 pts = self.vertices 939 elif "cell" in on: 940 pts = self.cell_centers 941 else: 942 raise ValueError(f"In compute_acoplanarity() set on to either 'cells' or 'points', not {on}") 943 944 for p in utils.progressbar(pts, delay=5, width=15, title=f"{on} acoplanarity"): 945 if n: 946 data = self.closest_point(p, n=n) 947 npts = n 948 elif radius: 949 data = self.closest_point(p, radius=radius) 950 npts = len(data) 951 952 try: 953 center = data.mean(axis=0) 954 res = np.linalg.svd(data - center) 955 acoplanarities.append(res[1][2] / npts) 956 except: 957 acoplanarities.append(-1.0) 958 959 if "point" in on: 960 self.pointdata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 961 else: 962 self.celldata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 963 return self 964 965 def distance_to(self, pcloud, signed=False, invert=False, name="Distance") -> np.ndarray: 966 """ 967 Computes the distance from one point cloud or mesh to another point cloud or mesh. 968 This new `pointdata` array is saved with default name "Distance". 969 970 Keywords `signed` and `invert` are used to compute signed distance, 971 but the mesh in that case must have polygonal faces (not a simple point cloud), 972 and normals must also be computed. 973 974 Examples: 975 - [distance2mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/distance2mesh.py) 976 977 ![](https://vedo.embl.es/images/basic/distance2mesh.png) 978 """ 979 if pcloud.dataset.GetNumberOfPolys(): 980 981 poly1 = self.dataset 982 poly2 = pcloud.dataset 983 df = vtki.new("DistancePolyDataFilter") 984 df.ComputeSecondDistanceOff() 985 df.SetInputData(0, poly1) 986 df.SetInputData(1, poly2) 987 df.SetSignedDistance(signed) 988 df.SetNegateDistance(invert) 989 df.Update() 990 scals = df.GetOutput().GetPointData().GetScalars() 991 dists = utils.vtk2numpy(scals) 992 993 else: # has no polygons 994 995 if signed: 996 vedo.logger.warning("distance_to() called with signed=True but input object has no polygons") 997 998 if not pcloud.point_locator: 999 pcloud.point_locator = vtki.new("PointLocator") 1000 pcloud.point_locator.SetDataSet(pcloud.dataset) 1001 pcloud.point_locator.BuildLocator() 1002 1003 ids = [] 1004 ps1 = self.vertices 1005 ps2 = pcloud.vertices 1006 for p in ps1: 1007 pid = pcloud.point_locator.FindClosestPoint(p) 1008 ids.append(pid) 1009 1010 deltas = ps2[ids] - ps1 1011 dists = np.linalg.norm(deltas, axis=1).astype(np.float32) 1012 scals = utils.numpy2vtk(dists) 1013 1014 scals.SetName(name) 1015 self.dataset.GetPointData().AddArray(scals) 1016 self.dataset.GetPointData().SetActiveScalars(scals.GetName()) 1017 rng = scals.GetRange() 1018 self.mapper.SetScalarRange(rng[0], rng[1]) 1019 self.mapper.ScalarVisibilityOn() 1020 1021 self.pipeline = utils.OperationNode( 1022 "distance_to", 1023 parents=[self, pcloud], 1024 shape="cylinder", 1025 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1026 ) 1027 return dists 1028 1029 def clean(self) -> Self: 1030 """Clean pointcloud or mesh by removing coincident points.""" 1031 cpd = vtki.new("CleanPolyData") 1032 cpd.PointMergingOn() 1033 cpd.ConvertLinesToPointsOff() 1034 cpd.ConvertPolysToLinesOff() 1035 cpd.ConvertStripsToPolysOff() 1036 cpd.SetInputData(self.dataset) 1037 cpd.Update() 1038 self._update(cpd.GetOutput()) 1039 self.pipeline = utils.OperationNode( 1040 "clean", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1041 ) 1042 return self 1043 1044 def subsample(self, fraction: float, absolute=False) -> Self: 1045 """ 1046 Subsample a point cloud by requiring that the points 1047 or vertices are far apart at least by the specified fraction of the object size. 1048 If a Mesh is passed the polygonal faces are not removed 1049 but holes can appear as their vertices are removed. 1050 1051 Examples: 1052 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1053 1054 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1055 1056 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1057 1058 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1059 """ 1060 if not absolute: 1061 if fraction > 1: 1062 vedo.logger.warning( 1063 f"subsample(fraction=...), fraction must be < 1, but is {fraction}" 1064 ) 1065 if fraction <= 0: 1066 return self 1067 1068 cpd = vtki.new("CleanPolyData") 1069 cpd.PointMergingOn() 1070 cpd.ConvertLinesToPointsOn() 1071 cpd.ConvertPolysToLinesOn() 1072 cpd.ConvertStripsToPolysOn() 1073 cpd.SetInputData(self.dataset) 1074 if absolute: 1075 cpd.SetTolerance(fraction / self.diagonal_size()) 1076 # cpd.SetToleranceIsAbsolute(absolute) 1077 else: 1078 cpd.SetTolerance(fraction) 1079 cpd.Update() 1080 1081 ps = 2 1082 if self.properties.GetRepresentation() == 0: 1083 ps = self.properties.GetPointSize() 1084 1085 self._update(cpd.GetOutput()) 1086 self.ps(ps) 1087 1088 self.pipeline = utils.OperationNode( 1089 "subsample", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1090 ) 1091 return self 1092 1093 def threshold(self, scalars: str, above=None, below=None, on="points") -> Self: 1094 """ 1095 Extracts cells where scalar value satisfies threshold criterion. 1096 1097 Arguments: 1098 scalars : (str) 1099 name of the scalars array. 1100 above : (float) 1101 minimum value of the scalar 1102 below : (float) 1103 maximum value of the scalar 1104 on : (str) 1105 if 'cells' assume array of scalars refers to cell data. 1106 1107 Examples: 1108 - [mesh_threshold.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_threshold.py) 1109 """ 1110 thres = vtki.new("Threshold") 1111 thres.SetInputData(self.dataset) 1112 1113 if on.startswith("c"): 1114 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_CELLS 1115 else: 1116 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_POINTS 1117 1118 thres.SetInputArrayToProcess(0, 0, 0, asso, scalars) 1119 1120 if above is None and below is not None: 1121 try: # vtk 9.2 1122 thres.ThresholdByLower(below) 1123 except AttributeError: # vtk 9.3 1124 thres.SetUpperThreshold(below) 1125 1126 elif below is None and above is not None: 1127 try: 1128 thres.ThresholdByUpper(above) 1129 except AttributeError: 1130 thres.SetLowerThreshold(above) 1131 else: 1132 try: 1133 thres.ThresholdBetween(above, below) 1134 except AttributeError: 1135 thres.SetUpperThreshold(below) 1136 thres.SetLowerThreshold(above) 1137 1138 thres.Update() 1139 1140 gf = vtki.new("GeometryFilter") 1141 gf.SetInputData(thres.GetOutput()) 1142 gf.Update() 1143 self._update(gf.GetOutput()) 1144 self.pipeline = utils.OperationNode("threshold", parents=[self]) 1145 return self 1146 1147 def quantize(self, value: float) -> Self: 1148 """ 1149 The user should input a value and all {x,y,z} coordinates 1150 will be quantized to that absolute grain size. 1151 """ 1152 qp = vtki.new("QuantizePolyDataPoints") 1153 qp.SetInputData(self.dataset) 1154 qp.SetQFactor(value) 1155 qp.Update() 1156 self._update(qp.GetOutput()) 1157 self.pipeline = utils.OperationNode("quantize", parents=[self]) 1158 return self 1159 1160 @property 1161 def vertex_normals(self) -> np.ndarray: 1162 """ 1163 Retrieve vertex normals as a numpy array. Same as `point_normals`. 1164 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1165 """ 1166 vtknormals = self.dataset.GetPointData().GetNormals() 1167 return utils.vtk2numpy(vtknormals) 1168 1169 @property 1170 def point_normals(self) -> np.ndarray: 1171 """ 1172 Retrieve vertex normals as a numpy array. Same as `vertex_normals`. 1173 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1174 """ 1175 vtknormals = self.dataset.GetPointData().GetNormals() 1176 return utils.vtk2numpy(vtknormals) 1177 1178 def align_to(self, target, iters=100, rigid=False, invert=False, use_centroids=False) -> Self: 1179 """ 1180 Aligned to target mesh through the `Iterative Closest Point` algorithm. 1181 1182 The core of the algorithm is to match each vertex in one surface with 1183 the closest surface point on the other, then apply the transformation 1184 that modify one surface to best match the other (in the least-square sense). 1185 1186 Arguments: 1187 rigid : (bool) 1188 if True do not allow scaling 1189 invert : (bool) 1190 if True start by aligning the target to the source but 1191 invert the transformation finally. Useful when the target is smaller 1192 than the source. 1193 use_centroids : (bool) 1194 start by matching the centroids of the two objects. 1195 1196 Examples: 1197 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1198 1199 ![](https://vedo.embl.es/images/basic/align1.png) 1200 1201 - [align2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align2.py) 1202 1203 ![](https://vedo.embl.es/images/basic/align2.png) 1204 """ 1205 icp = vtki.new("IterativeClosestPointTransform") 1206 icp.SetSource(self.dataset) 1207 icp.SetTarget(target.dataset) 1208 if invert: 1209 icp.Inverse() 1210 icp.SetMaximumNumberOfIterations(iters) 1211 if rigid: 1212 icp.GetLandmarkTransform().SetModeToRigidBody() 1213 icp.SetStartByMatchingCentroids(use_centroids) 1214 icp.Update() 1215 1216 self.apply_transform(icp.GetMatrix()) 1217 1218 self.pipeline = utils.OperationNode( 1219 "align_to", parents=[self, target], comment=f"rigid = {rigid}" 1220 ) 1221 return self 1222 1223 def align_to_bounding_box(self, msh, rigid=False) -> Self: 1224 """ 1225 Align the current object's bounding box to the bounding box 1226 of the input object. 1227 1228 Use `rigid=True` to disable scaling. 1229 1230 Example: 1231 [align6.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align6.py) 1232 """ 1233 lmt = vtki.vtkLandmarkTransform() 1234 ss = vtki.vtkPoints() 1235 xss0, xss1, yss0, yss1, zss0, zss1 = self.bounds() 1236 for p in [ 1237 [xss0, yss0, zss0], 1238 [xss1, yss0, zss0], 1239 [xss1, yss1, zss0], 1240 [xss0, yss1, zss0], 1241 [xss0, yss0, zss1], 1242 [xss1, yss0, zss1], 1243 [xss1, yss1, zss1], 1244 [xss0, yss1, zss1], 1245 ]: 1246 ss.InsertNextPoint(p) 1247 st = vtki.vtkPoints() 1248 xst0, xst1, yst0, yst1, zst0, zst1 = msh.bounds() 1249 for p in [ 1250 [xst0, yst0, zst0], 1251 [xst1, yst0, zst0], 1252 [xst1, yst1, zst0], 1253 [xst0, yst1, zst0], 1254 [xst0, yst0, zst1], 1255 [xst1, yst0, zst1], 1256 [xst1, yst1, zst1], 1257 [xst0, yst1, zst1], 1258 ]: 1259 st.InsertNextPoint(p) 1260 1261 lmt.SetSourceLandmarks(ss) 1262 lmt.SetTargetLandmarks(st) 1263 lmt.SetModeToAffine() 1264 if rigid: 1265 lmt.SetModeToRigidBody() 1266 lmt.Update() 1267 1268 LT = LinearTransform(lmt) 1269 self.apply_transform(LT) 1270 return self 1271 1272 def align_with_landmarks( 1273 self, 1274 source_landmarks, 1275 target_landmarks, 1276 rigid=False, 1277 affine=False, 1278 least_squares=False, 1279 ) -> Self: 1280 """ 1281 Transform mesh orientation and position based on a set of landmarks points. 1282 The algorithm finds the best matching of source points to target points 1283 in the mean least square sense, in one single step. 1284 1285 If `affine` is True the x, y and z axes can scale independently but stay collinear. 1286 With least_squares they can vary orientation. 1287 1288 Examples: 1289 - [align5.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align5.py) 1290 1291 ![](https://vedo.embl.es/images/basic/align5.png) 1292 """ 1293 1294 if utils.is_sequence(source_landmarks): 1295 ss = vtki.vtkPoints() 1296 for p in source_landmarks: 1297 ss.InsertNextPoint(p) 1298 else: 1299 ss = source_landmarks.dataset.GetPoints() 1300 if least_squares: 1301 source_landmarks = source_landmarks.vertices 1302 1303 if utils.is_sequence(target_landmarks): 1304 st = vtki.vtkPoints() 1305 for p in target_landmarks: 1306 st.InsertNextPoint(p) 1307 else: 1308 st = target_landmarks.GetPoints() 1309 if least_squares: 1310 target_landmarks = target_landmarks.vertices 1311 1312 if ss.GetNumberOfPoints() != st.GetNumberOfPoints(): 1313 n1 = ss.GetNumberOfPoints() 1314 n2 = st.GetNumberOfPoints() 1315 vedo.logger.error(f"source and target have different nr of points {n1} vs {n2}") 1316 raise RuntimeError() 1317 1318 if int(rigid) + int(affine) + int(least_squares) > 1: 1319 vedo.logger.error( 1320 "only one of rigid, affine, least_squares can be True at a time" 1321 ) 1322 raise RuntimeError() 1323 1324 lmt = vtki.vtkLandmarkTransform() 1325 lmt.SetSourceLandmarks(ss) 1326 lmt.SetTargetLandmarks(st) 1327 lmt.SetModeToSimilarity() 1328 1329 if rigid: 1330 lmt.SetModeToRigidBody() 1331 lmt.Update() 1332 1333 elif affine: 1334 lmt.SetModeToAffine() 1335 lmt.Update() 1336 1337 elif least_squares: 1338 cms = source_landmarks.mean(axis=0) 1339 cmt = target_landmarks.mean(axis=0) 1340 m = np.linalg.lstsq(source_landmarks - cms, target_landmarks - cmt, rcond=None)[0] 1341 M = vtki.vtkMatrix4x4() 1342 for i in range(3): 1343 for j in range(3): 1344 M.SetElement(j, i, m[i][j]) 1345 lmt = vtki.vtkTransform() 1346 lmt.Translate(cmt) 1347 lmt.Concatenate(M) 1348 lmt.Translate(-cms) 1349 1350 else: 1351 lmt.Update() 1352 1353 self.apply_transform(lmt) 1354 self.pipeline = utils.OperationNode("transform_with_landmarks", parents=[self]) 1355 return self 1356 1357 def normalize(self) -> Self: 1358 """Scale average size to unit. The scaling is performed around the center of mass.""" 1359 coords = self.vertices 1360 if not coords.shape[0]: 1361 return self 1362 cm = np.mean(coords, axis=0) 1363 pts = coords - cm 1364 xyz2 = np.sum(pts * pts, axis=0) 1365 scale = 1 / np.sqrt(np.sum(xyz2) / len(pts)) 1366 self.scale(scale, origin=cm) 1367 self.pipeline = utils.OperationNode("normalize", parents=[self]) 1368 return self 1369 1370 def mirror(self, axis="x", origin=True) -> Self: 1371 """ 1372 Mirror reflect along one of the cartesian axes 1373 1374 Arguments: 1375 axis : (str) 1376 axis to use for mirroring, must be set to `x, y, z`. 1377 Or any combination of those. 1378 origin : (list) 1379 use this point as the origin of the mirroring transformation. 1380 1381 Examples: 1382 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 1383 1384 ![](https://vedo.embl.es/images/basic/mirror.png) 1385 """ 1386 sx, sy, sz = 1, 1, 1 1387 if "x" in axis.lower(): sx = -1 1388 if "y" in axis.lower(): sy = -1 1389 if "z" in axis.lower(): sz = -1 1390 1391 self.scale([sx, sy, sz], origin=origin) 1392 1393 self.pipeline = utils.OperationNode( 1394 "mirror", comment=f"axis = {axis}", parents=[self]) 1395 1396 if sx * sy * sz < 0: 1397 if hasattr(self, "reverse"): 1398 self.reverse() 1399 return self 1400 1401 def flip_normals(self) -> Self: 1402 """Flip all normals orientation.""" 1403 rs = vtki.new("ReverseSense") 1404 rs.SetInputData(self.dataset) 1405 rs.ReverseCellsOff() 1406 rs.ReverseNormalsOn() 1407 rs.Update() 1408 self._update(rs.GetOutput()) 1409 self.pipeline = utils.OperationNode("flip_normals", parents=[self]) 1410 return self 1411 1412 def add_gaussian_noise(self, sigma=1.0) -> Self: 1413 """ 1414 Add gaussian noise to point positions. 1415 An extra array is added named "GaussianNoise" with the displacements. 1416 1417 Arguments: 1418 sigma : (float) 1419 nr. of standard deviations, expressed in percent of the diagonal size of mesh. 1420 Can also be a list `[sigma_x, sigma_y, sigma_z]`. 1421 1422 Example: 1423 ```python 1424 from vedo import Sphere 1425 Sphere().add_gaussian_noise(1.0).point_size(8).show().close() 1426 ``` 1427 """ 1428 sz = self.diagonal_size() 1429 pts = self.vertices 1430 n = len(pts) 1431 ns = (np.random.randn(n, 3) * sigma) * (sz / 100) 1432 vpts = vtki.vtkPoints() 1433 vpts.SetNumberOfPoints(n) 1434 vpts.SetData(utils.numpy2vtk(pts + ns, dtype=np.float32)) 1435 self.dataset.SetPoints(vpts) 1436 self.dataset.GetPoints().Modified() 1437 self.pointdata["GaussianNoise"] = -ns 1438 self.pipeline = utils.OperationNode( 1439 "gaussian_noise", parents=[self], shape="egg", comment=f"sigma = {sigma}" 1440 ) 1441 return self 1442 1443 def closest_point( 1444 self, pt, n=1, radius=None, return_point_id=False, return_cell_id=False 1445 ) -> Union[List[int], int, np.ndarray]: 1446 """ 1447 Find the closest point(s) on a mesh given from the input point `pt`. 1448 1449 Arguments: 1450 n : (int) 1451 if greater than 1, return a list of n ordered closest points 1452 radius : (float) 1453 if given, get all points within that radius. Then n is ignored. 1454 return_point_id : (bool) 1455 return point ID instead of coordinates 1456 return_cell_id : (bool) 1457 return cell ID in which the closest point sits 1458 1459 Examples: 1460 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1461 - [fitplanes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitplanes.py) 1462 - [quadratic_morphing.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/quadratic_morphing.py) 1463 1464 .. note:: 1465 The appropriate tree search locator is built on the fly and cached for speed. 1466 1467 If you want to reset it use `mymesh.point_locator=None` 1468 and / or `mymesh.cell_locator=None`. 1469 """ 1470 if len(pt) != 3: 1471 pt = [pt[0], pt[1], 0] 1472 1473 # NB: every time the mesh moves or is warped the locators are set to None 1474 if ((n > 1 or radius) or (n == 1 and return_point_id)) and not return_cell_id: 1475 poly = None 1476 if not self.point_locator: 1477 poly = self.dataset 1478 self.point_locator = vtki.new("StaticPointLocator") 1479 self.point_locator.SetDataSet(poly) 1480 self.point_locator.BuildLocator() 1481 1482 ########## 1483 if radius: 1484 vtklist = vtki.vtkIdList() 1485 self.point_locator.FindPointsWithinRadius(radius, pt, vtklist) 1486 elif n > 1: 1487 vtklist = vtki.vtkIdList() 1488 self.point_locator.FindClosestNPoints(n, pt, vtklist) 1489 else: # n==1 hence return_point_id==True 1490 ######## 1491 return self.point_locator.FindClosestPoint(pt) 1492 ######## 1493 1494 if return_point_id: 1495 ######## 1496 return utils.vtk2numpy(vtklist) 1497 ######## 1498 1499 if not poly: 1500 poly = self.dataset 1501 trgp = [] 1502 for i in range(vtklist.GetNumberOfIds()): 1503 trgp_ = [0, 0, 0] 1504 vi = vtklist.GetId(i) 1505 poly.GetPoints().GetPoint(vi, trgp_) 1506 trgp.append(trgp_) 1507 ######## 1508 return np.array(trgp) 1509 ######## 1510 1511 else: 1512 1513 if not self.cell_locator: 1514 poly = self.dataset 1515 1516 # As per Miquel example with limbs the vtkStaticCellLocator doesnt work !! 1517 # https://discourse.vtk.org/t/vtkstaticcelllocator-problem-vtk9-0-3/7854/4 1518 if vedo.vtk_version[0] >= 9 and vedo.vtk_version[1] > 0: 1519 self.cell_locator = vtki.new("StaticCellLocator") 1520 else: 1521 self.cell_locator = vtki.new("CellLocator") 1522 1523 self.cell_locator.SetDataSet(poly) 1524 self.cell_locator.BuildLocator() 1525 1526 if radius is not None: 1527 vedo.printc("Warning: closest_point() with radius is not implemented for cells.", c='r') 1528 1529 if n != 1: 1530 vedo.printc("Warning: closest_point() with n>1 is not implemented for cells.", c='r') 1531 1532 trgp = [0, 0, 0] 1533 cid = vtki.mutable(0) 1534 dist2 = vtki.mutable(0) 1535 subid = vtki.mutable(0) 1536 self.cell_locator.FindClosestPoint(pt, trgp, cid, subid, dist2) 1537 1538 if return_cell_id: 1539 return int(cid) 1540 1541 return np.array(trgp) 1542 1543 def auto_distance(self) -> np.ndarray: 1544 """ 1545 Calculate the distance to the closest point in the same cloud of points. 1546 The output is stored in a new pointdata array called "AutoDistance", 1547 and it is also returned by the function. 1548 """ 1549 points = self.vertices 1550 if not self.point_locator: 1551 self.point_locator = vtki.new("StaticPointLocator") 1552 self.point_locator.SetDataSet(self.dataset) 1553 self.point_locator.BuildLocator() 1554 qs = [] 1555 vtklist = vtki.vtkIdList() 1556 vtkpoints = self.dataset.GetPoints() 1557 for p in points: 1558 self.point_locator.FindClosestNPoints(2, p, vtklist) 1559 q = [0, 0, 0] 1560 pid = vtklist.GetId(1) 1561 vtkpoints.GetPoint(pid, q) 1562 qs.append(q) 1563 dists = np.linalg.norm(points - np.array(qs), axis=1) 1564 self.pointdata["AutoDistance"] = dists 1565 return dists 1566 1567 def hausdorff_distance(self, points) -> float: 1568 """ 1569 Compute the Hausdorff distance to the input point set. 1570 Returns a single `float`. 1571 1572 Example: 1573 ```python 1574 from vedo import * 1575 t = np.linspace(0, 2*np.pi, 100) 1576 x = 4/3 * sin(t)**3 1577 y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12 1578 pol1 = Line(np.c_[x,y], closed=True).triangulate() 1579 pol2 = Polygon(nsides=5).pos(2,2) 1580 d12 = pol1.distance_to(pol2) 1581 d21 = pol2.distance_to(pol1) 1582 pol1.lw(0).cmap("viridis") 1583 pol2.lw(0).cmap("viridis") 1584 print("distance d12, d21 :", min(d12), min(d21)) 1585 print("hausdorff distance:", pol1.hausdorff_distance(pol2)) 1586 print("chamfer distance :", pol1.chamfer_distance(pol2)) 1587 show(pol1, pol2, axes=1) 1588 ``` 1589 ![](https://vedo.embl.es/images/feats/heart.png) 1590 """ 1591 hp = vtki.new("HausdorffDistancePointSetFilter") 1592 hp.SetInputData(0, self.dataset) 1593 hp.SetInputData(1, points.dataset) 1594 hp.SetTargetDistanceMethodToPointToCell() 1595 hp.Update() 1596 return hp.GetHausdorffDistance() 1597 1598 def chamfer_distance(self, pcloud) -> float: 1599 """ 1600 Compute the Chamfer distance to the input point set. 1601 1602 Example: 1603 ```python 1604 from vedo import * 1605 cloud1 = np.random.randn(1000, 3) 1606 cloud2 = np.random.randn(1000, 3) + [1, 2, 3] 1607 c1 = Points(cloud1, r=5, c="red") 1608 c2 = Points(cloud2, r=5, c="green") 1609 d = c1.chamfer_distance(c2) 1610 show(f"Chamfer distance = {d}", c1, c2, axes=1).close() 1611 ``` 1612 """ 1613 # Definition of Chamfer distance may vary, here we use the average 1614 if not pcloud.point_locator: 1615 pcloud.point_locator = vtki.new("PointLocator") 1616 pcloud.point_locator.SetDataSet(pcloud.dataset) 1617 pcloud.point_locator.BuildLocator() 1618 if not self.point_locator: 1619 self.point_locator = vtki.new("PointLocator") 1620 self.point_locator.SetDataSet(self.dataset) 1621 self.point_locator.BuildLocator() 1622 1623 ps1 = self.vertices 1624 ps2 = pcloud.vertices 1625 1626 ids12 = [] 1627 for p in ps1: 1628 pid12 = pcloud.point_locator.FindClosestPoint(p) 1629 ids12.append(pid12) 1630 deltav = ps2[ids12] - ps1 1631 da = np.mean(np.linalg.norm(deltav, axis=1)) 1632 1633 ids21 = [] 1634 for p in ps2: 1635 pid21 = self.point_locator.FindClosestPoint(p) 1636 ids21.append(pid21) 1637 deltav = ps1[ids21] - ps2 1638 db = np.mean(np.linalg.norm(deltav, axis=1)) 1639 return (da + db) / 2 1640 1641 def remove_outliers(self, radius: float, neighbors=5) -> Self: 1642 """ 1643 Remove outliers from a cloud of points within the specified `radius` search. 1644 1645 Arguments: 1646 radius : (float) 1647 Specify the local search radius. 1648 neighbors : (int) 1649 Specify the number of neighbors that a point must have, 1650 within the specified radius, for the point to not be considered isolated. 1651 1652 Examples: 1653 - [clustering.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/clustering.py) 1654 1655 ![](https://vedo.embl.es/images/basic/clustering.png) 1656 """ 1657 removal = vtki.new("RadiusOutlierRemoval") 1658 removal.SetInputData(self.dataset) 1659 removal.SetRadius(radius) 1660 removal.SetNumberOfNeighbors(neighbors) 1661 removal.GenerateOutliersOff() 1662 removal.Update() 1663 inputobj = removal.GetOutput() 1664 if inputobj.GetNumberOfCells() == 0: 1665 carr = vtki.vtkCellArray() 1666 for i in range(inputobj.GetNumberOfPoints()): 1667 carr.InsertNextCell(1) 1668 carr.InsertCellPoint(i) 1669 inputobj.SetVerts(carr) 1670 self._update(removal.GetOutput()) 1671 self.pipeline = utils.OperationNode("remove_outliers", parents=[self]) 1672 return self 1673 1674 def relax_point_positions( 1675 self, 1676 n=10, 1677 iters=10, 1678 sub_iters=10, 1679 packing_factor=1, 1680 max_step=0, 1681 constraints=(), 1682 ) -> Self: 1683 """ 1684 Smooth mesh or points with a 1685 [Laplacian algorithm](https://vtk.org/doc/nightly/html/classvtkPointSmoothingFilter.html) 1686 variant. This modifies the coordinates of the input points by adjusting their positions 1687 to create a smooth distribution (and thereby form a pleasing packing of the points). 1688 Smoothing is performed by considering the effects of neighboring points on one another 1689 it uses a cubic cutoff function to produce repulsive forces between close points 1690 and attractive forces that are a little further away. 1691 1692 In general, the larger the neighborhood size, the greater the reduction in high frequency 1693 information. The memory and computational requirements of the algorithm may also 1694 significantly increase. 1695 1696 The algorithm incrementally adjusts the point positions through an iterative process. 1697 Basically points are moved due to the influence of neighboring points. 1698 1699 As points move, both the local connectivity and data attributes associated with each point 1700 must be updated. Rather than performing these expensive operations after every iteration, 1701 a number of sub-iterations can be specified. If so, then the neighborhood and attribute 1702 value updates occur only every sub iteration, which can improve performance significantly. 1703 1704 Arguments: 1705 n : (int) 1706 neighborhood size to calculate the Laplacian. 1707 iters : (int) 1708 number of iterations. 1709 sub_iters : (int) 1710 number of sub-iterations, i.e. the number of times the neighborhood and attribute 1711 value updates occur during each iteration. 1712 packing_factor : (float) 1713 adjust convergence speed. 1714 max_step : (float) 1715 Specify the maximum smoothing step size for each smoothing iteration. 1716 This limits the the distance over which a point can move in each iteration. 1717 As in all iterative methods, the stability of the process is sensitive to this parameter. 1718 In general, small step size and large numbers of iterations are more stable than a larger 1719 step size and a smaller numbers of iterations. 1720 constraints : (dict) 1721 dictionary of constraints. 1722 Point constraints are used to prevent points from moving, 1723 or to move only on a plane. This can prevent shrinking or growing point clouds. 1724 If enabled, a local topological analysis is performed to determine whether a point 1725 should be marked as fixed" i.e., never moves, or the point only moves on a plane, 1726 or the point can move freely. 1727 If all points in the neighborhood surrounding a point are in the cone defined by 1728 `fixed_angle`, then the point is classified as fixed. 1729 If all points in the neighborhood surrounding a point are in the cone defined by 1730 `boundary_angle`, then the point is classified as lying on a plane. 1731 Angles are expressed in degrees. 1732 1733 Example: 1734 ```py 1735 import numpy as np 1736 from vedo import Points, show 1737 from vedo.pyplot import histogram 1738 1739 vpts1 = Points(np.random.rand(10_000, 3)) 1740 dists = vpts1.auto_distance() 1741 h1 = histogram(dists, xlim=(0,0.08)).clone2d() 1742 1743 vpts2 = vpts1.clone().relax_point_positions(n=100, iters=20, sub_iters=10) 1744 dists = vpts2.auto_distance() 1745 h2 = histogram(dists, xlim=(0,0.08)).clone2d() 1746 1747 show([[vpts1, h1], [vpts2, h2]], N=2).close() 1748 ``` 1749 """ 1750 smooth = vtki.new("PointSmoothingFilter") 1751 smooth.SetInputData(self.dataset) 1752 smooth.SetSmoothingModeToUniform() 1753 smooth.SetNumberOfIterations(iters) 1754 smooth.SetNumberOfSubIterations(sub_iters) 1755 smooth.SetPackingFactor(packing_factor) 1756 if self.point_locator: 1757 smooth.SetLocator(self.point_locator) 1758 if not max_step: 1759 max_step = self.diagonal_size() / 100 1760 smooth.SetMaximumStepSize(max_step) 1761 smooth.SetNeighborhoodSize(n) 1762 if constraints: 1763 fixed_angle = constraints.get("fixed_angle", 45) 1764 boundary_angle = constraints.get("boundary_angle", 110) 1765 smooth.EnableConstraintsOn() 1766 smooth.SetFixedAngle(fixed_angle) 1767 smooth.SetBoundaryAngle(boundary_angle) 1768 smooth.GenerateConstraintScalarsOn() 1769 smooth.GenerateConstraintNormalsOn() 1770 smooth.Update() 1771 self._update(smooth.GetOutput()) 1772 self.metadata["PackingRadius"] = smooth.GetPackingRadius() 1773 self.pipeline = utils.OperationNode("relax_point_positions", parents=[self]) 1774 return self 1775 1776 def smooth_mls_1d(self, f=0.2, radius=None, n=0) -> Self: 1777 """ 1778 Smooth mesh or points with a `Moving Least Squares` variant. 1779 The point data array "Variances" will contain the residue calculated for each point. 1780 1781 Arguments: 1782 f : (float) 1783 smoothing factor - typical range is [0,2]. 1784 radius : (float) 1785 radius search in absolute units. 1786 If set then `f` is ignored. 1787 n : (int) 1788 number of neighbours to be used for the fit. 1789 If set then `f` and `radius` are ignored. 1790 1791 Examples: 1792 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1793 - [skeletonize.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/skeletonize.py) 1794 1795 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1796 """ 1797 coords = self.vertices 1798 ncoords = len(coords) 1799 1800 if n: 1801 Ncp = n 1802 elif radius: 1803 Ncp = 1 1804 else: 1805 Ncp = int(ncoords * f / 10) 1806 if Ncp < 5: 1807 vedo.logger.warning(f"Please choose a fraction higher than {f}") 1808 Ncp = 5 1809 1810 variances, newline = [], [] 1811 for p in coords: 1812 points = self.closest_point(p, n=Ncp, radius=radius) 1813 if len(points) < 4: 1814 continue 1815 1816 points = np.array(points) 1817 pointsmean = points.mean(axis=0) # plane center 1818 _, dd, vv = np.linalg.svd(points - pointsmean) 1819 newp = np.dot(p - pointsmean, vv[0]) * vv[0] + pointsmean 1820 variances.append(dd[1] + dd[2]) 1821 newline.append(newp) 1822 1823 self.pointdata["Variances"] = np.array(variances).astype(np.float32) 1824 self.vertices = newline 1825 self.pipeline = utils.OperationNode("smooth_mls_1d", parents=[self]) 1826 return self 1827 1828 def smooth_mls_2d(self, f=0.2, radius=None, n=0) -> Self: 1829 """ 1830 Smooth mesh or points with a `Moving Least Squares` algorithm variant. 1831 1832 The `mesh.pointdata['MLSVariance']` array will contain the residue calculated for each point. 1833 When a radius is specified, points that are isolated will not be moved and will get 1834 a 0 entry in array `mesh.pointdata['MLSValidPoint']`. 1835 1836 Arguments: 1837 f : (float) 1838 smoothing factor - typical range is [0, 2]. 1839 radius : (float | array) 1840 radius search in absolute units. Can be single value (float) or sequence 1841 for adaptive smoothing. If set then `f` is ignored. 1842 n : (int) 1843 number of neighbours to be used for the fit. 1844 If set then `f` and `radius` are ignored. 1845 1846 Examples: 1847 - [moving_least_squares2D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares2D.py) 1848 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1849 1850 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1851 """ 1852 coords = self.vertices 1853 ncoords = len(coords) 1854 1855 if n: 1856 Ncp = n 1857 radius = None 1858 elif radius is not None: 1859 Ncp = 1 1860 else: 1861 Ncp = int(ncoords * f / 100) 1862 if Ncp < 4: 1863 vedo.logger.error(f"please choose a f-value higher than {f}") 1864 Ncp = 4 1865 1866 variances, newpts, valid = [], [], [] 1867 radius_is_sequence = utils.is_sequence(radius) 1868 1869 pb = None 1870 if ncoords > 10000: 1871 pb = utils.ProgressBar(0, ncoords, delay=3) 1872 1873 for i, p in enumerate(coords): 1874 if pb: 1875 pb.print("smooth_mls_2d working ...") 1876 1877 # if a radius was provided for each point 1878 if radius_is_sequence: 1879 pts = self.closest_point(p, n=Ncp, radius=radius[i]) 1880 else: 1881 pts = self.closest_point(p, n=Ncp, radius=radius) 1882 1883 if len(pts) > 3: 1884 ptsmean = pts.mean(axis=0) # plane center 1885 _, dd, vv = np.linalg.svd(pts - ptsmean) 1886 cv = np.cross(vv[0], vv[1]) 1887 t = (np.dot(cv, ptsmean) - np.dot(cv, p)) / np.dot(cv, cv) 1888 newpts.append(p + cv * t) 1889 variances.append(dd[2]) 1890 if radius is not None: 1891 valid.append(1) 1892 else: 1893 newpts.append(p) 1894 variances.append(0) 1895 if radius is not None: 1896 valid.append(0) 1897 1898 if radius is not None: 1899 self.pointdata["MLSValidPoint"] = np.array(valid).astype(np.uint8) 1900 self.pointdata["MLSVariance"] = np.array(variances).astype(np.float32) 1901 1902 self.vertices = newpts 1903 1904 self.pipeline = utils.OperationNode("smooth_mls_2d", parents=[self]) 1905 return self 1906 1907 def smooth_lloyd_2d(self, iterations=2, bounds=None, options="Qbb Qc Qx") -> Self: 1908 """ 1909 Lloyd relaxation of a 2D pointcloud. 1910 1911 Arguments: 1912 iterations : (int) 1913 number of iterations. 1914 bounds : (list) 1915 bounding box of the domain. 1916 options : (str) 1917 options for the Qhull algorithm. 1918 """ 1919 # Credits: https://hatarilabs.com/ih-en/ 1920 # tutorial-to-create-a-geospatial-voronoi-sh-mesh-with-python-scipy-and-geopandas 1921 from scipy.spatial import Voronoi as scipy_voronoi 1922 1923 def _constrain_points(points): 1924 # Update any points that have drifted beyond the boundaries of this space 1925 if bounds is not None: 1926 for point in points: 1927 if point[0] < bounds[0]: point[0] = bounds[0] 1928 if point[0] > bounds[1]: point[0] = bounds[1] 1929 if point[1] < bounds[2]: point[1] = bounds[2] 1930 if point[1] > bounds[3]: point[1] = bounds[3] 1931 return points 1932 1933 def _find_centroid(vertices): 1934 # The equation for the method used here to find the centroid of a 1935 # 2D polygon is given here: https://en.wikipedia.org/wiki/Centroid#Of_a_polygon 1936 area = 0 1937 centroid_x = 0 1938 centroid_y = 0 1939 for i in range(len(vertices) - 1): 1940 step = (vertices[i, 0] * vertices[i + 1, 1]) - (vertices[i + 1, 0] * vertices[i, 1]) 1941 centroid_x += (vertices[i, 0] + vertices[i + 1, 0]) * step 1942 centroid_y += (vertices[i, 1] + vertices[i + 1, 1]) * step 1943 area += step 1944 if area: 1945 centroid_x = (1.0 / (3.0 * area)) * centroid_x 1946 centroid_y = (1.0 / (3.0 * area)) * centroid_y 1947 # prevent centroids from escaping bounding box 1948 return _constrain_points([[centroid_x, centroid_y]])[0] 1949 1950 def _relax(voron): 1951 # Moves each point to the centroid of its cell in the voronoi 1952 # map to "relax" the points (i.e. jitter the points so as 1953 # to spread them out within the space). 1954 centroids = [] 1955 for idx in voron.point_region: 1956 # the region is a series of indices into voronoi.vertices 1957 # remove point at infinity, designated by index -1 1958 region = [i for i in voron.regions[idx] if i != -1] 1959 # enclose the polygon 1960 region = region + [region[0]] 1961 verts = voron.vertices[region] 1962 # find the centroid of those vertices 1963 centroids.append(_find_centroid(verts)) 1964 return _constrain_points(centroids) 1965 1966 if bounds is None: 1967 bounds = self.bounds() 1968 1969 pts = self.vertices[:, (0, 1)] 1970 for i in range(iterations): 1971 vor = scipy_voronoi(pts, qhull_options=options) 1972 _constrain_points(vor.vertices) 1973 pts = _relax(vor) 1974 out = Points(pts) 1975 out.name = "MeshSmoothLloyd2D" 1976 out.pipeline = utils.OperationNode("smooth_lloyd", parents=[self]) 1977 return out 1978 1979 def project_on_plane(self, plane="z", point=None, direction=None) -> Self: 1980 """ 1981 Project the mesh on one of the Cartesian planes. 1982 1983 Arguments: 1984 plane : (str, Plane) 1985 if plane is `str`, plane can be one of ['x', 'y', 'z'], 1986 represents x-plane, y-plane and z-plane, respectively. 1987 Otherwise, plane should be an instance of `vedo.shapes.Plane`. 1988 point : (float, array) 1989 if plane is `str`, point should be a float represents the intercept. 1990 Otherwise, point is the camera point of perspective projection 1991 direction : (array) 1992 direction of oblique projection 1993 1994 Note: 1995 Parameters `point` and `direction` are only used if the given plane 1996 is an instance of `vedo.shapes.Plane`. And one of these two params 1997 should be left as `None` to specify the projection type. 1998 1999 Example: 2000 ```python 2001 s.project_on_plane(plane='z') # project to z-plane 2002 plane = Plane(pos=(4, 8, -4), normal=(-1, 0, 1), s=(5,5)) 2003 s.project_on_plane(plane=plane) # orthogonal projection 2004 s.project_on_plane(plane=plane, point=(6, 6, 6)) # perspective projection 2005 s.project_on_plane(plane=plane, direction=(1, 2, -1)) # oblique projection 2006 ``` 2007 2008 Examples: 2009 - [silhouette2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette2.py) 2010 2011 ![](https://vedo.embl.es/images/basic/silhouette2.png) 2012 """ 2013 coords = self.vertices 2014 2015 if plane == "x": 2016 coords[:, 0] = self.transform.position[0] 2017 intercept = self.xbounds()[0] if point is None else point 2018 self.x(intercept) 2019 elif plane == "y": 2020 coords[:, 1] = self.transform.position[1] 2021 intercept = self.ybounds()[0] if point is None else point 2022 self.y(intercept) 2023 elif plane == "z": 2024 coords[:, 2] = self.transform.position[2] 2025 intercept = self.zbounds()[0] if point is None else point 2026 self.z(intercept) 2027 2028 elif isinstance(plane, vedo.shapes.Plane): 2029 normal = plane.normal / np.linalg.norm(plane.normal) 2030 pl = np.hstack((normal, -np.dot(plane.pos(), normal))).reshape(4, 1) 2031 if direction is None and point is None: 2032 # orthogonal projection 2033 pt = np.hstack((normal, [0])).reshape(4, 1) 2034 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T # python3 only 2035 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2036 2037 elif direction is None: 2038 # perspective projection 2039 pt = np.hstack((np.array(point), [1])).reshape(4, 1) 2040 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2041 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2042 2043 elif point is None: 2044 # oblique projection 2045 pt = np.hstack((np.array(direction), [0])).reshape(4, 1) 2046 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2047 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2048 2049 coords = np.concatenate([coords, np.ones((coords.shape[:-1] + (1,)))], axis=-1) 2050 # coords = coords @ proj_mat.T 2051 coords = np.matmul(coords, proj_mat.T) 2052 coords = coords[:, :3] / coords[:, 3:] 2053 2054 else: 2055 vedo.logger.error(f"unknown plane {plane}") 2056 raise RuntimeError() 2057 2058 self.alpha(0.1) 2059 self.vertices = coords 2060 return self 2061 2062 def warp(self, source, target, sigma=1.0, mode="3d") -> Self: 2063 """ 2064 "Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set 2065 of source and target landmarks. Any point on the mesh close to a source landmark will 2066 be moved to a place close to the corresponding target landmark. 2067 The points in between are interpolated smoothly using 2068 Bookstein's Thin Plate Spline algorithm. 2069 2070 Transformation object can be accessed with `mesh.transform`. 2071 2072 Arguments: 2073 sigma : (float) 2074 specify the 'stiffness' of the spline. 2075 mode : (str) 2076 set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes) 2077 2078 Examples: 2079 - [interpolate_field.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/interpolate_field.py) 2080 - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py) 2081 - [warp2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp2.py) 2082 - [warp3.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp3.py) 2083 - [warp4a.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4a.py) 2084 - [warp4b.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4b.py) 2085 - [warp6.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp6.py) 2086 2087 ![](https://vedo.embl.es/images/advanced/warp2.png) 2088 """ 2089 parents = [self] 2090 2091 try: 2092 source = source.vertices 2093 parents.append(source) 2094 except AttributeError: 2095 source = utils.make3d(source) 2096 2097 try: 2098 target = target.vertices 2099 parents.append(target) 2100 except AttributeError: 2101 target = utils.make3d(target) 2102 2103 ns = len(source) 2104 nt = len(target) 2105 if ns != nt: 2106 vedo.logger.error(f"#source {ns} != {nt} #target points") 2107 raise RuntimeError() 2108 2109 NLT = NonLinearTransform() 2110 NLT.source_points = source 2111 NLT.target_points = target 2112 self.apply_transform(NLT) 2113 2114 self.pipeline = utils.OperationNode("warp", parents=parents) 2115 return self 2116 2117 def cut_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0), invert=False) -> Self: 2118 """ 2119 Cut the mesh with the plane defined by a point and a normal. 2120 2121 Arguments: 2122 origin : (array) 2123 the cutting plane goes through this point 2124 normal : (array) 2125 normal of the cutting plane 2126 2127 Example: 2128 ```python 2129 from vedo import Cube 2130 cube = Cube().cut_with_plane(normal=(1,1,1)) 2131 cube.back_color('pink').show().close() 2132 ``` 2133 ![](https://vedo.embl.es/images/feats/cut_with_plane_cube.png) 2134 2135 Examples: 2136 - [trail.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/trail.py) 2137 2138 ![](https://vedo.embl.es/images/simulations/trail.gif) 2139 2140 Check out also: 2141 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`. 2142 """ 2143 s = str(normal) 2144 if "x" in s: 2145 normal = (1, 0, 0) 2146 if "-" in s: 2147 normal = -np.array(normal) 2148 elif "y" in s: 2149 normal = (0, 1, 0) 2150 if "-" in s: 2151 normal = -np.array(normal) 2152 elif "z" in s: 2153 normal = (0, 0, 1) 2154 if "-" in s: 2155 normal = -np.array(normal) 2156 plane = vtki.vtkPlane() 2157 plane.SetOrigin(origin) 2158 plane.SetNormal(normal) 2159 2160 clipper = vtki.new("ClipPolyData") 2161 clipper.SetInputData(self.dataset) 2162 clipper.SetClipFunction(plane) 2163 clipper.GenerateClippedOutputOff() 2164 clipper.GenerateClipScalarsOff() 2165 clipper.SetInsideOut(invert) 2166 clipper.SetValue(0) 2167 clipper.Update() 2168 2169 self._update(clipper.GetOutput()) 2170 2171 self.pipeline = utils.OperationNode("cut_with_plane", parents=[self]) 2172 return self 2173 2174 def cut_with_planes(self, origins, normals, invert=False) -> Self: 2175 """ 2176 Cut the mesh with a convex set of planes defined by points and normals. 2177 2178 Arguments: 2179 origins : (array) 2180 each cutting plane goes through this point 2181 normals : (array) 2182 normal of each of the cutting planes 2183 invert : (bool) 2184 if True, cut outside instead of inside 2185 2186 Check out also: 2187 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()` 2188 """ 2189 2190 vpoints = vtki.vtkPoints() 2191 for p in utils.make3d(origins): 2192 vpoints.InsertNextPoint(p) 2193 normals = utils.make3d(normals) 2194 2195 planes = vtki.vtkPlanes() 2196 planes.SetPoints(vpoints) 2197 planes.SetNormals(utils.numpy2vtk(normals, dtype=float)) 2198 2199 clipper = vtki.new("ClipPolyData") 2200 clipper.SetInputData(self.dataset) 2201 clipper.SetInsideOut(invert) 2202 clipper.SetClipFunction(planes) 2203 clipper.GenerateClippedOutputOff() 2204 clipper.GenerateClipScalarsOff() 2205 clipper.SetValue(0) 2206 clipper.Update() 2207 2208 self._update(clipper.GetOutput()) 2209 2210 self.pipeline = utils.OperationNode("cut_with_planes", parents=[self]) 2211 return self 2212 2213 def cut_with_box(self, bounds, invert=False) -> Self: 2214 """ 2215 Cut the current mesh with a box or a set of boxes. 2216 This is much faster than `cut_with_mesh()`. 2217 2218 Input `bounds` can be either: 2219 - a Mesh or Points object 2220 - a list of 6 number representing a bounding box `[xmin,xmax, ymin,ymax, zmin,zmax]` 2221 - a list of bounding boxes like the above: `[[xmin1,...], [xmin2,...], ...]` 2222 2223 Example: 2224 ```python 2225 from vedo import Sphere, Cube, show 2226 mesh = Sphere(r=1, res=50) 2227 box = Cube(side=1.5).wireframe() 2228 mesh.cut_with_box(box) 2229 show(mesh, box, axes=1).close() 2230 ``` 2231 ![](https://vedo.embl.es/images/feats/cut_with_box_cube.png) 2232 2233 Check out also: 2234 `cut_with_line()`, `cut_with_plane()`, `cut_with_cylinder()` 2235 """ 2236 if isinstance(bounds, Points): 2237 bounds = bounds.bounds() 2238 2239 box = vtki.new("Box") 2240 if utils.is_sequence(bounds[0]): 2241 for bs in bounds: 2242 box.AddBounds(bs) 2243 else: 2244 box.SetBounds(bounds) 2245 2246 clipper = vtki.new("ClipPolyData") 2247 clipper.SetInputData(self.dataset) 2248 clipper.SetClipFunction(box) 2249 clipper.SetInsideOut(not invert) 2250 clipper.GenerateClippedOutputOff() 2251 clipper.GenerateClipScalarsOff() 2252 clipper.SetValue(0) 2253 clipper.Update() 2254 self._update(clipper.GetOutput()) 2255 2256 self.pipeline = utils.OperationNode("cut_with_box", parents=[self]) 2257 return self 2258 2259 def cut_with_line(self, points, invert=False, closed=True) -> Self: 2260 """ 2261 Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter. 2262 The polyline is defined by a set of points (z-coordinates are ignored). 2263 This is much faster than `cut_with_mesh()`. 2264 2265 Check out also: 2266 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2267 """ 2268 pplane = vtki.new("PolyPlane") 2269 if isinstance(points, Points): 2270 points = points.vertices.tolist() 2271 2272 if closed: 2273 if isinstance(points, np.ndarray): 2274 points = points.tolist() 2275 points.append(points[0]) 2276 2277 vpoints = vtki.vtkPoints() 2278 for p in points: 2279 if len(p) == 2: 2280 p = [p[0], p[1], 0.0] 2281 vpoints.InsertNextPoint(p) 2282 2283 n = len(points) 2284 polyline = vtki.new("PolyLine") 2285 polyline.Initialize(n, vpoints) 2286 polyline.GetPointIds().SetNumberOfIds(n) 2287 for i in range(n): 2288 polyline.GetPointIds().SetId(i, i) 2289 pplane.SetPolyLine(polyline) 2290 2291 clipper = vtki.new("ClipPolyData") 2292 clipper.SetInputData(self.dataset) 2293 clipper.SetClipFunction(pplane) 2294 clipper.SetInsideOut(invert) 2295 clipper.GenerateClippedOutputOff() 2296 clipper.GenerateClipScalarsOff() 2297 clipper.SetValue(0) 2298 clipper.Update() 2299 self._update(clipper.GetOutput()) 2300 2301 self.pipeline = utils.OperationNode("cut_with_line", parents=[self]) 2302 return self 2303 2304 def cut_with_cookiecutter(self, lines) -> Self: 2305 """ 2306 Cut the current mesh with a single line or a set of lines. 2307 2308 Input `lines` can be either: 2309 - a `Mesh` or `Points` object 2310 - a list of 3D points: `[(x1,y1,z1), (x2,y2,z2), ...]` 2311 - a list of 2D points: `[(x1,y1), (x2,y2), ...]` 2312 2313 Example: 2314 ```python 2315 from vedo import * 2316 grid = Mesh(dataurl + "dolfin_fine.vtk") 2317 grid.compute_quality().cmap("Greens") 2318 pols = merge( 2319 Polygon(nsides=10, r=0.3).pos(0.7, 0.3), 2320 Polygon(nsides=10, r=0.2).pos(0.3, 0.7), 2321 ) 2322 lines = pols.boundaries() 2323 cgrid = grid.clone().cut_with_cookiecutter(lines) 2324 grid.alpha(0.1).wireframe() 2325 show(grid, cgrid, lines, axes=8, bg='blackboard').close() 2326 ``` 2327 ![](https://vedo.embl.es/images/feats/cookiecutter.png) 2328 2329 Check out also: 2330 `cut_with_line()` and `cut_with_point_loop()` 2331 2332 Note: 2333 In case of a warning message like: 2334 "Mesh and trim loop point data attributes are different" 2335 consider interpolating the mesh point data to the loop points, 2336 Eg. (in the above example): 2337 ```python 2338 lines = pols.boundaries().interpolate_data_from(grid, n=2) 2339 ``` 2340 2341 Note: 2342 trying to invert the selection by reversing the loop order 2343 will have no effect in this method, hence it does not have 2344 the `invert` option. 2345 """ 2346 if utils.is_sequence(lines): 2347 lines = utils.make3d(lines) 2348 iline = list(range(len(lines))) + [0] 2349 poly = utils.buildPolyData(lines, lines=[iline]) 2350 else: 2351 poly = lines.dataset 2352 2353 # if invert: # not working 2354 # rev = vtki.new("ReverseSense") 2355 # rev.ReverseCellsOn() 2356 # rev.SetInputData(poly) 2357 # rev.Update() 2358 # poly = rev.GetOutput() 2359 2360 # Build loops from the polyline 2361 build_loops = vtki.new("ContourLoopExtraction") 2362 build_loops.SetGlobalWarningDisplay(0) 2363 build_loops.SetInputData(poly) 2364 build_loops.Update() 2365 boundary_poly = build_loops.GetOutput() 2366 2367 ccut = vtki.new("CookieCutter") 2368 ccut.SetInputData(self.dataset) 2369 ccut.SetLoopsData(boundary_poly) 2370 ccut.SetPointInterpolationToMeshEdges() 2371 # ccut.SetPointInterpolationToLoopEdges() 2372 ccut.PassCellDataOn() 2373 ccut.PassPointDataOn() 2374 ccut.Update() 2375 self._update(ccut.GetOutput()) 2376 2377 self.pipeline = utils.OperationNode("cut_with_cookiecutter", parents=[self]) 2378 return self 2379 2380 def cut_with_cylinder(self, center=(0, 0, 0), axis=(0, 0, 1), r=1, invert=False) -> Self: 2381 """ 2382 Cut the current mesh with an infinite cylinder. 2383 This is much faster than `cut_with_mesh()`. 2384 2385 Arguments: 2386 center : (array) 2387 the center of the cylinder 2388 normal : (array) 2389 direction of the cylinder axis 2390 r : (float) 2391 radius of the cylinder 2392 2393 Example: 2394 ```python 2395 from vedo import Disc, show 2396 disc = Disc(r1=1, r2=1.2) 2397 mesh = disc.extrude(3, res=50).linewidth(1) 2398 mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True) 2399 show(mesh, axes=1).close() 2400 ``` 2401 ![](https://vedo.embl.es/images/feats/cut_with_cylinder.png) 2402 2403 Examples: 2404 - [optics_main1.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/optics_main1.py) 2405 2406 Check out also: 2407 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2408 """ 2409 s = str(axis) 2410 if "x" in s: 2411 axis = (1, 0, 0) 2412 elif "y" in s: 2413 axis = (0, 1, 0) 2414 elif "z" in s: 2415 axis = (0, 0, 1) 2416 cyl = vtki.new("Cylinder") 2417 cyl.SetCenter(center) 2418 cyl.SetAxis(axis[0], axis[1], axis[2]) 2419 cyl.SetRadius(r) 2420 2421 clipper = vtki.new("ClipPolyData") 2422 clipper.SetInputData(self.dataset) 2423 clipper.SetClipFunction(cyl) 2424 clipper.SetInsideOut(not invert) 2425 clipper.GenerateClippedOutputOff() 2426 clipper.GenerateClipScalarsOff() 2427 clipper.SetValue(0) 2428 clipper.Update() 2429 self._update(clipper.GetOutput()) 2430 2431 self.pipeline = utils.OperationNode("cut_with_cylinder", parents=[self]) 2432 return self 2433 2434 def cut_with_sphere(self, center=(0, 0, 0), r=1.0, invert=False) -> Self: 2435 """ 2436 Cut the current mesh with an sphere. 2437 This is much faster than `cut_with_mesh()`. 2438 2439 Arguments: 2440 center : (array) 2441 the center of the sphere 2442 r : (float) 2443 radius of the sphere 2444 2445 Example: 2446 ```python 2447 from vedo import Disc, show 2448 disc = Disc(r1=1, r2=1.2) 2449 mesh = disc.extrude(3, res=50).linewidth(1) 2450 mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True) 2451 show(mesh, axes=1).close() 2452 ``` 2453 ![](https://vedo.embl.es/images/feats/cut_with_sphere.png) 2454 2455 Check out also: 2456 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2457 """ 2458 sph = vtki.new("Sphere") 2459 sph.SetCenter(center) 2460 sph.SetRadius(r) 2461 2462 clipper = vtki.new("ClipPolyData") 2463 clipper.SetInputData(self.dataset) 2464 clipper.SetClipFunction(sph) 2465 clipper.SetInsideOut(not invert) 2466 clipper.GenerateClippedOutputOff() 2467 clipper.GenerateClipScalarsOff() 2468 clipper.SetValue(0) 2469 clipper.Update() 2470 self._update(clipper.GetOutput()) 2471 self.pipeline = utils.OperationNode("cut_with_sphere", parents=[self]) 2472 return self 2473 2474 def cut_with_mesh(self, mesh, invert=False, keep=False) -> Union[Self, "vedo.Assembly"]: 2475 """ 2476 Cut an `Mesh` mesh with another `Mesh`. 2477 2478 Use `invert` to invert the selection. 2479 2480 Use `keep` to keep the cutoff part, in this case an `Assembly` is returned: 2481 the "cut" object and the "discarded" part of the original object. 2482 You can access both via `assembly.unpack()` method. 2483 2484 Example: 2485 ```python 2486 from vedo import * 2487 arr = np.random.randn(100000, 3)/2 2488 pts = Points(arr).c('red3').pos(5,0,0) 2489 cube = Cube().pos(4,0.5,0) 2490 assem = pts.cut_with_mesh(cube, keep=True) 2491 show(assem.unpack(), axes=1).close() 2492 ``` 2493 ![](https://vedo.embl.es/images/feats/cut_with_mesh.png) 2494 2495 Check out also: 2496 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2497 """ 2498 polymesh = mesh.dataset 2499 poly = self.dataset 2500 2501 # Create an array to hold distance information 2502 signed_distances = vtki.vtkFloatArray() 2503 signed_distances.SetNumberOfComponents(1) 2504 signed_distances.SetName("SignedDistances") 2505 2506 # implicit function that will be used to slice the mesh 2507 ippd = vtki.new("ImplicitPolyDataDistance") 2508 ippd.SetInput(polymesh) 2509 2510 # Evaluate the signed distance function at all of the grid points 2511 for pointId in range(poly.GetNumberOfPoints()): 2512 p = poly.GetPoint(pointId) 2513 signed_distance = ippd.EvaluateFunction(p) 2514 signed_distances.InsertNextValue(signed_distance) 2515 2516 currentscals = poly.GetPointData().GetScalars() 2517 if currentscals: 2518 currentscals = currentscals.GetName() 2519 2520 poly.GetPointData().AddArray(signed_distances) 2521 poly.GetPointData().SetActiveScalars("SignedDistances") 2522 2523 clipper = vtki.new("ClipPolyData") 2524 clipper.SetInputData(poly) 2525 clipper.SetInsideOut(not invert) 2526 clipper.SetGenerateClippedOutput(keep) 2527 clipper.SetValue(0.0) 2528 clipper.Update() 2529 cpoly = clipper.GetOutput() 2530 2531 if keep: 2532 kpoly = clipper.GetOutput(1) 2533 2534 vis = False 2535 if currentscals: 2536 cpoly.GetPointData().SetActiveScalars(currentscals) 2537 vis = self.mapper.GetScalarVisibility() 2538 2539 self._update(cpoly) 2540 2541 self.pointdata.remove("SignedDistances") 2542 self.mapper.SetScalarVisibility(vis) 2543 if keep: 2544 if isinstance(self, vedo.Mesh): 2545 cutoff = vedo.Mesh(kpoly) 2546 else: 2547 cutoff = vedo.Points(kpoly) 2548 # cutoff = self.__class__(kpoly) # this does not work properly 2549 cutoff.properties = vtki.vtkProperty() 2550 cutoff.properties.DeepCopy(self.properties) 2551 cutoff.actor.SetProperty(cutoff.properties) 2552 cutoff.c("k5").alpha(0.2) 2553 return vedo.Assembly([self, cutoff]) 2554 2555 self.pipeline = utils.OperationNode("cut_with_mesh", parents=[self, mesh]) 2556 return self 2557 2558 def cut_with_point_loop( 2559 self, points, invert=False, on="points", include_boundary=False 2560 ) -> Self: 2561 """ 2562 Cut an `Mesh` object with a set of points forming a closed loop. 2563 2564 Arguments: 2565 invert : (bool) 2566 invert selection (inside-out) 2567 on : (str) 2568 if 'cells' will extract the whole cells lying inside (or outside) the point loop 2569 include_boundary : (bool) 2570 include cells lying exactly on the boundary line. Only relevant on 'cells' mode 2571 2572 Examples: 2573 - [cut_with_points1.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points1.py) 2574 2575 ![](https://vedo.embl.es/images/advanced/cutWithPoints1.png) 2576 2577 - [cut_with_points2.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points2.py) 2578 2579 ![](https://vedo.embl.es/images/advanced/cutWithPoints2.png) 2580 """ 2581 if isinstance(points, Points): 2582 parents = [points] 2583 vpts = points.dataset.GetPoints() 2584 points = points.vertices 2585 else: 2586 parents = [self] 2587 vpts = vtki.vtkPoints() 2588 points = utils.make3d(points) 2589 for p in points: 2590 vpts.InsertNextPoint(p) 2591 2592 if "cell" in on: 2593 ippd = vtki.new("ImplicitSelectionLoop") 2594 ippd.SetLoop(vpts) 2595 ippd.AutomaticNormalGenerationOn() 2596 clipper = vtki.new("ExtractPolyDataGeometry") 2597 clipper.SetInputData(self.dataset) 2598 clipper.SetImplicitFunction(ippd) 2599 clipper.SetExtractInside(not invert) 2600 clipper.SetExtractBoundaryCells(include_boundary) 2601 else: 2602 spol = vtki.new("SelectPolyData") 2603 spol.SetLoop(vpts) 2604 spol.GenerateSelectionScalarsOn() 2605 spol.GenerateUnselectedOutputOff() 2606 spol.SetInputData(self.dataset) 2607 spol.Update() 2608 clipper = vtki.new("ClipPolyData") 2609 clipper.SetInputData(spol.GetOutput()) 2610 clipper.SetInsideOut(not invert) 2611 clipper.SetValue(0.0) 2612 clipper.Update() 2613 self._update(clipper.GetOutput()) 2614 2615 self.pipeline = utils.OperationNode("cut_with_pointloop", parents=parents) 2616 return self 2617 2618 def cut_with_scalar(self, value: float, name="", invert=False) -> Self: 2619 """ 2620 Cut a mesh or point cloud with some input scalar point-data. 2621 2622 Arguments: 2623 value : (float) 2624 cutting value 2625 name : (str) 2626 array name of the scalars to be used 2627 invert : (bool) 2628 flip selection 2629 2630 Example: 2631 ```python 2632 from vedo import * 2633 s = Sphere().lw(1) 2634 pts = s.vertices 2635 scalars = np.sin(3*pts[:,2]) + pts[:,0] 2636 s.pointdata["somevalues"] = scalars 2637 s.cut_with_scalar(0.3) 2638 s.cmap("Spectral", "somevalues").add_scalarbar() 2639 s.show(axes=1).close() 2640 ``` 2641 ![](https://vedo.embl.es/images/feats/cut_with_scalars.png) 2642 """ 2643 if name: 2644 self.pointdata.select(name) 2645 clipper = vtki.new("ClipPolyData") 2646 clipper.SetInputData(self.dataset) 2647 clipper.SetValue(value) 2648 clipper.GenerateClippedOutputOff() 2649 clipper.SetInsideOut(not invert) 2650 clipper.Update() 2651 self._update(clipper.GetOutput()) 2652 self.pipeline = utils.OperationNode("cut_with_scalar", parents=[self]) 2653 return self 2654 2655 def crop(self, 2656 top=None, bottom=None, right=None, left=None, front=None, back=None, 2657 bounds=()) -> Self: 2658 """ 2659 Crop an `Mesh` object. 2660 2661 Arguments: 2662 top : (float) 2663 fraction to crop from the top plane (positive z) 2664 bottom : (float) 2665 fraction to crop from the bottom plane (negative z) 2666 front : (float) 2667 fraction to crop from the front plane (positive y) 2668 back : (float) 2669 fraction to crop from the back plane (negative y) 2670 right : (float) 2671 fraction to crop from the right plane (positive x) 2672 left : (float) 2673 fraction to crop from the left plane (negative x) 2674 bounds : (list) 2675 bounding box of the crop region as `[x0,x1, y0,y1, z0,z1]` 2676 2677 Example: 2678 ```python 2679 from vedo import Sphere 2680 Sphere().crop(right=0.3, left=0.1).show() 2681 ``` 2682 ![](https://user-images.githubusercontent.com/32848391/57081955-0ef1e800-6cf6-11e9-99de-b45220939bc9.png) 2683 """ 2684 if not len(bounds): 2685 pos = np.array(self.pos()) 2686 x0, x1, y0, y1, z0, z1 = self.bounds() 2687 x0, y0, z0 = [x0, y0, z0] - pos 2688 x1, y1, z1 = [x1, y1, z1] - pos 2689 2690 dx, dy, dz = x1 - x0, y1 - y0, z1 - z0 2691 if top: 2692 z1 = z1 - top * dz 2693 if bottom: 2694 z0 = z0 + bottom * dz 2695 if front: 2696 y1 = y1 - front * dy 2697 if back: 2698 y0 = y0 + back * dy 2699 if right: 2700 x1 = x1 - right * dx 2701 if left: 2702 x0 = x0 + left * dx 2703 bounds = (x0, x1, y0, y1, z0, z1) 2704 2705 cu = vtki.new("Box") 2706 cu.SetBounds(bounds) 2707 2708 clipper = vtki.new("ClipPolyData") 2709 clipper.SetInputData(self.dataset) 2710 clipper.SetClipFunction(cu) 2711 clipper.InsideOutOn() 2712 clipper.GenerateClippedOutputOff() 2713 clipper.GenerateClipScalarsOff() 2714 clipper.SetValue(0) 2715 clipper.Update() 2716 self._update(clipper.GetOutput()) 2717 2718 self.pipeline = utils.OperationNode( 2719 "crop", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 2720 ) 2721 return self 2722 2723 def generate_surface_halo( 2724 self, 2725 distance=0.05, 2726 res=(50, 50, 50), 2727 bounds=(), 2728 maxdist=None, 2729 ) -> "vedo.Mesh": 2730 """ 2731 Generate the surface halo which sits at the specified distance from the input one. 2732 2733 Arguments: 2734 distance : (float) 2735 distance from the input surface 2736 res : (int) 2737 resolution of the surface 2738 bounds : (list) 2739 bounding box of the surface 2740 maxdist : (float) 2741 maximum distance to generate the surface 2742 """ 2743 if not bounds: 2744 bounds = self.bounds() 2745 2746 if not maxdist: 2747 maxdist = self.diagonal_size() / 2 2748 2749 imp = vtki.new("ImplicitModeller") 2750 imp.SetInputData(self.dataset) 2751 imp.SetSampleDimensions(res) 2752 if maxdist: 2753 imp.SetMaximumDistance(maxdist) 2754 if len(bounds) == 6: 2755 imp.SetModelBounds(bounds) 2756 contour = vtki.new("ContourFilter") 2757 contour.SetInputConnection(imp.GetOutputPort()) 2758 contour.SetValue(0, distance) 2759 contour.Update() 2760 out = vedo.Mesh(contour.GetOutput()) 2761 out.c("lightblue").alpha(0.25).lighting("off") 2762 out.pipeline = utils.OperationNode("generate_surface_halo", parents=[self]) 2763 return out 2764 2765 def generate_mesh( 2766 self, 2767 line_resolution=None, 2768 mesh_resolution=None, 2769 smooth=0.0, 2770 jitter=0.001, 2771 grid=None, 2772 quads=False, 2773 invert=False, 2774 ) -> Self: 2775 """ 2776 Generate a polygonal Mesh from a closed contour line. 2777 If line is not closed it will be closed with a straight segment. 2778 2779 Check also `generate_delaunay2d()`. 2780 2781 Arguments: 2782 line_resolution : (int) 2783 resolution of the contour line. The default is None, in this case 2784 the contour is not resampled. 2785 mesh_resolution : (int) 2786 resolution of the internal triangles not touching the boundary. 2787 smooth : (float) 2788 smoothing of the contour before meshing. 2789 jitter : (float) 2790 add a small noise to the internal points. 2791 grid : (Grid) 2792 manually pass a Grid object. The default is True. 2793 quads : (bool) 2794 generate a mesh of quads instead of triangles. 2795 invert : (bool) 2796 flip the line orientation. The default is False. 2797 2798 Examples: 2799 - [line2mesh_tri.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_tri.py) 2800 2801 ![](https://vedo.embl.es/images/advanced/line2mesh_tri.jpg) 2802 2803 - [line2mesh_quads.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_quads.py) 2804 2805 ![](https://vedo.embl.es/images/advanced/line2mesh_quads.png) 2806 """ 2807 if line_resolution is None: 2808 contour = vedo.shapes.Line(self.vertices) 2809 else: 2810 contour = vedo.shapes.Spline(self.vertices, smooth=smooth, res=line_resolution) 2811 contour.clean() 2812 2813 length = contour.length() 2814 density = length / contour.npoints 2815 # print(f"tomesh():\n\tline length = {length}") 2816 # print(f"\tdensity = {density} length/pt_separation") 2817 2818 x0, x1 = contour.xbounds() 2819 y0, y1 = contour.ybounds() 2820 2821 if grid is None: 2822 if mesh_resolution is None: 2823 resx = int((x1 - x0) / density + 0.5) 2824 resy = int((y1 - y0) / density + 0.5) 2825 # print(f"tmesh_resolution = {[resx, resy]}") 2826 else: 2827 if utils.is_sequence(mesh_resolution): 2828 resx, resy = mesh_resolution 2829 else: 2830 resx, resy = mesh_resolution, mesh_resolution 2831 grid = vedo.shapes.Grid( 2832 [(x0 + x1) / 2, (y0 + y1) / 2, 0], 2833 s=((x1 - x0) * 1.025, (y1 - y0) * 1.025), 2834 res=(resx, resy), 2835 ) 2836 else: 2837 grid = grid.clone() 2838 2839 cpts = contour.vertices 2840 2841 # make sure it's closed 2842 p0, p1 = cpts[0], cpts[-1] 2843 nj = max(2, int(utils.mag(p1 - p0) / density + 0.5)) 2844 joinline = vedo.shapes.Line(p1, p0, res=nj) 2845 contour = vedo.merge(contour, joinline).subsample(0.0001) 2846 2847 ####################################### quads 2848 if quads: 2849 cmesh = grid.clone().cut_with_point_loop(contour, on="cells", invert=invert) 2850 cmesh.wireframe(False).lw(0.5) 2851 cmesh.pipeline = utils.OperationNode( 2852 "generate_mesh", 2853 parents=[self, contour], 2854 comment=f"#quads {cmesh.dataset.GetNumberOfCells()}", 2855 ) 2856 return cmesh 2857 ############################################# 2858 2859 grid_tmp = grid.vertices.copy() 2860 2861 if jitter: 2862 np.random.seed(0) 2863 sigma = 1.0 / np.sqrt(grid.npoints) * grid.diagonal_size() * jitter 2864 # print(f"\tsigma jittering = {sigma}") 2865 grid_tmp += np.random.rand(grid.npoints, 3) * sigma 2866 grid_tmp[:, 2] = 0.0 2867 2868 todel = [] 2869 density /= np.sqrt(3) 2870 vgrid_tmp = Points(grid_tmp) 2871 2872 for p in contour.vertices: 2873 out = vgrid_tmp.closest_point(p, radius=density, return_point_id=True) 2874 todel += out.tolist() 2875 2876 grid_tmp = grid_tmp.tolist() 2877 for index in sorted(list(set(todel)), reverse=True): 2878 del grid_tmp[index] 2879 2880 points = contour.vertices.tolist() + grid_tmp 2881 if invert: 2882 boundary = list(reversed(range(contour.npoints))) 2883 else: 2884 boundary = list(range(contour.npoints)) 2885 2886 dln = Points(points).generate_delaunay2d(mode="xy", boundaries=[boundary]) 2887 dln.compute_normals(points=False) # fixes reversd faces 2888 dln.lw(1) 2889 2890 dln.pipeline = utils.OperationNode( 2891 "generate_mesh", 2892 parents=[self, contour], 2893 comment=f"#cells {dln.dataset.GetNumberOfCells()}", 2894 ) 2895 return dln 2896 2897 def reconstruct_surface( 2898 self, 2899 dims=(100, 100, 100), 2900 radius=None, 2901 sample_size=None, 2902 hole_filling=True, 2903 bounds=(), 2904 padding=0.05, 2905 ) -> "vedo.Mesh": 2906 """ 2907 Surface reconstruction from a scattered cloud of points. 2908 2909 Arguments: 2910 dims : (int) 2911 number of voxels in x, y and z to control precision. 2912 radius : (float) 2913 radius of influence of each point. 2914 Smaller values generally improve performance markedly. 2915 Note that after the signed distance function is computed, 2916 any voxel taking on the value >= radius 2917 is presumed to be "unseen" or uninitialized. 2918 sample_size : (int) 2919 if normals are not present 2920 they will be calculated using this sample size per point. 2921 hole_filling : (bool) 2922 enables hole filling, this generates 2923 separating surfaces between the empty and unseen portions of the volume. 2924 bounds : (list) 2925 region in space in which to perform the sampling 2926 in format (xmin,xmax, ymin,ymax, zim, zmax) 2927 padding : (float) 2928 increase by this fraction the bounding box 2929 2930 Examples: 2931 - [recosurface.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/recosurface.py) 2932 2933 ![](https://vedo.embl.es/images/advanced/recosurface.png) 2934 """ 2935 if not utils.is_sequence(dims): 2936 dims = (dims, dims, dims) 2937 2938 sdf = vtki.new("SignedDistance") 2939 2940 if len(bounds) == 6: 2941 sdf.SetBounds(bounds) 2942 else: 2943 x0, x1, y0, y1, z0, z1 = self.bounds() 2944 sdf.SetBounds( 2945 x0 - (x1 - x0) * padding, 2946 x1 + (x1 - x0) * padding, 2947 y0 - (y1 - y0) * padding, 2948 y1 + (y1 - y0) * padding, 2949 z0 - (z1 - z0) * padding, 2950 z1 + (z1 - z0) * padding, 2951 ) 2952 2953 bb = sdf.GetBounds() 2954 if bb[0]==bb[1]: 2955 vedo.logger.warning("reconstruct_surface(): zero x-range") 2956 if bb[2]==bb[3]: 2957 vedo.logger.warning("reconstruct_surface(): zero y-range") 2958 if bb[4]==bb[5]: 2959 vedo.logger.warning("reconstruct_surface(): zero z-range") 2960 2961 pd = self.dataset 2962 2963 if pd.GetPointData().GetNormals(): 2964 sdf.SetInputData(pd) 2965 else: 2966 normals = vtki.new("PCANormalEstimation") 2967 normals.SetInputData(pd) 2968 if not sample_size: 2969 sample_size = int(pd.GetNumberOfPoints() / 50) 2970 normals.SetSampleSize(sample_size) 2971 normals.SetNormalOrientationToGraphTraversal() 2972 sdf.SetInputConnection(normals.GetOutputPort()) 2973 # print("Recalculating normals with sample size =", sample_size) 2974 2975 if radius is None: 2976 radius = self.diagonal_size() / (sum(dims) / 3) * 5 2977 # print("Calculating mesh from points with radius =", radius) 2978 2979 sdf.SetRadius(radius) 2980 sdf.SetDimensions(dims) 2981 sdf.Update() 2982 2983 surface = vtki.new("ExtractSurface") 2984 surface.SetRadius(radius * 0.99) 2985 surface.SetHoleFilling(hole_filling) 2986 surface.ComputeNormalsOff() 2987 surface.ComputeGradientsOff() 2988 surface.SetInputConnection(sdf.GetOutputPort()) 2989 surface.Update() 2990 m = vedo.mesh.Mesh(surface.GetOutput(), c=self.color()) 2991 2992 m.pipeline = utils.OperationNode( 2993 "reconstruct_surface", 2994 parents=[self], 2995 comment=f"#pts {m.dataset.GetNumberOfPoints()}", 2996 ) 2997 return m 2998 2999 def compute_clustering(self, radius: float) -> Self: 3000 """ 3001 Cluster points in space. The `radius` is the radius of local search. 3002 3003 An array named "ClusterId" is added to `pointdata`. 3004 3005 Examples: 3006 - [clustering.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/clustering.py) 3007 3008 ![](https://vedo.embl.es/images/basic/clustering.png) 3009 """ 3010 cluster = vtki.new("EuclideanClusterExtraction") 3011 cluster.SetInputData(self.dataset) 3012 cluster.SetExtractionModeToAllClusters() 3013 cluster.SetRadius(radius) 3014 cluster.ColorClustersOn() 3015 cluster.Update() 3016 idsarr = cluster.GetOutput().GetPointData().GetArray("ClusterId") 3017 self.dataset.GetPointData().AddArray(idsarr) 3018 self.pipeline = utils.OperationNode( 3019 "compute_clustering", parents=[self], comment=f"radius = {radius}" 3020 ) 3021 return self 3022 3023 def compute_connections(self, radius, mode=0, regions=(), vrange=(0, 1), seeds=(), angle=0.0) -> Self: 3024 """ 3025 Extracts and/or segments points from a point cloud based on geometric distance measures 3026 (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range. 3027 The default operation is to segment the points into "connected" regions where the connection 3028 is determined by an appropriate distance measure. Each region is given a region id. 3029 3030 Optionally, the filter can output the largest connected region of points; a particular region 3031 (via id specification); those regions that are seeded using a list of input point ids; 3032 or the region of points closest to a specified position. 3033 3034 The key parameter of this filter is the radius defining a sphere around each point which defines 3035 a local neighborhood: any other points in the local neighborhood are assumed connected to the point. 3036 Note that the radius is defined in absolute terms. 3037 3038 Other parameters are used to further qualify what it means to be a neighboring point. 3039 For example, scalar range and/or point normals can be used to further constrain the neighborhood. 3040 Also the extraction mode defines how the filter operates. 3041 By default, all regions are extracted but it is possible to extract particular regions; 3042 the region closest to a seed point; seeded regions; or the largest region found while processing. 3043 By default, all regions are extracted. 3044 3045 On output, all points are labeled with a region number. 3046 However note that the number of input and output points may not be the same: 3047 if not extracting all regions then the output size may be less than the input size. 3048 3049 Arguments: 3050 radius : (float) 3051 variable specifying a local sphere used to define local point neighborhood 3052 mode : (int) 3053 - 0, Extract all regions 3054 - 1, Extract point seeded regions 3055 - 2, Extract largest region 3056 - 3, Test specified regions 3057 - 4, Extract all regions with scalar connectivity 3058 - 5, Extract point seeded regions 3059 regions : (list) 3060 a list of non-negative regions id to extract 3061 vrange : (list) 3062 scalar range to use to extract points based on scalar connectivity 3063 seeds : (list) 3064 a list of non-negative point seed ids 3065 angle : (list) 3066 points are connected if the angle between their normals is 3067 within this angle threshold (expressed in degrees). 3068 """ 3069 # https://vtk.org/doc/nightly/html/classvtkConnectedPointsFilter.html 3070 cpf = vtki.new("ConnectedPointsFilter") 3071 cpf.SetInputData(self.dataset) 3072 cpf.SetRadius(radius) 3073 if mode == 0: # Extract all regions 3074 pass 3075 3076 elif mode == 1: # Extract point seeded regions 3077 cpf.SetExtractionModeToPointSeededRegions() 3078 for s in seeds: 3079 cpf.AddSeed(s) 3080 3081 elif mode == 2: # Test largest region 3082 cpf.SetExtractionModeToLargestRegion() 3083 3084 elif mode == 3: # Test specified regions 3085 cpf.SetExtractionModeToSpecifiedRegions() 3086 for r in regions: 3087 cpf.AddSpecifiedRegion(r) 3088 3089 elif mode == 4: # Extract all regions with scalar connectivity 3090 cpf.SetExtractionModeToLargestRegion() 3091 cpf.ScalarConnectivityOn() 3092 cpf.SetScalarRange(vrange[0], vrange[1]) 3093 3094 elif mode == 5: # Extract point seeded regions 3095 cpf.SetExtractionModeToLargestRegion() 3096 cpf.ScalarConnectivityOn() 3097 cpf.SetScalarRange(vrange[0], vrange[1]) 3098 cpf.AlignedNormalsOn() 3099 cpf.SetNormalAngle(angle) 3100 3101 cpf.Update() 3102 self._update(cpf.GetOutput(), reset_locators=False) 3103 return self 3104 3105 def compute_camera_distance(self) -> np.ndarray: 3106 """ 3107 Calculate the distance from points to the camera. 3108 3109 A pointdata array is created with name 'DistanceToCamera' and returned. 3110 """ 3111 if vedo.plotter_instance and vedo.plotter_instance.renderer: 3112 poly = self.dataset 3113 dc = vtki.new("DistanceToCamera") 3114 dc.SetInputData(poly) 3115 dc.SetRenderer(vedo.plotter_instance.renderer) 3116 dc.Update() 3117 self._update(dc.GetOutput(), reset_locators=False) 3118 return self.pointdata["DistanceToCamera"] 3119 return np.array([]) 3120 3121 def densify(self, target_distance=0.1, nclosest=6, radius=None, niter=1, nmax=None) -> Self: 3122 """ 3123 Return a copy of the cloud with new added points. 3124 The new points are created in such a way that all points in any local neighborhood are 3125 within a target distance of one another. 3126 3127 For each input point, the distance to all points in its neighborhood is computed. 3128 If any of its neighbors is further than the target distance, 3129 the edge connecting the point and its neighbor is bisected and 3130 a new point is inserted at the bisection point. 3131 A single pass is completed once all the input points are visited. 3132 Then the process repeats to the number of iterations. 3133 3134 Examples: 3135 - [densifycloud.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/densifycloud.py) 3136 3137 ![](https://vedo.embl.es/images/volumetric/densifycloud.png) 3138 3139 .. note:: 3140 Points will be created in an iterative fashion until all points in their 3141 local neighborhood are the target distance apart or less. 3142 Note that the process may terminate early due to the 3143 number of iterations. By default the target distance is set to 0.5. 3144 Note that the target_distance should be less than the radius 3145 or nothing will change on output. 3146 3147 .. warning:: 3148 This class can generate a lot of points very quickly. 3149 The maximum number of iterations is by default set to =1.0 for this reason. 3150 Increase the number of iterations very carefully. 3151 Also, `nmax` can be set to limit the explosion of points. 3152 It is also recommended that a N closest neighborhood is used. 3153 3154 """ 3155 src = vtki.new("ProgrammableSource") 3156 opts = self.vertices 3157 # zeros = np.zeros(3) 3158 3159 def _read_points(): 3160 output = src.GetPolyDataOutput() 3161 points = vtki.vtkPoints() 3162 for p in opts: 3163 # print(p) 3164 # if not np.array_equal(p, zeros): 3165 points.InsertNextPoint(p) 3166 output.SetPoints(points) 3167 3168 src.SetExecuteMethod(_read_points) 3169 3170 dens = vtki.new("DensifyPointCloudFilter") 3171 dens.SetInputConnection(src.GetOutputPort()) 3172 # dens.SetInputData(self.dataset) # this does not work 3173 dens.InterpolateAttributeDataOn() 3174 dens.SetTargetDistance(target_distance) 3175 dens.SetMaximumNumberOfIterations(niter) 3176 if nmax: 3177 dens.SetMaximumNumberOfPoints(nmax) 3178 3179 if radius: 3180 dens.SetNeighborhoodTypeToRadius() 3181 dens.SetRadius(radius) 3182 elif nclosest: 3183 dens.SetNeighborhoodTypeToNClosest() 3184 dens.SetNumberOfClosestPoints(nclosest) 3185 else: 3186 vedo.logger.error("set either radius or nclosest") 3187 raise RuntimeError() 3188 dens.Update() 3189 3190 cld = Points(dens.GetOutput()) 3191 cld.copy_properties_from(self) 3192 cld.interpolate_data_from(self, n=nclosest, radius=radius) 3193 cld.name = "DensifiedCloud" 3194 cld.pipeline = utils.OperationNode( 3195 "densify", 3196 parents=[self], 3197 c="#e9c46a:", 3198 comment=f"#pts {cld.dataset.GetNumberOfPoints()}", 3199 ) 3200 return cld 3201 3202 ############################################################################### 3203 ## stuff returning a Volume 3204 ############################################################################### 3205 3206 def density( 3207 self, dims=(40, 40, 40), bounds=None, radius=None, compute_gradient=False, locator=None 3208 ) -> "vedo.Volume": 3209 """ 3210 Generate a density field from a point cloud. Input can also be a set of 3D coordinates. 3211 Output is a `Volume`. 3212 3213 The local neighborhood is specified as the `radius` around each sample position (each voxel). 3214 If left to None, the radius is automatically computed as the diagonal of the bounding box 3215 and can be accessed via `vol.metadata["radius"]`. 3216 The density is expressed as the number of counts in the radius search. 3217 3218 Arguments: 3219 dims : (int, list) 3220 number of voxels in x, y and z of the output Volume. 3221 compute_gradient : (bool) 3222 Turn on/off the generation of the gradient vector, 3223 gradient magnitude scalar, and function classification scalar. 3224 By default this is off. Note that this will increase execution time 3225 and the size of the output. (The names of these point data arrays are: 3226 "Gradient", "Gradient Magnitude", and "Classification") 3227 locator : (vtkPointLocator) 3228 can be assigned from a previous call for speed (access it via `object.point_locator`). 3229 3230 Examples: 3231 - [plot_density3d.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/plot_density3d.py) 3232 3233 ![](https://vedo.embl.es/images/pyplot/plot_density3d.png) 3234 """ 3235 pdf = vtki.new("PointDensityFilter") 3236 pdf.SetInputData(self.dataset) 3237 3238 if not utils.is_sequence(dims): 3239 dims = [dims, dims, dims] 3240 3241 if bounds is None: 3242 bounds = list(self.bounds()) 3243 elif len(bounds) == 4: 3244 bounds = [*bounds, 0, 0] 3245 3246 if bounds[5] - bounds[4] == 0 or len(dims) == 2: # its 2D 3247 dims = list(dims) 3248 dims = [dims[0], dims[1], 2] 3249 diag = self.diagonal_size() 3250 bounds[5] = bounds[4] + diag / 1000 3251 pdf.SetModelBounds(bounds) 3252 3253 pdf.SetSampleDimensions(dims) 3254 3255 if locator: 3256 pdf.SetLocator(locator) 3257 3258 pdf.SetDensityEstimateToFixedRadius() 3259 if radius is None: 3260 radius = self.diagonal_size() / 20 3261 pdf.SetRadius(radius) 3262 pdf.SetComputeGradient(compute_gradient) 3263 pdf.Update() 3264 3265 vol = vedo.Volume(pdf.GetOutput()).mode(1) 3266 vol.name = "PointDensity" 3267 vol.metadata["radius"] = radius 3268 vol.locator = pdf.GetLocator() 3269 vol.pipeline = utils.OperationNode( 3270 "density", parents=[self], comment=f"dims={tuple(vol.dimensions())}" 3271 ) 3272 return vol 3273 3274 3275 def tovolume( 3276 self, 3277 kernel="shepard", 3278 radius=None, 3279 n=None, 3280 bounds=None, 3281 null_value=None, 3282 dims=(25, 25, 25), 3283 ) -> "vedo.Volume": 3284 """ 3285 Generate a `Volume` by interpolating a scalar 3286 or vector field which is only known on a scattered set of points or mesh. 3287 Available interpolation kernels are: shepard, gaussian, or linear. 3288 3289 Arguments: 3290 kernel : (str) 3291 interpolation kernel type [shepard] 3292 radius : (float) 3293 radius of the local search 3294 n : (int) 3295 number of point to use for interpolation 3296 bounds : (list) 3297 bounding box of the output Volume object 3298 dims : (list) 3299 dimensions of the output Volume object 3300 null_value : (float) 3301 value to be assigned to invalid points 3302 3303 Examples: 3304 - [interpolate_volume.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/interpolate_volume.py) 3305 3306 ![](https://vedo.embl.es/images/volumetric/59095175-1ec5a300-8918-11e9-8bc0-fd35c8981e2b.jpg) 3307 """ 3308 if radius is None and not n: 3309 vedo.logger.error("please set either radius or n") 3310 raise RuntimeError 3311 3312 poly = self.dataset 3313 3314 # Create a probe volume 3315 probe = vtki.vtkImageData() 3316 probe.SetDimensions(dims) 3317 if bounds is None: 3318 bounds = self.bounds() 3319 probe.SetOrigin(bounds[0], bounds[2], bounds[4]) 3320 probe.SetSpacing( 3321 (bounds[1] - bounds[0]) / dims[0], 3322 (bounds[3] - bounds[2]) / dims[1], 3323 (bounds[5] - bounds[4]) / dims[2], 3324 ) 3325 3326 if not self.point_locator: 3327 self.point_locator = vtki.new("PointLocator") 3328 self.point_locator.SetDataSet(poly) 3329 self.point_locator.BuildLocator() 3330 3331 if kernel == "shepard": 3332 kern = vtki.new("ShepardKernel") 3333 kern.SetPowerParameter(2) 3334 elif kernel == "gaussian": 3335 kern = vtki.new("GaussianKernel") 3336 elif kernel == "linear": 3337 kern = vtki.new("LinearKernel") 3338 else: 3339 vedo.logger.error("Error in tovolume(), available kernels are:") 3340 vedo.logger.error(" [shepard, gaussian, linear]") 3341 raise RuntimeError() 3342 3343 if radius: 3344 kern.SetRadius(radius) 3345 3346 interpolator = vtki.new("PointInterpolator") 3347 interpolator.SetInputData(probe) 3348 interpolator.SetSourceData(poly) 3349 interpolator.SetKernel(kern) 3350 interpolator.SetLocator(self.point_locator) 3351 3352 if n: 3353 kern.SetNumberOfPoints(n) 3354 kern.SetKernelFootprintToNClosest() 3355 else: 3356 kern.SetRadius(radius) 3357 3358 if null_value is not None: 3359 interpolator.SetNullValue(null_value) 3360 else: 3361 interpolator.SetNullPointsStrategyToClosestPoint() 3362 interpolator.Update() 3363 3364 vol = vedo.Volume(interpolator.GetOutput()) 3365 3366 vol.pipeline = utils.OperationNode( 3367 "signed_distance", 3368 parents=[self], 3369 comment=f"dims={tuple(vol.dimensions())}", 3370 c="#e9c46a:#0096c7", 3371 ) 3372 return vol 3373 3374 ################################################################################# 3375 def generate_segments(self, istart=0, rmax=1e30, niter=3) -> "vedo.shapes.Lines": 3376 """ 3377 Generate a line segments from a set of points. 3378 The algorithm is based on the closest point search. 3379 3380 Returns a `Line` object. 3381 This object contains the a metadata array of used vertex counts in "UsedVertexCount" 3382 and the sum of the length of the segments in "SegmentsLengthSum". 3383 3384 Arguments: 3385 istart : (int) 3386 index of the starting point 3387 rmax : (float) 3388 maximum length of a segment 3389 niter : (int) 3390 number of iterations or passes through the points 3391 3392 Examples: 3393 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 3394 """ 3395 points = self.vertices 3396 segments = [] 3397 dists = [] 3398 n = len(points) 3399 used = np.zeros(n, dtype=int) 3400 for _ in range(niter): 3401 i = istart 3402 for _ in range(n): 3403 p = points[i] 3404 ids = self.closest_point(p, n=4, return_point_id=True) 3405 j = ids[1] 3406 if used[j] > 1 or [j, i] in segments: 3407 j = ids[2] 3408 if used[j] > 1: 3409 j = ids[3] 3410 d = np.linalg.norm(p - points[j]) 3411 if used[j] > 1 or used[i] > 1 or d > rmax: 3412 i += 1 3413 if i >= n: 3414 i = 0 3415 continue 3416 used[i] += 1 3417 used[j] += 1 3418 segments.append([i, j]) 3419 dists.append(d) 3420 i = j 3421 segments = np.array(segments, dtype=int) 3422 3423 lines = vedo.shapes.Lines(points[segments], c="k", lw=3) 3424 lines.metadata["UsedVertexCount"] = used 3425 lines.metadata["SegmentsLengthSum"] = np.sum(dists) 3426 lines.pipeline = utils.OperationNode("generate_segments", parents=[self]) 3427 lines.name = "Segments" 3428 return lines 3429 3430 def generate_delaunay2d( 3431 self, 3432 mode="scipy", 3433 boundaries=(), 3434 tol=None, 3435 alpha=0.0, 3436 offset=0.0, 3437 transform=None, 3438 ) -> "vedo.mesh.Mesh": 3439 """ 3440 Create a mesh from points in the XY plane. 3441 If `mode='fit'` then the filter computes a best fitting 3442 plane and projects the points onto it. 3443 3444 Check also `generate_mesh()`. 3445 3446 Arguments: 3447 tol : (float) 3448 specify a tolerance to control discarding of closely spaced points. 3449 This tolerance is specified as a fraction of the diagonal length of the bounding box of the points. 3450 alpha : (float) 3451 for a non-zero alpha value, only edges or triangles contained 3452 within a sphere centered at mesh vertices will be output. 3453 Otherwise, only triangles will be output. 3454 offset : (float) 3455 multiplier to control the size of the initial, bounding Delaunay triangulation. 3456 transform: (LinearTransform, NonLinearTransform) 3457 a transformation which is applied to points to generate a 2D problem. 3458 This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane. 3459 The points are transformed and triangulated. 3460 The topology of triangulated points is used as the output topology. 3461 3462 Examples: 3463 - [delaunay2d.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delaunay2d.py) 3464 3465 ![](https://vedo.embl.es/images/basic/delaunay2d.png) 3466 """ 3467 plist = self.vertices.copy() 3468 3469 ######################################################### 3470 if mode == "scipy": 3471 from scipy.spatial import Delaunay as scipy_delaunay 3472 3473 tri = scipy_delaunay(plist[:, 0:2]) 3474 return vedo.mesh.Mesh([plist, tri.simplices]) 3475 ########################################################## 3476 3477 pd = vtki.vtkPolyData() 3478 vpts = vtki.vtkPoints() 3479 vpts.SetData(utils.numpy2vtk(plist, dtype=np.float32)) 3480 pd.SetPoints(vpts) 3481 3482 delny = vtki.new("Delaunay2D") 3483 delny.SetInputData(pd) 3484 if tol: 3485 delny.SetTolerance(tol) 3486 delny.SetAlpha(alpha) 3487 delny.SetOffset(offset) 3488 3489 if transform: 3490 delny.SetTransform(transform.T) 3491 elif mode == "fit": 3492 delny.SetProjectionPlaneMode(vtki.get_class("VTK_BEST_FITTING_PLANE")) 3493 elif mode == "xy" and boundaries: 3494 boundary = vtki.vtkPolyData() 3495 boundary.SetPoints(vpts) 3496 cell_array = vtki.vtkCellArray() 3497 for b in boundaries: 3498 cpolygon = vtki.vtkPolygon() 3499 for idd in b: 3500 cpolygon.GetPointIds().InsertNextId(idd) 3501 cell_array.InsertNextCell(cpolygon) 3502 boundary.SetPolys(cell_array) 3503 delny.SetSourceData(boundary) 3504 3505 delny.Update() 3506 3507 msh = vedo.mesh.Mesh(delny.GetOutput()) 3508 msh.name = "Delaunay2D" 3509 msh.clean().lighting("off") 3510 msh.pipeline = utils.OperationNode( 3511 "delaunay2d", 3512 parents=[self], 3513 comment=f"#cells {msh.dataset.GetNumberOfCells()}", 3514 ) 3515 return msh 3516 3517 def generate_voronoi(self, padding=0.0, fit=False, method="vtk") -> "vedo.Mesh": 3518 """ 3519 Generate the 2D Voronoi convex tiling of the input points (z is ignored). 3520 The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon. 3521 3522 A cell array named "VoronoiID" is added to the output Mesh. 3523 3524 The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest 3525 to one of the input points. Voronoi tessellations are important in computational geometry 3526 (and many other fields), and are the dual of Delaunay triangulations. 3527 3528 Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored 3529 (although carried through to the output). 3530 If you desire to triangulate in a different plane, you can use fit=True. 3531 3532 A brief summary is as follows. Each (generating) input point is associated with 3533 an initial Voronoi tile, which is simply the bounding box of the point set. 3534 A locator is then used to identify nearby points: each neighbor in turn generates a 3535 clipping line positioned halfway between the generating point and the neighboring point, 3536 and orthogonal to the line connecting them. Clips are readily performed by evaluationg the 3537 vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line. 3538 If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip 3539 line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs, 3540 the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region 3541 containing the neighboring clip points. The clip region (along with the points contained in it) is grown 3542 by careful expansion (e.g., outward spiraling iterator over all candidate clip points). 3543 When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi 3544 tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi 3545 tessellation. Note that topological and geometric information is used to generate a valid triangulation 3546 (e.g., merging points and validating topology). 3547 3548 Arguments: 3549 pts : (list) 3550 list of input points. 3551 padding : (float) 3552 padding distance. The default is 0. 3553 fit : (bool) 3554 detect automatically the best fitting plane. The default is False. 3555 3556 Examples: 3557 - [voronoi1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi1.py) 3558 3559 ![](https://vedo.embl.es/images/basic/voronoi1.png) 3560 3561 - [voronoi2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi2.py) 3562 3563 ![](https://vedo.embl.es/images/advanced/voronoi2.png) 3564 """ 3565 pts = self.vertices 3566 3567 if method == "scipy": 3568 from scipy.spatial import Voronoi as scipy_voronoi 3569 3570 pts = np.asarray(pts)[:, (0, 1)] 3571 vor = scipy_voronoi(pts) 3572 regs = [] # filter out invalid indices 3573 for r in vor.regions: 3574 flag = True 3575 for x in r: 3576 if x < 0: 3577 flag = False 3578 break 3579 if flag and len(r) > 0: 3580 regs.append(r) 3581 3582 m = vedo.Mesh([vor.vertices, regs]) 3583 m.celldata["VoronoiID"] = np.array(list(range(len(regs)))).astype(int) 3584 3585 elif method == "vtk": 3586 vor = vtki.new("Voronoi2D") 3587 if isinstance(pts, Points): 3588 vor.SetInputData(pts) 3589 else: 3590 pts = np.asarray(pts) 3591 if pts.shape[1] == 2: 3592 pts = np.c_[pts, np.zeros(len(pts))] 3593 pd = vtki.vtkPolyData() 3594 vpts = vtki.vtkPoints() 3595 vpts.SetData(utils.numpy2vtk(pts, dtype=np.float32)) 3596 pd.SetPoints(vpts) 3597 vor.SetInputData(pd) 3598 vor.SetPadding(padding) 3599 vor.SetGenerateScalarsToPointIds() 3600 if fit: 3601 vor.SetProjectionPlaneModeToBestFittingPlane() 3602 else: 3603 vor.SetProjectionPlaneModeToXYPlane() 3604 vor.Update() 3605 poly = vor.GetOutput() 3606 arr = poly.GetCellData().GetArray(0) 3607 if arr: 3608 arr.SetName("VoronoiID") 3609 m = vedo.Mesh(poly, c="orange5") 3610 3611 else: 3612 vedo.logger.error(f"Unknown method {method} in voronoi()") 3613 raise RuntimeError 3614 3615 m.lw(2).lighting("off").wireframe() 3616 m.name = "Voronoi" 3617 return m 3618 3619 ########################################################################## 3620 def generate_delaunay3d(self, radius=0, tol=None) -> "vedo.TetMesh": 3621 """ 3622 Create 3D Delaunay triangulation of input points. 3623 3624 Arguments: 3625 radius : (float) 3626 specify distance (or "alpha") value to control output. 3627 For a non-zero values, only tetra contained within the circumsphere 3628 will be output. 3629 tol : (float) 3630 Specify a tolerance to control discarding of closely spaced points. 3631 This tolerance is specified as a fraction of the diagonal length of 3632 the bounding box of the points. 3633 """ 3634 deln = vtki.new("Delaunay3D") 3635 deln.SetInputData(self.dataset) 3636 deln.SetAlpha(radius) 3637 deln.AlphaTetsOn() 3638 deln.AlphaTrisOff() 3639 deln.AlphaLinesOff() 3640 deln.AlphaVertsOff() 3641 deln.BoundingTriangulationOff() 3642 if tol: 3643 deln.SetTolerance(tol) 3644 deln.Update() 3645 m = vedo.TetMesh(deln.GetOutput()) 3646 m.pipeline = utils.OperationNode( 3647 "generate_delaunay3d", c="#e9c46a:#edabab", parents=[self], 3648 ) 3649 m.name = "Delaunay3D" 3650 return m 3651 3652 #################################################### 3653 def visible_points(self, area=(), tol=None, invert=False) -> Union[Self, None]: 3654 """ 3655 Extract points based on whether they are visible or not. 3656 Visibility is determined by accessing the z-buffer of a rendering window. 3657 The position of each input point is converted into display coordinates, 3658 and then the z-value at that point is obtained. 3659 If within the user-specified tolerance, the point is considered visible. 3660 Associated data attributes are passed to the output as well. 3661 3662 This filter also allows you to specify a rectangular window in display (pixel) 3663 coordinates in which the visible points must lie. 3664 3665 Arguments: 3666 area : (list) 3667 specify a rectangular region as (xmin,xmax,ymin,ymax) 3668 tol : (float) 3669 a tolerance in normalized display coordinate system 3670 invert : (bool) 3671 select invisible points instead. 3672 3673 Example: 3674 ```python 3675 from vedo import Ellipsoid, show 3676 s = Ellipsoid().rotate_y(30) 3677 3678 # Camera options: pos, focal_point, viewup, distance 3679 camopts = dict(pos=(0,0,25), focal_point=(0,0,0)) 3680 show(s, camera=camopts, offscreen=True) 3681 3682 m = s.visible_points() 3683 # print('visible pts:', m.vertices) # numpy array 3684 show(m, new=True, axes=1).close() # optionally draw result in a new window 3685 ``` 3686 ![](https://vedo.embl.es/images/feats/visible_points.png) 3687 """ 3688 svp = vtki.new("SelectVisiblePoints") 3689 svp.SetInputData(self.dataset) 3690 3691 ren = None 3692 if vedo.plotter_instance: 3693 if vedo.plotter_instance.renderer: 3694 ren = vedo.plotter_instance.renderer 3695 svp.SetRenderer(ren) 3696 if not ren: 3697 vedo.logger.warning( 3698 "visible_points() can only be used after a rendering step" 3699 ) 3700 return None 3701 3702 if len(area) == 2: 3703 area = utils.flatten(area) 3704 if len(area) == 4: 3705 # specify a rectangular region 3706 svp.SetSelection(area[0], area[1], area[2], area[3]) 3707 if tol is not None: 3708 svp.SetTolerance(tol) 3709 if invert: 3710 svp.SelectInvisibleOn() 3711 svp.Update() 3712 3713 m = Points(svp.GetOutput()) 3714 m.name = "VisiblePoints" 3715 return m 3716 3717#################################################### 3718class CellCenters(Points): 3719 def __init__(self, pcloud): 3720 """ 3721 Generate `Points` at the center of the cells of any type of object. 3722 3723 Check out also `cell_centers()`. 3724 """ 3725 vcen = vtki.new("CellCenters") 3726 vcen.CopyArraysOn() 3727 vcen.VertexCellsOn() 3728 # vcen.ConvertGhostCellsToGhostPointsOn() 3729 try: 3730 vcen.SetInputData(pcloud.dataset) 3731 except AttributeError: 3732 vcen.SetInputData(pcloud) 3733 vcen.Update() 3734 super().__init__(vcen.GetOutput()) 3735 self.name = "CellCenters"
467class Points(PointsVisual, PointAlgorithms): 468 """Work with point clouds.""" 469 470 def __init__(self, inputobj=None, r=4, c=(0.2, 0.2, 0.2), alpha=1): 471 """ 472 Build an object made of only vertex points for a list of 2D/3D points. 473 Both shapes (N, 3) or (3, N) are accepted as input, if N>3. 474 475 Arguments: 476 inputobj : (list, tuple) 477 r : (int) 478 Point radius in units of pixels. 479 c : (str, list) 480 Color name or rgb tuple. 481 alpha : (float) 482 Transparency in range [0,1]. 483 484 Example: 485 ```python 486 from vedo import * 487 488 def fibonacci_sphere(n): 489 s = np.linspace(0, n, num=n, endpoint=False) 490 theta = s * 2.399963229728653 491 y = 1 - s * (2/(n-1)) 492 r = np.sqrt(1 - y * y) 493 x = np.cos(theta) * r 494 z = np.sin(theta) * r 495 return np._c[x,y,z] 496 497 Points(fibonacci_sphere(1000)).show(axes=1).close() 498 ``` 499 ![](https://vedo.embl.es/images/feats/fibonacci.png) 500 """ 501 # print("INIT POINTS") 502 super().__init__() 503 504 self.name = "" 505 self.filename = "" 506 self.file_size = "" 507 508 self.info = {} 509 self.time = time.time() 510 511 self.transform = LinearTransform() 512 self.point_locator = None 513 self.cell_locator = None 514 self.line_locator = None 515 516 self.actor = vtki.vtkActor() 517 self.properties = self.actor.GetProperty() 518 self.properties_backface = self.actor.GetBackfaceProperty() 519 self.mapper = vtki.new("PolyDataMapper") 520 self.dataset = vtki.vtkPolyData() 521 522 # Create weakref so actor can access this object (eg to pick/remove): 523 self.actor.retrieve_object = weak_ref_to(self) 524 525 try: 526 self.properties.RenderPointsAsSpheresOn() 527 except AttributeError: 528 pass 529 530 if inputobj is None: #################### 531 return 532 ########################################## 533 534 self.name = "Points" 535 536 ###### 537 if isinstance(inputobj, vtki.vtkActor): 538 self.dataset.DeepCopy(inputobj.GetMapper().GetInput()) 539 pr = vtki.vtkProperty() 540 pr.DeepCopy(inputobj.GetProperty()) 541 self.actor.SetProperty(pr) 542 self.properties = pr 543 self.mapper.SetScalarVisibility(inputobj.GetMapper().GetScalarVisibility()) 544 545 elif isinstance(inputobj, vtki.vtkPolyData): 546 self.dataset = inputobj 547 if self.dataset.GetNumberOfCells() == 0: 548 carr = vtki.vtkCellArray() 549 for i in range(self.dataset.GetNumberOfPoints()): 550 carr.InsertNextCell(1) 551 carr.InsertCellPoint(i) 552 self.dataset.SetVerts(carr) 553 554 elif isinstance(inputobj, Points): 555 self.dataset = inputobj.dataset 556 self.copy_properties_from(inputobj) 557 558 elif utils.is_sequence(inputobj): # passing point coords 559 self.dataset = utils.buildPolyData(utils.make3d(inputobj)) 560 561 elif isinstance(inputobj, str): 562 verts = vedo.file_io.load(inputobj) 563 self.filename = inputobj 564 self.dataset = verts.dataset 565 566 elif "meshlib" in str(type(inputobj)): 567 from meshlib import mrmeshnumpy as mn 568 self.dataset = utils.buildPolyData(mn.toNumpyArray(inputobj.points)) 569 570 else: 571 # try to extract the points from a generic VTK input data object 572 if hasattr(inputobj, "dataset"): 573 inputobj = inputobj.dataset 574 try: 575 vvpts = inputobj.GetPoints() 576 self.dataset = vtki.vtkPolyData() 577 self.dataset.SetPoints(vvpts) 578 for i in range(inputobj.GetPointData().GetNumberOfArrays()): 579 arr = inputobj.GetPointData().GetArray(i) 580 self.dataset.GetPointData().AddArray(arr) 581 except: 582 vedo.logger.error(f"cannot build Points from type {type(inputobj)}") 583 raise RuntimeError() 584 585 self.actor.SetMapper(self.mapper) 586 self.mapper.SetInputData(self.dataset) 587 588 self.properties.SetColor(colors.get_color(c)) 589 self.properties.SetOpacity(alpha) 590 self.properties.SetRepresentationToPoints() 591 self.properties.SetPointSize(r) 592 self.properties.LightingOff() 593 594 self.pipeline = utils.OperationNode( 595 self, parents=[], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 596 ) 597 598 def _update(self, polydata, reset_locators=True) -> Self: 599 """Overwrite the polygonal dataset with a new vtkPolyData.""" 600 self.dataset = polydata 601 self.mapper.SetInputData(self.dataset) 602 self.mapper.Modified() 603 if reset_locators: 604 self.point_locator = None 605 self.line_locator = None 606 self.cell_locator = None 607 return self 608 609 def __str__(self): 610 """Print a description of the Points/Mesh.""" 611 module = self.__class__.__module__ 612 name = self.__class__.__name__ 613 out = vedo.printc( 614 f"{module}.{name} at ({hex(self.memory_address())})".ljust(75), 615 c="g", bold=True, invert=True, return_string=True, 616 ) 617 out += "\x1b[0m\x1b[32;1m" 618 619 if self.name: 620 out += "name".ljust(14) + ": " + self.name 621 if "legend" in self.info.keys() and self.info["legend"]: 622 out+= f", legend='{self.info['legend']}'" 623 out += "\n" 624 625 if self.filename: 626 out+= "file name".ljust(14) + ": " + self.filename + "\n" 627 628 if not self.mapper.GetScalarVisibility(): 629 col = utils.precision(self.properties.GetColor(), 3) 630 cname = vedo.colors.get_color_name(self.properties.GetColor()) 631 out+= "color".ljust(14) + ": " + cname 632 out+= f", rgb={col}, alpha={self.properties.GetOpacity()}\n" 633 if self.actor.GetBackfaceProperty(): 634 bcol = self.actor.GetBackfaceProperty().GetDiffuseColor() 635 cname = vedo.colors.get_color_name(bcol) 636 out+= "backface color".ljust(14) + ": " 637 out+= f"{cname}, rgb={utils.precision(bcol,3)}\n" 638 639 npt = self.dataset.GetNumberOfPoints() 640 npo, nln = self.dataset.GetNumberOfPolys(), self.dataset.GetNumberOfLines() 641 out+= "elements".ljust(14) + f": vertices={npt:,} polygons={npo:,} lines={nln:,}" 642 if self.dataset.GetNumberOfStrips(): 643 out+= f", strips={self.dataset.GetNumberOfStrips():,}" 644 out+= "\n" 645 if self.dataset.GetNumberOfPieces() > 1: 646 out+= "pieces".ljust(14) + ": " + str(self.dataset.GetNumberOfPieces()) + "\n" 647 648 out+= "position".ljust(14) + ": " + f"{utils.precision(self.pos(), 6)}\n" 649 try: 650 sc = self.transform.get_scale() 651 out+= "scaling".ljust(14) + ": " 652 out+= utils.precision(sc, 6) + "\n" 653 except AttributeError: 654 pass 655 656 if self.npoints: 657 out+="size".ljust(14)+ ": average=" + utils.precision(self.average_size(),6) 658 out+=", diagonal="+ utils.precision(self.diagonal_size(), 6)+ "\n" 659 out+="center of mass".ljust(14) + ": " + utils.precision(self.center_of_mass(),6)+"\n" 660 661 bnds = self.bounds() 662 bx1, bx2 = utils.precision(bnds[0], 3), utils.precision(bnds[1], 3) 663 by1, by2 = utils.precision(bnds[2], 3), utils.precision(bnds[3], 3) 664 bz1, bz2 = utils.precision(bnds[4], 3), utils.precision(bnds[5], 3) 665 out+= "bounds".ljust(14) + ":" 666 out+= " x=(" + bx1 + ", " + bx2 + ")," 667 out+= " y=(" + by1 + ", " + by2 + ")," 668 out+= " z=(" + bz1 + ", " + bz2 + ")\n" 669 670 for key in self.pointdata.keys(): 671 arr = self.pointdata[key] 672 dim = arr.shape[1] if arr.ndim > 1 else 1 673 mark_active = "pointdata" 674 a_scalars = self.dataset.GetPointData().GetScalars() 675 a_vectors = self.dataset.GetPointData().GetVectors() 676 a_tensors = self.dataset.GetPointData().GetTensors() 677 if a_scalars and a_scalars.GetName() == key: 678 mark_active += " *" 679 elif a_vectors and a_vectors.GetName() == key: 680 mark_active += " **" 681 elif a_tensors and a_tensors.GetName() == key: 682 mark_active += " ***" 683 out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), dim={dim}' 684 if dim == 1 and len(arr): 685 rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3) 686 out += f", range=({rng})\n" 687 else: 688 out += "\n" 689 690 for key in self.celldata.keys(): 691 arr = self.celldata[key] 692 dim = arr.shape[1] if arr.ndim > 1 else 1 693 mark_active = "celldata" 694 a_scalars = self.dataset.GetCellData().GetScalars() 695 a_vectors = self.dataset.GetCellData().GetVectors() 696 a_tensors = self.dataset.GetCellData().GetTensors() 697 if a_scalars and a_scalars.GetName() == key: 698 mark_active += " *" 699 elif a_vectors and a_vectors.GetName() == key: 700 mark_active += " **" 701 elif a_tensors and a_tensors.GetName() == key: 702 mark_active += " ***" 703 out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), dim={dim}' 704 if dim == 1 and len(arr): 705 rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3) 706 out += f", range=({rng})\n" 707 else: 708 out += "\n" 709 710 for key in self.metadata.keys(): 711 arr = self.metadata[key] 712 if len(arr) > 3: 713 out+= "metadata".ljust(14) + ": " + f'"{key}" ({len(arr)} values)\n' 714 else: 715 out+= "metadata".ljust(14) + ": " + f'"{key}" = {arr}\n' 716 717 if self.picked3d is not None: 718 idp = self.closest_point(self.picked3d, return_point_id=True) 719 idc = self.closest_point(self.picked3d, return_cell_id=True) 720 out+= "clicked point".ljust(14) + ": " + utils.precision(self.picked3d, 6) 721 out+= f", pointID={idp}, cellID={idc}\n" 722 723 return out.rstrip() + "\x1b[0m" 724 725 def _repr_html_(self): 726 """ 727 HTML representation of the Point cloud object for Jupyter Notebooks. 728 729 Returns: 730 HTML text with the image and some properties. 731 """ 732 import io 733 import base64 734 from PIL import Image 735 736 library_name = "vedo.pointcloud.Points" 737 help_url = "https://vedo.embl.es/docs/vedo/pointcloud.html#Points" 738 739 arr = self.thumbnail() 740 im = Image.fromarray(arr) 741 buffered = io.BytesIO() 742 im.save(buffered, format="PNG", quality=100) 743 encoded = base64.b64encode(buffered.getvalue()).decode("utf-8") 744 url = "data:image/png;base64," + encoded 745 image = f"<img src='{url}'></img>" 746 747 bounds = "<br/>".join( 748 [ 749 utils.precision(min_x, 4) + " ... " + utils.precision(max_x, 4) 750 for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2]) 751 ] 752 ) 753 average_size = "{size:.3f}".format(size=self.average_size()) 754 755 help_text = "" 756 if self.name: 757 help_text += f"<b> {self.name}:   </b>" 758 help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>" 759 if self.filename: 760 dots = "" 761 if len(self.filename) > 30: 762 dots = "..." 763 help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>" 764 765 pdata = "" 766 if self.dataset.GetPointData().GetScalars(): 767 if self.dataset.GetPointData().GetScalars().GetName(): 768 name = self.dataset.GetPointData().GetScalars().GetName() 769 pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>" 770 771 cdata = "" 772 if self.dataset.GetCellData().GetScalars(): 773 if self.dataset.GetCellData().GetScalars().GetName(): 774 name = self.dataset.GetCellData().GetScalars().GetName() 775 cdata = "<tr><td><b> cell data array </b></td><td>" + name + "</td></tr>" 776 777 allt = [ 778 "<table>", 779 "<tr>", 780 "<td>", 781 image, 782 "</td>", 783 "<td style='text-align: center; vertical-align: center;'><br/>", 784 help_text, 785 "<table>", 786 "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>", 787 "<tr><td><b> center of mass </b></td><td>" 788 + utils.precision(self.center_of_mass(), 3) 789 + "</td></tr>", 790 "<tr><td><b> average size </b></td><td>" + str(average_size) + "</td></tr>", 791 "<tr><td><b> nr. points </b></td><td>" + str(self.npoints) + "</td></tr>", 792 pdata, 793 cdata, 794 "</table>", 795 "</table>", 796 ] 797 return "\n".join(allt) 798 799 ################################################################################## 800 def __add__(self, meshs): 801 """ 802 Add two meshes or a list of meshes together to form an `Assembly` object. 803 """ 804 if isinstance(meshs, list): 805 alist = [self] 806 for l in meshs: 807 if isinstance(l, vedo.Assembly): 808 alist += l.unpack() 809 else: 810 alist += l 811 return vedo.assembly.Assembly(alist) 812 813 if isinstance(meshs, vedo.Assembly): 814 return meshs + self # use Assembly.__add__ 815 816 return vedo.assembly.Assembly([self, meshs]) 817 818 def polydata(self, **kwargs): 819 """ 820 Obsolete. Use property `.dataset` instead. 821 Returns the underlying `vtkPolyData` object. 822 """ 823 colors.printc( 824 "WARNING: call to .polydata() is obsolete, use property .dataset instead.", 825 c="y") 826 return self.dataset 827 828 def __copy__(self): 829 return self.clone(deep=False) 830 831 def __deepcopy__(self, memo): 832 return self.clone(deep=memo) 833 834 def copy(self, deep=True) -> Self: 835 """Return a copy of the object. Alias of `clone()`.""" 836 return self.clone(deep=deep) 837 838 def clone(self, deep=True) -> Self: 839 """ 840 Clone a `PointCloud` or `Mesh` object to make an exact copy of it. 841 Alias of `copy()`. 842 843 Arguments: 844 deep : (bool) 845 if False return a shallow copy of the mesh without copying the points array. 846 847 Examples: 848 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 849 850 ![](https://vedo.embl.es/images/basic/mirror.png) 851 """ 852 poly = vtki.vtkPolyData() 853 if deep or isinstance(deep, dict): # if a memo object is passed this checks as True 854 poly.DeepCopy(self.dataset) 855 else: 856 poly.ShallowCopy(self.dataset) 857 858 if isinstance(self, vedo.Mesh): 859 cloned = vedo.Mesh(poly) 860 else: 861 cloned = Points(poly) 862 # print([self], self.__class__) 863 # cloned = self.__class__(poly) 864 865 cloned.transform = self.transform.clone() 866 867 cloned.copy_properties_from(self) 868 869 cloned.name = str(self.name) 870 cloned.filename = str(self.filename) 871 cloned.info = dict(self.info) 872 cloned.pipeline = utils.OperationNode("clone", parents=[self], shape="diamond", c="#edede9") 873 874 if isinstance(deep, dict): 875 deep[id(self)] = cloned 876 877 return cloned 878 879 def compute_normals_with_pca(self, n=20, orientation_point=None, invert=False) -> Self: 880 """ 881 Generate point normals using PCA (principal component analysis). 882 This algorithm estimates a local tangent plane around each sample point p 883 by considering a small neighborhood of points around p, and fitting a plane 884 to the neighborhood (via PCA). 885 886 Arguments: 887 n : (int) 888 neighborhood size to calculate the normal 889 orientation_point : (list) 890 adjust the +/- sign of the normals so that 891 the normals all point towards a specified point. If None, perform a traversal 892 of the point cloud and flip neighboring normals so that they are mutually consistent. 893 invert : (bool) 894 flip all normals 895 """ 896 poly = self.dataset 897 pcan = vtki.new("PCANormalEstimation") 898 pcan.SetInputData(poly) 899 pcan.SetSampleSize(n) 900 901 if orientation_point is not None: 902 pcan.SetNormalOrientationToPoint() 903 pcan.SetOrientationPoint(orientation_point) 904 else: 905 pcan.SetNormalOrientationToGraphTraversal() 906 907 if invert: 908 pcan.FlipNormalsOn() 909 pcan.Update() 910 911 varr = pcan.GetOutput().GetPointData().GetNormals() 912 varr.SetName("Normals") 913 self.dataset.GetPointData().SetNormals(varr) 914 self.dataset.GetPointData().Modified() 915 return self 916 917 def compute_acoplanarity(self, n=25, radius=None, on="points") -> Self: 918 """ 919 Compute acoplanarity which is a measure of how much a local region of the mesh 920 differs from a plane. 921 922 The information is stored in a `pointdata` or `celldata` array with name 'Acoplanarity'. 923 924 Either `n` (number of neighbour points) or `radius` (radius of local search) can be specified. 925 If a radius value is given and not enough points fall inside it, then a -1 is stored. 926 927 Example: 928 ```python 929 from vedo import * 930 msh = ParametricShape('RandomHills') 931 msh.compute_acoplanarity(radius=0.1, on='cells') 932 msh.cmap("coolwarm", on='cells').add_scalarbar() 933 msh.show(axes=1).close() 934 ``` 935 ![](https://vedo.embl.es/images/feats/acoplanarity.jpg) 936 """ 937 acoplanarities = [] 938 if "point" in on: 939 pts = self.vertices 940 elif "cell" in on: 941 pts = self.cell_centers 942 else: 943 raise ValueError(f"In compute_acoplanarity() set on to either 'cells' or 'points', not {on}") 944 945 for p in utils.progressbar(pts, delay=5, width=15, title=f"{on} acoplanarity"): 946 if n: 947 data = self.closest_point(p, n=n) 948 npts = n 949 elif radius: 950 data = self.closest_point(p, radius=radius) 951 npts = len(data) 952 953 try: 954 center = data.mean(axis=0) 955 res = np.linalg.svd(data - center) 956 acoplanarities.append(res[1][2] / npts) 957 except: 958 acoplanarities.append(-1.0) 959 960 if "point" in on: 961 self.pointdata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 962 else: 963 self.celldata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 964 return self 965 966 def distance_to(self, pcloud, signed=False, invert=False, name="Distance") -> np.ndarray: 967 """ 968 Computes the distance from one point cloud or mesh to another point cloud or mesh. 969 This new `pointdata` array is saved with default name "Distance". 970 971 Keywords `signed` and `invert` are used to compute signed distance, 972 but the mesh in that case must have polygonal faces (not a simple point cloud), 973 and normals must also be computed. 974 975 Examples: 976 - [distance2mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/distance2mesh.py) 977 978 ![](https://vedo.embl.es/images/basic/distance2mesh.png) 979 """ 980 if pcloud.dataset.GetNumberOfPolys(): 981 982 poly1 = self.dataset 983 poly2 = pcloud.dataset 984 df = vtki.new("DistancePolyDataFilter") 985 df.ComputeSecondDistanceOff() 986 df.SetInputData(0, poly1) 987 df.SetInputData(1, poly2) 988 df.SetSignedDistance(signed) 989 df.SetNegateDistance(invert) 990 df.Update() 991 scals = df.GetOutput().GetPointData().GetScalars() 992 dists = utils.vtk2numpy(scals) 993 994 else: # has no polygons 995 996 if signed: 997 vedo.logger.warning("distance_to() called with signed=True but input object has no polygons") 998 999 if not pcloud.point_locator: 1000 pcloud.point_locator = vtki.new("PointLocator") 1001 pcloud.point_locator.SetDataSet(pcloud.dataset) 1002 pcloud.point_locator.BuildLocator() 1003 1004 ids = [] 1005 ps1 = self.vertices 1006 ps2 = pcloud.vertices 1007 for p in ps1: 1008 pid = pcloud.point_locator.FindClosestPoint(p) 1009 ids.append(pid) 1010 1011 deltas = ps2[ids] - ps1 1012 dists = np.linalg.norm(deltas, axis=1).astype(np.float32) 1013 scals = utils.numpy2vtk(dists) 1014 1015 scals.SetName(name) 1016 self.dataset.GetPointData().AddArray(scals) 1017 self.dataset.GetPointData().SetActiveScalars(scals.GetName()) 1018 rng = scals.GetRange() 1019 self.mapper.SetScalarRange(rng[0], rng[1]) 1020 self.mapper.ScalarVisibilityOn() 1021 1022 self.pipeline = utils.OperationNode( 1023 "distance_to", 1024 parents=[self, pcloud], 1025 shape="cylinder", 1026 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1027 ) 1028 return dists 1029 1030 def clean(self) -> Self: 1031 """Clean pointcloud or mesh by removing coincident points.""" 1032 cpd = vtki.new("CleanPolyData") 1033 cpd.PointMergingOn() 1034 cpd.ConvertLinesToPointsOff() 1035 cpd.ConvertPolysToLinesOff() 1036 cpd.ConvertStripsToPolysOff() 1037 cpd.SetInputData(self.dataset) 1038 cpd.Update() 1039 self._update(cpd.GetOutput()) 1040 self.pipeline = utils.OperationNode( 1041 "clean", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1042 ) 1043 return self 1044 1045 def subsample(self, fraction: float, absolute=False) -> Self: 1046 """ 1047 Subsample a point cloud by requiring that the points 1048 or vertices are far apart at least by the specified fraction of the object size. 1049 If a Mesh is passed the polygonal faces are not removed 1050 but holes can appear as their vertices are removed. 1051 1052 Examples: 1053 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1054 1055 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1056 1057 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1058 1059 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1060 """ 1061 if not absolute: 1062 if fraction > 1: 1063 vedo.logger.warning( 1064 f"subsample(fraction=...), fraction must be < 1, but is {fraction}" 1065 ) 1066 if fraction <= 0: 1067 return self 1068 1069 cpd = vtki.new("CleanPolyData") 1070 cpd.PointMergingOn() 1071 cpd.ConvertLinesToPointsOn() 1072 cpd.ConvertPolysToLinesOn() 1073 cpd.ConvertStripsToPolysOn() 1074 cpd.SetInputData(self.dataset) 1075 if absolute: 1076 cpd.SetTolerance(fraction / self.diagonal_size()) 1077 # cpd.SetToleranceIsAbsolute(absolute) 1078 else: 1079 cpd.SetTolerance(fraction) 1080 cpd.Update() 1081 1082 ps = 2 1083 if self.properties.GetRepresentation() == 0: 1084 ps = self.properties.GetPointSize() 1085 1086 self._update(cpd.GetOutput()) 1087 self.ps(ps) 1088 1089 self.pipeline = utils.OperationNode( 1090 "subsample", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1091 ) 1092 return self 1093 1094 def threshold(self, scalars: str, above=None, below=None, on="points") -> Self: 1095 """ 1096 Extracts cells where scalar value satisfies threshold criterion. 1097 1098 Arguments: 1099 scalars : (str) 1100 name of the scalars array. 1101 above : (float) 1102 minimum value of the scalar 1103 below : (float) 1104 maximum value of the scalar 1105 on : (str) 1106 if 'cells' assume array of scalars refers to cell data. 1107 1108 Examples: 1109 - [mesh_threshold.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_threshold.py) 1110 """ 1111 thres = vtki.new("Threshold") 1112 thres.SetInputData(self.dataset) 1113 1114 if on.startswith("c"): 1115 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_CELLS 1116 else: 1117 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_POINTS 1118 1119 thres.SetInputArrayToProcess(0, 0, 0, asso, scalars) 1120 1121 if above is None and below is not None: 1122 try: # vtk 9.2 1123 thres.ThresholdByLower(below) 1124 except AttributeError: # vtk 9.3 1125 thres.SetUpperThreshold(below) 1126 1127 elif below is None and above is not None: 1128 try: 1129 thres.ThresholdByUpper(above) 1130 except AttributeError: 1131 thres.SetLowerThreshold(above) 1132 else: 1133 try: 1134 thres.ThresholdBetween(above, below) 1135 except AttributeError: 1136 thres.SetUpperThreshold(below) 1137 thres.SetLowerThreshold(above) 1138 1139 thres.Update() 1140 1141 gf = vtki.new("GeometryFilter") 1142 gf.SetInputData(thres.GetOutput()) 1143 gf.Update() 1144 self._update(gf.GetOutput()) 1145 self.pipeline = utils.OperationNode("threshold", parents=[self]) 1146 return self 1147 1148 def quantize(self, value: float) -> Self: 1149 """ 1150 The user should input a value and all {x,y,z} coordinates 1151 will be quantized to that absolute grain size. 1152 """ 1153 qp = vtki.new("QuantizePolyDataPoints") 1154 qp.SetInputData(self.dataset) 1155 qp.SetQFactor(value) 1156 qp.Update() 1157 self._update(qp.GetOutput()) 1158 self.pipeline = utils.OperationNode("quantize", parents=[self]) 1159 return self 1160 1161 @property 1162 def vertex_normals(self) -> np.ndarray: 1163 """ 1164 Retrieve vertex normals as a numpy array. Same as `point_normals`. 1165 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1166 """ 1167 vtknormals = self.dataset.GetPointData().GetNormals() 1168 return utils.vtk2numpy(vtknormals) 1169 1170 @property 1171 def point_normals(self) -> np.ndarray: 1172 """ 1173 Retrieve vertex normals as a numpy array. Same as `vertex_normals`. 1174 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1175 """ 1176 vtknormals = self.dataset.GetPointData().GetNormals() 1177 return utils.vtk2numpy(vtknormals) 1178 1179 def align_to(self, target, iters=100, rigid=False, invert=False, use_centroids=False) -> Self: 1180 """ 1181 Aligned to target mesh through the `Iterative Closest Point` algorithm. 1182 1183 The core of the algorithm is to match each vertex in one surface with 1184 the closest surface point on the other, then apply the transformation 1185 that modify one surface to best match the other (in the least-square sense). 1186 1187 Arguments: 1188 rigid : (bool) 1189 if True do not allow scaling 1190 invert : (bool) 1191 if True start by aligning the target to the source but 1192 invert the transformation finally. Useful when the target is smaller 1193 than the source. 1194 use_centroids : (bool) 1195 start by matching the centroids of the two objects. 1196 1197 Examples: 1198 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1199 1200 ![](https://vedo.embl.es/images/basic/align1.png) 1201 1202 - [align2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align2.py) 1203 1204 ![](https://vedo.embl.es/images/basic/align2.png) 1205 """ 1206 icp = vtki.new("IterativeClosestPointTransform") 1207 icp.SetSource(self.dataset) 1208 icp.SetTarget(target.dataset) 1209 if invert: 1210 icp.Inverse() 1211 icp.SetMaximumNumberOfIterations(iters) 1212 if rigid: 1213 icp.GetLandmarkTransform().SetModeToRigidBody() 1214 icp.SetStartByMatchingCentroids(use_centroids) 1215 icp.Update() 1216 1217 self.apply_transform(icp.GetMatrix()) 1218 1219 self.pipeline = utils.OperationNode( 1220 "align_to", parents=[self, target], comment=f"rigid = {rigid}" 1221 ) 1222 return self 1223 1224 def align_to_bounding_box(self, msh, rigid=False) -> Self: 1225 """ 1226 Align the current object's bounding box to the bounding box 1227 of the input object. 1228 1229 Use `rigid=True` to disable scaling. 1230 1231 Example: 1232 [align6.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align6.py) 1233 """ 1234 lmt = vtki.vtkLandmarkTransform() 1235 ss = vtki.vtkPoints() 1236 xss0, xss1, yss0, yss1, zss0, zss1 = self.bounds() 1237 for p in [ 1238 [xss0, yss0, zss0], 1239 [xss1, yss0, zss0], 1240 [xss1, yss1, zss0], 1241 [xss0, yss1, zss0], 1242 [xss0, yss0, zss1], 1243 [xss1, yss0, zss1], 1244 [xss1, yss1, zss1], 1245 [xss0, yss1, zss1], 1246 ]: 1247 ss.InsertNextPoint(p) 1248 st = vtki.vtkPoints() 1249 xst0, xst1, yst0, yst1, zst0, zst1 = msh.bounds() 1250 for p in [ 1251 [xst0, yst0, zst0], 1252 [xst1, yst0, zst0], 1253 [xst1, yst1, zst0], 1254 [xst0, yst1, zst0], 1255 [xst0, yst0, zst1], 1256 [xst1, yst0, zst1], 1257 [xst1, yst1, zst1], 1258 [xst0, yst1, zst1], 1259 ]: 1260 st.InsertNextPoint(p) 1261 1262 lmt.SetSourceLandmarks(ss) 1263 lmt.SetTargetLandmarks(st) 1264 lmt.SetModeToAffine() 1265 if rigid: 1266 lmt.SetModeToRigidBody() 1267 lmt.Update() 1268 1269 LT = LinearTransform(lmt) 1270 self.apply_transform(LT) 1271 return self 1272 1273 def align_with_landmarks( 1274 self, 1275 source_landmarks, 1276 target_landmarks, 1277 rigid=False, 1278 affine=False, 1279 least_squares=False, 1280 ) -> Self: 1281 """ 1282 Transform mesh orientation and position based on a set of landmarks points. 1283 The algorithm finds the best matching of source points to target points 1284 in the mean least square sense, in one single step. 1285 1286 If `affine` is True the x, y and z axes can scale independently but stay collinear. 1287 With least_squares they can vary orientation. 1288 1289 Examples: 1290 - [align5.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align5.py) 1291 1292 ![](https://vedo.embl.es/images/basic/align5.png) 1293 """ 1294 1295 if utils.is_sequence(source_landmarks): 1296 ss = vtki.vtkPoints() 1297 for p in source_landmarks: 1298 ss.InsertNextPoint(p) 1299 else: 1300 ss = source_landmarks.dataset.GetPoints() 1301 if least_squares: 1302 source_landmarks = source_landmarks.vertices 1303 1304 if utils.is_sequence(target_landmarks): 1305 st = vtki.vtkPoints() 1306 for p in target_landmarks: 1307 st.InsertNextPoint(p) 1308 else: 1309 st = target_landmarks.GetPoints() 1310 if least_squares: 1311 target_landmarks = target_landmarks.vertices 1312 1313 if ss.GetNumberOfPoints() != st.GetNumberOfPoints(): 1314 n1 = ss.GetNumberOfPoints() 1315 n2 = st.GetNumberOfPoints() 1316 vedo.logger.error(f"source and target have different nr of points {n1} vs {n2}") 1317 raise RuntimeError() 1318 1319 if int(rigid) + int(affine) + int(least_squares) > 1: 1320 vedo.logger.error( 1321 "only one of rigid, affine, least_squares can be True at a time" 1322 ) 1323 raise RuntimeError() 1324 1325 lmt = vtki.vtkLandmarkTransform() 1326 lmt.SetSourceLandmarks(ss) 1327 lmt.SetTargetLandmarks(st) 1328 lmt.SetModeToSimilarity() 1329 1330 if rigid: 1331 lmt.SetModeToRigidBody() 1332 lmt.Update() 1333 1334 elif affine: 1335 lmt.SetModeToAffine() 1336 lmt.Update() 1337 1338 elif least_squares: 1339 cms = source_landmarks.mean(axis=0) 1340 cmt = target_landmarks.mean(axis=0) 1341 m = np.linalg.lstsq(source_landmarks - cms, target_landmarks - cmt, rcond=None)[0] 1342 M = vtki.vtkMatrix4x4() 1343 for i in range(3): 1344 for j in range(3): 1345 M.SetElement(j, i, m[i][j]) 1346 lmt = vtki.vtkTransform() 1347 lmt.Translate(cmt) 1348 lmt.Concatenate(M) 1349 lmt.Translate(-cms) 1350 1351 else: 1352 lmt.Update() 1353 1354 self.apply_transform(lmt) 1355 self.pipeline = utils.OperationNode("transform_with_landmarks", parents=[self]) 1356 return self 1357 1358 def normalize(self) -> Self: 1359 """Scale average size to unit. The scaling is performed around the center of mass.""" 1360 coords = self.vertices 1361 if not coords.shape[0]: 1362 return self 1363 cm = np.mean(coords, axis=0) 1364 pts = coords - cm 1365 xyz2 = np.sum(pts * pts, axis=0) 1366 scale = 1 / np.sqrt(np.sum(xyz2) / len(pts)) 1367 self.scale(scale, origin=cm) 1368 self.pipeline = utils.OperationNode("normalize", parents=[self]) 1369 return self 1370 1371 def mirror(self, axis="x", origin=True) -> Self: 1372 """ 1373 Mirror reflect along one of the cartesian axes 1374 1375 Arguments: 1376 axis : (str) 1377 axis to use for mirroring, must be set to `x, y, z`. 1378 Or any combination of those. 1379 origin : (list) 1380 use this point as the origin of the mirroring transformation. 1381 1382 Examples: 1383 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 1384 1385 ![](https://vedo.embl.es/images/basic/mirror.png) 1386 """ 1387 sx, sy, sz = 1, 1, 1 1388 if "x" in axis.lower(): sx = -1 1389 if "y" in axis.lower(): sy = -1 1390 if "z" in axis.lower(): sz = -1 1391 1392 self.scale([sx, sy, sz], origin=origin) 1393 1394 self.pipeline = utils.OperationNode( 1395 "mirror", comment=f"axis = {axis}", parents=[self]) 1396 1397 if sx * sy * sz < 0: 1398 if hasattr(self, "reverse"): 1399 self.reverse() 1400 return self 1401 1402 def flip_normals(self) -> Self: 1403 """Flip all normals orientation.""" 1404 rs = vtki.new("ReverseSense") 1405 rs.SetInputData(self.dataset) 1406 rs.ReverseCellsOff() 1407 rs.ReverseNormalsOn() 1408 rs.Update() 1409 self._update(rs.GetOutput()) 1410 self.pipeline = utils.OperationNode("flip_normals", parents=[self]) 1411 return self 1412 1413 def add_gaussian_noise(self, sigma=1.0) -> Self: 1414 """ 1415 Add gaussian noise to point positions. 1416 An extra array is added named "GaussianNoise" with the displacements. 1417 1418 Arguments: 1419 sigma : (float) 1420 nr. of standard deviations, expressed in percent of the diagonal size of mesh. 1421 Can also be a list `[sigma_x, sigma_y, sigma_z]`. 1422 1423 Example: 1424 ```python 1425 from vedo import Sphere 1426 Sphere().add_gaussian_noise(1.0).point_size(8).show().close() 1427 ``` 1428 """ 1429 sz = self.diagonal_size() 1430 pts = self.vertices 1431 n = len(pts) 1432 ns = (np.random.randn(n, 3) * sigma) * (sz / 100) 1433 vpts = vtki.vtkPoints() 1434 vpts.SetNumberOfPoints(n) 1435 vpts.SetData(utils.numpy2vtk(pts + ns, dtype=np.float32)) 1436 self.dataset.SetPoints(vpts) 1437 self.dataset.GetPoints().Modified() 1438 self.pointdata["GaussianNoise"] = -ns 1439 self.pipeline = utils.OperationNode( 1440 "gaussian_noise", parents=[self], shape="egg", comment=f"sigma = {sigma}" 1441 ) 1442 return self 1443 1444 def closest_point( 1445 self, pt, n=1, radius=None, return_point_id=False, return_cell_id=False 1446 ) -> Union[List[int], int, np.ndarray]: 1447 """ 1448 Find the closest point(s) on a mesh given from the input point `pt`. 1449 1450 Arguments: 1451 n : (int) 1452 if greater than 1, return a list of n ordered closest points 1453 radius : (float) 1454 if given, get all points within that radius. Then n is ignored. 1455 return_point_id : (bool) 1456 return point ID instead of coordinates 1457 return_cell_id : (bool) 1458 return cell ID in which the closest point sits 1459 1460 Examples: 1461 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1462 - [fitplanes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitplanes.py) 1463 - [quadratic_morphing.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/quadratic_morphing.py) 1464 1465 .. note:: 1466 The appropriate tree search locator is built on the fly and cached for speed. 1467 1468 If you want to reset it use `mymesh.point_locator=None` 1469 and / or `mymesh.cell_locator=None`. 1470 """ 1471 if len(pt) != 3: 1472 pt = [pt[0], pt[1], 0] 1473 1474 # NB: every time the mesh moves or is warped the locators are set to None 1475 if ((n > 1 or radius) or (n == 1 and return_point_id)) and not return_cell_id: 1476 poly = None 1477 if not self.point_locator: 1478 poly = self.dataset 1479 self.point_locator = vtki.new("StaticPointLocator") 1480 self.point_locator.SetDataSet(poly) 1481 self.point_locator.BuildLocator() 1482 1483 ########## 1484 if radius: 1485 vtklist = vtki.vtkIdList() 1486 self.point_locator.FindPointsWithinRadius(radius, pt, vtklist) 1487 elif n > 1: 1488 vtklist = vtki.vtkIdList() 1489 self.point_locator.FindClosestNPoints(n, pt, vtklist) 1490 else: # n==1 hence return_point_id==True 1491 ######## 1492 return self.point_locator.FindClosestPoint(pt) 1493 ######## 1494 1495 if return_point_id: 1496 ######## 1497 return utils.vtk2numpy(vtklist) 1498 ######## 1499 1500 if not poly: 1501 poly = self.dataset 1502 trgp = [] 1503 for i in range(vtklist.GetNumberOfIds()): 1504 trgp_ = [0, 0, 0] 1505 vi = vtklist.GetId(i) 1506 poly.GetPoints().GetPoint(vi, trgp_) 1507 trgp.append(trgp_) 1508 ######## 1509 return np.array(trgp) 1510 ######## 1511 1512 else: 1513 1514 if not self.cell_locator: 1515 poly = self.dataset 1516 1517 # As per Miquel example with limbs the vtkStaticCellLocator doesnt work !! 1518 # https://discourse.vtk.org/t/vtkstaticcelllocator-problem-vtk9-0-3/7854/4 1519 if vedo.vtk_version[0] >= 9 and vedo.vtk_version[1] > 0: 1520 self.cell_locator = vtki.new("StaticCellLocator") 1521 else: 1522 self.cell_locator = vtki.new("CellLocator") 1523 1524 self.cell_locator.SetDataSet(poly) 1525 self.cell_locator.BuildLocator() 1526 1527 if radius is not None: 1528 vedo.printc("Warning: closest_point() with radius is not implemented for cells.", c='r') 1529 1530 if n != 1: 1531 vedo.printc("Warning: closest_point() with n>1 is not implemented for cells.", c='r') 1532 1533 trgp = [0, 0, 0] 1534 cid = vtki.mutable(0) 1535 dist2 = vtki.mutable(0) 1536 subid = vtki.mutable(0) 1537 self.cell_locator.FindClosestPoint(pt, trgp, cid, subid, dist2) 1538 1539 if return_cell_id: 1540 return int(cid) 1541 1542 return np.array(trgp) 1543 1544 def auto_distance(self) -> np.ndarray: 1545 """ 1546 Calculate the distance to the closest point in the same cloud of points. 1547 The output is stored in a new pointdata array called "AutoDistance", 1548 and it is also returned by the function. 1549 """ 1550 points = self.vertices 1551 if not self.point_locator: 1552 self.point_locator = vtki.new("StaticPointLocator") 1553 self.point_locator.SetDataSet(self.dataset) 1554 self.point_locator.BuildLocator() 1555 qs = [] 1556 vtklist = vtki.vtkIdList() 1557 vtkpoints = self.dataset.GetPoints() 1558 for p in points: 1559 self.point_locator.FindClosestNPoints(2, p, vtklist) 1560 q = [0, 0, 0] 1561 pid = vtklist.GetId(1) 1562 vtkpoints.GetPoint(pid, q) 1563 qs.append(q) 1564 dists = np.linalg.norm(points - np.array(qs), axis=1) 1565 self.pointdata["AutoDistance"] = dists 1566 return dists 1567 1568 def hausdorff_distance(self, points) -> float: 1569 """ 1570 Compute the Hausdorff distance to the input point set. 1571 Returns a single `float`. 1572 1573 Example: 1574 ```python 1575 from vedo import * 1576 t = np.linspace(0, 2*np.pi, 100) 1577 x = 4/3 * sin(t)**3 1578 y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12 1579 pol1 = Line(np.c_[x,y], closed=True).triangulate() 1580 pol2 = Polygon(nsides=5).pos(2,2) 1581 d12 = pol1.distance_to(pol2) 1582 d21 = pol2.distance_to(pol1) 1583 pol1.lw(0).cmap("viridis") 1584 pol2.lw(0).cmap("viridis") 1585 print("distance d12, d21 :", min(d12), min(d21)) 1586 print("hausdorff distance:", pol1.hausdorff_distance(pol2)) 1587 print("chamfer distance :", pol1.chamfer_distance(pol2)) 1588 show(pol1, pol2, axes=1) 1589 ``` 1590 ![](https://vedo.embl.es/images/feats/heart.png) 1591 """ 1592 hp = vtki.new("HausdorffDistancePointSetFilter") 1593 hp.SetInputData(0, self.dataset) 1594 hp.SetInputData(1, points.dataset) 1595 hp.SetTargetDistanceMethodToPointToCell() 1596 hp.Update() 1597 return hp.GetHausdorffDistance() 1598 1599 def chamfer_distance(self, pcloud) -> float: 1600 """ 1601 Compute the Chamfer distance to the input point set. 1602 1603 Example: 1604 ```python 1605 from vedo import * 1606 cloud1 = np.random.randn(1000, 3) 1607 cloud2 = np.random.randn(1000, 3) + [1, 2, 3] 1608 c1 = Points(cloud1, r=5, c="red") 1609 c2 = Points(cloud2, r=5, c="green") 1610 d = c1.chamfer_distance(c2) 1611 show(f"Chamfer distance = {d}", c1, c2, axes=1).close() 1612 ``` 1613 """ 1614 # Definition of Chamfer distance may vary, here we use the average 1615 if not pcloud.point_locator: 1616 pcloud.point_locator = vtki.new("PointLocator") 1617 pcloud.point_locator.SetDataSet(pcloud.dataset) 1618 pcloud.point_locator.BuildLocator() 1619 if not self.point_locator: 1620 self.point_locator = vtki.new("PointLocator") 1621 self.point_locator.SetDataSet(self.dataset) 1622 self.point_locator.BuildLocator() 1623 1624 ps1 = self.vertices 1625 ps2 = pcloud.vertices 1626 1627 ids12 = [] 1628 for p in ps1: 1629 pid12 = pcloud.point_locator.FindClosestPoint(p) 1630 ids12.append(pid12) 1631 deltav = ps2[ids12] - ps1 1632 da = np.mean(np.linalg.norm(deltav, axis=1)) 1633 1634 ids21 = [] 1635 for p in ps2: 1636 pid21 = self.point_locator.FindClosestPoint(p) 1637 ids21.append(pid21) 1638 deltav = ps1[ids21] - ps2 1639 db = np.mean(np.linalg.norm(deltav, axis=1)) 1640 return (da + db) / 2 1641 1642 def remove_outliers(self, radius: float, neighbors=5) -> Self: 1643 """ 1644 Remove outliers from a cloud of points within the specified `radius` search. 1645 1646 Arguments: 1647 radius : (float) 1648 Specify the local search radius. 1649 neighbors : (int) 1650 Specify the number of neighbors that a point must have, 1651 within the specified radius, for the point to not be considered isolated. 1652 1653 Examples: 1654 - [clustering.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/clustering.py) 1655 1656 ![](https://vedo.embl.es/images/basic/clustering.png) 1657 """ 1658 removal = vtki.new("RadiusOutlierRemoval") 1659 removal.SetInputData(self.dataset) 1660 removal.SetRadius(radius) 1661 removal.SetNumberOfNeighbors(neighbors) 1662 removal.GenerateOutliersOff() 1663 removal.Update() 1664 inputobj = removal.GetOutput() 1665 if inputobj.GetNumberOfCells() == 0: 1666 carr = vtki.vtkCellArray() 1667 for i in range(inputobj.GetNumberOfPoints()): 1668 carr.InsertNextCell(1) 1669 carr.InsertCellPoint(i) 1670 inputobj.SetVerts(carr) 1671 self._update(removal.GetOutput()) 1672 self.pipeline = utils.OperationNode("remove_outliers", parents=[self]) 1673 return self 1674 1675 def relax_point_positions( 1676 self, 1677 n=10, 1678 iters=10, 1679 sub_iters=10, 1680 packing_factor=1, 1681 max_step=0, 1682 constraints=(), 1683 ) -> Self: 1684 """ 1685 Smooth mesh or points with a 1686 [Laplacian algorithm](https://vtk.org/doc/nightly/html/classvtkPointSmoothingFilter.html) 1687 variant. This modifies the coordinates of the input points by adjusting their positions 1688 to create a smooth distribution (and thereby form a pleasing packing of the points). 1689 Smoothing is performed by considering the effects of neighboring points on one another 1690 it uses a cubic cutoff function to produce repulsive forces between close points 1691 and attractive forces that are a little further away. 1692 1693 In general, the larger the neighborhood size, the greater the reduction in high frequency 1694 information. The memory and computational requirements of the algorithm may also 1695 significantly increase. 1696 1697 The algorithm incrementally adjusts the point positions through an iterative process. 1698 Basically points are moved due to the influence of neighboring points. 1699 1700 As points move, both the local connectivity and data attributes associated with each point 1701 must be updated. Rather than performing these expensive operations after every iteration, 1702 a number of sub-iterations can be specified. If so, then the neighborhood and attribute 1703 value updates occur only every sub iteration, which can improve performance significantly. 1704 1705 Arguments: 1706 n : (int) 1707 neighborhood size to calculate the Laplacian. 1708 iters : (int) 1709 number of iterations. 1710 sub_iters : (int) 1711 number of sub-iterations, i.e. the number of times the neighborhood and attribute 1712 value updates occur during each iteration. 1713 packing_factor : (float) 1714 adjust convergence speed. 1715 max_step : (float) 1716 Specify the maximum smoothing step size for each smoothing iteration. 1717 This limits the the distance over which a point can move in each iteration. 1718 As in all iterative methods, the stability of the process is sensitive to this parameter. 1719 In general, small step size and large numbers of iterations are more stable than a larger 1720 step size and a smaller numbers of iterations. 1721 constraints : (dict) 1722 dictionary of constraints. 1723 Point constraints are used to prevent points from moving, 1724 or to move only on a plane. This can prevent shrinking or growing point clouds. 1725 If enabled, a local topological analysis is performed to determine whether a point 1726 should be marked as fixed" i.e., never moves, or the point only moves on a plane, 1727 or the point can move freely. 1728 If all points in the neighborhood surrounding a point are in the cone defined by 1729 `fixed_angle`, then the point is classified as fixed. 1730 If all points in the neighborhood surrounding a point are in the cone defined by 1731 `boundary_angle`, then the point is classified as lying on a plane. 1732 Angles are expressed in degrees. 1733 1734 Example: 1735 ```py 1736 import numpy as np 1737 from vedo import Points, show 1738 from vedo.pyplot import histogram 1739 1740 vpts1 = Points(np.random.rand(10_000, 3)) 1741 dists = vpts1.auto_distance() 1742 h1 = histogram(dists, xlim=(0,0.08)).clone2d() 1743 1744 vpts2 = vpts1.clone().relax_point_positions(n=100, iters=20, sub_iters=10) 1745 dists = vpts2.auto_distance() 1746 h2 = histogram(dists, xlim=(0,0.08)).clone2d() 1747 1748 show([[vpts1, h1], [vpts2, h2]], N=2).close() 1749 ``` 1750 """ 1751 smooth = vtki.new("PointSmoothingFilter") 1752 smooth.SetInputData(self.dataset) 1753 smooth.SetSmoothingModeToUniform() 1754 smooth.SetNumberOfIterations(iters) 1755 smooth.SetNumberOfSubIterations(sub_iters) 1756 smooth.SetPackingFactor(packing_factor) 1757 if self.point_locator: 1758 smooth.SetLocator(self.point_locator) 1759 if not max_step: 1760 max_step = self.diagonal_size() / 100 1761 smooth.SetMaximumStepSize(max_step) 1762 smooth.SetNeighborhoodSize(n) 1763 if constraints: 1764 fixed_angle = constraints.get("fixed_angle", 45) 1765 boundary_angle = constraints.get("boundary_angle", 110) 1766 smooth.EnableConstraintsOn() 1767 smooth.SetFixedAngle(fixed_angle) 1768 smooth.SetBoundaryAngle(boundary_angle) 1769 smooth.GenerateConstraintScalarsOn() 1770 smooth.GenerateConstraintNormalsOn() 1771 smooth.Update() 1772 self._update(smooth.GetOutput()) 1773 self.metadata["PackingRadius"] = smooth.GetPackingRadius() 1774 self.pipeline = utils.OperationNode("relax_point_positions", parents=[self]) 1775 return self 1776 1777 def smooth_mls_1d(self, f=0.2, radius=None, n=0) -> Self: 1778 """ 1779 Smooth mesh or points with a `Moving Least Squares` variant. 1780 The point data array "Variances" will contain the residue calculated for each point. 1781 1782 Arguments: 1783 f : (float) 1784 smoothing factor - typical range is [0,2]. 1785 radius : (float) 1786 radius search in absolute units. 1787 If set then `f` is ignored. 1788 n : (int) 1789 number of neighbours to be used for the fit. 1790 If set then `f` and `radius` are ignored. 1791 1792 Examples: 1793 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1794 - [skeletonize.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/skeletonize.py) 1795 1796 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1797 """ 1798 coords = self.vertices 1799 ncoords = len(coords) 1800 1801 if n: 1802 Ncp = n 1803 elif radius: 1804 Ncp = 1 1805 else: 1806 Ncp = int(ncoords * f / 10) 1807 if Ncp < 5: 1808 vedo.logger.warning(f"Please choose a fraction higher than {f}") 1809 Ncp = 5 1810 1811 variances, newline = [], [] 1812 for p in coords: 1813 points = self.closest_point(p, n=Ncp, radius=radius) 1814 if len(points) < 4: 1815 continue 1816 1817 points = np.array(points) 1818 pointsmean = points.mean(axis=0) # plane center 1819 _, dd, vv = np.linalg.svd(points - pointsmean) 1820 newp = np.dot(p - pointsmean, vv[0]) * vv[0] + pointsmean 1821 variances.append(dd[1] + dd[2]) 1822 newline.append(newp) 1823 1824 self.pointdata["Variances"] = np.array(variances).astype(np.float32) 1825 self.vertices = newline 1826 self.pipeline = utils.OperationNode("smooth_mls_1d", parents=[self]) 1827 return self 1828 1829 def smooth_mls_2d(self, f=0.2, radius=None, n=0) -> Self: 1830 """ 1831 Smooth mesh or points with a `Moving Least Squares` algorithm variant. 1832 1833 The `mesh.pointdata['MLSVariance']` array will contain the residue calculated for each point. 1834 When a radius is specified, points that are isolated will not be moved and will get 1835 a 0 entry in array `mesh.pointdata['MLSValidPoint']`. 1836 1837 Arguments: 1838 f : (float) 1839 smoothing factor - typical range is [0, 2]. 1840 radius : (float | array) 1841 radius search in absolute units. Can be single value (float) or sequence 1842 for adaptive smoothing. If set then `f` is ignored. 1843 n : (int) 1844 number of neighbours to be used for the fit. 1845 If set then `f` and `radius` are ignored. 1846 1847 Examples: 1848 - [moving_least_squares2D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares2D.py) 1849 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1850 1851 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1852 """ 1853 coords = self.vertices 1854 ncoords = len(coords) 1855 1856 if n: 1857 Ncp = n 1858 radius = None 1859 elif radius is not None: 1860 Ncp = 1 1861 else: 1862 Ncp = int(ncoords * f / 100) 1863 if Ncp < 4: 1864 vedo.logger.error(f"please choose a f-value higher than {f}") 1865 Ncp = 4 1866 1867 variances, newpts, valid = [], [], [] 1868 radius_is_sequence = utils.is_sequence(radius) 1869 1870 pb = None 1871 if ncoords > 10000: 1872 pb = utils.ProgressBar(0, ncoords, delay=3) 1873 1874 for i, p in enumerate(coords): 1875 if pb: 1876 pb.print("smooth_mls_2d working ...") 1877 1878 # if a radius was provided for each point 1879 if radius_is_sequence: 1880 pts = self.closest_point(p, n=Ncp, radius=radius[i]) 1881 else: 1882 pts = self.closest_point(p, n=Ncp, radius=radius) 1883 1884 if len(pts) > 3: 1885 ptsmean = pts.mean(axis=0) # plane center 1886 _, dd, vv = np.linalg.svd(pts - ptsmean) 1887 cv = np.cross(vv[0], vv[1]) 1888 t = (np.dot(cv, ptsmean) - np.dot(cv, p)) / np.dot(cv, cv) 1889 newpts.append(p + cv * t) 1890 variances.append(dd[2]) 1891 if radius is not None: 1892 valid.append(1) 1893 else: 1894 newpts.append(p) 1895 variances.append(0) 1896 if radius is not None: 1897 valid.append(0) 1898 1899 if radius is not None: 1900 self.pointdata["MLSValidPoint"] = np.array(valid).astype(np.uint8) 1901 self.pointdata["MLSVariance"] = np.array(variances).astype(np.float32) 1902 1903 self.vertices = newpts 1904 1905 self.pipeline = utils.OperationNode("smooth_mls_2d", parents=[self]) 1906 return self 1907 1908 def smooth_lloyd_2d(self, iterations=2, bounds=None, options="Qbb Qc Qx") -> Self: 1909 """ 1910 Lloyd relaxation of a 2D pointcloud. 1911 1912 Arguments: 1913 iterations : (int) 1914 number of iterations. 1915 bounds : (list) 1916 bounding box of the domain. 1917 options : (str) 1918 options for the Qhull algorithm. 1919 """ 1920 # Credits: https://hatarilabs.com/ih-en/ 1921 # tutorial-to-create-a-geospatial-voronoi-sh-mesh-with-python-scipy-and-geopandas 1922 from scipy.spatial import Voronoi as scipy_voronoi 1923 1924 def _constrain_points(points): 1925 # Update any points that have drifted beyond the boundaries of this space 1926 if bounds is not None: 1927 for point in points: 1928 if point[0] < bounds[0]: point[0] = bounds[0] 1929 if point[0] > bounds[1]: point[0] = bounds[1] 1930 if point[1] < bounds[2]: point[1] = bounds[2] 1931 if point[1] > bounds[3]: point[1] = bounds[3] 1932 return points 1933 1934 def _find_centroid(vertices): 1935 # The equation for the method used here to find the centroid of a 1936 # 2D polygon is given here: https://en.wikipedia.org/wiki/Centroid#Of_a_polygon 1937 area = 0 1938 centroid_x = 0 1939 centroid_y = 0 1940 for i in range(len(vertices) - 1): 1941 step = (vertices[i, 0] * vertices[i + 1, 1]) - (vertices[i + 1, 0] * vertices[i, 1]) 1942 centroid_x += (vertices[i, 0] + vertices[i + 1, 0]) * step 1943 centroid_y += (vertices[i, 1] + vertices[i + 1, 1]) * step 1944 area += step 1945 if area: 1946 centroid_x = (1.0 / (3.0 * area)) * centroid_x 1947 centroid_y = (1.0 / (3.0 * area)) * centroid_y 1948 # prevent centroids from escaping bounding box 1949 return _constrain_points([[centroid_x, centroid_y]])[0] 1950 1951 def _relax(voron): 1952 # Moves each point to the centroid of its cell in the voronoi 1953 # map to "relax" the points (i.e. jitter the points so as 1954 # to spread them out within the space). 1955 centroids = [] 1956 for idx in voron.point_region: 1957 # the region is a series of indices into voronoi.vertices 1958 # remove point at infinity, designated by index -1 1959 region = [i for i in voron.regions[idx] if i != -1] 1960 # enclose the polygon 1961 region = region + [region[0]] 1962 verts = voron.vertices[region] 1963 # find the centroid of those vertices 1964 centroids.append(_find_centroid(verts)) 1965 return _constrain_points(centroids) 1966 1967 if bounds is None: 1968 bounds = self.bounds() 1969 1970 pts = self.vertices[:, (0, 1)] 1971 for i in range(iterations): 1972 vor = scipy_voronoi(pts, qhull_options=options) 1973 _constrain_points(vor.vertices) 1974 pts = _relax(vor) 1975 out = Points(pts) 1976 out.name = "MeshSmoothLloyd2D" 1977 out.pipeline = utils.OperationNode("smooth_lloyd", parents=[self]) 1978 return out 1979 1980 def project_on_plane(self, plane="z", point=None, direction=None) -> Self: 1981 """ 1982 Project the mesh on one of the Cartesian planes. 1983 1984 Arguments: 1985 plane : (str, Plane) 1986 if plane is `str`, plane can be one of ['x', 'y', 'z'], 1987 represents x-plane, y-plane and z-plane, respectively. 1988 Otherwise, plane should be an instance of `vedo.shapes.Plane`. 1989 point : (float, array) 1990 if plane is `str`, point should be a float represents the intercept. 1991 Otherwise, point is the camera point of perspective projection 1992 direction : (array) 1993 direction of oblique projection 1994 1995 Note: 1996 Parameters `point` and `direction` are only used if the given plane 1997 is an instance of `vedo.shapes.Plane`. And one of these two params 1998 should be left as `None` to specify the projection type. 1999 2000 Example: 2001 ```python 2002 s.project_on_plane(plane='z') # project to z-plane 2003 plane = Plane(pos=(4, 8, -4), normal=(-1, 0, 1), s=(5,5)) 2004 s.project_on_plane(plane=plane) # orthogonal projection 2005 s.project_on_plane(plane=plane, point=(6, 6, 6)) # perspective projection 2006 s.project_on_plane(plane=plane, direction=(1, 2, -1)) # oblique projection 2007 ``` 2008 2009 Examples: 2010 - [silhouette2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette2.py) 2011 2012 ![](https://vedo.embl.es/images/basic/silhouette2.png) 2013 """ 2014 coords = self.vertices 2015 2016 if plane == "x": 2017 coords[:, 0] = self.transform.position[0] 2018 intercept = self.xbounds()[0] if point is None else point 2019 self.x(intercept) 2020 elif plane == "y": 2021 coords[:, 1] = self.transform.position[1] 2022 intercept = self.ybounds()[0] if point is None else point 2023 self.y(intercept) 2024 elif plane == "z": 2025 coords[:, 2] = self.transform.position[2] 2026 intercept = self.zbounds()[0] if point is None else point 2027 self.z(intercept) 2028 2029 elif isinstance(plane, vedo.shapes.Plane): 2030 normal = plane.normal / np.linalg.norm(plane.normal) 2031 pl = np.hstack((normal, -np.dot(plane.pos(), normal))).reshape(4, 1) 2032 if direction is None and point is None: 2033 # orthogonal projection 2034 pt = np.hstack((normal, [0])).reshape(4, 1) 2035 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T # python3 only 2036 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2037 2038 elif direction is None: 2039 # perspective projection 2040 pt = np.hstack((np.array(point), [1])).reshape(4, 1) 2041 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2042 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2043 2044 elif point is None: 2045 # oblique projection 2046 pt = np.hstack((np.array(direction), [0])).reshape(4, 1) 2047 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2048 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2049 2050 coords = np.concatenate([coords, np.ones((coords.shape[:-1] + (1,)))], axis=-1) 2051 # coords = coords @ proj_mat.T 2052 coords = np.matmul(coords, proj_mat.T) 2053 coords = coords[:, :3] / coords[:, 3:] 2054 2055 else: 2056 vedo.logger.error(f"unknown plane {plane}") 2057 raise RuntimeError() 2058 2059 self.alpha(0.1) 2060 self.vertices = coords 2061 return self 2062 2063 def warp(self, source, target, sigma=1.0, mode="3d") -> Self: 2064 """ 2065 "Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set 2066 of source and target landmarks. Any point on the mesh close to a source landmark will 2067 be moved to a place close to the corresponding target landmark. 2068 The points in between are interpolated smoothly using 2069 Bookstein's Thin Plate Spline algorithm. 2070 2071 Transformation object can be accessed with `mesh.transform`. 2072 2073 Arguments: 2074 sigma : (float) 2075 specify the 'stiffness' of the spline. 2076 mode : (str) 2077 set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes) 2078 2079 Examples: 2080 - [interpolate_field.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/interpolate_field.py) 2081 - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py) 2082 - [warp2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp2.py) 2083 - [warp3.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp3.py) 2084 - [warp4a.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4a.py) 2085 - [warp4b.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4b.py) 2086 - [warp6.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp6.py) 2087 2088 ![](https://vedo.embl.es/images/advanced/warp2.png) 2089 """ 2090 parents = [self] 2091 2092 try: 2093 source = source.vertices 2094 parents.append(source) 2095 except AttributeError: 2096 source = utils.make3d(source) 2097 2098 try: 2099 target = target.vertices 2100 parents.append(target) 2101 except AttributeError: 2102 target = utils.make3d(target) 2103 2104 ns = len(source) 2105 nt = len(target) 2106 if ns != nt: 2107 vedo.logger.error(f"#source {ns} != {nt} #target points") 2108 raise RuntimeError() 2109 2110 NLT = NonLinearTransform() 2111 NLT.source_points = source 2112 NLT.target_points = target 2113 self.apply_transform(NLT) 2114 2115 self.pipeline = utils.OperationNode("warp", parents=parents) 2116 return self 2117 2118 def cut_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0), invert=False) -> Self: 2119 """ 2120 Cut the mesh with the plane defined by a point and a normal. 2121 2122 Arguments: 2123 origin : (array) 2124 the cutting plane goes through this point 2125 normal : (array) 2126 normal of the cutting plane 2127 2128 Example: 2129 ```python 2130 from vedo import Cube 2131 cube = Cube().cut_with_plane(normal=(1,1,1)) 2132 cube.back_color('pink').show().close() 2133 ``` 2134 ![](https://vedo.embl.es/images/feats/cut_with_plane_cube.png) 2135 2136 Examples: 2137 - [trail.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/trail.py) 2138 2139 ![](https://vedo.embl.es/images/simulations/trail.gif) 2140 2141 Check out also: 2142 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`. 2143 """ 2144 s = str(normal) 2145 if "x" in s: 2146 normal = (1, 0, 0) 2147 if "-" in s: 2148 normal = -np.array(normal) 2149 elif "y" in s: 2150 normal = (0, 1, 0) 2151 if "-" in s: 2152 normal = -np.array(normal) 2153 elif "z" in s: 2154 normal = (0, 0, 1) 2155 if "-" in s: 2156 normal = -np.array(normal) 2157 plane = vtki.vtkPlane() 2158 plane.SetOrigin(origin) 2159 plane.SetNormal(normal) 2160 2161 clipper = vtki.new("ClipPolyData") 2162 clipper.SetInputData(self.dataset) 2163 clipper.SetClipFunction(plane) 2164 clipper.GenerateClippedOutputOff() 2165 clipper.GenerateClipScalarsOff() 2166 clipper.SetInsideOut(invert) 2167 clipper.SetValue(0) 2168 clipper.Update() 2169 2170 self._update(clipper.GetOutput()) 2171 2172 self.pipeline = utils.OperationNode("cut_with_plane", parents=[self]) 2173 return self 2174 2175 def cut_with_planes(self, origins, normals, invert=False) -> Self: 2176 """ 2177 Cut the mesh with a convex set of planes defined by points and normals. 2178 2179 Arguments: 2180 origins : (array) 2181 each cutting plane goes through this point 2182 normals : (array) 2183 normal of each of the cutting planes 2184 invert : (bool) 2185 if True, cut outside instead of inside 2186 2187 Check out also: 2188 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()` 2189 """ 2190 2191 vpoints = vtki.vtkPoints() 2192 for p in utils.make3d(origins): 2193 vpoints.InsertNextPoint(p) 2194 normals = utils.make3d(normals) 2195 2196 planes = vtki.vtkPlanes() 2197 planes.SetPoints(vpoints) 2198 planes.SetNormals(utils.numpy2vtk(normals, dtype=float)) 2199 2200 clipper = vtki.new("ClipPolyData") 2201 clipper.SetInputData(self.dataset) 2202 clipper.SetInsideOut(invert) 2203 clipper.SetClipFunction(planes) 2204 clipper.GenerateClippedOutputOff() 2205 clipper.GenerateClipScalarsOff() 2206 clipper.SetValue(0) 2207 clipper.Update() 2208 2209 self._update(clipper.GetOutput()) 2210 2211 self.pipeline = utils.OperationNode("cut_with_planes", parents=[self]) 2212 return self 2213 2214 def cut_with_box(self, bounds, invert=False) -> Self: 2215 """ 2216 Cut the current mesh with a box or a set of boxes. 2217 This is much faster than `cut_with_mesh()`. 2218 2219 Input `bounds` can be either: 2220 - a Mesh or Points object 2221 - a list of 6 number representing a bounding box `[xmin,xmax, ymin,ymax, zmin,zmax]` 2222 - a list of bounding boxes like the above: `[[xmin1,...], [xmin2,...], ...]` 2223 2224 Example: 2225 ```python 2226 from vedo import Sphere, Cube, show 2227 mesh = Sphere(r=1, res=50) 2228 box = Cube(side=1.5).wireframe() 2229 mesh.cut_with_box(box) 2230 show(mesh, box, axes=1).close() 2231 ``` 2232 ![](https://vedo.embl.es/images/feats/cut_with_box_cube.png) 2233 2234 Check out also: 2235 `cut_with_line()`, `cut_with_plane()`, `cut_with_cylinder()` 2236 """ 2237 if isinstance(bounds, Points): 2238 bounds = bounds.bounds() 2239 2240 box = vtki.new("Box") 2241 if utils.is_sequence(bounds[0]): 2242 for bs in bounds: 2243 box.AddBounds(bs) 2244 else: 2245 box.SetBounds(bounds) 2246 2247 clipper = vtki.new("ClipPolyData") 2248 clipper.SetInputData(self.dataset) 2249 clipper.SetClipFunction(box) 2250 clipper.SetInsideOut(not invert) 2251 clipper.GenerateClippedOutputOff() 2252 clipper.GenerateClipScalarsOff() 2253 clipper.SetValue(0) 2254 clipper.Update() 2255 self._update(clipper.GetOutput()) 2256 2257 self.pipeline = utils.OperationNode("cut_with_box", parents=[self]) 2258 return self 2259 2260 def cut_with_line(self, points, invert=False, closed=True) -> Self: 2261 """ 2262 Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter. 2263 The polyline is defined by a set of points (z-coordinates are ignored). 2264 This is much faster than `cut_with_mesh()`. 2265 2266 Check out also: 2267 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2268 """ 2269 pplane = vtki.new("PolyPlane") 2270 if isinstance(points, Points): 2271 points = points.vertices.tolist() 2272 2273 if closed: 2274 if isinstance(points, np.ndarray): 2275 points = points.tolist() 2276 points.append(points[0]) 2277 2278 vpoints = vtki.vtkPoints() 2279 for p in points: 2280 if len(p) == 2: 2281 p = [p[0], p[1], 0.0] 2282 vpoints.InsertNextPoint(p) 2283 2284 n = len(points) 2285 polyline = vtki.new("PolyLine") 2286 polyline.Initialize(n, vpoints) 2287 polyline.GetPointIds().SetNumberOfIds(n) 2288 for i in range(n): 2289 polyline.GetPointIds().SetId(i, i) 2290 pplane.SetPolyLine(polyline) 2291 2292 clipper = vtki.new("ClipPolyData") 2293 clipper.SetInputData(self.dataset) 2294 clipper.SetClipFunction(pplane) 2295 clipper.SetInsideOut(invert) 2296 clipper.GenerateClippedOutputOff() 2297 clipper.GenerateClipScalarsOff() 2298 clipper.SetValue(0) 2299 clipper.Update() 2300 self._update(clipper.GetOutput()) 2301 2302 self.pipeline = utils.OperationNode("cut_with_line", parents=[self]) 2303 return self 2304 2305 def cut_with_cookiecutter(self, lines) -> Self: 2306 """ 2307 Cut the current mesh with a single line or a set of lines. 2308 2309 Input `lines` can be either: 2310 - a `Mesh` or `Points` object 2311 - a list of 3D points: `[(x1,y1,z1), (x2,y2,z2), ...]` 2312 - a list of 2D points: `[(x1,y1), (x2,y2), ...]` 2313 2314 Example: 2315 ```python 2316 from vedo import * 2317 grid = Mesh(dataurl + "dolfin_fine.vtk") 2318 grid.compute_quality().cmap("Greens") 2319 pols = merge( 2320 Polygon(nsides=10, r=0.3).pos(0.7, 0.3), 2321 Polygon(nsides=10, r=0.2).pos(0.3, 0.7), 2322 ) 2323 lines = pols.boundaries() 2324 cgrid = grid.clone().cut_with_cookiecutter(lines) 2325 grid.alpha(0.1).wireframe() 2326 show(grid, cgrid, lines, axes=8, bg='blackboard').close() 2327 ``` 2328 ![](https://vedo.embl.es/images/feats/cookiecutter.png) 2329 2330 Check out also: 2331 `cut_with_line()` and `cut_with_point_loop()` 2332 2333 Note: 2334 In case of a warning message like: 2335 "Mesh and trim loop point data attributes are different" 2336 consider interpolating the mesh point data to the loop points, 2337 Eg. (in the above example): 2338 ```python 2339 lines = pols.boundaries().interpolate_data_from(grid, n=2) 2340 ``` 2341 2342 Note: 2343 trying to invert the selection by reversing the loop order 2344 will have no effect in this method, hence it does not have 2345 the `invert` option. 2346 """ 2347 if utils.is_sequence(lines): 2348 lines = utils.make3d(lines) 2349 iline = list(range(len(lines))) + [0] 2350 poly = utils.buildPolyData(lines, lines=[iline]) 2351 else: 2352 poly = lines.dataset 2353 2354 # if invert: # not working 2355 # rev = vtki.new("ReverseSense") 2356 # rev.ReverseCellsOn() 2357 # rev.SetInputData(poly) 2358 # rev.Update() 2359 # poly = rev.GetOutput() 2360 2361 # Build loops from the polyline 2362 build_loops = vtki.new("ContourLoopExtraction") 2363 build_loops.SetGlobalWarningDisplay(0) 2364 build_loops.SetInputData(poly) 2365 build_loops.Update() 2366 boundary_poly = build_loops.GetOutput() 2367 2368 ccut = vtki.new("CookieCutter") 2369 ccut.SetInputData(self.dataset) 2370 ccut.SetLoopsData(boundary_poly) 2371 ccut.SetPointInterpolationToMeshEdges() 2372 # ccut.SetPointInterpolationToLoopEdges() 2373 ccut.PassCellDataOn() 2374 ccut.PassPointDataOn() 2375 ccut.Update() 2376 self._update(ccut.GetOutput()) 2377 2378 self.pipeline = utils.OperationNode("cut_with_cookiecutter", parents=[self]) 2379 return self 2380 2381 def cut_with_cylinder(self, center=(0, 0, 0), axis=(0, 0, 1), r=1, invert=False) -> Self: 2382 """ 2383 Cut the current mesh with an infinite cylinder. 2384 This is much faster than `cut_with_mesh()`. 2385 2386 Arguments: 2387 center : (array) 2388 the center of the cylinder 2389 normal : (array) 2390 direction of the cylinder axis 2391 r : (float) 2392 radius of the cylinder 2393 2394 Example: 2395 ```python 2396 from vedo import Disc, show 2397 disc = Disc(r1=1, r2=1.2) 2398 mesh = disc.extrude(3, res=50).linewidth(1) 2399 mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True) 2400 show(mesh, axes=1).close() 2401 ``` 2402 ![](https://vedo.embl.es/images/feats/cut_with_cylinder.png) 2403 2404 Examples: 2405 - [optics_main1.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/optics_main1.py) 2406 2407 Check out also: 2408 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2409 """ 2410 s = str(axis) 2411 if "x" in s: 2412 axis = (1, 0, 0) 2413 elif "y" in s: 2414 axis = (0, 1, 0) 2415 elif "z" in s: 2416 axis = (0, 0, 1) 2417 cyl = vtki.new("Cylinder") 2418 cyl.SetCenter(center) 2419 cyl.SetAxis(axis[0], axis[1], axis[2]) 2420 cyl.SetRadius(r) 2421 2422 clipper = vtki.new("ClipPolyData") 2423 clipper.SetInputData(self.dataset) 2424 clipper.SetClipFunction(cyl) 2425 clipper.SetInsideOut(not invert) 2426 clipper.GenerateClippedOutputOff() 2427 clipper.GenerateClipScalarsOff() 2428 clipper.SetValue(0) 2429 clipper.Update() 2430 self._update(clipper.GetOutput()) 2431 2432 self.pipeline = utils.OperationNode("cut_with_cylinder", parents=[self]) 2433 return self 2434 2435 def cut_with_sphere(self, center=(0, 0, 0), r=1.0, invert=False) -> Self: 2436 """ 2437 Cut the current mesh with an sphere. 2438 This is much faster than `cut_with_mesh()`. 2439 2440 Arguments: 2441 center : (array) 2442 the center of the sphere 2443 r : (float) 2444 radius of the sphere 2445 2446 Example: 2447 ```python 2448 from vedo import Disc, show 2449 disc = Disc(r1=1, r2=1.2) 2450 mesh = disc.extrude(3, res=50).linewidth(1) 2451 mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True) 2452 show(mesh, axes=1).close() 2453 ``` 2454 ![](https://vedo.embl.es/images/feats/cut_with_sphere.png) 2455 2456 Check out also: 2457 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2458 """ 2459 sph = vtki.new("Sphere") 2460 sph.SetCenter(center) 2461 sph.SetRadius(r) 2462 2463 clipper = vtki.new("ClipPolyData") 2464 clipper.SetInputData(self.dataset) 2465 clipper.SetClipFunction(sph) 2466 clipper.SetInsideOut(not invert) 2467 clipper.GenerateClippedOutputOff() 2468 clipper.GenerateClipScalarsOff() 2469 clipper.SetValue(0) 2470 clipper.Update() 2471 self._update(clipper.GetOutput()) 2472 self.pipeline = utils.OperationNode("cut_with_sphere", parents=[self]) 2473 return self 2474 2475 def cut_with_mesh(self, mesh, invert=False, keep=False) -> Union[Self, "vedo.Assembly"]: 2476 """ 2477 Cut an `Mesh` mesh with another `Mesh`. 2478 2479 Use `invert` to invert the selection. 2480 2481 Use `keep` to keep the cutoff part, in this case an `Assembly` is returned: 2482 the "cut" object and the "discarded" part of the original object. 2483 You can access both via `assembly.unpack()` method. 2484 2485 Example: 2486 ```python 2487 from vedo import * 2488 arr = np.random.randn(100000, 3)/2 2489 pts = Points(arr).c('red3').pos(5,0,0) 2490 cube = Cube().pos(4,0.5,0) 2491 assem = pts.cut_with_mesh(cube, keep=True) 2492 show(assem.unpack(), axes=1).close() 2493 ``` 2494 ![](https://vedo.embl.es/images/feats/cut_with_mesh.png) 2495 2496 Check out also: 2497 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2498 """ 2499 polymesh = mesh.dataset 2500 poly = self.dataset 2501 2502 # Create an array to hold distance information 2503 signed_distances = vtki.vtkFloatArray() 2504 signed_distances.SetNumberOfComponents(1) 2505 signed_distances.SetName("SignedDistances") 2506 2507 # implicit function that will be used to slice the mesh 2508 ippd = vtki.new("ImplicitPolyDataDistance") 2509 ippd.SetInput(polymesh) 2510 2511 # Evaluate the signed distance function at all of the grid points 2512 for pointId in range(poly.GetNumberOfPoints()): 2513 p = poly.GetPoint(pointId) 2514 signed_distance = ippd.EvaluateFunction(p) 2515 signed_distances.InsertNextValue(signed_distance) 2516 2517 currentscals = poly.GetPointData().GetScalars() 2518 if currentscals: 2519 currentscals = currentscals.GetName() 2520 2521 poly.GetPointData().AddArray(signed_distances) 2522 poly.GetPointData().SetActiveScalars("SignedDistances") 2523 2524 clipper = vtki.new("ClipPolyData") 2525 clipper.SetInputData(poly) 2526 clipper.SetInsideOut(not invert) 2527 clipper.SetGenerateClippedOutput(keep) 2528 clipper.SetValue(0.0) 2529 clipper.Update() 2530 cpoly = clipper.GetOutput() 2531 2532 if keep: 2533 kpoly = clipper.GetOutput(1) 2534 2535 vis = False 2536 if currentscals: 2537 cpoly.GetPointData().SetActiveScalars(currentscals) 2538 vis = self.mapper.GetScalarVisibility() 2539 2540 self._update(cpoly) 2541 2542 self.pointdata.remove("SignedDistances") 2543 self.mapper.SetScalarVisibility(vis) 2544 if keep: 2545 if isinstance(self, vedo.Mesh): 2546 cutoff = vedo.Mesh(kpoly) 2547 else: 2548 cutoff = vedo.Points(kpoly) 2549 # cutoff = self.__class__(kpoly) # this does not work properly 2550 cutoff.properties = vtki.vtkProperty() 2551 cutoff.properties.DeepCopy(self.properties) 2552 cutoff.actor.SetProperty(cutoff.properties) 2553 cutoff.c("k5").alpha(0.2) 2554 return vedo.Assembly([self, cutoff]) 2555 2556 self.pipeline = utils.OperationNode("cut_with_mesh", parents=[self, mesh]) 2557 return self 2558 2559 def cut_with_point_loop( 2560 self, points, invert=False, on="points", include_boundary=False 2561 ) -> Self: 2562 """ 2563 Cut an `Mesh` object with a set of points forming a closed loop. 2564 2565 Arguments: 2566 invert : (bool) 2567 invert selection (inside-out) 2568 on : (str) 2569 if 'cells' will extract the whole cells lying inside (or outside) the point loop 2570 include_boundary : (bool) 2571 include cells lying exactly on the boundary line. Only relevant on 'cells' mode 2572 2573 Examples: 2574 - [cut_with_points1.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points1.py) 2575 2576 ![](https://vedo.embl.es/images/advanced/cutWithPoints1.png) 2577 2578 - [cut_with_points2.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points2.py) 2579 2580 ![](https://vedo.embl.es/images/advanced/cutWithPoints2.png) 2581 """ 2582 if isinstance(points, Points): 2583 parents = [points] 2584 vpts = points.dataset.GetPoints() 2585 points = points.vertices 2586 else: 2587 parents = [self] 2588 vpts = vtki.vtkPoints() 2589 points = utils.make3d(points) 2590 for p in points: 2591 vpts.InsertNextPoint(p) 2592 2593 if "cell" in on: 2594 ippd = vtki.new("ImplicitSelectionLoop") 2595 ippd.SetLoop(vpts) 2596 ippd.AutomaticNormalGenerationOn() 2597 clipper = vtki.new("ExtractPolyDataGeometry") 2598 clipper.SetInputData(self.dataset) 2599 clipper.SetImplicitFunction(ippd) 2600 clipper.SetExtractInside(not invert) 2601 clipper.SetExtractBoundaryCells(include_boundary) 2602 else: 2603 spol = vtki.new("SelectPolyData") 2604 spol.SetLoop(vpts) 2605 spol.GenerateSelectionScalarsOn() 2606 spol.GenerateUnselectedOutputOff() 2607 spol.SetInputData(self.dataset) 2608 spol.Update() 2609 clipper = vtki.new("ClipPolyData") 2610 clipper.SetInputData(spol.GetOutput()) 2611 clipper.SetInsideOut(not invert) 2612 clipper.SetValue(0.0) 2613 clipper.Update() 2614 self._update(clipper.GetOutput()) 2615 2616 self.pipeline = utils.OperationNode("cut_with_pointloop", parents=parents) 2617 return self 2618 2619 def cut_with_scalar(self, value: float, name="", invert=False) -> Self: 2620 """ 2621 Cut a mesh or point cloud with some input scalar point-data. 2622 2623 Arguments: 2624 value : (float) 2625 cutting value 2626 name : (str) 2627 array name of the scalars to be used 2628 invert : (bool) 2629 flip selection 2630 2631 Example: 2632 ```python 2633 from vedo import * 2634 s = Sphere().lw(1) 2635 pts = s.vertices 2636 scalars = np.sin(3*pts[:,2]) + pts[:,0] 2637 s.pointdata["somevalues"] = scalars 2638 s.cut_with_scalar(0.3) 2639 s.cmap("Spectral", "somevalues").add_scalarbar() 2640 s.show(axes=1).close() 2641 ``` 2642 ![](https://vedo.embl.es/images/feats/cut_with_scalars.png) 2643 """ 2644 if name: 2645 self.pointdata.select(name) 2646 clipper = vtki.new("ClipPolyData") 2647 clipper.SetInputData(self.dataset) 2648 clipper.SetValue(value) 2649 clipper.GenerateClippedOutputOff() 2650 clipper.SetInsideOut(not invert) 2651 clipper.Update() 2652 self._update(clipper.GetOutput()) 2653 self.pipeline = utils.OperationNode("cut_with_scalar", parents=[self]) 2654 return self 2655 2656 def crop(self, 2657 top=None, bottom=None, right=None, left=None, front=None, back=None, 2658 bounds=()) -> Self: 2659 """ 2660 Crop an `Mesh` object. 2661 2662 Arguments: 2663 top : (float) 2664 fraction to crop from the top plane (positive z) 2665 bottom : (float) 2666 fraction to crop from the bottom plane (negative z) 2667 front : (float) 2668 fraction to crop from the front plane (positive y) 2669 back : (float) 2670 fraction to crop from the back plane (negative y) 2671 right : (float) 2672 fraction to crop from the right plane (positive x) 2673 left : (float) 2674 fraction to crop from the left plane (negative x) 2675 bounds : (list) 2676 bounding box of the crop region as `[x0,x1, y0,y1, z0,z1]` 2677 2678 Example: 2679 ```python 2680 from vedo import Sphere 2681 Sphere().crop(right=0.3, left=0.1).show() 2682 ``` 2683 ![](https://user-images.githubusercontent.com/32848391/57081955-0ef1e800-6cf6-11e9-99de-b45220939bc9.png) 2684 """ 2685 if not len(bounds): 2686 pos = np.array(self.pos()) 2687 x0, x1, y0, y1, z0, z1 = self.bounds() 2688 x0, y0, z0 = [x0, y0, z0] - pos 2689 x1, y1, z1 = [x1, y1, z1] - pos 2690 2691 dx, dy, dz = x1 - x0, y1 - y0, z1 - z0 2692 if top: 2693 z1 = z1 - top * dz 2694 if bottom: 2695 z0 = z0 + bottom * dz 2696 if front: 2697 y1 = y1 - front * dy 2698 if back: 2699 y0 = y0 + back * dy 2700 if right: 2701 x1 = x1 - right * dx 2702 if left: 2703 x0 = x0 + left * dx 2704 bounds = (x0, x1, y0, y1, z0, z1) 2705 2706 cu = vtki.new("Box") 2707 cu.SetBounds(bounds) 2708 2709 clipper = vtki.new("ClipPolyData") 2710 clipper.SetInputData(self.dataset) 2711 clipper.SetClipFunction(cu) 2712 clipper.InsideOutOn() 2713 clipper.GenerateClippedOutputOff() 2714 clipper.GenerateClipScalarsOff() 2715 clipper.SetValue(0) 2716 clipper.Update() 2717 self._update(clipper.GetOutput()) 2718 2719 self.pipeline = utils.OperationNode( 2720 "crop", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 2721 ) 2722 return self 2723 2724 def generate_surface_halo( 2725 self, 2726 distance=0.05, 2727 res=(50, 50, 50), 2728 bounds=(), 2729 maxdist=None, 2730 ) -> "vedo.Mesh": 2731 """ 2732 Generate the surface halo which sits at the specified distance from the input one. 2733 2734 Arguments: 2735 distance : (float) 2736 distance from the input surface 2737 res : (int) 2738 resolution of the surface 2739 bounds : (list) 2740 bounding box of the surface 2741 maxdist : (float) 2742 maximum distance to generate the surface 2743 """ 2744 if not bounds: 2745 bounds = self.bounds() 2746 2747 if not maxdist: 2748 maxdist = self.diagonal_size() / 2 2749 2750 imp = vtki.new("ImplicitModeller") 2751 imp.SetInputData(self.dataset) 2752 imp.SetSampleDimensions(res) 2753 if maxdist: 2754 imp.SetMaximumDistance(maxdist) 2755 if len(bounds) == 6: 2756 imp.SetModelBounds(bounds) 2757 contour = vtki.new("ContourFilter") 2758 contour.SetInputConnection(imp.GetOutputPort()) 2759 contour.SetValue(0, distance) 2760 contour.Update() 2761 out = vedo.Mesh(contour.GetOutput()) 2762 out.c("lightblue").alpha(0.25).lighting("off") 2763 out.pipeline = utils.OperationNode("generate_surface_halo", parents=[self]) 2764 return out 2765 2766 def generate_mesh( 2767 self, 2768 line_resolution=None, 2769 mesh_resolution=None, 2770 smooth=0.0, 2771 jitter=0.001, 2772 grid=None, 2773 quads=False, 2774 invert=False, 2775 ) -> Self: 2776 """ 2777 Generate a polygonal Mesh from a closed contour line. 2778 If line is not closed it will be closed with a straight segment. 2779 2780 Check also `generate_delaunay2d()`. 2781 2782 Arguments: 2783 line_resolution : (int) 2784 resolution of the contour line. The default is None, in this case 2785 the contour is not resampled. 2786 mesh_resolution : (int) 2787 resolution of the internal triangles not touching the boundary. 2788 smooth : (float) 2789 smoothing of the contour before meshing. 2790 jitter : (float) 2791 add a small noise to the internal points. 2792 grid : (Grid) 2793 manually pass a Grid object. The default is True. 2794 quads : (bool) 2795 generate a mesh of quads instead of triangles. 2796 invert : (bool) 2797 flip the line orientation. The default is False. 2798 2799 Examples: 2800 - [line2mesh_tri.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_tri.py) 2801 2802 ![](https://vedo.embl.es/images/advanced/line2mesh_tri.jpg) 2803 2804 - [line2mesh_quads.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_quads.py) 2805 2806 ![](https://vedo.embl.es/images/advanced/line2mesh_quads.png) 2807 """ 2808 if line_resolution is None: 2809 contour = vedo.shapes.Line(self.vertices) 2810 else: 2811 contour = vedo.shapes.Spline(self.vertices, smooth=smooth, res=line_resolution) 2812 contour.clean() 2813 2814 length = contour.length() 2815 density = length / contour.npoints 2816 # print(f"tomesh():\n\tline length = {length}") 2817 # print(f"\tdensity = {density} length/pt_separation") 2818 2819 x0, x1 = contour.xbounds() 2820 y0, y1 = contour.ybounds() 2821 2822 if grid is None: 2823 if mesh_resolution is None: 2824 resx = int((x1 - x0) / density + 0.5) 2825 resy = int((y1 - y0) / density + 0.5) 2826 # print(f"tmesh_resolution = {[resx, resy]}") 2827 else: 2828 if utils.is_sequence(mesh_resolution): 2829 resx, resy = mesh_resolution 2830 else: 2831 resx, resy = mesh_resolution, mesh_resolution 2832 grid = vedo.shapes.Grid( 2833 [(x0 + x1) / 2, (y0 + y1) / 2, 0], 2834 s=((x1 - x0) * 1.025, (y1 - y0) * 1.025), 2835 res=(resx, resy), 2836 ) 2837 else: 2838 grid = grid.clone() 2839 2840 cpts = contour.vertices 2841 2842 # make sure it's closed 2843 p0, p1 = cpts[0], cpts[-1] 2844 nj = max(2, int(utils.mag(p1 - p0) / density + 0.5)) 2845 joinline = vedo.shapes.Line(p1, p0, res=nj) 2846 contour = vedo.merge(contour, joinline).subsample(0.0001) 2847 2848 ####################################### quads 2849 if quads: 2850 cmesh = grid.clone().cut_with_point_loop(contour, on="cells", invert=invert) 2851 cmesh.wireframe(False).lw(0.5) 2852 cmesh.pipeline = utils.OperationNode( 2853 "generate_mesh", 2854 parents=[self, contour], 2855 comment=f"#quads {cmesh.dataset.GetNumberOfCells()}", 2856 ) 2857 return cmesh 2858 ############################################# 2859 2860 grid_tmp = grid.vertices.copy() 2861 2862 if jitter: 2863 np.random.seed(0) 2864 sigma = 1.0 / np.sqrt(grid.npoints) * grid.diagonal_size() * jitter 2865 # print(f"\tsigma jittering = {sigma}") 2866 grid_tmp += np.random.rand(grid.npoints, 3) * sigma 2867 grid_tmp[:, 2] = 0.0 2868 2869 todel = [] 2870 density /= np.sqrt(3) 2871 vgrid_tmp = Points(grid_tmp) 2872 2873 for p in contour.vertices: 2874 out = vgrid_tmp.closest_point(p, radius=density, return_point_id=True) 2875 todel += out.tolist() 2876 2877 grid_tmp = grid_tmp.tolist() 2878 for index in sorted(list(set(todel)), reverse=True): 2879 del grid_tmp[index] 2880 2881 points = contour.vertices.tolist() + grid_tmp 2882 if invert: 2883 boundary = list(reversed(range(contour.npoints))) 2884 else: 2885 boundary = list(range(contour.npoints)) 2886 2887 dln = Points(points).generate_delaunay2d(mode="xy", boundaries=[boundary]) 2888 dln.compute_normals(points=False) # fixes reversd faces 2889 dln.lw(1) 2890 2891 dln.pipeline = utils.OperationNode( 2892 "generate_mesh", 2893 parents=[self, contour], 2894 comment=f"#cells {dln.dataset.GetNumberOfCells()}", 2895 ) 2896 return dln 2897 2898 def reconstruct_surface( 2899 self, 2900 dims=(100, 100, 100), 2901 radius=None, 2902 sample_size=None, 2903 hole_filling=True, 2904 bounds=(), 2905 padding=0.05, 2906 ) -> "vedo.Mesh": 2907 """ 2908 Surface reconstruction from a scattered cloud of points. 2909 2910 Arguments: 2911 dims : (int) 2912 number of voxels in x, y and z to control precision. 2913 radius : (float) 2914 radius of influence of each point. 2915 Smaller values generally improve performance markedly. 2916 Note that after the signed distance function is computed, 2917 any voxel taking on the value >= radius 2918 is presumed to be "unseen" or uninitialized. 2919 sample_size : (int) 2920 if normals are not present 2921 they will be calculated using this sample size per point. 2922 hole_filling : (bool) 2923 enables hole filling, this generates 2924 separating surfaces between the empty and unseen portions of the volume. 2925 bounds : (list) 2926 region in space in which to perform the sampling 2927 in format (xmin,xmax, ymin,ymax, zim, zmax) 2928 padding : (float) 2929 increase by this fraction the bounding box 2930 2931 Examples: 2932 - [recosurface.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/recosurface.py) 2933 2934 ![](https://vedo.embl.es/images/advanced/recosurface.png) 2935 """ 2936 if not utils.is_sequence(dims): 2937 dims = (dims, dims, dims) 2938 2939 sdf = vtki.new("SignedDistance") 2940 2941 if len(bounds) == 6: 2942 sdf.SetBounds(bounds) 2943 else: 2944 x0, x1, y0, y1, z0, z1 = self.bounds() 2945 sdf.SetBounds( 2946 x0 - (x1 - x0) * padding, 2947 x1 + (x1 - x0) * padding, 2948 y0 - (y1 - y0) * padding, 2949 y1 + (y1 - y0) * padding, 2950 z0 - (z1 - z0) * padding, 2951 z1 + (z1 - z0) * padding, 2952 ) 2953 2954 bb = sdf.GetBounds() 2955 if bb[0]==bb[1]: 2956 vedo.logger.warning("reconstruct_surface(): zero x-range") 2957 if bb[2]==bb[3]: 2958 vedo.logger.warning("reconstruct_surface(): zero y-range") 2959 if bb[4]==bb[5]: 2960 vedo.logger.warning("reconstruct_surface(): zero z-range") 2961 2962 pd = self.dataset 2963 2964 if pd.GetPointData().GetNormals(): 2965 sdf.SetInputData(pd) 2966 else: 2967 normals = vtki.new("PCANormalEstimation") 2968 normals.SetInputData(pd) 2969 if not sample_size: 2970 sample_size = int(pd.GetNumberOfPoints() / 50) 2971 normals.SetSampleSize(sample_size) 2972 normals.SetNormalOrientationToGraphTraversal() 2973 sdf.SetInputConnection(normals.GetOutputPort()) 2974 # print("Recalculating normals with sample size =", sample_size) 2975 2976 if radius is None: 2977 radius = self.diagonal_size() / (sum(dims) / 3) * 5 2978 # print("Calculating mesh from points with radius =", radius) 2979 2980 sdf.SetRadius(radius) 2981 sdf.SetDimensions(dims) 2982 sdf.Update() 2983 2984 surface = vtki.new("ExtractSurface") 2985 surface.SetRadius(radius * 0.99) 2986 surface.SetHoleFilling(hole_filling) 2987 surface.ComputeNormalsOff() 2988 surface.ComputeGradientsOff() 2989 surface.SetInputConnection(sdf.GetOutputPort()) 2990 surface.Update() 2991 m = vedo.mesh.Mesh(surface.GetOutput(), c=self.color()) 2992 2993 m.pipeline = utils.OperationNode( 2994 "reconstruct_surface", 2995 parents=[self], 2996 comment=f"#pts {m.dataset.GetNumberOfPoints()}", 2997 ) 2998 return m 2999 3000 def compute_clustering(self, radius: float) -> Self: 3001 """ 3002 Cluster points in space. The `radius` is the radius of local search. 3003 3004 An array named "ClusterId" is added to `pointdata`. 3005 3006 Examples: 3007 - [clustering.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/clustering.py) 3008 3009 ![](https://vedo.embl.es/images/basic/clustering.png) 3010 """ 3011 cluster = vtki.new("EuclideanClusterExtraction") 3012 cluster.SetInputData(self.dataset) 3013 cluster.SetExtractionModeToAllClusters() 3014 cluster.SetRadius(radius) 3015 cluster.ColorClustersOn() 3016 cluster.Update() 3017 idsarr = cluster.GetOutput().GetPointData().GetArray("ClusterId") 3018 self.dataset.GetPointData().AddArray(idsarr) 3019 self.pipeline = utils.OperationNode( 3020 "compute_clustering", parents=[self], comment=f"radius = {radius}" 3021 ) 3022 return self 3023 3024 def compute_connections(self, radius, mode=0, regions=(), vrange=(0, 1), seeds=(), angle=0.0) -> Self: 3025 """ 3026 Extracts and/or segments points from a point cloud based on geometric distance measures 3027 (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range. 3028 The default operation is to segment the points into "connected" regions where the connection 3029 is determined by an appropriate distance measure. Each region is given a region id. 3030 3031 Optionally, the filter can output the largest connected region of points; a particular region 3032 (via id specification); those regions that are seeded using a list of input point ids; 3033 or the region of points closest to a specified position. 3034 3035 The key parameter of this filter is the radius defining a sphere around each point which defines 3036 a local neighborhood: any other points in the local neighborhood are assumed connected to the point. 3037 Note that the radius is defined in absolute terms. 3038 3039 Other parameters are used to further qualify what it means to be a neighboring point. 3040 For example, scalar range and/or point normals can be used to further constrain the neighborhood. 3041 Also the extraction mode defines how the filter operates. 3042 By default, all regions are extracted but it is possible to extract particular regions; 3043 the region closest to a seed point; seeded regions; or the largest region found while processing. 3044 By default, all regions are extracted. 3045 3046 On output, all points are labeled with a region number. 3047 However note that the number of input and output points may not be the same: 3048 if not extracting all regions then the output size may be less than the input size. 3049 3050 Arguments: 3051 radius : (float) 3052 variable specifying a local sphere used to define local point neighborhood 3053 mode : (int) 3054 - 0, Extract all regions 3055 - 1, Extract point seeded regions 3056 - 2, Extract largest region 3057 - 3, Test specified regions 3058 - 4, Extract all regions with scalar connectivity 3059 - 5, Extract point seeded regions 3060 regions : (list) 3061 a list of non-negative regions id to extract 3062 vrange : (list) 3063 scalar range to use to extract points based on scalar connectivity 3064 seeds : (list) 3065 a list of non-negative point seed ids 3066 angle : (list) 3067 points are connected if the angle between their normals is 3068 within this angle threshold (expressed in degrees). 3069 """ 3070 # https://vtk.org/doc/nightly/html/classvtkConnectedPointsFilter.html 3071 cpf = vtki.new("ConnectedPointsFilter") 3072 cpf.SetInputData(self.dataset) 3073 cpf.SetRadius(radius) 3074 if mode == 0: # Extract all regions 3075 pass 3076 3077 elif mode == 1: # Extract point seeded regions 3078 cpf.SetExtractionModeToPointSeededRegions() 3079 for s in seeds: 3080 cpf.AddSeed(s) 3081 3082 elif mode == 2: # Test largest region 3083 cpf.SetExtractionModeToLargestRegion() 3084 3085 elif mode == 3: # Test specified regions 3086 cpf.SetExtractionModeToSpecifiedRegions() 3087 for r in regions: 3088 cpf.AddSpecifiedRegion(r) 3089 3090 elif mode == 4: # Extract all regions with scalar connectivity 3091 cpf.SetExtractionModeToLargestRegion() 3092 cpf.ScalarConnectivityOn() 3093 cpf.SetScalarRange(vrange[0], vrange[1]) 3094 3095 elif mode == 5: # Extract point seeded regions 3096 cpf.SetExtractionModeToLargestRegion() 3097 cpf.ScalarConnectivityOn() 3098 cpf.SetScalarRange(vrange[0], vrange[1]) 3099 cpf.AlignedNormalsOn() 3100 cpf.SetNormalAngle(angle) 3101 3102 cpf.Update() 3103 self._update(cpf.GetOutput(), reset_locators=False) 3104 return self 3105 3106 def compute_camera_distance(self) -> np.ndarray: 3107 """ 3108 Calculate the distance from points to the camera. 3109 3110 A pointdata array is created with name 'DistanceToCamera' and returned. 3111 """ 3112 if vedo.plotter_instance and vedo.plotter_instance.renderer: 3113 poly = self.dataset 3114 dc = vtki.new("DistanceToCamera") 3115 dc.SetInputData(poly) 3116 dc.SetRenderer(vedo.plotter_instance.renderer) 3117 dc.Update() 3118 self._update(dc.GetOutput(), reset_locators=False) 3119 return self.pointdata["DistanceToCamera"] 3120 return np.array([]) 3121 3122 def densify(self, target_distance=0.1, nclosest=6, radius=None, niter=1, nmax=None) -> Self: 3123 """ 3124 Return a copy of the cloud with new added points. 3125 The new points are created in such a way that all points in any local neighborhood are 3126 within a target distance of one another. 3127 3128 For each input point, the distance to all points in its neighborhood is computed. 3129 If any of its neighbors is further than the target distance, 3130 the edge connecting the point and its neighbor is bisected and 3131 a new point is inserted at the bisection point. 3132 A single pass is completed once all the input points are visited. 3133 Then the process repeats to the number of iterations. 3134 3135 Examples: 3136 - [densifycloud.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/densifycloud.py) 3137 3138 ![](https://vedo.embl.es/images/volumetric/densifycloud.png) 3139 3140 .. note:: 3141 Points will be created in an iterative fashion until all points in their 3142 local neighborhood are the target distance apart or less. 3143 Note that the process may terminate early due to the 3144 number of iterations. By default the target distance is set to 0.5. 3145 Note that the target_distance should be less than the radius 3146 or nothing will change on output. 3147 3148 .. warning:: 3149 This class can generate a lot of points very quickly. 3150 The maximum number of iterations is by default set to =1.0 for this reason. 3151 Increase the number of iterations very carefully. 3152 Also, `nmax` can be set to limit the explosion of points. 3153 It is also recommended that a N closest neighborhood is used. 3154 3155 """ 3156 src = vtki.new("ProgrammableSource") 3157 opts = self.vertices 3158 # zeros = np.zeros(3) 3159 3160 def _read_points(): 3161 output = src.GetPolyDataOutput() 3162 points = vtki.vtkPoints() 3163 for p in opts: 3164 # print(p) 3165 # if not np.array_equal(p, zeros): 3166 points.InsertNextPoint(p) 3167 output.SetPoints(points) 3168 3169 src.SetExecuteMethod(_read_points) 3170 3171 dens = vtki.new("DensifyPointCloudFilter") 3172 dens.SetInputConnection(src.GetOutputPort()) 3173 # dens.SetInputData(self.dataset) # this does not work 3174 dens.InterpolateAttributeDataOn() 3175 dens.SetTargetDistance(target_distance) 3176 dens.SetMaximumNumberOfIterations(niter) 3177 if nmax: 3178 dens.SetMaximumNumberOfPoints(nmax) 3179 3180 if radius: 3181 dens.SetNeighborhoodTypeToRadius() 3182 dens.SetRadius(radius) 3183 elif nclosest: 3184 dens.SetNeighborhoodTypeToNClosest() 3185 dens.SetNumberOfClosestPoints(nclosest) 3186 else: 3187 vedo.logger.error("set either radius or nclosest") 3188 raise RuntimeError() 3189 dens.Update() 3190 3191 cld = Points(dens.GetOutput()) 3192 cld.copy_properties_from(self) 3193 cld.interpolate_data_from(self, n=nclosest, radius=radius) 3194 cld.name = "DensifiedCloud" 3195 cld.pipeline = utils.OperationNode( 3196 "densify", 3197 parents=[self], 3198 c="#e9c46a:", 3199 comment=f"#pts {cld.dataset.GetNumberOfPoints()}", 3200 ) 3201 return cld 3202 3203 ############################################################################### 3204 ## stuff returning a Volume 3205 ############################################################################### 3206 3207 def density( 3208 self, dims=(40, 40, 40), bounds=None, radius=None, compute_gradient=False, locator=None 3209 ) -> "vedo.Volume": 3210 """ 3211 Generate a density field from a point cloud. Input can also be a set of 3D coordinates. 3212 Output is a `Volume`. 3213 3214 The local neighborhood is specified as the `radius` around each sample position (each voxel). 3215 If left to None, the radius is automatically computed as the diagonal of the bounding box 3216 and can be accessed via `vol.metadata["radius"]`. 3217 The density is expressed as the number of counts in the radius search. 3218 3219 Arguments: 3220 dims : (int, list) 3221 number of voxels in x, y and z of the output Volume. 3222 compute_gradient : (bool) 3223 Turn on/off the generation of the gradient vector, 3224 gradient magnitude scalar, and function classification scalar. 3225 By default this is off. Note that this will increase execution time 3226 and the size of the output. (The names of these point data arrays are: 3227 "Gradient", "Gradient Magnitude", and "Classification") 3228 locator : (vtkPointLocator) 3229 can be assigned from a previous call for speed (access it via `object.point_locator`). 3230 3231 Examples: 3232 - [plot_density3d.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/plot_density3d.py) 3233 3234 ![](https://vedo.embl.es/images/pyplot/plot_density3d.png) 3235 """ 3236 pdf = vtki.new("PointDensityFilter") 3237 pdf.SetInputData(self.dataset) 3238 3239 if not utils.is_sequence(dims): 3240 dims = [dims, dims, dims] 3241 3242 if bounds is None: 3243 bounds = list(self.bounds()) 3244 elif len(bounds) == 4: 3245 bounds = [*bounds, 0, 0] 3246 3247 if bounds[5] - bounds[4] == 0 or len(dims) == 2: # its 2D 3248 dims = list(dims) 3249 dims = [dims[0], dims[1], 2] 3250 diag = self.diagonal_size() 3251 bounds[5] = bounds[4] + diag / 1000 3252 pdf.SetModelBounds(bounds) 3253 3254 pdf.SetSampleDimensions(dims) 3255 3256 if locator: 3257 pdf.SetLocator(locator) 3258 3259 pdf.SetDensityEstimateToFixedRadius() 3260 if radius is None: 3261 radius = self.diagonal_size() / 20 3262 pdf.SetRadius(radius) 3263 pdf.SetComputeGradient(compute_gradient) 3264 pdf.Update() 3265 3266 vol = vedo.Volume(pdf.GetOutput()).mode(1) 3267 vol.name = "PointDensity" 3268 vol.metadata["radius"] = radius 3269 vol.locator = pdf.GetLocator() 3270 vol.pipeline = utils.OperationNode( 3271 "density", parents=[self], comment=f"dims={tuple(vol.dimensions())}" 3272 ) 3273 return vol 3274 3275 3276 def tovolume( 3277 self, 3278 kernel="shepard", 3279 radius=None, 3280 n=None, 3281 bounds=None, 3282 null_value=None, 3283 dims=(25, 25, 25), 3284 ) -> "vedo.Volume": 3285 """ 3286 Generate a `Volume` by interpolating a scalar 3287 or vector field which is only known on a scattered set of points or mesh. 3288 Available interpolation kernels are: shepard, gaussian, or linear. 3289 3290 Arguments: 3291 kernel : (str) 3292 interpolation kernel type [shepard] 3293 radius : (float) 3294 radius of the local search 3295 n : (int) 3296 number of point to use for interpolation 3297 bounds : (list) 3298 bounding box of the output Volume object 3299 dims : (list) 3300 dimensions of the output Volume object 3301 null_value : (float) 3302 value to be assigned to invalid points 3303 3304 Examples: 3305 - [interpolate_volume.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/interpolate_volume.py) 3306 3307 ![](https://vedo.embl.es/images/volumetric/59095175-1ec5a300-8918-11e9-8bc0-fd35c8981e2b.jpg) 3308 """ 3309 if radius is None and not n: 3310 vedo.logger.error("please set either radius or n") 3311 raise RuntimeError 3312 3313 poly = self.dataset 3314 3315 # Create a probe volume 3316 probe = vtki.vtkImageData() 3317 probe.SetDimensions(dims) 3318 if bounds is None: 3319 bounds = self.bounds() 3320 probe.SetOrigin(bounds[0], bounds[2], bounds[4]) 3321 probe.SetSpacing( 3322 (bounds[1] - bounds[0]) / dims[0], 3323 (bounds[3] - bounds[2]) / dims[1], 3324 (bounds[5] - bounds[4]) / dims[2], 3325 ) 3326 3327 if not self.point_locator: 3328 self.point_locator = vtki.new("PointLocator") 3329 self.point_locator.SetDataSet(poly) 3330 self.point_locator.BuildLocator() 3331 3332 if kernel == "shepard": 3333 kern = vtki.new("ShepardKernel") 3334 kern.SetPowerParameter(2) 3335 elif kernel == "gaussian": 3336 kern = vtki.new("GaussianKernel") 3337 elif kernel == "linear": 3338 kern = vtki.new("LinearKernel") 3339 else: 3340 vedo.logger.error("Error in tovolume(), available kernels are:") 3341 vedo.logger.error(" [shepard, gaussian, linear]") 3342 raise RuntimeError() 3343 3344 if radius: 3345 kern.SetRadius(radius) 3346 3347 interpolator = vtki.new("PointInterpolator") 3348 interpolator.SetInputData(probe) 3349 interpolator.SetSourceData(poly) 3350 interpolator.SetKernel(kern) 3351 interpolator.SetLocator(self.point_locator) 3352 3353 if n: 3354 kern.SetNumberOfPoints(n) 3355 kern.SetKernelFootprintToNClosest() 3356 else: 3357 kern.SetRadius(radius) 3358 3359 if null_value is not None: 3360 interpolator.SetNullValue(null_value) 3361 else: 3362 interpolator.SetNullPointsStrategyToClosestPoint() 3363 interpolator.Update() 3364 3365 vol = vedo.Volume(interpolator.GetOutput()) 3366 3367 vol.pipeline = utils.OperationNode( 3368 "signed_distance", 3369 parents=[self], 3370 comment=f"dims={tuple(vol.dimensions())}", 3371 c="#e9c46a:#0096c7", 3372 ) 3373 return vol 3374 3375 ################################################################################# 3376 def generate_segments(self, istart=0, rmax=1e30, niter=3) -> "vedo.shapes.Lines": 3377 """ 3378 Generate a line segments from a set of points. 3379 The algorithm is based on the closest point search. 3380 3381 Returns a `Line` object. 3382 This object contains the a metadata array of used vertex counts in "UsedVertexCount" 3383 and the sum of the length of the segments in "SegmentsLengthSum". 3384 3385 Arguments: 3386 istart : (int) 3387 index of the starting point 3388 rmax : (float) 3389 maximum length of a segment 3390 niter : (int) 3391 number of iterations or passes through the points 3392 3393 Examples: 3394 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 3395 """ 3396 points = self.vertices 3397 segments = [] 3398 dists = [] 3399 n = len(points) 3400 used = np.zeros(n, dtype=int) 3401 for _ in range(niter): 3402 i = istart 3403 for _ in range(n): 3404 p = points[i] 3405 ids = self.closest_point(p, n=4, return_point_id=True) 3406 j = ids[1] 3407 if used[j] > 1 or [j, i] in segments: 3408 j = ids[2] 3409 if used[j] > 1: 3410 j = ids[3] 3411 d = np.linalg.norm(p - points[j]) 3412 if used[j] > 1 or used[i] > 1 or d > rmax: 3413 i += 1 3414 if i >= n: 3415 i = 0 3416 continue 3417 used[i] += 1 3418 used[j] += 1 3419 segments.append([i, j]) 3420 dists.append(d) 3421 i = j 3422 segments = np.array(segments, dtype=int) 3423 3424 lines = vedo.shapes.Lines(points[segments], c="k", lw=3) 3425 lines.metadata["UsedVertexCount"] = used 3426 lines.metadata["SegmentsLengthSum"] = np.sum(dists) 3427 lines.pipeline = utils.OperationNode("generate_segments", parents=[self]) 3428 lines.name = "Segments" 3429 return lines 3430 3431 def generate_delaunay2d( 3432 self, 3433 mode="scipy", 3434 boundaries=(), 3435 tol=None, 3436 alpha=0.0, 3437 offset=0.0, 3438 transform=None, 3439 ) -> "vedo.mesh.Mesh": 3440 """ 3441 Create a mesh from points in the XY plane. 3442 If `mode='fit'` then the filter computes a best fitting 3443 plane and projects the points onto it. 3444 3445 Check also `generate_mesh()`. 3446 3447 Arguments: 3448 tol : (float) 3449 specify a tolerance to control discarding of closely spaced points. 3450 This tolerance is specified as a fraction of the diagonal length of the bounding box of the points. 3451 alpha : (float) 3452 for a non-zero alpha value, only edges or triangles contained 3453 within a sphere centered at mesh vertices will be output. 3454 Otherwise, only triangles will be output. 3455 offset : (float) 3456 multiplier to control the size of the initial, bounding Delaunay triangulation. 3457 transform: (LinearTransform, NonLinearTransform) 3458 a transformation which is applied to points to generate a 2D problem. 3459 This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane. 3460 The points are transformed and triangulated. 3461 The topology of triangulated points is used as the output topology. 3462 3463 Examples: 3464 - [delaunay2d.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delaunay2d.py) 3465 3466 ![](https://vedo.embl.es/images/basic/delaunay2d.png) 3467 """ 3468 plist = self.vertices.copy() 3469 3470 ######################################################### 3471 if mode == "scipy": 3472 from scipy.spatial import Delaunay as scipy_delaunay 3473 3474 tri = scipy_delaunay(plist[:, 0:2]) 3475 return vedo.mesh.Mesh([plist, tri.simplices]) 3476 ########################################################## 3477 3478 pd = vtki.vtkPolyData() 3479 vpts = vtki.vtkPoints() 3480 vpts.SetData(utils.numpy2vtk(plist, dtype=np.float32)) 3481 pd.SetPoints(vpts) 3482 3483 delny = vtki.new("Delaunay2D") 3484 delny.SetInputData(pd) 3485 if tol: 3486 delny.SetTolerance(tol) 3487 delny.SetAlpha(alpha) 3488 delny.SetOffset(offset) 3489 3490 if transform: 3491 delny.SetTransform(transform.T) 3492 elif mode == "fit": 3493 delny.SetProjectionPlaneMode(vtki.get_class("VTK_BEST_FITTING_PLANE")) 3494 elif mode == "xy" and boundaries: 3495 boundary = vtki.vtkPolyData() 3496 boundary.SetPoints(vpts) 3497 cell_array = vtki.vtkCellArray() 3498 for b in boundaries: 3499 cpolygon = vtki.vtkPolygon() 3500 for idd in b: 3501 cpolygon.GetPointIds().InsertNextId(idd) 3502 cell_array.InsertNextCell(cpolygon) 3503 boundary.SetPolys(cell_array) 3504 delny.SetSourceData(boundary) 3505 3506 delny.Update() 3507 3508 msh = vedo.mesh.Mesh(delny.GetOutput()) 3509 msh.name = "Delaunay2D" 3510 msh.clean().lighting("off") 3511 msh.pipeline = utils.OperationNode( 3512 "delaunay2d", 3513 parents=[self], 3514 comment=f"#cells {msh.dataset.GetNumberOfCells()}", 3515 ) 3516 return msh 3517 3518 def generate_voronoi(self, padding=0.0, fit=False, method="vtk") -> "vedo.Mesh": 3519 """ 3520 Generate the 2D Voronoi convex tiling of the input points (z is ignored). 3521 The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon. 3522 3523 A cell array named "VoronoiID" is added to the output Mesh. 3524 3525 The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest 3526 to one of the input points. Voronoi tessellations are important in computational geometry 3527 (and many other fields), and are the dual of Delaunay triangulations. 3528 3529 Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored 3530 (although carried through to the output). 3531 If you desire to triangulate in a different plane, you can use fit=True. 3532 3533 A brief summary is as follows. Each (generating) input point is associated with 3534 an initial Voronoi tile, which is simply the bounding box of the point set. 3535 A locator is then used to identify nearby points: each neighbor in turn generates a 3536 clipping line positioned halfway between the generating point and the neighboring point, 3537 and orthogonal to the line connecting them. Clips are readily performed by evaluationg the 3538 vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line. 3539 If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip 3540 line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs, 3541 the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region 3542 containing the neighboring clip points. The clip region (along with the points contained in it) is grown 3543 by careful expansion (e.g., outward spiraling iterator over all candidate clip points). 3544 When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi 3545 tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi 3546 tessellation. Note that topological and geometric information is used to generate a valid triangulation 3547 (e.g., merging points and validating topology). 3548 3549 Arguments: 3550 pts : (list) 3551 list of input points. 3552 padding : (float) 3553 padding distance. The default is 0. 3554 fit : (bool) 3555 detect automatically the best fitting plane. The default is False. 3556 3557 Examples: 3558 - [voronoi1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi1.py) 3559 3560 ![](https://vedo.embl.es/images/basic/voronoi1.png) 3561 3562 - [voronoi2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi2.py) 3563 3564 ![](https://vedo.embl.es/images/advanced/voronoi2.png) 3565 """ 3566 pts = self.vertices 3567 3568 if method == "scipy": 3569 from scipy.spatial import Voronoi as scipy_voronoi 3570 3571 pts = np.asarray(pts)[:, (0, 1)] 3572 vor = scipy_voronoi(pts) 3573 regs = [] # filter out invalid indices 3574 for r in vor.regions: 3575 flag = True 3576 for x in r: 3577 if x < 0: 3578 flag = False 3579 break 3580 if flag and len(r) > 0: 3581 regs.append(r) 3582 3583 m = vedo.Mesh([vor.vertices, regs]) 3584 m.celldata["VoronoiID"] = np.array(list(range(len(regs)))).astype(int) 3585 3586 elif method == "vtk": 3587 vor = vtki.new("Voronoi2D") 3588 if isinstance(pts, Points): 3589 vor.SetInputData(pts) 3590 else: 3591 pts = np.asarray(pts) 3592 if pts.shape[1] == 2: 3593 pts = np.c_[pts, np.zeros(len(pts))] 3594 pd = vtki.vtkPolyData() 3595 vpts = vtki.vtkPoints() 3596 vpts.SetData(utils.numpy2vtk(pts, dtype=np.float32)) 3597 pd.SetPoints(vpts) 3598 vor.SetInputData(pd) 3599 vor.SetPadding(padding) 3600 vor.SetGenerateScalarsToPointIds() 3601 if fit: 3602 vor.SetProjectionPlaneModeToBestFittingPlane() 3603 else: 3604 vor.SetProjectionPlaneModeToXYPlane() 3605 vor.Update() 3606 poly = vor.GetOutput() 3607 arr = poly.GetCellData().GetArray(0) 3608 if arr: 3609 arr.SetName("VoronoiID") 3610 m = vedo.Mesh(poly, c="orange5") 3611 3612 else: 3613 vedo.logger.error(f"Unknown method {method} in voronoi()") 3614 raise RuntimeError 3615 3616 m.lw(2).lighting("off").wireframe() 3617 m.name = "Voronoi" 3618 return m 3619 3620 ########################################################################## 3621 def generate_delaunay3d(self, radius=0, tol=None) -> "vedo.TetMesh": 3622 """ 3623 Create 3D Delaunay triangulation of input points. 3624 3625 Arguments: 3626 radius : (float) 3627 specify distance (or "alpha") value to control output. 3628 For a non-zero values, only tetra contained within the circumsphere 3629 will be output. 3630 tol : (float) 3631 Specify a tolerance to control discarding of closely spaced points. 3632 This tolerance is specified as a fraction of the diagonal length of 3633 the bounding box of the points. 3634 """ 3635 deln = vtki.new("Delaunay3D") 3636 deln.SetInputData(self.dataset) 3637 deln.SetAlpha(radius) 3638 deln.AlphaTetsOn() 3639 deln.AlphaTrisOff() 3640 deln.AlphaLinesOff() 3641 deln.AlphaVertsOff() 3642 deln.BoundingTriangulationOff() 3643 if tol: 3644 deln.SetTolerance(tol) 3645 deln.Update() 3646 m = vedo.TetMesh(deln.GetOutput()) 3647 m.pipeline = utils.OperationNode( 3648 "generate_delaunay3d", c="#e9c46a:#edabab", parents=[self], 3649 ) 3650 m.name = "Delaunay3D" 3651 return m 3652 3653 #################################################### 3654 def visible_points(self, area=(), tol=None, invert=False) -> Union[Self, None]: 3655 """ 3656 Extract points based on whether they are visible or not. 3657 Visibility is determined by accessing the z-buffer of a rendering window. 3658 The position of each input point is converted into display coordinates, 3659 and then the z-value at that point is obtained. 3660 If within the user-specified tolerance, the point is considered visible. 3661 Associated data attributes are passed to the output as well. 3662 3663 This filter also allows you to specify a rectangular window in display (pixel) 3664 coordinates in which the visible points must lie. 3665 3666 Arguments: 3667 area : (list) 3668 specify a rectangular region as (xmin,xmax,ymin,ymax) 3669 tol : (float) 3670 a tolerance in normalized display coordinate system 3671 invert : (bool) 3672 select invisible points instead. 3673 3674 Example: 3675 ```python 3676 from vedo import Ellipsoid, show 3677 s = Ellipsoid().rotate_y(30) 3678 3679 # Camera options: pos, focal_point, viewup, distance 3680 camopts = dict(pos=(0,0,25), focal_point=(0,0,0)) 3681 show(s, camera=camopts, offscreen=True) 3682 3683 m = s.visible_points() 3684 # print('visible pts:', m.vertices) # numpy array 3685 show(m, new=True, axes=1).close() # optionally draw result in a new window 3686 ``` 3687 ![](https://vedo.embl.es/images/feats/visible_points.png) 3688 """ 3689 svp = vtki.new("SelectVisiblePoints") 3690 svp.SetInputData(self.dataset) 3691 3692 ren = None 3693 if vedo.plotter_instance: 3694 if vedo.plotter_instance.renderer: 3695 ren = vedo.plotter_instance.renderer 3696 svp.SetRenderer(ren) 3697 if not ren: 3698 vedo.logger.warning( 3699 "visible_points() can only be used after a rendering step" 3700 ) 3701 return None 3702 3703 if len(area) == 2: 3704 area = utils.flatten(area) 3705 if len(area) == 4: 3706 # specify a rectangular region 3707 svp.SetSelection(area[0], area[1], area[2], area[3]) 3708 if tol is not None: 3709 svp.SetTolerance(tol) 3710 if invert: 3711 svp.SelectInvisibleOn() 3712 svp.Update() 3713 3714 m = Points(svp.GetOutput()) 3715 m.name = "VisiblePoints" 3716 return m
Work with point clouds.
470 def __init__(self, inputobj=None, r=4, c=(0.2, 0.2, 0.2), alpha=1): 471 """ 472 Build an object made of only vertex points for a list of 2D/3D points. 473 Both shapes (N, 3) or (3, N) are accepted as input, if N>3. 474 475 Arguments: 476 inputobj : (list, tuple) 477 r : (int) 478 Point radius in units of pixels. 479 c : (str, list) 480 Color name or rgb tuple. 481 alpha : (float) 482 Transparency in range [0,1]. 483 484 Example: 485 ```python 486 from vedo import * 487 488 def fibonacci_sphere(n): 489 s = np.linspace(0, n, num=n, endpoint=False) 490 theta = s * 2.399963229728653 491 y = 1 - s * (2/(n-1)) 492 r = np.sqrt(1 - y * y) 493 x = np.cos(theta) * r 494 z = np.sin(theta) * r 495 return np._c[x,y,z] 496 497 Points(fibonacci_sphere(1000)).show(axes=1).close() 498 ``` 499 ![](https://vedo.embl.es/images/feats/fibonacci.png) 500 """ 501 # print("INIT POINTS") 502 super().__init__() 503 504 self.name = "" 505 self.filename = "" 506 self.file_size = "" 507 508 self.info = {} 509 self.time = time.time() 510 511 self.transform = LinearTransform() 512 self.point_locator = None 513 self.cell_locator = None 514 self.line_locator = None 515 516 self.actor = vtki.vtkActor() 517 self.properties = self.actor.GetProperty() 518 self.properties_backface = self.actor.GetBackfaceProperty() 519 self.mapper = vtki.new("PolyDataMapper") 520 self.dataset = vtki.vtkPolyData() 521 522 # Create weakref so actor can access this object (eg to pick/remove): 523 self.actor.retrieve_object = weak_ref_to(self) 524 525 try: 526 self.properties.RenderPointsAsSpheresOn() 527 except AttributeError: 528 pass 529 530 if inputobj is None: #################### 531 return 532 ########################################## 533 534 self.name = "Points" 535 536 ###### 537 if isinstance(inputobj, vtki.vtkActor): 538 self.dataset.DeepCopy(inputobj.GetMapper().GetInput()) 539 pr = vtki.vtkProperty() 540 pr.DeepCopy(inputobj.GetProperty()) 541 self.actor.SetProperty(pr) 542 self.properties = pr 543 self.mapper.SetScalarVisibility(inputobj.GetMapper().GetScalarVisibility()) 544 545 elif isinstance(inputobj, vtki.vtkPolyData): 546 self.dataset = inputobj 547 if self.dataset.GetNumberOfCells() == 0: 548 carr = vtki.vtkCellArray() 549 for i in range(self.dataset.GetNumberOfPoints()): 550 carr.InsertNextCell(1) 551 carr.InsertCellPoint(i) 552 self.dataset.SetVerts(carr) 553 554 elif isinstance(inputobj, Points): 555 self.dataset = inputobj.dataset 556 self.copy_properties_from(inputobj) 557 558 elif utils.is_sequence(inputobj): # passing point coords 559 self.dataset = utils.buildPolyData(utils.make3d(inputobj)) 560 561 elif isinstance(inputobj, str): 562 verts = vedo.file_io.load(inputobj) 563 self.filename = inputobj 564 self.dataset = verts.dataset 565 566 elif "meshlib" in str(type(inputobj)): 567 from meshlib import mrmeshnumpy as mn 568 self.dataset = utils.buildPolyData(mn.toNumpyArray(inputobj.points)) 569 570 else: 571 # try to extract the points from a generic VTK input data object 572 if hasattr(inputobj, "dataset"): 573 inputobj = inputobj.dataset 574 try: 575 vvpts = inputobj.GetPoints() 576 self.dataset = vtki.vtkPolyData() 577 self.dataset.SetPoints(vvpts) 578 for i in range(inputobj.GetPointData().GetNumberOfArrays()): 579 arr = inputobj.GetPointData().GetArray(i) 580 self.dataset.GetPointData().AddArray(arr) 581 except: 582 vedo.logger.error(f"cannot build Points from type {type(inputobj)}") 583 raise RuntimeError() 584 585 self.actor.SetMapper(self.mapper) 586 self.mapper.SetInputData(self.dataset) 587 588 self.properties.SetColor(colors.get_color(c)) 589 self.properties.SetOpacity(alpha) 590 self.properties.SetRepresentationToPoints() 591 self.properties.SetPointSize(r) 592 self.properties.LightingOff() 593 594 self.pipeline = utils.OperationNode( 595 self, parents=[], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 596 )
Build an object made of only vertex points for a list of 2D/3D points. Both shapes (N, 3) or (3, N) are accepted as input, if N>3.
Arguments:
- inputobj : (list, tuple)
- r : (int) Point radius in units of pixels.
- c : (str, list) Color name or rgb tuple.
- alpha : (float) Transparency in range [0,1].
Example:
from vedo import * def fibonacci_sphere(n): s = np.linspace(0, n, num=n, endpoint=False) theta = s * 2.399963229728653 y = 1 - s * (2/(n-1)) r = np.sqrt(1 - y * y) x = np.cos(theta) * r z = np.sin(theta) * r return np._c[x,y,z] Points(fibonacci_sphere(1000)).show(axes=1).close()
818 def polydata(self, **kwargs): 819 """ 820 Obsolete. Use property `.dataset` instead. 821 Returns the underlying `vtkPolyData` object. 822 """ 823 colors.printc( 824 "WARNING: call to .polydata() is obsolete, use property .dataset instead.", 825 c="y") 826 return self.dataset
Obsolete. Use property .dataset
instead.
Returns the underlying vtkPolyData
object.
834 def copy(self, deep=True) -> Self: 835 """Return a copy of the object. Alias of `clone()`.""" 836 return self.clone(deep=deep)
Return a copy of the object. Alias of clone()
.
838 def clone(self, deep=True) -> Self: 839 """ 840 Clone a `PointCloud` or `Mesh` object to make an exact copy of it. 841 Alias of `copy()`. 842 843 Arguments: 844 deep : (bool) 845 if False return a shallow copy of the mesh without copying the points array. 846 847 Examples: 848 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 849 850 ![](https://vedo.embl.es/images/basic/mirror.png) 851 """ 852 poly = vtki.vtkPolyData() 853 if deep or isinstance(deep, dict): # if a memo object is passed this checks as True 854 poly.DeepCopy(self.dataset) 855 else: 856 poly.ShallowCopy(self.dataset) 857 858 if isinstance(self, vedo.Mesh): 859 cloned = vedo.Mesh(poly) 860 else: 861 cloned = Points(poly) 862 # print([self], self.__class__) 863 # cloned = self.__class__(poly) 864 865 cloned.transform = self.transform.clone() 866 867 cloned.copy_properties_from(self) 868 869 cloned.name = str(self.name) 870 cloned.filename = str(self.filename) 871 cloned.info = dict(self.info) 872 cloned.pipeline = utils.OperationNode("clone", parents=[self], shape="diamond", c="#edede9") 873 874 if isinstance(deep, dict): 875 deep[id(self)] = cloned 876 877 return cloned
879 def compute_normals_with_pca(self, n=20, orientation_point=None, invert=False) -> Self: 880 """ 881 Generate point normals using PCA (principal component analysis). 882 This algorithm estimates a local tangent plane around each sample point p 883 by considering a small neighborhood of points around p, and fitting a plane 884 to the neighborhood (via PCA). 885 886 Arguments: 887 n : (int) 888 neighborhood size to calculate the normal 889 orientation_point : (list) 890 adjust the +/- sign of the normals so that 891 the normals all point towards a specified point. If None, perform a traversal 892 of the point cloud and flip neighboring normals so that they are mutually consistent. 893 invert : (bool) 894 flip all normals 895 """ 896 poly = self.dataset 897 pcan = vtki.new("PCANormalEstimation") 898 pcan.SetInputData(poly) 899 pcan.SetSampleSize(n) 900 901 if orientation_point is not None: 902 pcan.SetNormalOrientationToPoint() 903 pcan.SetOrientationPoint(orientation_point) 904 else: 905 pcan.SetNormalOrientationToGraphTraversal() 906 907 if invert: 908 pcan.FlipNormalsOn() 909 pcan.Update() 910 911 varr = pcan.GetOutput().GetPointData().GetNormals() 912 varr.SetName("Normals") 913 self.dataset.GetPointData().SetNormals(varr) 914 self.dataset.GetPointData().Modified() 915 return self
Generate point normals using PCA (principal component analysis). This algorithm estimates a local tangent plane around each sample point p by considering a small neighborhood of points around p, and fitting a plane to the neighborhood (via PCA).
Arguments:
- n : (int) neighborhood size to calculate the normal
- orientation_point : (list) adjust the +/- sign of the normals so that the normals all point towards a specified point. If None, perform a traversal of the point cloud and flip neighboring normals so that they are mutually consistent.
- invert : (bool) flip all normals
917 def compute_acoplanarity(self, n=25, radius=None, on="points") -> Self: 918 """ 919 Compute acoplanarity which is a measure of how much a local region of the mesh 920 differs from a plane. 921 922 The information is stored in a `pointdata` or `celldata` array with name 'Acoplanarity'. 923 924 Either `n` (number of neighbour points) or `radius` (radius of local search) can be specified. 925 If a radius value is given and not enough points fall inside it, then a -1 is stored. 926 927 Example: 928 ```python 929 from vedo import * 930 msh = ParametricShape('RandomHills') 931 msh.compute_acoplanarity(radius=0.1, on='cells') 932 msh.cmap("coolwarm", on='cells').add_scalarbar() 933 msh.show(axes=1).close() 934 ``` 935 ![](https://vedo.embl.es/images/feats/acoplanarity.jpg) 936 """ 937 acoplanarities = [] 938 if "point" in on: 939 pts = self.vertices 940 elif "cell" in on: 941 pts = self.cell_centers 942 else: 943 raise ValueError(f"In compute_acoplanarity() set on to either 'cells' or 'points', not {on}") 944 945 for p in utils.progressbar(pts, delay=5, width=15, title=f"{on} acoplanarity"): 946 if n: 947 data = self.closest_point(p, n=n) 948 npts = n 949 elif radius: 950 data = self.closest_point(p, radius=radius) 951 npts = len(data) 952 953 try: 954 center = data.mean(axis=0) 955 res = np.linalg.svd(data - center) 956 acoplanarities.append(res[1][2] / npts) 957 except: 958 acoplanarities.append(-1.0) 959 960 if "point" in on: 961 self.pointdata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 962 else: 963 self.celldata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 964 return self
Compute acoplanarity which is a measure of how much a local region of the mesh differs from a plane.
The information is stored in a pointdata
or celldata
array with name 'Acoplanarity'.
Either n
(number of neighbour points) or radius
(radius of local search) can be specified.
If a radius value is given and not enough points fall inside it, then a -1 is stored.
Example:
from vedo import * msh = ParametricShape('RandomHills') msh.compute_acoplanarity(radius=0.1, on='cells') msh.cmap("coolwarm", on='cells').add_scalarbar() msh.show(axes=1).close()
966 def distance_to(self, pcloud, signed=False, invert=False, name="Distance") -> np.ndarray: 967 """ 968 Computes the distance from one point cloud or mesh to another point cloud or mesh. 969 This new `pointdata` array is saved with default name "Distance". 970 971 Keywords `signed` and `invert` are used to compute signed distance, 972 but the mesh in that case must have polygonal faces (not a simple point cloud), 973 and normals must also be computed. 974 975 Examples: 976 - [distance2mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/distance2mesh.py) 977 978 ![](https://vedo.embl.es/images/basic/distance2mesh.png) 979 """ 980 if pcloud.dataset.GetNumberOfPolys(): 981 982 poly1 = self.dataset 983 poly2 = pcloud.dataset 984 df = vtki.new("DistancePolyDataFilter") 985 df.ComputeSecondDistanceOff() 986 df.SetInputData(0, poly1) 987 df.SetInputData(1, poly2) 988 df.SetSignedDistance(signed) 989 df.SetNegateDistance(invert) 990 df.Update() 991 scals = df.GetOutput().GetPointData().GetScalars() 992 dists = utils.vtk2numpy(scals) 993 994 else: # has no polygons 995 996 if signed: 997 vedo.logger.warning("distance_to() called with signed=True but input object has no polygons") 998 999 if not pcloud.point_locator: 1000 pcloud.point_locator = vtki.new("PointLocator") 1001 pcloud.point_locator.SetDataSet(pcloud.dataset) 1002 pcloud.point_locator.BuildLocator() 1003 1004 ids = [] 1005 ps1 = self.vertices 1006 ps2 = pcloud.vertices 1007 for p in ps1: 1008 pid = pcloud.point_locator.FindClosestPoint(p) 1009 ids.append(pid) 1010 1011 deltas = ps2[ids] - ps1 1012 dists = np.linalg.norm(deltas, axis=1).astype(np.float32) 1013 scals = utils.numpy2vtk(dists) 1014 1015 scals.SetName(name) 1016 self.dataset.GetPointData().AddArray(scals) 1017 self.dataset.GetPointData().SetActiveScalars(scals.GetName()) 1018 rng = scals.GetRange() 1019 self.mapper.SetScalarRange(rng[0], rng[1]) 1020 self.mapper.ScalarVisibilityOn() 1021 1022 self.pipeline = utils.OperationNode( 1023 "distance_to", 1024 parents=[self, pcloud], 1025 shape="cylinder", 1026 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1027 ) 1028 return dists
Computes the distance from one point cloud or mesh to another point cloud or mesh.
This new pointdata
array is saved with default name "Distance".
Keywords signed
and invert
are used to compute signed distance,
but the mesh in that case must have polygonal faces (not a simple point cloud),
and normals must also be computed.
Examples:
1030 def clean(self) -> Self: 1031 """Clean pointcloud or mesh by removing coincident points.""" 1032 cpd = vtki.new("CleanPolyData") 1033 cpd.PointMergingOn() 1034 cpd.ConvertLinesToPointsOff() 1035 cpd.ConvertPolysToLinesOff() 1036 cpd.ConvertStripsToPolysOff() 1037 cpd.SetInputData(self.dataset) 1038 cpd.Update() 1039 self._update(cpd.GetOutput()) 1040 self.pipeline = utils.OperationNode( 1041 "clean", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1042 ) 1043 return self
Clean pointcloud or mesh by removing coincident points.
1045 def subsample(self, fraction: float, absolute=False) -> Self: 1046 """ 1047 Subsample a point cloud by requiring that the points 1048 or vertices are far apart at least by the specified fraction of the object size. 1049 If a Mesh is passed the polygonal faces are not removed 1050 but holes can appear as their vertices are removed. 1051 1052 Examples: 1053 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1054 1055 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1056 1057 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1058 1059 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1060 """ 1061 if not absolute: 1062 if fraction > 1: 1063 vedo.logger.warning( 1064 f"subsample(fraction=...), fraction must be < 1, but is {fraction}" 1065 ) 1066 if fraction <= 0: 1067 return self 1068 1069 cpd = vtki.new("CleanPolyData") 1070 cpd.PointMergingOn() 1071 cpd.ConvertLinesToPointsOn() 1072 cpd.ConvertPolysToLinesOn() 1073 cpd.ConvertStripsToPolysOn() 1074 cpd.SetInputData(self.dataset) 1075 if absolute: 1076 cpd.SetTolerance(fraction / self.diagonal_size()) 1077 # cpd.SetToleranceIsAbsolute(absolute) 1078 else: 1079 cpd.SetTolerance(fraction) 1080 cpd.Update() 1081 1082 ps = 2 1083 if self.properties.GetRepresentation() == 0: 1084 ps = self.properties.GetPointSize() 1085 1086 self._update(cpd.GetOutput()) 1087 self.ps(ps) 1088 1089 self.pipeline = utils.OperationNode( 1090 "subsample", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1091 ) 1092 return self
Subsample a point cloud by requiring that the points or vertices are far apart at least by the specified fraction of the object size. If a Mesh is passed the polygonal faces are not removed but holes can appear as their vertices are removed.
Examples:
1094 def threshold(self, scalars: str, above=None, below=None, on="points") -> Self: 1095 """ 1096 Extracts cells where scalar value satisfies threshold criterion. 1097 1098 Arguments: 1099 scalars : (str) 1100 name of the scalars array. 1101 above : (float) 1102 minimum value of the scalar 1103 below : (float) 1104 maximum value of the scalar 1105 on : (str) 1106 if 'cells' assume array of scalars refers to cell data. 1107 1108 Examples: 1109 - [mesh_threshold.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_threshold.py) 1110 """ 1111 thres = vtki.new("Threshold") 1112 thres.SetInputData(self.dataset) 1113 1114 if on.startswith("c"): 1115 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_CELLS 1116 else: 1117 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_POINTS 1118 1119 thres.SetInputArrayToProcess(0, 0, 0, asso, scalars) 1120 1121 if above is None and below is not None: 1122 try: # vtk 9.2 1123 thres.ThresholdByLower(below) 1124 except AttributeError: # vtk 9.3 1125 thres.SetUpperThreshold(below) 1126 1127 elif below is None and above is not None: 1128 try: 1129 thres.ThresholdByUpper(above) 1130 except AttributeError: 1131 thres.SetLowerThreshold(above) 1132 else: 1133 try: 1134 thres.ThresholdBetween(above, below) 1135 except AttributeError: 1136 thres.SetUpperThreshold(below) 1137 thres.SetLowerThreshold(above) 1138 1139 thres.Update() 1140 1141 gf = vtki.new("GeometryFilter") 1142 gf.SetInputData(thres.GetOutput()) 1143 gf.Update() 1144 self._update(gf.GetOutput()) 1145 self.pipeline = utils.OperationNode("threshold", parents=[self]) 1146 return self
Extracts cells where scalar value satisfies threshold criterion.
Arguments:
- scalars : (str) name of the scalars array.
- above : (float) minimum value of the scalar
- below : (float) maximum value of the scalar
- on : (str) if 'cells' assume array of scalars refers to cell data.
Examples:
1148 def quantize(self, value: float) -> Self: 1149 """ 1150 The user should input a value and all {x,y,z} coordinates 1151 will be quantized to that absolute grain size. 1152 """ 1153 qp = vtki.new("QuantizePolyDataPoints") 1154 qp.SetInputData(self.dataset) 1155 qp.SetQFactor(value) 1156 qp.Update() 1157 self._update(qp.GetOutput()) 1158 self.pipeline = utils.OperationNode("quantize", parents=[self]) 1159 return self
The user should input a value and all {x,y,z} coordinates will be quantized to that absolute grain size.
1161 @property 1162 def vertex_normals(self) -> np.ndarray: 1163 """ 1164 Retrieve vertex normals as a numpy array. Same as `point_normals`. 1165 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1166 """ 1167 vtknormals = self.dataset.GetPointData().GetNormals() 1168 return utils.vtk2numpy(vtknormals)
Retrieve vertex normals as a numpy array. Same as point_normals
.
Check out also compute_normals()
and compute_normals_with_pca()
.
1170 @property 1171 def point_normals(self) -> np.ndarray: 1172 """ 1173 Retrieve vertex normals as a numpy array. Same as `vertex_normals`. 1174 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1175 """ 1176 vtknormals = self.dataset.GetPointData().GetNormals() 1177 return utils.vtk2numpy(vtknormals)
Retrieve vertex normals as a numpy array. Same as vertex_normals
.
Check out also compute_normals()
and compute_normals_with_pca()
.
1179 def align_to(self, target, iters=100, rigid=False, invert=False, use_centroids=False) -> Self: 1180 """ 1181 Aligned to target mesh through the `Iterative Closest Point` algorithm. 1182 1183 The core of the algorithm is to match each vertex in one surface with 1184 the closest surface point on the other, then apply the transformation 1185 that modify one surface to best match the other (in the least-square sense). 1186 1187 Arguments: 1188 rigid : (bool) 1189 if True do not allow scaling 1190 invert : (bool) 1191 if True start by aligning the target to the source but 1192 invert the transformation finally. Useful when the target is smaller 1193 than the source. 1194 use_centroids : (bool) 1195 start by matching the centroids of the two objects. 1196 1197 Examples: 1198 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1199 1200 ![](https://vedo.embl.es/images/basic/align1.png) 1201 1202 - [align2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align2.py) 1203 1204 ![](https://vedo.embl.es/images/basic/align2.png) 1205 """ 1206 icp = vtki.new("IterativeClosestPointTransform") 1207 icp.SetSource(self.dataset) 1208 icp.SetTarget(target.dataset) 1209 if invert: 1210 icp.Inverse() 1211 icp.SetMaximumNumberOfIterations(iters) 1212 if rigid: 1213 icp.GetLandmarkTransform().SetModeToRigidBody() 1214 icp.SetStartByMatchingCentroids(use_centroids) 1215 icp.Update() 1216 1217 self.apply_transform(icp.GetMatrix()) 1218 1219 self.pipeline = utils.OperationNode( 1220 "align_to", parents=[self, target], comment=f"rigid = {rigid}" 1221 ) 1222 return self
Aligned to target mesh through the Iterative Closest Point
algorithm.
The core of the algorithm is to match each vertex in one surface with the closest surface point on the other, then apply the transformation that modify one surface to best match the other (in the least-square sense).
Arguments:
- rigid : (bool) if True do not allow scaling
- invert : (bool) if True start by aligning the target to the source but invert the transformation finally. Useful when the target is smaller than the source.
- use_centroids : (bool) start by matching the centroids of the two objects.
Examples:
1224 def align_to_bounding_box(self, msh, rigid=False) -> Self: 1225 """ 1226 Align the current object's bounding box to the bounding box 1227 of the input object. 1228 1229 Use `rigid=True` to disable scaling. 1230 1231 Example: 1232 [align6.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align6.py) 1233 """ 1234 lmt = vtki.vtkLandmarkTransform() 1235 ss = vtki.vtkPoints() 1236 xss0, xss1, yss0, yss1, zss0, zss1 = self.bounds() 1237 for p in [ 1238 [xss0, yss0, zss0], 1239 [xss1, yss0, zss0], 1240 [xss1, yss1, zss0], 1241 [xss0, yss1, zss0], 1242 [xss0, yss0, zss1], 1243 [xss1, yss0, zss1], 1244 [xss1, yss1, zss1], 1245 [xss0, yss1, zss1], 1246 ]: 1247 ss.InsertNextPoint(p) 1248 st = vtki.vtkPoints() 1249 xst0, xst1, yst0, yst1, zst0, zst1 = msh.bounds() 1250 for p in [ 1251 [xst0, yst0, zst0], 1252 [xst1, yst0, zst0], 1253 [xst1, yst1, zst0], 1254 [xst0, yst1, zst0], 1255 [xst0, yst0, zst1], 1256 [xst1, yst0, zst1], 1257 [xst1, yst1, zst1], 1258 [xst0, yst1, zst1], 1259 ]: 1260 st.InsertNextPoint(p) 1261 1262 lmt.SetSourceLandmarks(ss) 1263 lmt.SetTargetLandmarks(st) 1264 lmt.SetModeToAffine() 1265 if rigid: 1266 lmt.SetModeToRigidBody() 1267 lmt.Update() 1268 1269 LT = LinearTransform(lmt) 1270 self.apply_transform(LT) 1271 return self
Align the current object's bounding box to the bounding box of the input object.
Use rigid=True
to disable scaling.
Example:
1273 def align_with_landmarks( 1274 self, 1275 source_landmarks, 1276 target_landmarks, 1277 rigid=False, 1278 affine=False, 1279 least_squares=False, 1280 ) -> Self: 1281 """ 1282 Transform mesh orientation and position based on a set of landmarks points. 1283 The algorithm finds the best matching of source points to target points 1284 in the mean least square sense, in one single step. 1285 1286 If `affine` is True the x, y and z axes can scale independently but stay collinear. 1287 With least_squares they can vary orientation. 1288 1289 Examples: 1290 - [align5.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align5.py) 1291 1292 ![](https://vedo.embl.es/images/basic/align5.png) 1293 """ 1294 1295 if utils.is_sequence(source_landmarks): 1296 ss = vtki.vtkPoints() 1297 for p in source_landmarks: 1298 ss.InsertNextPoint(p) 1299 else: 1300 ss = source_landmarks.dataset.GetPoints() 1301 if least_squares: 1302 source_landmarks = source_landmarks.vertices 1303 1304 if utils.is_sequence(target_landmarks): 1305 st = vtki.vtkPoints() 1306 for p in target_landmarks: 1307 st.InsertNextPoint(p) 1308 else: 1309 st = target_landmarks.GetPoints() 1310 if least_squares: 1311 target_landmarks = target_landmarks.vertices 1312 1313 if ss.GetNumberOfPoints() != st.GetNumberOfPoints(): 1314 n1 = ss.GetNumberOfPoints() 1315 n2 = st.GetNumberOfPoints() 1316 vedo.logger.error(f"source and target have different nr of points {n1} vs {n2}") 1317 raise RuntimeError() 1318 1319 if int(rigid) + int(affine) + int(least_squares) > 1: 1320 vedo.logger.error( 1321 "only one of rigid, affine, least_squares can be True at a time" 1322 ) 1323 raise RuntimeError() 1324 1325 lmt = vtki.vtkLandmarkTransform() 1326 lmt.SetSourceLandmarks(ss) 1327 lmt.SetTargetLandmarks(st) 1328 lmt.SetModeToSimilarity() 1329 1330 if rigid: 1331 lmt.SetModeToRigidBody() 1332 lmt.Update() 1333 1334 elif affine: 1335 lmt.SetModeToAffine() 1336 lmt.Update() 1337 1338 elif least_squares: 1339 cms = source_landmarks.mean(axis=0) 1340 cmt = target_landmarks.mean(axis=0) 1341 m = np.linalg.lstsq(source_landmarks - cms, target_landmarks - cmt, rcond=None)[0] 1342 M = vtki.vtkMatrix4x4() 1343 for i in range(3): 1344 for j in range(3): 1345 M.SetElement(j, i, m[i][j]) 1346 lmt = vtki.vtkTransform() 1347 lmt.Translate(cmt) 1348 lmt.Concatenate(M) 1349 lmt.Translate(-cms) 1350 1351 else: 1352 lmt.Update() 1353 1354 self.apply_transform(lmt) 1355 self.pipeline = utils.OperationNode("transform_with_landmarks", parents=[self]) 1356 return self
Transform mesh orientation and position based on a set of landmarks points. The algorithm finds the best matching of source points to target points in the mean least square sense, in one single step.
If affine
is True the x, y and z axes can scale independently but stay collinear.
With least_squares they can vary orientation.
Examples:
1358 def normalize(self) -> Self: 1359 """Scale average size to unit. The scaling is performed around the center of mass.""" 1360 coords = self.vertices 1361 if not coords.shape[0]: 1362 return self 1363 cm = np.mean(coords, axis=0) 1364 pts = coords - cm 1365 xyz2 = np.sum(pts * pts, axis=0) 1366 scale = 1 / np.sqrt(np.sum(xyz2) / len(pts)) 1367 self.scale(scale, origin=cm) 1368 self.pipeline = utils.OperationNode("normalize", parents=[self]) 1369 return self
Scale average size to unit. The scaling is performed around the center of mass.
1371 def mirror(self, axis="x", origin=True) -> Self: 1372 """ 1373 Mirror reflect along one of the cartesian axes 1374 1375 Arguments: 1376 axis : (str) 1377 axis to use for mirroring, must be set to `x, y, z`. 1378 Or any combination of those. 1379 origin : (list) 1380 use this point as the origin of the mirroring transformation. 1381 1382 Examples: 1383 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 1384 1385 ![](https://vedo.embl.es/images/basic/mirror.png) 1386 """ 1387 sx, sy, sz = 1, 1, 1 1388 if "x" in axis.lower(): sx = -1 1389 if "y" in axis.lower(): sy = -1 1390 if "z" in axis.lower(): sz = -1 1391 1392 self.scale([sx, sy, sz], origin=origin) 1393 1394 self.pipeline = utils.OperationNode( 1395 "mirror", comment=f"axis = {axis}", parents=[self]) 1396 1397 if sx * sy * sz < 0: 1398 if hasattr(self, "reverse"): 1399 self.reverse() 1400 return self
Mirror reflect along one of the cartesian axes
Arguments:
- axis : (str)
axis to use for mirroring, must be set to
x, y, z
. Or any combination of those. - origin : (list) use this point as the origin of the mirroring transformation.
Examples:
1402 def flip_normals(self) -> Self: 1403 """Flip all normals orientation.""" 1404 rs = vtki.new("ReverseSense") 1405 rs.SetInputData(self.dataset) 1406 rs.ReverseCellsOff() 1407 rs.ReverseNormalsOn() 1408 rs.Update() 1409 self._update(rs.GetOutput()) 1410 self.pipeline = utils.OperationNode("flip_normals", parents=[self]) 1411 return self
Flip all normals orientation.
1413 def add_gaussian_noise(self, sigma=1.0) -> Self: 1414 """ 1415 Add gaussian noise to point positions. 1416 An extra array is added named "GaussianNoise" with the displacements. 1417 1418 Arguments: 1419 sigma : (float) 1420 nr. of standard deviations, expressed in percent of the diagonal size of mesh. 1421 Can also be a list `[sigma_x, sigma_y, sigma_z]`. 1422 1423 Example: 1424 ```python 1425 from vedo import Sphere 1426 Sphere().add_gaussian_noise(1.0).point_size(8).show().close() 1427 ``` 1428 """ 1429 sz = self.diagonal_size() 1430 pts = self.vertices 1431 n = len(pts) 1432 ns = (np.random.randn(n, 3) * sigma) * (sz / 100) 1433 vpts = vtki.vtkPoints() 1434 vpts.SetNumberOfPoints(n) 1435 vpts.SetData(utils.numpy2vtk(pts + ns, dtype=np.float32)) 1436 self.dataset.SetPoints(vpts) 1437 self.dataset.GetPoints().Modified() 1438 self.pointdata["GaussianNoise"] = -ns 1439 self.pipeline = utils.OperationNode( 1440 "gaussian_noise", parents=[self], shape="egg", comment=f"sigma = {sigma}" 1441 ) 1442 return self
Add gaussian noise to point positions. An extra array is added named "GaussianNoise" with the displacements.
Arguments:
- sigma : (float)
nr. of standard deviations, expressed in percent of the diagonal size of mesh.
Can also be a list
[sigma_x, sigma_y, sigma_z]
.
Example:
from vedo import Sphere Sphere().add_gaussian_noise(1.0).point_size(8).show().close()
1444 def closest_point( 1445 self, pt, n=1, radius=None, return_point_id=False, return_cell_id=False 1446 ) -> Union[List[int], int, np.ndarray]: 1447 """ 1448 Find the closest point(s) on a mesh given from the input point `pt`. 1449 1450 Arguments: 1451 n : (int) 1452 if greater than 1, return a list of n ordered closest points 1453 radius : (float) 1454 if given, get all points within that radius. Then n is ignored. 1455 return_point_id : (bool) 1456 return point ID instead of coordinates 1457 return_cell_id : (bool) 1458 return cell ID in which the closest point sits 1459 1460 Examples: 1461 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1462 - [fitplanes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitplanes.py) 1463 - [quadratic_morphing.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/quadratic_morphing.py) 1464 1465 .. note:: 1466 The appropriate tree search locator is built on the fly and cached for speed. 1467 1468 If you want to reset it use `mymesh.point_locator=None` 1469 and / or `mymesh.cell_locator=None`. 1470 """ 1471 if len(pt) != 3: 1472 pt = [pt[0], pt[1], 0] 1473 1474 # NB: every time the mesh moves or is warped the locators are set to None 1475 if ((n > 1 or radius) or (n == 1 and return_point_id)) and not return_cell_id: 1476 poly = None 1477 if not self.point_locator: 1478 poly = self.dataset 1479 self.point_locator = vtki.new("StaticPointLocator") 1480 self.point_locator.SetDataSet(poly) 1481 self.point_locator.BuildLocator() 1482 1483 ########## 1484 if radius: 1485 vtklist = vtki.vtkIdList() 1486 self.point_locator.FindPointsWithinRadius(radius, pt, vtklist) 1487 elif n > 1: 1488 vtklist = vtki.vtkIdList() 1489 self.point_locator.FindClosestNPoints(n, pt, vtklist) 1490 else: # n==1 hence return_point_id==True 1491 ######## 1492 return self.point_locator.FindClosestPoint(pt) 1493 ######## 1494 1495 if return_point_id: 1496 ######## 1497 return utils.vtk2numpy(vtklist) 1498 ######## 1499 1500 if not poly: 1501 poly = self.dataset 1502 trgp = [] 1503 for i in range(vtklist.GetNumberOfIds()): 1504 trgp_ = [0, 0, 0] 1505 vi = vtklist.GetId(i) 1506 poly.GetPoints().GetPoint(vi, trgp_) 1507 trgp.append(trgp_) 1508 ######## 1509 return np.array(trgp) 1510 ######## 1511 1512 else: 1513 1514 if not self.cell_locator: 1515 poly = self.dataset 1516 1517 # As per Miquel example with limbs the vtkStaticCellLocator doesnt work !! 1518 # https://discourse.vtk.org/t/vtkstaticcelllocator-problem-vtk9-0-3/7854/4 1519 if vedo.vtk_version[0] >= 9 and vedo.vtk_version[1] > 0: 1520 self.cell_locator = vtki.new("StaticCellLocator") 1521 else: 1522 self.cell_locator = vtki.new("CellLocator") 1523 1524 self.cell_locator.SetDataSet(poly) 1525 self.cell_locator.BuildLocator() 1526 1527 if radius is not None: 1528 vedo.printc("Warning: closest_point() with radius is not implemented for cells.", c='r') 1529 1530 if n != 1: 1531 vedo.printc("Warning: closest_point() with n>1 is not implemented for cells.", c='r') 1532 1533 trgp = [0, 0, 0] 1534 cid = vtki.mutable(0) 1535 dist2 = vtki.mutable(0) 1536 subid = vtki.mutable(0) 1537 self.cell_locator.FindClosestPoint(pt, trgp, cid, subid, dist2) 1538 1539 if return_cell_id: 1540 return int(cid) 1541 1542 return np.array(trgp)
Find the closest point(s) on a mesh given from the input point pt
.
Arguments:
- n : (int) if greater than 1, return a list of n ordered closest points
- radius : (float) if given, get all points within that radius. Then n is ignored.
- return_point_id : (bool) return point ID instead of coordinates
- return_cell_id : (bool) return cell ID in which the closest point sits
Examples:
The appropriate tree search locator is built on the fly and cached for speed.
If you want to reset it use mymesh.point_locator=None
and / or mymesh.cell_locator=None
.
1544 def auto_distance(self) -> np.ndarray: 1545 """ 1546 Calculate the distance to the closest point in the same cloud of points. 1547 The output is stored in a new pointdata array called "AutoDistance", 1548 and it is also returned by the function. 1549 """ 1550 points = self.vertices 1551 if not self.point_locator: 1552 self.point_locator = vtki.new("StaticPointLocator") 1553 self.point_locator.SetDataSet(self.dataset) 1554 self.point_locator.BuildLocator() 1555 qs = [] 1556 vtklist = vtki.vtkIdList() 1557 vtkpoints = self.dataset.GetPoints() 1558 for p in points: 1559 self.point_locator.FindClosestNPoints(2, p, vtklist) 1560 q = [0, 0, 0] 1561 pid = vtklist.GetId(1) 1562 vtkpoints.GetPoint(pid, q) 1563 qs.append(q) 1564 dists = np.linalg.norm(points - np.array(qs), axis=1) 1565 self.pointdata["AutoDistance"] = dists 1566 return dists
Calculate the distance to the closest point in the same cloud of points. The output is stored in a new pointdata array called "AutoDistance", and it is also returned by the function.
1568 def hausdorff_distance(self, points) -> float: 1569 """ 1570 Compute the Hausdorff distance to the input point set. 1571 Returns a single `float`. 1572 1573 Example: 1574 ```python 1575 from vedo import * 1576 t = np.linspace(0, 2*np.pi, 100) 1577 x = 4/3 * sin(t)**3 1578 y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12 1579 pol1 = Line(np.c_[x,y], closed=True).triangulate() 1580 pol2 = Polygon(nsides=5).pos(2,2) 1581 d12 = pol1.distance_to(pol2) 1582 d21 = pol2.distance_to(pol1) 1583 pol1.lw(0).cmap("viridis") 1584 pol2.lw(0).cmap("viridis") 1585 print("distance d12, d21 :", min(d12), min(d21)) 1586 print("hausdorff distance:", pol1.hausdorff_distance(pol2)) 1587 print("chamfer distance :", pol1.chamfer_distance(pol2)) 1588 show(pol1, pol2, axes=1) 1589 ``` 1590 ![](https://vedo.embl.es/images/feats/heart.png) 1591 """ 1592 hp = vtki.new("HausdorffDistancePointSetFilter") 1593 hp.SetInputData(0, self.dataset) 1594 hp.SetInputData(1, points.dataset) 1595 hp.SetTargetDistanceMethodToPointToCell() 1596 hp.Update() 1597 return hp.GetHausdorffDistance()
Compute the Hausdorff distance to the input point set.
Returns a single float
.
Example:
from vedo import * t = np.linspace(0, 2*np.pi, 100) x = 4/3 * sin(t)**3 y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12 pol1 = Line(np.c_[x,y], closed=True).triangulate() pol2 = Polygon(nsides=5).pos(2,2) d12 = pol1.distance_to(pol2) d21 = pol2.distance_to(pol1) pol1.lw(0).cmap("viridis") pol2.lw(0).cmap("viridis") print("distance d12, d21 :", min(d12), min(d21)) print("hausdorff distance:", pol1.hausdorff_distance(pol2)) print("chamfer distance :", pol1.chamfer_distance(pol2)) show(pol1, pol2, axes=1)
1599 def chamfer_distance(self, pcloud) -> float: 1600 """ 1601 Compute the Chamfer distance to the input point set. 1602 1603 Example: 1604 ```python 1605 from vedo import * 1606 cloud1 = np.random.randn(1000, 3) 1607 cloud2 = np.random.randn(1000, 3) + [1, 2, 3] 1608 c1 = Points(cloud1, r=5, c="red") 1609 c2 = Points(cloud2, r=5, c="green") 1610 d = c1.chamfer_distance(c2) 1611 show(f"Chamfer distance = {d}", c1, c2, axes=1).close() 1612 ``` 1613 """ 1614 # Definition of Chamfer distance may vary, here we use the average 1615 if not pcloud.point_locator: 1616 pcloud.point_locator = vtki.new("PointLocator") 1617 pcloud.point_locator.SetDataSet(pcloud.dataset) 1618 pcloud.point_locator.BuildLocator() 1619 if not self.point_locator: 1620 self.point_locator = vtki.new("PointLocator") 1621 self.point_locator.SetDataSet(self.dataset) 1622 self.point_locator.BuildLocator() 1623 1624 ps1 = self.vertices 1625 ps2 = pcloud.vertices 1626 1627 ids12 = [] 1628 for p in ps1: 1629 pid12 = pcloud.point_locator.FindClosestPoint(p) 1630 ids12.append(pid12) 1631 deltav = ps2[ids12] - ps1 1632 da = np.mean(np.linalg.norm(deltav, axis=1)) 1633 1634 ids21 = [] 1635 for p in ps2: 1636 pid21 = self.point_locator.FindClosestPoint(p) 1637 ids21.append(pid21) 1638 deltav = ps1[ids21] - ps2 1639 db = np.mean(np.linalg.norm(deltav, axis=1)) 1640 return (da + db) / 2
Compute the Chamfer distance to the input point set.
Example:
from vedo import * cloud1 = np.random.randn(1000, 3) cloud2 = np.random.randn(1000, 3) + [1, 2, 3] c1 = Points(cloud1, r=5, c="red") c2 = Points(cloud2, r=5, c="green") d = c1.chamfer_distance(c2) show(f"Chamfer distance = {d}", c1, c2, axes=1).close()
1642 def remove_outliers(self, radius: float, neighbors=5) -> Self: 1643 """ 1644 Remove outliers from a cloud of points within the specified `radius` search. 1645 1646 Arguments: 1647 radius : (float) 1648 Specify the local search radius. 1649 neighbors : (int) 1650 Specify the number of neighbors that a point must have, 1651 within the specified radius, for the point to not be considered isolated. 1652 1653 Examples: 1654 - [clustering.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/clustering.py) 1655 1656 ![](https://vedo.embl.es/images/basic/clustering.png) 1657 """ 1658 removal = vtki.new("RadiusOutlierRemoval") 1659 removal.SetInputData(self.dataset) 1660 removal.SetRadius(radius) 1661 removal.SetNumberOfNeighbors(neighbors) 1662 removal.GenerateOutliersOff() 1663 removal.Update() 1664 inputobj = removal.GetOutput() 1665 if inputobj.GetNumberOfCells() == 0: 1666 carr = vtki.vtkCellArray() 1667 for i in range(inputobj.GetNumberOfPoints()): 1668 carr.InsertNextCell(1) 1669 carr.InsertCellPoint(i) 1670 inputobj.SetVerts(carr) 1671 self._update(removal.GetOutput()) 1672 self.pipeline = utils.OperationNode("remove_outliers", parents=[self]) 1673 return self
Remove outliers from a cloud of points within the specified radius
search.
Arguments:
- radius : (float) Specify the local search radius.
- neighbors : (int) Specify the number of neighbors that a point must have, within the specified radius, for the point to not be considered isolated.
Examples:
1675 def relax_point_positions( 1676 self, 1677 n=10, 1678 iters=10, 1679 sub_iters=10, 1680 packing_factor=1, 1681 max_step=0, 1682 constraints=(), 1683 ) -> Self: 1684 """ 1685 Smooth mesh or points with a 1686 [Laplacian algorithm](https://vtk.org/doc/nightly/html/classvtkPointSmoothingFilter.html) 1687 variant. This modifies the coordinates of the input points by adjusting their positions 1688 to create a smooth distribution (and thereby form a pleasing packing of the points). 1689 Smoothing is performed by considering the effects of neighboring points on one another 1690 it uses a cubic cutoff function to produce repulsive forces between close points 1691 and attractive forces that are a little further away. 1692 1693 In general, the larger the neighborhood size, the greater the reduction in high frequency 1694 information. The memory and computational requirements of the algorithm may also 1695 significantly increase. 1696 1697 The algorithm incrementally adjusts the point positions through an iterative process. 1698 Basically points are moved due to the influence of neighboring points. 1699 1700 As points move, both the local connectivity and data attributes associated with each point 1701 must be updated. Rather than performing these expensive operations after every iteration, 1702 a number of sub-iterations can be specified. If so, then the neighborhood and attribute 1703 value updates occur only every sub iteration, which can improve performance significantly. 1704 1705 Arguments: 1706 n : (int) 1707 neighborhood size to calculate the Laplacian. 1708 iters : (int) 1709 number of iterations. 1710 sub_iters : (int) 1711 number of sub-iterations, i.e. the number of times the neighborhood and attribute 1712 value updates occur during each iteration. 1713 packing_factor : (float) 1714 adjust convergence speed. 1715 max_step : (float) 1716 Specify the maximum smoothing step size for each smoothing iteration. 1717 This limits the the distance over which a point can move in each iteration. 1718 As in all iterative methods, the stability of the process is sensitive to this parameter. 1719 In general, small step size and large numbers of iterations are more stable than a larger 1720 step size and a smaller numbers of iterations. 1721 constraints : (dict) 1722 dictionary of constraints. 1723 Point constraints are used to prevent points from moving, 1724 or to move only on a plane. This can prevent shrinking or growing point clouds. 1725 If enabled, a local topological analysis is performed to determine whether a point 1726 should be marked as fixed" i.e., never moves, or the point only moves on a plane, 1727 or the point can move freely. 1728 If all points in the neighborhood surrounding a point are in the cone defined by 1729 `fixed_angle`, then the point is classified as fixed. 1730 If all points in the neighborhood surrounding a point are in the cone defined by 1731 `boundary_angle`, then the point is classified as lying on a plane. 1732 Angles are expressed in degrees. 1733 1734 Example: 1735 ```py 1736 import numpy as np 1737 from vedo import Points, show 1738 from vedo.pyplot import histogram 1739 1740 vpts1 = Points(np.random.rand(10_000, 3)) 1741 dists = vpts1.auto_distance() 1742 h1 = histogram(dists, xlim=(0,0.08)).clone2d() 1743 1744 vpts2 = vpts1.clone().relax_point_positions(n=100, iters=20, sub_iters=10) 1745 dists = vpts2.auto_distance() 1746 h2 = histogram(dists, xlim=(0,0.08)).clone2d() 1747 1748 show([[vpts1, h1], [vpts2, h2]], N=2).close() 1749 ``` 1750 """ 1751 smooth = vtki.new("PointSmoothingFilter") 1752 smooth.SetInputData(self.dataset) 1753 smooth.SetSmoothingModeToUniform() 1754 smooth.SetNumberOfIterations(iters) 1755 smooth.SetNumberOfSubIterations(sub_iters) 1756 smooth.SetPackingFactor(packing_factor) 1757 if self.point_locator: 1758 smooth.SetLocator(self.point_locator) 1759 if not max_step: 1760 max_step = self.diagonal_size() / 100 1761 smooth.SetMaximumStepSize(max_step) 1762 smooth.SetNeighborhoodSize(n) 1763 if constraints: 1764 fixed_angle = constraints.get("fixed_angle", 45) 1765 boundary_angle = constraints.get("boundary_angle", 110) 1766 smooth.EnableConstraintsOn() 1767 smooth.SetFixedAngle(fixed_angle) 1768 smooth.SetBoundaryAngle(boundary_angle) 1769 smooth.GenerateConstraintScalarsOn() 1770 smooth.GenerateConstraintNormalsOn() 1771 smooth.Update() 1772 self._update(smooth.GetOutput()) 1773 self.metadata["PackingRadius"] = smooth.GetPackingRadius() 1774 self.pipeline = utils.OperationNode("relax_point_positions", parents=[self]) 1775 return self
Smooth mesh or points with a Laplacian algorithm variant. This modifies the coordinates of the input points by adjusting their positions to create a smooth distribution (and thereby form a pleasing packing of the points). Smoothing is performed by considering the effects of neighboring points on one another it uses a cubic cutoff function to produce repulsive forces between close points and attractive forces that are a little further away.
In general, the larger the neighborhood size, the greater the reduction in high frequency information. The memory and computational requirements of the algorithm may also significantly increase.
The algorithm incrementally adjusts the point positions through an iterative process. Basically points are moved due to the influence of neighboring points.
As points move, both the local connectivity and data attributes associated with each point must be updated. Rather than performing these expensive operations after every iteration, a number of sub-iterations can be specified. If so, then the neighborhood and attribute value updates occur only every sub iteration, which can improve performance significantly.
Arguments:
- n : (int) neighborhood size to calculate the Laplacian.
- iters : (int) number of iterations.
- sub_iters : (int) number of sub-iterations, i.e. the number of times the neighborhood and attribute value updates occur during each iteration.
- packing_factor : (float) adjust convergence speed.
- max_step : (float) Specify the maximum smoothing step size for each smoothing iteration. This limits the the distance over which a point can move in each iteration. As in all iterative methods, the stability of the process is sensitive to this parameter. In general, small step size and large numbers of iterations are more stable than a larger step size and a smaller numbers of iterations.
- constraints : (dict)
dictionary of constraints.
Point constraints are used to prevent points from moving,
or to move only on a plane. This can prevent shrinking or growing point clouds.
If enabled, a local topological analysis is performed to determine whether a point
should be marked as fixed" i.e., never moves, or the point only moves on a plane,
or the point can move freely.
If all points in the neighborhood surrounding a point are in the cone defined by
fixed_angle
, then the point is classified as fixed. If all points in the neighborhood surrounding a point are in the cone defined byboundary_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()
1777 def smooth_mls_1d(self, f=0.2, radius=None, n=0) -> Self: 1778 """ 1779 Smooth mesh or points with a `Moving Least Squares` variant. 1780 The point data array "Variances" will contain the residue calculated for each point. 1781 1782 Arguments: 1783 f : (float) 1784 smoothing factor - typical range is [0,2]. 1785 radius : (float) 1786 radius search in absolute units. 1787 If set then `f` is ignored. 1788 n : (int) 1789 number of neighbours to be used for the fit. 1790 If set then `f` and `radius` are ignored. 1791 1792 Examples: 1793 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1794 - [skeletonize.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/skeletonize.py) 1795 1796 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1797 """ 1798 coords = self.vertices 1799 ncoords = len(coords) 1800 1801 if n: 1802 Ncp = n 1803 elif radius: 1804 Ncp = 1 1805 else: 1806 Ncp = int(ncoords * f / 10) 1807 if Ncp < 5: 1808 vedo.logger.warning(f"Please choose a fraction higher than {f}") 1809 Ncp = 5 1810 1811 variances, newline = [], [] 1812 for p in coords: 1813 points = self.closest_point(p, n=Ncp, radius=radius) 1814 if len(points) < 4: 1815 continue 1816 1817 points = np.array(points) 1818 pointsmean = points.mean(axis=0) # plane center 1819 _, dd, vv = np.linalg.svd(points - pointsmean) 1820 newp = np.dot(p - pointsmean, vv[0]) * vv[0] + pointsmean 1821 variances.append(dd[1] + dd[2]) 1822 newline.append(newp) 1823 1824 self.pointdata["Variances"] = np.array(variances).astype(np.float32) 1825 self.vertices = newline 1826 self.pipeline = utils.OperationNode("smooth_mls_1d", parents=[self]) 1827 return self
Smooth mesh or points with a Moving Least Squares
variant.
The point data array "Variances" will contain the residue calculated for each point.
Arguments:
- f : (float) smoothing factor - typical range is [0,2].
- radius : (float)
radius search in absolute units.
If set then
f
is ignored. - n : (int)
number of neighbours to be used for the fit.
If set then
f
andradius
are ignored.
Examples:
1829 def smooth_mls_2d(self, f=0.2, radius=None, n=0) -> Self: 1830 """ 1831 Smooth mesh or points with a `Moving Least Squares` algorithm variant. 1832 1833 The `mesh.pointdata['MLSVariance']` array will contain the residue calculated for each point. 1834 When a radius is specified, points that are isolated will not be moved and will get 1835 a 0 entry in array `mesh.pointdata['MLSValidPoint']`. 1836 1837 Arguments: 1838 f : (float) 1839 smoothing factor - typical range is [0, 2]. 1840 radius : (float | array) 1841 radius search in absolute units. Can be single value (float) or sequence 1842 for adaptive smoothing. If set then `f` is ignored. 1843 n : (int) 1844 number of neighbours to be used for the fit. 1845 If set then `f` and `radius` are ignored. 1846 1847 Examples: 1848 - [moving_least_squares2D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares2D.py) 1849 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1850 1851 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1852 """ 1853 coords = self.vertices 1854 ncoords = len(coords) 1855 1856 if n: 1857 Ncp = n 1858 radius = None 1859 elif radius is not None: 1860 Ncp = 1 1861 else: 1862 Ncp = int(ncoords * f / 100) 1863 if Ncp < 4: 1864 vedo.logger.error(f"please choose a f-value higher than {f}") 1865 Ncp = 4 1866 1867 variances, newpts, valid = [], [], [] 1868 radius_is_sequence = utils.is_sequence(radius) 1869 1870 pb = None 1871 if ncoords > 10000: 1872 pb = utils.ProgressBar(0, ncoords, delay=3) 1873 1874 for i, p in enumerate(coords): 1875 if pb: 1876 pb.print("smooth_mls_2d working ...") 1877 1878 # if a radius was provided for each point 1879 if radius_is_sequence: 1880 pts = self.closest_point(p, n=Ncp, radius=radius[i]) 1881 else: 1882 pts = self.closest_point(p, n=Ncp, radius=radius) 1883 1884 if len(pts) > 3: 1885 ptsmean = pts.mean(axis=0) # plane center 1886 _, dd, vv = np.linalg.svd(pts - ptsmean) 1887 cv = np.cross(vv[0], vv[1]) 1888 t = (np.dot(cv, ptsmean) - np.dot(cv, p)) / np.dot(cv, cv) 1889 newpts.append(p + cv * t) 1890 variances.append(dd[2]) 1891 if radius is not None: 1892 valid.append(1) 1893 else: 1894 newpts.append(p) 1895 variances.append(0) 1896 if radius is not None: 1897 valid.append(0) 1898 1899 if radius is not None: 1900 self.pointdata["MLSValidPoint"] = np.array(valid).astype(np.uint8) 1901 self.pointdata["MLSVariance"] = np.array(variances).astype(np.float32) 1902 1903 self.vertices = newpts 1904 1905 self.pipeline = utils.OperationNode("smooth_mls_2d", parents=[self]) 1906 return self
Smooth mesh or points with a Moving Least Squares
algorithm variant.
The mesh.pointdata['MLSVariance']
array will contain the residue calculated for each point.
When a radius is specified, points that are isolated will not be moved and will get
a 0 entry in array mesh.pointdata['MLSValidPoint']
.
Arguments:
- f : (float) smoothing factor - typical range is [0, 2].
- radius : (float | array)
radius search in absolute units. Can be single value (float) or sequence
for adaptive smoothing. If set then
f
is ignored. - n : (int)
number of neighbours to be used for the fit.
If set then
f
andradius
are ignored.
Examples:
1908 def smooth_lloyd_2d(self, iterations=2, bounds=None, options="Qbb Qc Qx") -> Self: 1909 """ 1910 Lloyd relaxation of a 2D pointcloud. 1911 1912 Arguments: 1913 iterations : (int) 1914 number of iterations. 1915 bounds : (list) 1916 bounding box of the domain. 1917 options : (str) 1918 options for the Qhull algorithm. 1919 """ 1920 # Credits: https://hatarilabs.com/ih-en/ 1921 # tutorial-to-create-a-geospatial-voronoi-sh-mesh-with-python-scipy-and-geopandas 1922 from scipy.spatial import Voronoi as scipy_voronoi 1923 1924 def _constrain_points(points): 1925 # Update any points that have drifted beyond the boundaries of this space 1926 if bounds is not None: 1927 for point in points: 1928 if point[0] < bounds[0]: point[0] = bounds[0] 1929 if point[0] > bounds[1]: point[0] = bounds[1] 1930 if point[1] < bounds[2]: point[1] = bounds[2] 1931 if point[1] > bounds[3]: point[1] = bounds[3] 1932 return points 1933 1934 def _find_centroid(vertices): 1935 # The equation for the method used here to find the centroid of a 1936 # 2D polygon is given here: https://en.wikipedia.org/wiki/Centroid#Of_a_polygon 1937 area = 0 1938 centroid_x = 0 1939 centroid_y = 0 1940 for i in range(len(vertices) - 1): 1941 step = (vertices[i, 0] * vertices[i + 1, 1]) - (vertices[i + 1, 0] * vertices[i, 1]) 1942 centroid_x += (vertices[i, 0] + vertices[i + 1, 0]) * step 1943 centroid_y += (vertices[i, 1] + vertices[i + 1, 1]) * step 1944 area += step 1945 if area: 1946 centroid_x = (1.0 / (3.0 * area)) * centroid_x 1947 centroid_y = (1.0 / (3.0 * area)) * centroid_y 1948 # prevent centroids from escaping bounding box 1949 return _constrain_points([[centroid_x, centroid_y]])[0] 1950 1951 def _relax(voron): 1952 # Moves each point to the centroid of its cell in the voronoi 1953 # map to "relax" the points (i.e. jitter the points so as 1954 # to spread them out within the space). 1955 centroids = [] 1956 for idx in voron.point_region: 1957 # the region is a series of indices into voronoi.vertices 1958 # remove point at infinity, designated by index -1 1959 region = [i for i in voron.regions[idx] if i != -1] 1960 # enclose the polygon 1961 region = region + [region[0]] 1962 verts = voron.vertices[region] 1963 # find the centroid of those vertices 1964 centroids.append(_find_centroid(verts)) 1965 return _constrain_points(centroids) 1966 1967 if bounds is None: 1968 bounds = self.bounds() 1969 1970 pts = self.vertices[:, (0, 1)] 1971 for i in range(iterations): 1972 vor = scipy_voronoi(pts, qhull_options=options) 1973 _constrain_points(vor.vertices) 1974 pts = _relax(vor) 1975 out = Points(pts) 1976 out.name = "MeshSmoothLloyd2D" 1977 out.pipeline = utils.OperationNode("smooth_lloyd", parents=[self]) 1978 return out
Lloyd relaxation of a 2D pointcloud.
Arguments:
- iterations : (int) number of iterations.
- bounds : (list) bounding box of the domain.
- options : (str) options for the Qhull algorithm.
1980 def project_on_plane(self, plane="z", point=None, direction=None) -> Self: 1981 """ 1982 Project the mesh on one of the Cartesian planes. 1983 1984 Arguments: 1985 plane : (str, Plane) 1986 if plane is `str`, plane can be one of ['x', 'y', 'z'], 1987 represents x-plane, y-plane and z-plane, respectively. 1988 Otherwise, plane should be an instance of `vedo.shapes.Plane`. 1989 point : (float, array) 1990 if plane is `str`, point should be a float represents the intercept. 1991 Otherwise, point is the camera point of perspective projection 1992 direction : (array) 1993 direction of oblique projection 1994 1995 Note: 1996 Parameters `point` and `direction` are only used if the given plane 1997 is an instance of `vedo.shapes.Plane`. And one of these two params 1998 should be left as `None` to specify the projection type. 1999 2000 Example: 2001 ```python 2002 s.project_on_plane(plane='z') # project to z-plane 2003 plane = Plane(pos=(4, 8, -4), normal=(-1, 0, 1), s=(5,5)) 2004 s.project_on_plane(plane=plane) # orthogonal projection 2005 s.project_on_plane(plane=plane, point=(6, 6, 6)) # perspective projection 2006 s.project_on_plane(plane=plane, direction=(1, 2, -1)) # oblique projection 2007 ``` 2008 2009 Examples: 2010 - [silhouette2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette2.py) 2011 2012 ![](https://vedo.embl.es/images/basic/silhouette2.png) 2013 """ 2014 coords = self.vertices 2015 2016 if plane == "x": 2017 coords[:, 0] = self.transform.position[0] 2018 intercept = self.xbounds()[0] if point is None else point 2019 self.x(intercept) 2020 elif plane == "y": 2021 coords[:, 1] = self.transform.position[1] 2022 intercept = self.ybounds()[0] if point is None else point 2023 self.y(intercept) 2024 elif plane == "z": 2025 coords[:, 2] = self.transform.position[2] 2026 intercept = self.zbounds()[0] if point is None else point 2027 self.z(intercept) 2028 2029 elif isinstance(plane, vedo.shapes.Plane): 2030 normal = plane.normal / np.linalg.norm(plane.normal) 2031 pl = np.hstack((normal, -np.dot(plane.pos(), normal))).reshape(4, 1) 2032 if direction is None and point is None: 2033 # orthogonal projection 2034 pt = np.hstack((normal, [0])).reshape(4, 1) 2035 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T # python3 only 2036 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2037 2038 elif direction is None: 2039 # perspective projection 2040 pt = np.hstack((np.array(point), [1])).reshape(4, 1) 2041 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2042 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2043 2044 elif point is None: 2045 # oblique projection 2046 pt = np.hstack((np.array(direction), [0])).reshape(4, 1) 2047 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2048 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2049 2050 coords = np.concatenate([coords, np.ones((coords.shape[:-1] + (1,)))], axis=-1) 2051 # coords = coords @ proj_mat.T 2052 coords = np.matmul(coords, proj_mat.T) 2053 coords = coords[:, :3] / coords[:, 3:] 2054 2055 else: 2056 vedo.logger.error(f"unknown plane {plane}") 2057 raise RuntimeError() 2058 2059 self.alpha(0.1) 2060 self.vertices = coords 2061 return self
Project the mesh on one of the Cartesian planes.
Arguments:
- plane : (str, Plane)
if plane is
str
, plane can be one of ['x', 'y', 'z'], represents x-plane, y-plane and z-plane, respectively. Otherwise, plane should be an instance ofvedo.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
anddirection
are only used if the given plane is an instance ofvedo.shapes.Plane
. And one of these two params should be left asNone
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:
2063 def warp(self, source, target, sigma=1.0, mode="3d") -> Self: 2064 """ 2065 "Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set 2066 of source and target landmarks. Any point on the mesh close to a source landmark will 2067 be moved to a place close to the corresponding target landmark. 2068 The points in between are interpolated smoothly using 2069 Bookstein's Thin Plate Spline algorithm. 2070 2071 Transformation object can be accessed with `mesh.transform`. 2072 2073 Arguments: 2074 sigma : (float) 2075 specify the 'stiffness' of the spline. 2076 mode : (str) 2077 set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes) 2078 2079 Examples: 2080 - [interpolate_field.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/interpolate_field.py) 2081 - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py) 2082 - [warp2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp2.py) 2083 - [warp3.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp3.py) 2084 - [warp4a.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4a.py) 2085 - [warp4b.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4b.py) 2086 - [warp6.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp6.py) 2087 2088 ![](https://vedo.embl.es/images/advanced/warp2.png) 2089 """ 2090 parents = [self] 2091 2092 try: 2093 source = source.vertices 2094 parents.append(source) 2095 except AttributeError: 2096 source = utils.make3d(source) 2097 2098 try: 2099 target = target.vertices 2100 parents.append(target) 2101 except AttributeError: 2102 target = utils.make3d(target) 2103 2104 ns = len(source) 2105 nt = len(target) 2106 if ns != nt: 2107 vedo.logger.error(f"#source {ns} != {nt} #target points") 2108 raise RuntimeError() 2109 2110 NLT = NonLinearTransform() 2111 NLT.source_points = source 2112 NLT.target_points = target 2113 self.apply_transform(NLT) 2114 2115 self.pipeline = utils.OperationNode("warp", parents=parents) 2116 return self
"Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set of source and target landmarks. Any point on the mesh close to a source landmark will be moved to a place close to the corresponding target landmark. The points in between are interpolated smoothly using Bookstein's Thin Plate Spline algorithm.
Transformation object can be accessed with mesh.transform
.
Arguments:
- sigma : (float) specify the 'stiffness' of the spline.
- mode : (str) set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes)
Examples:
2118 def cut_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0), invert=False) -> Self: 2119 """ 2120 Cut the mesh with the plane defined by a point and a normal. 2121 2122 Arguments: 2123 origin : (array) 2124 the cutting plane goes through this point 2125 normal : (array) 2126 normal of the cutting plane 2127 2128 Example: 2129 ```python 2130 from vedo import Cube 2131 cube = Cube().cut_with_plane(normal=(1,1,1)) 2132 cube.back_color('pink').show().close() 2133 ``` 2134 ![](https://vedo.embl.es/images/feats/cut_with_plane_cube.png) 2135 2136 Examples: 2137 - [trail.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/trail.py) 2138 2139 ![](https://vedo.embl.es/images/simulations/trail.gif) 2140 2141 Check out also: 2142 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`. 2143 """ 2144 s = str(normal) 2145 if "x" in s: 2146 normal = (1, 0, 0) 2147 if "-" in s: 2148 normal = -np.array(normal) 2149 elif "y" in s: 2150 normal = (0, 1, 0) 2151 if "-" in s: 2152 normal = -np.array(normal) 2153 elif "z" in s: 2154 normal = (0, 0, 1) 2155 if "-" in s: 2156 normal = -np.array(normal) 2157 plane = vtki.vtkPlane() 2158 plane.SetOrigin(origin) 2159 plane.SetNormal(normal) 2160 2161 clipper = vtki.new("ClipPolyData") 2162 clipper.SetInputData(self.dataset) 2163 clipper.SetClipFunction(plane) 2164 clipper.GenerateClippedOutputOff() 2165 clipper.GenerateClipScalarsOff() 2166 clipper.SetInsideOut(invert) 2167 clipper.SetValue(0) 2168 clipper.Update() 2169 2170 self._update(clipper.GetOutput()) 2171 2172 self.pipeline = utils.OperationNode("cut_with_plane", parents=[self]) 2173 return self
Cut the mesh with the plane defined by a point and a normal.
Arguments:
- origin : (array) the cutting plane goes through this point
- normal : (array) normal of the cutting plane
Example:
from vedo import Cube cube = Cube().cut_with_plane(normal=(1,1,1)) cube.back_color('pink').show().close()
Examples:
Check out also:
2175 def cut_with_planes(self, origins, normals, invert=False) -> Self: 2176 """ 2177 Cut the mesh with a convex set of planes defined by points and normals. 2178 2179 Arguments: 2180 origins : (array) 2181 each cutting plane goes through this point 2182 normals : (array) 2183 normal of each of the cutting planes 2184 invert : (bool) 2185 if True, cut outside instead of inside 2186 2187 Check out also: 2188 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()` 2189 """ 2190 2191 vpoints = vtki.vtkPoints() 2192 for p in utils.make3d(origins): 2193 vpoints.InsertNextPoint(p) 2194 normals = utils.make3d(normals) 2195 2196 planes = vtki.vtkPlanes() 2197 planes.SetPoints(vpoints) 2198 planes.SetNormals(utils.numpy2vtk(normals, dtype=float)) 2199 2200 clipper = vtki.new("ClipPolyData") 2201 clipper.SetInputData(self.dataset) 2202 clipper.SetInsideOut(invert) 2203 clipper.SetClipFunction(planes) 2204 clipper.GenerateClippedOutputOff() 2205 clipper.GenerateClipScalarsOff() 2206 clipper.SetValue(0) 2207 clipper.Update() 2208 2209 self._update(clipper.GetOutput()) 2210 2211 self.pipeline = utils.OperationNode("cut_with_planes", parents=[self]) 2212 return self
Cut the mesh with a convex set of planes defined by points and normals.
Arguments:
- origins : (array) each cutting plane goes through this point
- normals : (array) normal of each of the cutting planes
- invert : (bool) if True, cut outside instead of inside
Check out also:
2214 def cut_with_box(self, bounds, invert=False) -> Self: 2215 """ 2216 Cut the current mesh with a box or a set of boxes. 2217 This is much faster than `cut_with_mesh()`. 2218 2219 Input `bounds` can be either: 2220 - a Mesh or Points object 2221 - a list of 6 number representing a bounding box `[xmin,xmax, ymin,ymax, zmin,zmax]` 2222 - a list of bounding boxes like the above: `[[xmin1,...], [xmin2,...], ...]` 2223 2224 Example: 2225 ```python 2226 from vedo import Sphere, Cube, show 2227 mesh = Sphere(r=1, res=50) 2228 box = Cube(side=1.5).wireframe() 2229 mesh.cut_with_box(box) 2230 show(mesh, box, axes=1).close() 2231 ``` 2232 ![](https://vedo.embl.es/images/feats/cut_with_box_cube.png) 2233 2234 Check out also: 2235 `cut_with_line()`, `cut_with_plane()`, `cut_with_cylinder()` 2236 """ 2237 if isinstance(bounds, Points): 2238 bounds = bounds.bounds() 2239 2240 box = vtki.new("Box") 2241 if utils.is_sequence(bounds[0]): 2242 for bs in bounds: 2243 box.AddBounds(bs) 2244 else: 2245 box.SetBounds(bounds) 2246 2247 clipper = vtki.new("ClipPolyData") 2248 clipper.SetInputData(self.dataset) 2249 clipper.SetClipFunction(box) 2250 clipper.SetInsideOut(not invert) 2251 clipper.GenerateClippedOutputOff() 2252 clipper.GenerateClipScalarsOff() 2253 clipper.SetValue(0) 2254 clipper.Update() 2255 self._update(clipper.GetOutput()) 2256 2257 self.pipeline = utils.OperationNode("cut_with_box", parents=[self]) 2258 return self
Cut the current mesh with a box or a set of boxes.
This is much faster than cut_with_mesh()
.
Input bounds
can be either:
- a Mesh or Points object
- a list of 6 number representing a bounding box
[xmin,xmax, ymin,ymax, zmin,zmax]
- a list of bounding boxes like the above:
[[xmin1,...], [xmin2,...], ...]
Example:
from vedo import Sphere, Cube, show mesh = Sphere(r=1, res=50) box = Cube(side=1.5).wireframe() mesh.cut_with_box(box) show(mesh, box, axes=1).close()
Check out also:
2260 def cut_with_line(self, points, invert=False, closed=True) -> Self: 2261 """ 2262 Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter. 2263 The polyline is defined by a set of points (z-coordinates are ignored). 2264 This is much faster than `cut_with_mesh()`. 2265 2266 Check out also: 2267 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2268 """ 2269 pplane = vtki.new("PolyPlane") 2270 if isinstance(points, Points): 2271 points = points.vertices.tolist() 2272 2273 if closed: 2274 if isinstance(points, np.ndarray): 2275 points = points.tolist() 2276 points.append(points[0]) 2277 2278 vpoints = vtki.vtkPoints() 2279 for p in points: 2280 if len(p) == 2: 2281 p = [p[0], p[1], 0.0] 2282 vpoints.InsertNextPoint(p) 2283 2284 n = len(points) 2285 polyline = vtki.new("PolyLine") 2286 polyline.Initialize(n, vpoints) 2287 polyline.GetPointIds().SetNumberOfIds(n) 2288 for i in range(n): 2289 polyline.GetPointIds().SetId(i, i) 2290 pplane.SetPolyLine(polyline) 2291 2292 clipper = vtki.new("ClipPolyData") 2293 clipper.SetInputData(self.dataset) 2294 clipper.SetClipFunction(pplane) 2295 clipper.SetInsideOut(invert) 2296 clipper.GenerateClippedOutputOff() 2297 clipper.GenerateClipScalarsOff() 2298 clipper.SetValue(0) 2299 clipper.Update() 2300 self._update(clipper.GetOutput()) 2301 2302 self.pipeline = utils.OperationNode("cut_with_line", parents=[self]) 2303 return self
Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter.
The polyline is defined by a set of points (z-coordinates are ignored).
This is much faster than cut_with_mesh()
.
Check out also:
2381 def cut_with_cylinder(self, center=(0, 0, 0), axis=(0, 0, 1), r=1, invert=False) -> Self: 2382 """ 2383 Cut the current mesh with an infinite cylinder. 2384 This is much faster than `cut_with_mesh()`. 2385 2386 Arguments: 2387 center : (array) 2388 the center of the cylinder 2389 normal : (array) 2390 direction of the cylinder axis 2391 r : (float) 2392 radius of the cylinder 2393 2394 Example: 2395 ```python 2396 from vedo import Disc, show 2397 disc = Disc(r1=1, r2=1.2) 2398 mesh = disc.extrude(3, res=50).linewidth(1) 2399 mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True) 2400 show(mesh, axes=1).close() 2401 ``` 2402 ![](https://vedo.embl.es/images/feats/cut_with_cylinder.png) 2403 2404 Examples: 2405 - [optics_main1.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/optics_main1.py) 2406 2407 Check out also: 2408 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2409 """ 2410 s = str(axis) 2411 if "x" in s: 2412 axis = (1, 0, 0) 2413 elif "y" in s: 2414 axis = (0, 1, 0) 2415 elif "z" in s: 2416 axis = (0, 0, 1) 2417 cyl = vtki.new("Cylinder") 2418 cyl.SetCenter(center) 2419 cyl.SetAxis(axis[0], axis[1], axis[2]) 2420 cyl.SetRadius(r) 2421 2422 clipper = vtki.new("ClipPolyData") 2423 clipper.SetInputData(self.dataset) 2424 clipper.SetClipFunction(cyl) 2425 clipper.SetInsideOut(not invert) 2426 clipper.GenerateClippedOutputOff() 2427 clipper.GenerateClipScalarsOff() 2428 clipper.SetValue(0) 2429 clipper.Update() 2430 self._update(clipper.GetOutput()) 2431 2432 self.pipeline = utils.OperationNode("cut_with_cylinder", parents=[self]) 2433 return self
Cut the current mesh with an infinite cylinder.
This is much faster than cut_with_mesh()
.
Arguments:
- center : (array) the center of the cylinder
- normal : (array) direction of the cylinder axis
- r : (float) radius of the cylinder
Example:
from vedo import Disc, show disc = Disc(r1=1, r2=1.2) mesh = disc.extrude(3, res=50).linewidth(1) mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True) show(mesh, axes=1).close()
Examples:
Check out also:
2435 def cut_with_sphere(self, center=(0, 0, 0), r=1.0, invert=False) -> Self: 2436 """ 2437 Cut the current mesh with an sphere. 2438 This is much faster than `cut_with_mesh()`. 2439 2440 Arguments: 2441 center : (array) 2442 the center of the sphere 2443 r : (float) 2444 radius of the sphere 2445 2446 Example: 2447 ```python 2448 from vedo import Disc, show 2449 disc = Disc(r1=1, r2=1.2) 2450 mesh = disc.extrude(3, res=50).linewidth(1) 2451 mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True) 2452 show(mesh, axes=1).close() 2453 ``` 2454 ![](https://vedo.embl.es/images/feats/cut_with_sphere.png) 2455 2456 Check out also: 2457 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2458 """ 2459 sph = vtki.new("Sphere") 2460 sph.SetCenter(center) 2461 sph.SetRadius(r) 2462 2463 clipper = vtki.new("ClipPolyData") 2464 clipper.SetInputData(self.dataset) 2465 clipper.SetClipFunction(sph) 2466 clipper.SetInsideOut(not invert) 2467 clipper.GenerateClippedOutputOff() 2468 clipper.GenerateClipScalarsOff() 2469 clipper.SetValue(0) 2470 clipper.Update() 2471 self._update(clipper.GetOutput()) 2472 self.pipeline = utils.OperationNode("cut_with_sphere", parents=[self]) 2473 return self
Cut the current mesh with an sphere.
This is much faster than cut_with_mesh()
.
Arguments:
- center : (array) the center of the sphere
- r : (float) radius of the sphere
Example:
from vedo import Disc, show disc = Disc(r1=1, r2=1.2) mesh = disc.extrude(3, res=50).linewidth(1) mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True) show(mesh, axes=1).close()
Check out also:
2475 def cut_with_mesh(self, mesh, invert=False, keep=False) -> Union[Self, "vedo.Assembly"]: 2476 """ 2477 Cut an `Mesh` mesh with another `Mesh`. 2478 2479 Use `invert` to invert the selection. 2480 2481 Use `keep` to keep the cutoff part, in this case an `Assembly` is returned: 2482 the "cut" object and the "discarded" part of the original object. 2483 You can access both via `assembly.unpack()` method. 2484 2485 Example: 2486 ```python 2487 from vedo import * 2488 arr = np.random.randn(100000, 3)/2 2489 pts = Points(arr).c('red3').pos(5,0,0) 2490 cube = Cube().pos(4,0.5,0) 2491 assem = pts.cut_with_mesh(cube, keep=True) 2492 show(assem.unpack(), axes=1).close() 2493 ``` 2494 ![](https://vedo.embl.es/images/feats/cut_with_mesh.png) 2495 2496 Check out also: 2497 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2498 """ 2499 polymesh = mesh.dataset 2500 poly = self.dataset 2501 2502 # Create an array to hold distance information 2503 signed_distances = vtki.vtkFloatArray() 2504 signed_distances.SetNumberOfComponents(1) 2505 signed_distances.SetName("SignedDistances") 2506 2507 # implicit function that will be used to slice the mesh 2508 ippd = vtki.new("ImplicitPolyDataDistance") 2509 ippd.SetInput(polymesh) 2510 2511 # Evaluate the signed distance function at all of the grid points 2512 for pointId in range(poly.GetNumberOfPoints()): 2513 p = poly.GetPoint(pointId) 2514 signed_distance = ippd.EvaluateFunction(p) 2515 signed_distances.InsertNextValue(signed_distance) 2516 2517 currentscals = poly.GetPointData().GetScalars() 2518 if currentscals: 2519 currentscals = currentscals.GetName() 2520 2521 poly.GetPointData().AddArray(signed_distances) 2522 poly.GetPointData().SetActiveScalars("SignedDistances") 2523 2524 clipper = vtki.new("ClipPolyData") 2525 clipper.SetInputData(poly) 2526 clipper.SetInsideOut(not invert) 2527 clipper.SetGenerateClippedOutput(keep) 2528 clipper.SetValue(0.0) 2529 clipper.Update() 2530 cpoly = clipper.GetOutput() 2531 2532 if keep: 2533 kpoly = clipper.GetOutput(1) 2534 2535 vis = False 2536 if currentscals: 2537 cpoly.GetPointData().SetActiveScalars(currentscals) 2538 vis = self.mapper.GetScalarVisibility() 2539 2540 self._update(cpoly) 2541 2542 self.pointdata.remove("SignedDistances") 2543 self.mapper.SetScalarVisibility(vis) 2544 if keep: 2545 if isinstance(self, vedo.Mesh): 2546 cutoff = vedo.Mesh(kpoly) 2547 else: 2548 cutoff = vedo.Points(kpoly) 2549 # cutoff = self.__class__(kpoly) # this does not work properly 2550 cutoff.properties = vtki.vtkProperty() 2551 cutoff.properties.DeepCopy(self.properties) 2552 cutoff.actor.SetProperty(cutoff.properties) 2553 cutoff.c("k5").alpha(0.2) 2554 return vedo.Assembly([self, cutoff]) 2555 2556 self.pipeline = utils.OperationNode("cut_with_mesh", parents=[self, mesh]) 2557 return self
Cut an Mesh
mesh with another Mesh
.
Use invert
to invert the selection.
Use keep
to keep the cutoff part, in this case an Assembly
is returned:
the "cut" object and the "discarded" part of the original object.
You can access both via assembly.unpack()
method.
Example:
from vedo import *
arr = np.random.randn(100000, 3)/2
pts = Points(arr).c('red3').pos(5,0,0)
cube = Cube().pos(4,0.5,0)
assem = pts.cut_with_mesh(cube, keep=True)
show(assem.unpack(), axes=1).close()
Check out also:
2559 def cut_with_point_loop( 2560 self, points, invert=False, on="points", include_boundary=False 2561 ) -> Self: 2562 """ 2563 Cut an `Mesh` object with a set of points forming a closed loop. 2564 2565 Arguments: 2566 invert : (bool) 2567 invert selection (inside-out) 2568 on : (str) 2569 if 'cells' will extract the whole cells lying inside (or outside) the point loop 2570 include_boundary : (bool) 2571 include cells lying exactly on the boundary line. Only relevant on 'cells' mode 2572 2573 Examples: 2574 - [cut_with_points1.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points1.py) 2575 2576 ![](https://vedo.embl.es/images/advanced/cutWithPoints1.png) 2577 2578 - [cut_with_points2.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points2.py) 2579 2580 ![](https://vedo.embl.es/images/advanced/cutWithPoints2.png) 2581 """ 2582 if isinstance(points, Points): 2583 parents = [points] 2584 vpts = points.dataset.GetPoints() 2585 points = points.vertices 2586 else: 2587 parents = [self] 2588 vpts = vtki.vtkPoints() 2589 points = utils.make3d(points) 2590 for p in points: 2591 vpts.InsertNextPoint(p) 2592 2593 if "cell" in on: 2594 ippd = vtki.new("ImplicitSelectionLoop") 2595 ippd.SetLoop(vpts) 2596 ippd.AutomaticNormalGenerationOn() 2597 clipper = vtki.new("ExtractPolyDataGeometry") 2598 clipper.SetInputData(self.dataset) 2599 clipper.SetImplicitFunction(ippd) 2600 clipper.SetExtractInside(not invert) 2601 clipper.SetExtractBoundaryCells(include_boundary) 2602 else: 2603 spol = vtki.new("SelectPolyData") 2604 spol.SetLoop(vpts) 2605 spol.GenerateSelectionScalarsOn() 2606 spol.GenerateUnselectedOutputOff() 2607 spol.SetInputData(self.dataset) 2608 spol.Update() 2609 clipper = vtki.new("ClipPolyData") 2610 clipper.SetInputData(spol.GetOutput()) 2611 clipper.SetInsideOut(not invert) 2612 clipper.SetValue(0.0) 2613 clipper.Update() 2614 self._update(clipper.GetOutput()) 2615 2616 self.pipeline = utils.OperationNode("cut_with_pointloop", parents=parents) 2617 return self
Cut an Mesh
object with a set of points forming a closed loop.
Arguments:
- invert : (bool) invert selection (inside-out)
- on : (str) if 'cells' will extract the whole cells lying inside (or outside) the point loop
- include_boundary : (bool) include cells lying exactly on the boundary line. Only relevant on 'cells' mode
Examples:
2619 def cut_with_scalar(self, value: float, name="", invert=False) -> Self: 2620 """ 2621 Cut a mesh or point cloud with some input scalar point-data. 2622 2623 Arguments: 2624 value : (float) 2625 cutting value 2626 name : (str) 2627 array name of the scalars to be used 2628 invert : (bool) 2629 flip selection 2630 2631 Example: 2632 ```python 2633 from vedo import * 2634 s = Sphere().lw(1) 2635 pts = s.vertices 2636 scalars = np.sin(3*pts[:,2]) + pts[:,0] 2637 s.pointdata["somevalues"] = scalars 2638 s.cut_with_scalar(0.3) 2639 s.cmap("Spectral", "somevalues").add_scalarbar() 2640 s.show(axes=1).close() 2641 ``` 2642 ![](https://vedo.embl.es/images/feats/cut_with_scalars.png) 2643 """ 2644 if name: 2645 self.pointdata.select(name) 2646 clipper = vtki.new("ClipPolyData") 2647 clipper.SetInputData(self.dataset) 2648 clipper.SetValue(value) 2649 clipper.GenerateClippedOutputOff() 2650 clipper.SetInsideOut(not invert) 2651 clipper.Update() 2652 self._update(clipper.GetOutput()) 2653 self.pipeline = utils.OperationNode("cut_with_scalar", parents=[self]) 2654 return self
Cut a mesh or point cloud with some input scalar point-data.
Arguments:
- value : (float) cutting value
- name : (str) array name of the scalars to be used
- invert : (bool) flip selection
Example:
from vedo import * s = Sphere().lw(1) pts = s.vertices scalars = np.sin(3*pts[:,2]) + pts[:,0] s.pointdata["somevalues"] = scalars s.cut_with_scalar(0.3) s.cmap("Spectral", "somevalues").add_scalarbar() s.show(axes=1).close()
2656 def crop(self, 2657 top=None, bottom=None, right=None, left=None, front=None, back=None, 2658 bounds=()) -> Self: 2659 """ 2660 Crop an `Mesh` object. 2661 2662 Arguments: 2663 top : (float) 2664 fraction to crop from the top plane (positive z) 2665 bottom : (float) 2666 fraction to crop from the bottom plane (negative z) 2667 front : (float) 2668 fraction to crop from the front plane (positive y) 2669 back : (float) 2670 fraction to crop from the back plane (negative y) 2671 right : (float) 2672 fraction to crop from the right plane (positive x) 2673 left : (float) 2674 fraction to crop from the left plane (negative x) 2675 bounds : (list) 2676 bounding box of the crop region as `[x0,x1, y0,y1, z0,z1]` 2677 2678 Example: 2679 ```python 2680 from vedo import Sphere 2681 Sphere().crop(right=0.3, left=0.1).show() 2682 ``` 2683 ![](https://user-images.githubusercontent.com/32848391/57081955-0ef1e800-6cf6-11e9-99de-b45220939bc9.png) 2684 """ 2685 if not len(bounds): 2686 pos = np.array(self.pos()) 2687 x0, x1, y0, y1, z0, z1 = self.bounds() 2688 x0, y0, z0 = [x0, y0, z0] - pos 2689 x1, y1, z1 = [x1, y1, z1] - pos 2690 2691 dx, dy, dz = x1 - x0, y1 - y0, z1 - z0 2692 if top: 2693 z1 = z1 - top * dz 2694 if bottom: 2695 z0 = z0 + bottom * dz 2696 if front: 2697 y1 = y1 - front * dy 2698 if back: 2699 y0 = y0 + back * dy 2700 if right: 2701 x1 = x1 - right * dx 2702 if left: 2703 x0 = x0 + left * dx 2704 bounds = (x0, x1, y0, y1, z0, z1) 2705 2706 cu = vtki.new("Box") 2707 cu.SetBounds(bounds) 2708 2709 clipper = vtki.new("ClipPolyData") 2710 clipper.SetInputData(self.dataset) 2711 clipper.SetClipFunction(cu) 2712 clipper.InsideOutOn() 2713 clipper.GenerateClippedOutputOff() 2714 clipper.GenerateClipScalarsOff() 2715 clipper.SetValue(0) 2716 clipper.Update() 2717 self._update(clipper.GetOutput()) 2718 2719 self.pipeline = utils.OperationNode( 2720 "crop", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 2721 ) 2722 return self
Crop an Mesh
object.
Arguments:
- top : (float) fraction to crop from the top plane (positive z)
- bottom : (float) fraction to crop from the bottom plane (negative z)
- front : (float) fraction to crop from the front plane (positive y)
- back : (float) fraction to crop from the back plane (negative y)
- right : (float) fraction to crop from the right plane (positive x)
- left : (float) fraction to crop from the left plane (negative x)
- bounds : (list)
bounding box of the crop region as
[x0,x1, y0,y1, z0,z1]
Example:
from vedo import Sphere Sphere().crop(right=0.3, left=0.1).show()
2724 def generate_surface_halo( 2725 self, 2726 distance=0.05, 2727 res=(50, 50, 50), 2728 bounds=(), 2729 maxdist=None, 2730 ) -> "vedo.Mesh": 2731 """ 2732 Generate the surface halo which sits at the specified distance from the input one. 2733 2734 Arguments: 2735 distance : (float) 2736 distance from the input surface 2737 res : (int) 2738 resolution of the surface 2739 bounds : (list) 2740 bounding box of the surface 2741 maxdist : (float) 2742 maximum distance to generate the surface 2743 """ 2744 if not bounds: 2745 bounds = self.bounds() 2746 2747 if not maxdist: 2748 maxdist = self.diagonal_size() / 2 2749 2750 imp = vtki.new("ImplicitModeller") 2751 imp.SetInputData(self.dataset) 2752 imp.SetSampleDimensions(res) 2753 if maxdist: 2754 imp.SetMaximumDistance(maxdist) 2755 if len(bounds) == 6: 2756 imp.SetModelBounds(bounds) 2757 contour = vtki.new("ContourFilter") 2758 contour.SetInputConnection(imp.GetOutputPort()) 2759 contour.SetValue(0, distance) 2760 contour.Update() 2761 out = vedo.Mesh(contour.GetOutput()) 2762 out.c("lightblue").alpha(0.25).lighting("off") 2763 out.pipeline = utils.OperationNode("generate_surface_halo", parents=[self]) 2764 return out
Generate the surface halo which sits at the specified distance from the input one.
Arguments:
- distance : (float) distance from the input surface
- res : (int) resolution of the surface
- bounds : (list) bounding box of the surface
- maxdist : (float) maximum distance to generate the surface
2766 def generate_mesh( 2767 self, 2768 line_resolution=None, 2769 mesh_resolution=None, 2770 smooth=0.0, 2771 jitter=0.001, 2772 grid=None, 2773 quads=False, 2774 invert=False, 2775 ) -> Self: 2776 """ 2777 Generate a polygonal Mesh from a closed contour line. 2778 If line is not closed it will be closed with a straight segment. 2779 2780 Check also `generate_delaunay2d()`. 2781 2782 Arguments: 2783 line_resolution : (int) 2784 resolution of the contour line. The default is None, in this case 2785 the contour is not resampled. 2786 mesh_resolution : (int) 2787 resolution of the internal triangles not touching the boundary. 2788 smooth : (float) 2789 smoothing of the contour before meshing. 2790 jitter : (float) 2791 add a small noise to the internal points. 2792 grid : (Grid) 2793 manually pass a Grid object. The default is True. 2794 quads : (bool) 2795 generate a mesh of quads instead of triangles. 2796 invert : (bool) 2797 flip the line orientation. The default is False. 2798 2799 Examples: 2800 - [line2mesh_tri.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_tri.py) 2801 2802 ![](https://vedo.embl.es/images/advanced/line2mesh_tri.jpg) 2803 2804 - [line2mesh_quads.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_quads.py) 2805 2806 ![](https://vedo.embl.es/images/advanced/line2mesh_quads.png) 2807 """ 2808 if line_resolution is None: 2809 contour = vedo.shapes.Line(self.vertices) 2810 else: 2811 contour = vedo.shapes.Spline(self.vertices, smooth=smooth, res=line_resolution) 2812 contour.clean() 2813 2814 length = contour.length() 2815 density = length / contour.npoints 2816 # print(f"tomesh():\n\tline length = {length}") 2817 # print(f"\tdensity = {density} length/pt_separation") 2818 2819 x0, x1 = contour.xbounds() 2820 y0, y1 = contour.ybounds() 2821 2822 if grid is None: 2823 if mesh_resolution is None: 2824 resx = int((x1 - x0) / density + 0.5) 2825 resy = int((y1 - y0) / density + 0.5) 2826 # print(f"tmesh_resolution = {[resx, resy]}") 2827 else: 2828 if utils.is_sequence(mesh_resolution): 2829 resx, resy = mesh_resolution 2830 else: 2831 resx, resy = mesh_resolution, mesh_resolution 2832 grid = vedo.shapes.Grid( 2833 [(x0 + x1) / 2, (y0 + y1) / 2, 0], 2834 s=((x1 - x0) * 1.025, (y1 - y0) * 1.025), 2835 res=(resx, resy), 2836 ) 2837 else: 2838 grid = grid.clone() 2839 2840 cpts = contour.vertices 2841 2842 # make sure it's closed 2843 p0, p1 = cpts[0], cpts[-1] 2844 nj = max(2, int(utils.mag(p1 - p0) / density + 0.5)) 2845 joinline = vedo.shapes.Line(p1, p0, res=nj) 2846 contour = vedo.merge(contour, joinline).subsample(0.0001) 2847 2848 ####################################### quads 2849 if quads: 2850 cmesh = grid.clone().cut_with_point_loop(contour, on="cells", invert=invert) 2851 cmesh.wireframe(False).lw(0.5) 2852 cmesh.pipeline = utils.OperationNode( 2853 "generate_mesh", 2854 parents=[self, contour], 2855 comment=f"#quads {cmesh.dataset.GetNumberOfCells()}", 2856 ) 2857 return cmesh 2858 ############################################# 2859 2860 grid_tmp = grid.vertices.copy() 2861 2862 if jitter: 2863 np.random.seed(0) 2864 sigma = 1.0 / np.sqrt(grid.npoints) * grid.diagonal_size() * jitter 2865 # print(f"\tsigma jittering = {sigma}") 2866 grid_tmp += np.random.rand(grid.npoints, 3) * sigma 2867 grid_tmp[:, 2] = 0.0 2868 2869 todel = [] 2870 density /= np.sqrt(3) 2871 vgrid_tmp = Points(grid_tmp) 2872 2873 for p in contour.vertices: 2874 out = vgrid_tmp.closest_point(p, radius=density, return_point_id=True) 2875 todel += out.tolist() 2876 2877 grid_tmp = grid_tmp.tolist() 2878 for index in sorted(list(set(todel)), reverse=True): 2879 del grid_tmp[index] 2880 2881 points = contour.vertices.tolist() + grid_tmp 2882 if invert: 2883 boundary = list(reversed(range(contour.npoints))) 2884 else: 2885 boundary = list(range(contour.npoints)) 2886 2887 dln = Points(points).generate_delaunay2d(mode="xy", boundaries=[boundary]) 2888 dln.compute_normals(points=False) # fixes reversd faces 2889 dln.lw(1) 2890 2891 dln.pipeline = utils.OperationNode( 2892 "generate_mesh", 2893 parents=[self, contour], 2894 comment=f"#cells {dln.dataset.GetNumberOfCells()}", 2895 ) 2896 return dln
Generate a polygonal Mesh from a closed contour line. If line is not closed it will be closed with a straight segment.
Check also generate_delaunay2d()
.
Arguments:
- line_resolution : (int) resolution of the contour line. The default is None, in this case the contour is not resampled.
- mesh_resolution : (int) resolution of the internal triangles not touching the boundary.
- smooth : (float) smoothing of the contour before meshing.
- jitter : (float) add a small noise to the internal points.
- grid : (Grid) manually pass a Grid object. The default is True.
- quads : (bool) generate a mesh of quads instead of triangles.
- invert : (bool) flip the line orientation. The default is False.
Examples:
2898 def reconstruct_surface( 2899 self, 2900 dims=(100, 100, 100), 2901 radius=None, 2902 sample_size=None, 2903 hole_filling=True, 2904 bounds=(), 2905 padding=0.05, 2906 ) -> "vedo.Mesh": 2907 """ 2908 Surface reconstruction from a scattered cloud of points. 2909 2910 Arguments: 2911 dims : (int) 2912 number of voxels in x, y and z to control precision. 2913 radius : (float) 2914 radius of influence of each point. 2915 Smaller values generally improve performance markedly. 2916 Note that after the signed distance function is computed, 2917 any voxel taking on the value >= radius 2918 is presumed to be "unseen" or uninitialized. 2919 sample_size : (int) 2920 if normals are not present 2921 they will be calculated using this sample size per point. 2922 hole_filling : (bool) 2923 enables hole filling, this generates 2924 separating surfaces between the empty and unseen portions of the volume. 2925 bounds : (list) 2926 region in space in which to perform the sampling 2927 in format (xmin,xmax, ymin,ymax, zim, zmax) 2928 padding : (float) 2929 increase by this fraction the bounding box 2930 2931 Examples: 2932 - [recosurface.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/recosurface.py) 2933 2934 ![](https://vedo.embl.es/images/advanced/recosurface.png) 2935 """ 2936 if not utils.is_sequence(dims): 2937 dims = (dims, dims, dims) 2938 2939 sdf = vtki.new("SignedDistance") 2940 2941 if len(bounds) == 6: 2942 sdf.SetBounds(bounds) 2943 else: 2944 x0, x1, y0, y1, z0, z1 = self.bounds() 2945 sdf.SetBounds( 2946 x0 - (x1 - x0) * padding, 2947 x1 + (x1 - x0) * padding, 2948 y0 - (y1 - y0) * padding, 2949 y1 + (y1 - y0) * padding, 2950 z0 - (z1 - z0) * padding, 2951 z1 + (z1 - z0) * padding, 2952 ) 2953 2954 bb = sdf.GetBounds() 2955 if bb[0]==bb[1]: 2956 vedo.logger.warning("reconstruct_surface(): zero x-range") 2957 if bb[2]==bb[3]: 2958 vedo.logger.warning("reconstruct_surface(): zero y-range") 2959 if bb[4]==bb[5]: 2960 vedo.logger.warning("reconstruct_surface(): zero z-range") 2961 2962 pd = self.dataset 2963 2964 if pd.GetPointData().GetNormals(): 2965 sdf.SetInputData(pd) 2966 else: 2967 normals = vtki.new("PCANormalEstimation") 2968 normals.SetInputData(pd) 2969 if not sample_size: 2970 sample_size = int(pd.GetNumberOfPoints() / 50) 2971 normals.SetSampleSize(sample_size) 2972 normals.SetNormalOrientationToGraphTraversal() 2973 sdf.SetInputConnection(normals.GetOutputPort()) 2974 # print("Recalculating normals with sample size =", sample_size) 2975 2976 if radius is None: 2977 radius = self.diagonal_size() / (sum(dims) / 3) * 5 2978 # print("Calculating mesh from points with radius =", radius) 2979 2980 sdf.SetRadius(radius) 2981 sdf.SetDimensions(dims) 2982 sdf.Update() 2983 2984 surface = vtki.new("ExtractSurface") 2985 surface.SetRadius(radius * 0.99) 2986 surface.SetHoleFilling(hole_filling) 2987 surface.ComputeNormalsOff() 2988 surface.ComputeGradientsOff() 2989 surface.SetInputConnection(sdf.GetOutputPort()) 2990 surface.Update() 2991 m = vedo.mesh.Mesh(surface.GetOutput(), c=self.color()) 2992 2993 m.pipeline = utils.OperationNode( 2994 "reconstruct_surface", 2995 parents=[self], 2996 comment=f"#pts {m.dataset.GetNumberOfPoints()}", 2997 ) 2998 return m
Surface reconstruction from a scattered cloud of points.
Arguments:
- dims : (int) number of voxels in x, y and z to control precision.
- radius : (float) radius of influence of each point. Smaller values generally improve performance markedly. Note that after the signed distance function is computed, any voxel taking on the value >= radius is presumed to be "unseen" or uninitialized.
- sample_size : (int) if normals are not present they will be calculated using this sample size per point.
- hole_filling : (bool) enables hole filling, this generates separating surfaces between the empty and unseen portions of the volume.
- bounds : (list) region in space in which to perform the sampling in format (xmin,xmax, ymin,ymax, zim, zmax)
- padding : (float) increase by this fraction the bounding box
Examples:
3000 def compute_clustering(self, radius: float) -> Self: 3001 """ 3002 Cluster points in space. The `radius` is the radius of local search. 3003 3004 An array named "ClusterId" is added to `pointdata`. 3005 3006 Examples: 3007 - [clustering.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/clustering.py) 3008 3009 ![](https://vedo.embl.es/images/basic/clustering.png) 3010 """ 3011 cluster = vtki.new("EuclideanClusterExtraction") 3012 cluster.SetInputData(self.dataset) 3013 cluster.SetExtractionModeToAllClusters() 3014 cluster.SetRadius(radius) 3015 cluster.ColorClustersOn() 3016 cluster.Update() 3017 idsarr = cluster.GetOutput().GetPointData().GetArray("ClusterId") 3018 self.dataset.GetPointData().AddArray(idsarr) 3019 self.pipeline = utils.OperationNode( 3020 "compute_clustering", parents=[self], comment=f"radius = {radius}" 3021 ) 3022 return self
Cluster points in space. The radius
is the radius of local search.
An array named "ClusterId" is added to pointdata
.
Examples:
3024 def compute_connections(self, radius, mode=0, regions=(), vrange=(0, 1), seeds=(), angle=0.0) -> Self: 3025 """ 3026 Extracts and/or segments points from a point cloud based on geometric distance measures 3027 (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range. 3028 The default operation is to segment the points into "connected" regions where the connection 3029 is determined by an appropriate distance measure. Each region is given a region id. 3030 3031 Optionally, the filter can output the largest connected region of points; a particular region 3032 (via id specification); those regions that are seeded using a list of input point ids; 3033 or the region of points closest to a specified position. 3034 3035 The key parameter of this filter is the radius defining a sphere around each point which defines 3036 a local neighborhood: any other points in the local neighborhood are assumed connected to the point. 3037 Note that the radius is defined in absolute terms. 3038 3039 Other parameters are used to further qualify what it means to be a neighboring point. 3040 For example, scalar range and/or point normals can be used to further constrain the neighborhood. 3041 Also the extraction mode defines how the filter operates. 3042 By default, all regions are extracted but it is possible to extract particular regions; 3043 the region closest to a seed point; seeded regions; or the largest region found while processing. 3044 By default, all regions are extracted. 3045 3046 On output, all points are labeled with a region number. 3047 However note that the number of input and output points may not be the same: 3048 if not extracting all regions then the output size may be less than the input size. 3049 3050 Arguments: 3051 radius : (float) 3052 variable specifying a local sphere used to define local point neighborhood 3053 mode : (int) 3054 - 0, Extract all regions 3055 - 1, Extract point seeded regions 3056 - 2, Extract largest region 3057 - 3, Test specified regions 3058 - 4, Extract all regions with scalar connectivity 3059 - 5, Extract point seeded regions 3060 regions : (list) 3061 a list of non-negative regions id to extract 3062 vrange : (list) 3063 scalar range to use to extract points based on scalar connectivity 3064 seeds : (list) 3065 a list of non-negative point seed ids 3066 angle : (list) 3067 points are connected if the angle between their normals is 3068 within this angle threshold (expressed in degrees). 3069 """ 3070 # https://vtk.org/doc/nightly/html/classvtkConnectedPointsFilter.html 3071 cpf = vtki.new("ConnectedPointsFilter") 3072 cpf.SetInputData(self.dataset) 3073 cpf.SetRadius(radius) 3074 if mode == 0: # Extract all regions 3075 pass 3076 3077 elif mode == 1: # Extract point seeded regions 3078 cpf.SetExtractionModeToPointSeededRegions() 3079 for s in seeds: 3080 cpf.AddSeed(s) 3081 3082 elif mode == 2: # Test largest region 3083 cpf.SetExtractionModeToLargestRegion() 3084 3085 elif mode == 3: # Test specified regions 3086 cpf.SetExtractionModeToSpecifiedRegions() 3087 for r in regions: 3088 cpf.AddSpecifiedRegion(r) 3089 3090 elif mode == 4: # Extract all regions with scalar connectivity 3091 cpf.SetExtractionModeToLargestRegion() 3092 cpf.ScalarConnectivityOn() 3093 cpf.SetScalarRange(vrange[0], vrange[1]) 3094 3095 elif mode == 5: # Extract point seeded regions 3096 cpf.SetExtractionModeToLargestRegion() 3097 cpf.ScalarConnectivityOn() 3098 cpf.SetScalarRange(vrange[0], vrange[1]) 3099 cpf.AlignedNormalsOn() 3100 cpf.SetNormalAngle(angle) 3101 3102 cpf.Update() 3103 self._update(cpf.GetOutput(), reset_locators=False) 3104 return self
Extracts and/or segments points from a point cloud based on geometric distance measures (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range. The default operation is to segment the points into "connected" regions where the connection is determined by an appropriate distance measure. Each region is given a region id.
Optionally, the filter can output the largest connected region of points; a particular region (via id specification); those regions that are seeded using a list of input point ids; or the region of points closest to a specified position.
The key parameter of this filter is the radius defining a sphere around each point which defines a local neighborhood: any other points in the local neighborhood are assumed connected to the point. Note that the radius is defined in absolute terms.
Other parameters are used to further qualify what it means to be a neighboring point. For example, scalar range and/or point normals can be used to further constrain the neighborhood. Also the extraction mode defines how the filter operates. By default, all regions are extracted but it is possible to extract particular regions; the region closest to a seed point; seeded regions; or the largest region found while processing. By default, all regions are extracted.
On output, all points are labeled with a region number. However note that the number of input and output points may not be the same: if not extracting all regions then the output size may be less than the input size.
Arguments:
- radius : (float) variable specifying a local sphere used to define local point neighborhood
- mode : (int)
- 0, Extract all regions
- 1, Extract point seeded regions
- 2, Extract largest region
- 3, Test specified regions
- 4, Extract all regions with scalar connectivity
- 5, Extract point seeded regions
- regions : (list) a list of non-negative regions id to extract
- vrange : (list) scalar range to use to extract points based on scalar connectivity
- seeds : (list) a list of non-negative point seed ids
- angle : (list) points are connected if the angle between their normals is within this angle threshold (expressed in degrees).
3106 def compute_camera_distance(self) -> np.ndarray: 3107 """ 3108 Calculate the distance from points to the camera. 3109 3110 A pointdata array is created with name 'DistanceToCamera' and returned. 3111 """ 3112 if vedo.plotter_instance and vedo.plotter_instance.renderer: 3113 poly = self.dataset 3114 dc = vtki.new("DistanceToCamera") 3115 dc.SetInputData(poly) 3116 dc.SetRenderer(vedo.plotter_instance.renderer) 3117 dc.Update() 3118 self._update(dc.GetOutput(), reset_locators=False) 3119 return self.pointdata["DistanceToCamera"] 3120 return np.array([])
Calculate the distance from points to the camera.
A pointdata array is created with name 'DistanceToCamera' and returned.
3122 def densify(self, target_distance=0.1, nclosest=6, radius=None, niter=1, nmax=None) -> Self: 3123 """ 3124 Return a copy of the cloud with new added points. 3125 The new points are created in such a way that all points in any local neighborhood are 3126 within a target distance of one another. 3127 3128 For each input point, the distance to all points in its neighborhood is computed. 3129 If any of its neighbors is further than the target distance, 3130 the edge connecting the point and its neighbor is bisected and 3131 a new point is inserted at the bisection point. 3132 A single pass is completed once all the input points are visited. 3133 Then the process repeats to the number of iterations. 3134 3135 Examples: 3136 - [densifycloud.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/densifycloud.py) 3137 3138 ![](https://vedo.embl.es/images/volumetric/densifycloud.png) 3139 3140 .. note:: 3141 Points will be created in an iterative fashion until all points in their 3142 local neighborhood are the target distance apart or less. 3143 Note that the process may terminate early due to the 3144 number of iterations. By default the target distance is set to 0.5. 3145 Note that the target_distance should be less than the radius 3146 or nothing will change on output. 3147 3148 .. warning:: 3149 This class can generate a lot of points very quickly. 3150 The maximum number of iterations is by default set to =1.0 for this reason. 3151 Increase the number of iterations very carefully. 3152 Also, `nmax` can be set to limit the explosion of points. 3153 It is also recommended that a N closest neighborhood is used. 3154 3155 """ 3156 src = vtki.new("ProgrammableSource") 3157 opts = self.vertices 3158 # zeros = np.zeros(3) 3159 3160 def _read_points(): 3161 output = src.GetPolyDataOutput() 3162 points = vtki.vtkPoints() 3163 for p in opts: 3164 # print(p) 3165 # if not np.array_equal(p, zeros): 3166 points.InsertNextPoint(p) 3167 output.SetPoints(points) 3168 3169 src.SetExecuteMethod(_read_points) 3170 3171 dens = vtki.new("DensifyPointCloudFilter") 3172 dens.SetInputConnection(src.GetOutputPort()) 3173 # dens.SetInputData(self.dataset) # this does not work 3174 dens.InterpolateAttributeDataOn() 3175 dens.SetTargetDistance(target_distance) 3176 dens.SetMaximumNumberOfIterations(niter) 3177 if nmax: 3178 dens.SetMaximumNumberOfPoints(nmax) 3179 3180 if radius: 3181 dens.SetNeighborhoodTypeToRadius() 3182 dens.SetRadius(radius) 3183 elif nclosest: 3184 dens.SetNeighborhoodTypeToNClosest() 3185 dens.SetNumberOfClosestPoints(nclosest) 3186 else: 3187 vedo.logger.error("set either radius or nclosest") 3188 raise RuntimeError() 3189 dens.Update() 3190 3191 cld = Points(dens.GetOutput()) 3192 cld.copy_properties_from(self) 3193 cld.interpolate_data_from(self, n=nclosest, radius=radius) 3194 cld.name = "DensifiedCloud" 3195 cld.pipeline = utils.OperationNode( 3196 "densify", 3197 parents=[self], 3198 c="#e9c46a:", 3199 comment=f"#pts {cld.dataset.GetNumberOfPoints()}", 3200 ) 3201 return cld
Return a copy of the cloud with new added points. The new points are created in such a way that all points in any local neighborhood are within a target distance of one another.
For each input point, the distance to all points in its neighborhood is computed. If any of its neighbors is further than the target distance, the edge connecting the point and its neighbor is bisected and a new point is inserted at the bisection point. A single pass is completed once all the input points are visited. Then the process repeats to the number of iterations.
Examples:
Points will be created in an iterative fashion until all points in their local neighborhood are the target distance apart or less. Note that the process may terminate early due to the number of iterations. By default the target distance is set to 0.5. Note that the target_distance should be less than the radius or nothing will change on output.
This class can generate a lot of points very quickly.
The maximum number of iterations is by default set to =1.0 for this reason.
Increase the number of iterations very carefully.
Also, nmax
can be set to limit the explosion of points.
It is also recommended that a N closest neighborhood is used.
3207 def density( 3208 self, dims=(40, 40, 40), bounds=None, radius=None, compute_gradient=False, locator=None 3209 ) -> "vedo.Volume": 3210 """ 3211 Generate a density field from a point cloud. Input can also be a set of 3D coordinates. 3212 Output is a `Volume`. 3213 3214 The local neighborhood is specified as the `radius` around each sample position (each voxel). 3215 If left to None, the radius is automatically computed as the diagonal of the bounding box 3216 and can be accessed via `vol.metadata["radius"]`. 3217 The density is expressed as the number of counts in the radius search. 3218 3219 Arguments: 3220 dims : (int, list) 3221 number of voxels in x, y and z of the output Volume. 3222 compute_gradient : (bool) 3223 Turn on/off the generation of the gradient vector, 3224 gradient magnitude scalar, and function classification scalar. 3225 By default this is off. Note that this will increase execution time 3226 and the size of the output. (The names of these point data arrays are: 3227 "Gradient", "Gradient Magnitude", and "Classification") 3228 locator : (vtkPointLocator) 3229 can be assigned from a previous call for speed (access it via `object.point_locator`). 3230 3231 Examples: 3232 - [plot_density3d.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/plot_density3d.py) 3233 3234 ![](https://vedo.embl.es/images/pyplot/plot_density3d.png) 3235 """ 3236 pdf = vtki.new("PointDensityFilter") 3237 pdf.SetInputData(self.dataset) 3238 3239 if not utils.is_sequence(dims): 3240 dims = [dims, dims, dims] 3241 3242 if bounds is None: 3243 bounds = list(self.bounds()) 3244 elif len(bounds) == 4: 3245 bounds = [*bounds, 0, 0] 3246 3247 if bounds[5] - bounds[4] == 0 or len(dims) == 2: # its 2D 3248 dims = list(dims) 3249 dims = [dims[0], dims[1], 2] 3250 diag = self.diagonal_size() 3251 bounds[5] = bounds[4] + diag / 1000 3252 pdf.SetModelBounds(bounds) 3253 3254 pdf.SetSampleDimensions(dims) 3255 3256 if locator: 3257 pdf.SetLocator(locator) 3258 3259 pdf.SetDensityEstimateToFixedRadius() 3260 if radius is None: 3261 radius = self.diagonal_size() / 20 3262 pdf.SetRadius(radius) 3263 pdf.SetComputeGradient(compute_gradient) 3264 pdf.Update() 3265 3266 vol = vedo.Volume(pdf.GetOutput()).mode(1) 3267 vol.name = "PointDensity" 3268 vol.metadata["radius"] = radius 3269 vol.locator = pdf.GetLocator() 3270 vol.pipeline = utils.OperationNode( 3271 "density", parents=[self], comment=f"dims={tuple(vol.dimensions())}" 3272 ) 3273 return vol
Generate a density field from a point cloud. Input can also be a set of 3D coordinates.
Output is a Volume
.
The local neighborhood is specified as the radius
around each sample position (each voxel).
If left to None, the radius is automatically computed as the diagonal of the bounding box
and can be accessed via vol.metadata["radius"]
.
The density is expressed as the number of counts in the radius search.
Arguments:
- dims : (int, list) number of voxels in x, y and z of the output Volume.
- compute_gradient : (bool) Turn on/off the generation of the gradient vector, gradient magnitude scalar, and function classification scalar. By default this is off. Note that this will increase execution time and the size of the output. (The names of these point data arrays are: "Gradient", "Gradient Magnitude", and "Classification")
- locator : (vtkPointLocator)
can be assigned from a previous call for speed (access it via
object.point_locator
).
Examples:
3276 def tovolume( 3277 self, 3278 kernel="shepard", 3279 radius=None, 3280 n=None, 3281 bounds=None, 3282 null_value=None, 3283 dims=(25, 25, 25), 3284 ) -> "vedo.Volume": 3285 """ 3286 Generate a `Volume` by interpolating a scalar 3287 or vector field which is only known on a scattered set of points or mesh. 3288 Available interpolation kernels are: shepard, gaussian, or linear. 3289 3290 Arguments: 3291 kernel : (str) 3292 interpolation kernel type [shepard] 3293 radius : (float) 3294 radius of the local search 3295 n : (int) 3296 number of point to use for interpolation 3297 bounds : (list) 3298 bounding box of the output Volume object 3299 dims : (list) 3300 dimensions of the output Volume object 3301 null_value : (float) 3302 value to be assigned to invalid points 3303 3304 Examples: 3305 - [interpolate_volume.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/interpolate_volume.py) 3306 3307 ![](https://vedo.embl.es/images/volumetric/59095175-1ec5a300-8918-11e9-8bc0-fd35c8981e2b.jpg) 3308 """ 3309 if radius is None and not n: 3310 vedo.logger.error("please set either radius or n") 3311 raise RuntimeError 3312 3313 poly = self.dataset 3314 3315 # Create a probe volume 3316 probe = vtki.vtkImageData() 3317 probe.SetDimensions(dims) 3318 if bounds is None: 3319 bounds = self.bounds() 3320 probe.SetOrigin(bounds[0], bounds[2], bounds[4]) 3321 probe.SetSpacing( 3322 (bounds[1] - bounds[0]) / dims[0], 3323 (bounds[3] - bounds[2]) / dims[1], 3324 (bounds[5] - bounds[4]) / dims[2], 3325 ) 3326 3327 if not self.point_locator: 3328 self.point_locator = vtki.new("PointLocator") 3329 self.point_locator.SetDataSet(poly) 3330 self.point_locator.BuildLocator() 3331 3332 if kernel == "shepard": 3333 kern = vtki.new("ShepardKernel") 3334 kern.SetPowerParameter(2) 3335 elif kernel == "gaussian": 3336 kern = vtki.new("GaussianKernel") 3337 elif kernel == "linear": 3338 kern = vtki.new("LinearKernel") 3339 else: 3340 vedo.logger.error("Error in tovolume(), available kernels are:") 3341 vedo.logger.error(" [shepard, gaussian, linear]") 3342 raise RuntimeError() 3343 3344 if radius: 3345 kern.SetRadius(radius) 3346 3347 interpolator = vtki.new("PointInterpolator") 3348 interpolator.SetInputData(probe) 3349 interpolator.SetSourceData(poly) 3350 interpolator.SetKernel(kern) 3351 interpolator.SetLocator(self.point_locator) 3352 3353 if n: 3354 kern.SetNumberOfPoints(n) 3355 kern.SetKernelFootprintToNClosest() 3356 else: 3357 kern.SetRadius(radius) 3358 3359 if null_value is not None: 3360 interpolator.SetNullValue(null_value) 3361 else: 3362 interpolator.SetNullPointsStrategyToClosestPoint() 3363 interpolator.Update() 3364 3365 vol = vedo.Volume(interpolator.GetOutput()) 3366 3367 vol.pipeline = utils.OperationNode( 3368 "signed_distance", 3369 parents=[self], 3370 comment=f"dims={tuple(vol.dimensions())}", 3371 c="#e9c46a:#0096c7", 3372 ) 3373 return vol
Generate a Volume
by interpolating a scalar
or vector field which is only known on a scattered set of points or mesh.
Available interpolation kernels are: shepard, gaussian, or linear.
Arguments:
- kernel : (str) interpolation kernel type [shepard]
- radius : (float) radius of the local search
- n : (int) number of point to use for interpolation
- bounds : (list) bounding box of the output Volume object
- dims : (list) dimensions of the output Volume object
- null_value : (float) value to be assigned to invalid points
Examples:
3376 def generate_segments(self, istart=0, rmax=1e30, niter=3) -> "vedo.shapes.Lines": 3377 """ 3378 Generate a line segments from a set of points. 3379 The algorithm is based on the closest point search. 3380 3381 Returns a `Line` object. 3382 This object contains the a metadata array of used vertex counts in "UsedVertexCount" 3383 and the sum of the length of the segments in "SegmentsLengthSum". 3384 3385 Arguments: 3386 istart : (int) 3387 index of the starting point 3388 rmax : (float) 3389 maximum length of a segment 3390 niter : (int) 3391 number of iterations or passes through the points 3392 3393 Examples: 3394 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 3395 """ 3396 points = self.vertices 3397 segments = [] 3398 dists = [] 3399 n = len(points) 3400 used = np.zeros(n, dtype=int) 3401 for _ in range(niter): 3402 i = istart 3403 for _ in range(n): 3404 p = points[i] 3405 ids = self.closest_point(p, n=4, return_point_id=True) 3406 j = ids[1] 3407 if used[j] > 1 or [j, i] in segments: 3408 j = ids[2] 3409 if used[j] > 1: 3410 j = ids[3] 3411 d = np.linalg.norm(p - points[j]) 3412 if used[j] > 1 or used[i] > 1 or d > rmax: 3413 i += 1 3414 if i >= n: 3415 i = 0 3416 continue 3417 used[i] += 1 3418 used[j] += 1 3419 segments.append([i, j]) 3420 dists.append(d) 3421 i = j 3422 segments = np.array(segments, dtype=int) 3423 3424 lines = vedo.shapes.Lines(points[segments], c="k", lw=3) 3425 lines.metadata["UsedVertexCount"] = used 3426 lines.metadata["SegmentsLengthSum"] = np.sum(dists) 3427 lines.pipeline = utils.OperationNode("generate_segments", parents=[self]) 3428 lines.name = "Segments" 3429 return lines
Generate a line segments from a set of points. The algorithm is based on the closest point search.
Returns a Line
object.
This object contains the a metadata array of used vertex counts in "UsedVertexCount"
and the sum of the length of the segments in "SegmentsLengthSum".
Arguments:
- istart : (int) index of the starting point
- rmax : (float) maximum length of a segment
- niter : (int) number of iterations or passes through the points
Examples:
3431 def generate_delaunay2d( 3432 self, 3433 mode="scipy", 3434 boundaries=(), 3435 tol=None, 3436 alpha=0.0, 3437 offset=0.0, 3438 transform=None, 3439 ) -> "vedo.mesh.Mesh": 3440 """ 3441 Create a mesh from points in the XY plane. 3442 If `mode='fit'` then the filter computes a best fitting 3443 plane and projects the points onto it. 3444 3445 Check also `generate_mesh()`. 3446 3447 Arguments: 3448 tol : (float) 3449 specify a tolerance to control discarding of closely spaced points. 3450 This tolerance is specified as a fraction of the diagonal length of the bounding box of the points. 3451 alpha : (float) 3452 for a non-zero alpha value, only edges or triangles contained 3453 within a sphere centered at mesh vertices will be output. 3454 Otherwise, only triangles will be output. 3455 offset : (float) 3456 multiplier to control the size of the initial, bounding Delaunay triangulation. 3457 transform: (LinearTransform, NonLinearTransform) 3458 a transformation which is applied to points to generate a 2D problem. 3459 This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane. 3460 The points are transformed and triangulated. 3461 The topology of triangulated points is used as the output topology. 3462 3463 Examples: 3464 - [delaunay2d.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delaunay2d.py) 3465 3466 ![](https://vedo.embl.es/images/basic/delaunay2d.png) 3467 """ 3468 plist = self.vertices.copy() 3469 3470 ######################################################### 3471 if mode == "scipy": 3472 from scipy.spatial import Delaunay as scipy_delaunay 3473 3474 tri = scipy_delaunay(plist[:, 0:2]) 3475 return vedo.mesh.Mesh([plist, tri.simplices]) 3476 ########################################################## 3477 3478 pd = vtki.vtkPolyData() 3479 vpts = vtki.vtkPoints() 3480 vpts.SetData(utils.numpy2vtk(plist, dtype=np.float32)) 3481 pd.SetPoints(vpts) 3482 3483 delny = vtki.new("Delaunay2D") 3484 delny.SetInputData(pd) 3485 if tol: 3486 delny.SetTolerance(tol) 3487 delny.SetAlpha(alpha) 3488 delny.SetOffset(offset) 3489 3490 if transform: 3491 delny.SetTransform(transform.T) 3492 elif mode == "fit": 3493 delny.SetProjectionPlaneMode(vtki.get_class("VTK_BEST_FITTING_PLANE")) 3494 elif mode == "xy" and boundaries: 3495 boundary = vtki.vtkPolyData() 3496 boundary.SetPoints(vpts) 3497 cell_array = vtki.vtkCellArray() 3498 for b in boundaries: 3499 cpolygon = vtki.vtkPolygon() 3500 for idd in b: 3501 cpolygon.GetPointIds().InsertNextId(idd) 3502 cell_array.InsertNextCell(cpolygon) 3503 boundary.SetPolys(cell_array) 3504 delny.SetSourceData(boundary) 3505 3506 delny.Update() 3507 3508 msh = vedo.mesh.Mesh(delny.GetOutput()) 3509 msh.name = "Delaunay2D" 3510 msh.clean().lighting("off") 3511 msh.pipeline = utils.OperationNode( 3512 "delaunay2d", 3513 parents=[self], 3514 comment=f"#cells {msh.dataset.GetNumberOfCells()}", 3515 ) 3516 return msh
Create a mesh from points in the XY plane.
If mode='fit'
then the filter computes a best fitting
plane and projects the points onto it.
Check also generate_mesh()
.
Arguments:
- tol : (float) specify a tolerance to control discarding of closely spaced points. This tolerance is specified as a fraction of the diagonal length of the bounding box of the points.
- alpha : (float) for a non-zero alpha value, only edges or triangles contained within a sphere centered at mesh vertices will be output. Otherwise, only triangles will be output.
- offset : (float) multiplier to control the size of the initial, bounding Delaunay triangulation.
- transform: (LinearTransform, NonLinearTransform) a transformation which is applied to points to generate a 2D problem. This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane. The points are transformed and triangulated. The topology of triangulated points is used as the output topology.
Examples:
3518 def generate_voronoi(self, padding=0.0, fit=False, method="vtk") -> "vedo.Mesh": 3519 """ 3520 Generate the 2D Voronoi convex tiling of the input points (z is ignored). 3521 The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon. 3522 3523 A cell array named "VoronoiID" is added to the output Mesh. 3524 3525 The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest 3526 to one of the input points. Voronoi tessellations are important in computational geometry 3527 (and many other fields), and are the dual of Delaunay triangulations. 3528 3529 Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored 3530 (although carried through to the output). 3531 If you desire to triangulate in a different plane, you can use fit=True. 3532 3533 A brief summary is as follows. Each (generating) input point is associated with 3534 an initial Voronoi tile, which is simply the bounding box of the point set. 3535 A locator is then used to identify nearby points: each neighbor in turn generates a 3536 clipping line positioned halfway between the generating point and the neighboring point, 3537 and orthogonal to the line connecting them. Clips are readily performed by evaluationg the 3538 vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line. 3539 If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip 3540 line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs, 3541 the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region 3542 containing the neighboring clip points. The clip region (along with the points contained in it) is grown 3543 by careful expansion (e.g., outward spiraling iterator over all candidate clip points). 3544 When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi 3545 tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi 3546 tessellation. Note that topological and geometric information is used to generate a valid triangulation 3547 (e.g., merging points and validating topology). 3548 3549 Arguments: 3550 pts : (list) 3551 list of input points. 3552 padding : (float) 3553 padding distance. The default is 0. 3554 fit : (bool) 3555 detect automatically the best fitting plane. The default is False. 3556 3557 Examples: 3558 - [voronoi1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi1.py) 3559 3560 ![](https://vedo.embl.es/images/basic/voronoi1.png) 3561 3562 - [voronoi2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi2.py) 3563 3564 ![](https://vedo.embl.es/images/advanced/voronoi2.png) 3565 """ 3566 pts = self.vertices 3567 3568 if method == "scipy": 3569 from scipy.spatial import Voronoi as scipy_voronoi 3570 3571 pts = np.asarray(pts)[:, (0, 1)] 3572 vor = scipy_voronoi(pts) 3573 regs = [] # filter out invalid indices 3574 for r in vor.regions: 3575 flag = True 3576 for x in r: 3577 if x < 0: 3578 flag = False 3579 break 3580 if flag and len(r) > 0: 3581 regs.append(r) 3582 3583 m = vedo.Mesh([vor.vertices, regs]) 3584 m.celldata["VoronoiID"] = np.array(list(range(len(regs)))).astype(int) 3585 3586 elif method == "vtk": 3587 vor = vtki.new("Voronoi2D") 3588 if isinstance(pts, Points): 3589 vor.SetInputData(pts) 3590 else: 3591 pts = np.asarray(pts) 3592 if pts.shape[1] == 2: 3593 pts = np.c_[pts, np.zeros(len(pts))] 3594 pd = vtki.vtkPolyData() 3595 vpts = vtki.vtkPoints() 3596 vpts.SetData(utils.numpy2vtk(pts, dtype=np.float32)) 3597 pd.SetPoints(vpts) 3598 vor.SetInputData(pd) 3599 vor.SetPadding(padding) 3600 vor.SetGenerateScalarsToPointIds() 3601 if fit: 3602 vor.SetProjectionPlaneModeToBestFittingPlane() 3603 else: 3604 vor.SetProjectionPlaneModeToXYPlane() 3605 vor.Update() 3606 poly = vor.GetOutput() 3607 arr = poly.GetCellData().GetArray(0) 3608 if arr: 3609 arr.SetName("VoronoiID") 3610 m = vedo.Mesh(poly, c="orange5") 3611 3612 else: 3613 vedo.logger.error(f"Unknown method {method} in voronoi()") 3614 raise RuntimeError 3615 3616 m.lw(2).lighting("off").wireframe() 3617 m.name = "Voronoi" 3618 return m
Generate the 2D Voronoi convex tiling of the input points (z is ignored). The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon.
A cell array named "VoronoiID" is added to the output Mesh.
The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest to one of the input points. Voronoi tessellations are important in computational geometry (and many other fields), and are the dual of Delaunay triangulations.
Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored (although carried through to the output). If you desire to triangulate in a different plane, you can use fit=True.
A brief summary is as follows. Each (generating) input point is associated with an initial Voronoi tile, which is simply the bounding box of the point set. A locator is then used to identify nearby points: each neighbor in turn generates a clipping line positioned halfway between the generating point and the neighboring point, and orthogonal to the line connecting them. Clips are readily performed by evaluationg the vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line. If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs, the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region containing the neighboring clip points. The clip region (along with the points contained in it) is grown by careful expansion (e.g., outward spiraling iterator over all candidate clip points). When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi tessellation. Note that topological and geometric information is used to generate a valid triangulation (e.g., merging points and validating topology).
Arguments:
- pts : (list) list of input points.
- padding : (float) padding distance. The default is 0.
- fit : (bool) detect automatically the best fitting plane. The default is False.
Examples:
3621 def generate_delaunay3d(self, radius=0, tol=None) -> "vedo.TetMesh": 3622 """ 3623 Create 3D Delaunay triangulation of input points. 3624 3625 Arguments: 3626 radius : (float) 3627 specify distance (or "alpha") value to control output. 3628 For a non-zero values, only tetra contained within the circumsphere 3629 will be output. 3630 tol : (float) 3631 Specify a tolerance to control discarding of closely spaced points. 3632 This tolerance is specified as a fraction of the diagonal length of 3633 the bounding box of the points. 3634 """ 3635 deln = vtki.new("Delaunay3D") 3636 deln.SetInputData(self.dataset) 3637 deln.SetAlpha(radius) 3638 deln.AlphaTetsOn() 3639 deln.AlphaTrisOff() 3640 deln.AlphaLinesOff() 3641 deln.AlphaVertsOff() 3642 deln.BoundingTriangulationOff() 3643 if tol: 3644 deln.SetTolerance(tol) 3645 deln.Update() 3646 m = vedo.TetMesh(deln.GetOutput()) 3647 m.pipeline = utils.OperationNode( 3648 "generate_delaunay3d", c="#e9c46a:#edabab", parents=[self], 3649 ) 3650 m.name = "Delaunay3D" 3651 return m
Create 3D Delaunay triangulation of input points.
Arguments:
- radius : (float) specify distance (or "alpha") value to control output. For a non-zero values, only tetra contained within the circumsphere will be output.
- tol : (float) Specify a tolerance to control discarding of closely spaced points. This tolerance is specified as a fraction of the diagonal length of the bounding box of the points.
3654 def visible_points(self, area=(), tol=None, invert=False) -> Union[Self, None]: 3655 """ 3656 Extract points based on whether they are visible or not. 3657 Visibility is determined by accessing the z-buffer of a rendering window. 3658 The position of each input point is converted into display coordinates, 3659 and then the z-value at that point is obtained. 3660 If within the user-specified tolerance, the point is considered visible. 3661 Associated data attributes are passed to the output as well. 3662 3663 This filter also allows you to specify a rectangular window in display (pixel) 3664 coordinates in which the visible points must lie. 3665 3666 Arguments: 3667 area : (list) 3668 specify a rectangular region as (xmin,xmax,ymin,ymax) 3669 tol : (float) 3670 a tolerance in normalized display coordinate system 3671 invert : (bool) 3672 select invisible points instead. 3673 3674 Example: 3675 ```python 3676 from vedo import Ellipsoid, show 3677 s = Ellipsoid().rotate_y(30) 3678 3679 # Camera options: pos, focal_point, viewup, distance 3680 camopts = dict(pos=(0,0,25), focal_point=(0,0,0)) 3681 show(s, camera=camopts, offscreen=True) 3682 3683 m = s.visible_points() 3684 # print('visible pts:', m.vertices) # numpy array 3685 show(m, new=True, axes=1).close() # optionally draw result in a new window 3686 ``` 3687 ![](https://vedo.embl.es/images/feats/visible_points.png) 3688 """ 3689 svp = vtki.new("SelectVisiblePoints") 3690 svp.SetInputData(self.dataset) 3691 3692 ren = None 3693 if vedo.plotter_instance: 3694 if vedo.plotter_instance.renderer: 3695 ren = vedo.plotter_instance.renderer 3696 svp.SetRenderer(ren) 3697 if not ren: 3698 vedo.logger.warning( 3699 "visible_points() can only be used after a rendering step" 3700 ) 3701 return None 3702 3703 if len(area) == 2: 3704 area = utils.flatten(area) 3705 if len(area) == 4: 3706 # specify a rectangular region 3707 svp.SetSelection(area[0], area[1], area[2], area[3]) 3708 if tol is not None: 3709 svp.SetTolerance(tol) 3710 if invert: 3711 svp.SelectInvisibleOn() 3712 svp.Update() 3713 3714 m = Points(svp.GetOutput()) 3715 m.name = "VisiblePoints" 3716 return m
Extract points based on whether they are visible or not. Visibility is determined by accessing the z-buffer of a rendering window. The position of each input point is converted into display coordinates, and then the z-value at that point is obtained. If within the user-specified tolerance, the point is considered visible. Associated data attributes are passed to the output as well.
This filter also allows you to specify a rectangular window in display (pixel) coordinates in which the visible points must lie.
Arguments:
- area : (list) specify a rectangular region as (xmin,xmax,ymin,ymax)
- tol : (float) a tolerance in normalized display coordinate system
- invert : (bool) select invisible points instead.
Example:
from vedo import Ellipsoid, show s = Ellipsoid().rotate_y(30) # Camera options: pos, focal_point, viewup, distance camopts = dict(pos=(0,0,25), focal_point=(0,0,0)) show(s, camera=camopts, offscreen=True) m = s.visible_points() # print('visible pts:', m.vertices) # numpy array show(m, new=True, axes=1).close() # optionally draw result in a new window
Inherited Members
- 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
- vedo.visual.CommonVisual
- 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
455def Point(pos=(0, 0, 0), r=12, c="red", alpha=1.0) -> Self: 456 """ 457 Create a simple point in space. 458 459 .. note:: if you are creating many points you should use class `Points` instead! 460 """ 461 pt = Points([[0,0,0]], r, c, alpha).pos(pos) 462 pt.name = "Point" 463 return pt
Create a simple point in space.
if you are creating many points you should use class Points
instead!
3719class CellCenters(Points): 3720 def __init__(self, pcloud): 3721 """ 3722 Generate `Points` at the center of the cells of any type of object. 3723 3724 Check out also `cell_centers()`. 3725 """ 3726 vcen = vtki.new("CellCenters") 3727 vcen.CopyArraysOn() 3728 vcen.VertexCellsOn() 3729 # vcen.ConvertGhostCellsToGhostPointsOn() 3730 try: 3731 vcen.SetInputData(pcloud.dataset) 3732 except AttributeError: 3733 vcen.SetInputData(pcloud) 3734 vcen.Update() 3735 super().__init__(vcen.GetOutput()) 3736 self.name = "CellCenters"
Work with point clouds.
3720 def __init__(self, pcloud): 3721 """ 3722 Generate `Points` at the center of the cells of any type of object. 3723 3724 Check out also `cell_centers()`. 3725 """ 3726 vcen = vtki.new("CellCenters") 3727 vcen.CopyArraysOn() 3728 vcen.VertexCellsOn() 3729 # vcen.ConvertGhostCellsToGhostPointsOn() 3730 try: 3731 vcen.SetInputData(pcloud.dataset) 3732 except AttributeError: 3733 vcen.SetInputData(pcloud) 3734 vcen.Update() 3735 super().__init__(vcen.GetOutput()) 3736 self.name = "CellCenters"
Generate Points
at the center of the cells of any type of object.
Check out also cell_centers()
.
Inherited Members
- Points
- polydata
- copy
- clone
- compute_normals_with_pca
- compute_acoplanarity
- distance_to
- clean
- subsample
- threshold
- quantize
- vertex_normals
- point_normals
- align_to
- align_to_bounding_box
- align_with_landmarks
- normalize
- mirror
- flip_normals
- add_gaussian_noise
- closest_point
- auto_distance
- hausdorff_distance
- chamfer_distance
- remove_outliers
- relax_point_positions
- smooth_mls_1d
- smooth_mls_2d
- smooth_lloyd_2d
- project_on_plane
- warp
- cut_with_plane
- cut_with_planes
- cut_with_box
- cut_with_line
- cut_with_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
- vedo.visual.CommonVisual
- 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
43def merge(*meshs, flag=False) -> Union["vedo.Mesh", "vedo.Points", None]: 44 """ 45 Build a new Mesh (or Points) formed by the fusion of the inputs. 46 47 Similar to Assembly, but in this case the input objects become a single entity. 48 49 To keep track of the original identities of the inputs you can set `flag=True`. 50 In this case a `pointdata` array of ids is added to the output with name "OriginalMeshID". 51 52 Examples: 53 - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py) 54 55 ![](https://vedo.embl.es/images/advanced/warp1.png) 56 57 - [value_iteration.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/value_iteration.py) 58 59 """ 60 objs = [a for a in utils.flatten(meshs) if a] 61 62 if not objs: 63 return None 64 65 idarr = [] 66 polyapp = vtki.new("AppendPolyData") 67 for i, ob in enumerate(objs): 68 polyapp.AddInputData(ob.dataset) 69 if flag: 70 idarr += [i] * ob.dataset.GetNumberOfPoints() 71 polyapp.Update() 72 mpoly = polyapp.GetOutput() 73 74 if flag: 75 varr = utils.numpy2vtk(idarr, dtype=np.uint16, name="OriginalMeshID") 76 mpoly.GetPointData().AddArray(varr) 77 78 has_mesh = False 79 for ob in objs: 80 if isinstance(ob, vedo.Mesh): 81 has_mesh = True 82 break 83 84 if has_mesh: 85 msh = vedo.Mesh(mpoly) 86 else: 87 msh = Points(mpoly) # type: ignore 88 89 msh.copy_properties_from(objs[0]) 90 91 msh.pipeline = utils.OperationNode( 92 "merge", parents=objs, comment=f"#pts {msh.dataset.GetNumberOfPoints()}" 93 ) 94 return msh
Build a new Mesh (or Points) formed by the fusion of the inputs.
Similar to Assembly, but in this case the input objects become a single entity.
To keep track of the original identities of the inputs you can set flag=True
.
In this case a pointdata
array of ids is added to the output with name "OriginalMeshID".
Examples:
97def delaunay2d(plist, **kwargs) -> Self: 98 """delaunay2d() is deprecated, use Points().generate_delaunay2d() instead.""" 99 if isinstance(plist, Points): 100 plist = plist.vertices 101 else: 102 plist = np.ascontiguousarray(plist) 103 plist = utils.make3d(plist) 104 pp = Points(plist).generate_delaunay2d(**kwargs) 105 print("WARNING: delaunay2d() is deprecated, use Points().generate_delaunay2d() instead") 106 return pp
delaunay2d() is deprecated, use Points().generate_delaunay2d() instead.
149def fit_line(points: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Line": 150 """ 151 Fits a line through points. 152 153 Extra info is stored in `Line.slope`, `Line.center`, `Line.variances`. 154 155 Examples: 156 - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py) 157 158 ![](https://vedo.embl.es/images/advanced/fitline.png) 159 """ 160 if isinstance(points, Points): 161 points = points.vertices 162 data = np.asarray(points) 163 datamean = data.mean(axis=0) 164 _, dd, vv = np.linalg.svd(data - datamean) 165 vv = vv[0] / np.linalg.norm(vv[0]) 166 # vv contains the first principal component, i.e. the direction 167 # vector of the best fit line in the least squares sense. 168 xyz_min = data.min(axis=0) 169 xyz_max = data.max(axis=0) 170 a = np.linalg.norm(xyz_min - datamean) 171 b = np.linalg.norm(xyz_max - datamean) 172 p1 = datamean - a * vv 173 p2 = datamean + b * vv 174 line = vedo.shapes.Line(p1, p2, lw=1) 175 line.slope = vv 176 line.center = datamean 177 line.variances = dd 178 return line
Fits a line through points.
Extra info is stored in Line.slope
, Line.center
, Line.variances
.
Examples:
181def fit_circle(points: Union[np.ndarray, "vedo.Points"]) -> tuple: 182 """ 183 Fits a circle through a set of 3D points, with a very fast non-iterative method. 184 185 Returns the tuple `(center, radius, normal_to_circle)`. 186 187 .. warning:: 188 trying to fit s-shaped points will inevitably lead to instabilities and 189 circles of small radius. 190 191 References: 192 *J.F. Crawford, Nucl. Instr. Meth. 211, 1983, 223-225.* 193 """ 194 if isinstance(points, Points): 195 points = points.vertices 196 data = np.asarray(points) 197 198 offs = data.mean(axis=0) 199 data, n0 = _rotate_points(data - offs) 200 201 xi = data[:, 0] 202 yi = data[:, 1] 203 204 x = sum(xi) 205 xi2 = xi * xi 206 xx = sum(xi2) 207 xxx = sum(xi2 * xi) 208 209 y = sum(yi) 210 yi2 = yi * yi 211 yy = sum(yi2) 212 yyy = sum(yi2 * yi) 213 214 xiyi = xi * yi 215 xy = sum(xiyi) 216 xyy = sum(xiyi * yi) 217 xxy = sum(xi * xiyi) 218 219 N = len(xi) 220 k = (xx + yy) / N 221 222 a1 = xx - x * x / N 223 b1 = xy - x * y / N 224 c1 = 0.5 * (xxx + xyy - x * k) 225 226 a2 = xy - x * y / N 227 b2 = yy - y * y / N 228 c2 = 0.5 * (xxy + yyy - y * k) 229 230 d = a2 * b1 - a1 * b2 231 if not d: 232 return offs, 0, n0 233 x0 = (b1 * c2 - b2 * c1) / d 234 y0 = (c1 - a1 * x0) / b1 235 236 R = np.sqrt(x0 * x0 + y0 * y0 - 1 / N * (2 * x0 * x + 2 * y0 * y - xx - yy)) 237 238 c, _ = _rotate_points([x0, y0, 0], (0, 0, 1), n0) 239 240 return c[0] + offs, R, n0
Fits a circle through a set of 3D points, with a very fast non-iterative method.
Returns the tuple (center, radius, normal_to_circle)
.
trying to fit s-shaped points will inevitably lead to instabilities and circles of small radius.
References:
J.F. Crawford, Nucl. Instr. Meth. 211, 1983, 223-225.
243def fit_plane(points: Union[np.ndarray, "vedo.Points"], signed=False) -> "vedo.shapes.Plane": 244 """ 245 Fits a plane to a set of points. 246 247 Extra info is stored in `Plane.normal`, `Plane.center`, `Plane.variance`. 248 249 Arguments: 250 signed : (bool) 251 if True flip sign of the normal based on the ordering of the points 252 253 Examples: 254 - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py) 255 256 ![](https://vedo.embl.es/images/advanced/fitline.png) 257 """ 258 if isinstance(points, Points): 259 points = points.vertices 260 data = np.asarray(points) 261 datamean = data.mean(axis=0) 262 pts = data - datamean 263 res = np.linalg.svd(pts) 264 dd, vv = res[1], res[2] 265 n = np.cross(vv[0], vv[1]) 266 if signed: 267 v = np.zeros_like(pts) 268 for i in range(len(pts) - 1): 269 vi = np.cross(pts[i], pts[i + 1]) 270 v[i] = vi / np.linalg.norm(vi) 271 ns = np.mean(v, axis=0) # normal to the points plane 272 if np.dot(n, ns) < 0: 273 n = -n 274 xyz_min = data.min(axis=0) 275 xyz_max = data.max(axis=0) 276 s = np.linalg.norm(xyz_max - xyz_min) 277 pla = vedo.shapes.Plane(datamean, n, s=[s, s]) 278 pla.variance = dd[2] 279 pla.name = "FitPlane" 280 return pla
Fits a plane to a set of points.
Extra info is stored in Plane.normal
, Plane.center
, Plane.variance
.
Arguments:
- signed : (bool) if True flip sign of the normal based on the ordering of the points
Examples:
283def fit_sphere(coords: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Sphere": 284 """ 285 Fits a sphere to a set of points. 286 287 Extra info is stored in `Sphere.radius`, `Sphere.center`, `Sphere.residue`. 288 289 Examples: 290 - [fitspheres1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitspheres1.py) 291 292 ![](https://vedo.embl.es/images/advanced/fitspheres1.jpg) 293 """ 294 if isinstance(coords, Points): 295 coords = coords.vertices 296 coords = np.array(coords) 297 n = len(coords) 298 A = np.zeros((n, 4)) 299 A[:, :-1] = coords * 2 300 A[:, 3] = 1 301 f = np.zeros((n, 1)) 302 x = coords[:, 0] 303 y = coords[:, 1] 304 z = coords[:, 2] 305 f[:, 0] = x * x + y * y + z * z 306 try: 307 C, residue, rank, _ = np.linalg.lstsq(A, f, rcond=-1) # solve AC=f 308 except: 309 C, residue, rank, _ = np.linalg.lstsq(A, f) # solve AC=f 310 if rank < 4: 311 return None 312 t = (C[0] * C[0]) + (C[1] * C[1]) + (C[2] * C[2]) + C[3] 313 radius = np.sqrt(t)[0] 314 center = np.array([C[0][0], C[1][0], C[2][0]]) 315 if len(residue) > 0: 316 residue = np.sqrt(residue[0]) / n 317 else: 318 residue = 0 319 sph = vedo.shapes.Sphere(center, radius, c=(1, 0, 0)).wireframe(1) 320 sph.radius = radius 321 sph.center = center 322 sph.residue = residue 323 sph.name = "FitSphere" 324 return sph
Fits a sphere to a set of points.
Extra info is stored in Sphere.radius
, Sphere.center
, Sphere.residue
.
Examples:
327def pca_ellipse(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=60) -> Union["vedo.shapes.Circle", None]: 328 """ 329 Create the oriented 2D ellipse that contains the fraction `pvalue` of points. 330 PCA (Principal Component Analysis) is used to compute the ellipse orientation. 331 332 Parameter `pvalue` sets the specified fraction of points inside the ellipse. 333 Normalized directions are stored in `ellipse.axis1`, `ellipse.axis2`. 334 Axes sizes are stored in `ellipse.va`, `ellipse.vb` 335 336 Arguments: 337 pvalue : (float) 338 ellipse will include this fraction of points 339 res : (int) 340 resolution of the ellipse 341 342 Examples: 343 - [pca_ellipse.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipse.py) 344 - [histo_pca.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_pca.py) 345 346 ![](https://vedo.embl.es/images/pyplot/histo_pca.png) 347 """ 348 from scipy.stats import f 349 350 if isinstance(points, Points): 351 coords = points.vertices 352 else: 353 coords = points 354 if len(coords) < 4: 355 vedo.logger.warning("in pca_ellipse(), there are not enough points!") 356 return None 357 358 P = np.array(coords, dtype=float)[:, (0, 1)] 359 cov = np.cov(P, rowvar=0) # type: ignore 360 _, s, R = np.linalg.svd(cov) # singular value decomposition 361 p, n = s.size, P.shape[0] 362 fppf = f.ppf(pvalue, p, n - p) # f % point function 363 u = np.sqrt(s * fppf / 2) * 2 # semi-axes (largest first) 364 ua, ub = u 365 center = utils.make3d(np.mean(P, axis=0)) # centroid of the ellipse 366 367 t = LinearTransform(R.T * u).translate(center) 368 elli = vedo.shapes.Circle(alpha=0.75, res=res) 369 elli.apply_transform(t) 370 elli.properties.LightingOff() 371 372 elli.pvalue = pvalue 373 elli.center = np.array([center[0], center[1], 0]) 374 elli.nr_of_points = n 375 elli.va = ua 376 elli.vb = ub 377 378 # we subtract center because it's in t 379 elli.axis1 = t.move([1, 0, 0]) - center 380 elli.axis2 = t.move([0, 1, 0]) - center 381 382 elli.axis1 /= np.linalg.norm(elli.axis1) 383 elli.axis2 /= np.linalg.norm(elli.axis2) 384 elli.name = "PCAEllipse" 385 return elli
Create the oriented 2D ellipse that contains the fraction pvalue
of points.
PCA (Principal Component Analysis) is used to compute the ellipse orientation.
Parameter pvalue
sets the specified fraction of points inside the ellipse.
Normalized directions are stored in ellipse.axis1
, ellipse.axis2
.
Axes sizes are stored in ellipse.va
, ellipse.vb
Arguments:
- pvalue : (float) ellipse will include this fraction of points
- res : (int) resolution of the ellipse
Examples:
388def pca_ellipsoid(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=24) -> Union["vedo.shapes.Ellipsoid", None]: 389 """ 390 Create the oriented ellipsoid that contains the fraction `pvalue` of points. 391 PCA (Principal Component Analysis) is used to compute the ellipsoid orientation. 392 393 Axes sizes can be accessed in `ellips.va`, `ellips.vb`, `ellips.vc`, 394 normalized directions are stored in `ellips.axis1`, `ellips.axis2` and `ellips.axis3`. 395 Center of mass is stored in `ellips.center`. 396 397 Asphericity can be accessed in `ellips.asphericity()` and ellips.asphericity_error(). 398 A value of 0 means a perfect sphere. 399 400 Arguments: 401 pvalue : (float) 402 ellipsoid will include this fraction of points 403 404 Examples: 405 [pca_ellipsoid.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipsoid.py) 406 407 ![](https://vedo.embl.es/images/basic/pca.png) 408 409 See also: 410 `pca_ellipse()` for a 2D ellipse. 411 """ 412 from scipy.stats import f 413 414 if isinstance(points, Points): 415 coords = points.vertices 416 else: 417 coords = points 418 if len(coords) < 4: 419 vedo.logger.warning("in pca_ellipsoid(), not enough input points!") 420 return None 421 422 P = np.array(coords, ndmin=2, dtype=float) 423 cov = np.cov(P, rowvar=0) # type: ignore 424 _, s, R = np.linalg.svd(cov) # singular value decomposition 425 p, n = s.size, P.shape[0] 426 fppf = f.ppf(pvalue, p, n-p)*(n-1)*p*(n+1)/n/(n-p) # f % point function 427 u = np.sqrt(s*fppf) 428 ua, ub, uc = u # semi-axes (largest first) 429 center = np.mean(P, axis=0) # centroid of the hyperellipsoid 430 431 t = LinearTransform(R.T * u).translate(center) 432 elli = vedo.shapes.Ellipsoid((0,0,0), (1,0,0), (0,1,0), (0,0,1), res=res) 433 elli.apply_transform(t) 434 elli.alpha(0.25) 435 elli.properties.LightingOff() 436 437 elli.pvalue = pvalue 438 elli.nr_of_points = n 439 elli.center = center 440 elli.va = ua 441 elli.vb = ub 442 elli.vc = uc 443 # we subtract center because it's in t 444 elli.axis1 = np.array(t.move([1, 0, 0])) - center 445 elli.axis2 = np.array(t.move([0, 1, 0])) - center 446 elli.axis3 = np.array(t.move([0, 0, 1])) - center 447 elli.axis1 /= np.linalg.norm(elli.axis1) 448 elli.axis2 /= np.linalg.norm(elli.axis2) 449 elli.axis3 /= np.linalg.norm(elli.axis3) 450 elli.name = "PCAEllipsoid" 451 return elli
Create the oriented ellipsoid that contains the fraction pvalue
of points.
PCA (Principal Component Analysis) is used to compute the ellipsoid orientation.
Axes sizes can be accessed in ellips.va
, ellips.vb
, ellips.vc
,
normalized directions are stored in ellips.axis1
, ellips.axis2
and ellips.axis3
.
Center of mass is stored in ellips.center
.
Asphericity can be accessed in ellips.asphericity()
and ellips.asphericity_error().
A value of 0 means a perfect sphere.
Arguments:
- pvalue : (float) ellipsoid will include this fraction of points
Examples:
![](https://vedo.embl.es/images/basic/pca.png)
See also:
pca_ellipse()
for a 2D ellipse.