vedo.pointcloud
Submodule to work with point clouds
1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3import time 4from typing import Self, Union, List 5from weakref import ref as weak_ref_to 6import numpy as np 7 8import vedo.vtkclasses as vtki 9 10import vedo 11from vedo import colors 12from vedo import utils 13from vedo.transformations import LinearTransform, NonLinearTransform 14from vedo.core import PointAlgorithms 15from vedo.visual import PointsVisual 16 17__docformat__ = "google" 18 19__doc__ = """ 20Submodule to work with point clouds <br> 21 22![](https://vedo.embl.es/images/basic/pca.png) 23""" 24 25__all__ = [ 26 "Points", 27 "Point", 28 "CellCenters", 29 "merge", 30 "delaunay2d", # deprecated, use .generate_delaunay2d() 31 "fit_line", 32 "fit_circle", 33 "fit_plane", 34 "fit_sphere", 35 "pca_ellipse", 36 "pca_ellipsoid", 37] 38 39 40#################################################### 41def merge(*meshs, flag=False) -> Union["vedo.Mesh", "vedo.Points", None]: 42 """ 43 Build a new Mesh (or Points) formed by the fusion of the inputs. 44 45 Similar to Assembly, but in this case the input objects become a single entity. 46 47 To keep track of the original identities of the inputs you can set `flag=True`. 48 In this case a `pointdata` array of ids is added to the output with name "OriginalMeshID". 49 50 Examples: 51 - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py) 52 53 ![](https://vedo.embl.es/images/advanced/warp1.png) 54 55 - [value_iteration.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/value_iteration.py) 56 57 """ 58 objs = [a for a in utils.flatten(meshs) if a] 59 60 if not objs: 61 return None 62 63 idarr = [] 64 polyapp = vtki.new("AppendPolyData") 65 for i, ob in enumerate(objs): 66 polyapp.AddInputData(ob.dataset) 67 if flag: 68 idarr += [i] * ob.dataset.GetNumberOfPoints() 69 polyapp.Update() 70 mpoly = polyapp.GetOutput() 71 72 if flag: 73 varr = utils.numpy2vtk(idarr, dtype=np.uint16, name="OriginalMeshID") 74 mpoly.GetPointData().AddArray(varr) 75 76 has_mesh = False 77 for ob in objs: 78 if isinstance(ob, vedo.Mesh): 79 has_mesh = True 80 break 81 82 if has_mesh: 83 msh = vedo.Mesh(mpoly) 84 else: 85 msh = Points(mpoly) # type: ignore 86 87 msh.copy_properties_from(objs[0]) 88 89 msh.pipeline = utils.OperationNode( 90 "merge", parents=objs, comment=f"#pts {msh.dataset.GetNumberOfPoints()}" 91 ) 92 return msh 93 94 95def delaunay2d(plist, **kwargs) -> Self: 96 """delaunay2d() is deprecated, use Points().generate_delaunay2d() instead.""" 97 if isinstance(plist, Points): 98 plist = plist.vertices 99 else: 100 plist = np.ascontiguousarray(plist) 101 plist = utils.make3d(plist) 102 pp = Points(plist).generate_delaunay2d(**kwargs) 103 print("WARNING: delaunay2d() is deprecated, use Points().generate_delaunay2d() instead") 104 return pp 105 106 107def _rotate_points(points, n0=None, n1=(0, 0, 1)) -> Union[np.ndarray, tuple]: 108 # Rotate a set of 3D points from direction n0 to direction n1. 109 # Return the rotated points and the normal to the fitting plane (if n0 is None). 110 # The pointing direction of the normal in this case is arbitrary. 111 points = np.asarray(points) 112 113 if points.ndim == 1: 114 points = points[np.newaxis, :] 115 116 if len(points[0]) == 2: 117 return points, (0, 0, 1) 118 119 if n0 is None: # fit plane 120 datamean = points.mean(axis=0) 121 vv = np.linalg.svd(points - datamean)[2] 122 n0 = np.cross(vv[0], vv[1]) 123 124 n0 = n0 / np.linalg.norm(n0) 125 n1 = n1 / np.linalg.norm(n1) 126 k = np.cross(n0, n1) 127 l = np.linalg.norm(k) 128 if not l: 129 k = n0 130 k /= np.linalg.norm(k) 131 132 ct = np.dot(n0, n1) 133 theta = np.arccos(ct) 134 st = np.sin(theta) 135 v = k * (1 - ct) 136 137 rpoints = [] 138 for p in points: 139 a = p * ct 140 b = np.cross(k, p) * st 141 c = v * np.dot(k, p) 142 rpoints.append(a + b + c) 143 144 return np.array(rpoints), n0 145 146 147def fit_line(points: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Line": 148 """ 149 Fits a line through points. 150 151 Extra info is stored in `Line.slope`, `Line.center`, `Line.variances`. 152 153 Examples: 154 - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py) 155 156 ![](https://vedo.embl.es/images/advanced/fitline.png) 157 """ 158 if isinstance(points, Points): 159 points = points.vertices 160 data = np.asarray(points) 161 datamean = data.mean(axis=0) 162 _, dd, vv = np.linalg.svd(data - datamean) 163 vv = vv[0] / np.linalg.norm(vv[0]) 164 # vv contains the first principal component, i.e. the direction 165 # vector of the best fit line in the least squares sense. 166 xyz_min = data.min(axis=0) 167 xyz_max = data.max(axis=0) 168 a = np.linalg.norm(xyz_min - datamean) 169 b = np.linalg.norm(xyz_max - datamean) 170 p1 = datamean - a * vv 171 p2 = datamean + b * vv 172 line = vedo.shapes.Line(p1, p2, lw=1) 173 line.slope = vv 174 line.center = datamean 175 line.variances = dd 176 return line 177 178 179def fit_circle(points: Union[np.ndarray, "vedo.Points"]) -> tuple: 180 """ 181 Fits a circle through a set of 3D points, with a very fast non-iterative method. 182 183 Returns the tuple `(center, radius, normal_to_circle)`. 184 185 .. warning:: 186 trying to fit s-shaped points will inevitably lead to instabilities and 187 circles of small radius. 188 189 References: 190 *J.F. Crawford, Nucl. Instr. Meth. 211, 1983, 223-225.* 191 """ 192 if isinstance(points, Points): 193 points = points.vertices 194 data = np.asarray(points) 195 196 offs = data.mean(axis=0) 197 data, n0 = _rotate_points(data - offs) 198 199 xi = data[:, 0] 200 yi = data[:, 1] 201 202 x = sum(xi) 203 xi2 = xi * xi 204 xx = sum(xi2) 205 xxx = sum(xi2 * xi) 206 207 y = sum(yi) 208 yi2 = yi * yi 209 yy = sum(yi2) 210 yyy = sum(yi2 * yi) 211 212 xiyi = xi * yi 213 xy = sum(xiyi) 214 xyy = sum(xiyi * yi) 215 xxy = sum(xi * xiyi) 216 217 N = len(xi) 218 k = (xx + yy) / N 219 220 a1 = xx - x * x / N 221 b1 = xy - x * y / N 222 c1 = 0.5 * (xxx + xyy - x * k) 223 224 a2 = xy - x * y / N 225 b2 = yy - y * y / N 226 c2 = 0.5 * (xxy + yyy - y * k) 227 228 d = a2 * b1 - a1 * b2 229 if not d: 230 return offs, 0, n0 231 x0 = (b1 * c2 - b2 * c1) / d 232 y0 = (c1 - a1 * x0) / b1 233 234 R = np.sqrt(x0 * x0 + y0 * y0 - 1 / N * (2 * x0 * x + 2 * y0 * y - xx - yy)) 235 236 c, _ = _rotate_points([x0, y0, 0], (0, 0, 1), n0) 237 238 return c[0] + offs, R, n0 239 240 241def fit_plane(points: Union[np.ndarray, "vedo.Points"], signed=False) -> "vedo.shapes.Plane": 242 """ 243 Fits a plane to a set of points. 244 245 Extra info is stored in `Plane.normal`, `Plane.center`, `Plane.variance`. 246 247 Arguments: 248 signed : (bool) 249 if True flip sign of the normal based on the ordering of the points 250 251 Examples: 252 - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py) 253 254 ![](https://vedo.embl.es/images/advanced/fitline.png) 255 """ 256 if isinstance(points, Points): 257 points = points.vertices 258 data = np.asarray(points) 259 datamean = data.mean(axis=0) 260 pts = data - datamean 261 res = np.linalg.svd(pts) 262 dd, vv = res[1], res[2] 263 n = np.cross(vv[0], vv[1]) 264 if signed: 265 v = np.zeros_like(pts) 266 for i in range(len(pts) - 1): 267 vi = np.cross(pts[i], pts[i + 1]) 268 v[i] = vi / np.linalg.norm(vi) 269 ns = np.mean(v, axis=0) # normal to the points plane 270 if np.dot(n, ns) < 0: 271 n = -n 272 xyz_min = data.min(axis=0) 273 xyz_max = data.max(axis=0) 274 s = np.linalg.norm(xyz_max - xyz_min) 275 pla = vedo.shapes.Plane(datamean, n, s=[s, s]) 276 pla.variance = dd[2] 277 pla.name = "FitPlane" 278 return pla 279 280 281def fit_sphere(coords: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Sphere": 282 """ 283 Fits a sphere to a set of points. 284 285 Extra info is stored in `Sphere.radius`, `Sphere.center`, `Sphere.residue`. 286 287 Examples: 288 - [fitspheres1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitspheres1.py) 289 290 ![](https://vedo.embl.es/images/advanced/fitspheres1.jpg) 291 """ 292 if isinstance(coords, Points): 293 coords = coords.vertices 294 coords = np.array(coords) 295 n = len(coords) 296 A = np.zeros((n, 4)) 297 A[:, :-1] = coords * 2 298 A[:, 3] = 1 299 f = np.zeros((n, 1)) 300 x = coords[:, 0] 301 y = coords[:, 1] 302 z = coords[:, 2] 303 f[:, 0] = x * x + y * y + z * z 304 try: 305 C, residue, rank, _ = np.linalg.lstsq(A, f, rcond=-1) # solve AC=f 306 except: 307 C, residue, rank, _ = np.linalg.lstsq(A, f) # solve AC=f 308 if rank < 4: 309 return None 310 t = (C[0] * C[0]) + (C[1] * C[1]) + (C[2] * C[2]) + C[3] 311 radius = np.sqrt(t)[0] 312 center = np.array([C[0][0], C[1][0], C[2][0]]) 313 if len(residue) > 0: 314 residue = np.sqrt(residue[0]) / n 315 else: 316 residue = 0 317 sph = vedo.shapes.Sphere(center, radius, c=(1, 0, 0)).wireframe(1) 318 sph.radius = radius 319 sph.center = center 320 sph.residue = residue 321 sph.name = "FitSphere" 322 return sph 323 324 325def pca_ellipse(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=60) -> Union["vedo.shapes.Circle", None]: 326 """ 327 Create the oriented 2D ellipse that contains the fraction `pvalue` of points. 328 PCA (Principal Component Analysis) is used to compute the ellipse orientation. 329 330 Parameter `pvalue` sets the specified fraction of points inside the ellipse. 331 Normalized directions are stored in `ellipse.axis1`, `ellipse.axis2`. 332 Axes sizes are stored in `ellipse.va`, `ellipse.vb` 333 334 Arguments: 335 pvalue : (float) 336 ellipse will include this fraction of points 337 res : (int) 338 resolution of the ellipse 339 340 Examples: 341 - [pca_ellipse.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipse.py) 342 - [histo_pca.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_pca.py) 343 344 ![](https://vedo.embl.es/images/pyplot/histo_pca.png) 345 """ 346 from scipy.stats import f 347 348 if isinstance(points, Points): 349 coords = points.vertices 350 else: 351 coords = points 352 if len(coords) < 4: 353 vedo.logger.warning("in pca_ellipse(), there are not enough points!") 354 return None 355 356 P = np.array(coords, dtype=float)[:, (0, 1)] 357 cov = np.cov(P, rowvar=0) # type: ignore 358 _, s, R = np.linalg.svd(cov) # singular value decomposition 359 p, n = s.size, P.shape[0] 360 fppf = f.ppf(pvalue, p, n - p) # f % point function 361 u = np.sqrt(s * fppf / 2) * 2 # semi-axes (largest first) 362 ua, ub = u 363 center = utils.make3d(np.mean(P, axis=0)) # centroid of the ellipse 364 365 t = LinearTransform(R.T * u).translate(center) 366 elli = vedo.shapes.Circle(alpha=0.75, res=res) 367 elli.apply_transform(t) 368 elli.properties.LightingOff() 369 370 elli.pvalue = pvalue 371 elli.center = np.array([center[0], center[1], 0]) 372 elli.nr_of_points = n 373 elli.va = ua 374 elli.vb = ub 375 376 # we subtract center because it's in t 377 elli.axis1 = t.move([1, 0, 0]) - center 378 elli.axis2 = t.move([0, 1, 0]) - center 379 380 elli.axis1 /= np.linalg.norm(elli.axis1) 381 elli.axis2 /= np.linalg.norm(elli.axis2) 382 elli.name = "PCAEllipse" 383 return elli 384 385 386def pca_ellipsoid(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=24) -> Union["vedo.shapes.Ellipsoid", None]: 387 """ 388 Create the oriented ellipsoid that contains the fraction `pvalue` of points. 389 PCA (Principal Component Analysis) is used to compute the ellipsoid orientation. 390 391 Axes sizes can be accessed in `ellips.va`, `ellips.vb`, `ellips.vc`, 392 normalized directions are stored in `ellips.axis1`, `ellips.axis2` and `ellips.axis3`. 393 Center of mass is stored in `ellips.center`. 394 395 Asphericity can be accessed in `ellips.asphericity()` and ellips.asphericity_error(). 396 A value of 0 means a perfect sphere. 397 398 Arguments: 399 pvalue : (float) 400 ellipsoid will include this fraction of points 401 402 Examples: 403 [pca_ellipsoid.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipsoid.py) 404 405 ![](https://vedo.embl.es/images/basic/pca.png) 406 407 See also: 408 `pca_ellipse()` for a 2D ellipse. 409 """ 410 from scipy.stats import f 411 412 if isinstance(points, Points): 413 coords = points.vertices 414 else: 415 coords = points 416 if len(coords) < 4: 417 vedo.logger.warning("in pca_ellipsoid(), not enough input points!") 418 return None 419 420 P = np.array(coords, ndmin=2, dtype=float) 421 cov = np.cov(P, rowvar=0) # type: ignore 422 _, s, R = np.linalg.svd(cov) # singular value decomposition 423 p, n = s.size, P.shape[0] 424 fppf = f.ppf(pvalue, p, n-p)*(n-1)*p*(n+1)/n/(n-p) # f % point function 425 u = np.sqrt(s*fppf) 426 ua, ub, uc = u # semi-axes (largest first) 427 center = np.mean(P, axis=0) # centroid of the hyperellipsoid 428 429 t = LinearTransform(R.T * u).translate(center) 430 elli = vedo.shapes.Ellipsoid((0,0,0), (1,0,0), (0,1,0), (0,0,1), res=res) 431 elli.apply_transform(t) 432 elli.alpha(0.25) 433 elli.properties.LightingOff() 434 435 elli.pvalue = pvalue 436 elli.nr_of_points = n 437 elli.center = center 438 elli.va = ua 439 elli.vb = ub 440 elli.vc = uc 441 # we subtract center because it's in t 442 elli.axis1 = np.array(t.move([1, 0, 0])) - center 443 elli.axis2 = np.array(t.move([0, 1, 0])) - center 444 elli.axis3 = np.array(t.move([0, 0, 1])) - center 445 elli.axis1 /= np.linalg.norm(elli.axis1) 446 elli.axis2 /= np.linalg.norm(elli.axis2) 447 elli.axis3 /= np.linalg.norm(elli.axis3) 448 elli.name = "PCAEllipsoid" 449 return elli 450 451 452################################################### 453def Point(pos=(0, 0, 0), r=12, c="red", alpha=1.0) -> Self: 454 """ 455 Create a simple point in space. 456 457 .. note:: if you are creating many points you should use class `Points` instead! 458 """ 459 pt = Points([pos], r, c, alpha) 460 # pt.pos(pos) # dont set position, just the point coords 461 pt.name = "Point" 462 return pt 463 464 465################################################### 466class Points(PointsVisual, PointAlgorithms): 467 """Work with point clouds.""" 468 469 def __init__(self, inputobj=None, r=4, c=(0.2, 0.2, 0.2), alpha=1): 470 """ 471 Build an object made of only vertex points for a list of 2D/3D points. 472 Both shapes (N, 3) or (3, N) are accepted as input, if N>3. 473 For very large point clouds a list of colors and alpha can be assigned to each 474 point in the form c=[(R,G,B,A), ... ] where 0<=R<256, ... 0<=A<256. 475 476 Arguments: 477 inputobj : (list, tuple) 478 r : (int) 479 Point radius in units of pixels. 480 c : (str, list) 481 Color name or rgb tuple. 482 alpha : (float) 483 Transparency in range [0,1]. 484 485 Example: 486 ```python 487 from vedo import * 488 489 def fibonacci_sphere(n): 490 s = np.linspace(0, n, num=n, endpoint=False) 491 theta = s * 2.399963229728653 492 y = 1 - s * (2/(n-1)) 493 r = np.sqrt(1 - y * y) 494 x = np.cos(theta) * r 495 z = np.sin(theta) * r 496 return np._c[x,y,z] 497 498 Points(fibonacci_sphere(1000)).show(axes=1).close() 499 ``` 500 ![](https://vedo.embl.es/images/feats/fibonacci.png) 501 """ 502 # print("INIT POINTS") 503 super().__init__() 504 505 self.name = "" 506 self.filename = "" 507 self.file_size = "" 508 509 self.info = {} 510 self.time = time.time() 511 512 self.transform = LinearTransform() 513 self.point_locator = None 514 self.cell_locator = None 515 self.line_locator = None 516 517 self.actor = vtki.vtkActor() 518 self.properties = self.actor.GetProperty() 519 self.properties_backface = self.actor.GetBackfaceProperty() 520 self.mapper = vtki.new("PolyDataMapper") 521 self.dataset = vtki.vtkPolyData() 522 523 # Create weakref so actor can access this object (eg to pick/remove): 524 self.actor.retrieve_object = weak_ref_to(self) 525 526 try: 527 self.properties.RenderPointsAsSpheresOn() 528 except AttributeError: 529 pass 530 531 if inputobj is None: #################### 532 return 533 ########################################## 534 535 self.name = "Points" 536 537 ###### 538 if isinstance(inputobj, vtki.vtkActor): 539 self.dataset.DeepCopy(inputobj.GetMapper().GetInput()) 540 pr = vtki.vtkProperty() 541 pr.DeepCopy(inputobj.GetProperty()) 542 self.actor.SetProperty(pr) 543 self.properties = pr 544 self.mapper.SetScalarVisibility(inputobj.GetMapper().GetScalarVisibility()) 545 546 elif isinstance(inputobj, vtki.vtkPolyData): 547 self.dataset = inputobj 548 if self.dataset.GetNumberOfCells() == 0: 549 carr = vtki.vtkCellArray() 550 for i in range(self.dataset.GetNumberOfPoints()): 551 carr.InsertNextCell(1) 552 carr.InsertCellPoint(i) 553 self.dataset.SetVerts(carr) 554 555 elif isinstance(inputobj, Points): 556 self.dataset = inputobj.dataset 557 self.copy_properties_from(inputobj) 558 559 elif utils.is_sequence(inputobj): # passing point coords 560 self.dataset = utils.buildPolyData(utils.make3d(inputobj)) 561 562 elif isinstance(inputobj, str): 563 verts = vedo.file_io.load(inputobj) 564 self.filename = inputobj 565 self.dataset = verts.dataset 566 567 else: 568 # try to extract the points from a generic VTK input data object 569 if hasattr(inputobj, "dataset"): 570 inputobj = inputobj.dataset 571 try: 572 vvpts = inputobj.GetPoints() 573 self.dataset = vtki.vtkPolyData() 574 self.dataset.SetPoints(vvpts) 575 for i in range(inputobj.GetPointData().GetNumberOfArrays()): 576 arr = inputobj.GetPointData().GetArray(i) 577 self.dataset.GetPointData().AddArray(arr) 578 except: 579 vedo.logger.error(f"cannot build Points from type {type(inputobj)}") 580 raise RuntimeError() 581 582 self.actor.SetMapper(self.mapper) 583 self.mapper.SetInputData(self.dataset) 584 585 self.properties.SetColor(colors.get_color(c)) 586 self.properties.SetOpacity(alpha) 587 self.properties.SetRepresentationToPoints() 588 self.properties.SetPointSize(r) 589 self.properties.LightingOff() 590 591 self.pipeline = utils.OperationNode( 592 self, parents=[], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 593 ) 594 595 def _update(self, polydata, reset_locators=True) -> Self: 596 """Overwrite the polygonal dataset with a new vtkPolyData.""" 597 self.dataset = polydata 598 self.mapper.SetInputData(self.dataset) 599 self.mapper.Modified() 600 if reset_locators: 601 self.point_locator = None 602 self.line_locator = None 603 self.cell_locator = None 604 return self 605 606 def __str__(self): 607 """Print a description of the Points/Mesh.""" 608 module = self.__class__.__module__ 609 name = self.__class__.__name__ 610 out = vedo.printc( 611 f"{module}.{name} at ({hex(self.memory_address())})".ljust(75), 612 c="g", bold=True, invert=True, return_string=True, 613 ) 614 out += "\x1b[0m\x1b[32;1m" 615 616 if self.name: 617 out += "name".ljust(14) + ": " + self.name 618 if "legend" in self.info.keys() and self.info["legend"]: 619 out+= f", legend='{self.info['legend']}'" 620 out += "\n" 621 622 if self.filename: 623 out+= "file name".ljust(14) + ": " + self.filename + "\n" 624 625 if not self.mapper.GetScalarVisibility(): 626 col = utils.precision(self.properties.GetColor(), 3) 627 cname = vedo.colors.get_color_name(self.properties.GetColor()) 628 out+= "color".ljust(14) + ": " + cname 629 out+= f", rgb={col}, alpha={self.properties.GetOpacity()}\n" 630 if self.actor.GetBackfaceProperty(): 631 bcol = self.actor.GetBackfaceProperty().GetDiffuseColor() 632 cname = vedo.colors.get_color_name(bcol) 633 out+= "backface color".ljust(14) + ": " 634 out+= f"{cname}, rgb={utils.precision(bcol,3)}\n" 635 636 npt = self.dataset.GetNumberOfPoints() 637 npo, nln = self.dataset.GetNumberOfPolys(), self.dataset.GetNumberOfLines() 638 out+= "elements".ljust(14) + f": vertices={npt:,} polygons={npo:,} lines={nln:,}" 639 if self.dataset.GetNumberOfStrips(): 640 out+= f", strips={self.dataset.GetNumberOfStrips():,}" 641 out+= "\n" 642 if self.dataset.GetNumberOfPieces() > 1: 643 out+= "pieces".ljust(14) + ": " + str(self.dataset.GetNumberOfPieces()) + "\n" 644 645 out+= "position".ljust(14) + ": " + f"{utils.precision(self.pos(), 6)}\n" 646 try: 647 sc = self.transform.get_scale() 648 out+= "scaling".ljust(14) + ": " 649 out+= utils.precision(sc, 6) + "\n" 650 except AttributeError: 651 pass 652 653 if self.npoints: 654 out+="size".ljust(14)+ ": average=" + utils.precision(self.average_size(),6) 655 out+=", diagonal="+ utils.precision(self.diagonal_size(), 6)+ "\n" 656 out+="center of mass".ljust(14) + ": " + utils.precision(self.center_of_mass(),6)+"\n" 657 658 bnds = self.bounds() 659 bx1, bx2 = utils.precision(bnds[0], 3), utils.precision(bnds[1], 3) 660 by1, by2 = utils.precision(bnds[2], 3), utils.precision(bnds[3], 3) 661 bz1, bz2 = utils.precision(bnds[4], 3), utils.precision(bnds[5], 3) 662 out+= "bounds".ljust(14) + ":" 663 out+= " x=(" + bx1 + ", " + bx2 + ")," 664 out+= " y=(" + by1 + ", " + by2 + ")," 665 out+= " z=(" + bz1 + ", " + bz2 + ")\n" 666 667 for key in self.pointdata.keys(): 668 arr = self.pointdata[key] 669 dim = arr.shape[1] if arr.ndim > 1 else 1 670 mark_active = "pointdata" 671 a_scalars = self.dataset.GetPointData().GetScalars() 672 a_vectors = self.dataset.GetPointData().GetVectors() 673 a_tensors = self.dataset.GetPointData().GetTensors() 674 if a_scalars and a_scalars.GetName() == key: 675 mark_active += " *" 676 elif a_vectors and a_vectors.GetName() == key: 677 mark_active += " **" 678 elif a_tensors and a_tensors.GetName() == key: 679 mark_active += " ***" 680 out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), dim={dim}' 681 if dim == 1: 682 rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3) 683 out += f", range=({rng})\n" 684 else: 685 out += "\n" 686 687 for key in self.celldata.keys(): 688 arr = self.celldata[key] 689 dim = arr.shape[1] if arr.ndim > 1 else 1 690 mark_active = "celldata" 691 a_scalars = self.dataset.GetCellData().GetScalars() 692 a_vectors = self.dataset.GetCellData().GetVectors() 693 a_tensors = self.dataset.GetCellData().GetTensors() 694 if a_scalars and a_scalars.GetName() == key: 695 mark_active += " *" 696 elif a_vectors and a_vectors.GetName() == key: 697 mark_active += " **" 698 elif a_tensors and a_tensors.GetName() == key: 699 mark_active += " ***" 700 out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), dim={dim}' 701 if dim == 1: 702 rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3) 703 out += f", range=({rng})\n" 704 else: 705 out += "\n" 706 707 for key in self.metadata.keys(): 708 arr = self.metadata[key] 709 if len(arr) > 3: 710 out+= "metadata".ljust(14) + ": " + f'"{key}" ({len(arr)} values)\n' 711 else: 712 out+= "metadata".ljust(14) + ": " + f'"{key}" = {arr}\n' 713 714 if self.picked3d is not None: 715 idp = self.closest_point(self.picked3d, return_point_id=True) 716 idc = self.closest_point(self.picked3d, return_cell_id=True) 717 out+= "clicked point".ljust(14) + ": " + utils.precision(self.picked3d, 6) 718 out+= f", pointID={idp}, cellID={idc}\n" 719 720 return out.rstrip() + "\x1b[0m" 721 722 def _repr_html_(self): 723 """ 724 HTML representation of the Point cloud object for Jupyter Notebooks. 725 726 Returns: 727 HTML text with the image and some properties. 728 """ 729 import io 730 import base64 731 from PIL import Image 732 733 library_name = "vedo.pointcloud.Points" 734 help_url = "https://vedo.embl.es/docs/vedo/pointcloud.html#Points" 735 736 arr = self.thumbnail() 737 im = Image.fromarray(arr) 738 buffered = io.BytesIO() 739 im.save(buffered, format="PNG", quality=100) 740 encoded = base64.b64encode(buffered.getvalue()).decode("utf-8") 741 url = "data:image/png;base64," + encoded 742 image = f"<img src='{url}'></img>" 743 744 bounds = "<br/>".join( 745 [ 746 utils.precision(min_x, 4) + " ... " + utils.precision(max_x, 4) 747 for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2]) 748 ] 749 ) 750 average_size = "{size:.3f}".format(size=self.average_size()) 751 752 help_text = "" 753 if self.name: 754 help_text += f"<b> {self.name}:   </b>" 755 help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>" 756 if self.filename: 757 dots = "" 758 if len(self.filename) > 30: 759 dots = "..." 760 help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>" 761 762 pdata = "" 763 if self.dataset.GetPointData().GetScalars(): 764 if self.dataset.GetPointData().GetScalars().GetName(): 765 name = self.dataset.GetPointData().GetScalars().GetName() 766 pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>" 767 768 cdata = "" 769 if self.dataset.GetCellData().GetScalars(): 770 if self.dataset.GetCellData().GetScalars().GetName(): 771 name = self.dataset.GetCellData().GetScalars().GetName() 772 cdata = "<tr><td><b> cell data array </b></td><td>" + name + "</td></tr>" 773 774 allt = [ 775 "<table>", 776 "<tr>", 777 "<td>", 778 image, 779 "</td>", 780 "<td style='text-align: center; vertical-align: center;'><br/>", 781 help_text, 782 "<table>", 783 "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>", 784 "<tr><td><b> center of mass </b></td><td>" 785 + utils.precision(self.center_of_mass(), 3) 786 + "</td></tr>", 787 "<tr><td><b> average size </b></td><td>" + str(average_size) + "</td></tr>", 788 "<tr><td><b> nr. points </b></td><td>" + str(self.npoints) + "</td></tr>", 789 pdata, 790 cdata, 791 "</table>", 792 "</table>", 793 ] 794 return "\n".join(allt) 795 796 ################################################################################## 797 def __add__(self, meshs): 798 """ 799 Add two meshes or a list of meshes together to form an `Assembly` object. 800 """ 801 if isinstance(meshs, list): 802 alist = [self] 803 for l in meshs: 804 if isinstance(l, vedo.Assembly): 805 alist += l.unpack() 806 else: 807 alist += l 808 return vedo.assembly.Assembly(alist) 809 810 if isinstance(meshs, vedo.Assembly): 811 return meshs + self # use Assembly.__add__ 812 813 return vedo.assembly.Assembly([self, meshs]) 814 815 def polydata(self, **kwargs): 816 """ 817 Obsolete. Use property `.dataset` instead. 818 Returns the underlying `vtkPolyData` object. 819 """ 820 colors.printc( 821 "WARNING: call to .polydata() is obsolete, use property .dataset instead.", 822 c="y") 823 return self.dataset 824 825 def __copy__(self): 826 return self.clone(deep=False) 827 828 def __deepcopy__(self, memo): 829 return self.clone(deep=memo) 830 831 def copy(self, deep=True) -> Self: 832 """Return a copy of the object. Alias of `clone()`.""" 833 return self.clone(deep=deep) 834 835 def clone(self, deep=True) -> Self: 836 """ 837 Clone a `PointCloud` or `Mesh` object to make an exact copy of it. 838 Alias of `copy()`. 839 840 Arguments: 841 deep : (bool) 842 if False return a shallow copy of the mesh without copying the points array. 843 844 Examples: 845 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 846 847 ![](https://vedo.embl.es/images/basic/mirror.png) 848 """ 849 poly = vtki.vtkPolyData() 850 if deep or isinstance(deep, dict): # if a memo object is passed this checks as True 851 poly.DeepCopy(self.dataset) 852 else: 853 poly.ShallowCopy(self.dataset) 854 855 if isinstance(self, vedo.Mesh): 856 cloned = vedo.Mesh(poly) 857 else: 858 cloned = Points(poly) 859 # print([self], self.__class__) 860 # cloned = self.__class__(poly) 861 862 cloned.transform = self.transform.clone() 863 864 cloned.copy_properties_from(self) 865 866 cloned.name = str(self.name) 867 cloned.filename = str(self.filename) 868 cloned.info = dict(self.info) 869 cloned.pipeline = utils.OperationNode("clone", parents=[self], shape="diamond", c="#edede9") 870 871 if isinstance(deep, dict): 872 deep[id(self)] = cloned 873 874 return cloned 875 876 def compute_normals_with_pca(self, n=20, orientation_point=None, invert=False) -> Self: 877 """ 878 Generate point normals using PCA (principal component analysis). 879 This algorithm estimates a local tangent plane around each sample point p 880 by considering a small neighborhood of points around p, and fitting a plane 881 to the neighborhood (via PCA). 882 883 Arguments: 884 n : (int) 885 neighborhood size to calculate the normal 886 orientation_point : (list) 887 adjust the +/- sign of the normals so that 888 the normals all point towards a specified point. If None, perform a traversal 889 of the point cloud and flip neighboring normals so that they are mutually consistent. 890 invert : (bool) 891 flip all normals 892 """ 893 poly = self.dataset 894 pcan = vtki.new("PCANormalEstimation") 895 pcan.SetInputData(poly) 896 pcan.SetSampleSize(n) 897 898 if orientation_point is not None: 899 pcan.SetNormalOrientationToPoint() 900 pcan.SetOrientationPoint(orientation_point) 901 else: 902 pcan.SetNormalOrientationToGraphTraversal() 903 904 if invert: 905 pcan.FlipNormalsOn() 906 pcan.Update() 907 908 varr = pcan.GetOutput().GetPointData().GetNormals() 909 varr.SetName("Normals") 910 self.dataset.GetPointData().SetNormals(varr) 911 self.dataset.GetPointData().Modified() 912 return self 913 914 def compute_acoplanarity(self, n=25, radius=None, on="points") -> Self: 915 """ 916 Compute acoplanarity which is a measure of how much a local region of the mesh 917 differs from a plane. 918 919 The information is stored in a `pointdata` or `celldata` array with name 'Acoplanarity'. 920 921 Either `n` (number of neighbour points) or `radius` (radius of local search) can be specified. 922 If a radius value is given and not enough points fall inside it, then a -1 is stored. 923 924 Example: 925 ```python 926 from vedo import * 927 msh = ParametricShape('RandomHills') 928 msh.compute_acoplanarity(radius=0.1, on='cells') 929 msh.cmap("coolwarm", on='cells').add_scalarbar() 930 msh.show(axes=1).close() 931 ``` 932 ![](https://vedo.embl.es/images/feats/acoplanarity.jpg) 933 """ 934 acoplanarities = [] 935 if "point" in on: 936 pts = self.vertices 937 elif "cell" in on: 938 pts = self.cell_centers 939 else: 940 raise ValueError(f"In compute_acoplanarity() set on to either 'cells' or 'points', not {on}") 941 942 for p in utils.progressbar(pts, delay=5, width=15, title=f"{on} acoplanarity"): 943 if n: 944 data = self.closest_point(p, n=n) 945 npts = n 946 elif radius: 947 data = self.closest_point(p, radius=radius) 948 npts = len(data) 949 950 try: 951 center = data.mean(axis=0) 952 res = np.linalg.svd(data - center) 953 acoplanarities.append(res[1][2] / npts) 954 except: 955 acoplanarities.append(-1.0) 956 957 if "point" in on: 958 self.pointdata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 959 else: 960 self.celldata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 961 return self 962 963 def distance_to(self, pcloud, signed=False, invert=False, name="Distance") -> np.ndarray: 964 """ 965 Computes the distance from one point cloud or mesh to another point cloud or mesh. 966 This new `pointdata` array is saved with default name "Distance". 967 968 Keywords `signed` and `invert` are used to compute signed distance, 969 but the mesh in that case must have polygonal faces (not a simple point cloud), 970 and normals must also be computed. 971 972 Examples: 973 - [distance2mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/distance2mesh.py) 974 975 ![](https://vedo.embl.es/images/basic/distance2mesh.png) 976 """ 977 if pcloud.dataset.GetNumberOfPolys(): 978 979 poly1 = self.dataset 980 poly2 = pcloud.dataset 981 df = vtki.new("DistancePolyDataFilter") 982 df.ComputeSecondDistanceOff() 983 df.SetInputData(0, poly1) 984 df.SetInputData(1, poly2) 985 df.SetSignedDistance(signed) 986 df.SetNegateDistance(invert) 987 df.Update() 988 scals = df.GetOutput().GetPointData().GetScalars() 989 dists = utils.vtk2numpy(scals) 990 991 else: # has no polygons 992 993 if signed: 994 vedo.logger.warning("distance_to() called with signed=True but input object has no polygons") 995 996 if not pcloud.point_locator: 997 pcloud.point_locator = vtki.new("PointLocator") 998 pcloud.point_locator.SetDataSet(pcloud.dataset) 999 pcloud.point_locator.BuildLocator() 1000 1001 ids = [] 1002 ps1 = self.vertices 1003 ps2 = pcloud.vertices 1004 for p in ps1: 1005 pid = pcloud.point_locator.FindClosestPoint(p) 1006 ids.append(pid) 1007 1008 deltas = ps2[ids] - ps1 1009 dists = np.linalg.norm(deltas, axis=1).astype(np.float32) 1010 scals = utils.numpy2vtk(dists) 1011 1012 scals.SetName(name) 1013 self.dataset.GetPointData().AddArray(scals) 1014 self.dataset.GetPointData().SetActiveScalars(scals.GetName()) 1015 rng = scals.GetRange() 1016 self.mapper.SetScalarRange(rng[0], rng[1]) 1017 self.mapper.ScalarVisibilityOn() 1018 1019 self.pipeline = utils.OperationNode( 1020 "distance_to", 1021 parents=[self, pcloud], 1022 shape="cylinder", 1023 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1024 ) 1025 return dists 1026 1027 def clean(self) -> Self: 1028 """Clean pointcloud or mesh by removing coincident points.""" 1029 cpd = vtki.new("CleanPolyData") 1030 cpd.PointMergingOn() 1031 cpd.ConvertLinesToPointsOff() 1032 cpd.ConvertPolysToLinesOff() 1033 cpd.ConvertStripsToPolysOff() 1034 cpd.SetInputData(self.dataset) 1035 cpd.Update() 1036 self._update(cpd.GetOutput()) 1037 self.pipeline = utils.OperationNode( 1038 "clean", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1039 ) 1040 return self 1041 1042 def subsample(self, fraction: float, absolute=False) -> Self: 1043 """ 1044 Subsample a point cloud by requiring that the points 1045 or vertices are far apart at least by the specified fraction of the object size. 1046 If a Mesh is passed the polygonal faces are not removed 1047 but holes can appear as their vertices are removed. 1048 1049 Examples: 1050 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1051 1052 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1053 1054 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1055 1056 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1057 """ 1058 if not absolute: 1059 if fraction > 1: 1060 vedo.logger.warning( 1061 f"subsample(fraction=...), fraction must be < 1, but is {fraction}" 1062 ) 1063 if fraction <= 0: 1064 return self 1065 1066 cpd = vtki.new("CleanPolyData") 1067 cpd.PointMergingOn() 1068 cpd.ConvertLinesToPointsOn() 1069 cpd.ConvertPolysToLinesOn() 1070 cpd.ConvertStripsToPolysOn() 1071 cpd.SetInputData(self.dataset) 1072 if absolute: 1073 cpd.SetTolerance(fraction / self.diagonal_size()) 1074 # cpd.SetToleranceIsAbsolute(absolute) 1075 else: 1076 cpd.SetTolerance(fraction) 1077 cpd.Update() 1078 1079 ps = 2 1080 if self.properties.GetRepresentation() == 0: 1081 ps = self.properties.GetPointSize() 1082 1083 self._update(cpd.GetOutput()) 1084 self.ps(ps) 1085 1086 self.pipeline = utils.OperationNode( 1087 "subsample", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1088 ) 1089 return self 1090 1091 def threshold(self, scalars: str, above=None, below=None, on="points") -> Self: 1092 """ 1093 Extracts cells where scalar value satisfies threshold criterion. 1094 1095 Arguments: 1096 scalars : (str) 1097 name of the scalars array. 1098 above : (float) 1099 minimum value of the scalar 1100 below : (float) 1101 maximum value of the scalar 1102 on : (str) 1103 if 'cells' assume array of scalars refers to cell data. 1104 1105 Examples: 1106 - [mesh_threshold.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_threshold.py) 1107 """ 1108 thres = vtki.new("Threshold") 1109 thres.SetInputData(self.dataset) 1110 1111 if on.startswith("c"): 1112 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_CELLS 1113 else: 1114 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_POINTS 1115 1116 thres.SetInputArrayToProcess(0, 0, 0, asso, scalars) 1117 1118 if above is None and below is not None: 1119 try: # vtk 9.2 1120 thres.ThresholdByLower(below) 1121 except AttributeError: # vtk 9.3 1122 thres.SetUpperThreshold(below) 1123 1124 elif below is None and above is not None: 1125 try: 1126 thres.ThresholdByUpper(above) 1127 except AttributeError: 1128 thres.SetLowerThreshold(above) 1129 else: 1130 try: 1131 thres.ThresholdBetween(above, below) 1132 except AttributeError: 1133 thres.SetUpperThreshold(below) 1134 thres.SetLowerThreshold(above) 1135 1136 thres.Update() 1137 1138 gf = vtki.new("GeometryFilter") 1139 gf.SetInputData(thres.GetOutput()) 1140 gf.Update() 1141 self._update(gf.GetOutput()) 1142 self.pipeline = utils.OperationNode("threshold", parents=[self]) 1143 return self 1144 1145 def quantize(self, value: float) -> Self: 1146 """ 1147 The user should input a value and all {x,y,z} coordinates 1148 will be quantized to that absolute grain size. 1149 """ 1150 qp = vtki.new("QuantizePolyDataPoints") 1151 qp.SetInputData(self.dataset) 1152 qp.SetQFactor(value) 1153 qp.Update() 1154 self._update(qp.GetOutput()) 1155 self.pipeline = utils.OperationNode("quantize", parents=[self]) 1156 return self 1157 1158 @property 1159 def vertex_normals(self) -> np.ndarray: 1160 """ 1161 Retrieve vertex normals as a numpy array. Same as `point_normals`. 1162 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1163 """ 1164 vtknormals = self.dataset.GetPointData().GetNormals() 1165 return utils.vtk2numpy(vtknormals) 1166 1167 @property 1168 def point_normals(self) -> np.ndarray: 1169 """ 1170 Retrieve vertex normals as a numpy array. Same as `vertex_normals`. 1171 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1172 """ 1173 vtknormals = self.dataset.GetPointData().GetNormals() 1174 return utils.vtk2numpy(vtknormals) 1175 1176 def align_to(self, target, iters=100, rigid=False, invert=False, use_centroids=False) -> Self: 1177 """ 1178 Aligned to target mesh through the `Iterative Closest Point` algorithm. 1179 1180 The core of the algorithm is to match each vertex in one surface with 1181 the closest surface point on the other, then apply the transformation 1182 that modify one surface to best match the other (in the least-square sense). 1183 1184 Arguments: 1185 rigid : (bool) 1186 if True do not allow scaling 1187 invert : (bool) 1188 if True start by aligning the target to the source but 1189 invert the transformation finally. Useful when the target is smaller 1190 than the source. 1191 use_centroids : (bool) 1192 start by matching the centroids of the two objects. 1193 1194 Examples: 1195 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1196 1197 ![](https://vedo.embl.es/images/basic/align1.png) 1198 1199 - [align2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align2.py) 1200 1201 ![](https://vedo.embl.es/images/basic/align2.png) 1202 """ 1203 icp = vtki.new("IterativeClosestPointTransform") 1204 icp.SetSource(self.dataset) 1205 icp.SetTarget(target.dataset) 1206 if invert: 1207 icp.Inverse() 1208 icp.SetMaximumNumberOfIterations(iters) 1209 if rigid: 1210 icp.GetLandmarkTransform().SetModeToRigidBody() 1211 icp.SetStartByMatchingCentroids(use_centroids) 1212 icp.Update() 1213 1214 self.apply_transform(icp.GetMatrix()) 1215 1216 self.pipeline = utils.OperationNode( 1217 "align_to", parents=[self, target], comment=f"rigid = {rigid}" 1218 ) 1219 return self 1220 1221 def align_to_bounding_box(self, msh, rigid=False) -> Self: 1222 """ 1223 Align the current object's bounding box to the bounding box 1224 of the input object. 1225 1226 Use `rigid=True` to disable scaling. 1227 1228 Example: 1229 [align6.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align6.py) 1230 """ 1231 lmt = vtki.vtkLandmarkTransform() 1232 ss = vtki.vtkPoints() 1233 xss0, xss1, yss0, yss1, zss0, zss1 = self.bounds() 1234 for p in [ 1235 [xss0, yss0, zss0], 1236 [xss1, yss0, zss0], 1237 [xss1, yss1, zss0], 1238 [xss0, yss1, zss0], 1239 [xss0, yss0, zss1], 1240 [xss1, yss0, zss1], 1241 [xss1, yss1, zss1], 1242 [xss0, yss1, zss1], 1243 ]: 1244 ss.InsertNextPoint(p) 1245 st = vtki.vtkPoints() 1246 xst0, xst1, yst0, yst1, zst0, zst1 = msh.bounds() 1247 for p in [ 1248 [xst0, yst0, zst0], 1249 [xst1, yst0, zst0], 1250 [xst1, yst1, zst0], 1251 [xst0, yst1, zst0], 1252 [xst0, yst0, zst1], 1253 [xst1, yst0, zst1], 1254 [xst1, yst1, zst1], 1255 [xst0, yst1, zst1], 1256 ]: 1257 st.InsertNextPoint(p) 1258 1259 lmt.SetSourceLandmarks(ss) 1260 lmt.SetTargetLandmarks(st) 1261 lmt.SetModeToAffine() 1262 if rigid: 1263 lmt.SetModeToRigidBody() 1264 lmt.Update() 1265 1266 LT = LinearTransform(lmt) 1267 self.apply_transform(LT) 1268 return self 1269 1270 def align_with_landmarks( 1271 self, 1272 source_landmarks, 1273 target_landmarks, 1274 rigid=False, 1275 affine=False, 1276 least_squares=False, 1277 ) -> Self: 1278 """ 1279 Transform mesh orientation and position based on a set of landmarks points. 1280 The algorithm finds the best matching of source points to target points 1281 in the mean least square sense, in one single step. 1282 1283 If `affine` is True the x, y and z axes can scale independently but stay collinear. 1284 With least_squares they can vary orientation. 1285 1286 Examples: 1287 - [align5.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align5.py) 1288 1289 ![](https://vedo.embl.es/images/basic/align5.png) 1290 """ 1291 1292 if utils.is_sequence(source_landmarks): 1293 ss = vtki.vtkPoints() 1294 for p in source_landmarks: 1295 ss.InsertNextPoint(p) 1296 else: 1297 ss = source_landmarks.dataset.GetPoints() 1298 if least_squares: 1299 source_landmarks = source_landmarks.vertices 1300 1301 if utils.is_sequence(target_landmarks): 1302 st = vtki.vtkPoints() 1303 for p in target_landmarks: 1304 st.InsertNextPoint(p) 1305 else: 1306 st = target_landmarks.GetPoints() 1307 if least_squares: 1308 target_landmarks = target_landmarks.vertices 1309 1310 if ss.GetNumberOfPoints() != st.GetNumberOfPoints(): 1311 n1 = ss.GetNumberOfPoints() 1312 n2 = st.GetNumberOfPoints() 1313 vedo.logger.error(f"source and target have different nr of points {n1} vs {n2}") 1314 raise RuntimeError() 1315 1316 if int(rigid) + int(affine) + int(least_squares) > 1: 1317 vedo.logger.error( 1318 "only one of rigid, affine, least_squares can be True at a time" 1319 ) 1320 raise RuntimeError() 1321 1322 lmt = vtki.vtkLandmarkTransform() 1323 lmt.SetSourceLandmarks(ss) 1324 lmt.SetTargetLandmarks(st) 1325 lmt.SetModeToSimilarity() 1326 1327 if rigid: 1328 lmt.SetModeToRigidBody() 1329 lmt.Update() 1330 1331 elif affine: 1332 lmt.SetModeToAffine() 1333 lmt.Update() 1334 1335 elif least_squares: 1336 cms = source_landmarks.mean(axis=0) 1337 cmt = target_landmarks.mean(axis=0) 1338 m = np.linalg.lstsq(source_landmarks - cms, target_landmarks - cmt, rcond=None)[0] 1339 M = vtki.vtkMatrix4x4() 1340 for i in range(3): 1341 for j in range(3): 1342 M.SetElement(j, i, m[i][j]) 1343 lmt = vtki.vtkTransform() 1344 lmt.Translate(cmt) 1345 lmt.Concatenate(M) 1346 lmt.Translate(-cms) 1347 1348 else: 1349 lmt.Update() 1350 1351 self.apply_transform(lmt) 1352 self.pipeline = utils.OperationNode("transform_with_landmarks", parents=[self]) 1353 return self 1354 1355 def normalize(self) -> Self: 1356 """Scale average size to unit. The scaling is performed around the center of mass.""" 1357 coords = self.vertices 1358 if not coords.shape[0]: 1359 return self 1360 cm = np.mean(coords, axis=0) 1361 pts = coords - cm 1362 xyz2 = np.sum(pts * pts, axis=0) 1363 scale = 1 / np.sqrt(np.sum(xyz2) / len(pts)) 1364 self.scale(scale, origin=cm) 1365 self.pipeline = utils.OperationNode("normalize", parents=[self]) 1366 return self 1367 1368 def mirror(self, axis="x", origin=True) -> Self: 1369 """ 1370 Mirror reflect along one of the cartesian axes 1371 1372 Arguments: 1373 axis : (str) 1374 axis to use for mirroring, must be set to `x, y, z`. 1375 Or any combination of those. 1376 origin : (list) 1377 use this point as the origin of the mirroring transformation. 1378 1379 Examples: 1380 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 1381 1382 ![](https://vedo.embl.es/images/basic/mirror.png) 1383 """ 1384 sx, sy, sz = 1, 1, 1 1385 if "x" in axis.lower(): sx = -1 1386 if "y" in axis.lower(): sy = -1 1387 if "z" in axis.lower(): sz = -1 1388 1389 self.scale([sx, sy, sz], origin=origin) 1390 1391 self.pipeline = utils.OperationNode( 1392 "mirror", comment=f"axis = {axis}", parents=[self]) 1393 1394 if sx * sy * sz < 0: 1395 if hasattr(self, "reverse"): 1396 self.reverse() 1397 return self 1398 1399 def flip_normals(self) -> Self: 1400 """Flip all normals orientation.""" 1401 rs = vtki.new("ReverseSense") 1402 rs.SetInputData(self.dataset) 1403 rs.ReverseCellsOff() 1404 rs.ReverseNormalsOn() 1405 rs.Update() 1406 self._update(rs.GetOutput()) 1407 self.pipeline = utils.OperationNode("flip_normals", parents=[self]) 1408 return self 1409 1410 def add_gaussian_noise(self, sigma=1.0) -> Self: 1411 """ 1412 Add gaussian noise to point positions. 1413 An extra array is added named "GaussianNoise" with the displacements. 1414 1415 Arguments: 1416 sigma : (float) 1417 nr. of standard deviations, expressed in percent of the diagonal size of mesh. 1418 Can also be a list `[sigma_x, sigma_y, sigma_z]`. 1419 1420 Example: 1421 ```python 1422 from vedo import Sphere 1423 Sphere().add_gaussian_noise(1.0).point_size(8).show().close() 1424 ``` 1425 """ 1426 sz = self.diagonal_size() 1427 pts = self.vertices 1428 n = len(pts) 1429 ns = (np.random.randn(n, 3) * sigma) * (sz / 100) 1430 vpts = vtki.vtkPoints() 1431 vpts.SetNumberOfPoints(n) 1432 vpts.SetData(utils.numpy2vtk(pts + ns, dtype=np.float32)) 1433 self.dataset.SetPoints(vpts) 1434 self.dataset.GetPoints().Modified() 1435 self.pointdata["GaussianNoise"] = -ns 1436 self.pipeline = utils.OperationNode( 1437 "gaussian_noise", parents=[self], shape="egg", comment=f"sigma = {sigma}" 1438 ) 1439 return self 1440 1441 def closest_point( 1442 self, pt, n=1, radius=None, return_point_id=False, return_cell_id=False 1443 ) -> Union[List[int], int, np.ndarray]: 1444 """ 1445 Find the closest point(s) on a mesh given from the input point `pt`. 1446 1447 Arguments: 1448 n : (int) 1449 if greater than 1, return a list of n ordered closest points 1450 radius : (float) 1451 if given, get all points within that radius. Then n is ignored. 1452 return_point_id : (bool) 1453 return point ID instead of coordinates 1454 return_cell_id : (bool) 1455 return cell ID in which the closest point sits 1456 1457 Examples: 1458 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1459 - [fitplanes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitplanes.py) 1460 - [quadratic_morphing.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/quadratic_morphing.py) 1461 1462 .. note:: 1463 The appropriate tree search locator is built on the fly and cached for speed. 1464 1465 If you want to reset it use `mymesh.point_locator=None` 1466 and / or `mymesh.cell_locator=None`. 1467 """ 1468 if len(pt) != 3: 1469 pt = [pt[0], pt[1], 0] 1470 1471 # NB: every time the mesh moves or is warped the locators are set to None 1472 if ((n > 1 or radius) or (n == 1 and return_point_id)) and not return_cell_id: 1473 poly = None 1474 if not self.point_locator: 1475 poly = self.dataset 1476 self.point_locator = vtki.new("StaticPointLocator") 1477 self.point_locator.SetDataSet(poly) 1478 self.point_locator.BuildLocator() 1479 1480 ########## 1481 if radius: 1482 vtklist = vtki.vtkIdList() 1483 self.point_locator.FindPointsWithinRadius(radius, pt, vtklist) 1484 elif n > 1: 1485 vtklist = vtki.vtkIdList() 1486 self.point_locator.FindClosestNPoints(n, pt, vtklist) 1487 else: # n==1 hence return_point_id==True 1488 ######## 1489 return self.point_locator.FindClosestPoint(pt) 1490 ######## 1491 1492 if return_point_id: 1493 ######## 1494 return utils.vtk2numpy(vtklist) 1495 ######## 1496 1497 if not poly: 1498 poly = self.dataset 1499 trgp = [] 1500 for i in range(vtklist.GetNumberOfIds()): 1501 trgp_ = [0, 0, 0] 1502 vi = vtklist.GetId(i) 1503 poly.GetPoints().GetPoint(vi, trgp_) 1504 trgp.append(trgp_) 1505 ######## 1506 return np.array(trgp) 1507 ######## 1508 1509 else: 1510 1511 if not self.cell_locator: 1512 poly = self.dataset 1513 1514 # As per Miquel example with limbs the vtkStaticCellLocator doesnt work !! 1515 # https://discourse.vtk.org/t/vtkstaticcelllocator-problem-vtk9-0-3/7854/4 1516 if vedo.vtk_version[0] >= 9 and vedo.vtk_version[1] > 0: 1517 self.cell_locator = vtki.new("StaticCellLocator") 1518 else: 1519 self.cell_locator = vtki.new("CellLocator") 1520 1521 self.cell_locator.SetDataSet(poly) 1522 self.cell_locator.BuildLocator() 1523 1524 if radius is not None: 1525 vedo.printc("Warning: closest_point() with radius is not implemented for cells.", c='r') 1526 1527 if n != 1: 1528 vedo.printc("Warning: closest_point() with n>1 is not implemented for cells.", c='r') 1529 1530 trgp = [0, 0, 0] 1531 cid = vtki.mutable(0) 1532 dist2 = vtki.mutable(0) 1533 subid = vtki.mutable(0) 1534 self.cell_locator.FindClosestPoint(pt, trgp, cid, subid, dist2) 1535 1536 if return_cell_id: 1537 return int(cid) 1538 1539 return np.array(trgp) 1540 1541 def auto_distance(self) -> np.ndarray: 1542 """ 1543 Calculate the distance to the closest point in the same cloud of points. 1544 The output is stored in a new pointdata array called "AutoDistance", 1545 and it is also returned by the function. 1546 """ 1547 points = self.vertices 1548 if not self.point_locator: 1549 self.point_locator = vtki.new("StaticPointLocator") 1550 self.point_locator.SetDataSet(self.dataset) 1551 self.point_locator.BuildLocator() 1552 qs = [] 1553 vtklist = vtki.vtkIdList() 1554 vtkpoints = self.dataset.GetPoints() 1555 for p in points: 1556 self.point_locator.FindClosestNPoints(2, p, vtklist) 1557 q = [0, 0, 0] 1558 pid = vtklist.GetId(1) 1559 vtkpoints.GetPoint(pid, q) 1560 qs.append(q) 1561 dists = np.linalg.norm(points - np.array(qs), axis=1) 1562 self.pointdata["AutoDistance"] = dists 1563 return dists 1564 1565 def hausdorff_distance(self, points) -> float: 1566 """ 1567 Compute the Hausdorff distance to the input point set. 1568 Returns a single `float`. 1569 1570 Example: 1571 ```python 1572 from vedo import * 1573 t = np.linspace(0, 2*np.pi, 100) 1574 x = 4/3 * sin(t)**3 1575 y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12 1576 pol1 = Line(np.c_[x,y], closed=True).triangulate() 1577 pol2 = Polygon(nsides=5).pos(2,2) 1578 d12 = pol1.distance_to(pol2) 1579 d21 = pol2.distance_to(pol1) 1580 pol1.lw(0).cmap("viridis") 1581 pol2.lw(0).cmap("viridis") 1582 print("distance d12, d21 :", min(d12), min(d21)) 1583 print("hausdorff distance:", pol1.hausdorff_distance(pol2)) 1584 print("chamfer distance :", pol1.chamfer_distance(pol2)) 1585 show(pol1, pol2, axes=1) 1586 ``` 1587 ![](https://vedo.embl.es/images/feats/heart.png) 1588 """ 1589 hp = vtki.new("HausdorffDistancePointSetFilter") 1590 hp.SetInputData(0, self.dataset) 1591 hp.SetInputData(1, points.dataset) 1592 hp.SetTargetDistanceMethodToPointToCell() 1593 hp.Update() 1594 return hp.GetHausdorffDistance() 1595 1596 def chamfer_distance(self, pcloud) -> float: 1597 """ 1598 Compute the Chamfer distance to the input point set. 1599 1600 Example: 1601 ```python 1602 from vedo import * 1603 cloud1 = np.random.randn(1000, 3) 1604 cloud2 = np.random.randn(1000, 3) + [1, 2, 3] 1605 c1 = Points(cloud1, r=5, c="red") 1606 c2 = Points(cloud2, r=5, c="green") 1607 d = c1.chamfer_distance(c2) 1608 show(f"Chamfer distance = {d}", c1, c2, axes=1).close() 1609 ``` 1610 """ 1611 # Definition of Chamfer distance may vary, here we use the average 1612 if not pcloud.point_locator: 1613 pcloud.point_locator = vtki.new("PointLocator") 1614 pcloud.point_locator.SetDataSet(pcloud.dataset) 1615 pcloud.point_locator.BuildLocator() 1616 if not self.point_locator: 1617 self.point_locator = vtki.new("PointLocator") 1618 self.point_locator.SetDataSet(self.dataset) 1619 self.point_locator.BuildLocator() 1620 1621 ps1 = self.vertices 1622 ps2 = pcloud.vertices 1623 1624 ids12 = [] 1625 for p in ps1: 1626 pid12 = pcloud.point_locator.FindClosestPoint(p) 1627 ids12.append(pid12) 1628 deltav = ps2[ids12] - ps1 1629 da = np.mean(np.linalg.norm(deltav, axis=1)) 1630 1631 ids21 = [] 1632 for p in ps2: 1633 pid21 = self.point_locator.FindClosestPoint(p) 1634 ids21.append(pid21) 1635 deltav = ps1[ids21] - ps2 1636 db = np.mean(np.linalg.norm(deltav, axis=1)) 1637 return (da + db) / 2 1638 1639 def remove_outliers(self, radius: float, neighbors=5) -> Self: 1640 """ 1641 Remove outliers from a cloud of points within the specified `radius` search. 1642 1643 Arguments: 1644 radius : (float) 1645 Specify the local search radius. 1646 neighbors : (int) 1647 Specify the number of neighbors that a point must have, 1648 within the specified radius, for the point to not be considered isolated. 1649 1650 Examples: 1651 - [clustering.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/clustering.py) 1652 1653 ![](https://vedo.embl.es/images/basic/clustering.png) 1654 """ 1655 removal = vtki.new("RadiusOutlierRemoval") 1656 removal.SetInputData(self.dataset) 1657 removal.SetRadius(radius) 1658 removal.SetNumberOfNeighbors(neighbors) 1659 removal.GenerateOutliersOff() 1660 removal.Update() 1661 inputobj = removal.GetOutput() 1662 if inputobj.GetNumberOfCells() == 0: 1663 carr = vtki.vtkCellArray() 1664 for i in range(inputobj.GetNumberOfPoints()): 1665 carr.InsertNextCell(1) 1666 carr.InsertCellPoint(i) 1667 inputobj.SetVerts(carr) 1668 self._update(removal.GetOutput()) 1669 self.pipeline = utils.OperationNode("remove_outliers", parents=[self]) 1670 return self 1671 1672 def relax_point_positions( 1673 self, 1674 n=10, 1675 iters=10, 1676 sub_iters=10, 1677 packing_factor=1, 1678 max_step=0, 1679 constraints=(), 1680 ) -> Self: 1681 """ 1682 Smooth mesh or points with a 1683 [Laplacian algorithm](https://vtk.org/doc/nightly/html/classvtkPointSmoothingFilter.html) 1684 variant. This modifies the coordinates of the input points by adjusting their positions 1685 to create a smooth distribution (and thereby form a pleasing packing of the points). 1686 Smoothing is performed by considering the effects of neighboring points on one another 1687 it uses a cubic cutoff function to produce repulsive forces between close points 1688 and attractive forces that are a little further away. 1689 1690 In general, the larger the neighborhood size, the greater the reduction in high frequency 1691 information. The memory and computational requirements of the algorithm may also 1692 significantly increase. 1693 1694 The algorithm incrementally adjusts the point positions through an iterative process. 1695 Basically points are moved due to the influence of neighboring points. 1696 1697 As points move, both the local connectivity and data attributes associated with each point 1698 must be updated. Rather than performing these expensive operations after every iteration, 1699 a number of sub-iterations can be specified. If so, then the neighborhood and attribute 1700 value updates occur only every sub iteration, which can improve performance significantly. 1701 1702 Arguments: 1703 n : (int) 1704 neighborhood size to calculate the Laplacian. 1705 iters : (int) 1706 number of iterations. 1707 sub_iters : (int) 1708 number of sub-iterations, i.e. the number of times the neighborhood and attribute 1709 value updates occur during each iteration. 1710 packing_factor : (float) 1711 adjust convergence speed. 1712 max_step : (float) 1713 Specify the maximum smoothing step size for each smoothing iteration. 1714 This limits the the distance over which a point can move in each iteration. 1715 As in all iterative methods, the stability of the process is sensitive to this parameter. 1716 In general, small step size and large numbers of iterations are more stable than a larger 1717 step size and a smaller numbers of iterations. 1718 constraints : (dict) 1719 dictionary of constraints. 1720 Point constraints are used to prevent points from moving, 1721 or to move only on a plane. This can prevent shrinking or growing point clouds. 1722 If enabled, a local topological analysis is performed to determine whether a point 1723 should be marked as fixed" i.e., never moves, or the point only moves on a plane, 1724 or the point can move freely. 1725 If all points in the neighborhood surrounding a point are in the cone defined by 1726 `fixed_angle`, then the point is classified as fixed. 1727 If all points in the neighborhood surrounding a point are in the cone defined by 1728 `boundary_angle`, then the point is classified as lying on a plane. 1729 Angles are expressed in degrees. 1730 1731 Example: 1732 ```py 1733 import numpy as np 1734 from vedo import Points, show 1735 from vedo.pyplot import histogram 1736 1737 vpts1 = Points(np.random.rand(10_000, 3)) 1738 dists = vpts1.auto_distance() 1739 h1 = histogram(dists, xlim=(0,0.08)).clone2d() 1740 1741 vpts2 = vpts1.clone().relax_point_positions(n=100, iters=20, sub_iters=10) 1742 dists = vpts2.auto_distance() 1743 h2 = histogram(dists, xlim=(0,0.08)).clone2d() 1744 1745 show([[vpts1, h1], [vpts2, h2]], N=2).close() 1746 ``` 1747 """ 1748 smooth = vtki.new("PointSmoothingFilter") 1749 smooth.SetInputData(self.dataset) 1750 smooth.SetSmoothingModeToUniform() 1751 smooth.SetNumberOfIterations(iters) 1752 smooth.SetNumberOfSubIterations(sub_iters) 1753 smooth.SetPackingFactor(packing_factor) 1754 if self.point_locator: 1755 smooth.SetLocator(self.point_locator) 1756 if not max_step: 1757 max_step = self.diagonal_size() / 100 1758 smooth.SetMaximumStepSize(max_step) 1759 smooth.SetNeighborhoodSize(n) 1760 if constraints: 1761 fixed_angle = constraints.get("fixed_angle", 45) 1762 boundary_angle = constraints.get("boundary_angle", 110) 1763 smooth.EnableConstraintsOn() 1764 smooth.SetFixedAngle(fixed_angle) 1765 smooth.SetBoundaryAngle(boundary_angle) 1766 smooth.GenerateConstraintScalarsOn() 1767 smooth.GenerateConstraintNormalsOn() 1768 smooth.Update() 1769 self._update(smooth.GetOutput()) 1770 self.metadata["PackingRadius"] = smooth.GetPackingRadius() 1771 self.pipeline = utils.OperationNode("relax_point_positions", parents=[self]) 1772 return self 1773 1774 def smooth_mls_1d(self, f=0.2, radius=None, n=0) -> Self: 1775 """ 1776 Smooth mesh or points with a `Moving Least Squares` variant. 1777 The point data array "Variances" will contain the residue calculated for each point. 1778 1779 Arguments: 1780 f : (float) 1781 smoothing factor - typical range is [0,2]. 1782 radius : (float) 1783 radius search in absolute units. 1784 If set then `f` is ignored. 1785 n : (int) 1786 number of neighbours to be used for the fit. 1787 If set then `f` and `radius` are ignored. 1788 1789 Examples: 1790 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1791 - [skeletonize.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/skeletonize.py) 1792 1793 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1794 """ 1795 coords = self.vertices 1796 ncoords = len(coords) 1797 1798 if n: 1799 Ncp = n 1800 elif radius: 1801 Ncp = 1 1802 else: 1803 Ncp = int(ncoords * f / 10) 1804 if Ncp < 5: 1805 vedo.logger.warning(f"Please choose a fraction higher than {f}") 1806 Ncp = 5 1807 1808 variances, newline = [], [] 1809 for p in coords: 1810 points = self.closest_point(p, n=Ncp, radius=radius) 1811 if len(points) < 4: 1812 continue 1813 1814 points = np.array(points) 1815 pointsmean = points.mean(axis=0) # plane center 1816 _, dd, vv = np.linalg.svd(points - pointsmean) 1817 newp = np.dot(p - pointsmean, vv[0]) * vv[0] + pointsmean 1818 variances.append(dd[1] + dd[2]) 1819 newline.append(newp) 1820 1821 self.pointdata["Variances"] = np.array(variances).astype(np.float32) 1822 self.vertices = newline 1823 self.pipeline = utils.OperationNode("smooth_mls_1d", parents=[self]) 1824 return self 1825 1826 def smooth_mls_2d(self, f=0.2, radius=None, n=0) -> Self: 1827 """ 1828 Smooth mesh or points with a `Moving Least Squares` algorithm variant. 1829 1830 The `mesh.pointdata['MLSVariance']` array will contain the residue calculated for each point. 1831 When a radius is specified, points that are isolated will not be moved and will get 1832 a 0 entry in array `mesh.pointdata['MLSValidPoint']`. 1833 1834 Arguments: 1835 f : (float) 1836 smoothing factor - typical range is [0, 2]. 1837 radius : (float | array) 1838 radius search in absolute units. Can be single value (float) or sequence 1839 for adaptive smoothing. If set then `f` is ignored. 1840 n : (int) 1841 number of neighbours to be used for the fit. 1842 If set then `f` and `radius` are ignored. 1843 1844 Examples: 1845 - [moving_least_squares2D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares2D.py) 1846 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1847 1848 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1849 """ 1850 coords = self.vertices 1851 ncoords = len(coords) 1852 1853 if n: 1854 Ncp = n 1855 radius = None 1856 elif radius is not None: 1857 Ncp = 1 1858 else: 1859 Ncp = int(ncoords * f / 100) 1860 if Ncp < 4: 1861 vedo.logger.error(f"please choose a f-value higher than {f}") 1862 Ncp = 4 1863 1864 variances, newpts, valid = [], [], [] 1865 radius_is_sequence = utils.is_sequence(radius) 1866 1867 pb = None 1868 if ncoords > 10000: 1869 pb = utils.ProgressBar(0, ncoords, delay=3) 1870 1871 for i, p in enumerate(coords): 1872 if pb: 1873 pb.print("smooth_mls_2d working ...") 1874 1875 # if a radius was provided for each point 1876 if radius_is_sequence: 1877 pts = self.closest_point(p, n=Ncp, radius=radius[i]) 1878 else: 1879 pts = self.closest_point(p, n=Ncp, radius=radius) 1880 1881 if len(pts) > 3: 1882 ptsmean = pts.mean(axis=0) # plane center 1883 _, dd, vv = np.linalg.svd(pts - ptsmean) 1884 cv = np.cross(vv[0], vv[1]) 1885 t = (np.dot(cv, ptsmean) - np.dot(cv, p)) / np.dot(cv, cv) 1886 newpts.append(p + cv * t) 1887 variances.append(dd[2]) 1888 if radius is not None: 1889 valid.append(1) 1890 else: 1891 newpts.append(p) 1892 variances.append(0) 1893 if radius is not None: 1894 valid.append(0) 1895 1896 if radius is not None: 1897 self.pointdata["MLSValidPoint"] = np.array(valid).astype(np.uint8) 1898 self.pointdata["MLSVariance"] = np.array(variances).astype(np.float32) 1899 1900 self.vertices = newpts 1901 1902 self.pipeline = utils.OperationNode("smooth_mls_2d", parents=[self]) 1903 return self 1904 1905 def smooth_lloyd_2d(self, iterations=2, bounds=None, options="Qbb Qc Qx") -> Self: 1906 """ 1907 Lloyd relaxation of a 2D pointcloud. 1908 1909 Arguments: 1910 iterations : (int) 1911 number of iterations. 1912 bounds : (list) 1913 bounding box of the domain. 1914 options : (str) 1915 options for the Qhull algorithm. 1916 """ 1917 # Credits: https://hatarilabs.com/ih-en/ 1918 # tutorial-to-create-a-geospatial-voronoi-sh-mesh-with-python-scipy-and-geopandas 1919 from scipy.spatial import Voronoi as scipy_voronoi 1920 1921 def _constrain_points(points): 1922 # Update any points that have drifted beyond the boundaries of this space 1923 if bounds is not None: 1924 for point in points: 1925 if point[0] < bounds[0]: point[0] = bounds[0] 1926 if point[0] > bounds[1]: point[0] = bounds[1] 1927 if point[1] < bounds[2]: point[1] = bounds[2] 1928 if point[1] > bounds[3]: point[1] = bounds[3] 1929 return points 1930 1931 def _find_centroid(vertices): 1932 # The equation for the method used here to find the centroid of a 1933 # 2D polygon is given here: https://en.wikipedia.org/wiki/Centroid#Of_a_polygon 1934 area = 0 1935 centroid_x = 0 1936 centroid_y = 0 1937 for i in range(len(vertices) - 1): 1938 step = (vertices[i, 0] * vertices[i + 1, 1]) - (vertices[i + 1, 0] * vertices[i, 1]) 1939 centroid_x += (vertices[i, 0] + vertices[i + 1, 0]) * step 1940 centroid_y += (vertices[i, 1] + vertices[i + 1, 1]) * step 1941 area += step 1942 if area: 1943 centroid_x = (1.0 / (3.0 * area)) * centroid_x 1944 centroid_y = (1.0 / (3.0 * area)) * centroid_y 1945 # prevent centroids from escaping bounding box 1946 return _constrain_points([[centroid_x, centroid_y]])[0] 1947 1948 def _relax(voron): 1949 # Moves each point to the centroid of its cell in the voronoi 1950 # map to "relax" the points (i.e. jitter the points so as 1951 # to spread them out within the space). 1952 centroids = [] 1953 for idx in voron.point_region: 1954 # the region is a series of indices into voronoi.vertices 1955 # remove point at infinity, designated by index -1 1956 region = [i for i in voron.regions[idx] if i != -1] 1957 # enclose the polygon 1958 region = region + [region[0]] 1959 verts = voron.vertices[region] 1960 # find the centroid of those vertices 1961 centroids.append(_find_centroid(verts)) 1962 return _constrain_points(centroids) 1963 1964 if bounds is None: 1965 bounds = self.bounds() 1966 1967 pts = self.vertices[:, (0, 1)] 1968 for i in range(iterations): 1969 vor = scipy_voronoi(pts, qhull_options=options) 1970 _constrain_points(vor.vertices) 1971 pts = _relax(vor) 1972 out = Points(pts) 1973 out.name = "MeshSmoothLloyd2D" 1974 out.pipeline = utils.OperationNode("smooth_lloyd", parents=[self]) 1975 return out 1976 1977 def project_on_plane(self, plane="z", point=None, direction=None) -> Self: 1978 """ 1979 Project the mesh on one of the Cartesian planes. 1980 1981 Arguments: 1982 plane : (str, Plane) 1983 if plane is `str`, plane can be one of ['x', 'y', 'z'], 1984 represents x-plane, y-plane and z-plane, respectively. 1985 Otherwise, plane should be an instance of `vedo.shapes.Plane`. 1986 point : (float, array) 1987 if plane is `str`, point should be a float represents the intercept. 1988 Otherwise, point is the camera point of perspective projection 1989 direction : (array) 1990 direction of oblique projection 1991 1992 Note: 1993 Parameters `point` and `direction` are only used if the given plane 1994 is an instance of `vedo.shapes.Plane`. And one of these two params 1995 should be left as `None` to specify the projection type. 1996 1997 Example: 1998 ```python 1999 s.project_on_plane(plane='z') # project to z-plane 2000 plane = Plane(pos=(4, 8, -4), normal=(-1, 0, 1), s=(5,5)) 2001 s.project_on_plane(plane=plane) # orthogonal projection 2002 s.project_on_plane(plane=plane, point=(6, 6, 6)) # perspective projection 2003 s.project_on_plane(plane=plane, direction=(1, 2, -1)) # oblique projection 2004 ``` 2005 2006 Examples: 2007 - [silhouette2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette2.py) 2008 2009 ![](https://vedo.embl.es/images/basic/silhouette2.png) 2010 """ 2011 coords = self.vertices 2012 2013 if plane == "x": 2014 coords[:, 0] = self.transform.position[0] 2015 intercept = self.xbounds()[0] if point is None else point 2016 self.x(intercept) 2017 elif plane == "y": 2018 coords[:, 1] = self.transform.position[1] 2019 intercept = self.ybounds()[0] if point is None else point 2020 self.y(intercept) 2021 elif plane == "z": 2022 coords[:, 2] = self.transform.position[2] 2023 intercept = self.zbounds()[0] if point is None else point 2024 self.z(intercept) 2025 2026 elif isinstance(plane, vedo.shapes.Plane): 2027 normal = plane.normal / np.linalg.norm(plane.normal) 2028 pl = np.hstack((normal, -np.dot(plane.pos(), normal))).reshape(4, 1) 2029 if direction is None and point is None: 2030 # orthogonal projection 2031 pt = np.hstack((normal, [0])).reshape(4, 1) 2032 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T # python3 only 2033 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2034 2035 elif direction is None: 2036 # perspective projection 2037 pt = np.hstack((np.array(point), [1])).reshape(4, 1) 2038 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2039 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2040 2041 elif point is None: 2042 # oblique projection 2043 pt = np.hstack((np.array(direction), [0])).reshape(4, 1) 2044 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2045 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2046 2047 coords = np.concatenate([coords, np.ones((coords.shape[:-1] + (1,)))], axis=-1) 2048 # coords = coords @ proj_mat.T 2049 coords = np.matmul(coords, proj_mat.T) 2050 coords = coords[:, :3] / coords[:, 3:] 2051 2052 else: 2053 vedo.logger.error(f"unknown plane {plane}") 2054 raise RuntimeError() 2055 2056 self.alpha(0.1) 2057 self.vertices = coords 2058 return self 2059 2060 def warp(self, source, target, sigma=1.0, mode="3d") -> Self: 2061 """ 2062 "Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set 2063 of source and target landmarks. Any point on the mesh close to a source landmark will 2064 be moved to a place close to the corresponding target landmark. 2065 The points in between are interpolated smoothly using 2066 Bookstein's Thin Plate Spline algorithm. 2067 2068 Transformation object can be accessed with `mesh.transform`. 2069 2070 Arguments: 2071 sigma : (float) 2072 specify the 'stiffness' of the spline. 2073 mode : (str) 2074 set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes) 2075 2076 Examples: 2077 - [interpolate_field.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/interpolate_field.py) 2078 - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py) 2079 - [warp2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp2.py) 2080 - [warp3.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp3.py) 2081 - [warp4a.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4a.py) 2082 - [warp4b.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4b.py) 2083 - [warp6.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp6.py) 2084 2085 ![](https://vedo.embl.es/images/advanced/warp2.png) 2086 """ 2087 parents = [self] 2088 2089 try: 2090 source = source.vertices 2091 parents.append(source) 2092 except AttributeError: 2093 source = utils.make3d(source) 2094 2095 try: 2096 target = target.vertices 2097 parents.append(target) 2098 except AttributeError: 2099 target = utils.make3d(target) 2100 2101 ns = len(source) 2102 nt = len(target) 2103 if ns != nt: 2104 vedo.logger.error(f"#source {ns} != {nt} #target points") 2105 raise RuntimeError() 2106 2107 NLT = NonLinearTransform() 2108 NLT.source_points = source 2109 NLT.target_points = target 2110 self.apply_transform(NLT) 2111 2112 self.pipeline = utils.OperationNode("warp", parents=parents) 2113 return self 2114 2115 def cut_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0), invert=False) -> Self: 2116 """ 2117 Cut the mesh with the plane defined by a point and a normal. 2118 2119 Arguments: 2120 origin : (array) 2121 the cutting plane goes through this point 2122 normal : (array) 2123 normal of the cutting plane 2124 2125 Example: 2126 ```python 2127 from vedo import Cube 2128 cube = Cube().cut_with_plane(normal=(1,1,1)) 2129 cube.back_color('pink').show().close() 2130 ``` 2131 ![](https://vedo.embl.es/images/feats/cut_with_plane_cube.png) 2132 2133 Examples: 2134 - [trail.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/trail.py) 2135 2136 ![](https://vedo.embl.es/images/simulations/trail.gif) 2137 2138 Check out also: 2139 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`. 2140 """ 2141 s = str(normal) 2142 if "x" in s: 2143 normal = (1, 0, 0) 2144 if "-" in s: 2145 normal = -np.array(normal) 2146 elif "y" in s: 2147 normal = (0, 1, 0) 2148 if "-" in s: 2149 normal = -np.array(normal) 2150 elif "z" in s: 2151 normal = (0, 0, 1) 2152 if "-" in s: 2153 normal = -np.array(normal) 2154 plane = vtki.vtkPlane() 2155 plane.SetOrigin(origin) 2156 plane.SetNormal(normal) 2157 2158 clipper = vtki.new("ClipPolyData") 2159 clipper.SetInputData(self.dataset) 2160 clipper.SetClipFunction(plane) 2161 clipper.GenerateClippedOutputOff() 2162 clipper.GenerateClipScalarsOff() 2163 clipper.SetInsideOut(invert) 2164 clipper.SetValue(0) 2165 clipper.Update() 2166 2167 self._update(clipper.GetOutput()) 2168 2169 self.pipeline = utils.OperationNode("cut_with_plane", parents=[self]) 2170 return self 2171 2172 def cut_with_planes(self, origins, normals, invert=False) -> Self: 2173 """ 2174 Cut the mesh with a convex set of planes defined by points and normals. 2175 2176 Arguments: 2177 origins : (array) 2178 each cutting plane goes through this point 2179 normals : (array) 2180 normal of each of the cutting planes 2181 invert : (bool) 2182 if True, cut outside instead of inside 2183 2184 Check out also: 2185 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()` 2186 """ 2187 2188 vpoints = vtki.vtkPoints() 2189 for p in utils.make3d(origins): 2190 vpoints.InsertNextPoint(p) 2191 normals = utils.make3d(normals) 2192 2193 planes = vtki.vtkPlanes() 2194 planes.SetPoints(vpoints) 2195 planes.SetNormals(utils.numpy2vtk(normals, dtype=float)) 2196 2197 clipper = vtki.new("ClipPolyData") 2198 clipper.SetInputData(self.dataset) 2199 clipper.SetInsideOut(invert) 2200 clipper.SetClipFunction(planes) 2201 clipper.GenerateClippedOutputOff() 2202 clipper.GenerateClipScalarsOff() 2203 clipper.SetValue(0) 2204 clipper.Update() 2205 2206 self._update(clipper.GetOutput()) 2207 2208 self.pipeline = utils.OperationNode("cut_with_planes", parents=[self]) 2209 return self 2210 2211 def cut_with_box(self, bounds, invert=False) -> Self: 2212 """ 2213 Cut the current mesh with a box or a set of boxes. 2214 This is much faster than `cut_with_mesh()`. 2215 2216 Input `bounds` can be either: 2217 - a Mesh or Points object 2218 - a list of 6 number representing a bounding box `[xmin,xmax, ymin,ymax, zmin,zmax]` 2219 - a list of bounding boxes like the above: `[[xmin1,...], [xmin2,...], ...]` 2220 2221 Example: 2222 ```python 2223 from vedo import Sphere, Cube, show 2224 mesh = Sphere(r=1, res=50) 2225 box = Cube(side=1.5).wireframe() 2226 mesh.cut_with_box(box) 2227 show(mesh, box, axes=1).close() 2228 ``` 2229 ![](https://vedo.embl.es/images/feats/cut_with_box_cube.png) 2230 2231 Check out also: 2232 `cut_with_line()`, `cut_with_plane()`, `cut_with_cylinder()` 2233 """ 2234 if isinstance(bounds, Points): 2235 bounds = bounds.bounds() 2236 2237 box = vtki.new("Box") 2238 if utils.is_sequence(bounds[0]): 2239 for bs in bounds: 2240 box.AddBounds(bs) 2241 else: 2242 box.SetBounds(bounds) 2243 2244 clipper = vtki.new("ClipPolyData") 2245 clipper.SetInputData(self.dataset) 2246 clipper.SetClipFunction(box) 2247 clipper.SetInsideOut(not invert) 2248 clipper.GenerateClippedOutputOff() 2249 clipper.GenerateClipScalarsOff() 2250 clipper.SetValue(0) 2251 clipper.Update() 2252 self._update(clipper.GetOutput()) 2253 2254 self.pipeline = utils.OperationNode("cut_with_box", parents=[self]) 2255 return self 2256 2257 def cut_with_line(self, points, invert=False, closed=True) -> Self: 2258 """ 2259 Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter. 2260 The polyline is defined by a set of points (z-coordinates are ignored). 2261 This is much faster than `cut_with_mesh()`. 2262 2263 Check out also: 2264 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2265 """ 2266 pplane = vtki.new("PolyPlane") 2267 if isinstance(points, Points): 2268 points = points.vertices.tolist() 2269 2270 if closed: 2271 if isinstance(points, np.ndarray): 2272 points = points.tolist() 2273 points.append(points[0]) 2274 2275 vpoints = vtki.vtkPoints() 2276 for p in points: 2277 if len(p) == 2: 2278 p = [p[0], p[1], 0.0] 2279 vpoints.InsertNextPoint(p) 2280 2281 n = len(points) 2282 polyline = vtki.new("PolyLine") 2283 polyline.Initialize(n, vpoints) 2284 polyline.GetPointIds().SetNumberOfIds(n) 2285 for i in range(n): 2286 polyline.GetPointIds().SetId(i, i) 2287 pplane.SetPolyLine(polyline) 2288 2289 clipper = vtki.new("ClipPolyData") 2290 clipper.SetInputData(self.dataset) 2291 clipper.SetClipFunction(pplane) 2292 clipper.SetInsideOut(invert) 2293 clipper.GenerateClippedOutputOff() 2294 clipper.GenerateClipScalarsOff() 2295 clipper.SetValue(0) 2296 clipper.Update() 2297 self._update(clipper.GetOutput()) 2298 2299 self.pipeline = utils.OperationNode("cut_with_line", parents=[self]) 2300 return self 2301 2302 def cut_with_cookiecutter(self, lines) -> Self: 2303 """ 2304 Cut the current mesh with a single line or a set of lines. 2305 2306 Input `lines` can be either: 2307 - a `Mesh` or `Points` object 2308 - a list of 3D points: `[(x1,y1,z1), (x2,y2,z2), ...]` 2309 - a list of 2D points: `[(x1,y1), (x2,y2), ...]` 2310 2311 Example: 2312 ```python 2313 from vedo import * 2314 grid = Mesh(dataurl + "dolfin_fine.vtk") 2315 grid.compute_quality().cmap("Greens") 2316 pols = merge( 2317 Polygon(nsides=10, r=0.3).pos(0.7, 0.3), 2318 Polygon(nsides=10, r=0.2).pos(0.3, 0.7), 2319 ) 2320 lines = pols.boundaries() 2321 cgrid = grid.clone().cut_with_cookiecutter(lines) 2322 grid.alpha(0.1).wireframe() 2323 show(grid, cgrid, lines, axes=8, bg='blackboard').close() 2324 ``` 2325 ![](https://vedo.embl.es/images/feats/cookiecutter.png) 2326 2327 Check out also: 2328 `cut_with_line()` and `cut_with_point_loop()` 2329 2330 Note: 2331 In case of a warning message like: 2332 "Mesh and trim loop point data attributes are different" 2333 consider interpolating the mesh point data to the loop points, 2334 Eg. (in the above example): 2335 ```python 2336 lines = pols.boundaries().interpolate_data_from(grid, n=2) 2337 ``` 2338 2339 Note: 2340 trying to invert the selection by reversing the loop order 2341 will have no effect in this method, hence it does not have 2342 the `invert` option. 2343 """ 2344 if utils.is_sequence(lines): 2345 lines = utils.make3d(lines) 2346 iline = list(range(len(lines))) + [0] 2347 poly = utils.buildPolyData(lines, lines=[iline]) 2348 else: 2349 poly = lines.dataset 2350 2351 # if invert: # not working 2352 # rev = vtki.new("ReverseSense") 2353 # rev.ReverseCellsOn() 2354 # rev.SetInputData(poly) 2355 # rev.Update() 2356 # poly = rev.GetOutput() 2357 2358 # Build loops from the polyline 2359 build_loops = vtki.new("ContourLoopExtraction") 2360 build_loops.SetGlobalWarningDisplay(0) 2361 build_loops.SetInputData(poly) 2362 build_loops.Update() 2363 boundary_poly = build_loops.GetOutput() 2364 2365 ccut = vtki.new("CookieCutter") 2366 ccut.SetInputData(self.dataset) 2367 ccut.SetLoopsData(boundary_poly) 2368 ccut.SetPointInterpolationToMeshEdges() 2369 # ccut.SetPointInterpolationToLoopEdges() 2370 ccut.PassCellDataOn() 2371 ccut.PassPointDataOn() 2372 ccut.Update() 2373 self._update(ccut.GetOutput()) 2374 2375 self.pipeline = utils.OperationNode("cut_with_cookiecutter", parents=[self]) 2376 return self 2377 2378 def cut_with_cylinder(self, center=(0, 0, 0), axis=(0, 0, 1), r=1, invert=False) -> Self: 2379 """ 2380 Cut the current mesh with an infinite cylinder. 2381 This is much faster than `cut_with_mesh()`. 2382 2383 Arguments: 2384 center : (array) 2385 the center of the cylinder 2386 normal : (array) 2387 direction of the cylinder axis 2388 r : (float) 2389 radius of the cylinder 2390 2391 Example: 2392 ```python 2393 from vedo import Disc, show 2394 disc = Disc(r1=1, r2=1.2) 2395 mesh = disc.extrude(3, res=50).linewidth(1) 2396 mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True) 2397 show(mesh, axes=1).close() 2398 ``` 2399 ![](https://vedo.embl.es/images/feats/cut_with_cylinder.png) 2400 2401 Examples: 2402 - [optics_main1.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/optics_main1.py) 2403 2404 Check out also: 2405 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2406 """ 2407 s = str(axis) 2408 if "x" in s: 2409 axis = (1, 0, 0) 2410 elif "y" in s: 2411 axis = (0, 1, 0) 2412 elif "z" in s: 2413 axis = (0, 0, 1) 2414 cyl = vtki.new("Cylinder") 2415 cyl.SetCenter(center) 2416 cyl.SetAxis(axis[0], axis[1], axis[2]) 2417 cyl.SetRadius(r) 2418 2419 clipper = vtki.new("ClipPolyData") 2420 clipper.SetInputData(self.dataset) 2421 clipper.SetClipFunction(cyl) 2422 clipper.SetInsideOut(not invert) 2423 clipper.GenerateClippedOutputOff() 2424 clipper.GenerateClipScalarsOff() 2425 clipper.SetValue(0) 2426 clipper.Update() 2427 self._update(clipper.GetOutput()) 2428 2429 self.pipeline = utils.OperationNode("cut_with_cylinder", parents=[self]) 2430 return self 2431 2432 def cut_with_sphere(self, center=(0, 0, 0), r=1.0, invert=False) -> Self: 2433 """ 2434 Cut the current mesh with an sphere. 2435 This is much faster than `cut_with_mesh()`. 2436 2437 Arguments: 2438 center : (array) 2439 the center of the sphere 2440 r : (float) 2441 radius of the sphere 2442 2443 Example: 2444 ```python 2445 from vedo import Disc, show 2446 disc = Disc(r1=1, r2=1.2) 2447 mesh = disc.extrude(3, res=50).linewidth(1) 2448 mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True) 2449 show(mesh, axes=1).close() 2450 ``` 2451 ![](https://vedo.embl.es/images/feats/cut_with_sphere.png) 2452 2453 Check out also: 2454 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2455 """ 2456 sph = vtki.new("Sphere") 2457 sph.SetCenter(center) 2458 sph.SetRadius(r) 2459 2460 clipper = vtki.new("ClipPolyData") 2461 clipper.SetInputData(self.dataset) 2462 clipper.SetClipFunction(sph) 2463 clipper.SetInsideOut(not invert) 2464 clipper.GenerateClippedOutputOff() 2465 clipper.GenerateClipScalarsOff() 2466 clipper.SetValue(0) 2467 clipper.Update() 2468 self._update(clipper.GetOutput()) 2469 self.pipeline = utils.OperationNode("cut_with_sphere", parents=[self]) 2470 return self 2471 2472 def cut_with_mesh(self, mesh, invert=False, keep=False) -> Union[Self, "vedo.Assembly"]: 2473 """ 2474 Cut an `Mesh` mesh with another `Mesh`. 2475 2476 Use `invert` to invert the selection. 2477 2478 Use `keep` to keep the cutoff part, in this case an `Assembly` is returned: 2479 the "cut" object and the "discarded" part of the original object. 2480 You can access both via `assembly.unpack()` method. 2481 2482 Example: 2483 ```python 2484 from vedo import * 2485 arr = np.random.randn(100000, 3)/2 2486 pts = Points(arr).c('red3').pos(5,0,0) 2487 cube = Cube().pos(4,0.5,0) 2488 assem = pts.cut_with_mesh(cube, keep=True) 2489 show(assem.unpack(), axes=1).close() 2490 ``` 2491 ![](https://vedo.embl.es/images/feats/cut_with_mesh.png) 2492 2493 Check out also: 2494 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2495 """ 2496 polymesh = mesh.dataset 2497 poly = self.dataset 2498 2499 # Create an array to hold distance information 2500 signed_distances = vtki.vtkFloatArray() 2501 signed_distances.SetNumberOfComponents(1) 2502 signed_distances.SetName("SignedDistances") 2503 2504 # implicit function that will be used to slice the mesh 2505 ippd = vtki.new("ImplicitPolyDataDistance") 2506 ippd.SetInput(polymesh) 2507 2508 # Evaluate the signed distance function at all of the grid points 2509 for pointId in range(poly.GetNumberOfPoints()): 2510 p = poly.GetPoint(pointId) 2511 signed_distance = ippd.EvaluateFunction(p) 2512 signed_distances.InsertNextValue(signed_distance) 2513 2514 currentscals = poly.GetPointData().GetScalars() 2515 if currentscals: 2516 currentscals = currentscals.GetName() 2517 2518 poly.GetPointData().AddArray(signed_distances) 2519 poly.GetPointData().SetActiveScalars("SignedDistances") 2520 2521 clipper = vtki.new("ClipPolyData") 2522 clipper.SetInputData(poly) 2523 clipper.SetInsideOut(not invert) 2524 clipper.SetGenerateClippedOutput(keep) 2525 clipper.SetValue(0.0) 2526 clipper.Update() 2527 cpoly = clipper.GetOutput() 2528 2529 if keep: 2530 kpoly = clipper.GetOutput(1) 2531 2532 vis = False 2533 if currentscals: 2534 cpoly.GetPointData().SetActiveScalars(currentscals) 2535 vis = self.mapper.GetScalarVisibility() 2536 2537 self._update(cpoly) 2538 2539 self.pointdata.remove("SignedDistances") 2540 self.mapper.SetScalarVisibility(vis) 2541 if keep: 2542 if isinstance(self, vedo.Mesh): 2543 cutoff = vedo.Mesh(kpoly) 2544 else: 2545 cutoff = vedo.Points(kpoly) 2546 # cutoff = self.__class__(kpoly) # this does not work properly 2547 cutoff.properties = vtki.vtkProperty() 2548 cutoff.properties.DeepCopy(self.properties) 2549 cutoff.actor.SetProperty(cutoff.properties) 2550 cutoff.c("k5").alpha(0.2) 2551 return vedo.Assembly([self, cutoff]) 2552 2553 self.pipeline = utils.OperationNode("cut_with_mesh", parents=[self, mesh]) 2554 return self 2555 2556 def cut_with_point_loop( 2557 self, points, invert=False, on="points", include_boundary=False 2558 ) -> Self: 2559 """ 2560 Cut an `Mesh` object with a set of points forming a closed loop. 2561 2562 Arguments: 2563 invert : (bool) 2564 invert selection (inside-out) 2565 on : (str) 2566 if 'cells' will extract the whole cells lying inside (or outside) the point loop 2567 include_boundary : (bool) 2568 include cells lying exactly on the boundary line. Only relevant on 'cells' mode 2569 2570 Examples: 2571 - [cut_with_points1.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points1.py) 2572 2573 ![](https://vedo.embl.es/images/advanced/cutWithPoints1.png) 2574 2575 - [cut_with_points2.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points2.py) 2576 2577 ![](https://vedo.embl.es/images/advanced/cutWithPoints2.png) 2578 """ 2579 if isinstance(points, Points): 2580 parents = [points] 2581 vpts = points.dataset.GetPoints() 2582 points = points.vertices 2583 else: 2584 parents = [self] 2585 vpts = vtki.vtkPoints() 2586 points = utils.make3d(points) 2587 for p in points: 2588 vpts.InsertNextPoint(p) 2589 2590 if "cell" in on: 2591 ippd = vtki.new("ImplicitSelectionLoop") 2592 ippd.SetLoop(vpts) 2593 ippd.AutomaticNormalGenerationOn() 2594 clipper = vtki.new("ExtractPolyDataGeometry") 2595 clipper.SetInputData(self.dataset) 2596 clipper.SetImplicitFunction(ippd) 2597 clipper.SetExtractInside(not invert) 2598 clipper.SetExtractBoundaryCells(include_boundary) 2599 else: 2600 spol = vtki.new("SelectPolyData") 2601 spol.SetLoop(vpts) 2602 spol.GenerateSelectionScalarsOn() 2603 spol.GenerateUnselectedOutputOff() 2604 spol.SetInputData(self.dataset) 2605 spol.Update() 2606 clipper = vtki.new("ClipPolyData") 2607 clipper.SetInputData(spol.GetOutput()) 2608 clipper.SetInsideOut(not invert) 2609 clipper.SetValue(0.0) 2610 clipper.Update() 2611 self._update(clipper.GetOutput()) 2612 2613 self.pipeline = utils.OperationNode("cut_with_pointloop", parents=parents) 2614 return self 2615 2616 def cut_with_scalar(self, value: float, name="", invert=False) -> Self: 2617 """ 2618 Cut a mesh or point cloud with some input scalar point-data. 2619 2620 Arguments: 2621 value : (float) 2622 cutting value 2623 name : (str) 2624 array name of the scalars to be used 2625 invert : (bool) 2626 flip selection 2627 2628 Example: 2629 ```python 2630 from vedo import * 2631 s = Sphere().lw(1) 2632 pts = s.vertices 2633 scalars = np.sin(3*pts[:,2]) + pts[:,0] 2634 s.pointdata["somevalues"] = scalars 2635 s.cut_with_scalar(0.3) 2636 s.cmap("Spectral", "somevalues").add_scalarbar() 2637 s.show(axes=1).close() 2638 ``` 2639 ![](https://vedo.embl.es/images/feats/cut_with_scalars.png) 2640 """ 2641 if name: 2642 self.pointdata.select(name) 2643 clipper = vtki.new("ClipPolyData") 2644 clipper.SetInputData(self.dataset) 2645 clipper.SetValue(value) 2646 clipper.GenerateClippedOutputOff() 2647 clipper.SetInsideOut(not invert) 2648 clipper.Update() 2649 self._update(clipper.GetOutput()) 2650 self.pipeline = utils.OperationNode("cut_with_scalar", parents=[self]) 2651 return self 2652 2653 def crop(self, 2654 top=None, bottom=None, right=None, left=None, front=None, back=None, 2655 bounds=()) -> Self: 2656 """ 2657 Crop an `Mesh` object. 2658 2659 Arguments: 2660 top : (float) 2661 fraction to crop from the top plane (positive z) 2662 bottom : (float) 2663 fraction to crop from the bottom plane (negative z) 2664 front : (float) 2665 fraction to crop from the front plane (positive y) 2666 back : (float) 2667 fraction to crop from the back plane (negative y) 2668 right : (float) 2669 fraction to crop from the right plane (positive x) 2670 left : (float) 2671 fraction to crop from the left plane (negative x) 2672 bounds : (list) 2673 bounding box of the crop region as `[x0,x1, y0,y1, z0,z1]` 2674 2675 Example: 2676 ```python 2677 from vedo import Sphere 2678 Sphere().crop(right=0.3, left=0.1).show() 2679 ``` 2680 ![](https://user-images.githubusercontent.com/32848391/57081955-0ef1e800-6cf6-11e9-99de-b45220939bc9.png) 2681 """ 2682 if not len(bounds): 2683 pos = np.array(self.pos()) 2684 x0, x1, y0, y1, z0, z1 = self.bounds() 2685 x0, y0, z0 = [x0, y0, z0] - pos 2686 x1, y1, z1 = [x1, y1, z1] - pos 2687 2688 dx, dy, dz = x1 - x0, y1 - y0, z1 - z0 2689 if top: 2690 z1 = z1 - top * dz 2691 if bottom: 2692 z0 = z0 + bottom * dz 2693 if front: 2694 y1 = y1 - front * dy 2695 if back: 2696 y0 = y0 + back * dy 2697 if right: 2698 x1 = x1 - right * dx 2699 if left: 2700 x0 = x0 + left * dx 2701 bounds = (x0, x1, y0, y1, z0, z1) 2702 2703 cu = vtki.new("Box") 2704 cu.SetBounds(bounds) 2705 2706 clipper = vtki.new("ClipPolyData") 2707 clipper.SetInputData(self.dataset) 2708 clipper.SetClipFunction(cu) 2709 clipper.InsideOutOn() 2710 clipper.GenerateClippedOutputOff() 2711 clipper.GenerateClipScalarsOff() 2712 clipper.SetValue(0) 2713 clipper.Update() 2714 self._update(clipper.GetOutput()) 2715 2716 self.pipeline = utils.OperationNode( 2717 "crop", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 2718 ) 2719 return self 2720 2721 def generate_surface_halo( 2722 self, 2723 distance=0.05, 2724 res=(50, 50, 50), 2725 bounds=(), 2726 maxdist=None, 2727 ) -> "vedo.Mesh": 2728 """ 2729 Generate the surface halo which sits at the specified distance from the input one. 2730 2731 Arguments: 2732 distance : (float) 2733 distance from the input surface 2734 res : (int) 2735 resolution of the surface 2736 bounds : (list) 2737 bounding box of the surface 2738 maxdist : (float) 2739 maximum distance to generate the surface 2740 """ 2741 if not bounds: 2742 bounds = self.bounds() 2743 2744 if not maxdist: 2745 maxdist = self.diagonal_size() / 2 2746 2747 imp = vtki.new("ImplicitModeller") 2748 imp.SetInputData(self.dataset) 2749 imp.SetSampleDimensions(res) 2750 if maxdist: 2751 imp.SetMaximumDistance(maxdist) 2752 if len(bounds) == 6: 2753 imp.SetModelBounds(bounds) 2754 contour = vtki.new("ContourFilter") 2755 contour.SetInputConnection(imp.GetOutputPort()) 2756 contour.SetValue(0, distance) 2757 contour.Update() 2758 out = vedo.Mesh(contour.GetOutput()) 2759 out.c("lightblue").alpha(0.25).lighting("off") 2760 out.pipeline = utils.OperationNode("generate_surface_halo", parents=[self]) 2761 return out 2762 2763 def generate_mesh( 2764 self, 2765 line_resolution=None, 2766 mesh_resolution=None, 2767 smooth=0.0, 2768 jitter=0.001, 2769 grid=None, 2770 quads=False, 2771 invert=False, 2772 ) -> Self: 2773 """ 2774 Generate a polygonal Mesh from a closed contour line. 2775 If line is not closed it will be closed with a straight segment. 2776 2777 Check also `generate_delaunay2d()`. 2778 2779 Arguments: 2780 line_resolution : (int) 2781 resolution of the contour line. The default is None, in this case 2782 the contour is not resampled. 2783 mesh_resolution : (int) 2784 resolution of the internal triangles not touching the boundary. 2785 smooth : (float) 2786 smoothing of the contour before meshing. 2787 jitter : (float) 2788 add a small noise to the internal points. 2789 grid : (Grid) 2790 manually pass a Grid object. The default is True. 2791 quads : (bool) 2792 generate a mesh of quads instead of triangles. 2793 invert : (bool) 2794 flip the line orientation. The default is False. 2795 2796 Examples: 2797 - [line2mesh_tri.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_tri.py) 2798 2799 ![](https://vedo.embl.es/images/advanced/line2mesh_tri.jpg) 2800 2801 - [line2mesh_quads.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_quads.py) 2802 2803 ![](https://vedo.embl.es/images/advanced/line2mesh_quads.png) 2804 """ 2805 if line_resolution is None: 2806 contour = vedo.shapes.Line(self.vertices) 2807 else: 2808 contour = vedo.shapes.Spline(self.vertices, smooth=smooth, res=line_resolution) 2809 contour.clean() 2810 2811 length = contour.length() 2812 density = length / contour.npoints 2813 # print(f"tomesh():\n\tline length = {length}") 2814 # print(f"\tdensity = {density} length/pt_separation") 2815 2816 x0, x1 = contour.xbounds() 2817 y0, y1 = contour.ybounds() 2818 2819 if grid is None: 2820 if mesh_resolution is None: 2821 resx = int((x1 - x0) / density + 0.5) 2822 resy = int((y1 - y0) / density + 0.5) 2823 # print(f"tmesh_resolution = {[resx, resy]}") 2824 else: 2825 if utils.is_sequence(mesh_resolution): 2826 resx, resy = mesh_resolution 2827 else: 2828 resx, resy = mesh_resolution, mesh_resolution 2829 grid = vedo.shapes.Grid( 2830 [(x0 + x1) / 2, (y0 + y1) / 2, 0], 2831 s=((x1 - x0) * 1.025, (y1 - y0) * 1.025), 2832 res=(resx, resy), 2833 ) 2834 else: 2835 grid = grid.clone() 2836 2837 cpts = contour.vertices 2838 2839 # make sure it's closed 2840 p0, p1 = cpts[0], cpts[-1] 2841 nj = max(2, int(utils.mag(p1 - p0) / density + 0.5)) 2842 joinline = vedo.shapes.Line(p1, p0, res=nj) 2843 contour = vedo.merge(contour, joinline).subsample(0.0001) 2844 2845 ####################################### quads 2846 if quads: 2847 cmesh = grid.clone().cut_with_point_loop(contour, on="cells", invert=invert) 2848 cmesh.wireframe(False).lw(0.5) 2849 cmesh.pipeline = utils.OperationNode( 2850 "generate_mesh", 2851 parents=[self, contour], 2852 comment=f"#quads {cmesh.dataset.GetNumberOfCells()}", 2853 ) 2854 return cmesh 2855 ############################################# 2856 2857 grid_tmp = grid.vertices.copy() 2858 2859 if jitter: 2860 np.random.seed(0) 2861 sigma = 1.0 / np.sqrt(grid.npoints) * grid.diagonal_size() * jitter 2862 # print(f"\tsigma jittering = {sigma}") 2863 grid_tmp += np.random.rand(grid.npoints, 3) * sigma 2864 grid_tmp[:, 2] = 0.0 2865 2866 todel = [] 2867 density /= np.sqrt(3) 2868 vgrid_tmp = Points(grid_tmp) 2869 2870 for p in contour.vertices: 2871 out = vgrid_tmp.closest_point(p, radius=density, return_point_id=True) 2872 todel += out.tolist() 2873 2874 grid_tmp = grid_tmp.tolist() 2875 for index in sorted(list(set(todel)), reverse=True): 2876 del grid_tmp[index] 2877 2878 points = contour.vertices.tolist() + grid_tmp 2879 if invert: 2880 boundary = list(reversed(range(contour.npoints))) 2881 else: 2882 boundary = list(range(contour.npoints)) 2883 2884 dln = Points(points).generate_delaunay2d(mode="xy", boundaries=[boundary]) 2885 dln.compute_normals(points=False) # fixes reversd faces 2886 dln.lw(1) 2887 2888 dln.pipeline = utils.OperationNode( 2889 "generate_mesh", 2890 parents=[self, contour], 2891 comment=f"#cells {dln.dataset.GetNumberOfCells()}", 2892 ) 2893 return dln 2894 2895 def reconstruct_surface( 2896 self, 2897 dims=(100, 100, 100), 2898 radius=None, 2899 sample_size=None, 2900 hole_filling=True, 2901 bounds=(), 2902 padding=0.05, 2903 ) -> "vedo.Mesh": 2904 """ 2905 Surface reconstruction from a scattered cloud of points. 2906 2907 Arguments: 2908 dims : (int) 2909 number of voxels in x, y and z to control precision. 2910 radius : (float) 2911 radius of influence of each point. 2912 Smaller values generally improve performance markedly. 2913 Note that after the signed distance function is computed, 2914 any voxel taking on the value >= radius 2915 is presumed to be "unseen" or uninitialized. 2916 sample_size : (int) 2917 if normals are not present 2918 they will be calculated using this sample size per point. 2919 hole_filling : (bool) 2920 enables hole filling, this generates 2921 separating surfaces between the empty and unseen portions of the volume. 2922 bounds : (list) 2923 region in space in which to perform the sampling 2924 in format (xmin,xmax, ymin,ymax, zim, zmax) 2925 padding : (float) 2926 increase by this fraction the bounding box 2927 2928 Examples: 2929 - [recosurface.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/recosurface.py) 2930 2931 ![](https://vedo.embl.es/images/advanced/recosurface.png) 2932 """ 2933 if not utils.is_sequence(dims): 2934 dims = (dims, dims, dims) 2935 2936 sdf = vtki.new("SignedDistance") 2937 2938 if len(bounds) == 6: 2939 sdf.SetBounds(bounds) 2940 else: 2941 x0, x1, y0, y1, z0, z1 = self.bounds() 2942 sdf.SetBounds( 2943 x0 - (x1 - x0) * padding, 2944 x1 + (x1 - x0) * padding, 2945 y0 - (y1 - y0) * padding, 2946 y1 + (y1 - y0) * padding, 2947 z0 - (z1 - z0) * padding, 2948 z1 + (z1 - z0) * padding, 2949 ) 2950 2951 bb = sdf.GetBounds() 2952 if bb[0]==bb[1]: 2953 vedo.logger.warning("reconstruct_surface(): zero x-range") 2954 if bb[2]==bb[3]: 2955 vedo.logger.warning("reconstruct_surface(): zero y-range") 2956 if bb[4]==bb[5]: 2957 vedo.logger.warning("reconstruct_surface(): zero z-range") 2958 2959 pd = self.dataset 2960 2961 if pd.GetPointData().GetNormals(): 2962 sdf.SetInputData(pd) 2963 else: 2964 normals = vtki.new("PCANormalEstimation") 2965 normals.SetInputData(pd) 2966 if not sample_size: 2967 sample_size = int(pd.GetNumberOfPoints() / 50) 2968 normals.SetSampleSize(sample_size) 2969 normals.SetNormalOrientationToGraphTraversal() 2970 sdf.SetInputConnection(normals.GetOutputPort()) 2971 # print("Recalculating normals with sample size =", sample_size) 2972 2973 if radius is None: 2974 radius = self.diagonal_size() / (sum(dims) / 3) * 5 2975 # print("Calculating mesh from points with radius =", radius) 2976 2977 sdf.SetRadius(radius) 2978 sdf.SetDimensions(dims) 2979 sdf.Update() 2980 2981 surface = vtki.new("ExtractSurface") 2982 surface.SetRadius(radius * 0.99) 2983 surface.SetHoleFilling(hole_filling) 2984 surface.ComputeNormalsOff() 2985 surface.ComputeGradientsOff() 2986 surface.SetInputConnection(sdf.GetOutputPort()) 2987 surface.Update() 2988 m = vedo.mesh.Mesh(surface.GetOutput(), c=self.color()) 2989 2990 m.pipeline = utils.OperationNode( 2991 "reconstruct_surface", 2992 parents=[self], 2993 comment=f"#pts {m.dataset.GetNumberOfPoints()}", 2994 ) 2995 return m 2996 2997 def compute_clustering(self, radius: float) -> Self: 2998 """ 2999 Cluster points in space. The `radius` is the radius of local search. 3000 3001 An array named "ClusterId" is added to `pointdata`. 3002 3003 Examples: 3004 - [clustering.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/clustering.py) 3005 3006 ![](https://vedo.embl.es/images/basic/clustering.png) 3007 """ 3008 cluster = vtki.new("EuclideanClusterExtraction") 3009 cluster.SetInputData(self.dataset) 3010 cluster.SetExtractionModeToAllClusters() 3011 cluster.SetRadius(radius) 3012 cluster.ColorClustersOn() 3013 cluster.Update() 3014 idsarr = cluster.GetOutput().GetPointData().GetArray("ClusterId") 3015 self.dataset.GetPointData().AddArray(idsarr) 3016 self.pipeline = utils.OperationNode( 3017 "compute_clustering", parents=[self], comment=f"radius = {radius}" 3018 ) 3019 return self 3020 3021 def compute_connections(self, radius, mode=0, regions=(), vrange=(0, 1), seeds=(), angle=0.0) -> Self: 3022 """ 3023 Extracts and/or segments points from a point cloud based on geometric distance measures 3024 (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range. 3025 The default operation is to segment the points into "connected" regions where the connection 3026 is determined by an appropriate distance measure. Each region is given a region id. 3027 3028 Optionally, the filter can output the largest connected region of points; a particular region 3029 (via id specification); those regions that are seeded using a list of input point ids; 3030 or the region of points closest to a specified position. 3031 3032 The key parameter of this filter is the radius defining a sphere around each point which defines 3033 a local neighborhood: any other points in the local neighborhood are assumed connected to the point. 3034 Note that the radius is defined in absolute terms. 3035 3036 Other parameters are used to further qualify what it means to be a neighboring point. 3037 For example, scalar range and/or point normals can be used to further constrain the neighborhood. 3038 Also the extraction mode defines how the filter operates. 3039 By default, all regions are extracted but it is possible to extract particular regions; 3040 the region closest to a seed point; seeded regions; or the largest region found while processing. 3041 By default, all regions are extracted. 3042 3043 On output, all points are labeled with a region number. 3044 However note that the number of input and output points may not be the same: 3045 if not extracting all regions then the output size may be less than the input size. 3046 3047 Arguments: 3048 radius : (float) 3049 variable specifying a local sphere used to define local point neighborhood 3050 mode : (int) 3051 - 0, Extract all regions 3052 - 1, Extract point seeded regions 3053 - 2, Extract largest region 3054 - 3, Test specified regions 3055 - 4, Extract all regions with scalar connectivity 3056 - 5, Extract point seeded regions 3057 regions : (list) 3058 a list of non-negative regions id to extract 3059 vrange : (list) 3060 scalar range to use to extract points based on scalar connectivity 3061 seeds : (list) 3062 a list of non-negative point seed ids 3063 angle : (list) 3064 points are connected if the angle between their normals is 3065 within this angle threshold (expressed in degrees). 3066 """ 3067 # https://vtk.org/doc/nightly/html/classvtkConnectedPointsFilter.html 3068 cpf = vtki.new("ConnectedPointsFilter") 3069 cpf.SetInputData(self.dataset) 3070 cpf.SetRadius(radius) 3071 if mode == 0: # Extract all regions 3072 pass 3073 3074 elif mode == 1: # Extract point seeded regions 3075 cpf.SetExtractionModeToPointSeededRegions() 3076 for s in seeds: 3077 cpf.AddSeed(s) 3078 3079 elif mode == 2: # Test largest region 3080 cpf.SetExtractionModeToLargestRegion() 3081 3082 elif mode == 3: # Test specified regions 3083 cpf.SetExtractionModeToSpecifiedRegions() 3084 for r in regions: 3085 cpf.AddSpecifiedRegion(r) 3086 3087 elif mode == 4: # Extract all regions with scalar connectivity 3088 cpf.SetExtractionModeToLargestRegion() 3089 cpf.ScalarConnectivityOn() 3090 cpf.SetScalarRange(vrange[0], vrange[1]) 3091 3092 elif mode == 5: # Extract point seeded regions 3093 cpf.SetExtractionModeToLargestRegion() 3094 cpf.ScalarConnectivityOn() 3095 cpf.SetScalarRange(vrange[0], vrange[1]) 3096 cpf.AlignedNormalsOn() 3097 cpf.SetNormalAngle(angle) 3098 3099 cpf.Update() 3100 self._update(cpf.GetOutput(), reset_locators=False) 3101 return self 3102 3103 def compute_camera_distance(self) -> np.ndarray: 3104 """ 3105 Calculate the distance from points to the camera. 3106 3107 A pointdata array is created with name 'DistanceToCamera' and returned. 3108 """ 3109 if vedo.plotter_instance and vedo.plotter_instance.renderer: 3110 poly = self.dataset 3111 dc = vtki.new("DistanceToCamera") 3112 dc.SetInputData(poly) 3113 dc.SetRenderer(vedo.plotter_instance.renderer) 3114 dc.Update() 3115 self._update(dc.GetOutput(), reset_locators=False) 3116 return self.pointdata["DistanceToCamera"] 3117 return np.array([]) 3118 3119 def densify(self, target_distance=0.1, nclosest=6, radius=None, niter=1, nmax=None) -> Self: 3120 """ 3121 Return a copy of the cloud with new added points. 3122 The new points are created in such a way that all points in any local neighborhood are 3123 within a target distance of one another. 3124 3125 For each input point, the distance to all points in its neighborhood is computed. 3126 If any of its neighbors is further than the target distance, 3127 the edge connecting the point and its neighbor is bisected and 3128 a new point is inserted at the bisection point. 3129 A single pass is completed once all the input points are visited. 3130 Then the process repeats to the number of iterations. 3131 3132 Examples: 3133 - [densifycloud.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/densifycloud.py) 3134 3135 ![](https://vedo.embl.es/images/volumetric/densifycloud.png) 3136 3137 .. note:: 3138 Points will be created in an iterative fashion until all points in their 3139 local neighborhood are the target distance apart or less. 3140 Note that the process may terminate early due to the 3141 number of iterations. By default the target distance is set to 0.5. 3142 Note that the target_distance should be less than the radius 3143 or nothing will change on output. 3144 3145 .. warning:: 3146 This class can generate a lot of points very quickly. 3147 The maximum number of iterations is by default set to =1.0 for this reason. 3148 Increase the number of iterations very carefully. 3149 Also, `nmax` can be set to limit the explosion of points. 3150 It is also recommended that a N closest neighborhood is used. 3151 3152 """ 3153 src = vtki.new("ProgrammableSource") 3154 opts = self.vertices 3155 3156 def _read_points(): 3157 output = src.GetPolyDataOutput() 3158 points = vtki.vtkPoints() 3159 for p in opts: 3160 points.InsertNextPoint(p) 3161 output.SetPoints(points) 3162 3163 src.SetExecuteMethod(_read_points) 3164 3165 dens = vtki.new("DensifyPointCloudFilter") 3166 dens.SetInputConnection(src.GetOutputPort()) 3167 dens.InterpolateAttributeDataOn() 3168 dens.SetTargetDistance(target_distance) 3169 dens.SetMaximumNumberOfIterations(niter) 3170 if nmax: 3171 dens.SetMaximumNumberOfPoints(nmax) 3172 3173 if radius: 3174 dens.SetNeighborhoodTypeToRadius() 3175 dens.SetRadius(radius) 3176 elif nclosest: 3177 dens.SetNeighborhoodTypeToNClosest() 3178 dens.SetNumberOfClosestPoints(nclosest) 3179 else: 3180 vedo.logger.error("set either radius or nclosest") 3181 raise RuntimeError() 3182 dens.Update() 3183 pts = utils.vtk2numpy(dens.GetOutput().GetPoints().GetData()) 3184 cld = Points(pts, c=None).point_size(self.properties.GetPointSize()) 3185 cld.interpolate_data_from(self, n=nclosest, radius=radius) 3186 cld.name = "DensifiedCloud" 3187 3188 cld.pipeline = utils.OperationNode( 3189 "densify", 3190 parents=[self], 3191 c="#e9c46a:", 3192 comment=f"#pts {cld.dataset.GetNumberOfPoints()}", 3193 ) 3194 return cld 3195 3196 ############################################################################### 3197 ## stuff returning a Volume 3198 ############################################################################### 3199 3200 def density( 3201 self, dims=(40, 40, 40), bounds=None, radius=None, compute_gradient=False, locator=None 3202 ) -> "vedo.Volume": 3203 """ 3204 Generate a density field from a point cloud. Input can also be a set of 3D coordinates. 3205 Output is a `Volume`. 3206 3207 The local neighborhood is specified as the `radius` around each sample position (each voxel). 3208 If left to None, the radius is automatically computed as the diagonal of the bounding box 3209 and can be accessed via `vol.metadata["radius"]`. 3210 The density is expressed as the number of counts in the radius search. 3211 3212 Arguments: 3213 dims : (int, list) 3214 number of voxels in x, y and z of the output Volume. 3215 compute_gradient : (bool) 3216 Turn on/off the generation of the gradient vector, 3217 gradient magnitude scalar, and function classification scalar. 3218 By default this is off. Note that this will increase execution time 3219 and the size of the output. (The names of these point data arrays are: 3220 "Gradient", "Gradient Magnitude", and "Classification") 3221 locator : (vtkPointLocator) 3222 can be assigned from a previous call for speed (access it via `object.point_locator`). 3223 3224 Examples: 3225 - [plot_density3d.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/plot_density3d.py) 3226 3227 ![](https://vedo.embl.es/images/pyplot/plot_density3d.png) 3228 """ 3229 pdf = vtki.new("PointDensityFilter") 3230 pdf.SetInputData(self.dataset) 3231 3232 if not utils.is_sequence(dims): 3233 dims = [dims, dims, dims] 3234 3235 if bounds is None: 3236 bounds = list(self.bounds()) 3237 elif len(bounds) == 4: 3238 bounds = [*bounds, 0, 0] 3239 3240 if bounds[5] - bounds[4] == 0 or len(dims) == 2: # its 2D 3241 dims = list(dims) 3242 dims = [dims[0], dims[1], 2] 3243 diag = self.diagonal_size() 3244 bounds[5] = bounds[4] + diag / 1000 3245 pdf.SetModelBounds(bounds) 3246 3247 pdf.SetSampleDimensions(dims) 3248 3249 if locator: 3250 pdf.SetLocator(locator) 3251 3252 pdf.SetDensityEstimateToFixedRadius() 3253 if radius is None: 3254 radius = self.diagonal_size() / 20 3255 pdf.SetRadius(radius) 3256 pdf.SetComputeGradient(compute_gradient) 3257 pdf.Update() 3258 3259 vol = vedo.Volume(pdf.GetOutput()).mode(1) 3260 vol.name = "PointDensity" 3261 vol.metadata["radius"] = radius 3262 vol.locator = pdf.GetLocator() 3263 vol.pipeline = utils.OperationNode( 3264 "density", parents=[self], comment=f"dims={tuple(vol.dimensions())}" 3265 ) 3266 return vol 3267 3268 3269 def tovolume( 3270 self, 3271 kernel="shepard", 3272 radius=None, 3273 n=None, 3274 bounds=None, 3275 null_value=None, 3276 dims=(25, 25, 25), 3277 ) -> "vedo.Volume": 3278 """ 3279 Generate a `Volume` by interpolating a scalar 3280 or vector field which is only known on a scattered set of points or mesh. 3281 Available interpolation kernels are: shepard, gaussian, or linear. 3282 3283 Arguments: 3284 kernel : (str) 3285 interpolation kernel type [shepard] 3286 radius : (float) 3287 radius of the local search 3288 n : (int) 3289 number of point to use for interpolation 3290 bounds : (list) 3291 bounding box of the output Volume object 3292 dims : (list) 3293 dimensions of the output Volume object 3294 null_value : (float) 3295 value to be assigned to invalid points 3296 3297 Examples: 3298 - [interpolate_volume.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/interpolate_volume.py) 3299 3300 ![](https://vedo.embl.es/images/volumetric/59095175-1ec5a300-8918-11e9-8bc0-fd35c8981e2b.jpg) 3301 """ 3302 if radius is None and not n: 3303 vedo.logger.error("please set either radius or n") 3304 raise RuntimeError 3305 3306 poly = self.dataset 3307 3308 # Create a probe volume 3309 probe = vtki.vtkImageData() 3310 probe.SetDimensions(dims) 3311 if bounds is None: 3312 bounds = self.bounds() 3313 probe.SetOrigin(bounds[0], bounds[2], bounds[4]) 3314 probe.SetSpacing( 3315 (bounds[1] - bounds[0]) / dims[0], 3316 (bounds[3] - bounds[2]) / dims[1], 3317 (bounds[5] - bounds[4]) / dims[2], 3318 ) 3319 3320 if not self.point_locator: 3321 self.point_locator = vtki.new("PointLocator") 3322 self.point_locator.SetDataSet(poly) 3323 self.point_locator.BuildLocator() 3324 3325 if kernel == "shepard": 3326 kern = vtki.new("ShepardKernel") 3327 kern.SetPowerParameter(2) 3328 elif kernel == "gaussian": 3329 kern = vtki.new("GaussianKernel") 3330 elif kernel == "linear": 3331 kern = vtki.new("LinearKernel") 3332 else: 3333 vedo.logger.error("Error in tovolume(), available kernels are:") 3334 vedo.logger.error(" [shepard, gaussian, linear]") 3335 raise RuntimeError() 3336 3337 if radius: 3338 kern.SetRadius(radius) 3339 3340 interpolator = vtki.new("PointInterpolator") 3341 interpolator.SetInputData(probe) 3342 interpolator.SetSourceData(poly) 3343 interpolator.SetKernel(kern) 3344 interpolator.SetLocator(self.point_locator) 3345 3346 if n: 3347 kern.SetNumberOfPoints(n) 3348 kern.SetKernelFootprintToNClosest() 3349 else: 3350 kern.SetRadius(radius) 3351 3352 if null_value is not None: 3353 interpolator.SetNullValue(null_value) 3354 else: 3355 interpolator.SetNullPointsStrategyToClosestPoint() 3356 interpolator.Update() 3357 3358 vol = vedo.Volume(interpolator.GetOutput()) 3359 3360 vol.pipeline = utils.OperationNode( 3361 "signed_distance", 3362 parents=[self], 3363 comment=f"dims={tuple(vol.dimensions())}", 3364 c="#e9c46a:#0096c7", 3365 ) 3366 return vol 3367 3368 ################################################################################# 3369 def generate_segments(self, istart=0, rmax=1e30, niter=3) -> "vedo.shapes.Lines": 3370 """ 3371 Generate a line segments from a set of points. 3372 The algorithm is based on the closest point search. 3373 3374 Returns a `Line` object. 3375 This object contains the a metadata array of used vertex counts in "UsedVertexCount" 3376 and the sum of the length of the segments in "SegmentsLengthSum". 3377 3378 Arguments: 3379 istart : (int) 3380 index of the starting point 3381 rmax : (float) 3382 maximum length of a segment 3383 niter : (int) 3384 number of iterations or passes through the points 3385 3386 Examples: 3387 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 3388 """ 3389 points = self.vertices 3390 segments = [] 3391 dists = [] 3392 n = len(points) 3393 used = np.zeros(n, dtype=int) 3394 for _ in range(niter): 3395 i = istart 3396 for _ in range(n): 3397 p = points[i] 3398 ids = self.closest_point(p, n=4, return_point_id=True) 3399 j = ids[1] 3400 if used[j] > 1 or [j, i] in segments: 3401 j = ids[2] 3402 if used[j] > 1: 3403 j = ids[3] 3404 d = np.linalg.norm(p - points[j]) 3405 if used[j] > 1 or used[i] > 1 or d > rmax: 3406 i += 1 3407 if i >= n: 3408 i = 0 3409 continue 3410 used[i] += 1 3411 used[j] += 1 3412 segments.append([i, j]) 3413 dists.append(d) 3414 i = j 3415 segments = np.array(segments, dtype=int) 3416 3417 lines = vedo.shapes.Lines(points[segments], c="k", lw=3) 3418 lines.metadata["UsedVertexCount"] = used 3419 lines.metadata["SegmentsLengthSum"] = np.sum(dists) 3420 lines.pipeline = utils.OperationNode("generate_segments", parents=[self]) 3421 lines.name = "Segments" 3422 return lines 3423 3424 def generate_delaunay2d( 3425 self, 3426 mode="scipy", 3427 boundaries=(), 3428 tol=None, 3429 alpha=0.0, 3430 offset=0.0, 3431 transform=None, 3432 ) -> "vedo.mesh.Mesh": 3433 """ 3434 Create a mesh from points in the XY plane. 3435 If `mode='fit'` then the filter computes a best fitting 3436 plane and projects the points onto it. 3437 3438 Check also `generate_mesh()`. 3439 3440 Arguments: 3441 tol : (float) 3442 specify a tolerance to control discarding of closely spaced points. 3443 This tolerance is specified as a fraction of the diagonal length of the bounding box of the points. 3444 alpha : (float) 3445 for a non-zero alpha value, only edges or triangles contained 3446 within a sphere centered at mesh vertices will be output. 3447 Otherwise, only triangles will be output. 3448 offset : (float) 3449 multiplier to control the size of the initial, bounding Delaunay triangulation. 3450 transform: (LinearTransform, NonLinearTransform) 3451 a transformation which is applied to points to generate a 2D problem. 3452 This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane. 3453 The points are transformed and triangulated. 3454 The topology of triangulated points is used as the output topology. 3455 3456 Examples: 3457 - [delaunay2d.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delaunay2d.py) 3458 3459 ![](https://vedo.embl.es/images/basic/delaunay2d.png) 3460 """ 3461 plist = self.vertices.copy() 3462 3463 ######################################################### 3464 if mode == "scipy": 3465 from scipy.spatial import Delaunay as scipy_delaunay 3466 3467 tri = scipy_delaunay(plist[:, 0:2]) 3468 return vedo.mesh.Mesh([plist, tri.simplices]) 3469 ########################################################## 3470 3471 pd = vtki.vtkPolyData() 3472 vpts = vtki.vtkPoints() 3473 vpts.SetData(utils.numpy2vtk(plist, dtype=np.float32)) 3474 pd.SetPoints(vpts) 3475 3476 delny = vtki.new("Delaunay2D") 3477 delny.SetInputData(pd) 3478 if tol: 3479 delny.SetTolerance(tol) 3480 delny.SetAlpha(alpha) 3481 delny.SetOffset(offset) 3482 3483 if transform: 3484 delny.SetTransform(transform.T) 3485 elif mode == "fit": 3486 delny.SetProjectionPlaneMode(vtki.get_class("VTK_BEST_FITTING_PLANE")) 3487 elif mode == "xy" and boundaries: 3488 boundary = vtki.vtkPolyData() 3489 boundary.SetPoints(vpts) 3490 cell_array = vtki.vtkCellArray() 3491 for b in boundaries: 3492 cpolygon = vtki.vtkPolygon() 3493 for idd in b: 3494 cpolygon.GetPointIds().InsertNextId(idd) 3495 cell_array.InsertNextCell(cpolygon) 3496 boundary.SetPolys(cell_array) 3497 delny.SetSourceData(boundary) 3498 3499 delny.Update() 3500 3501 msh = vedo.mesh.Mesh(delny.GetOutput()) 3502 msh.name = "Delaunay2D" 3503 msh.clean().lighting("off") 3504 msh.pipeline = utils.OperationNode( 3505 "delaunay2d", 3506 parents=[self], 3507 comment=f"#cells {msh.dataset.GetNumberOfCells()}", 3508 ) 3509 return msh 3510 3511 def generate_voronoi(self, padding=0.0, fit=False, method="vtk") -> "vedo.Mesh": 3512 """ 3513 Generate the 2D Voronoi convex tiling of the input points (z is ignored). 3514 The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon. 3515 3516 A cell array named "VoronoiID" is added to the output Mesh. 3517 3518 The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest 3519 to one of the input points. Voronoi tessellations are important in computational geometry 3520 (and many other fields), and are the dual of Delaunay triangulations. 3521 3522 Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored 3523 (although carried through to the output). 3524 If you desire to triangulate in a different plane, you can use fit=True. 3525 3526 A brief summary is as follows. Each (generating) input point is associated with 3527 an initial Voronoi tile, which is simply the bounding box of the point set. 3528 A locator is then used to identify nearby points: each neighbor in turn generates a 3529 clipping line positioned halfway between the generating point and the neighboring point, 3530 and orthogonal to the line connecting them. Clips are readily performed by evaluationg the 3531 vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line. 3532 If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip 3533 line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs, 3534 the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region 3535 containing the neighboring clip points. The clip region (along with the points contained in it) is grown 3536 by careful expansion (e.g., outward spiraling iterator over all candidate clip points). 3537 When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi 3538 tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi 3539 tessellation. Note that topological and geometric information is used to generate a valid triangulation 3540 (e.g., merging points and validating topology). 3541 3542 Arguments: 3543 pts : (list) 3544 list of input points. 3545 padding : (float) 3546 padding distance. The default is 0. 3547 fit : (bool) 3548 detect automatically the best fitting plane. The default is False. 3549 3550 Examples: 3551 - [voronoi1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi1.py) 3552 3553 ![](https://vedo.embl.es/images/basic/voronoi1.png) 3554 3555 - [voronoi2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi2.py) 3556 3557 ![](https://vedo.embl.es/images/advanced/voronoi2.png) 3558 """ 3559 pts = self.vertices 3560 3561 if method == "scipy": 3562 from scipy.spatial import Voronoi as scipy_voronoi 3563 3564 pts = np.asarray(pts)[:, (0, 1)] 3565 vor = scipy_voronoi(pts) 3566 regs = [] # filter out invalid indices 3567 for r in vor.regions: 3568 flag = True 3569 for x in r: 3570 if x < 0: 3571 flag = False 3572 break 3573 if flag and len(r) > 0: 3574 regs.append(r) 3575 3576 m = vedo.Mesh([vor.vertices, regs]) 3577 m.celldata["VoronoiID"] = np.array(list(range(len(regs)))).astype(int) 3578 3579 elif method == "vtk": 3580 vor = vtki.new("Voronoi2D") 3581 if isinstance(pts, Points): 3582 vor.SetInputData(pts) 3583 else: 3584 pts = np.asarray(pts) 3585 if pts.shape[1] == 2: 3586 pts = np.c_[pts, np.zeros(len(pts))] 3587 pd = vtki.vtkPolyData() 3588 vpts = vtki.vtkPoints() 3589 vpts.SetData(utils.numpy2vtk(pts, dtype=np.float32)) 3590 pd.SetPoints(vpts) 3591 vor.SetInputData(pd) 3592 vor.SetPadding(padding) 3593 vor.SetGenerateScalarsToPointIds() 3594 if fit: 3595 vor.SetProjectionPlaneModeToBestFittingPlane() 3596 else: 3597 vor.SetProjectionPlaneModeToXYPlane() 3598 vor.Update() 3599 poly = vor.GetOutput() 3600 arr = poly.GetCellData().GetArray(0) 3601 if arr: 3602 arr.SetName("VoronoiID") 3603 m = vedo.Mesh(poly, c="orange5") 3604 3605 else: 3606 vedo.logger.error(f"Unknown method {method} in voronoi()") 3607 raise RuntimeError 3608 3609 m.lw(2).lighting("off").wireframe() 3610 m.name = "Voronoi" 3611 return m 3612 3613 ########################################################################## 3614 def generate_delaunay3d(self, radius=0, tol=None) -> "vedo.TetMesh": 3615 """ 3616 Create 3D Delaunay triangulation of input points. 3617 3618 Arguments: 3619 radius : (float) 3620 specify distance (or "alpha") value to control output. 3621 For a non-zero values, only tetra contained within the circumsphere 3622 will be output. 3623 tol : (float) 3624 Specify a tolerance to control discarding of closely spaced points. 3625 This tolerance is specified as a fraction of the diagonal length of 3626 the bounding box of the points. 3627 """ 3628 deln = vtki.new("Delaunay3D") 3629 deln.SetInputData(self.dataset) 3630 deln.SetAlpha(radius) 3631 deln.AlphaTetsOn() 3632 deln.AlphaTrisOff() 3633 deln.AlphaLinesOff() 3634 deln.AlphaVertsOff() 3635 deln.BoundingTriangulationOff() 3636 if tol: 3637 deln.SetTolerance(tol) 3638 deln.Update() 3639 m = vedo.TetMesh(deln.GetOutput()) 3640 m.pipeline = utils.OperationNode( 3641 "generate_delaunay3d", c="#e9c46a:#edabab", parents=[self], 3642 ) 3643 m.name = "Delaunay3D" 3644 return m 3645 3646 #################################################### 3647 def visible_points(self, area=(), tol=None, invert=False) -> Union[Self, None]: 3648 """ 3649 Extract points based on whether they are visible or not. 3650 Visibility is determined by accessing the z-buffer of a rendering window. 3651 The position of each input point is converted into display coordinates, 3652 and then the z-value at that point is obtained. 3653 If within the user-specified tolerance, the point is considered visible. 3654 Associated data attributes are passed to the output as well. 3655 3656 This filter also allows you to specify a rectangular window in display (pixel) 3657 coordinates in which the visible points must lie. 3658 3659 Arguments: 3660 area : (list) 3661 specify a rectangular region as (xmin,xmax,ymin,ymax) 3662 tol : (float) 3663 a tolerance in normalized display coordinate system 3664 invert : (bool) 3665 select invisible points instead. 3666 3667 Example: 3668 ```python 3669 from vedo import Ellipsoid, show 3670 s = Ellipsoid().rotate_y(30) 3671 3672 # Camera options: pos, focal_point, viewup, distance 3673 camopts = dict(pos=(0,0,25), focal_point=(0,0,0)) 3674 show(s, camera=camopts, offscreen=True) 3675 3676 m = s.visible_points() 3677 # print('visible pts:', m.vertices) # numpy array 3678 show(m, new=True, axes=1).close() # optionally draw result in a new window 3679 ``` 3680 ![](https://vedo.embl.es/images/feats/visible_points.png) 3681 """ 3682 svp = vtki.new("SelectVisiblePoints") 3683 svp.SetInputData(self.dataset) 3684 3685 ren = None 3686 if vedo.plotter_instance: 3687 if vedo.plotter_instance.renderer: 3688 ren = vedo.plotter_instance.renderer 3689 svp.SetRenderer(ren) 3690 if not ren: 3691 vedo.logger.warning( 3692 "visible_points() can only be used after a rendering step" 3693 ) 3694 return None 3695 3696 if len(area) == 2: 3697 area = utils.flatten(area) 3698 if len(area) == 4: 3699 # specify a rectangular region 3700 svp.SetSelection(area[0], area[1], area[2], area[3]) 3701 if tol is not None: 3702 svp.SetTolerance(tol) 3703 if invert: 3704 svp.SelectInvisibleOn() 3705 svp.Update() 3706 3707 m = Points(svp.GetOutput()) 3708 m.name = "VisiblePoints" 3709 return m 3710 3711#################################################### 3712class CellCenters(Points): 3713 def __init__(self, pcloud): 3714 """ 3715 Generate `Points` at the center of the cells of any type of object. 3716 3717 Check out also `cell_centers()`. 3718 """ 3719 vcen = vtki.new("CellCenters") 3720 vcen.CopyArraysOn() 3721 vcen.VertexCellsOn() 3722 # vcen.ConvertGhostCellsToGhostPointsOn() 3723 try: 3724 vcen.SetInputData(pcloud.dataset) 3725 except AttributeError: 3726 vcen.SetInputData(pcloud) 3727 vcen.Update() 3728 super().__init__(vcen.GetOutput()) 3729 self.name = "CellCenters"
467class Points(PointsVisual, PointAlgorithms): 468 """Work with point clouds.""" 469 470 def __init__(self, inputobj=None, r=4, c=(0.2, 0.2, 0.2), alpha=1): 471 """ 472 Build an object made of only vertex points for a list of 2D/3D points. 473 Both shapes (N, 3) or (3, N) are accepted as input, if N>3. 474 For very large point clouds a list of colors and alpha can be assigned to each 475 point in the form c=[(R,G,B,A), ... ] where 0<=R<256, ... 0<=A<256. 476 477 Arguments: 478 inputobj : (list, tuple) 479 r : (int) 480 Point radius in units of pixels. 481 c : (str, list) 482 Color name or rgb tuple. 483 alpha : (float) 484 Transparency in range [0,1]. 485 486 Example: 487 ```python 488 from vedo import * 489 490 def fibonacci_sphere(n): 491 s = np.linspace(0, n, num=n, endpoint=False) 492 theta = s * 2.399963229728653 493 y = 1 - s * (2/(n-1)) 494 r = np.sqrt(1 - y * y) 495 x = np.cos(theta) * r 496 z = np.sin(theta) * r 497 return np._c[x,y,z] 498 499 Points(fibonacci_sphere(1000)).show(axes=1).close() 500 ``` 501 ![](https://vedo.embl.es/images/feats/fibonacci.png) 502 """ 503 # print("INIT POINTS") 504 super().__init__() 505 506 self.name = "" 507 self.filename = "" 508 self.file_size = "" 509 510 self.info = {} 511 self.time = time.time() 512 513 self.transform = LinearTransform() 514 self.point_locator = None 515 self.cell_locator = None 516 self.line_locator = None 517 518 self.actor = vtki.vtkActor() 519 self.properties = self.actor.GetProperty() 520 self.properties_backface = self.actor.GetBackfaceProperty() 521 self.mapper = vtki.new("PolyDataMapper") 522 self.dataset = vtki.vtkPolyData() 523 524 # Create weakref so actor can access this object (eg to pick/remove): 525 self.actor.retrieve_object = weak_ref_to(self) 526 527 try: 528 self.properties.RenderPointsAsSpheresOn() 529 except AttributeError: 530 pass 531 532 if inputobj is None: #################### 533 return 534 ########################################## 535 536 self.name = "Points" 537 538 ###### 539 if isinstance(inputobj, vtki.vtkActor): 540 self.dataset.DeepCopy(inputobj.GetMapper().GetInput()) 541 pr = vtki.vtkProperty() 542 pr.DeepCopy(inputobj.GetProperty()) 543 self.actor.SetProperty(pr) 544 self.properties = pr 545 self.mapper.SetScalarVisibility(inputobj.GetMapper().GetScalarVisibility()) 546 547 elif isinstance(inputobj, vtki.vtkPolyData): 548 self.dataset = inputobj 549 if self.dataset.GetNumberOfCells() == 0: 550 carr = vtki.vtkCellArray() 551 for i in range(self.dataset.GetNumberOfPoints()): 552 carr.InsertNextCell(1) 553 carr.InsertCellPoint(i) 554 self.dataset.SetVerts(carr) 555 556 elif isinstance(inputobj, Points): 557 self.dataset = inputobj.dataset 558 self.copy_properties_from(inputobj) 559 560 elif utils.is_sequence(inputobj): # passing point coords 561 self.dataset = utils.buildPolyData(utils.make3d(inputobj)) 562 563 elif isinstance(inputobj, str): 564 verts = vedo.file_io.load(inputobj) 565 self.filename = inputobj 566 self.dataset = verts.dataset 567 568 else: 569 # try to extract the points from a generic VTK input data object 570 if hasattr(inputobj, "dataset"): 571 inputobj = inputobj.dataset 572 try: 573 vvpts = inputobj.GetPoints() 574 self.dataset = vtki.vtkPolyData() 575 self.dataset.SetPoints(vvpts) 576 for i in range(inputobj.GetPointData().GetNumberOfArrays()): 577 arr = inputobj.GetPointData().GetArray(i) 578 self.dataset.GetPointData().AddArray(arr) 579 except: 580 vedo.logger.error(f"cannot build Points from type {type(inputobj)}") 581 raise RuntimeError() 582 583 self.actor.SetMapper(self.mapper) 584 self.mapper.SetInputData(self.dataset) 585 586 self.properties.SetColor(colors.get_color(c)) 587 self.properties.SetOpacity(alpha) 588 self.properties.SetRepresentationToPoints() 589 self.properties.SetPointSize(r) 590 self.properties.LightingOff() 591 592 self.pipeline = utils.OperationNode( 593 self, parents=[], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 594 ) 595 596 def _update(self, polydata, reset_locators=True) -> Self: 597 """Overwrite the polygonal dataset with a new vtkPolyData.""" 598 self.dataset = polydata 599 self.mapper.SetInputData(self.dataset) 600 self.mapper.Modified() 601 if reset_locators: 602 self.point_locator = None 603 self.line_locator = None 604 self.cell_locator = None 605 return self 606 607 def __str__(self): 608 """Print a description of the Points/Mesh.""" 609 module = self.__class__.__module__ 610 name = self.__class__.__name__ 611 out = vedo.printc( 612 f"{module}.{name} at ({hex(self.memory_address())})".ljust(75), 613 c="g", bold=True, invert=True, return_string=True, 614 ) 615 out += "\x1b[0m\x1b[32;1m" 616 617 if self.name: 618 out += "name".ljust(14) + ": " + self.name 619 if "legend" in self.info.keys() and self.info["legend"]: 620 out+= f", legend='{self.info['legend']}'" 621 out += "\n" 622 623 if self.filename: 624 out+= "file name".ljust(14) + ": " + self.filename + "\n" 625 626 if not self.mapper.GetScalarVisibility(): 627 col = utils.precision(self.properties.GetColor(), 3) 628 cname = vedo.colors.get_color_name(self.properties.GetColor()) 629 out+= "color".ljust(14) + ": " + cname 630 out+= f", rgb={col}, alpha={self.properties.GetOpacity()}\n" 631 if self.actor.GetBackfaceProperty(): 632 bcol = self.actor.GetBackfaceProperty().GetDiffuseColor() 633 cname = vedo.colors.get_color_name(bcol) 634 out+= "backface color".ljust(14) + ": " 635 out+= f"{cname}, rgb={utils.precision(bcol,3)}\n" 636 637 npt = self.dataset.GetNumberOfPoints() 638 npo, nln = self.dataset.GetNumberOfPolys(), self.dataset.GetNumberOfLines() 639 out+= "elements".ljust(14) + f": vertices={npt:,} polygons={npo:,} lines={nln:,}" 640 if self.dataset.GetNumberOfStrips(): 641 out+= f", strips={self.dataset.GetNumberOfStrips():,}" 642 out+= "\n" 643 if self.dataset.GetNumberOfPieces() > 1: 644 out+= "pieces".ljust(14) + ": " + str(self.dataset.GetNumberOfPieces()) + "\n" 645 646 out+= "position".ljust(14) + ": " + f"{utils.precision(self.pos(), 6)}\n" 647 try: 648 sc = self.transform.get_scale() 649 out+= "scaling".ljust(14) + ": " 650 out+= utils.precision(sc, 6) + "\n" 651 except AttributeError: 652 pass 653 654 if self.npoints: 655 out+="size".ljust(14)+ ": average=" + utils.precision(self.average_size(),6) 656 out+=", diagonal="+ utils.precision(self.diagonal_size(), 6)+ "\n" 657 out+="center of mass".ljust(14) + ": " + utils.precision(self.center_of_mass(),6)+"\n" 658 659 bnds = self.bounds() 660 bx1, bx2 = utils.precision(bnds[0], 3), utils.precision(bnds[1], 3) 661 by1, by2 = utils.precision(bnds[2], 3), utils.precision(bnds[3], 3) 662 bz1, bz2 = utils.precision(bnds[4], 3), utils.precision(bnds[5], 3) 663 out+= "bounds".ljust(14) + ":" 664 out+= " x=(" + bx1 + ", " + bx2 + ")," 665 out+= " y=(" + by1 + ", " + by2 + ")," 666 out+= " z=(" + bz1 + ", " + bz2 + ")\n" 667 668 for key in self.pointdata.keys(): 669 arr = self.pointdata[key] 670 dim = arr.shape[1] if arr.ndim > 1 else 1 671 mark_active = "pointdata" 672 a_scalars = self.dataset.GetPointData().GetScalars() 673 a_vectors = self.dataset.GetPointData().GetVectors() 674 a_tensors = self.dataset.GetPointData().GetTensors() 675 if a_scalars and a_scalars.GetName() == key: 676 mark_active += " *" 677 elif a_vectors and a_vectors.GetName() == key: 678 mark_active += " **" 679 elif a_tensors and a_tensors.GetName() == key: 680 mark_active += " ***" 681 out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), dim={dim}' 682 if dim == 1: 683 rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3) 684 out += f", range=({rng})\n" 685 else: 686 out += "\n" 687 688 for key in self.celldata.keys(): 689 arr = self.celldata[key] 690 dim = arr.shape[1] if arr.ndim > 1 else 1 691 mark_active = "celldata" 692 a_scalars = self.dataset.GetCellData().GetScalars() 693 a_vectors = self.dataset.GetCellData().GetVectors() 694 a_tensors = self.dataset.GetCellData().GetTensors() 695 if a_scalars and a_scalars.GetName() == key: 696 mark_active += " *" 697 elif a_vectors and a_vectors.GetName() == key: 698 mark_active += " **" 699 elif a_tensors and a_tensors.GetName() == key: 700 mark_active += " ***" 701 out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), dim={dim}' 702 if dim == 1: 703 rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3) 704 out += f", range=({rng})\n" 705 else: 706 out += "\n" 707 708 for key in self.metadata.keys(): 709 arr = self.metadata[key] 710 if len(arr) > 3: 711 out+= "metadata".ljust(14) + ": " + f'"{key}" ({len(arr)} values)\n' 712 else: 713 out+= "metadata".ljust(14) + ": " + f'"{key}" = {arr}\n' 714 715 if self.picked3d is not None: 716 idp = self.closest_point(self.picked3d, return_point_id=True) 717 idc = self.closest_point(self.picked3d, return_cell_id=True) 718 out+= "clicked point".ljust(14) + ": " + utils.precision(self.picked3d, 6) 719 out+= f", pointID={idp}, cellID={idc}\n" 720 721 return out.rstrip() + "\x1b[0m" 722 723 def _repr_html_(self): 724 """ 725 HTML representation of the Point cloud object for Jupyter Notebooks. 726 727 Returns: 728 HTML text with the image and some properties. 729 """ 730 import io 731 import base64 732 from PIL import Image 733 734 library_name = "vedo.pointcloud.Points" 735 help_url = "https://vedo.embl.es/docs/vedo/pointcloud.html#Points" 736 737 arr = self.thumbnail() 738 im = Image.fromarray(arr) 739 buffered = io.BytesIO() 740 im.save(buffered, format="PNG", quality=100) 741 encoded = base64.b64encode(buffered.getvalue()).decode("utf-8") 742 url = "data:image/png;base64," + encoded 743 image = f"<img src='{url}'></img>" 744 745 bounds = "<br/>".join( 746 [ 747 utils.precision(min_x, 4) + " ... " + utils.precision(max_x, 4) 748 for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2]) 749 ] 750 ) 751 average_size = "{size:.3f}".format(size=self.average_size()) 752 753 help_text = "" 754 if self.name: 755 help_text += f"<b> {self.name}:   </b>" 756 help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>" 757 if self.filename: 758 dots = "" 759 if len(self.filename) > 30: 760 dots = "..." 761 help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>" 762 763 pdata = "" 764 if self.dataset.GetPointData().GetScalars(): 765 if self.dataset.GetPointData().GetScalars().GetName(): 766 name = self.dataset.GetPointData().GetScalars().GetName() 767 pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>" 768 769 cdata = "" 770 if self.dataset.GetCellData().GetScalars(): 771 if self.dataset.GetCellData().GetScalars().GetName(): 772 name = self.dataset.GetCellData().GetScalars().GetName() 773 cdata = "<tr><td><b> cell data array </b></td><td>" + name + "</td></tr>" 774 775 allt = [ 776 "<table>", 777 "<tr>", 778 "<td>", 779 image, 780 "</td>", 781 "<td style='text-align: center; vertical-align: center;'><br/>", 782 help_text, 783 "<table>", 784 "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>", 785 "<tr><td><b> center of mass </b></td><td>" 786 + utils.precision(self.center_of_mass(), 3) 787 + "</td></tr>", 788 "<tr><td><b> average size </b></td><td>" + str(average_size) + "</td></tr>", 789 "<tr><td><b> nr. points </b></td><td>" + str(self.npoints) + "</td></tr>", 790 pdata, 791 cdata, 792 "</table>", 793 "</table>", 794 ] 795 return "\n".join(allt) 796 797 ################################################################################## 798 def __add__(self, meshs): 799 """ 800 Add two meshes or a list of meshes together to form an `Assembly` object. 801 """ 802 if isinstance(meshs, list): 803 alist = [self] 804 for l in meshs: 805 if isinstance(l, vedo.Assembly): 806 alist += l.unpack() 807 else: 808 alist += l 809 return vedo.assembly.Assembly(alist) 810 811 if isinstance(meshs, vedo.Assembly): 812 return meshs + self # use Assembly.__add__ 813 814 return vedo.assembly.Assembly([self, meshs]) 815 816 def polydata(self, **kwargs): 817 """ 818 Obsolete. Use property `.dataset` instead. 819 Returns the underlying `vtkPolyData` object. 820 """ 821 colors.printc( 822 "WARNING: call to .polydata() is obsolete, use property .dataset instead.", 823 c="y") 824 return self.dataset 825 826 def __copy__(self): 827 return self.clone(deep=False) 828 829 def __deepcopy__(self, memo): 830 return self.clone(deep=memo) 831 832 def copy(self, deep=True) -> Self: 833 """Return a copy of the object. Alias of `clone()`.""" 834 return self.clone(deep=deep) 835 836 def clone(self, deep=True) -> Self: 837 """ 838 Clone a `PointCloud` or `Mesh` object to make an exact copy of it. 839 Alias of `copy()`. 840 841 Arguments: 842 deep : (bool) 843 if False return a shallow copy of the mesh without copying the points array. 844 845 Examples: 846 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 847 848 ![](https://vedo.embl.es/images/basic/mirror.png) 849 """ 850 poly = vtki.vtkPolyData() 851 if deep or isinstance(deep, dict): # if a memo object is passed this checks as True 852 poly.DeepCopy(self.dataset) 853 else: 854 poly.ShallowCopy(self.dataset) 855 856 if isinstance(self, vedo.Mesh): 857 cloned = vedo.Mesh(poly) 858 else: 859 cloned = Points(poly) 860 # print([self], self.__class__) 861 # cloned = self.__class__(poly) 862 863 cloned.transform = self.transform.clone() 864 865 cloned.copy_properties_from(self) 866 867 cloned.name = str(self.name) 868 cloned.filename = str(self.filename) 869 cloned.info = dict(self.info) 870 cloned.pipeline = utils.OperationNode("clone", parents=[self], shape="diamond", c="#edede9") 871 872 if isinstance(deep, dict): 873 deep[id(self)] = cloned 874 875 return cloned 876 877 def compute_normals_with_pca(self, n=20, orientation_point=None, invert=False) -> Self: 878 """ 879 Generate point normals using PCA (principal component analysis). 880 This algorithm estimates a local tangent plane around each sample point p 881 by considering a small neighborhood of points around p, and fitting a plane 882 to the neighborhood (via PCA). 883 884 Arguments: 885 n : (int) 886 neighborhood size to calculate the normal 887 orientation_point : (list) 888 adjust the +/- sign of the normals so that 889 the normals all point towards a specified point. If None, perform a traversal 890 of the point cloud and flip neighboring normals so that they are mutually consistent. 891 invert : (bool) 892 flip all normals 893 """ 894 poly = self.dataset 895 pcan = vtki.new("PCANormalEstimation") 896 pcan.SetInputData(poly) 897 pcan.SetSampleSize(n) 898 899 if orientation_point is not None: 900 pcan.SetNormalOrientationToPoint() 901 pcan.SetOrientationPoint(orientation_point) 902 else: 903 pcan.SetNormalOrientationToGraphTraversal() 904 905 if invert: 906 pcan.FlipNormalsOn() 907 pcan.Update() 908 909 varr = pcan.GetOutput().GetPointData().GetNormals() 910 varr.SetName("Normals") 911 self.dataset.GetPointData().SetNormals(varr) 912 self.dataset.GetPointData().Modified() 913 return self 914 915 def compute_acoplanarity(self, n=25, radius=None, on="points") -> Self: 916 """ 917 Compute acoplanarity which is a measure of how much a local region of the mesh 918 differs from a plane. 919 920 The information is stored in a `pointdata` or `celldata` array with name 'Acoplanarity'. 921 922 Either `n` (number of neighbour points) or `radius` (radius of local search) can be specified. 923 If a radius value is given and not enough points fall inside it, then a -1 is stored. 924 925 Example: 926 ```python 927 from vedo import * 928 msh = ParametricShape('RandomHills') 929 msh.compute_acoplanarity(radius=0.1, on='cells') 930 msh.cmap("coolwarm", on='cells').add_scalarbar() 931 msh.show(axes=1).close() 932 ``` 933 ![](https://vedo.embl.es/images/feats/acoplanarity.jpg) 934 """ 935 acoplanarities = [] 936 if "point" in on: 937 pts = self.vertices 938 elif "cell" in on: 939 pts = self.cell_centers 940 else: 941 raise ValueError(f"In compute_acoplanarity() set on to either 'cells' or 'points', not {on}") 942 943 for p in utils.progressbar(pts, delay=5, width=15, title=f"{on} acoplanarity"): 944 if n: 945 data = self.closest_point(p, n=n) 946 npts = n 947 elif radius: 948 data = self.closest_point(p, radius=radius) 949 npts = len(data) 950 951 try: 952 center = data.mean(axis=0) 953 res = np.linalg.svd(data - center) 954 acoplanarities.append(res[1][2] / npts) 955 except: 956 acoplanarities.append(-1.0) 957 958 if "point" in on: 959 self.pointdata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 960 else: 961 self.celldata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 962 return self 963 964 def distance_to(self, pcloud, signed=False, invert=False, name="Distance") -> np.ndarray: 965 """ 966 Computes the distance from one point cloud or mesh to another point cloud or mesh. 967 This new `pointdata` array is saved with default name "Distance". 968 969 Keywords `signed` and `invert` are used to compute signed distance, 970 but the mesh in that case must have polygonal faces (not a simple point cloud), 971 and normals must also be computed. 972 973 Examples: 974 - [distance2mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/distance2mesh.py) 975 976 ![](https://vedo.embl.es/images/basic/distance2mesh.png) 977 """ 978 if pcloud.dataset.GetNumberOfPolys(): 979 980 poly1 = self.dataset 981 poly2 = pcloud.dataset 982 df = vtki.new("DistancePolyDataFilter") 983 df.ComputeSecondDistanceOff() 984 df.SetInputData(0, poly1) 985 df.SetInputData(1, poly2) 986 df.SetSignedDistance(signed) 987 df.SetNegateDistance(invert) 988 df.Update() 989 scals = df.GetOutput().GetPointData().GetScalars() 990 dists = utils.vtk2numpy(scals) 991 992 else: # has no polygons 993 994 if signed: 995 vedo.logger.warning("distance_to() called with signed=True but input object has no polygons") 996 997 if not pcloud.point_locator: 998 pcloud.point_locator = vtki.new("PointLocator") 999 pcloud.point_locator.SetDataSet(pcloud.dataset) 1000 pcloud.point_locator.BuildLocator() 1001 1002 ids = [] 1003 ps1 = self.vertices 1004 ps2 = pcloud.vertices 1005 for p in ps1: 1006 pid = pcloud.point_locator.FindClosestPoint(p) 1007 ids.append(pid) 1008 1009 deltas = ps2[ids] - ps1 1010 dists = np.linalg.norm(deltas, axis=1).astype(np.float32) 1011 scals = utils.numpy2vtk(dists) 1012 1013 scals.SetName(name) 1014 self.dataset.GetPointData().AddArray(scals) 1015 self.dataset.GetPointData().SetActiveScalars(scals.GetName()) 1016 rng = scals.GetRange() 1017 self.mapper.SetScalarRange(rng[0], rng[1]) 1018 self.mapper.ScalarVisibilityOn() 1019 1020 self.pipeline = utils.OperationNode( 1021 "distance_to", 1022 parents=[self, pcloud], 1023 shape="cylinder", 1024 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1025 ) 1026 return dists 1027 1028 def clean(self) -> Self: 1029 """Clean pointcloud or mesh by removing coincident points.""" 1030 cpd = vtki.new("CleanPolyData") 1031 cpd.PointMergingOn() 1032 cpd.ConvertLinesToPointsOff() 1033 cpd.ConvertPolysToLinesOff() 1034 cpd.ConvertStripsToPolysOff() 1035 cpd.SetInputData(self.dataset) 1036 cpd.Update() 1037 self._update(cpd.GetOutput()) 1038 self.pipeline = utils.OperationNode( 1039 "clean", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1040 ) 1041 return self 1042 1043 def subsample(self, fraction: float, absolute=False) -> Self: 1044 """ 1045 Subsample a point cloud by requiring that the points 1046 or vertices are far apart at least by the specified fraction of the object size. 1047 If a Mesh is passed the polygonal faces are not removed 1048 but holes can appear as their vertices are removed. 1049 1050 Examples: 1051 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1052 1053 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1054 1055 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1056 1057 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1058 """ 1059 if not absolute: 1060 if fraction > 1: 1061 vedo.logger.warning( 1062 f"subsample(fraction=...), fraction must be < 1, but is {fraction}" 1063 ) 1064 if fraction <= 0: 1065 return self 1066 1067 cpd = vtki.new("CleanPolyData") 1068 cpd.PointMergingOn() 1069 cpd.ConvertLinesToPointsOn() 1070 cpd.ConvertPolysToLinesOn() 1071 cpd.ConvertStripsToPolysOn() 1072 cpd.SetInputData(self.dataset) 1073 if absolute: 1074 cpd.SetTolerance(fraction / self.diagonal_size()) 1075 # cpd.SetToleranceIsAbsolute(absolute) 1076 else: 1077 cpd.SetTolerance(fraction) 1078 cpd.Update() 1079 1080 ps = 2 1081 if self.properties.GetRepresentation() == 0: 1082 ps = self.properties.GetPointSize() 1083 1084 self._update(cpd.GetOutput()) 1085 self.ps(ps) 1086 1087 self.pipeline = utils.OperationNode( 1088 "subsample", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1089 ) 1090 return self 1091 1092 def threshold(self, scalars: str, above=None, below=None, on="points") -> Self: 1093 """ 1094 Extracts cells where scalar value satisfies threshold criterion. 1095 1096 Arguments: 1097 scalars : (str) 1098 name of the scalars array. 1099 above : (float) 1100 minimum value of the scalar 1101 below : (float) 1102 maximum value of the scalar 1103 on : (str) 1104 if 'cells' assume array of scalars refers to cell data. 1105 1106 Examples: 1107 - [mesh_threshold.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_threshold.py) 1108 """ 1109 thres = vtki.new("Threshold") 1110 thres.SetInputData(self.dataset) 1111 1112 if on.startswith("c"): 1113 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_CELLS 1114 else: 1115 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_POINTS 1116 1117 thres.SetInputArrayToProcess(0, 0, 0, asso, scalars) 1118 1119 if above is None and below is not None: 1120 try: # vtk 9.2 1121 thres.ThresholdByLower(below) 1122 except AttributeError: # vtk 9.3 1123 thres.SetUpperThreshold(below) 1124 1125 elif below is None and above is not None: 1126 try: 1127 thres.ThresholdByUpper(above) 1128 except AttributeError: 1129 thres.SetLowerThreshold(above) 1130 else: 1131 try: 1132 thres.ThresholdBetween(above, below) 1133 except AttributeError: 1134 thres.SetUpperThreshold(below) 1135 thres.SetLowerThreshold(above) 1136 1137 thres.Update() 1138 1139 gf = vtki.new("GeometryFilter") 1140 gf.SetInputData(thres.GetOutput()) 1141 gf.Update() 1142 self._update(gf.GetOutput()) 1143 self.pipeline = utils.OperationNode("threshold", parents=[self]) 1144 return self 1145 1146 def quantize(self, value: float) -> Self: 1147 """ 1148 The user should input a value and all {x,y,z} coordinates 1149 will be quantized to that absolute grain size. 1150 """ 1151 qp = vtki.new("QuantizePolyDataPoints") 1152 qp.SetInputData(self.dataset) 1153 qp.SetQFactor(value) 1154 qp.Update() 1155 self._update(qp.GetOutput()) 1156 self.pipeline = utils.OperationNode("quantize", parents=[self]) 1157 return self 1158 1159 @property 1160 def vertex_normals(self) -> np.ndarray: 1161 """ 1162 Retrieve vertex normals as a numpy array. Same as `point_normals`. 1163 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1164 """ 1165 vtknormals = self.dataset.GetPointData().GetNormals() 1166 return utils.vtk2numpy(vtknormals) 1167 1168 @property 1169 def point_normals(self) -> np.ndarray: 1170 """ 1171 Retrieve vertex normals as a numpy array. Same as `vertex_normals`. 1172 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1173 """ 1174 vtknormals = self.dataset.GetPointData().GetNormals() 1175 return utils.vtk2numpy(vtknormals) 1176 1177 def align_to(self, target, iters=100, rigid=False, invert=False, use_centroids=False) -> Self: 1178 """ 1179 Aligned to target mesh through the `Iterative Closest Point` algorithm. 1180 1181 The core of the algorithm is to match each vertex in one surface with 1182 the closest surface point on the other, then apply the transformation 1183 that modify one surface to best match the other (in the least-square sense). 1184 1185 Arguments: 1186 rigid : (bool) 1187 if True do not allow scaling 1188 invert : (bool) 1189 if True start by aligning the target to the source but 1190 invert the transformation finally. Useful when the target is smaller 1191 than the source. 1192 use_centroids : (bool) 1193 start by matching the centroids of the two objects. 1194 1195 Examples: 1196 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1197 1198 ![](https://vedo.embl.es/images/basic/align1.png) 1199 1200 - [align2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align2.py) 1201 1202 ![](https://vedo.embl.es/images/basic/align2.png) 1203 """ 1204 icp = vtki.new("IterativeClosestPointTransform") 1205 icp.SetSource(self.dataset) 1206 icp.SetTarget(target.dataset) 1207 if invert: 1208 icp.Inverse() 1209 icp.SetMaximumNumberOfIterations(iters) 1210 if rigid: 1211 icp.GetLandmarkTransform().SetModeToRigidBody() 1212 icp.SetStartByMatchingCentroids(use_centroids) 1213 icp.Update() 1214 1215 self.apply_transform(icp.GetMatrix()) 1216 1217 self.pipeline = utils.OperationNode( 1218 "align_to", parents=[self, target], comment=f"rigid = {rigid}" 1219 ) 1220 return self 1221 1222 def align_to_bounding_box(self, msh, rigid=False) -> Self: 1223 """ 1224 Align the current object's bounding box to the bounding box 1225 of the input object. 1226 1227 Use `rigid=True` to disable scaling. 1228 1229 Example: 1230 [align6.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align6.py) 1231 """ 1232 lmt = vtki.vtkLandmarkTransform() 1233 ss = vtki.vtkPoints() 1234 xss0, xss1, yss0, yss1, zss0, zss1 = self.bounds() 1235 for p in [ 1236 [xss0, yss0, zss0], 1237 [xss1, yss0, zss0], 1238 [xss1, yss1, zss0], 1239 [xss0, yss1, zss0], 1240 [xss0, yss0, zss1], 1241 [xss1, yss0, zss1], 1242 [xss1, yss1, zss1], 1243 [xss0, yss1, zss1], 1244 ]: 1245 ss.InsertNextPoint(p) 1246 st = vtki.vtkPoints() 1247 xst0, xst1, yst0, yst1, zst0, zst1 = msh.bounds() 1248 for p in [ 1249 [xst0, yst0, zst0], 1250 [xst1, yst0, zst0], 1251 [xst1, yst1, zst0], 1252 [xst0, yst1, zst0], 1253 [xst0, yst0, zst1], 1254 [xst1, yst0, zst1], 1255 [xst1, yst1, zst1], 1256 [xst0, yst1, zst1], 1257 ]: 1258 st.InsertNextPoint(p) 1259 1260 lmt.SetSourceLandmarks(ss) 1261 lmt.SetTargetLandmarks(st) 1262 lmt.SetModeToAffine() 1263 if rigid: 1264 lmt.SetModeToRigidBody() 1265 lmt.Update() 1266 1267 LT = LinearTransform(lmt) 1268 self.apply_transform(LT) 1269 return self 1270 1271 def align_with_landmarks( 1272 self, 1273 source_landmarks, 1274 target_landmarks, 1275 rigid=False, 1276 affine=False, 1277 least_squares=False, 1278 ) -> Self: 1279 """ 1280 Transform mesh orientation and position based on a set of landmarks points. 1281 The algorithm finds the best matching of source points to target points 1282 in the mean least square sense, in one single step. 1283 1284 If `affine` is True the x, y and z axes can scale independently but stay collinear. 1285 With least_squares they can vary orientation. 1286 1287 Examples: 1288 - [align5.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align5.py) 1289 1290 ![](https://vedo.embl.es/images/basic/align5.png) 1291 """ 1292 1293 if utils.is_sequence(source_landmarks): 1294 ss = vtki.vtkPoints() 1295 for p in source_landmarks: 1296 ss.InsertNextPoint(p) 1297 else: 1298 ss = source_landmarks.dataset.GetPoints() 1299 if least_squares: 1300 source_landmarks = source_landmarks.vertices 1301 1302 if utils.is_sequence(target_landmarks): 1303 st = vtki.vtkPoints() 1304 for p in target_landmarks: 1305 st.InsertNextPoint(p) 1306 else: 1307 st = target_landmarks.GetPoints() 1308 if least_squares: 1309 target_landmarks = target_landmarks.vertices 1310 1311 if ss.GetNumberOfPoints() != st.GetNumberOfPoints(): 1312 n1 = ss.GetNumberOfPoints() 1313 n2 = st.GetNumberOfPoints() 1314 vedo.logger.error(f"source and target have different nr of points {n1} vs {n2}") 1315 raise RuntimeError() 1316 1317 if int(rigid) + int(affine) + int(least_squares) > 1: 1318 vedo.logger.error( 1319 "only one of rigid, affine, least_squares can be True at a time" 1320 ) 1321 raise RuntimeError() 1322 1323 lmt = vtki.vtkLandmarkTransform() 1324 lmt.SetSourceLandmarks(ss) 1325 lmt.SetTargetLandmarks(st) 1326 lmt.SetModeToSimilarity() 1327 1328 if rigid: 1329 lmt.SetModeToRigidBody() 1330 lmt.Update() 1331 1332 elif affine: 1333 lmt.SetModeToAffine() 1334 lmt.Update() 1335 1336 elif least_squares: 1337 cms = source_landmarks.mean(axis=0) 1338 cmt = target_landmarks.mean(axis=0) 1339 m = np.linalg.lstsq(source_landmarks - cms, target_landmarks - cmt, rcond=None)[0] 1340 M = vtki.vtkMatrix4x4() 1341 for i in range(3): 1342 for j in range(3): 1343 M.SetElement(j, i, m[i][j]) 1344 lmt = vtki.vtkTransform() 1345 lmt.Translate(cmt) 1346 lmt.Concatenate(M) 1347 lmt.Translate(-cms) 1348 1349 else: 1350 lmt.Update() 1351 1352 self.apply_transform(lmt) 1353 self.pipeline = utils.OperationNode("transform_with_landmarks", parents=[self]) 1354 return self 1355 1356 def normalize(self) -> Self: 1357 """Scale average size to unit. The scaling is performed around the center of mass.""" 1358 coords = self.vertices 1359 if not coords.shape[0]: 1360 return self 1361 cm = np.mean(coords, axis=0) 1362 pts = coords - cm 1363 xyz2 = np.sum(pts * pts, axis=0) 1364 scale = 1 / np.sqrt(np.sum(xyz2) / len(pts)) 1365 self.scale(scale, origin=cm) 1366 self.pipeline = utils.OperationNode("normalize", parents=[self]) 1367 return self 1368 1369 def mirror(self, axis="x", origin=True) -> Self: 1370 """ 1371 Mirror reflect along one of the cartesian axes 1372 1373 Arguments: 1374 axis : (str) 1375 axis to use for mirroring, must be set to `x, y, z`. 1376 Or any combination of those. 1377 origin : (list) 1378 use this point as the origin of the mirroring transformation. 1379 1380 Examples: 1381 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 1382 1383 ![](https://vedo.embl.es/images/basic/mirror.png) 1384 """ 1385 sx, sy, sz = 1, 1, 1 1386 if "x" in axis.lower(): sx = -1 1387 if "y" in axis.lower(): sy = -1 1388 if "z" in axis.lower(): sz = -1 1389 1390 self.scale([sx, sy, sz], origin=origin) 1391 1392 self.pipeline = utils.OperationNode( 1393 "mirror", comment=f"axis = {axis}", parents=[self]) 1394 1395 if sx * sy * sz < 0: 1396 if hasattr(self, "reverse"): 1397 self.reverse() 1398 return self 1399 1400 def flip_normals(self) -> Self: 1401 """Flip all normals orientation.""" 1402 rs = vtki.new("ReverseSense") 1403 rs.SetInputData(self.dataset) 1404 rs.ReverseCellsOff() 1405 rs.ReverseNormalsOn() 1406 rs.Update() 1407 self._update(rs.GetOutput()) 1408 self.pipeline = utils.OperationNode("flip_normals", parents=[self]) 1409 return self 1410 1411 def add_gaussian_noise(self, sigma=1.0) -> Self: 1412 """ 1413 Add gaussian noise to point positions. 1414 An extra array is added named "GaussianNoise" with the displacements. 1415 1416 Arguments: 1417 sigma : (float) 1418 nr. of standard deviations, expressed in percent of the diagonal size of mesh. 1419 Can also be a list `[sigma_x, sigma_y, sigma_z]`. 1420 1421 Example: 1422 ```python 1423 from vedo import Sphere 1424 Sphere().add_gaussian_noise(1.0).point_size(8).show().close() 1425 ``` 1426 """ 1427 sz = self.diagonal_size() 1428 pts = self.vertices 1429 n = len(pts) 1430 ns = (np.random.randn(n, 3) * sigma) * (sz / 100) 1431 vpts = vtki.vtkPoints() 1432 vpts.SetNumberOfPoints(n) 1433 vpts.SetData(utils.numpy2vtk(pts + ns, dtype=np.float32)) 1434 self.dataset.SetPoints(vpts) 1435 self.dataset.GetPoints().Modified() 1436 self.pointdata["GaussianNoise"] = -ns 1437 self.pipeline = utils.OperationNode( 1438 "gaussian_noise", parents=[self], shape="egg", comment=f"sigma = {sigma}" 1439 ) 1440 return self 1441 1442 def closest_point( 1443 self, pt, n=1, radius=None, return_point_id=False, return_cell_id=False 1444 ) -> Union[List[int], int, np.ndarray]: 1445 """ 1446 Find the closest point(s) on a mesh given from the input point `pt`. 1447 1448 Arguments: 1449 n : (int) 1450 if greater than 1, return a list of n ordered closest points 1451 radius : (float) 1452 if given, get all points within that radius. Then n is ignored. 1453 return_point_id : (bool) 1454 return point ID instead of coordinates 1455 return_cell_id : (bool) 1456 return cell ID in which the closest point sits 1457 1458 Examples: 1459 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1460 - [fitplanes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitplanes.py) 1461 - [quadratic_morphing.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/quadratic_morphing.py) 1462 1463 .. note:: 1464 The appropriate tree search locator is built on the fly and cached for speed. 1465 1466 If you want to reset it use `mymesh.point_locator=None` 1467 and / or `mymesh.cell_locator=None`. 1468 """ 1469 if len(pt) != 3: 1470 pt = [pt[0], pt[1], 0] 1471 1472 # NB: every time the mesh moves or is warped the locators are set to None 1473 if ((n > 1 or radius) or (n == 1 and return_point_id)) and not return_cell_id: 1474 poly = None 1475 if not self.point_locator: 1476 poly = self.dataset 1477 self.point_locator = vtki.new("StaticPointLocator") 1478 self.point_locator.SetDataSet(poly) 1479 self.point_locator.BuildLocator() 1480 1481 ########## 1482 if radius: 1483 vtklist = vtki.vtkIdList() 1484 self.point_locator.FindPointsWithinRadius(radius, pt, vtklist) 1485 elif n > 1: 1486 vtklist = vtki.vtkIdList() 1487 self.point_locator.FindClosestNPoints(n, pt, vtklist) 1488 else: # n==1 hence return_point_id==True 1489 ######## 1490 return self.point_locator.FindClosestPoint(pt) 1491 ######## 1492 1493 if return_point_id: 1494 ######## 1495 return utils.vtk2numpy(vtklist) 1496 ######## 1497 1498 if not poly: 1499 poly = self.dataset 1500 trgp = [] 1501 for i in range(vtklist.GetNumberOfIds()): 1502 trgp_ = [0, 0, 0] 1503 vi = vtklist.GetId(i) 1504 poly.GetPoints().GetPoint(vi, trgp_) 1505 trgp.append(trgp_) 1506 ######## 1507 return np.array(trgp) 1508 ######## 1509 1510 else: 1511 1512 if not self.cell_locator: 1513 poly = self.dataset 1514 1515 # As per Miquel example with limbs the vtkStaticCellLocator doesnt work !! 1516 # https://discourse.vtk.org/t/vtkstaticcelllocator-problem-vtk9-0-3/7854/4 1517 if vedo.vtk_version[0] >= 9 and vedo.vtk_version[1] > 0: 1518 self.cell_locator = vtki.new("StaticCellLocator") 1519 else: 1520 self.cell_locator = vtki.new("CellLocator") 1521 1522 self.cell_locator.SetDataSet(poly) 1523 self.cell_locator.BuildLocator() 1524 1525 if radius is not None: 1526 vedo.printc("Warning: closest_point() with radius is not implemented for cells.", c='r') 1527 1528 if n != 1: 1529 vedo.printc("Warning: closest_point() with n>1 is not implemented for cells.", c='r') 1530 1531 trgp = [0, 0, 0] 1532 cid = vtki.mutable(0) 1533 dist2 = vtki.mutable(0) 1534 subid = vtki.mutable(0) 1535 self.cell_locator.FindClosestPoint(pt, trgp, cid, subid, dist2) 1536 1537 if return_cell_id: 1538 return int(cid) 1539 1540 return np.array(trgp) 1541 1542 def auto_distance(self) -> np.ndarray: 1543 """ 1544 Calculate the distance to the closest point in the same cloud of points. 1545 The output is stored in a new pointdata array called "AutoDistance", 1546 and it is also returned by the function. 1547 """ 1548 points = self.vertices 1549 if not self.point_locator: 1550 self.point_locator = vtki.new("StaticPointLocator") 1551 self.point_locator.SetDataSet(self.dataset) 1552 self.point_locator.BuildLocator() 1553 qs = [] 1554 vtklist = vtki.vtkIdList() 1555 vtkpoints = self.dataset.GetPoints() 1556 for p in points: 1557 self.point_locator.FindClosestNPoints(2, p, vtklist) 1558 q = [0, 0, 0] 1559 pid = vtklist.GetId(1) 1560 vtkpoints.GetPoint(pid, q) 1561 qs.append(q) 1562 dists = np.linalg.norm(points - np.array(qs), axis=1) 1563 self.pointdata["AutoDistance"] = dists 1564 return dists 1565 1566 def hausdorff_distance(self, points) -> float: 1567 """ 1568 Compute the Hausdorff distance to the input point set. 1569 Returns a single `float`. 1570 1571 Example: 1572 ```python 1573 from vedo import * 1574 t = np.linspace(0, 2*np.pi, 100) 1575 x = 4/3 * sin(t)**3 1576 y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12 1577 pol1 = Line(np.c_[x,y], closed=True).triangulate() 1578 pol2 = Polygon(nsides=5).pos(2,2) 1579 d12 = pol1.distance_to(pol2) 1580 d21 = pol2.distance_to(pol1) 1581 pol1.lw(0).cmap("viridis") 1582 pol2.lw(0).cmap("viridis") 1583 print("distance d12, d21 :", min(d12), min(d21)) 1584 print("hausdorff distance:", pol1.hausdorff_distance(pol2)) 1585 print("chamfer distance :", pol1.chamfer_distance(pol2)) 1586 show(pol1, pol2, axes=1) 1587 ``` 1588 ![](https://vedo.embl.es/images/feats/heart.png) 1589 """ 1590 hp = vtki.new("HausdorffDistancePointSetFilter") 1591 hp.SetInputData(0, self.dataset) 1592 hp.SetInputData(1, points.dataset) 1593 hp.SetTargetDistanceMethodToPointToCell() 1594 hp.Update() 1595 return hp.GetHausdorffDistance() 1596 1597 def chamfer_distance(self, pcloud) -> float: 1598 """ 1599 Compute the Chamfer distance to the input point set. 1600 1601 Example: 1602 ```python 1603 from vedo import * 1604 cloud1 = np.random.randn(1000, 3) 1605 cloud2 = np.random.randn(1000, 3) + [1, 2, 3] 1606 c1 = Points(cloud1, r=5, c="red") 1607 c2 = Points(cloud2, r=5, c="green") 1608 d = c1.chamfer_distance(c2) 1609 show(f"Chamfer distance = {d}", c1, c2, axes=1).close() 1610 ``` 1611 """ 1612 # Definition of Chamfer distance may vary, here we use the average 1613 if not pcloud.point_locator: 1614 pcloud.point_locator = vtki.new("PointLocator") 1615 pcloud.point_locator.SetDataSet(pcloud.dataset) 1616 pcloud.point_locator.BuildLocator() 1617 if not self.point_locator: 1618 self.point_locator = vtki.new("PointLocator") 1619 self.point_locator.SetDataSet(self.dataset) 1620 self.point_locator.BuildLocator() 1621 1622 ps1 = self.vertices 1623 ps2 = pcloud.vertices 1624 1625 ids12 = [] 1626 for p in ps1: 1627 pid12 = pcloud.point_locator.FindClosestPoint(p) 1628 ids12.append(pid12) 1629 deltav = ps2[ids12] - ps1 1630 da = np.mean(np.linalg.norm(deltav, axis=1)) 1631 1632 ids21 = [] 1633 for p in ps2: 1634 pid21 = self.point_locator.FindClosestPoint(p) 1635 ids21.append(pid21) 1636 deltav = ps1[ids21] - ps2 1637 db = np.mean(np.linalg.norm(deltav, axis=1)) 1638 return (da + db) / 2 1639 1640 def remove_outliers(self, radius: float, neighbors=5) -> Self: 1641 """ 1642 Remove outliers from a cloud of points within the specified `radius` search. 1643 1644 Arguments: 1645 radius : (float) 1646 Specify the local search radius. 1647 neighbors : (int) 1648 Specify the number of neighbors that a point must have, 1649 within the specified radius, for the point to not be considered isolated. 1650 1651 Examples: 1652 - [clustering.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/clustering.py) 1653 1654 ![](https://vedo.embl.es/images/basic/clustering.png) 1655 """ 1656 removal = vtki.new("RadiusOutlierRemoval") 1657 removal.SetInputData(self.dataset) 1658 removal.SetRadius(radius) 1659 removal.SetNumberOfNeighbors(neighbors) 1660 removal.GenerateOutliersOff() 1661 removal.Update() 1662 inputobj = removal.GetOutput() 1663 if inputobj.GetNumberOfCells() == 0: 1664 carr = vtki.vtkCellArray() 1665 for i in range(inputobj.GetNumberOfPoints()): 1666 carr.InsertNextCell(1) 1667 carr.InsertCellPoint(i) 1668 inputobj.SetVerts(carr) 1669 self._update(removal.GetOutput()) 1670 self.pipeline = utils.OperationNode("remove_outliers", parents=[self]) 1671 return self 1672 1673 def relax_point_positions( 1674 self, 1675 n=10, 1676 iters=10, 1677 sub_iters=10, 1678 packing_factor=1, 1679 max_step=0, 1680 constraints=(), 1681 ) -> Self: 1682 """ 1683 Smooth mesh or points with a 1684 [Laplacian algorithm](https://vtk.org/doc/nightly/html/classvtkPointSmoothingFilter.html) 1685 variant. This modifies the coordinates of the input points by adjusting their positions 1686 to create a smooth distribution (and thereby form a pleasing packing of the points). 1687 Smoothing is performed by considering the effects of neighboring points on one another 1688 it uses a cubic cutoff function to produce repulsive forces between close points 1689 and attractive forces that are a little further away. 1690 1691 In general, the larger the neighborhood size, the greater the reduction in high frequency 1692 information. The memory and computational requirements of the algorithm may also 1693 significantly increase. 1694 1695 The algorithm incrementally adjusts the point positions through an iterative process. 1696 Basically points are moved due to the influence of neighboring points. 1697 1698 As points move, both the local connectivity and data attributes associated with each point 1699 must be updated. Rather than performing these expensive operations after every iteration, 1700 a number of sub-iterations can be specified. If so, then the neighborhood and attribute 1701 value updates occur only every sub iteration, which can improve performance significantly. 1702 1703 Arguments: 1704 n : (int) 1705 neighborhood size to calculate the Laplacian. 1706 iters : (int) 1707 number of iterations. 1708 sub_iters : (int) 1709 number of sub-iterations, i.e. the number of times the neighborhood and attribute 1710 value updates occur during each iteration. 1711 packing_factor : (float) 1712 adjust convergence speed. 1713 max_step : (float) 1714 Specify the maximum smoothing step size for each smoothing iteration. 1715 This limits the the distance over which a point can move in each iteration. 1716 As in all iterative methods, the stability of the process is sensitive to this parameter. 1717 In general, small step size and large numbers of iterations are more stable than a larger 1718 step size and a smaller numbers of iterations. 1719 constraints : (dict) 1720 dictionary of constraints. 1721 Point constraints are used to prevent points from moving, 1722 or to move only on a plane. This can prevent shrinking or growing point clouds. 1723 If enabled, a local topological analysis is performed to determine whether a point 1724 should be marked as fixed" i.e., never moves, or the point only moves on a plane, 1725 or the point can move freely. 1726 If all points in the neighborhood surrounding a point are in the cone defined by 1727 `fixed_angle`, then the point is classified as fixed. 1728 If all points in the neighborhood surrounding a point are in the cone defined by 1729 `boundary_angle`, then the point is classified as lying on a plane. 1730 Angles are expressed in degrees. 1731 1732 Example: 1733 ```py 1734 import numpy as np 1735 from vedo import Points, show 1736 from vedo.pyplot import histogram 1737 1738 vpts1 = Points(np.random.rand(10_000, 3)) 1739 dists = vpts1.auto_distance() 1740 h1 = histogram(dists, xlim=(0,0.08)).clone2d() 1741 1742 vpts2 = vpts1.clone().relax_point_positions(n=100, iters=20, sub_iters=10) 1743 dists = vpts2.auto_distance() 1744 h2 = histogram(dists, xlim=(0,0.08)).clone2d() 1745 1746 show([[vpts1, h1], [vpts2, h2]], N=2).close() 1747 ``` 1748 """ 1749 smooth = vtki.new("PointSmoothingFilter") 1750 smooth.SetInputData(self.dataset) 1751 smooth.SetSmoothingModeToUniform() 1752 smooth.SetNumberOfIterations(iters) 1753 smooth.SetNumberOfSubIterations(sub_iters) 1754 smooth.SetPackingFactor(packing_factor) 1755 if self.point_locator: 1756 smooth.SetLocator(self.point_locator) 1757 if not max_step: 1758 max_step = self.diagonal_size() / 100 1759 smooth.SetMaximumStepSize(max_step) 1760 smooth.SetNeighborhoodSize(n) 1761 if constraints: 1762 fixed_angle = constraints.get("fixed_angle", 45) 1763 boundary_angle = constraints.get("boundary_angle", 110) 1764 smooth.EnableConstraintsOn() 1765 smooth.SetFixedAngle(fixed_angle) 1766 smooth.SetBoundaryAngle(boundary_angle) 1767 smooth.GenerateConstraintScalarsOn() 1768 smooth.GenerateConstraintNormalsOn() 1769 smooth.Update() 1770 self._update(smooth.GetOutput()) 1771 self.metadata["PackingRadius"] = smooth.GetPackingRadius() 1772 self.pipeline = utils.OperationNode("relax_point_positions", parents=[self]) 1773 return self 1774 1775 def smooth_mls_1d(self, f=0.2, radius=None, n=0) -> Self: 1776 """ 1777 Smooth mesh or points with a `Moving Least Squares` variant. 1778 The point data array "Variances" will contain the residue calculated for each point. 1779 1780 Arguments: 1781 f : (float) 1782 smoothing factor - typical range is [0,2]. 1783 radius : (float) 1784 radius search in absolute units. 1785 If set then `f` is ignored. 1786 n : (int) 1787 number of neighbours to be used for the fit. 1788 If set then `f` and `radius` are ignored. 1789 1790 Examples: 1791 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1792 - [skeletonize.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/skeletonize.py) 1793 1794 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1795 """ 1796 coords = self.vertices 1797 ncoords = len(coords) 1798 1799 if n: 1800 Ncp = n 1801 elif radius: 1802 Ncp = 1 1803 else: 1804 Ncp = int(ncoords * f / 10) 1805 if Ncp < 5: 1806 vedo.logger.warning(f"Please choose a fraction higher than {f}") 1807 Ncp = 5 1808 1809 variances, newline = [], [] 1810 for p in coords: 1811 points = self.closest_point(p, n=Ncp, radius=radius) 1812 if len(points) < 4: 1813 continue 1814 1815 points = np.array(points) 1816 pointsmean = points.mean(axis=0) # plane center 1817 _, dd, vv = np.linalg.svd(points - pointsmean) 1818 newp = np.dot(p - pointsmean, vv[0]) * vv[0] + pointsmean 1819 variances.append(dd[1] + dd[2]) 1820 newline.append(newp) 1821 1822 self.pointdata["Variances"] = np.array(variances).astype(np.float32) 1823 self.vertices = newline 1824 self.pipeline = utils.OperationNode("smooth_mls_1d", parents=[self]) 1825 return self 1826 1827 def smooth_mls_2d(self, f=0.2, radius=None, n=0) -> Self: 1828 """ 1829 Smooth mesh or points with a `Moving Least Squares` algorithm variant. 1830 1831 The `mesh.pointdata['MLSVariance']` array will contain the residue calculated for each point. 1832 When a radius is specified, points that are isolated will not be moved and will get 1833 a 0 entry in array `mesh.pointdata['MLSValidPoint']`. 1834 1835 Arguments: 1836 f : (float) 1837 smoothing factor - typical range is [0, 2]. 1838 radius : (float | array) 1839 radius search in absolute units. Can be single value (float) or sequence 1840 for adaptive smoothing. If set then `f` is ignored. 1841 n : (int) 1842 number of neighbours to be used for the fit. 1843 If set then `f` and `radius` are ignored. 1844 1845 Examples: 1846 - [moving_least_squares2D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares2D.py) 1847 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1848 1849 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1850 """ 1851 coords = self.vertices 1852 ncoords = len(coords) 1853 1854 if n: 1855 Ncp = n 1856 radius = None 1857 elif radius is not None: 1858 Ncp = 1 1859 else: 1860 Ncp = int(ncoords * f / 100) 1861 if Ncp < 4: 1862 vedo.logger.error(f"please choose a f-value higher than {f}") 1863 Ncp = 4 1864 1865 variances, newpts, valid = [], [], [] 1866 radius_is_sequence = utils.is_sequence(radius) 1867 1868 pb = None 1869 if ncoords > 10000: 1870 pb = utils.ProgressBar(0, ncoords, delay=3) 1871 1872 for i, p in enumerate(coords): 1873 if pb: 1874 pb.print("smooth_mls_2d working ...") 1875 1876 # if a radius was provided for each point 1877 if radius_is_sequence: 1878 pts = self.closest_point(p, n=Ncp, radius=radius[i]) 1879 else: 1880 pts = self.closest_point(p, n=Ncp, radius=radius) 1881 1882 if len(pts) > 3: 1883 ptsmean = pts.mean(axis=0) # plane center 1884 _, dd, vv = np.linalg.svd(pts - ptsmean) 1885 cv = np.cross(vv[0], vv[1]) 1886 t = (np.dot(cv, ptsmean) - np.dot(cv, p)) / np.dot(cv, cv) 1887 newpts.append(p + cv * t) 1888 variances.append(dd[2]) 1889 if radius is not None: 1890 valid.append(1) 1891 else: 1892 newpts.append(p) 1893 variances.append(0) 1894 if radius is not None: 1895 valid.append(0) 1896 1897 if radius is not None: 1898 self.pointdata["MLSValidPoint"] = np.array(valid).astype(np.uint8) 1899 self.pointdata["MLSVariance"] = np.array(variances).astype(np.float32) 1900 1901 self.vertices = newpts 1902 1903 self.pipeline = utils.OperationNode("smooth_mls_2d", parents=[self]) 1904 return self 1905 1906 def smooth_lloyd_2d(self, iterations=2, bounds=None, options="Qbb Qc Qx") -> Self: 1907 """ 1908 Lloyd relaxation of a 2D pointcloud. 1909 1910 Arguments: 1911 iterations : (int) 1912 number of iterations. 1913 bounds : (list) 1914 bounding box of the domain. 1915 options : (str) 1916 options for the Qhull algorithm. 1917 """ 1918 # Credits: https://hatarilabs.com/ih-en/ 1919 # tutorial-to-create-a-geospatial-voronoi-sh-mesh-with-python-scipy-and-geopandas 1920 from scipy.spatial import Voronoi as scipy_voronoi 1921 1922 def _constrain_points(points): 1923 # Update any points that have drifted beyond the boundaries of this space 1924 if bounds is not None: 1925 for point in points: 1926 if point[0] < bounds[0]: point[0] = bounds[0] 1927 if point[0] > bounds[1]: point[0] = bounds[1] 1928 if point[1] < bounds[2]: point[1] = bounds[2] 1929 if point[1] > bounds[3]: point[1] = bounds[3] 1930 return points 1931 1932 def _find_centroid(vertices): 1933 # The equation for the method used here to find the centroid of a 1934 # 2D polygon is given here: https://en.wikipedia.org/wiki/Centroid#Of_a_polygon 1935 area = 0 1936 centroid_x = 0 1937 centroid_y = 0 1938 for i in range(len(vertices) - 1): 1939 step = (vertices[i, 0] * vertices[i + 1, 1]) - (vertices[i + 1, 0] * vertices[i, 1]) 1940 centroid_x += (vertices[i, 0] + vertices[i + 1, 0]) * step 1941 centroid_y += (vertices[i, 1] + vertices[i + 1, 1]) * step 1942 area += step 1943 if area: 1944 centroid_x = (1.0 / (3.0 * area)) * centroid_x 1945 centroid_y = (1.0 / (3.0 * area)) * centroid_y 1946 # prevent centroids from escaping bounding box 1947 return _constrain_points([[centroid_x, centroid_y]])[0] 1948 1949 def _relax(voron): 1950 # Moves each point to the centroid of its cell in the voronoi 1951 # map to "relax" the points (i.e. jitter the points so as 1952 # to spread them out within the space). 1953 centroids = [] 1954 for idx in voron.point_region: 1955 # the region is a series of indices into voronoi.vertices 1956 # remove point at infinity, designated by index -1 1957 region = [i for i in voron.regions[idx] if i != -1] 1958 # enclose the polygon 1959 region = region + [region[0]] 1960 verts = voron.vertices[region] 1961 # find the centroid of those vertices 1962 centroids.append(_find_centroid(verts)) 1963 return _constrain_points(centroids) 1964 1965 if bounds is None: 1966 bounds = self.bounds() 1967 1968 pts = self.vertices[:, (0, 1)] 1969 for i in range(iterations): 1970 vor = scipy_voronoi(pts, qhull_options=options) 1971 _constrain_points(vor.vertices) 1972 pts = _relax(vor) 1973 out = Points(pts) 1974 out.name = "MeshSmoothLloyd2D" 1975 out.pipeline = utils.OperationNode("smooth_lloyd", parents=[self]) 1976 return out 1977 1978 def project_on_plane(self, plane="z", point=None, direction=None) -> Self: 1979 """ 1980 Project the mesh on one of the Cartesian planes. 1981 1982 Arguments: 1983 plane : (str, Plane) 1984 if plane is `str`, plane can be one of ['x', 'y', 'z'], 1985 represents x-plane, y-plane and z-plane, respectively. 1986 Otherwise, plane should be an instance of `vedo.shapes.Plane`. 1987 point : (float, array) 1988 if plane is `str`, point should be a float represents the intercept. 1989 Otherwise, point is the camera point of perspective projection 1990 direction : (array) 1991 direction of oblique projection 1992 1993 Note: 1994 Parameters `point` and `direction` are only used if the given plane 1995 is an instance of `vedo.shapes.Plane`. And one of these two params 1996 should be left as `None` to specify the projection type. 1997 1998 Example: 1999 ```python 2000 s.project_on_plane(plane='z') # project to z-plane 2001 plane = Plane(pos=(4, 8, -4), normal=(-1, 0, 1), s=(5,5)) 2002 s.project_on_plane(plane=plane) # orthogonal projection 2003 s.project_on_plane(plane=plane, point=(6, 6, 6)) # perspective projection 2004 s.project_on_plane(plane=plane, direction=(1, 2, -1)) # oblique projection 2005 ``` 2006 2007 Examples: 2008 - [silhouette2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette2.py) 2009 2010 ![](https://vedo.embl.es/images/basic/silhouette2.png) 2011 """ 2012 coords = self.vertices 2013 2014 if plane == "x": 2015 coords[:, 0] = self.transform.position[0] 2016 intercept = self.xbounds()[0] if point is None else point 2017 self.x(intercept) 2018 elif plane == "y": 2019 coords[:, 1] = self.transform.position[1] 2020 intercept = self.ybounds()[0] if point is None else point 2021 self.y(intercept) 2022 elif plane == "z": 2023 coords[:, 2] = self.transform.position[2] 2024 intercept = self.zbounds()[0] if point is None else point 2025 self.z(intercept) 2026 2027 elif isinstance(plane, vedo.shapes.Plane): 2028 normal = plane.normal / np.linalg.norm(plane.normal) 2029 pl = np.hstack((normal, -np.dot(plane.pos(), normal))).reshape(4, 1) 2030 if direction is None and point is None: 2031 # orthogonal projection 2032 pt = np.hstack((normal, [0])).reshape(4, 1) 2033 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T # python3 only 2034 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2035 2036 elif direction is None: 2037 # perspective projection 2038 pt = np.hstack((np.array(point), [1])).reshape(4, 1) 2039 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2040 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2041 2042 elif point is None: 2043 # oblique projection 2044 pt = np.hstack((np.array(direction), [0])).reshape(4, 1) 2045 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2046 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2047 2048 coords = np.concatenate([coords, np.ones((coords.shape[:-1] + (1,)))], axis=-1) 2049 # coords = coords @ proj_mat.T 2050 coords = np.matmul(coords, proj_mat.T) 2051 coords = coords[:, :3] / coords[:, 3:] 2052 2053 else: 2054 vedo.logger.error(f"unknown plane {plane}") 2055 raise RuntimeError() 2056 2057 self.alpha(0.1) 2058 self.vertices = coords 2059 return self 2060 2061 def warp(self, source, target, sigma=1.0, mode="3d") -> Self: 2062 """ 2063 "Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set 2064 of source and target landmarks. Any point on the mesh close to a source landmark will 2065 be moved to a place close to the corresponding target landmark. 2066 The points in between are interpolated smoothly using 2067 Bookstein's Thin Plate Spline algorithm. 2068 2069 Transformation object can be accessed with `mesh.transform`. 2070 2071 Arguments: 2072 sigma : (float) 2073 specify the 'stiffness' of the spline. 2074 mode : (str) 2075 set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes) 2076 2077 Examples: 2078 - [interpolate_field.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/interpolate_field.py) 2079 - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py) 2080 - [warp2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp2.py) 2081 - [warp3.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp3.py) 2082 - [warp4a.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4a.py) 2083 - [warp4b.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4b.py) 2084 - [warp6.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp6.py) 2085 2086 ![](https://vedo.embl.es/images/advanced/warp2.png) 2087 """ 2088 parents = [self] 2089 2090 try: 2091 source = source.vertices 2092 parents.append(source) 2093 except AttributeError: 2094 source = utils.make3d(source) 2095 2096 try: 2097 target = target.vertices 2098 parents.append(target) 2099 except AttributeError: 2100 target = utils.make3d(target) 2101 2102 ns = len(source) 2103 nt = len(target) 2104 if ns != nt: 2105 vedo.logger.error(f"#source {ns} != {nt} #target points") 2106 raise RuntimeError() 2107 2108 NLT = NonLinearTransform() 2109 NLT.source_points = source 2110 NLT.target_points = target 2111 self.apply_transform(NLT) 2112 2113 self.pipeline = utils.OperationNode("warp", parents=parents) 2114 return self 2115 2116 def cut_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0), invert=False) -> Self: 2117 """ 2118 Cut the mesh with the plane defined by a point and a normal. 2119 2120 Arguments: 2121 origin : (array) 2122 the cutting plane goes through this point 2123 normal : (array) 2124 normal of the cutting plane 2125 2126 Example: 2127 ```python 2128 from vedo import Cube 2129 cube = Cube().cut_with_plane(normal=(1,1,1)) 2130 cube.back_color('pink').show().close() 2131 ``` 2132 ![](https://vedo.embl.es/images/feats/cut_with_plane_cube.png) 2133 2134 Examples: 2135 - [trail.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/trail.py) 2136 2137 ![](https://vedo.embl.es/images/simulations/trail.gif) 2138 2139 Check out also: 2140 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`. 2141 """ 2142 s = str(normal) 2143 if "x" in s: 2144 normal = (1, 0, 0) 2145 if "-" in s: 2146 normal = -np.array(normal) 2147 elif "y" in s: 2148 normal = (0, 1, 0) 2149 if "-" in s: 2150 normal = -np.array(normal) 2151 elif "z" in s: 2152 normal = (0, 0, 1) 2153 if "-" in s: 2154 normal = -np.array(normal) 2155 plane = vtki.vtkPlane() 2156 plane.SetOrigin(origin) 2157 plane.SetNormal(normal) 2158 2159 clipper = vtki.new("ClipPolyData") 2160 clipper.SetInputData(self.dataset) 2161 clipper.SetClipFunction(plane) 2162 clipper.GenerateClippedOutputOff() 2163 clipper.GenerateClipScalarsOff() 2164 clipper.SetInsideOut(invert) 2165 clipper.SetValue(0) 2166 clipper.Update() 2167 2168 self._update(clipper.GetOutput()) 2169 2170 self.pipeline = utils.OperationNode("cut_with_plane", parents=[self]) 2171 return self 2172 2173 def cut_with_planes(self, origins, normals, invert=False) -> Self: 2174 """ 2175 Cut the mesh with a convex set of planes defined by points and normals. 2176 2177 Arguments: 2178 origins : (array) 2179 each cutting plane goes through this point 2180 normals : (array) 2181 normal of each of the cutting planes 2182 invert : (bool) 2183 if True, cut outside instead of inside 2184 2185 Check out also: 2186 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()` 2187 """ 2188 2189 vpoints = vtki.vtkPoints() 2190 for p in utils.make3d(origins): 2191 vpoints.InsertNextPoint(p) 2192 normals = utils.make3d(normals) 2193 2194 planes = vtki.vtkPlanes() 2195 planes.SetPoints(vpoints) 2196 planes.SetNormals(utils.numpy2vtk(normals, dtype=float)) 2197 2198 clipper = vtki.new("ClipPolyData") 2199 clipper.SetInputData(self.dataset) 2200 clipper.SetInsideOut(invert) 2201 clipper.SetClipFunction(planes) 2202 clipper.GenerateClippedOutputOff() 2203 clipper.GenerateClipScalarsOff() 2204 clipper.SetValue(0) 2205 clipper.Update() 2206 2207 self._update(clipper.GetOutput()) 2208 2209 self.pipeline = utils.OperationNode("cut_with_planes", parents=[self]) 2210 return self 2211 2212 def cut_with_box(self, bounds, invert=False) -> Self: 2213 """ 2214 Cut the current mesh with a box or a set of boxes. 2215 This is much faster than `cut_with_mesh()`. 2216 2217 Input `bounds` can be either: 2218 - a Mesh or Points object 2219 - a list of 6 number representing a bounding box `[xmin,xmax, ymin,ymax, zmin,zmax]` 2220 - a list of bounding boxes like the above: `[[xmin1,...], [xmin2,...], ...]` 2221 2222 Example: 2223 ```python 2224 from vedo import Sphere, Cube, show 2225 mesh = Sphere(r=1, res=50) 2226 box = Cube(side=1.5).wireframe() 2227 mesh.cut_with_box(box) 2228 show(mesh, box, axes=1).close() 2229 ``` 2230 ![](https://vedo.embl.es/images/feats/cut_with_box_cube.png) 2231 2232 Check out also: 2233 `cut_with_line()`, `cut_with_plane()`, `cut_with_cylinder()` 2234 """ 2235 if isinstance(bounds, Points): 2236 bounds = bounds.bounds() 2237 2238 box = vtki.new("Box") 2239 if utils.is_sequence(bounds[0]): 2240 for bs in bounds: 2241 box.AddBounds(bs) 2242 else: 2243 box.SetBounds(bounds) 2244 2245 clipper = vtki.new("ClipPolyData") 2246 clipper.SetInputData(self.dataset) 2247 clipper.SetClipFunction(box) 2248 clipper.SetInsideOut(not invert) 2249 clipper.GenerateClippedOutputOff() 2250 clipper.GenerateClipScalarsOff() 2251 clipper.SetValue(0) 2252 clipper.Update() 2253 self._update(clipper.GetOutput()) 2254 2255 self.pipeline = utils.OperationNode("cut_with_box", parents=[self]) 2256 return self 2257 2258 def cut_with_line(self, points, invert=False, closed=True) -> Self: 2259 """ 2260 Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter. 2261 The polyline is defined by a set of points (z-coordinates are ignored). 2262 This is much faster than `cut_with_mesh()`. 2263 2264 Check out also: 2265 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2266 """ 2267 pplane = vtki.new("PolyPlane") 2268 if isinstance(points, Points): 2269 points = points.vertices.tolist() 2270 2271 if closed: 2272 if isinstance(points, np.ndarray): 2273 points = points.tolist() 2274 points.append(points[0]) 2275 2276 vpoints = vtki.vtkPoints() 2277 for p in points: 2278 if len(p) == 2: 2279 p = [p[0], p[1], 0.0] 2280 vpoints.InsertNextPoint(p) 2281 2282 n = len(points) 2283 polyline = vtki.new("PolyLine") 2284 polyline.Initialize(n, vpoints) 2285 polyline.GetPointIds().SetNumberOfIds(n) 2286 for i in range(n): 2287 polyline.GetPointIds().SetId(i, i) 2288 pplane.SetPolyLine(polyline) 2289 2290 clipper = vtki.new("ClipPolyData") 2291 clipper.SetInputData(self.dataset) 2292 clipper.SetClipFunction(pplane) 2293 clipper.SetInsideOut(invert) 2294 clipper.GenerateClippedOutputOff() 2295 clipper.GenerateClipScalarsOff() 2296 clipper.SetValue(0) 2297 clipper.Update() 2298 self._update(clipper.GetOutput()) 2299 2300 self.pipeline = utils.OperationNode("cut_with_line", parents=[self]) 2301 return self 2302 2303 def cut_with_cookiecutter(self, lines) -> Self: 2304 """ 2305 Cut the current mesh with a single line or a set of lines. 2306 2307 Input `lines` can be either: 2308 - a `Mesh` or `Points` object 2309 - a list of 3D points: `[(x1,y1,z1), (x2,y2,z2), ...]` 2310 - a list of 2D points: `[(x1,y1), (x2,y2), ...]` 2311 2312 Example: 2313 ```python 2314 from vedo import * 2315 grid = Mesh(dataurl + "dolfin_fine.vtk") 2316 grid.compute_quality().cmap("Greens") 2317 pols = merge( 2318 Polygon(nsides=10, r=0.3).pos(0.7, 0.3), 2319 Polygon(nsides=10, r=0.2).pos(0.3, 0.7), 2320 ) 2321 lines = pols.boundaries() 2322 cgrid = grid.clone().cut_with_cookiecutter(lines) 2323 grid.alpha(0.1).wireframe() 2324 show(grid, cgrid, lines, axes=8, bg='blackboard').close() 2325 ``` 2326 ![](https://vedo.embl.es/images/feats/cookiecutter.png) 2327 2328 Check out also: 2329 `cut_with_line()` and `cut_with_point_loop()` 2330 2331 Note: 2332 In case of a warning message like: 2333 "Mesh and trim loop point data attributes are different" 2334 consider interpolating the mesh point data to the loop points, 2335 Eg. (in the above example): 2336 ```python 2337 lines = pols.boundaries().interpolate_data_from(grid, n=2) 2338 ``` 2339 2340 Note: 2341 trying to invert the selection by reversing the loop order 2342 will have no effect in this method, hence it does not have 2343 the `invert` option. 2344 """ 2345 if utils.is_sequence(lines): 2346 lines = utils.make3d(lines) 2347 iline = list(range(len(lines))) + [0] 2348 poly = utils.buildPolyData(lines, lines=[iline]) 2349 else: 2350 poly = lines.dataset 2351 2352 # if invert: # not working 2353 # rev = vtki.new("ReverseSense") 2354 # rev.ReverseCellsOn() 2355 # rev.SetInputData(poly) 2356 # rev.Update() 2357 # poly = rev.GetOutput() 2358 2359 # Build loops from the polyline 2360 build_loops = vtki.new("ContourLoopExtraction") 2361 build_loops.SetGlobalWarningDisplay(0) 2362 build_loops.SetInputData(poly) 2363 build_loops.Update() 2364 boundary_poly = build_loops.GetOutput() 2365 2366 ccut = vtki.new("CookieCutter") 2367 ccut.SetInputData(self.dataset) 2368 ccut.SetLoopsData(boundary_poly) 2369 ccut.SetPointInterpolationToMeshEdges() 2370 # ccut.SetPointInterpolationToLoopEdges() 2371 ccut.PassCellDataOn() 2372 ccut.PassPointDataOn() 2373 ccut.Update() 2374 self._update(ccut.GetOutput()) 2375 2376 self.pipeline = utils.OperationNode("cut_with_cookiecutter", parents=[self]) 2377 return self 2378 2379 def cut_with_cylinder(self, center=(0, 0, 0), axis=(0, 0, 1), r=1, invert=False) -> Self: 2380 """ 2381 Cut the current mesh with an infinite cylinder. 2382 This is much faster than `cut_with_mesh()`. 2383 2384 Arguments: 2385 center : (array) 2386 the center of the cylinder 2387 normal : (array) 2388 direction of the cylinder axis 2389 r : (float) 2390 radius of the cylinder 2391 2392 Example: 2393 ```python 2394 from vedo import Disc, show 2395 disc = Disc(r1=1, r2=1.2) 2396 mesh = disc.extrude(3, res=50).linewidth(1) 2397 mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True) 2398 show(mesh, axes=1).close() 2399 ``` 2400 ![](https://vedo.embl.es/images/feats/cut_with_cylinder.png) 2401 2402 Examples: 2403 - [optics_main1.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/optics_main1.py) 2404 2405 Check out also: 2406 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2407 """ 2408 s = str(axis) 2409 if "x" in s: 2410 axis = (1, 0, 0) 2411 elif "y" in s: 2412 axis = (0, 1, 0) 2413 elif "z" in s: 2414 axis = (0, 0, 1) 2415 cyl = vtki.new("Cylinder") 2416 cyl.SetCenter(center) 2417 cyl.SetAxis(axis[0], axis[1], axis[2]) 2418 cyl.SetRadius(r) 2419 2420 clipper = vtki.new("ClipPolyData") 2421 clipper.SetInputData(self.dataset) 2422 clipper.SetClipFunction(cyl) 2423 clipper.SetInsideOut(not invert) 2424 clipper.GenerateClippedOutputOff() 2425 clipper.GenerateClipScalarsOff() 2426 clipper.SetValue(0) 2427 clipper.Update() 2428 self._update(clipper.GetOutput()) 2429 2430 self.pipeline = utils.OperationNode("cut_with_cylinder", parents=[self]) 2431 return self 2432 2433 def cut_with_sphere(self, center=(0, 0, 0), r=1.0, invert=False) -> Self: 2434 """ 2435 Cut the current mesh with an sphere. 2436 This is much faster than `cut_with_mesh()`. 2437 2438 Arguments: 2439 center : (array) 2440 the center of the sphere 2441 r : (float) 2442 radius of the sphere 2443 2444 Example: 2445 ```python 2446 from vedo import Disc, show 2447 disc = Disc(r1=1, r2=1.2) 2448 mesh = disc.extrude(3, res=50).linewidth(1) 2449 mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True) 2450 show(mesh, axes=1).close() 2451 ``` 2452 ![](https://vedo.embl.es/images/feats/cut_with_sphere.png) 2453 2454 Check out also: 2455 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2456 """ 2457 sph = vtki.new("Sphere") 2458 sph.SetCenter(center) 2459 sph.SetRadius(r) 2460 2461 clipper = vtki.new("ClipPolyData") 2462 clipper.SetInputData(self.dataset) 2463 clipper.SetClipFunction(sph) 2464 clipper.SetInsideOut(not invert) 2465 clipper.GenerateClippedOutputOff() 2466 clipper.GenerateClipScalarsOff() 2467 clipper.SetValue(0) 2468 clipper.Update() 2469 self._update(clipper.GetOutput()) 2470 self.pipeline = utils.OperationNode("cut_with_sphere", parents=[self]) 2471 return self 2472 2473 def cut_with_mesh(self, mesh, invert=False, keep=False) -> Union[Self, "vedo.Assembly"]: 2474 """ 2475 Cut an `Mesh` mesh with another `Mesh`. 2476 2477 Use `invert` to invert the selection. 2478 2479 Use `keep` to keep the cutoff part, in this case an `Assembly` is returned: 2480 the "cut" object and the "discarded" part of the original object. 2481 You can access both via `assembly.unpack()` method. 2482 2483 Example: 2484 ```python 2485 from vedo import * 2486 arr = np.random.randn(100000, 3)/2 2487 pts = Points(arr).c('red3').pos(5,0,0) 2488 cube = Cube().pos(4,0.5,0) 2489 assem = pts.cut_with_mesh(cube, keep=True) 2490 show(assem.unpack(), axes=1).close() 2491 ``` 2492 ![](https://vedo.embl.es/images/feats/cut_with_mesh.png) 2493 2494 Check out also: 2495 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2496 """ 2497 polymesh = mesh.dataset 2498 poly = self.dataset 2499 2500 # Create an array to hold distance information 2501 signed_distances = vtki.vtkFloatArray() 2502 signed_distances.SetNumberOfComponents(1) 2503 signed_distances.SetName("SignedDistances") 2504 2505 # implicit function that will be used to slice the mesh 2506 ippd = vtki.new("ImplicitPolyDataDistance") 2507 ippd.SetInput(polymesh) 2508 2509 # Evaluate the signed distance function at all of the grid points 2510 for pointId in range(poly.GetNumberOfPoints()): 2511 p = poly.GetPoint(pointId) 2512 signed_distance = ippd.EvaluateFunction(p) 2513 signed_distances.InsertNextValue(signed_distance) 2514 2515 currentscals = poly.GetPointData().GetScalars() 2516 if currentscals: 2517 currentscals = currentscals.GetName() 2518 2519 poly.GetPointData().AddArray(signed_distances) 2520 poly.GetPointData().SetActiveScalars("SignedDistances") 2521 2522 clipper = vtki.new("ClipPolyData") 2523 clipper.SetInputData(poly) 2524 clipper.SetInsideOut(not invert) 2525 clipper.SetGenerateClippedOutput(keep) 2526 clipper.SetValue(0.0) 2527 clipper.Update() 2528 cpoly = clipper.GetOutput() 2529 2530 if keep: 2531 kpoly = clipper.GetOutput(1) 2532 2533 vis = False 2534 if currentscals: 2535 cpoly.GetPointData().SetActiveScalars(currentscals) 2536 vis = self.mapper.GetScalarVisibility() 2537 2538 self._update(cpoly) 2539 2540 self.pointdata.remove("SignedDistances") 2541 self.mapper.SetScalarVisibility(vis) 2542 if keep: 2543 if isinstance(self, vedo.Mesh): 2544 cutoff = vedo.Mesh(kpoly) 2545 else: 2546 cutoff = vedo.Points(kpoly) 2547 # cutoff = self.__class__(kpoly) # this does not work properly 2548 cutoff.properties = vtki.vtkProperty() 2549 cutoff.properties.DeepCopy(self.properties) 2550 cutoff.actor.SetProperty(cutoff.properties) 2551 cutoff.c("k5").alpha(0.2) 2552 return vedo.Assembly([self, cutoff]) 2553 2554 self.pipeline = utils.OperationNode("cut_with_mesh", parents=[self, mesh]) 2555 return self 2556 2557 def cut_with_point_loop( 2558 self, points, invert=False, on="points", include_boundary=False 2559 ) -> Self: 2560 """ 2561 Cut an `Mesh` object with a set of points forming a closed loop. 2562 2563 Arguments: 2564 invert : (bool) 2565 invert selection (inside-out) 2566 on : (str) 2567 if 'cells' will extract the whole cells lying inside (or outside) the point loop 2568 include_boundary : (bool) 2569 include cells lying exactly on the boundary line. Only relevant on 'cells' mode 2570 2571 Examples: 2572 - [cut_with_points1.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points1.py) 2573 2574 ![](https://vedo.embl.es/images/advanced/cutWithPoints1.png) 2575 2576 - [cut_with_points2.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points2.py) 2577 2578 ![](https://vedo.embl.es/images/advanced/cutWithPoints2.png) 2579 """ 2580 if isinstance(points, Points): 2581 parents = [points] 2582 vpts = points.dataset.GetPoints() 2583 points = points.vertices 2584 else: 2585 parents = [self] 2586 vpts = vtki.vtkPoints() 2587 points = utils.make3d(points) 2588 for p in points: 2589 vpts.InsertNextPoint(p) 2590 2591 if "cell" in on: 2592 ippd = vtki.new("ImplicitSelectionLoop") 2593 ippd.SetLoop(vpts) 2594 ippd.AutomaticNormalGenerationOn() 2595 clipper = vtki.new("ExtractPolyDataGeometry") 2596 clipper.SetInputData(self.dataset) 2597 clipper.SetImplicitFunction(ippd) 2598 clipper.SetExtractInside(not invert) 2599 clipper.SetExtractBoundaryCells(include_boundary) 2600 else: 2601 spol = vtki.new("SelectPolyData") 2602 spol.SetLoop(vpts) 2603 spol.GenerateSelectionScalarsOn() 2604 spol.GenerateUnselectedOutputOff() 2605 spol.SetInputData(self.dataset) 2606 spol.Update() 2607 clipper = vtki.new("ClipPolyData") 2608 clipper.SetInputData(spol.GetOutput()) 2609 clipper.SetInsideOut(not invert) 2610 clipper.SetValue(0.0) 2611 clipper.Update() 2612 self._update(clipper.GetOutput()) 2613 2614 self.pipeline = utils.OperationNode("cut_with_pointloop", parents=parents) 2615 return self 2616 2617 def cut_with_scalar(self, value: float, name="", invert=False) -> Self: 2618 """ 2619 Cut a mesh or point cloud with some input scalar point-data. 2620 2621 Arguments: 2622 value : (float) 2623 cutting value 2624 name : (str) 2625 array name of the scalars to be used 2626 invert : (bool) 2627 flip selection 2628 2629 Example: 2630 ```python 2631 from vedo import * 2632 s = Sphere().lw(1) 2633 pts = s.vertices 2634 scalars = np.sin(3*pts[:,2]) + pts[:,0] 2635 s.pointdata["somevalues"] = scalars 2636 s.cut_with_scalar(0.3) 2637 s.cmap("Spectral", "somevalues").add_scalarbar() 2638 s.show(axes=1).close() 2639 ``` 2640 ![](https://vedo.embl.es/images/feats/cut_with_scalars.png) 2641 """ 2642 if name: 2643 self.pointdata.select(name) 2644 clipper = vtki.new("ClipPolyData") 2645 clipper.SetInputData(self.dataset) 2646 clipper.SetValue(value) 2647 clipper.GenerateClippedOutputOff() 2648 clipper.SetInsideOut(not invert) 2649 clipper.Update() 2650 self._update(clipper.GetOutput()) 2651 self.pipeline = utils.OperationNode("cut_with_scalar", parents=[self]) 2652 return self 2653 2654 def crop(self, 2655 top=None, bottom=None, right=None, left=None, front=None, back=None, 2656 bounds=()) -> Self: 2657 """ 2658 Crop an `Mesh` object. 2659 2660 Arguments: 2661 top : (float) 2662 fraction to crop from the top plane (positive z) 2663 bottom : (float) 2664 fraction to crop from the bottom plane (negative z) 2665 front : (float) 2666 fraction to crop from the front plane (positive y) 2667 back : (float) 2668 fraction to crop from the back plane (negative y) 2669 right : (float) 2670 fraction to crop from the right plane (positive x) 2671 left : (float) 2672 fraction to crop from the left plane (negative x) 2673 bounds : (list) 2674 bounding box of the crop region as `[x0,x1, y0,y1, z0,z1]` 2675 2676 Example: 2677 ```python 2678 from vedo import Sphere 2679 Sphere().crop(right=0.3, left=0.1).show() 2680 ``` 2681 ![](https://user-images.githubusercontent.com/32848391/57081955-0ef1e800-6cf6-11e9-99de-b45220939bc9.png) 2682 """ 2683 if not len(bounds): 2684 pos = np.array(self.pos()) 2685 x0, x1, y0, y1, z0, z1 = self.bounds() 2686 x0, y0, z0 = [x0, y0, z0] - pos 2687 x1, y1, z1 = [x1, y1, z1] - pos 2688 2689 dx, dy, dz = x1 - x0, y1 - y0, z1 - z0 2690 if top: 2691 z1 = z1 - top * dz 2692 if bottom: 2693 z0 = z0 + bottom * dz 2694 if front: 2695 y1 = y1 - front * dy 2696 if back: 2697 y0 = y0 + back * dy 2698 if right: 2699 x1 = x1 - right * dx 2700 if left: 2701 x0 = x0 + left * dx 2702 bounds = (x0, x1, y0, y1, z0, z1) 2703 2704 cu = vtki.new("Box") 2705 cu.SetBounds(bounds) 2706 2707 clipper = vtki.new("ClipPolyData") 2708 clipper.SetInputData(self.dataset) 2709 clipper.SetClipFunction(cu) 2710 clipper.InsideOutOn() 2711 clipper.GenerateClippedOutputOff() 2712 clipper.GenerateClipScalarsOff() 2713 clipper.SetValue(0) 2714 clipper.Update() 2715 self._update(clipper.GetOutput()) 2716 2717 self.pipeline = utils.OperationNode( 2718 "crop", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 2719 ) 2720 return self 2721 2722 def generate_surface_halo( 2723 self, 2724 distance=0.05, 2725 res=(50, 50, 50), 2726 bounds=(), 2727 maxdist=None, 2728 ) -> "vedo.Mesh": 2729 """ 2730 Generate the surface halo which sits at the specified distance from the input one. 2731 2732 Arguments: 2733 distance : (float) 2734 distance from the input surface 2735 res : (int) 2736 resolution of the surface 2737 bounds : (list) 2738 bounding box of the surface 2739 maxdist : (float) 2740 maximum distance to generate the surface 2741 """ 2742 if not bounds: 2743 bounds = self.bounds() 2744 2745 if not maxdist: 2746 maxdist = self.diagonal_size() / 2 2747 2748 imp = vtki.new("ImplicitModeller") 2749 imp.SetInputData(self.dataset) 2750 imp.SetSampleDimensions(res) 2751 if maxdist: 2752 imp.SetMaximumDistance(maxdist) 2753 if len(bounds) == 6: 2754 imp.SetModelBounds(bounds) 2755 contour = vtki.new("ContourFilter") 2756 contour.SetInputConnection(imp.GetOutputPort()) 2757 contour.SetValue(0, distance) 2758 contour.Update() 2759 out = vedo.Mesh(contour.GetOutput()) 2760 out.c("lightblue").alpha(0.25).lighting("off") 2761 out.pipeline = utils.OperationNode("generate_surface_halo", parents=[self]) 2762 return out 2763 2764 def generate_mesh( 2765 self, 2766 line_resolution=None, 2767 mesh_resolution=None, 2768 smooth=0.0, 2769 jitter=0.001, 2770 grid=None, 2771 quads=False, 2772 invert=False, 2773 ) -> Self: 2774 """ 2775 Generate a polygonal Mesh from a closed contour line. 2776 If line is not closed it will be closed with a straight segment. 2777 2778 Check also `generate_delaunay2d()`. 2779 2780 Arguments: 2781 line_resolution : (int) 2782 resolution of the contour line. The default is None, in this case 2783 the contour is not resampled. 2784 mesh_resolution : (int) 2785 resolution of the internal triangles not touching the boundary. 2786 smooth : (float) 2787 smoothing of the contour before meshing. 2788 jitter : (float) 2789 add a small noise to the internal points. 2790 grid : (Grid) 2791 manually pass a Grid object. The default is True. 2792 quads : (bool) 2793 generate a mesh of quads instead of triangles. 2794 invert : (bool) 2795 flip the line orientation. The default is False. 2796 2797 Examples: 2798 - [line2mesh_tri.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_tri.py) 2799 2800 ![](https://vedo.embl.es/images/advanced/line2mesh_tri.jpg) 2801 2802 - [line2mesh_quads.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_quads.py) 2803 2804 ![](https://vedo.embl.es/images/advanced/line2mesh_quads.png) 2805 """ 2806 if line_resolution is None: 2807 contour = vedo.shapes.Line(self.vertices) 2808 else: 2809 contour = vedo.shapes.Spline(self.vertices, smooth=smooth, res=line_resolution) 2810 contour.clean() 2811 2812 length = contour.length() 2813 density = length / contour.npoints 2814 # print(f"tomesh():\n\tline length = {length}") 2815 # print(f"\tdensity = {density} length/pt_separation") 2816 2817 x0, x1 = contour.xbounds() 2818 y0, y1 = contour.ybounds() 2819 2820 if grid is None: 2821 if mesh_resolution is None: 2822 resx = int((x1 - x0) / density + 0.5) 2823 resy = int((y1 - y0) / density + 0.5) 2824 # print(f"tmesh_resolution = {[resx, resy]}") 2825 else: 2826 if utils.is_sequence(mesh_resolution): 2827 resx, resy = mesh_resolution 2828 else: 2829 resx, resy = mesh_resolution, mesh_resolution 2830 grid = vedo.shapes.Grid( 2831 [(x0 + x1) / 2, (y0 + y1) / 2, 0], 2832 s=((x1 - x0) * 1.025, (y1 - y0) * 1.025), 2833 res=(resx, resy), 2834 ) 2835 else: 2836 grid = grid.clone() 2837 2838 cpts = contour.vertices 2839 2840 # make sure it's closed 2841 p0, p1 = cpts[0], cpts[-1] 2842 nj = max(2, int(utils.mag(p1 - p0) / density + 0.5)) 2843 joinline = vedo.shapes.Line(p1, p0, res=nj) 2844 contour = vedo.merge(contour, joinline).subsample(0.0001) 2845 2846 ####################################### quads 2847 if quads: 2848 cmesh = grid.clone().cut_with_point_loop(contour, on="cells", invert=invert) 2849 cmesh.wireframe(False).lw(0.5) 2850 cmesh.pipeline = utils.OperationNode( 2851 "generate_mesh", 2852 parents=[self, contour], 2853 comment=f"#quads {cmesh.dataset.GetNumberOfCells()}", 2854 ) 2855 return cmesh 2856 ############################################# 2857 2858 grid_tmp = grid.vertices.copy() 2859 2860 if jitter: 2861 np.random.seed(0) 2862 sigma = 1.0 / np.sqrt(grid.npoints) * grid.diagonal_size() * jitter 2863 # print(f"\tsigma jittering = {sigma}") 2864 grid_tmp += np.random.rand(grid.npoints, 3) * sigma 2865 grid_tmp[:, 2] = 0.0 2866 2867 todel = [] 2868 density /= np.sqrt(3) 2869 vgrid_tmp = Points(grid_tmp) 2870 2871 for p in contour.vertices: 2872 out = vgrid_tmp.closest_point(p, radius=density, return_point_id=True) 2873 todel += out.tolist() 2874 2875 grid_tmp = grid_tmp.tolist() 2876 for index in sorted(list(set(todel)), reverse=True): 2877 del grid_tmp[index] 2878 2879 points = contour.vertices.tolist() + grid_tmp 2880 if invert: 2881 boundary = list(reversed(range(contour.npoints))) 2882 else: 2883 boundary = list(range(contour.npoints)) 2884 2885 dln = Points(points).generate_delaunay2d(mode="xy", boundaries=[boundary]) 2886 dln.compute_normals(points=False) # fixes reversd faces 2887 dln.lw(1) 2888 2889 dln.pipeline = utils.OperationNode( 2890 "generate_mesh", 2891 parents=[self, contour], 2892 comment=f"#cells {dln.dataset.GetNumberOfCells()}", 2893 ) 2894 return dln 2895 2896 def reconstruct_surface( 2897 self, 2898 dims=(100, 100, 100), 2899 radius=None, 2900 sample_size=None, 2901 hole_filling=True, 2902 bounds=(), 2903 padding=0.05, 2904 ) -> "vedo.Mesh": 2905 """ 2906 Surface reconstruction from a scattered cloud of points. 2907 2908 Arguments: 2909 dims : (int) 2910 number of voxels in x, y and z to control precision. 2911 radius : (float) 2912 radius of influence of each point. 2913 Smaller values generally improve performance markedly. 2914 Note that after the signed distance function is computed, 2915 any voxel taking on the value >= radius 2916 is presumed to be "unseen" or uninitialized. 2917 sample_size : (int) 2918 if normals are not present 2919 they will be calculated using this sample size per point. 2920 hole_filling : (bool) 2921 enables hole filling, this generates 2922 separating surfaces between the empty and unseen portions of the volume. 2923 bounds : (list) 2924 region in space in which to perform the sampling 2925 in format (xmin,xmax, ymin,ymax, zim, zmax) 2926 padding : (float) 2927 increase by this fraction the bounding box 2928 2929 Examples: 2930 - [recosurface.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/recosurface.py) 2931 2932 ![](https://vedo.embl.es/images/advanced/recosurface.png) 2933 """ 2934 if not utils.is_sequence(dims): 2935 dims = (dims, dims, dims) 2936 2937 sdf = vtki.new("SignedDistance") 2938 2939 if len(bounds) == 6: 2940 sdf.SetBounds(bounds) 2941 else: 2942 x0, x1, y0, y1, z0, z1 = self.bounds() 2943 sdf.SetBounds( 2944 x0 - (x1 - x0) * padding, 2945 x1 + (x1 - x0) * padding, 2946 y0 - (y1 - y0) * padding, 2947 y1 + (y1 - y0) * padding, 2948 z0 - (z1 - z0) * padding, 2949 z1 + (z1 - z0) * padding, 2950 ) 2951 2952 bb = sdf.GetBounds() 2953 if bb[0]==bb[1]: 2954 vedo.logger.warning("reconstruct_surface(): zero x-range") 2955 if bb[2]==bb[3]: 2956 vedo.logger.warning("reconstruct_surface(): zero y-range") 2957 if bb[4]==bb[5]: 2958 vedo.logger.warning("reconstruct_surface(): zero z-range") 2959 2960 pd = self.dataset 2961 2962 if pd.GetPointData().GetNormals(): 2963 sdf.SetInputData(pd) 2964 else: 2965 normals = vtki.new("PCANormalEstimation") 2966 normals.SetInputData(pd) 2967 if not sample_size: 2968 sample_size = int(pd.GetNumberOfPoints() / 50) 2969 normals.SetSampleSize(sample_size) 2970 normals.SetNormalOrientationToGraphTraversal() 2971 sdf.SetInputConnection(normals.GetOutputPort()) 2972 # print("Recalculating normals with sample size =", sample_size) 2973 2974 if radius is None: 2975 radius = self.diagonal_size() / (sum(dims) / 3) * 5 2976 # print("Calculating mesh from points with radius =", radius) 2977 2978 sdf.SetRadius(radius) 2979 sdf.SetDimensions(dims) 2980 sdf.Update() 2981 2982 surface = vtki.new("ExtractSurface") 2983 surface.SetRadius(radius * 0.99) 2984 surface.SetHoleFilling(hole_filling) 2985 surface.ComputeNormalsOff() 2986 surface.ComputeGradientsOff() 2987 surface.SetInputConnection(sdf.GetOutputPort()) 2988 surface.Update() 2989 m = vedo.mesh.Mesh(surface.GetOutput(), c=self.color()) 2990 2991 m.pipeline = utils.OperationNode( 2992 "reconstruct_surface", 2993 parents=[self], 2994 comment=f"#pts {m.dataset.GetNumberOfPoints()}", 2995 ) 2996 return m 2997 2998 def compute_clustering(self, radius: float) -> Self: 2999 """ 3000 Cluster points in space. The `radius` is the radius of local search. 3001 3002 An array named "ClusterId" is added to `pointdata`. 3003 3004 Examples: 3005 - [clustering.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/clustering.py) 3006 3007 ![](https://vedo.embl.es/images/basic/clustering.png) 3008 """ 3009 cluster = vtki.new("EuclideanClusterExtraction") 3010 cluster.SetInputData(self.dataset) 3011 cluster.SetExtractionModeToAllClusters() 3012 cluster.SetRadius(radius) 3013 cluster.ColorClustersOn() 3014 cluster.Update() 3015 idsarr = cluster.GetOutput().GetPointData().GetArray("ClusterId") 3016 self.dataset.GetPointData().AddArray(idsarr) 3017 self.pipeline = utils.OperationNode( 3018 "compute_clustering", parents=[self], comment=f"radius = {radius}" 3019 ) 3020 return self 3021 3022 def compute_connections(self, radius, mode=0, regions=(), vrange=(0, 1), seeds=(), angle=0.0) -> Self: 3023 """ 3024 Extracts and/or segments points from a point cloud based on geometric distance measures 3025 (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range. 3026 The default operation is to segment the points into "connected" regions where the connection 3027 is determined by an appropriate distance measure. Each region is given a region id. 3028 3029 Optionally, the filter can output the largest connected region of points; a particular region 3030 (via id specification); those regions that are seeded using a list of input point ids; 3031 or the region of points closest to a specified position. 3032 3033 The key parameter of this filter is the radius defining a sphere around each point which defines 3034 a local neighborhood: any other points in the local neighborhood are assumed connected to the point. 3035 Note that the radius is defined in absolute terms. 3036 3037 Other parameters are used to further qualify what it means to be a neighboring point. 3038 For example, scalar range and/or point normals can be used to further constrain the neighborhood. 3039 Also the extraction mode defines how the filter operates. 3040 By default, all regions are extracted but it is possible to extract particular regions; 3041 the region closest to a seed point; seeded regions; or the largest region found while processing. 3042 By default, all regions are extracted. 3043 3044 On output, all points are labeled with a region number. 3045 However note that the number of input and output points may not be the same: 3046 if not extracting all regions then the output size may be less than the input size. 3047 3048 Arguments: 3049 radius : (float) 3050 variable specifying a local sphere used to define local point neighborhood 3051 mode : (int) 3052 - 0, Extract all regions 3053 - 1, Extract point seeded regions 3054 - 2, Extract largest region 3055 - 3, Test specified regions 3056 - 4, Extract all regions with scalar connectivity 3057 - 5, Extract point seeded regions 3058 regions : (list) 3059 a list of non-negative regions id to extract 3060 vrange : (list) 3061 scalar range to use to extract points based on scalar connectivity 3062 seeds : (list) 3063 a list of non-negative point seed ids 3064 angle : (list) 3065 points are connected if the angle between their normals is 3066 within this angle threshold (expressed in degrees). 3067 """ 3068 # https://vtk.org/doc/nightly/html/classvtkConnectedPointsFilter.html 3069 cpf = vtki.new("ConnectedPointsFilter") 3070 cpf.SetInputData(self.dataset) 3071 cpf.SetRadius(radius) 3072 if mode == 0: # Extract all regions 3073 pass 3074 3075 elif mode == 1: # Extract point seeded regions 3076 cpf.SetExtractionModeToPointSeededRegions() 3077 for s in seeds: 3078 cpf.AddSeed(s) 3079 3080 elif mode == 2: # Test largest region 3081 cpf.SetExtractionModeToLargestRegion() 3082 3083 elif mode == 3: # Test specified regions 3084 cpf.SetExtractionModeToSpecifiedRegions() 3085 for r in regions: 3086 cpf.AddSpecifiedRegion(r) 3087 3088 elif mode == 4: # Extract all regions with scalar connectivity 3089 cpf.SetExtractionModeToLargestRegion() 3090 cpf.ScalarConnectivityOn() 3091 cpf.SetScalarRange(vrange[0], vrange[1]) 3092 3093 elif mode == 5: # Extract point seeded regions 3094 cpf.SetExtractionModeToLargestRegion() 3095 cpf.ScalarConnectivityOn() 3096 cpf.SetScalarRange(vrange[0], vrange[1]) 3097 cpf.AlignedNormalsOn() 3098 cpf.SetNormalAngle(angle) 3099 3100 cpf.Update() 3101 self._update(cpf.GetOutput(), reset_locators=False) 3102 return self 3103 3104 def compute_camera_distance(self) -> np.ndarray: 3105 """ 3106 Calculate the distance from points to the camera. 3107 3108 A pointdata array is created with name 'DistanceToCamera' and returned. 3109 """ 3110 if vedo.plotter_instance and vedo.plotter_instance.renderer: 3111 poly = self.dataset 3112 dc = vtki.new("DistanceToCamera") 3113 dc.SetInputData(poly) 3114 dc.SetRenderer(vedo.plotter_instance.renderer) 3115 dc.Update() 3116 self._update(dc.GetOutput(), reset_locators=False) 3117 return self.pointdata["DistanceToCamera"] 3118 return np.array([]) 3119 3120 def densify(self, target_distance=0.1, nclosest=6, radius=None, niter=1, nmax=None) -> Self: 3121 """ 3122 Return a copy of the cloud with new added points. 3123 The new points are created in such a way that all points in any local neighborhood are 3124 within a target distance of one another. 3125 3126 For each input point, the distance to all points in its neighborhood is computed. 3127 If any of its neighbors is further than the target distance, 3128 the edge connecting the point and its neighbor is bisected and 3129 a new point is inserted at the bisection point. 3130 A single pass is completed once all the input points are visited. 3131 Then the process repeats to the number of iterations. 3132 3133 Examples: 3134 - [densifycloud.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/densifycloud.py) 3135 3136 ![](https://vedo.embl.es/images/volumetric/densifycloud.png) 3137 3138 .. note:: 3139 Points will be created in an iterative fashion until all points in their 3140 local neighborhood are the target distance apart or less. 3141 Note that the process may terminate early due to the 3142 number of iterations. By default the target distance is set to 0.5. 3143 Note that the target_distance should be less than the radius 3144 or nothing will change on output. 3145 3146 .. warning:: 3147 This class can generate a lot of points very quickly. 3148 The maximum number of iterations is by default set to =1.0 for this reason. 3149 Increase the number of iterations very carefully. 3150 Also, `nmax` can be set to limit the explosion of points. 3151 It is also recommended that a N closest neighborhood is used. 3152 3153 """ 3154 src = vtki.new("ProgrammableSource") 3155 opts = self.vertices 3156 3157 def _read_points(): 3158 output = src.GetPolyDataOutput() 3159 points = vtki.vtkPoints() 3160 for p in opts: 3161 points.InsertNextPoint(p) 3162 output.SetPoints(points) 3163 3164 src.SetExecuteMethod(_read_points) 3165 3166 dens = vtki.new("DensifyPointCloudFilter") 3167 dens.SetInputConnection(src.GetOutputPort()) 3168 dens.InterpolateAttributeDataOn() 3169 dens.SetTargetDistance(target_distance) 3170 dens.SetMaximumNumberOfIterations(niter) 3171 if nmax: 3172 dens.SetMaximumNumberOfPoints(nmax) 3173 3174 if radius: 3175 dens.SetNeighborhoodTypeToRadius() 3176 dens.SetRadius(radius) 3177 elif nclosest: 3178 dens.SetNeighborhoodTypeToNClosest() 3179 dens.SetNumberOfClosestPoints(nclosest) 3180 else: 3181 vedo.logger.error("set either radius or nclosest") 3182 raise RuntimeError() 3183 dens.Update() 3184 pts = utils.vtk2numpy(dens.GetOutput().GetPoints().GetData()) 3185 cld = Points(pts, c=None).point_size(self.properties.GetPointSize()) 3186 cld.interpolate_data_from(self, n=nclosest, radius=radius) 3187 cld.name = "DensifiedCloud" 3188 3189 cld.pipeline = utils.OperationNode( 3190 "densify", 3191 parents=[self], 3192 c="#e9c46a:", 3193 comment=f"#pts {cld.dataset.GetNumberOfPoints()}", 3194 ) 3195 return cld 3196 3197 ############################################################################### 3198 ## stuff returning a Volume 3199 ############################################################################### 3200 3201 def density( 3202 self, dims=(40, 40, 40), bounds=None, radius=None, compute_gradient=False, locator=None 3203 ) -> "vedo.Volume": 3204 """ 3205 Generate a density field from a point cloud. Input can also be a set of 3D coordinates. 3206 Output is a `Volume`. 3207 3208 The local neighborhood is specified as the `radius` around each sample position (each voxel). 3209 If left to None, the radius is automatically computed as the diagonal of the bounding box 3210 and can be accessed via `vol.metadata["radius"]`. 3211 The density is expressed as the number of counts in the radius search. 3212 3213 Arguments: 3214 dims : (int, list) 3215 number of voxels in x, y and z of the output Volume. 3216 compute_gradient : (bool) 3217 Turn on/off the generation of the gradient vector, 3218 gradient magnitude scalar, and function classification scalar. 3219 By default this is off. Note that this will increase execution time 3220 and the size of the output. (The names of these point data arrays are: 3221 "Gradient", "Gradient Magnitude", and "Classification") 3222 locator : (vtkPointLocator) 3223 can be assigned from a previous call for speed (access it via `object.point_locator`). 3224 3225 Examples: 3226 - [plot_density3d.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/plot_density3d.py) 3227 3228 ![](https://vedo.embl.es/images/pyplot/plot_density3d.png) 3229 """ 3230 pdf = vtki.new("PointDensityFilter") 3231 pdf.SetInputData(self.dataset) 3232 3233 if not utils.is_sequence(dims): 3234 dims = [dims, dims, dims] 3235 3236 if bounds is None: 3237 bounds = list(self.bounds()) 3238 elif len(bounds) == 4: 3239 bounds = [*bounds, 0, 0] 3240 3241 if bounds[5] - bounds[4] == 0 or len(dims) == 2: # its 2D 3242 dims = list(dims) 3243 dims = [dims[0], dims[1], 2] 3244 diag = self.diagonal_size() 3245 bounds[5] = bounds[4] + diag / 1000 3246 pdf.SetModelBounds(bounds) 3247 3248 pdf.SetSampleDimensions(dims) 3249 3250 if locator: 3251 pdf.SetLocator(locator) 3252 3253 pdf.SetDensityEstimateToFixedRadius() 3254 if radius is None: 3255 radius = self.diagonal_size() / 20 3256 pdf.SetRadius(radius) 3257 pdf.SetComputeGradient(compute_gradient) 3258 pdf.Update() 3259 3260 vol = vedo.Volume(pdf.GetOutput()).mode(1) 3261 vol.name = "PointDensity" 3262 vol.metadata["radius"] = radius 3263 vol.locator = pdf.GetLocator() 3264 vol.pipeline = utils.OperationNode( 3265 "density", parents=[self], comment=f"dims={tuple(vol.dimensions())}" 3266 ) 3267 return vol 3268 3269 3270 def tovolume( 3271 self, 3272 kernel="shepard", 3273 radius=None, 3274 n=None, 3275 bounds=None, 3276 null_value=None, 3277 dims=(25, 25, 25), 3278 ) -> "vedo.Volume": 3279 """ 3280 Generate a `Volume` by interpolating a scalar 3281 or vector field which is only known on a scattered set of points or mesh. 3282 Available interpolation kernels are: shepard, gaussian, or linear. 3283 3284 Arguments: 3285 kernel : (str) 3286 interpolation kernel type [shepard] 3287 radius : (float) 3288 radius of the local search 3289 n : (int) 3290 number of point to use for interpolation 3291 bounds : (list) 3292 bounding box of the output Volume object 3293 dims : (list) 3294 dimensions of the output Volume object 3295 null_value : (float) 3296 value to be assigned to invalid points 3297 3298 Examples: 3299 - [interpolate_volume.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/interpolate_volume.py) 3300 3301 ![](https://vedo.embl.es/images/volumetric/59095175-1ec5a300-8918-11e9-8bc0-fd35c8981e2b.jpg) 3302 """ 3303 if radius is None and not n: 3304 vedo.logger.error("please set either radius or n") 3305 raise RuntimeError 3306 3307 poly = self.dataset 3308 3309 # Create a probe volume 3310 probe = vtki.vtkImageData() 3311 probe.SetDimensions(dims) 3312 if bounds is None: 3313 bounds = self.bounds() 3314 probe.SetOrigin(bounds[0], bounds[2], bounds[4]) 3315 probe.SetSpacing( 3316 (bounds[1] - bounds[0]) / dims[0], 3317 (bounds[3] - bounds[2]) / dims[1], 3318 (bounds[5] - bounds[4]) / dims[2], 3319 ) 3320 3321 if not self.point_locator: 3322 self.point_locator = vtki.new("PointLocator") 3323 self.point_locator.SetDataSet(poly) 3324 self.point_locator.BuildLocator() 3325 3326 if kernel == "shepard": 3327 kern = vtki.new("ShepardKernel") 3328 kern.SetPowerParameter(2) 3329 elif kernel == "gaussian": 3330 kern = vtki.new("GaussianKernel") 3331 elif kernel == "linear": 3332 kern = vtki.new("LinearKernel") 3333 else: 3334 vedo.logger.error("Error in tovolume(), available kernels are:") 3335 vedo.logger.error(" [shepard, gaussian, linear]") 3336 raise RuntimeError() 3337 3338 if radius: 3339 kern.SetRadius(radius) 3340 3341 interpolator = vtki.new("PointInterpolator") 3342 interpolator.SetInputData(probe) 3343 interpolator.SetSourceData(poly) 3344 interpolator.SetKernel(kern) 3345 interpolator.SetLocator(self.point_locator) 3346 3347 if n: 3348 kern.SetNumberOfPoints(n) 3349 kern.SetKernelFootprintToNClosest() 3350 else: 3351 kern.SetRadius(radius) 3352 3353 if null_value is not None: 3354 interpolator.SetNullValue(null_value) 3355 else: 3356 interpolator.SetNullPointsStrategyToClosestPoint() 3357 interpolator.Update() 3358 3359 vol = vedo.Volume(interpolator.GetOutput()) 3360 3361 vol.pipeline = utils.OperationNode( 3362 "signed_distance", 3363 parents=[self], 3364 comment=f"dims={tuple(vol.dimensions())}", 3365 c="#e9c46a:#0096c7", 3366 ) 3367 return vol 3368 3369 ################################################################################# 3370 def generate_segments(self, istart=0, rmax=1e30, niter=3) -> "vedo.shapes.Lines": 3371 """ 3372 Generate a line segments from a set of points. 3373 The algorithm is based on the closest point search. 3374 3375 Returns a `Line` object. 3376 This object contains the a metadata array of used vertex counts in "UsedVertexCount" 3377 and the sum of the length of the segments in "SegmentsLengthSum". 3378 3379 Arguments: 3380 istart : (int) 3381 index of the starting point 3382 rmax : (float) 3383 maximum length of a segment 3384 niter : (int) 3385 number of iterations or passes through the points 3386 3387 Examples: 3388 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 3389 """ 3390 points = self.vertices 3391 segments = [] 3392 dists = [] 3393 n = len(points) 3394 used = np.zeros(n, dtype=int) 3395 for _ in range(niter): 3396 i = istart 3397 for _ in range(n): 3398 p = points[i] 3399 ids = self.closest_point(p, n=4, return_point_id=True) 3400 j = ids[1] 3401 if used[j] > 1 or [j, i] in segments: 3402 j = ids[2] 3403 if used[j] > 1: 3404 j = ids[3] 3405 d = np.linalg.norm(p - points[j]) 3406 if used[j] > 1 or used[i] > 1 or d > rmax: 3407 i += 1 3408 if i >= n: 3409 i = 0 3410 continue 3411 used[i] += 1 3412 used[j] += 1 3413 segments.append([i, j]) 3414 dists.append(d) 3415 i = j 3416 segments = np.array(segments, dtype=int) 3417 3418 lines = vedo.shapes.Lines(points[segments], c="k", lw=3) 3419 lines.metadata["UsedVertexCount"] = used 3420 lines.metadata["SegmentsLengthSum"] = np.sum(dists) 3421 lines.pipeline = utils.OperationNode("generate_segments", parents=[self]) 3422 lines.name = "Segments" 3423 return lines 3424 3425 def generate_delaunay2d( 3426 self, 3427 mode="scipy", 3428 boundaries=(), 3429 tol=None, 3430 alpha=0.0, 3431 offset=0.0, 3432 transform=None, 3433 ) -> "vedo.mesh.Mesh": 3434 """ 3435 Create a mesh from points in the XY plane. 3436 If `mode='fit'` then the filter computes a best fitting 3437 plane and projects the points onto it. 3438 3439 Check also `generate_mesh()`. 3440 3441 Arguments: 3442 tol : (float) 3443 specify a tolerance to control discarding of closely spaced points. 3444 This tolerance is specified as a fraction of the diagonal length of the bounding box of the points. 3445 alpha : (float) 3446 for a non-zero alpha value, only edges or triangles contained 3447 within a sphere centered at mesh vertices will be output. 3448 Otherwise, only triangles will be output. 3449 offset : (float) 3450 multiplier to control the size of the initial, bounding Delaunay triangulation. 3451 transform: (LinearTransform, NonLinearTransform) 3452 a transformation which is applied to points to generate a 2D problem. 3453 This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane. 3454 The points are transformed and triangulated. 3455 The topology of triangulated points is used as the output topology. 3456 3457 Examples: 3458 - [delaunay2d.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delaunay2d.py) 3459 3460 ![](https://vedo.embl.es/images/basic/delaunay2d.png) 3461 """ 3462 plist = self.vertices.copy() 3463 3464 ######################################################### 3465 if mode == "scipy": 3466 from scipy.spatial import Delaunay as scipy_delaunay 3467 3468 tri = scipy_delaunay(plist[:, 0:2]) 3469 return vedo.mesh.Mesh([plist, tri.simplices]) 3470 ########################################################## 3471 3472 pd = vtki.vtkPolyData() 3473 vpts = vtki.vtkPoints() 3474 vpts.SetData(utils.numpy2vtk(plist, dtype=np.float32)) 3475 pd.SetPoints(vpts) 3476 3477 delny = vtki.new("Delaunay2D") 3478 delny.SetInputData(pd) 3479 if tol: 3480 delny.SetTolerance(tol) 3481 delny.SetAlpha(alpha) 3482 delny.SetOffset(offset) 3483 3484 if transform: 3485 delny.SetTransform(transform.T) 3486 elif mode == "fit": 3487 delny.SetProjectionPlaneMode(vtki.get_class("VTK_BEST_FITTING_PLANE")) 3488 elif mode == "xy" and boundaries: 3489 boundary = vtki.vtkPolyData() 3490 boundary.SetPoints(vpts) 3491 cell_array = vtki.vtkCellArray() 3492 for b in boundaries: 3493 cpolygon = vtki.vtkPolygon() 3494 for idd in b: 3495 cpolygon.GetPointIds().InsertNextId(idd) 3496 cell_array.InsertNextCell(cpolygon) 3497 boundary.SetPolys(cell_array) 3498 delny.SetSourceData(boundary) 3499 3500 delny.Update() 3501 3502 msh = vedo.mesh.Mesh(delny.GetOutput()) 3503 msh.name = "Delaunay2D" 3504 msh.clean().lighting("off") 3505 msh.pipeline = utils.OperationNode( 3506 "delaunay2d", 3507 parents=[self], 3508 comment=f"#cells {msh.dataset.GetNumberOfCells()}", 3509 ) 3510 return msh 3511 3512 def generate_voronoi(self, padding=0.0, fit=False, method="vtk") -> "vedo.Mesh": 3513 """ 3514 Generate the 2D Voronoi convex tiling of the input points (z is ignored). 3515 The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon. 3516 3517 A cell array named "VoronoiID" is added to the output Mesh. 3518 3519 The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest 3520 to one of the input points. Voronoi tessellations are important in computational geometry 3521 (and many other fields), and are the dual of Delaunay triangulations. 3522 3523 Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored 3524 (although carried through to the output). 3525 If you desire to triangulate in a different plane, you can use fit=True. 3526 3527 A brief summary is as follows. Each (generating) input point is associated with 3528 an initial Voronoi tile, which is simply the bounding box of the point set. 3529 A locator is then used to identify nearby points: each neighbor in turn generates a 3530 clipping line positioned halfway between the generating point and the neighboring point, 3531 and orthogonal to the line connecting them. Clips are readily performed by evaluationg the 3532 vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line. 3533 If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip 3534 line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs, 3535 the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region 3536 containing the neighboring clip points. The clip region (along with the points contained in it) is grown 3537 by careful expansion (e.g., outward spiraling iterator over all candidate clip points). 3538 When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi 3539 tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi 3540 tessellation. Note that topological and geometric information is used to generate a valid triangulation 3541 (e.g., merging points and validating topology). 3542 3543 Arguments: 3544 pts : (list) 3545 list of input points. 3546 padding : (float) 3547 padding distance. The default is 0. 3548 fit : (bool) 3549 detect automatically the best fitting plane. The default is False. 3550 3551 Examples: 3552 - [voronoi1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi1.py) 3553 3554 ![](https://vedo.embl.es/images/basic/voronoi1.png) 3555 3556 - [voronoi2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi2.py) 3557 3558 ![](https://vedo.embl.es/images/advanced/voronoi2.png) 3559 """ 3560 pts = self.vertices 3561 3562 if method == "scipy": 3563 from scipy.spatial import Voronoi as scipy_voronoi 3564 3565 pts = np.asarray(pts)[:, (0, 1)] 3566 vor = scipy_voronoi(pts) 3567 regs = [] # filter out invalid indices 3568 for r in vor.regions: 3569 flag = True 3570 for x in r: 3571 if x < 0: 3572 flag = False 3573 break 3574 if flag and len(r) > 0: 3575 regs.append(r) 3576 3577 m = vedo.Mesh([vor.vertices, regs]) 3578 m.celldata["VoronoiID"] = np.array(list(range(len(regs)))).astype(int) 3579 3580 elif method == "vtk": 3581 vor = vtki.new("Voronoi2D") 3582 if isinstance(pts, Points): 3583 vor.SetInputData(pts) 3584 else: 3585 pts = np.asarray(pts) 3586 if pts.shape[1] == 2: 3587 pts = np.c_[pts, np.zeros(len(pts))] 3588 pd = vtki.vtkPolyData() 3589 vpts = vtki.vtkPoints() 3590 vpts.SetData(utils.numpy2vtk(pts, dtype=np.float32)) 3591 pd.SetPoints(vpts) 3592 vor.SetInputData(pd) 3593 vor.SetPadding(padding) 3594 vor.SetGenerateScalarsToPointIds() 3595 if fit: 3596 vor.SetProjectionPlaneModeToBestFittingPlane() 3597 else: 3598 vor.SetProjectionPlaneModeToXYPlane() 3599 vor.Update() 3600 poly = vor.GetOutput() 3601 arr = poly.GetCellData().GetArray(0) 3602 if arr: 3603 arr.SetName("VoronoiID") 3604 m = vedo.Mesh(poly, c="orange5") 3605 3606 else: 3607 vedo.logger.error(f"Unknown method {method} in voronoi()") 3608 raise RuntimeError 3609 3610 m.lw(2).lighting("off").wireframe() 3611 m.name = "Voronoi" 3612 return m 3613 3614 ########################################################################## 3615 def generate_delaunay3d(self, radius=0, tol=None) -> "vedo.TetMesh": 3616 """ 3617 Create 3D Delaunay triangulation of input points. 3618 3619 Arguments: 3620 radius : (float) 3621 specify distance (or "alpha") value to control output. 3622 For a non-zero values, only tetra contained within the circumsphere 3623 will be output. 3624 tol : (float) 3625 Specify a tolerance to control discarding of closely spaced points. 3626 This tolerance is specified as a fraction of the diagonal length of 3627 the bounding box of the points. 3628 """ 3629 deln = vtki.new("Delaunay3D") 3630 deln.SetInputData(self.dataset) 3631 deln.SetAlpha(radius) 3632 deln.AlphaTetsOn() 3633 deln.AlphaTrisOff() 3634 deln.AlphaLinesOff() 3635 deln.AlphaVertsOff() 3636 deln.BoundingTriangulationOff() 3637 if tol: 3638 deln.SetTolerance(tol) 3639 deln.Update() 3640 m = vedo.TetMesh(deln.GetOutput()) 3641 m.pipeline = utils.OperationNode( 3642 "generate_delaunay3d", c="#e9c46a:#edabab", parents=[self], 3643 ) 3644 m.name = "Delaunay3D" 3645 return m 3646 3647 #################################################### 3648 def visible_points(self, area=(), tol=None, invert=False) -> Union[Self, None]: 3649 """ 3650 Extract points based on whether they are visible or not. 3651 Visibility is determined by accessing the z-buffer of a rendering window. 3652 The position of each input point is converted into display coordinates, 3653 and then the z-value at that point is obtained. 3654 If within the user-specified tolerance, the point is considered visible. 3655 Associated data attributes are passed to the output as well. 3656 3657 This filter also allows you to specify a rectangular window in display (pixel) 3658 coordinates in which the visible points must lie. 3659 3660 Arguments: 3661 area : (list) 3662 specify a rectangular region as (xmin,xmax,ymin,ymax) 3663 tol : (float) 3664 a tolerance in normalized display coordinate system 3665 invert : (bool) 3666 select invisible points instead. 3667 3668 Example: 3669 ```python 3670 from vedo import Ellipsoid, show 3671 s = Ellipsoid().rotate_y(30) 3672 3673 # Camera options: pos, focal_point, viewup, distance 3674 camopts = dict(pos=(0,0,25), focal_point=(0,0,0)) 3675 show(s, camera=camopts, offscreen=True) 3676 3677 m = s.visible_points() 3678 # print('visible pts:', m.vertices) # numpy array 3679 show(m, new=True, axes=1).close() # optionally draw result in a new window 3680 ``` 3681 ![](https://vedo.embl.es/images/feats/visible_points.png) 3682 """ 3683 svp = vtki.new("SelectVisiblePoints") 3684 svp.SetInputData(self.dataset) 3685 3686 ren = None 3687 if vedo.plotter_instance: 3688 if vedo.plotter_instance.renderer: 3689 ren = vedo.plotter_instance.renderer 3690 svp.SetRenderer(ren) 3691 if not ren: 3692 vedo.logger.warning( 3693 "visible_points() can only be used after a rendering step" 3694 ) 3695 return None 3696 3697 if len(area) == 2: 3698 area = utils.flatten(area) 3699 if len(area) == 4: 3700 # specify a rectangular region 3701 svp.SetSelection(area[0], area[1], area[2], area[3]) 3702 if tol is not None: 3703 svp.SetTolerance(tol) 3704 if invert: 3705 svp.SelectInvisibleOn() 3706 svp.Update() 3707 3708 m = Points(svp.GetOutput()) 3709 m.name = "VisiblePoints" 3710 return m
Work with point clouds.
470 def __init__(self, inputobj=None, r=4, c=(0.2, 0.2, 0.2), alpha=1): 471 """ 472 Build an object made of only vertex points for a list of 2D/3D points. 473 Both shapes (N, 3) or (3, N) are accepted as input, if N>3. 474 For very large point clouds a list of colors and alpha can be assigned to each 475 point in the form c=[(R,G,B,A), ... ] where 0<=R<256, ... 0<=A<256. 476 477 Arguments: 478 inputobj : (list, tuple) 479 r : (int) 480 Point radius in units of pixels. 481 c : (str, list) 482 Color name or rgb tuple. 483 alpha : (float) 484 Transparency in range [0,1]. 485 486 Example: 487 ```python 488 from vedo import * 489 490 def fibonacci_sphere(n): 491 s = np.linspace(0, n, num=n, endpoint=False) 492 theta = s * 2.399963229728653 493 y = 1 - s * (2/(n-1)) 494 r = np.sqrt(1 - y * y) 495 x = np.cos(theta) * r 496 z = np.sin(theta) * r 497 return np._c[x,y,z] 498 499 Points(fibonacci_sphere(1000)).show(axes=1).close() 500 ``` 501 ![](https://vedo.embl.es/images/feats/fibonacci.png) 502 """ 503 # print("INIT POINTS") 504 super().__init__() 505 506 self.name = "" 507 self.filename = "" 508 self.file_size = "" 509 510 self.info = {} 511 self.time = time.time() 512 513 self.transform = LinearTransform() 514 self.point_locator = None 515 self.cell_locator = None 516 self.line_locator = None 517 518 self.actor = vtki.vtkActor() 519 self.properties = self.actor.GetProperty() 520 self.properties_backface = self.actor.GetBackfaceProperty() 521 self.mapper = vtki.new("PolyDataMapper") 522 self.dataset = vtki.vtkPolyData() 523 524 # Create weakref so actor can access this object (eg to pick/remove): 525 self.actor.retrieve_object = weak_ref_to(self) 526 527 try: 528 self.properties.RenderPointsAsSpheresOn() 529 except AttributeError: 530 pass 531 532 if inputobj is None: #################### 533 return 534 ########################################## 535 536 self.name = "Points" 537 538 ###### 539 if isinstance(inputobj, vtki.vtkActor): 540 self.dataset.DeepCopy(inputobj.GetMapper().GetInput()) 541 pr = vtki.vtkProperty() 542 pr.DeepCopy(inputobj.GetProperty()) 543 self.actor.SetProperty(pr) 544 self.properties = pr 545 self.mapper.SetScalarVisibility(inputobj.GetMapper().GetScalarVisibility()) 546 547 elif isinstance(inputobj, vtki.vtkPolyData): 548 self.dataset = inputobj 549 if self.dataset.GetNumberOfCells() == 0: 550 carr = vtki.vtkCellArray() 551 for i in range(self.dataset.GetNumberOfPoints()): 552 carr.InsertNextCell(1) 553 carr.InsertCellPoint(i) 554 self.dataset.SetVerts(carr) 555 556 elif isinstance(inputobj, Points): 557 self.dataset = inputobj.dataset 558 self.copy_properties_from(inputobj) 559 560 elif utils.is_sequence(inputobj): # passing point coords 561 self.dataset = utils.buildPolyData(utils.make3d(inputobj)) 562 563 elif isinstance(inputobj, str): 564 verts = vedo.file_io.load(inputobj) 565 self.filename = inputobj 566 self.dataset = verts.dataset 567 568 else: 569 # try to extract the points from a generic VTK input data object 570 if hasattr(inputobj, "dataset"): 571 inputobj = inputobj.dataset 572 try: 573 vvpts = inputobj.GetPoints() 574 self.dataset = vtki.vtkPolyData() 575 self.dataset.SetPoints(vvpts) 576 for i in range(inputobj.GetPointData().GetNumberOfArrays()): 577 arr = inputobj.GetPointData().GetArray(i) 578 self.dataset.GetPointData().AddArray(arr) 579 except: 580 vedo.logger.error(f"cannot build Points from type {type(inputobj)}") 581 raise RuntimeError() 582 583 self.actor.SetMapper(self.mapper) 584 self.mapper.SetInputData(self.dataset) 585 586 self.properties.SetColor(colors.get_color(c)) 587 self.properties.SetOpacity(alpha) 588 self.properties.SetRepresentationToPoints() 589 self.properties.SetPointSize(r) 590 self.properties.LightingOff() 591 592 self.pipeline = utils.OperationNode( 593 self, parents=[], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 594 )
Build an object made of only vertex points for a list of 2D/3D points. Both shapes (N, 3) or (3, N) are accepted as input, if N>3. For very large point clouds a list of colors and alpha can be assigned to each point in the form c=[(R,G,B,A), ... ] where 0<=R<256, ... 0<=A<256.
Arguments:
- inputobj : (list, tuple)
- r : (int) Point radius in units of pixels.
- c : (str, list) Color name or rgb tuple.
- alpha : (float) Transparency in range [0,1].
Example:
from vedo import * def fibonacci_sphere(n): s = np.linspace(0, n, num=n, endpoint=False) theta = s * 2.399963229728653 y = 1 - s * (2/(n-1)) r = np.sqrt(1 - y * y) x = np.cos(theta) * r z = np.sin(theta) * r return np._c[x,y,z] Points(fibonacci_sphere(1000)).show(axes=1).close()
816 def polydata(self, **kwargs): 817 """ 818 Obsolete. Use property `.dataset` instead. 819 Returns the underlying `vtkPolyData` object. 820 """ 821 colors.printc( 822 "WARNING: call to .polydata() is obsolete, use property .dataset instead.", 823 c="y") 824 return self.dataset
Obsolete. Use property .dataset
instead.
Returns the underlying vtkPolyData
object.
832 def copy(self, deep=True) -> Self: 833 """Return a copy of the object. Alias of `clone()`.""" 834 return self.clone(deep=deep)
Return a copy of the object. Alias of clone()
.
836 def clone(self, deep=True) -> Self: 837 """ 838 Clone a `PointCloud` or `Mesh` object to make an exact copy of it. 839 Alias of `copy()`. 840 841 Arguments: 842 deep : (bool) 843 if False return a shallow copy of the mesh without copying the points array. 844 845 Examples: 846 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 847 848 ![](https://vedo.embl.es/images/basic/mirror.png) 849 """ 850 poly = vtki.vtkPolyData() 851 if deep or isinstance(deep, dict): # if a memo object is passed this checks as True 852 poly.DeepCopy(self.dataset) 853 else: 854 poly.ShallowCopy(self.dataset) 855 856 if isinstance(self, vedo.Mesh): 857 cloned = vedo.Mesh(poly) 858 else: 859 cloned = Points(poly) 860 # print([self], self.__class__) 861 # cloned = self.__class__(poly) 862 863 cloned.transform = self.transform.clone() 864 865 cloned.copy_properties_from(self) 866 867 cloned.name = str(self.name) 868 cloned.filename = str(self.filename) 869 cloned.info = dict(self.info) 870 cloned.pipeline = utils.OperationNode("clone", parents=[self], shape="diamond", c="#edede9") 871 872 if isinstance(deep, dict): 873 deep[id(self)] = cloned 874 875 return cloned
877 def compute_normals_with_pca(self, n=20, orientation_point=None, invert=False) -> Self: 878 """ 879 Generate point normals using PCA (principal component analysis). 880 This algorithm estimates a local tangent plane around each sample point p 881 by considering a small neighborhood of points around p, and fitting a plane 882 to the neighborhood (via PCA). 883 884 Arguments: 885 n : (int) 886 neighborhood size to calculate the normal 887 orientation_point : (list) 888 adjust the +/- sign of the normals so that 889 the normals all point towards a specified point. If None, perform a traversal 890 of the point cloud and flip neighboring normals so that they are mutually consistent. 891 invert : (bool) 892 flip all normals 893 """ 894 poly = self.dataset 895 pcan = vtki.new("PCANormalEstimation") 896 pcan.SetInputData(poly) 897 pcan.SetSampleSize(n) 898 899 if orientation_point is not None: 900 pcan.SetNormalOrientationToPoint() 901 pcan.SetOrientationPoint(orientation_point) 902 else: 903 pcan.SetNormalOrientationToGraphTraversal() 904 905 if invert: 906 pcan.FlipNormalsOn() 907 pcan.Update() 908 909 varr = pcan.GetOutput().GetPointData().GetNormals() 910 varr.SetName("Normals") 911 self.dataset.GetPointData().SetNormals(varr) 912 self.dataset.GetPointData().Modified() 913 return self
Generate point normals using PCA (principal component analysis). This algorithm estimates a local tangent plane around each sample point p by considering a small neighborhood of points around p, and fitting a plane to the neighborhood (via PCA).
Arguments:
- n : (int) neighborhood size to calculate the normal
- orientation_point : (list) adjust the +/- sign of the normals so that the normals all point towards a specified point. If None, perform a traversal of the point cloud and flip neighboring normals so that they are mutually consistent.
- invert : (bool) flip all normals
915 def compute_acoplanarity(self, n=25, radius=None, on="points") -> Self: 916 """ 917 Compute acoplanarity which is a measure of how much a local region of the mesh 918 differs from a plane. 919 920 The information is stored in a `pointdata` or `celldata` array with name 'Acoplanarity'. 921 922 Either `n` (number of neighbour points) or `radius` (radius of local search) can be specified. 923 If a radius value is given and not enough points fall inside it, then a -1 is stored. 924 925 Example: 926 ```python 927 from vedo import * 928 msh = ParametricShape('RandomHills') 929 msh.compute_acoplanarity(radius=0.1, on='cells') 930 msh.cmap("coolwarm", on='cells').add_scalarbar() 931 msh.show(axes=1).close() 932 ``` 933 ![](https://vedo.embl.es/images/feats/acoplanarity.jpg) 934 """ 935 acoplanarities = [] 936 if "point" in on: 937 pts = self.vertices 938 elif "cell" in on: 939 pts = self.cell_centers 940 else: 941 raise ValueError(f"In compute_acoplanarity() set on to either 'cells' or 'points', not {on}") 942 943 for p in utils.progressbar(pts, delay=5, width=15, title=f"{on} acoplanarity"): 944 if n: 945 data = self.closest_point(p, n=n) 946 npts = n 947 elif radius: 948 data = self.closest_point(p, radius=radius) 949 npts = len(data) 950 951 try: 952 center = data.mean(axis=0) 953 res = np.linalg.svd(data - center) 954 acoplanarities.append(res[1][2] / npts) 955 except: 956 acoplanarities.append(-1.0) 957 958 if "point" in on: 959 self.pointdata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 960 else: 961 self.celldata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 962 return self
Compute acoplanarity which is a measure of how much a local region of the mesh differs from a plane.
The information is stored in a pointdata
or celldata
array with name 'Acoplanarity'.
Either n
(number of neighbour points) or radius
(radius of local search) can be specified.
If a radius value is given and not enough points fall inside it, then a -1 is stored.
Example:
from vedo import * msh = ParametricShape('RandomHills') msh.compute_acoplanarity(radius=0.1, on='cells') msh.cmap("coolwarm", on='cells').add_scalarbar() msh.show(axes=1).close()
964 def distance_to(self, pcloud, signed=False, invert=False, name="Distance") -> np.ndarray: 965 """ 966 Computes the distance from one point cloud or mesh to another point cloud or mesh. 967 This new `pointdata` array is saved with default name "Distance". 968 969 Keywords `signed` and `invert` are used to compute signed distance, 970 but the mesh in that case must have polygonal faces (not a simple point cloud), 971 and normals must also be computed. 972 973 Examples: 974 - [distance2mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/distance2mesh.py) 975 976 ![](https://vedo.embl.es/images/basic/distance2mesh.png) 977 """ 978 if pcloud.dataset.GetNumberOfPolys(): 979 980 poly1 = self.dataset 981 poly2 = pcloud.dataset 982 df = vtki.new("DistancePolyDataFilter") 983 df.ComputeSecondDistanceOff() 984 df.SetInputData(0, poly1) 985 df.SetInputData(1, poly2) 986 df.SetSignedDistance(signed) 987 df.SetNegateDistance(invert) 988 df.Update() 989 scals = df.GetOutput().GetPointData().GetScalars() 990 dists = utils.vtk2numpy(scals) 991 992 else: # has no polygons 993 994 if signed: 995 vedo.logger.warning("distance_to() called with signed=True but input object has no polygons") 996 997 if not pcloud.point_locator: 998 pcloud.point_locator = vtki.new("PointLocator") 999 pcloud.point_locator.SetDataSet(pcloud.dataset) 1000 pcloud.point_locator.BuildLocator() 1001 1002 ids = [] 1003 ps1 = self.vertices 1004 ps2 = pcloud.vertices 1005 for p in ps1: 1006 pid = pcloud.point_locator.FindClosestPoint(p) 1007 ids.append(pid) 1008 1009 deltas = ps2[ids] - ps1 1010 dists = np.linalg.norm(deltas, axis=1).astype(np.float32) 1011 scals = utils.numpy2vtk(dists) 1012 1013 scals.SetName(name) 1014 self.dataset.GetPointData().AddArray(scals) 1015 self.dataset.GetPointData().SetActiveScalars(scals.GetName()) 1016 rng = scals.GetRange() 1017 self.mapper.SetScalarRange(rng[0], rng[1]) 1018 self.mapper.ScalarVisibilityOn() 1019 1020 self.pipeline = utils.OperationNode( 1021 "distance_to", 1022 parents=[self, pcloud], 1023 shape="cylinder", 1024 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1025 ) 1026 return dists
Computes the distance from one point cloud or mesh to another point cloud or mesh.
This new pointdata
array is saved with default name "Distance".
Keywords signed
and invert
are used to compute signed distance,
but the mesh in that case must have polygonal faces (not a simple point cloud),
and normals must also be computed.
Examples:
1028 def clean(self) -> Self: 1029 """Clean pointcloud or mesh by removing coincident points.""" 1030 cpd = vtki.new("CleanPolyData") 1031 cpd.PointMergingOn() 1032 cpd.ConvertLinesToPointsOff() 1033 cpd.ConvertPolysToLinesOff() 1034 cpd.ConvertStripsToPolysOff() 1035 cpd.SetInputData(self.dataset) 1036 cpd.Update() 1037 self._update(cpd.GetOutput()) 1038 self.pipeline = utils.OperationNode( 1039 "clean", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1040 ) 1041 return self
Clean pointcloud or mesh by removing coincident points.
1043 def subsample(self, fraction: float, absolute=False) -> Self: 1044 """ 1045 Subsample a point cloud by requiring that the points 1046 or vertices are far apart at least by the specified fraction of the object size. 1047 If a Mesh is passed the polygonal faces are not removed 1048 but holes can appear as their vertices are removed. 1049 1050 Examples: 1051 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1052 1053 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1054 1055 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1056 1057 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1058 """ 1059 if not absolute: 1060 if fraction > 1: 1061 vedo.logger.warning( 1062 f"subsample(fraction=...), fraction must be < 1, but is {fraction}" 1063 ) 1064 if fraction <= 0: 1065 return self 1066 1067 cpd = vtki.new("CleanPolyData") 1068 cpd.PointMergingOn() 1069 cpd.ConvertLinesToPointsOn() 1070 cpd.ConvertPolysToLinesOn() 1071 cpd.ConvertStripsToPolysOn() 1072 cpd.SetInputData(self.dataset) 1073 if absolute: 1074 cpd.SetTolerance(fraction / self.diagonal_size()) 1075 # cpd.SetToleranceIsAbsolute(absolute) 1076 else: 1077 cpd.SetTolerance(fraction) 1078 cpd.Update() 1079 1080 ps = 2 1081 if self.properties.GetRepresentation() == 0: 1082 ps = self.properties.GetPointSize() 1083 1084 self._update(cpd.GetOutput()) 1085 self.ps(ps) 1086 1087 self.pipeline = utils.OperationNode( 1088 "subsample", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1089 ) 1090 return self
Subsample a point cloud by requiring that the points or vertices are far apart at least by the specified fraction of the object size. If a Mesh is passed the polygonal faces are not removed but holes can appear as their vertices are removed.
Examples:
1092 def threshold(self, scalars: str, above=None, below=None, on="points") -> Self: 1093 """ 1094 Extracts cells where scalar value satisfies threshold criterion. 1095 1096 Arguments: 1097 scalars : (str) 1098 name of the scalars array. 1099 above : (float) 1100 minimum value of the scalar 1101 below : (float) 1102 maximum value of the scalar 1103 on : (str) 1104 if 'cells' assume array of scalars refers to cell data. 1105 1106 Examples: 1107 - [mesh_threshold.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_threshold.py) 1108 """ 1109 thres = vtki.new("Threshold") 1110 thres.SetInputData(self.dataset) 1111 1112 if on.startswith("c"): 1113 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_CELLS 1114 else: 1115 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_POINTS 1116 1117 thres.SetInputArrayToProcess(0, 0, 0, asso, scalars) 1118 1119 if above is None and below is not None: 1120 try: # vtk 9.2 1121 thres.ThresholdByLower(below) 1122 except AttributeError: # vtk 9.3 1123 thres.SetUpperThreshold(below) 1124 1125 elif below is None and above is not None: 1126 try: 1127 thres.ThresholdByUpper(above) 1128 except AttributeError: 1129 thres.SetLowerThreshold(above) 1130 else: 1131 try: 1132 thres.ThresholdBetween(above, below) 1133 except AttributeError: 1134 thres.SetUpperThreshold(below) 1135 thres.SetLowerThreshold(above) 1136 1137 thres.Update() 1138 1139 gf = vtki.new("GeometryFilter") 1140 gf.SetInputData(thres.GetOutput()) 1141 gf.Update() 1142 self._update(gf.GetOutput()) 1143 self.pipeline = utils.OperationNode("threshold", parents=[self]) 1144 return self
Extracts cells where scalar value satisfies threshold criterion.
Arguments:
- scalars : (str) name of the scalars array.
- above : (float) minimum value of the scalar
- below : (float) maximum value of the scalar
- on : (str) if 'cells' assume array of scalars refers to cell data.
Examples:
1146 def quantize(self, value: float) -> Self: 1147 """ 1148 The user should input a value and all {x,y,z} coordinates 1149 will be quantized to that absolute grain size. 1150 """ 1151 qp = vtki.new("QuantizePolyDataPoints") 1152 qp.SetInputData(self.dataset) 1153 qp.SetQFactor(value) 1154 qp.Update() 1155 self._update(qp.GetOutput()) 1156 self.pipeline = utils.OperationNode("quantize", parents=[self]) 1157 return self
The user should input a value and all {x,y,z} coordinates will be quantized to that absolute grain size.
1159 @property 1160 def vertex_normals(self) -> np.ndarray: 1161 """ 1162 Retrieve vertex normals as a numpy array. Same as `point_normals`. 1163 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1164 """ 1165 vtknormals = self.dataset.GetPointData().GetNormals() 1166 return utils.vtk2numpy(vtknormals)
Retrieve vertex normals as a numpy array. Same as point_normals
.
Check out also compute_normals()
and compute_normals_with_pca()
.
1168 @property 1169 def point_normals(self) -> np.ndarray: 1170 """ 1171 Retrieve vertex normals as a numpy array. Same as `vertex_normals`. 1172 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1173 """ 1174 vtknormals = self.dataset.GetPointData().GetNormals() 1175 return utils.vtk2numpy(vtknormals)
Retrieve vertex normals as a numpy array. Same as vertex_normals
.
Check out also compute_normals()
and compute_normals_with_pca()
.
1177 def align_to(self, target, iters=100, rigid=False, invert=False, use_centroids=False) -> Self: 1178 """ 1179 Aligned to target mesh through the `Iterative Closest Point` algorithm. 1180 1181 The core of the algorithm is to match each vertex in one surface with 1182 the closest surface point on the other, then apply the transformation 1183 that modify one surface to best match the other (in the least-square sense). 1184 1185 Arguments: 1186 rigid : (bool) 1187 if True do not allow scaling 1188 invert : (bool) 1189 if True start by aligning the target to the source but 1190 invert the transformation finally. Useful when the target is smaller 1191 than the source. 1192 use_centroids : (bool) 1193 start by matching the centroids of the two objects. 1194 1195 Examples: 1196 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1197 1198 ![](https://vedo.embl.es/images/basic/align1.png) 1199 1200 - [align2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align2.py) 1201 1202 ![](https://vedo.embl.es/images/basic/align2.png) 1203 """ 1204 icp = vtki.new("IterativeClosestPointTransform") 1205 icp.SetSource(self.dataset) 1206 icp.SetTarget(target.dataset) 1207 if invert: 1208 icp.Inverse() 1209 icp.SetMaximumNumberOfIterations(iters) 1210 if rigid: 1211 icp.GetLandmarkTransform().SetModeToRigidBody() 1212 icp.SetStartByMatchingCentroids(use_centroids) 1213 icp.Update() 1214 1215 self.apply_transform(icp.GetMatrix()) 1216 1217 self.pipeline = utils.OperationNode( 1218 "align_to", parents=[self, target], comment=f"rigid = {rigid}" 1219 ) 1220 return self
Aligned to target mesh through the Iterative Closest Point
algorithm.
The core of the algorithm is to match each vertex in one surface with the closest surface point on the other, then apply the transformation that modify one surface to best match the other (in the least-square sense).
Arguments:
- rigid : (bool) if True do not allow scaling
- invert : (bool) if True start by aligning the target to the source but invert the transformation finally. Useful when the target is smaller than the source.
- use_centroids : (bool) start by matching the centroids of the two objects.
Examples:
1222 def align_to_bounding_box(self, msh, rigid=False) -> Self: 1223 """ 1224 Align the current object's bounding box to the bounding box 1225 of the input object. 1226 1227 Use `rigid=True` to disable scaling. 1228 1229 Example: 1230 [align6.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align6.py) 1231 """ 1232 lmt = vtki.vtkLandmarkTransform() 1233 ss = vtki.vtkPoints() 1234 xss0, xss1, yss0, yss1, zss0, zss1 = self.bounds() 1235 for p in [ 1236 [xss0, yss0, zss0], 1237 [xss1, yss0, zss0], 1238 [xss1, yss1, zss0], 1239 [xss0, yss1, zss0], 1240 [xss0, yss0, zss1], 1241 [xss1, yss0, zss1], 1242 [xss1, yss1, zss1], 1243 [xss0, yss1, zss1], 1244 ]: 1245 ss.InsertNextPoint(p) 1246 st = vtki.vtkPoints() 1247 xst0, xst1, yst0, yst1, zst0, zst1 = msh.bounds() 1248 for p in [ 1249 [xst0, yst0, zst0], 1250 [xst1, yst0, zst0], 1251 [xst1, yst1, zst0], 1252 [xst0, yst1, zst0], 1253 [xst0, yst0, zst1], 1254 [xst1, yst0, zst1], 1255 [xst1, yst1, zst1], 1256 [xst0, yst1, zst1], 1257 ]: 1258 st.InsertNextPoint(p) 1259 1260 lmt.SetSourceLandmarks(ss) 1261 lmt.SetTargetLandmarks(st) 1262 lmt.SetModeToAffine() 1263 if rigid: 1264 lmt.SetModeToRigidBody() 1265 lmt.Update() 1266 1267 LT = LinearTransform(lmt) 1268 self.apply_transform(LT) 1269 return self
Align the current object's bounding box to the bounding box of the input object.
Use rigid=True
to disable scaling.
Example:
1271 def align_with_landmarks( 1272 self, 1273 source_landmarks, 1274 target_landmarks, 1275 rigid=False, 1276 affine=False, 1277 least_squares=False, 1278 ) -> Self: 1279 """ 1280 Transform mesh orientation and position based on a set of landmarks points. 1281 The algorithm finds the best matching of source points to target points 1282 in the mean least square sense, in one single step. 1283 1284 If `affine` is True the x, y and z axes can scale independently but stay collinear. 1285 With least_squares they can vary orientation. 1286 1287 Examples: 1288 - [align5.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align5.py) 1289 1290 ![](https://vedo.embl.es/images/basic/align5.png) 1291 """ 1292 1293 if utils.is_sequence(source_landmarks): 1294 ss = vtki.vtkPoints() 1295 for p in source_landmarks: 1296 ss.InsertNextPoint(p) 1297 else: 1298 ss = source_landmarks.dataset.GetPoints() 1299 if least_squares: 1300 source_landmarks = source_landmarks.vertices 1301 1302 if utils.is_sequence(target_landmarks): 1303 st = vtki.vtkPoints() 1304 for p in target_landmarks: 1305 st.InsertNextPoint(p) 1306 else: 1307 st = target_landmarks.GetPoints() 1308 if least_squares: 1309 target_landmarks = target_landmarks.vertices 1310 1311 if ss.GetNumberOfPoints() != st.GetNumberOfPoints(): 1312 n1 = ss.GetNumberOfPoints() 1313 n2 = st.GetNumberOfPoints() 1314 vedo.logger.error(f"source and target have different nr of points {n1} vs {n2}") 1315 raise RuntimeError() 1316 1317 if int(rigid) + int(affine) + int(least_squares) > 1: 1318 vedo.logger.error( 1319 "only one of rigid, affine, least_squares can be True at a time" 1320 ) 1321 raise RuntimeError() 1322 1323 lmt = vtki.vtkLandmarkTransform() 1324 lmt.SetSourceLandmarks(ss) 1325 lmt.SetTargetLandmarks(st) 1326 lmt.SetModeToSimilarity() 1327 1328 if rigid: 1329 lmt.SetModeToRigidBody() 1330 lmt.Update() 1331 1332 elif affine: 1333 lmt.SetModeToAffine() 1334 lmt.Update() 1335 1336 elif least_squares: 1337 cms = source_landmarks.mean(axis=0) 1338 cmt = target_landmarks.mean(axis=0) 1339 m = np.linalg.lstsq(source_landmarks - cms, target_landmarks - cmt, rcond=None)[0] 1340 M = vtki.vtkMatrix4x4() 1341 for i in range(3): 1342 for j in range(3): 1343 M.SetElement(j, i, m[i][j]) 1344 lmt = vtki.vtkTransform() 1345 lmt.Translate(cmt) 1346 lmt.Concatenate(M) 1347 lmt.Translate(-cms) 1348 1349 else: 1350 lmt.Update() 1351 1352 self.apply_transform(lmt) 1353 self.pipeline = utils.OperationNode("transform_with_landmarks", parents=[self]) 1354 return self
Transform mesh orientation and position based on a set of landmarks points. The algorithm finds the best matching of source points to target points in the mean least square sense, in one single step.
If affine
is True the x, y and z axes can scale independently but stay collinear.
With least_squares they can vary orientation.
Examples:
1356 def normalize(self) -> Self: 1357 """Scale average size to unit. The scaling is performed around the center of mass.""" 1358 coords = self.vertices 1359 if not coords.shape[0]: 1360 return self 1361 cm = np.mean(coords, axis=0) 1362 pts = coords - cm 1363 xyz2 = np.sum(pts * pts, axis=0) 1364 scale = 1 / np.sqrt(np.sum(xyz2) / len(pts)) 1365 self.scale(scale, origin=cm) 1366 self.pipeline = utils.OperationNode("normalize", parents=[self]) 1367 return self
Scale average size to unit. The scaling is performed around the center of mass.
1369 def mirror(self, axis="x", origin=True) -> Self: 1370 """ 1371 Mirror reflect along one of the cartesian axes 1372 1373 Arguments: 1374 axis : (str) 1375 axis to use for mirroring, must be set to `x, y, z`. 1376 Or any combination of those. 1377 origin : (list) 1378 use this point as the origin of the mirroring transformation. 1379 1380 Examples: 1381 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 1382 1383 ![](https://vedo.embl.es/images/basic/mirror.png) 1384 """ 1385 sx, sy, sz = 1, 1, 1 1386 if "x" in axis.lower(): sx = -1 1387 if "y" in axis.lower(): sy = -1 1388 if "z" in axis.lower(): sz = -1 1389 1390 self.scale([sx, sy, sz], origin=origin) 1391 1392 self.pipeline = utils.OperationNode( 1393 "mirror", comment=f"axis = {axis}", parents=[self]) 1394 1395 if sx * sy * sz < 0: 1396 if hasattr(self, "reverse"): 1397 self.reverse() 1398 return self
Mirror reflect along one of the cartesian axes
Arguments:
- axis : (str)
axis to use for mirroring, must be set to
x, y, z
. Or any combination of those. - origin : (list) use this point as the origin of the mirroring transformation.
Examples:
1400 def flip_normals(self) -> Self: 1401 """Flip all normals orientation.""" 1402 rs = vtki.new("ReverseSense") 1403 rs.SetInputData(self.dataset) 1404 rs.ReverseCellsOff() 1405 rs.ReverseNormalsOn() 1406 rs.Update() 1407 self._update(rs.GetOutput()) 1408 self.pipeline = utils.OperationNode("flip_normals", parents=[self]) 1409 return self
Flip all normals orientation.
1411 def add_gaussian_noise(self, sigma=1.0) -> Self: 1412 """ 1413 Add gaussian noise to point positions. 1414 An extra array is added named "GaussianNoise" with the displacements. 1415 1416 Arguments: 1417 sigma : (float) 1418 nr. of standard deviations, expressed in percent of the diagonal size of mesh. 1419 Can also be a list `[sigma_x, sigma_y, sigma_z]`. 1420 1421 Example: 1422 ```python 1423 from vedo import Sphere 1424 Sphere().add_gaussian_noise(1.0).point_size(8).show().close() 1425 ``` 1426 """ 1427 sz = self.diagonal_size() 1428 pts = self.vertices 1429 n = len(pts) 1430 ns = (np.random.randn(n, 3) * sigma) * (sz / 100) 1431 vpts = vtki.vtkPoints() 1432 vpts.SetNumberOfPoints(n) 1433 vpts.SetData(utils.numpy2vtk(pts + ns, dtype=np.float32)) 1434 self.dataset.SetPoints(vpts) 1435 self.dataset.GetPoints().Modified() 1436 self.pointdata["GaussianNoise"] = -ns 1437 self.pipeline = utils.OperationNode( 1438 "gaussian_noise", parents=[self], shape="egg", comment=f"sigma = {sigma}" 1439 ) 1440 return self
Add gaussian noise to point positions. An extra array is added named "GaussianNoise" with the displacements.
Arguments:
- sigma : (float)
nr. of standard deviations, expressed in percent of the diagonal size of mesh.
Can also be a list
[sigma_x, sigma_y, sigma_z]
.
Example:
from vedo import Sphere Sphere().add_gaussian_noise(1.0).point_size(8).show().close()
1442 def closest_point( 1443 self, pt, n=1, radius=None, return_point_id=False, return_cell_id=False 1444 ) -> Union[List[int], int, np.ndarray]: 1445 """ 1446 Find the closest point(s) on a mesh given from the input point `pt`. 1447 1448 Arguments: 1449 n : (int) 1450 if greater than 1, return a list of n ordered closest points 1451 radius : (float) 1452 if given, get all points within that radius. Then n is ignored. 1453 return_point_id : (bool) 1454 return point ID instead of coordinates 1455 return_cell_id : (bool) 1456 return cell ID in which the closest point sits 1457 1458 Examples: 1459 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1460 - [fitplanes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitplanes.py) 1461 - [quadratic_morphing.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/quadratic_morphing.py) 1462 1463 .. note:: 1464 The appropriate tree search locator is built on the fly and cached for speed. 1465 1466 If you want to reset it use `mymesh.point_locator=None` 1467 and / or `mymesh.cell_locator=None`. 1468 """ 1469 if len(pt) != 3: 1470 pt = [pt[0], pt[1], 0] 1471 1472 # NB: every time the mesh moves or is warped the locators are set to None 1473 if ((n > 1 or radius) or (n == 1 and return_point_id)) and not return_cell_id: 1474 poly = None 1475 if not self.point_locator: 1476 poly = self.dataset 1477 self.point_locator = vtki.new("StaticPointLocator") 1478 self.point_locator.SetDataSet(poly) 1479 self.point_locator.BuildLocator() 1480 1481 ########## 1482 if radius: 1483 vtklist = vtki.vtkIdList() 1484 self.point_locator.FindPointsWithinRadius(radius, pt, vtklist) 1485 elif n > 1: 1486 vtklist = vtki.vtkIdList() 1487 self.point_locator.FindClosestNPoints(n, pt, vtklist) 1488 else: # n==1 hence return_point_id==True 1489 ######## 1490 return self.point_locator.FindClosestPoint(pt) 1491 ######## 1492 1493 if return_point_id: 1494 ######## 1495 return utils.vtk2numpy(vtklist) 1496 ######## 1497 1498 if not poly: 1499 poly = self.dataset 1500 trgp = [] 1501 for i in range(vtklist.GetNumberOfIds()): 1502 trgp_ = [0, 0, 0] 1503 vi = vtklist.GetId(i) 1504 poly.GetPoints().GetPoint(vi, trgp_) 1505 trgp.append(trgp_) 1506 ######## 1507 return np.array(trgp) 1508 ######## 1509 1510 else: 1511 1512 if not self.cell_locator: 1513 poly = self.dataset 1514 1515 # As per Miquel example with limbs the vtkStaticCellLocator doesnt work !! 1516 # https://discourse.vtk.org/t/vtkstaticcelllocator-problem-vtk9-0-3/7854/4 1517 if vedo.vtk_version[0] >= 9 and vedo.vtk_version[1] > 0: 1518 self.cell_locator = vtki.new("StaticCellLocator") 1519 else: 1520 self.cell_locator = vtki.new("CellLocator") 1521 1522 self.cell_locator.SetDataSet(poly) 1523 self.cell_locator.BuildLocator() 1524 1525 if radius is not None: 1526 vedo.printc("Warning: closest_point() with radius is not implemented for cells.", c='r') 1527 1528 if n != 1: 1529 vedo.printc("Warning: closest_point() with n>1 is not implemented for cells.", c='r') 1530 1531 trgp = [0, 0, 0] 1532 cid = vtki.mutable(0) 1533 dist2 = vtki.mutable(0) 1534 subid = vtki.mutable(0) 1535 self.cell_locator.FindClosestPoint(pt, trgp, cid, subid, dist2) 1536 1537 if return_cell_id: 1538 return int(cid) 1539 1540 return np.array(trgp)
Find the closest point(s) on a mesh given from the input point pt
.
Arguments:
- n : (int) if greater than 1, return a list of n ordered closest points
- radius : (float) if given, get all points within that radius. Then n is ignored.
- return_point_id : (bool) return point ID instead of coordinates
- return_cell_id : (bool) return cell ID in which the closest point sits
Examples:
The appropriate tree search locator is built on the fly and cached for speed.
If you want to reset it use mymesh.point_locator=None
and / or mymesh.cell_locator=None
.
1542 def auto_distance(self) -> np.ndarray: 1543 """ 1544 Calculate the distance to the closest point in the same cloud of points. 1545 The output is stored in a new pointdata array called "AutoDistance", 1546 and it is also returned by the function. 1547 """ 1548 points = self.vertices 1549 if not self.point_locator: 1550 self.point_locator = vtki.new("StaticPointLocator") 1551 self.point_locator.SetDataSet(self.dataset) 1552 self.point_locator.BuildLocator() 1553 qs = [] 1554 vtklist = vtki.vtkIdList() 1555 vtkpoints = self.dataset.GetPoints() 1556 for p in points: 1557 self.point_locator.FindClosestNPoints(2, p, vtklist) 1558 q = [0, 0, 0] 1559 pid = vtklist.GetId(1) 1560 vtkpoints.GetPoint(pid, q) 1561 qs.append(q) 1562 dists = np.linalg.norm(points - np.array(qs), axis=1) 1563 self.pointdata["AutoDistance"] = dists 1564 return dists
Calculate the distance to the closest point in the same cloud of points. The output is stored in a new pointdata array called "AutoDistance", and it is also returned by the function.
1566 def hausdorff_distance(self, points) -> float: 1567 """ 1568 Compute the Hausdorff distance to the input point set. 1569 Returns a single `float`. 1570 1571 Example: 1572 ```python 1573 from vedo import * 1574 t = np.linspace(0, 2*np.pi, 100) 1575 x = 4/3 * sin(t)**3 1576 y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12 1577 pol1 = Line(np.c_[x,y], closed=True).triangulate() 1578 pol2 = Polygon(nsides=5).pos(2,2) 1579 d12 = pol1.distance_to(pol2) 1580 d21 = pol2.distance_to(pol1) 1581 pol1.lw(0).cmap("viridis") 1582 pol2.lw(0).cmap("viridis") 1583 print("distance d12, d21 :", min(d12), min(d21)) 1584 print("hausdorff distance:", pol1.hausdorff_distance(pol2)) 1585 print("chamfer distance :", pol1.chamfer_distance(pol2)) 1586 show(pol1, pol2, axes=1) 1587 ``` 1588 ![](https://vedo.embl.es/images/feats/heart.png) 1589 """ 1590 hp = vtki.new("HausdorffDistancePointSetFilter") 1591 hp.SetInputData(0, self.dataset) 1592 hp.SetInputData(1, points.dataset) 1593 hp.SetTargetDistanceMethodToPointToCell() 1594 hp.Update() 1595 return hp.GetHausdorffDistance()
Compute the Hausdorff distance to the input point set.
Returns a single float
.
Example:
from vedo import * t = np.linspace(0, 2*np.pi, 100) x = 4/3 * sin(t)**3 y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12 pol1 = Line(np.c_[x,y], closed=True).triangulate() pol2 = Polygon(nsides=5).pos(2,2) d12 = pol1.distance_to(pol2) d21 = pol2.distance_to(pol1) pol1.lw(0).cmap("viridis") pol2.lw(0).cmap("viridis") print("distance d12, d21 :", min(d12), min(d21)) print("hausdorff distance:", pol1.hausdorff_distance(pol2)) print("chamfer distance :", pol1.chamfer_distance(pol2)) show(pol1, pol2, axes=1)
1597 def chamfer_distance(self, pcloud) -> float: 1598 """ 1599 Compute the Chamfer distance to the input point set. 1600 1601 Example: 1602 ```python 1603 from vedo import * 1604 cloud1 = np.random.randn(1000, 3) 1605 cloud2 = np.random.randn(1000, 3) + [1, 2, 3] 1606 c1 = Points(cloud1, r=5, c="red") 1607 c2 = Points(cloud2, r=5, c="green") 1608 d = c1.chamfer_distance(c2) 1609 show(f"Chamfer distance = {d}", c1, c2, axes=1).close() 1610 ``` 1611 """ 1612 # Definition of Chamfer distance may vary, here we use the average 1613 if not pcloud.point_locator: 1614 pcloud.point_locator = vtki.new("PointLocator") 1615 pcloud.point_locator.SetDataSet(pcloud.dataset) 1616 pcloud.point_locator.BuildLocator() 1617 if not self.point_locator: 1618 self.point_locator = vtki.new("PointLocator") 1619 self.point_locator.SetDataSet(self.dataset) 1620 self.point_locator.BuildLocator() 1621 1622 ps1 = self.vertices 1623 ps2 = pcloud.vertices 1624 1625 ids12 = [] 1626 for p in ps1: 1627 pid12 = pcloud.point_locator.FindClosestPoint(p) 1628 ids12.append(pid12) 1629 deltav = ps2[ids12] - ps1 1630 da = np.mean(np.linalg.norm(deltav, axis=1)) 1631 1632 ids21 = [] 1633 for p in ps2: 1634 pid21 = self.point_locator.FindClosestPoint(p) 1635 ids21.append(pid21) 1636 deltav = ps1[ids21] - ps2 1637 db = np.mean(np.linalg.norm(deltav, axis=1)) 1638 return (da + db) / 2
Compute the Chamfer distance to the input point set.
Example:
from vedo import * cloud1 = np.random.randn(1000, 3) cloud2 = np.random.randn(1000, 3) + [1, 2, 3] c1 = Points(cloud1, r=5, c="red") c2 = Points(cloud2, r=5, c="green") d = c1.chamfer_distance(c2) show(f"Chamfer distance = {d}", c1, c2, axes=1).close()
1640 def remove_outliers(self, radius: float, neighbors=5) -> Self: 1641 """ 1642 Remove outliers from a cloud of points within the specified `radius` search. 1643 1644 Arguments: 1645 radius : (float) 1646 Specify the local search radius. 1647 neighbors : (int) 1648 Specify the number of neighbors that a point must have, 1649 within the specified radius, for the point to not be considered isolated. 1650 1651 Examples: 1652 - [clustering.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/clustering.py) 1653 1654 ![](https://vedo.embl.es/images/basic/clustering.png) 1655 """ 1656 removal = vtki.new("RadiusOutlierRemoval") 1657 removal.SetInputData(self.dataset) 1658 removal.SetRadius(radius) 1659 removal.SetNumberOfNeighbors(neighbors) 1660 removal.GenerateOutliersOff() 1661 removal.Update() 1662 inputobj = removal.GetOutput() 1663 if inputobj.GetNumberOfCells() == 0: 1664 carr = vtki.vtkCellArray() 1665 for i in range(inputobj.GetNumberOfPoints()): 1666 carr.InsertNextCell(1) 1667 carr.InsertCellPoint(i) 1668 inputobj.SetVerts(carr) 1669 self._update(removal.GetOutput()) 1670 self.pipeline = utils.OperationNode("remove_outliers", parents=[self]) 1671 return self
Remove outliers from a cloud of points within the specified radius
search.
Arguments:
- radius : (float) Specify the local search radius.
- neighbors : (int) Specify the number of neighbors that a point must have, within the specified radius, for the point to not be considered isolated.
Examples:
1673 def relax_point_positions( 1674 self, 1675 n=10, 1676 iters=10, 1677 sub_iters=10, 1678 packing_factor=1, 1679 max_step=0, 1680 constraints=(), 1681 ) -> Self: 1682 """ 1683 Smooth mesh or points with a 1684 [Laplacian algorithm](https://vtk.org/doc/nightly/html/classvtkPointSmoothingFilter.html) 1685 variant. This modifies the coordinates of the input points by adjusting their positions 1686 to create a smooth distribution (and thereby form a pleasing packing of the points). 1687 Smoothing is performed by considering the effects of neighboring points on one another 1688 it uses a cubic cutoff function to produce repulsive forces between close points 1689 and attractive forces that are a little further away. 1690 1691 In general, the larger the neighborhood size, the greater the reduction in high frequency 1692 information. The memory and computational requirements of the algorithm may also 1693 significantly increase. 1694 1695 The algorithm incrementally adjusts the point positions through an iterative process. 1696 Basically points are moved due to the influence of neighboring points. 1697 1698 As points move, both the local connectivity and data attributes associated with each point 1699 must be updated. Rather than performing these expensive operations after every iteration, 1700 a number of sub-iterations can be specified. If so, then the neighborhood and attribute 1701 value updates occur only every sub iteration, which can improve performance significantly. 1702 1703 Arguments: 1704 n : (int) 1705 neighborhood size to calculate the Laplacian. 1706 iters : (int) 1707 number of iterations. 1708 sub_iters : (int) 1709 number of sub-iterations, i.e. the number of times the neighborhood and attribute 1710 value updates occur during each iteration. 1711 packing_factor : (float) 1712 adjust convergence speed. 1713 max_step : (float) 1714 Specify the maximum smoothing step size for each smoothing iteration. 1715 This limits the the distance over which a point can move in each iteration. 1716 As in all iterative methods, the stability of the process is sensitive to this parameter. 1717 In general, small step size and large numbers of iterations are more stable than a larger 1718 step size and a smaller numbers of iterations. 1719 constraints : (dict) 1720 dictionary of constraints. 1721 Point constraints are used to prevent points from moving, 1722 or to move only on a plane. This can prevent shrinking or growing point clouds. 1723 If enabled, a local topological analysis is performed to determine whether a point 1724 should be marked as fixed" i.e., never moves, or the point only moves on a plane, 1725 or the point can move freely. 1726 If all points in the neighborhood surrounding a point are in the cone defined by 1727 `fixed_angle`, then the point is classified as fixed. 1728 If all points in the neighborhood surrounding a point are in the cone defined by 1729 `boundary_angle`, then the point is classified as lying on a plane. 1730 Angles are expressed in degrees. 1731 1732 Example: 1733 ```py 1734 import numpy as np 1735 from vedo import Points, show 1736 from vedo.pyplot import histogram 1737 1738 vpts1 = Points(np.random.rand(10_000, 3)) 1739 dists = vpts1.auto_distance() 1740 h1 = histogram(dists, xlim=(0,0.08)).clone2d() 1741 1742 vpts2 = vpts1.clone().relax_point_positions(n=100, iters=20, sub_iters=10) 1743 dists = vpts2.auto_distance() 1744 h2 = histogram(dists, xlim=(0,0.08)).clone2d() 1745 1746 show([[vpts1, h1], [vpts2, h2]], N=2).close() 1747 ``` 1748 """ 1749 smooth = vtki.new("PointSmoothingFilter") 1750 smooth.SetInputData(self.dataset) 1751 smooth.SetSmoothingModeToUniform() 1752 smooth.SetNumberOfIterations(iters) 1753 smooth.SetNumberOfSubIterations(sub_iters) 1754 smooth.SetPackingFactor(packing_factor) 1755 if self.point_locator: 1756 smooth.SetLocator(self.point_locator) 1757 if not max_step: 1758 max_step = self.diagonal_size() / 100 1759 smooth.SetMaximumStepSize(max_step) 1760 smooth.SetNeighborhoodSize(n) 1761 if constraints: 1762 fixed_angle = constraints.get("fixed_angle", 45) 1763 boundary_angle = constraints.get("boundary_angle", 110) 1764 smooth.EnableConstraintsOn() 1765 smooth.SetFixedAngle(fixed_angle) 1766 smooth.SetBoundaryAngle(boundary_angle) 1767 smooth.GenerateConstraintScalarsOn() 1768 smooth.GenerateConstraintNormalsOn() 1769 smooth.Update() 1770 self._update(smooth.GetOutput()) 1771 self.metadata["PackingRadius"] = smooth.GetPackingRadius() 1772 self.pipeline = utils.OperationNode("relax_point_positions", parents=[self]) 1773 return self
Smooth mesh or points with a Laplacian algorithm variant. This modifies the coordinates of the input points by adjusting their positions to create a smooth distribution (and thereby form a pleasing packing of the points). Smoothing is performed by considering the effects of neighboring points on one another it uses a cubic cutoff function to produce repulsive forces between close points and attractive forces that are a little further away.
In general, the larger the neighborhood size, the greater the reduction in high frequency information. The memory and computational requirements of the algorithm may also significantly increase.
The algorithm incrementally adjusts the point positions through an iterative process. Basically points are moved due to the influence of neighboring points.
As points move, both the local connectivity and data attributes associated with each point must be updated. Rather than performing these expensive operations after every iteration, a number of sub-iterations can be specified. If so, then the neighborhood and attribute value updates occur only every sub iteration, which can improve performance significantly.
Arguments:
- n : (int) neighborhood size to calculate the Laplacian.
- iters : (int) number of iterations.
- sub_iters : (int) number of sub-iterations, i.e. the number of times the neighborhood and attribute value updates occur during each iteration.
- packing_factor : (float) adjust convergence speed.
- max_step : (float) Specify the maximum smoothing step size for each smoothing iteration. This limits the the distance over which a point can move in each iteration. As in all iterative methods, the stability of the process is sensitive to this parameter. In general, small step size and large numbers of iterations are more stable than a larger step size and a smaller numbers of iterations.
- constraints : (dict)
dictionary of constraints.
Point constraints are used to prevent points from moving,
or to move only on a plane. This can prevent shrinking or growing point clouds.
If enabled, a local topological analysis is performed to determine whether a point
should be marked as fixed" i.e., never moves, or the point only moves on a plane,
or the point can move freely.
If all points in the neighborhood surrounding a point are in the cone defined by
fixed_angle
, then the point is classified as fixed. If all points in the neighborhood surrounding a point are in the cone defined 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()
1775 def smooth_mls_1d(self, f=0.2, radius=None, n=0) -> Self: 1776 """ 1777 Smooth mesh or points with a `Moving Least Squares` variant. 1778 The point data array "Variances" will contain the residue calculated for each point. 1779 1780 Arguments: 1781 f : (float) 1782 smoothing factor - typical range is [0,2]. 1783 radius : (float) 1784 radius search in absolute units. 1785 If set then `f` is ignored. 1786 n : (int) 1787 number of neighbours to be used for the fit. 1788 If set then `f` and `radius` are ignored. 1789 1790 Examples: 1791 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1792 - [skeletonize.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/skeletonize.py) 1793 1794 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1795 """ 1796 coords = self.vertices 1797 ncoords = len(coords) 1798 1799 if n: 1800 Ncp = n 1801 elif radius: 1802 Ncp = 1 1803 else: 1804 Ncp = int(ncoords * f / 10) 1805 if Ncp < 5: 1806 vedo.logger.warning(f"Please choose a fraction higher than {f}") 1807 Ncp = 5 1808 1809 variances, newline = [], [] 1810 for p in coords: 1811 points = self.closest_point(p, n=Ncp, radius=radius) 1812 if len(points) < 4: 1813 continue 1814 1815 points = np.array(points) 1816 pointsmean = points.mean(axis=0) # plane center 1817 _, dd, vv = np.linalg.svd(points - pointsmean) 1818 newp = np.dot(p - pointsmean, vv[0]) * vv[0] + pointsmean 1819 variances.append(dd[1] + dd[2]) 1820 newline.append(newp) 1821 1822 self.pointdata["Variances"] = np.array(variances).astype(np.float32) 1823 self.vertices = newline 1824 self.pipeline = utils.OperationNode("smooth_mls_1d", parents=[self]) 1825 return self
Smooth mesh or points with a Moving Least Squares
variant.
The point data array "Variances" will contain the residue calculated for each point.
Arguments:
- f : (float) smoothing factor - typical range is [0,2].
- radius : (float)
radius search in absolute units.
If set then
f
is ignored. - n : (int)
number of neighbours to be used for the fit.
If set then
f
andradius
are ignored.
Examples:
1827 def smooth_mls_2d(self, f=0.2, radius=None, n=0) -> Self: 1828 """ 1829 Smooth mesh or points with a `Moving Least Squares` algorithm variant. 1830 1831 The `mesh.pointdata['MLSVariance']` array will contain the residue calculated for each point. 1832 When a radius is specified, points that are isolated will not be moved and will get 1833 a 0 entry in array `mesh.pointdata['MLSValidPoint']`. 1834 1835 Arguments: 1836 f : (float) 1837 smoothing factor - typical range is [0, 2]. 1838 radius : (float | array) 1839 radius search in absolute units. Can be single value (float) or sequence 1840 for adaptive smoothing. If set then `f` is ignored. 1841 n : (int) 1842 number of neighbours to be used for the fit. 1843 If set then `f` and `radius` are ignored. 1844 1845 Examples: 1846 - [moving_least_squares2D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares2D.py) 1847 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1848 1849 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1850 """ 1851 coords = self.vertices 1852 ncoords = len(coords) 1853 1854 if n: 1855 Ncp = n 1856 radius = None 1857 elif radius is not None: 1858 Ncp = 1 1859 else: 1860 Ncp = int(ncoords * f / 100) 1861 if Ncp < 4: 1862 vedo.logger.error(f"please choose a f-value higher than {f}") 1863 Ncp = 4 1864 1865 variances, newpts, valid = [], [], [] 1866 radius_is_sequence = utils.is_sequence(radius) 1867 1868 pb = None 1869 if ncoords > 10000: 1870 pb = utils.ProgressBar(0, ncoords, delay=3) 1871 1872 for i, p in enumerate(coords): 1873 if pb: 1874 pb.print("smooth_mls_2d working ...") 1875 1876 # if a radius was provided for each point 1877 if radius_is_sequence: 1878 pts = self.closest_point(p, n=Ncp, radius=radius[i]) 1879 else: 1880 pts = self.closest_point(p, n=Ncp, radius=radius) 1881 1882 if len(pts) > 3: 1883 ptsmean = pts.mean(axis=0) # plane center 1884 _, dd, vv = np.linalg.svd(pts - ptsmean) 1885 cv = np.cross(vv[0], vv[1]) 1886 t = (np.dot(cv, ptsmean) - np.dot(cv, p)) / np.dot(cv, cv) 1887 newpts.append(p + cv * t) 1888 variances.append(dd[2]) 1889 if radius is not None: 1890 valid.append(1) 1891 else: 1892 newpts.append(p) 1893 variances.append(0) 1894 if radius is not None: 1895 valid.append(0) 1896 1897 if radius is not None: 1898 self.pointdata["MLSValidPoint"] = np.array(valid).astype(np.uint8) 1899 self.pointdata["MLSVariance"] = np.array(variances).astype(np.float32) 1900 1901 self.vertices = newpts 1902 1903 self.pipeline = utils.OperationNode("smooth_mls_2d", parents=[self]) 1904 return self
Smooth mesh or points with a Moving Least Squares
algorithm variant.
The mesh.pointdata['MLSVariance']
array will contain the residue calculated for each point.
When a radius is specified, points that are isolated will not be moved and will get
a 0 entry in array mesh.pointdata['MLSValidPoint']
.
Arguments:
- f : (float) smoothing factor - typical range is [0, 2].
- radius : (float | array)
radius search in absolute units. Can be single value (float) or sequence
for adaptive smoothing. If set then
f
is ignored. - n : (int)
number of neighbours to be used for the fit.
If set then
f
andradius
are ignored.
Examples:
1906 def smooth_lloyd_2d(self, iterations=2, bounds=None, options="Qbb Qc Qx") -> Self: 1907 """ 1908 Lloyd relaxation of a 2D pointcloud. 1909 1910 Arguments: 1911 iterations : (int) 1912 number of iterations. 1913 bounds : (list) 1914 bounding box of the domain. 1915 options : (str) 1916 options for the Qhull algorithm. 1917 """ 1918 # Credits: https://hatarilabs.com/ih-en/ 1919 # tutorial-to-create-a-geospatial-voronoi-sh-mesh-with-python-scipy-and-geopandas 1920 from scipy.spatial import Voronoi as scipy_voronoi 1921 1922 def _constrain_points(points): 1923 # Update any points that have drifted beyond the boundaries of this space 1924 if bounds is not None: 1925 for point in points: 1926 if point[0] < bounds[0]: point[0] = bounds[0] 1927 if point[0] > bounds[1]: point[0] = bounds[1] 1928 if point[1] < bounds[2]: point[1] = bounds[2] 1929 if point[1] > bounds[3]: point[1] = bounds[3] 1930 return points 1931 1932 def _find_centroid(vertices): 1933 # The equation for the method used here to find the centroid of a 1934 # 2D polygon is given here: https://en.wikipedia.org/wiki/Centroid#Of_a_polygon 1935 area = 0 1936 centroid_x = 0 1937 centroid_y = 0 1938 for i in range(len(vertices) - 1): 1939 step = (vertices[i, 0] * vertices[i + 1, 1]) - (vertices[i + 1, 0] * vertices[i, 1]) 1940 centroid_x += (vertices[i, 0] + vertices[i + 1, 0]) * step 1941 centroid_y += (vertices[i, 1] + vertices[i + 1, 1]) * step 1942 area += step 1943 if area: 1944 centroid_x = (1.0 / (3.0 * area)) * centroid_x 1945 centroid_y = (1.0 / (3.0 * area)) * centroid_y 1946 # prevent centroids from escaping bounding box 1947 return _constrain_points([[centroid_x, centroid_y]])[0] 1948 1949 def _relax(voron): 1950 # Moves each point to the centroid of its cell in the voronoi 1951 # map to "relax" the points (i.e. jitter the points so as 1952 # to spread them out within the space). 1953 centroids = [] 1954 for idx in voron.point_region: 1955 # the region is a series of indices into voronoi.vertices 1956 # remove point at infinity, designated by index -1 1957 region = [i for i in voron.regions[idx] if i != -1] 1958 # enclose the polygon 1959 region = region + [region[0]] 1960 verts = voron.vertices[region] 1961 # find the centroid of those vertices 1962 centroids.append(_find_centroid(verts)) 1963 return _constrain_points(centroids) 1964 1965 if bounds is None: 1966 bounds = self.bounds() 1967 1968 pts = self.vertices[:, (0, 1)] 1969 for i in range(iterations): 1970 vor = scipy_voronoi(pts, qhull_options=options) 1971 _constrain_points(vor.vertices) 1972 pts = _relax(vor) 1973 out = Points(pts) 1974 out.name = "MeshSmoothLloyd2D" 1975 out.pipeline = utils.OperationNode("smooth_lloyd", parents=[self]) 1976 return out
Lloyd relaxation of a 2D pointcloud.
Arguments:
- iterations : (int) number of iterations.
- bounds : (list) bounding box of the domain.
- options : (str) options for the Qhull algorithm.
1978 def project_on_plane(self, plane="z", point=None, direction=None) -> Self: 1979 """ 1980 Project the mesh on one of the Cartesian planes. 1981 1982 Arguments: 1983 plane : (str, Plane) 1984 if plane is `str`, plane can be one of ['x', 'y', 'z'], 1985 represents x-plane, y-plane and z-plane, respectively. 1986 Otherwise, plane should be an instance of `vedo.shapes.Plane`. 1987 point : (float, array) 1988 if plane is `str`, point should be a float represents the intercept. 1989 Otherwise, point is the camera point of perspective projection 1990 direction : (array) 1991 direction of oblique projection 1992 1993 Note: 1994 Parameters `point` and `direction` are only used if the given plane 1995 is an instance of `vedo.shapes.Plane`. And one of these two params 1996 should be left as `None` to specify the projection type. 1997 1998 Example: 1999 ```python 2000 s.project_on_plane(plane='z') # project to z-plane 2001 plane = Plane(pos=(4, 8, -4), normal=(-1, 0, 1), s=(5,5)) 2002 s.project_on_plane(plane=plane) # orthogonal projection 2003 s.project_on_plane(plane=plane, point=(6, 6, 6)) # perspective projection 2004 s.project_on_plane(plane=plane, direction=(1, 2, -1)) # oblique projection 2005 ``` 2006 2007 Examples: 2008 - [silhouette2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette2.py) 2009 2010 ![](https://vedo.embl.es/images/basic/silhouette2.png) 2011 """ 2012 coords = self.vertices 2013 2014 if plane == "x": 2015 coords[:, 0] = self.transform.position[0] 2016 intercept = self.xbounds()[0] if point is None else point 2017 self.x(intercept) 2018 elif plane == "y": 2019 coords[:, 1] = self.transform.position[1] 2020 intercept = self.ybounds()[0] if point is None else point 2021 self.y(intercept) 2022 elif plane == "z": 2023 coords[:, 2] = self.transform.position[2] 2024 intercept = self.zbounds()[0] if point is None else point 2025 self.z(intercept) 2026 2027 elif isinstance(plane, vedo.shapes.Plane): 2028 normal = plane.normal / np.linalg.norm(plane.normal) 2029 pl = np.hstack((normal, -np.dot(plane.pos(), normal))).reshape(4, 1) 2030 if direction is None and point is None: 2031 # orthogonal projection 2032 pt = np.hstack((normal, [0])).reshape(4, 1) 2033 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T # python3 only 2034 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2035 2036 elif direction is None: 2037 # perspective projection 2038 pt = np.hstack((np.array(point), [1])).reshape(4, 1) 2039 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2040 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2041 2042 elif point is None: 2043 # oblique projection 2044 pt = np.hstack((np.array(direction), [0])).reshape(4, 1) 2045 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2046 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2047 2048 coords = np.concatenate([coords, np.ones((coords.shape[:-1] + (1,)))], axis=-1) 2049 # coords = coords @ proj_mat.T 2050 coords = np.matmul(coords, proj_mat.T) 2051 coords = coords[:, :3] / coords[:, 3:] 2052 2053 else: 2054 vedo.logger.error(f"unknown plane {plane}") 2055 raise RuntimeError() 2056 2057 self.alpha(0.1) 2058 self.vertices = coords 2059 return self
Project the mesh on one of the Cartesian planes.
Arguments:
- plane : (str, Plane)
if plane is
str
, plane can be one of ['x', 'y', 'z'], represents x-plane, y-plane and z-plane, respectively. Otherwise, plane should be an instance 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:
2061 def warp(self, source, target, sigma=1.0, mode="3d") -> Self: 2062 """ 2063 "Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set 2064 of source and target landmarks. Any point on the mesh close to a source landmark will 2065 be moved to a place close to the corresponding target landmark. 2066 The points in between are interpolated smoothly using 2067 Bookstein's Thin Plate Spline algorithm. 2068 2069 Transformation object can be accessed with `mesh.transform`. 2070 2071 Arguments: 2072 sigma : (float) 2073 specify the 'stiffness' of the spline. 2074 mode : (str) 2075 set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes) 2076 2077 Examples: 2078 - [interpolate_field.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/interpolate_field.py) 2079 - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py) 2080 - [warp2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp2.py) 2081 - [warp3.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp3.py) 2082 - [warp4a.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4a.py) 2083 - [warp4b.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4b.py) 2084 - [warp6.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp6.py) 2085 2086 ![](https://vedo.embl.es/images/advanced/warp2.png) 2087 """ 2088 parents = [self] 2089 2090 try: 2091 source = source.vertices 2092 parents.append(source) 2093 except AttributeError: 2094 source = utils.make3d(source) 2095 2096 try: 2097 target = target.vertices 2098 parents.append(target) 2099 except AttributeError: 2100 target = utils.make3d(target) 2101 2102 ns = len(source) 2103 nt = len(target) 2104 if ns != nt: 2105 vedo.logger.error(f"#source {ns} != {nt} #target points") 2106 raise RuntimeError() 2107 2108 NLT = NonLinearTransform() 2109 NLT.source_points = source 2110 NLT.target_points = target 2111 self.apply_transform(NLT) 2112 2113 self.pipeline = utils.OperationNode("warp", parents=parents) 2114 return self
"Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set of source and target landmarks. Any point on the mesh close to a source landmark will be moved to a place close to the corresponding target landmark. The points in between are interpolated smoothly using Bookstein's Thin Plate Spline algorithm.
Transformation object can be accessed with mesh.transform
.
Arguments:
- sigma : (float) specify the 'stiffness' of the spline.
- mode : (str) set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes)
Examples:
2116 def cut_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0), invert=False) -> Self: 2117 """ 2118 Cut the mesh with the plane defined by a point and a normal. 2119 2120 Arguments: 2121 origin : (array) 2122 the cutting plane goes through this point 2123 normal : (array) 2124 normal of the cutting plane 2125 2126 Example: 2127 ```python 2128 from vedo import Cube 2129 cube = Cube().cut_with_plane(normal=(1,1,1)) 2130 cube.back_color('pink').show().close() 2131 ``` 2132 ![](https://vedo.embl.es/images/feats/cut_with_plane_cube.png) 2133 2134 Examples: 2135 - [trail.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/trail.py) 2136 2137 ![](https://vedo.embl.es/images/simulations/trail.gif) 2138 2139 Check out also: 2140 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`. 2141 """ 2142 s = str(normal) 2143 if "x" in s: 2144 normal = (1, 0, 0) 2145 if "-" in s: 2146 normal = -np.array(normal) 2147 elif "y" in s: 2148 normal = (0, 1, 0) 2149 if "-" in s: 2150 normal = -np.array(normal) 2151 elif "z" in s: 2152 normal = (0, 0, 1) 2153 if "-" in s: 2154 normal = -np.array(normal) 2155 plane = vtki.vtkPlane() 2156 plane.SetOrigin(origin) 2157 plane.SetNormal(normal) 2158 2159 clipper = vtki.new("ClipPolyData") 2160 clipper.SetInputData(self.dataset) 2161 clipper.SetClipFunction(plane) 2162 clipper.GenerateClippedOutputOff() 2163 clipper.GenerateClipScalarsOff() 2164 clipper.SetInsideOut(invert) 2165 clipper.SetValue(0) 2166 clipper.Update() 2167 2168 self._update(clipper.GetOutput()) 2169 2170 self.pipeline = utils.OperationNode("cut_with_plane", parents=[self]) 2171 return self
Cut the mesh with the plane defined by a point and a normal.
Arguments:
- origin : (array) the cutting plane goes through this point
- normal : (array) normal of the cutting plane
Example:
from vedo import Cube cube = Cube().cut_with_plane(normal=(1,1,1)) cube.back_color('pink').show().close()
Examples:
Check out also:
2173 def cut_with_planes(self, origins, normals, invert=False) -> Self: 2174 """ 2175 Cut the mesh with a convex set of planes defined by points and normals. 2176 2177 Arguments: 2178 origins : (array) 2179 each cutting plane goes through this point 2180 normals : (array) 2181 normal of each of the cutting planes 2182 invert : (bool) 2183 if True, cut outside instead of inside 2184 2185 Check out also: 2186 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()` 2187 """ 2188 2189 vpoints = vtki.vtkPoints() 2190 for p in utils.make3d(origins): 2191 vpoints.InsertNextPoint(p) 2192 normals = utils.make3d(normals) 2193 2194 planes = vtki.vtkPlanes() 2195 planes.SetPoints(vpoints) 2196 planes.SetNormals(utils.numpy2vtk(normals, dtype=float)) 2197 2198 clipper = vtki.new("ClipPolyData") 2199 clipper.SetInputData(self.dataset) 2200 clipper.SetInsideOut(invert) 2201 clipper.SetClipFunction(planes) 2202 clipper.GenerateClippedOutputOff() 2203 clipper.GenerateClipScalarsOff() 2204 clipper.SetValue(0) 2205 clipper.Update() 2206 2207 self._update(clipper.GetOutput()) 2208 2209 self.pipeline = utils.OperationNode("cut_with_planes", parents=[self]) 2210 return self
Cut the mesh with a convex set of planes defined by points and normals.
Arguments:
- origins : (array) each cutting plane goes through this point
- normals : (array) normal of each of the cutting planes
- invert : (bool) if True, cut outside instead of inside
Check out also:
2212 def cut_with_box(self, bounds, invert=False) -> Self: 2213 """ 2214 Cut the current mesh with a box or a set of boxes. 2215 This is much faster than `cut_with_mesh()`. 2216 2217 Input `bounds` can be either: 2218 - a Mesh or Points object 2219 - a list of 6 number representing a bounding box `[xmin,xmax, ymin,ymax, zmin,zmax]` 2220 - a list of bounding boxes like the above: `[[xmin1,...], [xmin2,...], ...]` 2221 2222 Example: 2223 ```python 2224 from vedo import Sphere, Cube, show 2225 mesh = Sphere(r=1, res=50) 2226 box = Cube(side=1.5).wireframe() 2227 mesh.cut_with_box(box) 2228 show(mesh, box, axes=1).close() 2229 ``` 2230 ![](https://vedo.embl.es/images/feats/cut_with_box_cube.png) 2231 2232 Check out also: 2233 `cut_with_line()`, `cut_with_plane()`, `cut_with_cylinder()` 2234 """ 2235 if isinstance(bounds, Points): 2236 bounds = bounds.bounds() 2237 2238 box = vtki.new("Box") 2239 if utils.is_sequence(bounds[0]): 2240 for bs in bounds: 2241 box.AddBounds(bs) 2242 else: 2243 box.SetBounds(bounds) 2244 2245 clipper = vtki.new("ClipPolyData") 2246 clipper.SetInputData(self.dataset) 2247 clipper.SetClipFunction(box) 2248 clipper.SetInsideOut(not invert) 2249 clipper.GenerateClippedOutputOff() 2250 clipper.GenerateClipScalarsOff() 2251 clipper.SetValue(0) 2252 clipper.Update() 2253 self._update(clipper.GetOutput()) 2254 2255 self.pipeline = utils.OperationNode("cut_with_box", parents=[self]) 2256 return self
Cut the current mesh with a box or a set of boxes.
This is much faster than cut_with_mesh()
.
Input bounds
can be either:
- a Mesh or Points object
- a list of 6 number representing a bounding box
[xmin,xmax, ymin,ymax, zmin,zmax]
- a list of bounding boxes like the above:
[[xmin1,...], [xmin2,...], ...]
Example:
from vedo import Sphere, Cube, show mesh = Sphere(r=1, res=50) box = Cube(side=1.5).wireframe() mesh.cut_with_box(box) show(mesh, box, axes=1).close()
Check out also:
2258 def cut_with_line(self, points, invert=False, closed=True) -> Self: 2259 """ 2260 Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter. 2261 The polyline is defined by a set of points (z-coordinates are ignored). 2262 This is much faster than `cut_with_mesh()`. 2263 2264 Check out also: 2265 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2266 """ 2267 pplane = vtki.new("PolyPlane") 2268 if isinstance(points, Points): 2269 points = points.vertices.tolist() 2270 2271 if closed: 2272 if isinstance(points, np.ndarray): 2273 points = points.tolist() 2274 points.append(points[0]) 2275 2276 vpoints = vtki.vtkPoints() 2277 for p in points: 2278 if len(p) == 2: 2279 p = [p[0], p[1], 0.0] 2280 vpoints.InsertNextPoint(p) 2281 2282 n = len(points) 2283 polyline = vtki.new("PolyLine") 2284 polyline.Initialize(n, vpoints) 2285 polyline.GetPointIds().SetNumberOfIds(n) 2286 for i in range(n): 2287 polyline.GetPointIds().SetId(i, i) 2288 pplane.SetPolyLine(polyline) 2289 2290 clipper = vtki.new("ClipPolyData") 2291 clipper.SetInputData(self.dataset) 2292 clipper.SetClipFunction(pplane) 2293 clipper.SetInsideOut(invert) 2294 clipper.GenerateClippedOutputOff() 2295 clipper.GenerateClipScalarsOff() 2296 clipper.SetValue(0) 2297 clipper.Update() 2298 self._update(clipper.GetOutput()) 2299 2300 self.pipeline = utils.OperationNode("cut_with_line", parents=[self]) 2301 return self
Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter.
The polyline is defined by a set of points (z-coordinates are ignored).
This is much faster than cut_with_mesh()
.
Check out also:
2379 def cut_with_cylinder(self, center=(0, 0, 0), axis=(0, 0, 1), r=1, invert=False) -> Self: 2380 """ 2381 Cut the current mesh with an infinite cylinder. 2382 This is much faster than `cut_with_mesh()`. 2383 2384 Arguments: 2385 center : (array) 2386 the center of the cylinder 2387 normal : (array) 2388 direction of the cylinder axis 2389 r : (float) 2390 radius of the cylinder 2391 2392 Example: 2393 ```python 2394 from vedo import Disc, show 2395 disc = Disc(r1=1, r2=1.2) 2396 mesh = disc.extrude(3, res=50).linewidth(1) 2397 mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True) 2398 show(mesh, axes=1).close() 2399 ``` 2400 ![](https://vedo.embl.es/images/feats/cut_with_cylinder.png) 2401 2402 Examples: 2403 - [optics_main1.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/optics_main1.py) 2404 2405 Check out also: 2406 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2407 """ 2408 s = str(axis) 2409 if "x" in s: 2410 axis = (1, 0, 0) 2411 elif "y" in s: 2412 axis = (0, 1, 0) 2413 elif "z" in s: 2414 axis = (0, 0, 1) 2415 cyl = vtki.new("Cylinder") 2416 cyl.SetCenter(center) 2417 cyl.SetAxis(axis[0], axis[1], axis[2]) 2418 cyl.SetRadius(r) 2419 2420 clipper = vtki.new("ClipPolyData") 2421 clipper.SetInputData(self.dataset) 2422 clipper.SetClipFunction(cyl) 2423 clipper.SetInsideOut(not invert) 2424 clipper.GenerateClippedOutputOff() 2425 clipper.GenerateClipScalarsOff() 2426 clipper.SetValue(0) 2427 clipper.Update() 2428 self._update(clipper.GetOutput()) 2429 2430 self.pipeline = utils.OperationNode("cut_with_cylinder", parents=[self]) 2431 return self
Cut the current mesh with an infinite cylinder.
This is much faster than cut_with_mesh()
.
Arguments:
- center : (array) the center of the cylinder
- normal : (array) direction of the cylinder axis
- r : (float) radius of the cylinder
Example:
from vedo import Disc, show disc = Disc(r1=1, r2=1.2) mesh = disc.extrude(3, res=50).linewidth(1) mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True) show(mesh, axes=1).close()
Examples:
Check out also:
2433 def cut_with_sphere(self, center=(0, 0, 0), r=1.0, invert=False) -> Self: 2434 """ 2435 Cut the current mesh with an sphere. 2436 This is much faster than `cut_with_mesh()`. 2437 2438 Arguments: 2439 center : (array) 2440 the center of the sphere 2441 r : (float) 2442 radius of the sphere 2443 2444 Example: 2445 ```python 2446 from vedo import Disc, show 2447 disc = Disc(r1=1, r2=1.2) 2448 mesh = disc.extrude(3, res=50).linewidth(1) 2449 mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True) 2450 show(mesh, axes=1).close() 2451 ``` 2452 ![](https://vedo.embl.es/images/feats/cut_with_sphere.png) 2453 2454 Check out also: 2455 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2456 """ 2457 sph = vtki.new("Sphere") 2458 sph.SetCenter(center) 2459 sph.SetRadius(r) 2460 2461 clipper = vtki.new("ClipPolyData") 2462 clipper.SetInputData(self.dataset) 2463 clipper.SetClipFunction(sph) 2464 clipper.SetInsideOut(not invert) 2465 clipper.GenerateClippedOutputOff() 2466 clipper.GenerateClipScalarsOff() 2467 clipper.SetValue(0) 2468 clipper.Update() 2469 self._update(clipper.GetOutput()) 2470 self.pipeline = utils.OperationNode("cut_with_sphere", parents=[self]) 2471 return self
Cut the current mesh with an sphere.
This is much faster than cut_with_mesh()
.
Arguments:
- center : (array) the center of the sphere
- r : (float) radius of the sphere
Example:
from vedo import Disc, show disc = Disc(r1=1, r2=1.2) mesh = disc.extrude(3, res=50).linewidth(1) mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True) show(mesh, axes=1).close()
Check out also:
2473 def cut_with_mesh(self, mesh, invert=False, keep=False) -> Union[Self, "vedo.Assembly"]: 2474 """ 2475 Cut an `Mesh` mesh with another `Mesh`. 2476 2477 Use `invert` to invert the selection. 2478 2479 Use `keep` to keep the cutoff part, in this case an `Assembly` is returned: 2480 the "cut" object and the "discarded" part of the original object. 2481 You can access both via `assembly.unpack()` method. 2482 2483 Example: 2484 ```python 2485 from vedo import * 2486 arr = np.random.randn(100000, 3)/2 2487 pts = Points(arr).c('red3').pos(5,0,0) 2488 cube = Cube().pos(4,0.5,0) 2489 assem = pts.cut_with_mesh(cube, keep=True) 2490 show(assem.unpack(), axes=1).close() 2491 ``` 2492 ![](https://vedo.embl.es/images/feats/cut_with_mesh.png) 2493 2494 Check out also: 2495 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2496 """ 2497 polymesh = mesh.dataset 2498 poly = self.dataset 2499 2500 # Create an array to hold distance information 2501 signed_distances = vtki.vtkFloatArray() 2502 signed_distances.SetNumberOfComponents(1) 2503 signed_distances.SetName("SignedDistances") 2504 2505 # implicit function that will be used to slice the mesh 2506 ippd = vtki.new("ImplicitPolyDataDistance") 2507 ippd.SetInput(polymesh) 2508 2509 # Evaluate the signed distance function at all of the grid points 2510 for pointId in range(poly.GetNumberOfPoints()): 2511 p = poly.GetPoint(pointId) 2512 signed_distance = ippd.EvaluateFunction(p) 2513 signed_distances.InsertNextValue(signed_distance) 2514 2515 currentscals = poly.GetPointData().GetScalars() 2516 if currentscals: 2517 currentscals = currentscals.GetName() 2518 2519 poly.GetPointData().AddArray(signed_distances) 2520 poly.GetPointData().SetActiveScalars("SignedDistances") 2521 2522 clipper = vtki.new("ClipPolyData") 2523 clipper.SetInputData(poly) 2524 clipper.SetInsideOut(not invert) 2525 clipper.SetGenerateClippedOutput(keep) 2526 clipper.SetValue(0.0) 2527 clipper.Update() 2528 cpoly = clipper.GetOutput() 2529 2530 if keep: 2531 kpoly = clipper.GetOutput(1) 2532 2533 vis = False 2534 if currentscals: 2535 cpoly.GetPointData().SetActiveScalars(currentscals) 2536 vis = self.mapper.GetScalarVisibility() 2537 2538 self._update(cpoly) 2539 2540 self.pointdata.remove("SignedDistances") 2541 self.mapper.SetScalarVisibility(vis) 2542 if keep: 2543 if isinstance(self, vedo.Mesh): 2544 cutoff = vedo.Mesh(kpoly) 2545 else: 2546 cutoff = vedo.Points(kpoly) 2547 # cutoff = self.__class__(kpoly) # this does not work properly 2548 cutoff.properties = vtki.vtkProperty() 2549 cutoff.properties.DeepCopy(self.properties) 2550 cutoff.actor.SetProperty(cutoff.properties) 2551 cutoff.c("k5").alpha(0.2) 2552 return vedo.Assembly([self, cutoff]) 2553 2554 self.pipeline = utils.OperationNode("cut_with_mesh", parents=[self, mesh]) 2555 return self
Cut an Mesh
mesh with another Mesh
.
Use invert
to invert the selection.
Use keep
to keep the cutoff part, in this case an Assembly
is returned:
the "cut" object and the "discarded" part of the original object.
You can access both via assembly.unpack()
method.
Example:
from vedo import *
arr = np.random.randn(100000, 3)/2
pts = Points(arr).c('red3').pos(5,0,0)
cube = Cube().pos(4,0.5,0)
assem = pts.cut_with_mesh(cube, keep=True)
show(assem.unpack(), axes=1).close()
Check out also:
2557 def cut_with_point_loop( 2558 self, points, invert=False, on="points", include_boundary=False 2559 ) -> Self: 2560 """ 2561 Cut an `Mesh` object with a set of points forming a closed loop. 2562 2563 Arguments: 2564 invert : (bool) 2565 invert selection (inside-out) 2566 on : (str) 2567 if 'cells' will extract the whole cells lying inside (or outside) the point loop 2568 include_boundary : (bool) 2569 include cells lying exactly on the boundary line. Only relevant on 'cells' mode 2570 2571 Examples: 2572 - [cut_with_points1.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points1.py) 2573 2574 ![](https://vedo.embl.es/images/advanced/cutWithPoints1.png) 2575 2576 - [cut_with_points2.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points2.py) 2577 2578 ![](https://vedo.embl.es/images/advanced/cutWithPoints2.png) 2579 """ 2580 if isinstance(points, Points): 2581 parents = [points] 2582 vpts = points.dataset.GetPoints() 2583 points = points.vertices 2584 else: 2585 parents = [self] 2586 vpts = vtki.vtkPoints() 2587 points = utils.make3d(points) 2588 for p in points: 2589 vpts.InsertNextPoint(p) 2590 2591 if "cell" in on: 2592 ippd = vtki.new("ImplicitSelectionLoop") 2593 ippd.SetLoop(vpts) 2594 ippd.AutomaticNormalGenerationOn() 2595 clipper = vtki.new("ExtractPolyDataGeometry") 2596 clipper.SetInputData(self.dataset) 2597 clipper.SetImplicitFunction(ippd) 2598 clipper.SetExtractInside(not invert) 2599 clipper.SetExtractBoundaryCells(include_boundary) 2600 else: 2601 spol = vtki.new("SelectPolyData") 2602 spol.SetLoop(vpts) 2603 spol.GenerateSelectionScalarsOn() 2604 spol.GenerateUnselectedOutputOff() 2605 spol.SetInputData(self.dataset) 2606 spol.Update() 2607 clipper = vtki.new("ClipPolyData") 2608 clipper.SetInputData(spol.GetOutput()) 2609 clipper.SetInsideOut(not invert) 2610 clipper.SetValue(0.0) 2611 clipper.Update() 2612 self._update(clipper.GetOutput()) 2613 2614 self.pipeline = utils.OperationNode("cut_with_pointloop", parents=parents) 2615 return self
Cut an Mesh
object with a set of points forming a closed loop.
Arguments:
- invert : (bool) invert selection (inside-out)
- on : (str) if 'cells' will extract the whole cells lying inside (or outside) the point loop
- include_boundary : (bool) include cells lying exactly on the boundary line. Only relevant on 'cells' mode
Examples:
2617 def cut_with_scalar(self, value: float, name="", invert=False) -> Self: 2618 """ 2619 Cut a mesh or point cloud with some input scalar point-data. 2620 2621 Arguments: 2622 value : (float) 2623 cutting value 2624 name : (str) 2625 array name of the scalars to be used 2626 invert : (bool) 2627 flip selection 2628 2629 Example: 2630 ```python 2631 from vedo import * 2632 s = Sphere().lw(1) 2633 pts = s.vertices 2634 scalars = np.sin(3*pts[:,2]) + pts[:,0] 2635 s.pointdata["somevalues"] = scalars 2636 s.cut_with_scalar(0.3) 2637 s.cmap("Spectral", "somevalues").add_scalarbar() 2638 s.show(axes=1).close() 2639 ``` 2640 ![](https://vedo.embl.es/images/feats/cut_with_scalars.png) 2641 """ 2642 if name: 2643 self.pointdata.select(name) 2644 clipper = vtki.new("ClipPolyData") 2645 clipper.SetInputData(self.dataset) 2646 clipper.SetValue(value) 2647 clipper.GenerateClippedOutputOff() 2648 clipper.SetInsideOut(not invert) 2649 clipper.Update() 2650 self._update(clipper.GetOutput()) 2651 self.pipeline = utils.OperationNode("cut_with_scalar", parents=[self]) 2652 return self
Cut a mesh or point cloud with some input scalar point-data.
Arguments:
- value : (float) cutting value
- name : (str) array name of the scalars to be used
- invert : (bool) flip selection
Example:
from vedo import * s = Sphere().lw(1) pts = s.vertices scalars = np.sin(3*pts[:,2]) + pts[:,0] s.pointdata["somevalues"] = scalars s.cut_with_scalar(0.3) s.cmap("Spectral", "somevalues").add_scalarbar() s.show(axes=1).close()
2654 def crop(self, 2655 top=None, bottom=None, right=None, left=None, front=None, back=None, 2656 bounds=()) -> Self: 2657 """ 2658 Crop an `Mesh` object. 2659 2660 Arguments: 2661 top : (float) 2662 fraction to crop from the top plane (positive z) 2663 bottom : (float) 2664 fraction to crop from the bottom plane (negative z) 2665 front : (float) 2666 fraction to crop from the front plane (positive y) 2667 back : (float) 2668 fraction to crop from the back plane (negative y) 2669 right : (float) 2670 fraction to crop from the right plane (positive x) 2671 left : (float) 2672 fraction to crop from the left plane (negative x) 2673 bounds : (list) 2674 bounding box of the crop region as `[x0,x1, y0,y1, z0,z1]` 2675 2676 Example: 2677 ```python 2678 from vedo import Sphere 2679 Sphere().crop(right=0.3, left=0.1).show() 2680 ``` 2681 ![](https://user-images.githubusercontent.com/32848391/57081955-0ef1e800-6cf6-11e9-99de-b45220939bc9.png) 2682 """ 2683 if not len(bounds): 2684 pos = np.array(self.pos()) 2685 x0, x1, y0, y1, z0, z1 = self.bounds() 2686 x0, y0, z0 = [x0, y0, z0] - pos 2687 x1, y1, z1 = [x1, y1, z1] - pos 2688 2689 dx, dy, dz = x1 - x0, y1 - y0, z1 - z0 2690 if top: 2691 z1 = z1 - top * dz 2692 if bottom: 2693 z0 = z0 + bottom * dz 2694 if front: 2695 y1 = y1 - front * dy 2696 if back: 2697 y0 = y0 + back * dy 2698 if right: 2699 x1 = x1 - right * dx 2700 if left: 2701 x0 = x0 + left * dx 2702 bounds = (x0, x1, y0, y1, z0, z1) 2703 2704 cu = vtki.new("Box") 2705 cu.SetBounds(bounds) 2706 2707 clipper = vtki.new("ClipPolyData") 2708 clipper.SetInputData(self.dataset) 2709 clipper.SetClipFunction(cu) 2710 clipper.InsideOutOn() 2711 clipper.GenerateClippedOutputOff() 2712 clipper.GenerateClipScalarsOff() 2713 clipper.SetValue(0) 2714 clipper.Update() 2715 self._update(clipper.GetOutput()) 2716 2717 self.pipeline = utils.OperationNode( 2718 "crop", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 2719 ) 2720 return self
Crop an Mesh
object.
Arguments:
- top : (float) fraction to crop from the top plane (positive z)
- bottom : (float) fraction to crop from the bottom plane (negative z)
- front : (float) fraction to crop from the front plane (positive y)
- back : (float) fraction to crop from the back plane (negative y)
- right : (float) fraction to crop from the right plane (positive x)
- left : (float) fraction to crop from the left plane (negative x)
- bounds : (list)
bounding box of the crop region as
[x0,x1, y0,y1, z0,z1]
Example:
from vedo import Sphere Sphere().crop(right=0.3, left=0.1).show()
2722 def generate_surface_halo( 2723 self, 2724 distance=0.05, 2725 res=(50, 50, 50), 2726 bounds=(), 2727 maxdist=None, 2728 ) -> "vedo.Mesh": 2729 """ 2730 Generate the surface halo which sits at the specified distance from the input one. 2731 2732 Arguments: 2733 distance : (float) 2734 distance from the input surface 2735 res : (int) 2736 resolution of the surface 2737 bounds : (list) 2738 bounding box of the surface 2739 maxdist : (float) 2740 maximum distance to generate the surface 2741 """ 2742 if not bounds: 2743 bounds = self.bounds() 2744 2745 if not maxdist: 2746 maxdist = self.diagonal_size() / 2 2747 2748 imp = vtki.new("ImplicitModeller") 2749 imp.SetInputData(self.dataset) 2750 imp.SetSampleDimensions(res) 2751 if maxdist: 2752 imp.SetMaximumDistance(maxdist) 2753 if len(bounds) == 6: 2754 imp.SetModelBounds(bounds) 2755 contour = vtki.new("ContourFilter") 2756 contour.SetInputConnection(imp.GetOutputPort()) 2757 contour.SetValue(0, distance) 2758 contour.Update() 2759 out = vedo.Mesh(contour.GetOutput()) 2760 out.c("lightblue").alpha(0.25).lighting("off") 2761 out.pipeline = utils.OperationNode("generate_surface_halo", parents=[self]) 2762 return out
Generate the surface halo which sits at the specified distance from the input one.
Arguments:
- distance : (float) distance from the input surface
- res : (int) resolution of the surface
- bounds : (list) bounding box of the surface
- maxdist : (float) maximum distance to generate the surface
2764 def generate_mesh( 2765 self, 2766 line_resolution=None, 2767 mesh_resolution=None, 2768 smooth=0.0, 2769 jitter=0.001, 2770 grid=None, 2771 quads=False, 2772 invert=False, 2773 ) -> Self: 2774 """ 2775 Generate a polygonal Mesh from a closed contour line. 2776 If line is not closed it will be closed with a straight segment. 2777 2778 Check also `generate_delaunay2d()`. 2779 2780 Arguments: 2781 line_resolution : (int) 2782 resolution of the contour line. The default is None, in this case 2783 the contour is not resampled. 2784 mesh_resolution : (int) 2785 resolution of the internal triangles not touching the boundary. 2786 smooth : (float) 2787 smoothing of the contour before meshing. 2788 jitter : (float) 2789 add a small noise to the internal points. 2790 grid : (Grid) 2791 manually pass a Grid object. The default is True. 2792 quads : (bool) 2793 generate a mesh of quads instead of triangles. 2794 invert : (bool) 2795 flip the line orientation. The default is False. 2796 2797 Examples: 2798 - [line2mesh_tri.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_tri.py) 2799 2800 ![](https://vedo.embl.es/images/advanced/line2mesh_tri.jpg) 2801 2802 - [line2mesh_quads.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_quads.py) 2803 2804 ![](https://vedo.embl.es/images/advanced/line2mesh_quads.png) 2805 """ 2806 if line_resolution is None: 2807 contour = vedo.shapes.Line(self.vertices) 2808 else: 2809 contour = vedo.shapes.Spline(self.vertices, smooth=smooth, res=line_resolution) 2810 contour.clean() 2811 2812 length = contour.length() 2813 density = length / contour.npoints 2814 # print(f"tomesh():\n\tline length = {length}") 2815 # print(f"\tdensity = {density} length/pt_separation") 2816 2817 x0, x1 = contour.xbounds() 2818 y0, y1 = contour.ybounds() 2819 2820 if grid is None: 2821 if mesh_resolution is None: 2822 resx = int((x1 - x0) / density + 0.5) 2823 resy = int((y1 - y0) / density + 0.5) 2824 # print(f"tmesh_resolution = {[resx, resy]}") 2825 else: 2826 if utils.is_sequence(mesh_resolution): 2827 resx, resy = mesh_resolution 2828 else: 2829 resx, resy = mesh_resolution, mesh_resolution 2830 grid = vedo.shapes.Grid( 2831 [(x0 + x1) / 2, (y0 + y1) / 2, 0], 2832 s=((x1 - x0) * 1.025, (y1 - y0) * 1.025), 2833 res=(resx, resy), 2834 ) 2835 else: 2836 grid = grid.clone() 2837 2838 cpts = contour.vertices 2839 2840 # make sure it's closed 2841 p0, p1 = cpts[0], cpts[-1] 2842 nj = max(2, int(utils.mag(p1 - p0) / density + 0.5)) 2843 joinline = vedo.shapes.Line(p1, p0, res=nj) 2844 contour = vedo.merge(contour, joinline).subsample(0.0001) 2845 2846 ####################################### quads 2847 if quads: 2848 cmesh = grid.clone().cut_with_point_loop(contour, on="cells", invert=invert) 2849 cmesh.wireframe(False).lw(0.5) 2850 cmesh.pipeline = utils.OperationNode( 2851 "generate_mesh", 2852 parents=[self, contour], 2853 comment=f"#quads {cmesh.dataset.GetNumberOfCells()}", 2854 ) 2855 return cmesh 2856 ############################################# 2857 2858 grid_tmp = grid.vertices.copy() 2859 2860 if jitter: 2861 np.random.seed(0) 2862 sigma = 1.0 / np.sqrt(grid.npoints) * grid.diagonal_size() * jitter 2863 # print(f"\tsigma jittering = {sigma}") 2864 grid_tmp += np.random.rand(grid.npoints, 3) * sigma 2865 grid_tmp[:, 2] = 0.0 2866 2867 todel = [] 2868 density /= np.sqrt(3) 2869 vgrid_tmp = Points(grid_tmp) 2870 2871 for p in contour.vertices: 2872 out = vgrid_tmp.closest_point(p, radius=density, return_point_id=True) 2873 todel += out.tolist() 2874 2875 grid_tmp = grid_tmp.tolist() 2876 for index in sorted(list(set(todel)), reverse=True): 2877 del grid_tmp[index] 2878 2879 points = contour.vertices.tolist() + grid_tmp 2880 if invert: 2881 boundary = list(reversed(range(contour.npoints))) 2882 else: 2883 boundary = list(range(contour.npoints)) 2884 2885 dln = Points(points).generate_delaunay2d(mode="xy", boundaries=[boundary]) 2886 dln.compute_normals(points=False) # fixes reversd faces 2887 dln.lw(1) 2888 2889 dln.pipeline = utils.OperationNode( 2890 "generate_mesh", 2891 parents=[self, contour], 2892 comment=f"#cells {dln.dataset.GetNumberOfCells()}", 2893 ) 2894 return dln
Generate a polygonal Mesh from a closed contour line. If line is not closed it will be closed with a straight segment.
Check also generate_delaunay2d()
.
Arguments:
- line_resolution : (int) resolution of the contour line. The default is None, in this case the contour is not resampled.
- mesh_resolution : (int) resolution of the internal triangles not touching the boundary.
- smooth : (float) smoothing of the contour before meshing.
- jitter : (float) add a small noise to the internal points.
- grid : (Grid) manually pass a Grid object. The default is True.
- quads : (bool) generate a mesh of quads instead of triangles.
- invert : (bool) flip the line orientation. The default is False.
Examples:
2896 def reconstruct_surface( 2897 self, 2898 dims=(100, 100, 100), 2899 radius=None, 2900 sample_size=None, 2901 hole_filling=True, 2902 bounds=(), 2903 padding=0.05, 2904 ) -> "vedo.Mesh": 2905 """ 2906 Surface reconstruction from a scattered cloud of points. 2907 2908 Arguments: 2909 dims : (int) 2910 number of voxels in x, y and z to control precision. 2911 radius : (float) 2912 radius of influence of each point. 2913 Smaller values generally improve performance markedly. 2914 Note that after the signed distance function is computed, 2915 any voxel taking on the value >= radius 2916 is presumed to be "unseen" or uninitialized. 2917 sample_size : (int) 2918 if normals are not present 2919 they will be calculated using this sample size per point. 2920 hole_filling : (bool) 2921 enables hole filling, this generates 2922 separating surfaces between the empty and unseen portions of the volume. 2923 bounds : (list) 2924 region in space in which to perform the sampling 2925 in format (xmin,xmax, ymin,ymax, zim, zmax) 2926 padding : (float) 2927 increase by this fraction the bounding box 2928 2929 Examples: 2930 - [recosurface.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/recosurface.py) 2931 2932 ![](https://vedo.embl.es/images/advanced/recosurface.png) 2933 """ 2934 if not utils.is_sequence(dims): 2935 dims = (dims, dims, dims) 2936 2937 sdf = vtki.new("SignedDistance") 2938 2939 if len(bounds) == 6: 2940 sdf.SetBounds(bounds) 2941 else: 2942 x0, x1, y0, y1, z0, z1 = self.bounds() 2943 sdf.SetBounds( 2944 x0 - (x1 - x0) * padding, 2945 x1 + (x1 - x0) * padding, 2946 y0 - (y1 - y0) * padding, 2947 y1 + (y1 - y0) * padding, 2948 z0 - (z1 - z0) * padding, 2949 z1 + (z1 - z0) * padding, 2950 ) 2951 2952 bb = sdf.GetBounds() 2953 if bb[0]==bb[1]: 2954 vedo.logger.warning("reconstruct_surface(): zero x-range") 2955 if bb[2]==bb[3]: 2956 vedo.logger.warning("reconstruct_surface(): zero y-range") 2957 if bb[4]==bb[5]: 2958 vedo.logger.warning("reconstruct_surface(): zero z-range") 2959 2960 pd = self.dataset 2961 2962 if pd.GetPointData().GetNormals(): 2963 sdf.SetInputData(pd) 2964 else: 2965 normals = vtki.new("PCANormalEstimation") 2966 normals.SetInputData(pd) 2967 if not sample_size: 2968 sample_size = int(pd.GetNumberOfPoints() / 50) 2969 normals.SetSampleSize(sample_size) 2970 normals.SetNormalOrientationToGraphTraversal() 2971 sdf.SetInputConnection(normals.GetOutputPort()) 2972 # print("Recalculating normals with sample size =", sample_size) 2973 2974 if radius is None: 2975 radius = self.diagonal_size() / (sum(dims) / 3) * 5 2976 # print("Calculating mesh from points with radius =", radius) 2977 2978 sdf.SetRadius(radius) 2979 sdf.SetDimensions(dims) 2980 sdf.Update() 2981 2982 surface = vtki.new("ExtractSurface") 2983 surface.SetRadius(radius * 0.99) 2984 surface.SetHoleFilling(hole_filling) 2985 surface.ComputeNormalsOff() 2986 surface.ComputeGradientsOff() 2987 surface.SetInputConnection(sdf.GetOutputPort()) 2988 surface.Update() 2989 m = vedo.mesh.Mesh(surface.GetOutput(), c=self.color()) 2990 2991 m.pipeline = utils.OperationNode( 2992 "reconstruct_surface", 2993 parents=[self], 2994 comment=f"#pts {m.dataset.GetNumberOfPoints()}", 2995 ) 2996 return m
Surface reconstruction from a scattered cloud of points.
Arguments:
- dims : (int) number of voxels in x, y and z to control precision.
- radius : (float) radius of influence of each point. Smaller values generally improve performance markedly. Note that after the signed distance function is computed, any voxel taking on the value >= radius is presumed to be "unseen" or uninitialized.
- sample_size : (int) if normals are not present they will be calculated using this sample size per point.
- hole_filling : (bool) enables hole filling, this generates separating surfaces between the empty and unseen portions of the volume.
- bounds : (list) region in space in which to perform the sampling in format (xmin,xmax, ymin,ymax, zim, zmax)
- padding : (float) increase by this fraction the bounding box
Examples:
2998 def compute_clustering(self, radius: float) -> Self: 2999 """ 3000 Cluster points in space. The `radius` is the radius of local search. 3001 3002 An array named "ClusterId" is added to `pointdata`. 3003 3004 Examples: 3005 - [clustering.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/clustering.py) 3006 3007 ![](https://vedo.embl.es/images/basic/clustering.png) 3008 """ 3009 cluster = vtki.new("EuclideanClusterExtraction") 3010 cluster.SetInputData(self.dataset) 3011 cluster.SetExtractionModeToAllClusters() 3012 cluster.SetRadius(radius) 3013 cluster.ColorClustersOn() 3014 cluster.Update() 3015 idsarr = cluster.GetOutput().GetPointData().GetArray("ClusterId") 3016 self.dataset.GetPointData().AddArray(idsarr) 3017 self.pipeline = utils.OperationNode( 3018 "compute_clustering", parents=[self], comment=f"radius = {radius}" 3019 ) 3020 return self
Cluster points in space. The radius
is the radius of local search.
An array named "ClusterId" is added to pointdata
.
Examples:
3022 def compute_connections(self, radius, mode=0, regions=(), vrange=(0, 1), seeds=(), angle=0.0) -> Self: 3023 """ 3024 Extracts and/or segments points from a point cloud based on geometric distance measures 3025 (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range. 3026 The default operation is to segment the points into "connected" regions where the connection 3027 is determined by an appropriate distance measure. Each region is given a region id. 3028 3029 Optionally, the filter can output the largest connected region of points; a particular region 3030 (via id specification); those regions that are seeded using a list of input point ids; 3031 or the region of points closest to a specified position. 3032 3033 The key parameter of this filter is the radius defining a sphere around each point which defines 3034 a local neighborhood: any other points in the local neighborhood are assumed connected to the point. 3035 Note that the radius is defined in absolute terms. 3036 3037 Other parameters are used to further qualify what it means to be a neighboring point. 3038 For example, scalar range and/or point normals can be used to further constrain the neighborhood. 3039 Also the extraction mode defines how the filter operates. 3040 By default, all regions are extracted but it is possible to extract particular regions; 3041 the region closest to a seed point; seeded regions; or the largest region found while processing. 3042 By default, all regions are extracted. 3043 3044 On output, all points are labeled with a region number. 3045 However note that the number of input and output points may not be the same: 3046 if not extracting all regions then the output size may be less than the input size. 3047 3048 Arguments: 3049 radius : (float) 3050 variable specifying a local sphere used to define local point neighborhood 3051 mode : (int) 3052 - 0, Extract all regions 3053 - 1, Extract point seeded regions 3054 - 2, Extract largest region 3055 - 3, Test specified regions 3056 - 4, Extract all regions with scalar connectivity 3057 - 5, Extract point seeded regions 3058 regions : (list) 3059 a list of non-negative regions id to extract 3060 vrange : (list) 3061 scalar range to use to extract points based on scalar connectivity 3062 seeds : (list) 3063 a list of non-negative point seed ids 3064 angle : (list) 3065 points are connected if the angle between their normals is 3066 within this angle threshold (expressed in degrees). 3067 """ 3068 # https://vtk.org/doc/nightly/html/classvtkConnectedPointsFilter.html 3069 cpf = vtki.new("ConnectedPointsFilter") 3070 cpf.SetInputData(self.dataset) 3071 cpf.SetRadius(radius) 3072 if mode == 0: # Extract all regions 3073 pass 3074 3075 elif mode == 1: # Extract point seeded regions 3076 cpf.SetExtractionModeToPointSeededRegions() 3077 for s in seeds: 3078 cpf.AddSeed(s) 3079 3080 elif mode == 2: # Test largest region 3081 cpf.SetExtractionModeToLargestRegion() 3082 3083 elif mode == 3: # Test specified regions 3084 cpf.SetExtractionModeToSpecifiedRegions() 3085 for r in regions: 3086 cpf.AddSpecifiedRegion(r) 3087 3088 elif mode == 4: # Extract all regions with scalar connectivity 3089 cpf.SetExtractionModeToLargestRegion() 3090 cpf.ScalarConnectivityOn() 3091 cpf.SetScalarRange(vrange[0], vrange[1]) 3092 3093 elif mode == 5: # Extract point seeded regions 3094 cpf.SetExtractionModeToLargestRegion() 3095 cpf.ScalarConnectivityOn() 3096 cpf.SetScalarRange(vrange[0], vrange[1]) 3097 cpf.AlignedNormalsOn() 3098 cpf.SetNormalAngle(angle) 3099 3100 cpf.Update() 3101 self._update(cpf.GetOutput(), reset_locators=False) 3102 return self
Extracts and/or segments points from a point cloud based on geometric distance measures (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range. The default operation is to segment the points into "connected" regions where the connection is determined by an appropriate distance measure. Each region is given a region id.
Optionally, the filter can output the largest connected region of points; a particular region (via id specification); those regions that are seeded using a list of input point ids; or the region of points closest to a specified position.
The key parameter of this filter is the radius defining a sphere around each point which defines a local neighborhood: any other points in the local neighborhood are assumed connected to the point. Note that the radius is defined in absolute terms.
Other parameters are used to further qualify what it means to be a neighboring point. For example, scalar range and/or point normals can be used to further constrain the neighborhood. Also the extraction mode defines how the filter operates. By default, all regions are extracted but it is possible to extract particular regions; the region closest to a seed point; seeded regions; or the largest region found while processing. By default, all regions are extracted.
On output, all points are labeled with a region number. However note that the number of input and output points may not be the same: if not extracting all regions then the output size may be less than the input size.
Arguments:
- radius : (float) variable specifying a local sphere used to define local point neighborhood
- mode : (int)
- 0, Extract all regions
- 1, Extract point seeded regions
- 2, Extract largest region
- 3, Test specified regions
- 4, Extract all regions with scalar connectivity
- 5, Extract point seeded regions
- regions : (list) a list of non-negative regions id to extract
- vrange : (list) scalar range to use to extract points based on scalar connectivity
- seeds : (list) a list of non-negative point seed ids
- angle : (list) points are connected if the angle between their normals is within this angle threshold (expressed in degrees).
3104 def compute_camera_distance(self) -> np.ndarray: 3105 """ 3106 Calculate the distance from points to the camera. 3107 3108 A pointdata array is created with name 'DistanceToCamera' and returned. 3109 """ 3110 if vedo.plotter_instance and vedo.plotter_instance.renderer: 3111 poly = self.dataset 3112 dc = vtki.new("DistanceToCamera") 3113 dc.SetInputData(poly) 3114 dc.SetRenderer(vedo.plotter_instance.renderer) 3115 dc.Update() 3116 self._update(dc.GetOutput(), reset_locators=False) 3117 return self.pointdata["DistanceToCamera"] 3118 return np.array([])
Calculate the distance from points to the camera.
A pointdata array is created with name 'DistanceToCamera' and returned.
3120 def densify(self, target_distance=0.1, nclosest=6, radius=None, niter=1, nmax=None) -> Self: 3121 """ 3122 Return a copy of the cloud with new added points. 3123 The new points are created in such a way that all points in any local neighborhood are 3124 within a target distance of one another. 3125 3126 For each input point, the distance to all points in its neighborhood is computed. 3127 If any of its neighbors is further than the target distance, 3128 the edge connecting the point and its neighbor is bisected and 3129 a new point is inserted at the bisection point. 3130 A single pass is completed once all the input points are visited. 3131 Then the process repeats to the number of iterations. 3132 3133 Examples: 3134 - [densifycloud.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/densifycloud.py) 3135 3136 ![](https://vedo.embl.es/images/volumetric/densifycloud.png) 3137 3138 .. note:: 3139 Points will be created in an iterative fashion until all points in their 3140 local neighborhood are the target distance apart or less. 3141 Note that the process may terminate early due to the 3142 number of iterations. By default the target distance is set to 0.5. 3143 Note that the target_distance should be less than the radius 3144 or nothing will change on output. 3145 3146 .. warning:: 3147 This class can generate a lot of points very quickly. 3148 The maximum number of iterations is by default set to =1.0 for this reason. 3149 Increase the number of iterations very carefully. 3150 Also, `nmax` can be set to limit the explosion of points. 3151 It is also recommended that a N closest neighborhood is used. 3152 3153 """ 3154 src = vtki.new("ProgrammableSource") 3155 opts = self.vertices 3156 3157 def _read_points(): 3158 output = src.GetPolyDataOutput() 3159 points = vtki.vtkPoints() 3160 for p in opts: 3161 points.InsertNextPoint(p) 3162 output.SetPoints(points) 3163 3164 src.SetExecuteMethod(_read_points) 3165 3166 dens = vtki.new("DensifyPointCloudFilter") 3167 dens.SetInputConnection(src.GetOutputPort()) 3168 dens.InterpolateAttributeDataOn() 3169 dens.SetTargetDistance(target_distance) 3170 dens.SetMaximumNumberOfIterations(niter) 3171 if nmax: 3172 dens.SetMaximumNumberOfPoints(nmax) 3173 3174 if radius: 3175 dens.SetNeighborhoodTypeToRadius() 3176 dens.SetRadius(radius) 3177 elif nclosest: 3178 dens.SetNeighborhoodTypeToNClosest() 3179 dens.SetNumberOfClosestPoints(nclosest) 3180 else: 3181 vedo.logger.error("set either radius or nclosest") 3182 raise RuntimeError() 3183 dens.Update() 3184 pts = utils.vtk2numpy(dens.GetOutput().GetPoints().GetData()) 3185 cld = Points(pts, c=None).point_size(self.properties.GetPointSize()) 3186 cld.interpolate_data_from(self, n=nclosest, radius=radius) 3187 cld.name = "DensifiedCloud" 3188 3189 cld.pipeline = utils.OperationNode( 3190 "densify", 3191 parents=[self], 3192 c="#e9c46a:", 3193 comment=f"#pts {cld.dataset.GetNumberOfPoints()}", 3194 ) 3195 return cld
Return a copy of the cloud with new added points. The new points are created in such a way that all points in any local neighborhood are within a target distance of one another.
For each input point, the distance to all points in its neighborhood is computed. If any of its neighbors is further than the target distance, the edge connecting the point and its neighbor is bisected and a new point is inserted at the bisection point. A single pass is completed once all the input points are visited. Then the process repeats to the number of iterations.
Examples:
Points will be created in an iterative fashion until all points in their local neighborhood are the target distance apart or less. Note that the process may terminate early due to the number of iterations. By default the target distance is set to 0.5. Note that the target_distance should be less than the radius or nothing will change on output.
This class can generate a lot of points very quickly.
The maximum number of iterations is by default set to =1.0 for this reason.
Increase the number of iterations very carefully.
Also, nmax
can be set to limit the explosion of points.
It is also recommended that a N closest neighborhood is used.
3201 def density( 3202 self, dims=(40, 40, 40), bounds=None, radius=None, compute_gradient=False, locator=None 3203 ) -> "vedo.Volume": 3204 """ 3205 Generate a density field from a point cloud. Input can also be a set of 3D coordinates. 3206 Output is a `Volume`. 3207 3208 The local neighborhood is specified as the `radius` around each sample position (each voxel). 3209 If left to None, the radius is automatically computed as the diagonal of the bounding box 3210 and can be accessed via `vol.metadata["radius"]`. 3211 The density is expressed as the number of counts in the radius search. 3212 3213 Arguments: 3214 dims : (int, list) 3215 number of voxels in x, y and z of the output Volume. 3216 compute_gradient : (bool) 3217 Turn on/off the generation of the gradient vector, 3218 gradient magnitude scalar, and function classification scalar. 3219 By default this is off. Note that this will increase execution time 3220 and the size of the output. (The names of these point data arrays are: 3221 "Gradient", "Gradient Magnitude", and "Classification") 3222 locator : (vtkPointLocator) 3223 can be assigned from a previous call for speed (access it via `object.point_locator`). 3224 3225 Examples: 3226 - [plot_density3d.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/plot_density3d.py) 3227 3228 ![](https://vedo.embl.es/images/pyplot/plot_density3d.png) 3229 """ 3230 pdf = vtki.new("PointDensityFilter") 3231 pdf.SetInputData(self.dataset) 3232 3233 if not utils.is_sequence(dims): 3234 dims = [dims, dims, dims] 3235 3236 if bounds is None: 3237 bounds = list(self.bounds()) 3238 elif len(bounds) == 4: 3239 bounds = [*bounds, 0, 0] 3240 3241 if bounds[5] - bounds[4] == 0 or len(dims) == 2: # its 2D 3242 dims = list(dims) 3243 dims = [dims[0], dims[1], 2] 3244 diag = self.diagonal_size() 3245 bounds[5] = bounds[4] + diag / 1000 3246 pdf.SetModelBounds(bounds) 3247 3248 pdf.SetSampleDimensions(dims) 3249 3250 if locator: 3251 pdf.SetLocator(locator) 3252 3253 pdf.SetDensityEstimateToFixedRadius() 3254 if radius is None: 3255 radius = self.diagonal_size() / 20 3256 pdf.SetRadius(radius) 3257 pdf.SetComputeGradient(compute_gradient) 3258 pdf.Update() 3259 3260 vol = vedo.Volume(pdf.GetOutput()).mode(1) 3261 vol.name = "PointDensity" 3262 vol.metadata["radius"] = radius 3263 vol.locator = pdf.GetLocator() 3264 vol.pipeline = utils.OperationNode( 3265 "density", parents=[self], comment=f"dims={tuple(vol.dimensions())}" 3266 ) 3267 return vol
Generate a density field from a point cloud. Input can also be a set of 3D coordinates.
Output is a Volume
.
The local neighborhood is specified as the radius
around each sample position (each voxel).
If left to None, the radius is automatically computed as the diagonal of the bounding box
and can be accessed via vol.metadata["radius"]
.
The density is expressed as the number of counts in the radius search.
Arguments:
- dims : (int, list) number of voxels in x, y and z of the output Volume.
- compute_gradient : (bool) Turn on/off the generation of the gradient vector, gradient magnitude scalar, and function classification scalar. By default this is off. Note that this will increase execution time and the size of the output. (The names of these point data arrays are: "Gradient", "Gradient Magnitude", and "Classification")
- locator : (vtkPointLocator)
can be assigned from a previous call for speed (access it via
object.point_locator
).
Examples:
3270 def tovolume( 3271 self, 3272 kernel="shepard", 3273 radius=None, 3274 n=None, 3275 bounds=None, 3276 null_value=None, 3277 dims=(25, 25, 25), 3278 ) -> "vedo.Volume": 3279 """ 3280 Generate a `Volume` by interpolating a scalar 3281 or vector field which is only known on a scattered set of points or mesh. 3282 Available interpolation kernels are: shepard, gaussian, or linear. 3283 3284 Arguments: 3285 kernel : (str) 3286 interpolation kernel type [shepard] 3287 radius : (float) 3288 radius of the local search 3289 n : (int) 3290 number of point to use for interpolation 3291 bounds : (list) 3292 bounding box of the output Volume object 3293 dims : (list) 3294 dimensions of the output Volume object 3295 null_value : (float) 3296 value to be assigned to invalid points 3297 3298 Examples: 3299 - [interpolate_volume.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/interpolate_volume.py) 3300 3301 ![](https://vedo.embl.es/images/volumetric/59095175-1ec5a300-8918-11e9-8bc0-fd35c8981e2b.jpg) 3302 """ 3303 if radius is None and not n: 3304 vedo.logger.error("please set either radius or n") 3305 raise RuntimeError 3306 3307 poly = self.dataset 3308 3309 # Create a probe volume 3310 probe = vtki.vtkImageData() 3311 probe.SetDimensions(dims) 3312 if bounds is None: 3313 bounds = self.bounds() 3314 probe.SetOrigin(bounds[0], bounds[2], bounds[4]) 3315 probe.SetSpacing( 3316 (bounds[1] - bounds[0]) / dims[0], 3317 (bounds[3] - bounds[2]) / dims[1], 3318 (bounds[5] - bounds[4]) / dims[2], 3319 ) 3320 3321 if not self.point_locator: 3322 self.point_locator = vtki.new("PointLocator") 3323 self.point_locator.SetDataSet(poly) 3324 self.point_locator.BuildLocator() 3325 3326 if kernel == "shepard": 3327 kern = vtki.new("ShepardKernel") 3328 kern.SetPowerParameter(2) 3329 elif kernel == "gaussian": 3330 kern = vtki.new("GaussianKernel") 3331 elif kernel == "linear": 3332 kern = vtki.new("LinearKernel") 3333 else: 3334 vedo.logger.error("Error in tovolume(), available kernels are:") 3335 vedo.logger.error(" [shepard, gaussian, linear]") 3336 raise RuntimeError() 3337 3338 if radius: 3339 kern.SetRadius(radius) 3340 3341 interpolator = vtki.new("PointInterpolator") 3342 interpolator.SetInputData(probe) 3343 interpolator.SetSourceData(poly) 3344 interpolator.SetKernel(kern) 3345 interpolator.SetLocator(self.point_locator) 3346 3347 if n: 3348 kern.SetNumberOfPoints(n) 3349 kern.SetKernelFootprintToNClosest() 3350 else: 3351 kern.SetRadius(radius) 3352 3353 if null_value is not None: 3354 interpolator.SetNullValue(null_value) 3355 else: 3356 interpolator.SetNullPointsStrategyToClosestPoint() 3357 interpolator.Update() 3358 3359 vol = vedo.Volume(interpolator.GetOutput()) 3360 3361 vol.pipeline = utils.OperationNode( 3362 "signed_distance", 3363 parents=[self], 3364 comment=f"dims={tuple(vol.dimensions())}", 3365 c="#e9c46a:#0096c7", 3366 ) 3367 return vol
Generate a Volume
by interpolating a scalar
or vector field which is only known on a scattered set of points or mesh.
Available interpolation kernels are: shepard, gaussian, or linear.
Arguments:
- kernel : (str) interpolation kernel type [shepard]
- radius : (float) radius of the local search
- n : (int) number of point to use for interpolation
- bounds : (list) bounding box of the output Volume object
- dims : (list) dimensions of the output Volume object
- null_value : (float) value to be assigned to invalid points
Examples:
3370 def generate_segments(self, istart=0, rmax=1e30, niter=3) -> "vedo.shapes.Lines": 3371 """ 3372 Generate a line segments from a set of points. 3373 The algorithm is based on the closest point search. 3374 3375 Returns a `Line` object. 3376 This object contains the a metadata array of used vertex counts in "UsedVertexCount" 3377 and the sum of the length of the segments in "SegmentsLengthSum". 3378 3379 Arguments: 3380 istart : (int) 3381 index of the starting point 3382 rmax : (float) 3383 maximum length of a segment 3384 niter : (int) 3385 number of iterations or passes through the points 3386 3387 Examples: 3388 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 3389 """ 3390 points = self.vertices 3391 segments = [] 3392 dists = [] 3393 n = len(points) 3394 used = np.zeros(n, dtype=int) 3395 for _ in range(niter): 3396 i = istart 3397 for _ in range(n): 3398 p = points[i] 3399 ids = self.closest_point(p, n=4, return_point_id=True) 3400 j = ids[1] 3401 if used[j] > 1 or [j, i] in segments: 3402 j = ids[2] 3403 if used[j] > 1: 3404 j = ids[3] 3405 d = np.linalg.norm(p - points[j]) 3406 if used[j] > 1 or used[i] > 1 or d > rmax: 3407 i += 1 3408 if i >= n: 3409 i = 0 3410 continue 3411 used[i] += 1 3412 used[j] += 1 3413 segments.append([i, j]) 3414 dists.append(d) 3415 i = j 3416 segments = np.array(segments, dtype=int) 3417 3418 lines = vedo.shapes.Lines(points[segments], c="k", lw=3) 3419 lines.metadata["UsedVertexCount"] = used 3420 lines.metadata["SegmentsLengthSum"] = np.sum(dists) 3421 lines.pipeline = utils.OperationNode("generate_segments", parents=[self]) 3422 lines.name = "Segments" 3423 return lines
Generate a line segments from a set of points. The algorithm is based on the closest point search.
Returns a Line
object.
This object contains the a metadata array of used vertex counts in "UsedVertexCount"
and the sum of the length of the segments in "SegmentsLengthSum".
Arguments:
- istart : (int) index of the starting point
- rmax : (float) maximum length of a segment
- niter : (int) number of iterations or passes through the points
Examples:
3425 def generate_delaunay2d( 3426 self, 3427 mode="scipy", 3428 boundaries=(), 3429 tol=None, 3430 alpha=0.0, 3431 offset=0.0, 3432 transform=None, 3433 ) -> "vedo.mesh.Mesh": 3434 """ 3435 Create a mesh from points in the XY plane. 3436 If `mode='fit'` then the filter computes a best fitting 3437 plane and projects the points onto it. 3438 3439 Check also `generate_mesh()`. 3440 3441 Arguments: 3442 tol : (float) 3443 specify a tolerance to control discarding of closely spaced points. 3444 This tolerance is specified as a fraction of the diagonal length of the bounding box of the points. 3445 alpha : (float) 3446 for a non-zero alpha value, only edges or triangles contained 3447 within a sphere centered at mesh vertices will be output. 3448 Otherwise, only triangles will be output. 3449 offset : (float) 3450 multiplier to control the size of the initial, bounding Delaunay triangulation. 3451 transform: (LinearTransform, NonLinearTransform) 3452 a transformation which is applied to points to generate a 2D problem. 3453 This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane. 3454 The points are transformed and triangulated. 3455 The topology of triangulated points is used as the output topology. 3456 3457 Examples: 3458 - [delaunay2d.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delaunay2d.py) 3459 3460 ![](https://vedo.embl.es/images/basic/delaunay2d.png) 3461 """ 3462 plist = self.vertices.copy() 3463 3464 ######################################################### 3465 if mode == "scipy": 3466 from scipy.spatial import Delaunay as scipy_delaunay 3467 3468 tri = scipy_delaunay(plist[:, 0:2]) 3469 return vedo.mesh.Mesh([plist, tri.simplices]) 3470 ########################################################## 3471 3472 pd = vtki.vtkPolyData() 3473 vpts = vtki.vtkPoints() 3474 vpts.SetData(utils.numpy2vtk(plist, dtype=np.float32)) 3475 pd.SetPoints(vpts) 3476 3477 delny = vtki.new("Delaunay2D") 3478 delny.SetInputData(pd) 3479 if tol: 3480 delny.SetTolerance(tol) 3481 delny.SetAlpha(alpha) 3482 delny.SetOffset(offset) 3483 3484 if transform: 3485 delny.SetTransform(transform.T) 3486 elif mode == "fit": 3487 delny.SetProjectionPlaneMode(vtki.get_class("VTK_BEST_FITTING_PLANE")) 3488 elif mode == "xy" and boundaries: 3489 boundary = vtki.vtkPolyData() 3490 boundary.SetPoints(vpts) 3491 cell_array = vtki.vtkCellArray() 3492 for b in boundaries: 3493 cpolygon = vtki.vtkPolygon() 3494 for idd in b: 3495 cpolygon.GetPointIds().InsertNextId(idd) 3496 cell_array.InsertNextCell(cpolygon) 3497 boundary.SetPolys(cell_array) 3498 delny.SetSourceData(boundary) 3499 3500 delny.Update() 3501 3502 msh = vedo.mesh.Mesh(delny.GetOutput()) 3503 msh.name = "Delaunay2D" 3504 msh.clean().lighting("off") 3505 msh.pipeline = utils.OperationNode( 3506 "delaunay2d", 3507 parents=[self], 3508 comment=f"#cells {msh.dataset.GetNumberOfCells()}", 3509 ) 3510 return msh
Create a mesh from points in the XY plane.
If mode='fit'
then the filter computes a best fitting
plane and projects the points onto it.
Check also generate_mesh()
.
Arguments:
- tol : (float) specify a tolerance to control discarding of closely spaced points. This tolerance is specified as a fraction of the diagonal length of the bounding box of the points.
- alpha : (float) for a non-zero alpha value, only edges or triangles contained within a sphere centered at mesh vertices will be output. Otherwise, only triangles will be output.
- offset : (float) multiplier to control the size of the initial, bounding Delaunay triangulation.
- transform: (LinearTransform, NonLinearTransform) a transformation which is applied to points to generate a 2D problem. This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane. The points are transformed and triangulated. The topology of triangulated points is used as the output topology.
Examples:
3512 def generate_voronoi(self, padding=0.0, fit=False, method="vtk") -> "vedo.Mesh": 3513 """ 3514 Generate the 2D Voronoi convex tiling of the input points (z is ignored). 3515 The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon. 3516 3517 A cell array named "VoronoiID" is added to the output Mesh. 3518 3519 The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest 3520 to one of the input points. Voronoi tessellations are important in computational geometry 3521 (and many other fields), and are the dual of Delaunay triangulations. 3522 3523 Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored 3524 (although carried through to the output). 3525 If you desire to triangulate in a different plane, you can use fit=True. 3526 3527 A brief summary is as follows. Each (generating) input point is associated with 3528 an initial Voronoi tile, which is simply the bounding box of the point set. 3529 A locator is then used to identify nearby points: each neighbor in turn generates a 3530 clipping line positioned halfway between the generating point and the neighboring point, 3531 and orthogonal to the line connecting them. Clips are readily performed by evaluationg the 3532 vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line. 3533 If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip 3534 line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs, 3535 the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region 3536 containing the neighboring clip points. The clip region (along with the points contained in it) is grown 3537 by careful expansion (e.g., outward spiraling iterator over all candidate clip points). 3538 When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi 3539 tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi 3540 tessellation. Note that topological and geometric information is used to generate a valid triangulation 3541 (e.g., merging points and validating topology). 3542 3543 Arguments: 3544 pts : (list) 3545 list of input points. 3546 padding : (float) 3547 padding distance. The default is 0. 3548 fit : (bool) 3549 detect automatically the best fitting plane. The default is False. 3550 3551 Examples: 3552 - [voronoi1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi1.py) 3553 3554 ![](https://vedo.embl.es/images/basic/voronoi1.png) 3555 3556 - [voronoi2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi2.py) 3557 3558 ![](https://vedo.embl.es/images/advanced/voronoi2.png) 3559 """ 3560 pts = self.vertices 3561 3562 if method == "scipy": 3563 from scipy.spatial import Voronoi as scipy_voronoi 3564 3565 pts = np.asarray(pts)[:, (0, 1)] 3566 vor = scipy_voronoi(pts) 3567 regs = [] # filter out invalid indices 3568 for r in vor.regions: 3569 flag = True 3570 for x in r: 3571 if x < 0: 3572 flag = False 3573 break 3574 if flag and len(r) > 0: 3575 regs.append(r) 3576 3577 m = vedo.Mesh([vor.vertices, regs]) 3578 m.celldata["VoronoiID"] = np.array(list(range(len(regs)))).astype(int) 3579 3580 elif method == "vtk": 3581 vor = vtki.new("Voronoi2D") 3582 if isinstance(pts, Points): 3583 vor.SetInputData(pts) 3584 else: 3585 pts = np.asarray(pts) 3586 if pts.shape[1] == 2: 3587 pts = np.c_[pts, np.zeros(len(pts))] 3588 pd = vtki.vtkPolyData() 3589 vpts = vtki.vtkPoints() 3590 vpts.SetData(utils.numpy2vtk(pts, dtype=np.float32)) 3591 pd.SetPoints(vpts) 3592 vor.SetInputData(pd) 3593 vor.SetPadding(padding) 3594 vor.SetGenerateScalarsToPointIds() 3595 if fit: 3596 vor.SetProjectionPlaneModeToBestFittingPlane() 3597 else: 3598 vor.SetProjectionPlaneModeToXYPlane() 3599 vor.Update() 3600 poly = vor.GetOutput() 3601 arr = poly.GetCellData().GetArray(0) 3602 if arr: 3603 arr.SetName("VoronoiID") 3604 m = vedo.Mesh(poly, c="orange5") 3605 3606 else: 3607 vedo.logger.error(f"Unknown method {method} in voronoi()") 3608 raise RuntimeError 3609 3610 m.lw(2).lighting("off").wireframe() 3611 m.name = "Voronoi" 3612 return m
Generate the 2D Voronoi convex tiling of the input points (z is ignored). The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon.
A cell array named "VoronoiID" is added to the output Mesh.
The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest to one of the input points. Voronoi tessellations are important in computational geometry (and many other fields), and are the dual of Delaunay triangulations.
Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored (although carried through to the output). If you desire to triangulate in a different plane, you can use fit=True.
A brief summary is as follows. Each (generating) input point is associated with an initial Voronoi tile, which is simply the bounding box of the point set. A locator is then used to identify nearby points: each neighbor in turn generates a clipping line positioned halfway between the generating point and the neighboring point, and orthogonal to the line connecting them. Clips are readily performed by evaluationg the vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line. If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs, the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region containing the neighboring clip points. The clip region (along with the points contained in it) is grown by careful expansion (e.g., outward spiraling iterator over all candidate clip points). When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi tessellation. Note that topological and geometric information is used to generate a valid triangulation (e.g., merging points and validating topology).
Arguments:
- pts : (list) list of input points.
- padding : (float) padding distance. The default is 0.
- fit : (bool) detect automatically the best fitting plane. The default is False.
Examples:
3615 def generate_delaunay3d(self, radius=0, tol=None) -> "vedo.TetMesh": 3616 """ 3617 Create 3D Delaunay triangulation of input points. 3618 3619 Arguments: 3620 radius : (float) 3621 specify distance (or "alpha") value to control output. 3622 For a non-zero values, only tetra contained within the circumsphere 3623 will be output. 3624 tol : (float) 3625 Specify a tolerance to control discarding of closely spaced points. 3626 This tolerance is specified as a fraction of the diagonal length of 3627 the bounding box of the points. 3628 """ 3629 deln = vtki.new("Delaunay3D") 3630 deln.SetInputData(self.dataset) 3631 deln.SetAlpha(radius) 3632 deln.AlphaTetsOn() 3633 deln.AlphaTrisOff() 3634 deln.AlphaLinesOff() 3635 deln.AlphaVertsOff() 3636 deln.BoundingTriangulationOff() 3637 if tol: 3638 deln.SetTolerance(tol) 3639 deln.Update() 3640 m = vedo.TetMesh(deln.GetOutput()) 3641 m.pipeline = utils.OperationNode( 3642 "generate_delaunay3d", c="#e9c46a:#edabab", parents=[self], 3643 ) 3644 m.name = "Delaunay3D" 3645 return m
Create 3D Delaunay triangulation of input points.
Arguments:
- radius : (float) specify distance (or "alpha") value to control output. For a non-zero values, only tetra contained within the circumsphere will be output.
- tol : (float) Specify a tolerance to control discarding of closely spaced points. This tolerance is specified as a fraction of the diagonal length of the bounding box of the points.
3648 def visible_points(self, area=(), tol=None, invert=False) -> Union[Self, None]: 3649 """ 3650 Extract points based on whether they are visible or not. 3651 Visibility is determined by accessing the z-buffer of a rendering window. 3652 The position of each input point is converted into display coordinates, 3653 and then the z-value at that point is obtained. 3654 If within the user-specified tolerance, the point is considered visible. 3655 Associated data attributes are passed to the output as well. 3656 3657 This filter also allows you to specify a rectangular window in display (pixel) 3658 coordinates in which the visible points must lie. 3659 3660 Arguments: 3661 area : (list) 3662 specify a rectangular region as (xmin,xmax,ymin,ymax) 3663 tol : (float) 3664 a tolerance in normalized display coordinate system 3665 invert : (bool) 3666 select invisible points instead. 3667 3668 Example: 3669 ```python 3670 from vedo import Ellipsoid, show 3671 s = Ellipsoid().rotate_y(30) 3672 3673 # Camera options: pos, focal_point, viewup, distance 3674 camopts = dict(pos=(0,0,25), focal_point=(0,0,0)) 3675 show(s, camera=camopts, offscreen=True) 3676 3677 m = s.visible_points() 3678 # print('visible pts:', m.vertices) # numpy array 3679 show(m, new=True, axes=1).close() # optionally draw result in a new window 3680 ``` 3681 ![](https://vedo.embl.es/images/feats/visible_points.png) 3682 """ 3683 svp = vtki.new("SelectVisiblePoints") 3684 svp.SetInputData(self.dataset) 3685 3686 ren = None 3687 if vedo.plotter_instance: 3688 if vedo.plotter_instance.renderer: 3689 ren = vedo.plotter_instance.renderer 3690 svp.SetRenderer(ren) 3691 if not ren: 3692 vedo.logger.warning( 3693 "visible_points() can only be used after a rendering step" 3694 ) 3695 return None 3696 3697 if len(area) == 2: 3698 area = utils.flatten(area) 3699 if len(area) == 4: 3700 # specify a rectangular region 3701 svp.SetSelection(area[0], area[1], area[2], area[3]) 3702 if tol is not None: 3703 svp.SetTolerance(tol) 3704 if invert: 3705 svp.SelectInvisibleOn() 3706 svp.Update() 3707 3708 m = Points(svp.GetOutput()) 3709 m.name = "VisiblePoints" 3710 return m
Extract points based on whether they are visible or not. Visibility is determined by accessing the z-buffer of a rendering window. The position of each input point is converted into display coordinates, and then the z-value at that point is obtained. If within the user-specified tolerance, the point is considered visible. Associated data attributes are passed to the output as well.
This filter also allows you to specify a rectangular window in display (pixel) coordinates in which the visible points must lie.
Arguments:
- area : (list) specify a rectangular region as (xmin,xmax,ymin,ymax)
- tol : (float) a tolerance in normalized display coordinate system
- invert : (bool) select invisible points instead.
Example:
from vedo import Ellipsoid, show s = Ellipsoid().rotate_y(30) # Camera options: pos, focal_point, viewup, distance camopts = dict(pos=(0,0,25), focal_point=(0,0,0)) show(s, camera=camopts, offscreen=True) m = s.visible_points() # print('visible pts:', m.vertices) # numpy array show(m, new=True, axes=1).close() # optionally draw result in a new window
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
- 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
454def Point(pos=(0, 0, 0), r=12, c="red", alpha=1.0) -> Self: 455 """ 456 Create a simple point in space. 457 458 .. note:: if you are creating many points you should use class `Points` instead! 459 """ 460 pt = Points([pos], r, c, alpha) 461 # pt.pos(pos) # dont set position, just the point coords 462 pt.name = "Point" 463 return pt
Create a simple point in space.
if you are creating many points you should use class Points
instead!
3713class CellCenters(Points): 3714 def __init__(self, pcloud): 3715 """ 3716 Generate `Points` at the center of the cells of any type of object. 3717 3718 Check out also `cell_centers()`. 3719 """ 3720 vcen = vtki.new("CellCenters") 3721 vcen.CopyArraysOn() 3722 vcen.VertexCellsOn() 3723 # vcen.ConvertGhostCellsToGhostPointsOn() 3724 try: 3725 vcen.SetInputData(pcloud.dataset) 3726 except AttributeError: 3727 vcen.SetInputData(pcloud) 3728 vcen.Update() 3729 super().__init__(vcen.GetOutput()) 3730 self.name = "CellCenters"
Work with point clouds.
3714 def __init__(self, pcloud): 3715 """ 3716 Generate `Points` at the center of the cells of any type of object. 3717 3718 Check out also `cell_centers()`. 3719 """ 3720 vcen = vtki.new("CellCenters") 3721 vcen.CopyArraysOn() 3722 vcen.VertexCellsOn() 3723 # vcen.ConvertGhostCellsToGhostPointsOn() 3724 try: 3725 vcen.SetInputData(pcloud.dataset) 3726 except AttributeError: 3727 vcen.SetInputData(pcloud) 3728 vcen.Update() 3729 super().__init__(vcen.GetOutput()) 3730 self.name = "CellCenters"
Generate Points
at the center of the cells of any type of object.
Check out also cell_centers()
.
Inherited Members
- Points
- polydata
- copy
- clone
- compute_normals_with_pca
- compute_acoplanarity
- distance_to
- clean
- subsample
- threshold
- quantize
- vertex_normals
- point_normals
- align_to
- align_to_bounding_box
- align_with_landmarks
- normalize
- mirror
- flip_normals
- add_gaussian_noise
- closest_point
- auto_distance
- hausdorff_distance
- chamfer_distance
- remove_outliers
- relax_point_positions
- smooth_mls_1d
- smooth_mls_2d
- smooth_lloyd_2d
- project_on_plane
- warp
- cut_with_plane
- cut_with_planes
- cut_with_box
- cut_with_line
- cut_with_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
- 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
42def merge(*meshs, flag=False) -> Union["vedo.Mesh", "vedo.Points", None]: 43 """ 44 Build a new Mesh (or Points) formed by the fusion of the inputs. 45 46 Similar to Assembly, but in this case the input objects become a single entity. 47 48 To keep track of the original identities of the inputs you can set `flag=True`. 49 In this case a `pointdata` array of ids is added to the output with name "OriginalMeshID". 50 51 Examples: 52 - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py) 53 54 ![](https://vedo.embl.es/images/advanced/warp1.png) 55 56 - [value_iteration.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/value_iteration.py) 57 58 """ 59 objs = [a for a in utils.flatten(meshs) if a] 60 61 if not objs: 62 return None 63 64 idarr = [] 65 polyapp = vtki.new("AppendPolyData") 66 for i, ob in enumerate(objs): 67 polyapp.AddInputData(ob.dataset) 68 if flag: 69 idarr += [i] * ob.dataset.GetNumberOfPoints() 70 polyapp.Update() 71 mpoly = polyapp.GetOutput() 72 73 if flag: 74 varr = utils.numpy2vtk(idarr, dtype=np.uint16, name="OriginalMeshID") 75 mpoly.GetPointData().AddArray(varr) 76 77 has_mesh = False 78 for ob in objs: 79 if isinstance(ob, vedo.Mesh): 80 has_mesh = True 81 break 82 83 if has_mesh: 84 msh = vedo.Mesh(mpoly) 85 else: 86 msh = Points(mpoly) # type: ignore 87 88 msh.copy_properties_from(objs[0]) 89 90 msh.pipeline = utils.OperationNode( 91 "merge", parents=objs, comment=f"#pts {msh.dataset.GetNumberOfPoints()}" 92 ) 93 return msh
Build a new Mesh (or Points) formed by the fusion of the inputs.
Similar to Assembly, but in this case the input objects become a single entity.
To keep track of the original identities of the inputs you can set flag=True
.
In this case a pointdata
array of ids is added to the output with name "OriginalMeshID".
Examples:
96def delaunay2d(plist, **kwargs) -> Self: 97 """delaunay2d() is deprecated, use Points().generate_delaunay2d() instead.""" 98 if isinstance(plist, Points): 99 plist = plist.vertices 100 else: 101 plist = np.ascontiguousarray(plist) 102 plist = utils.make3d(plist) 103 pp = Points(plist).generate_delaunay2d(**kwargs) 104 print("WARNING: delaunay2d() is deprecated, use Points().generate_delaunay2d() instead") 105 return pp
delaunay2d() is deprecated, use Points().generate_delaunay2d() instead.
148def fit_line(points: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Line": 149 """ 150 Fits a line through points. 151 152 Extra info is stored in `Line.slope`, `Line.center`, `Line.variances`. 153 154 Examples: 155 - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py) 156 157 ![](https://vedo.embl.es/images/advanced/fitline.png) 158 """ 159 if isinstance(points, Points): 160 points = points.vertices 161 data = np.asarray(points) 162 datamean = data.mean(axis=0) 163 _, dd, vv = np.linalg.svd(data - datamean) 164 vv = vv[0] / np.linalg.norm(vv[0]) 165 # vv contains the first principal component, i.e. the direction 166 # vector of the best fit line in the least squares sense. 167 xyz_min = data.min(axis=0) 168 xyz_max = data.max(axis=0) 169 a = np.linalg.norm(xyz_min - datamean) 170 b = np.linalg.norm(xyz_max - datamean) 171 p1 = datamean - a * vv 172 p2 = datamean + b * vv 173 line = vedo.shapes.Line(p1, p2, lw=1) 174 line.slope = vv 175 line.center = datamean 176 line.variances = dd 177 return line
Fits a line through points.
Extra info is stored in Line.slope
, Line.center
, Line.variances
.
Examples:
180def fit_circle(points: Union[np.ndarray, "vedo.Points"]) -> tuple: 181 """ 182 Fits a circle through a set of 3D points, with a very fast non-iterative method. 183 184 Returns the tuple `(center, radius, normal_to_circle)`. 185 186 .. warning:: 187 trying to fit s-shaped points will inevitably lead to instabilities and 188 circles of small radius. 189 190 References: 191 *J.F. Crawford, Nucl. Instr. Meth. 211, 1983, 223-225.* 192 """ 193 if isinstance(points, Points): 194 points = points.vertices 195 data = np.asarray(points) 196 197 offs = data.mean(axis=0) 198 data, n0 = _rotate_points(data - offs) 199 200 xi = data[:, 0] 201 yi = data[:, 1] 202 203 x = sum(xi) 204 xi2 = xi * xi 205 xx = sum(xi2) 206 xxx = sum(xi2 * xi) 207 208 y = sum(yi) 209 yi2 = yi * yi 210 yy = sum(yi2) 211 yyy = sum(yi2 * yi) 212 213 xiyi = xi * yi 214 xy = sum(xiyi) 215 xyy = sum(xiyi * yi) 216 xxy = sum(xi * xiyi) 217 218 N = len(xi) 219 k = (xx + yy) / N 220 221 a1 = xx - x * x / N 222 b1 = xy - x * y / N 223 c1 = 0.5 * (xxx + xyy - x * k) 224 225 a2 = xy - x * y / N 226 b2 = yy - y * y / N 227 c2 = 0.5 * (xxy + yyy - y * k) 228 229 d = a2 * b1 - a1 * b2 230 if not d: 231 return offs, 0, n0 232 x0 = (b1 * c2 - b2 * c1) / d 233 y0 = (c1 - a1 * x0) / b1 234 235 R = np.sqrt(x0 * x0 + y0 * y0 - 1 / N * (2 * x0 * x + 2 * y0 * y - xx - yy)) 236 237 c, _ = _rotate_points([x0, y0, 0], (0, 0, 1), n0) 238 239 return c[0] + offs, R, n0
Fits a circle through a set of 3D points, with a very fast non-iterative method.
Returns the tuple (center, radius, normal_to_circle)
.
trying to fit s-shaped points will inevitably lead to instabilities and circles of small radius.
References:
J.F. Crawford, Nucl. Instr. Meth. 211, 1983, 223-225.
242def fit_plane(points: Union[np.ndarray, "vedo.Points"], signed=False) -> "vedo.shapes.Plane": 243 """ 244 Fits a plane to a set of points. 245 246 Extra info is stored in `Plane.normal`, `Plane.center`, `Plane.variance`. 247 248 Arguments: 249 signed : (bool) 250 if True flip sign of the normal based on the ordering of the points 251 252 Examples: 253 - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py) 254 255 ![](https://vedo.embl.es/images/advanced/fitline.png) 256 """ 257 if isinstance(points, Points): 258 points = points.vertices 259 data = np.asarray(points) 260 datamean = data.mean(axis=0) 261 pts = data - datamean 262 res = np.linalg.svd(pts) 263 dd, vv = res[1], res[2] 264 n = np.cross(vv[0], vv[1]) 265 if signed: 266 v = np.zeros_like(pts) 267 for i in range(len(pts) - 1): 268 vi = np.cross(pts[i], pts[i + 1]) 269 v[i] = vi / np.linalg.norm(vi) 270 ns = np.mean(v, axis=0) # normal to the points plane 271 if np.dot(n, ns) < 0: 272 n = -n 273 xyz_min = data.min(axis=0) 274 xyz_max = data.max(axis=0) 275 s = np.linalg.norm(xyz_max - xyz_min) 276 pla = vedo.shapes.Plane(datamean, n, s=[s, s]) 277 pla.variance = dd[2] 278 pla.name = "FitPlane" 279 return pla
Fits a plane to a set of points.
Extra info is stored in Plane.normal
, Plane.center
, Plane.variance
.
Arguments:
- signed : (bool) if True flip sign of the normal based on the ordering of the points
Examples:
282def fit_sphere(coords: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Sphere": 283 """ 284 Fits a sphere to a set of points. 285 286 Extra info is stored in `Sphere.radius`, `Sphere.center`, `Sphere.residue`. 287 288 Examples: 289 - [fitspheres1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitspheres1.py) 290 291 ![](https://vedo.embl.es/images/advanced/fitspheres1.jpg) 292 """ 293 if isinstance(coords, Points): 294 coords = coords.vertices 295 coords = np.array(coords) 296 n = len(coords) 297 A = np.zeros((n, 4)) 298 A[:, :-1] = coords * 2 299 A[:, 3] = 1 300 f = np.zeros((n, 1)) 301 x = coords[:, 0] 302 y = coords[:, 1] 303 z = coords[:, 2] 304 f[:, 0] = x * x + y * y + z * z 305 try: 306 C, residue, rank, _ = np.linalg.lstsq(A, f, rcond=-1) # solve AC=f 307 except: 308 C, residue, rank, _ = np.linalg.lstsq(A, f) # solve AC=f 309 if rank < 4: 310 return None 311 t = (C[0] * C[0]) + (C[1] * C[1]) + (C[2] * C[2]) + C[3] 312 radius = np.sqrt(t)[0] 313 center = np.array([C[0][0], C[1][0], C[2][0]]) 314 if len(residue) > 0: 315 residue = np.sqrt(residue[0]) / n 316 else: 317 residue = 0 318 sph = vedo.shapes.Sphere(center, radius, c=(1, 0, 0)).wireframe(1) 319 sph.radius = radius 320 sph.center = center 321 sph.residue = residue 322 sph.name = "FitSphere" 323 return sph
Fits a sphere to a set of points.
Extra info is stored in Sphere.radius
, Sphere.center
, Sphere.residue
.
Examples:
326def pca_ellipse(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=60) -> Union["vedo.shapes.Circle", None]: 327 """ 328 Create the oriented 2D ellipse that contains the fraction `pvalue` of points. 329 PCA (Principal Component Analysis) is used to compute the ellipse orientation. 330 331 Parameter `pvalue` sets the specified fraction of points inside the ellipse. 332 Normalized directions are stored in `ellipse.axis1`, `ellipse.axis2`. 333 Axes sizes are stored in `ellipse.va`, `ellipse.vb` 334 335 Arguments: 336 pvalue : (float) 337 ellipse will include this fraction of points 338 res : (int) 339 resolution of the ellipse 340 341 Examples: 342 - [pca_ellipse.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipse.py) 343 - [histo_pca.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_pca.py) 344 345 ![](https://vedo.embl.es/images/pyplot/histo_pca.png) 346 """ 347 from scipy.stats import f 348 349 if isinstance(points, Points): 350 coords = points.vertices 351 else: 352 coords = points 353 if len(coords) < 4: 354 vedo.logger.warning("in pca_ellipse(), there are not enough points!") 355 return None 356 357 P = np.array(coords, dtype=float)[:, (0, 1)] 358 cov = np.cov(P, rowvar=0) # type: ignore 359 _, s, R = np.linalg.svd(cov) # singular value decomposition 360 p, n = s.size, P.shape[0] 361 fppf = f.ppf(pvalue, p, n - p) # f % point function 362 u = np.sqrt(s * fppf / 2) * 2 # semi-axes (largest first) 363 ua, ub = u 364 center = utils.make3d(np.mean(P, axis=0)) # centroid of the ellipse 365 366 t = LinearTransform(R.T * u).translate(center) 367 elli = vedo.shapes.Circle(alpha=0.75, res=res) 368 elli.apply_transform(t) 369 elli.properties.LightingOff() 370 371 elli.pvalue = pvalue 372 elli.center = np.array([center[0], center[1], 0]) 373 elli.nr_of_points = n 374 elli.va = ua 375 elli.vb = ub 376 377 # we subtract center because it's in t 378 elli.axis1 = t.move([1, 0, 0]) - center 379 elli.axis2 = t.move([0, 1, 0]) - center 380 381 elli.axis1 /= np.linalg.norm(elli.axis1) 382 elli.axis2 /= np.linalg.norm(elli.axis2) 383 elli.name = "PCAEllipse" 384 return elli
Create the oriented 2D ellipse that contains the fraction pvalue
of points.
PCA (Principal Component Analysis) is used to compute the ellipse orientation.
Parameter pvalue
sets the specified fraction of points inside the ellipse.
Normalized directions are stored in ellipse.axis1
, ellipse.axis2
.
Axes sizes are stored in ellipse.va
, ellipse.vb
Arguments:
- pvalue : (float) ellipse will include this fraction of points
- res : (int) resolution of the ellipse
Examples:
387def pca_ellipsoid(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=24) -> Union["vedo.shapes.Ellipsoid", None]: 388 """ 389 Create the oriented ellipsoid that contains the fraction `pvalue` of points. 390 PCA (Principal Component Analysis) is used to compute the ellipsoid orientation. 391 392 Axes sizes can be accessed in `ellips.va`, `ellips.vb`, `ellips.vc`, 393 normalized directions are stored in `ellips.axis1`, `ellips.axis2` and `ellips.axis3`. 394 Center of mass is stored in `ellips.center`. 395 396 Asphericity can be accessed in `ellips.asphericity()` and ellips.asphericity_error(). 397 A value of 0 means a perfect sphere. 398 399 Arguments: 400 pvalue : (float) 401 ellipsoid will include this fraction of points 402 403 Examples: 404 [pca_ellipsoid.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipsoid.py) 405 406 ![](https://vedo.embl.es/images/basic/pca.png) 407 408 See also: 409 `pca_ellipse()` for a 2D ellipse. 410 """ 411 from scipy.stats import f 412 413 if isinstance(points, Points): 414 coords = points.vertices 415 else: 416 coords = points 417 if len(coords) < 4: 418 vedo.logger.warning("in pca_ellipsoid(), not enough input points!") 419 return None 420 421 P = np.array(coords, ndmin=2, dtype=float) 422 cov = np.cov(P, rowvar=0) # type: ignore 423 _, s, R = np.linalg.svd(cov) # singular value decomposition 424 p, n = s.size, P.shape[0] 425 fppf = f.ppf(pvalue, p, n-p)*(n-1)*p*(n+1)/n/(n-p) # f % point function 426 u = np.sqrt(s*fppf) 427 ua, ub, uc = u # semi-axes (largest first) 428 center = np.mean(P, axis=0) # centroid of the hyperellipsoid 429 430 t = LinearTransform(R.T * u).translate(center) 431 elli = vedo.shapes.Ellipsoid((0,0,0), (1,0,0), (0,1,0), (0,0,1), res=res) 432 elli.apply_transform(t) 433 elli.alpha(0.25) 434 elli.properties.LightingOff() 435 436 elli.pvalue = pvalue 437 elli.nr_of_points = n 438 elli.center = center 439 elli.va = ua 440 elli.vb = ub 441 elli.vc = uc 442 # we subtract center because it's in t 443 elli.axis1 = np.array(t.move([1, 0, 0])) - center 444 elli.axis2 = np.array(t.move([0, 1, 0])) - center 445 elli.axis3 = np.array(t.move([0, 0, 1])) - center 446 elli.axis1 /= np.linalg.norm(elli.axis1) 447 elli.axis2 /= np.linalg.norm(elli.axis2) 448 elli.axis3 /= np.linalg.norm(elli.axis3) 449 elli.name = "PCAEllipsoid" 450 return elli
Create the oriented ellipsoid that contains the fraction pvalue
of points.
PCA (Principal Component Analysis) is used to compute the ellipsoid orientation.
Axes sizes can be accessed in ellips.va
, ellips.vb
, ellips.vc
,
normalized directions are stored in ellips.axis1
, ellips.axis2
and ellips.axis3
.
Center of mass is stored in ellips.center
.
Asphericity can be accessed in ellips.asphericity()
and ellips.asphericity_error().
A value of 0 means a perfect sphere.
Arguments:
- pvalue : (float) ellipsoid will include this fraction of points
Examples:
![](https://vedo.embl.es/images/basic/pca.png)
See also:
pca_ellipse()
for a 2D ellipse.