vedo.pointcloud
Submodule to work with point clouds.
1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3import time 4from typing import Union, List 5from typing_extensions import Self 6from weakref import ref as weak_ref_to 7import numpy as np 8 9import vedo.vtkclasses as vtki 10 11import vedo 12from vedo import colors 13from vedo import utils 14from vedo.transformations import LinearTransform, NonLinearTransform 15from vedo.core import PointAlgorithms 16from vedo.visual import PointsVisual 17 18__docformat__ = "google" 19 20__doc__ = """ 21Submodule to work with point clouds. 22 23![](https://vedo.embl.es/images/basic/pca.png) 24""" 25 26__all__ = [ 27 "Points", 28 "Point", 29 "CellCenters", 30 "merge", 31 "fit_line", 32 "fit_circle", 33 "fit_plane", 34 "fit_sphere", 35 "pca_ellipse", 36 "pca_ellipsoid", 37] 38 39 40#################################################### 41def merge(*meshs, flag=False) -> Union["vedo.Mesh", "vedo.Points", None]: 42 """ 43 Build a new Mesh (or Points) formed by the fusion of the inputs. 44 45 Similar to Assembly, but in this case the input objects become a single entity. 46 47 To keep track of the original identities of the inputs you can set `flag=True`. 48 In this case a `pointdata` array of ids is added to the output with name "OriginalMeshID". 49 50 Examples: 51 - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py) 52 53 ![](https://vedo.embl.es/images/advanced/warp1.png) 54 55 - [value_iteration.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/value_iteration.py) 56 57 """ 58 objs = [a for a in utils.flatten(meshs) if a] 59 60 if not objs: 61 return None 62 63 idarr = [] 64 polyapp = vtki.new("AppendPolyData") 65 for i, ob in enumerate(objs): 66 polyapp.AddInputData(ob.dataset) 67 if flag: 68 idarr += [i] * ob.dataset.GetNumberOfPoints() 69 polyapp.Update() 70 mpoly = polyapp.GetOutput() 71 72 if flag: 73 varr = utils.numpy2vtk(idarr, dtype=np.uint16, name="OriginalMeshID") 74 mpoly.GetPointData().AddArray(varr) 75 76 has_mesh = False 77 for ob in objs: 78 if isinstance(ob, vedo.Mesh): 79 has_mesh = True 80 break 81 82 if has_mesh: 83 msh = vedo.Mesh(mpoly) 84 else: 85 msh = Points(mpoly) # type: ignore 86 87 msh.copy_properties_from(objs[0]) 88 89 msh.pipeline = utils.OperationNode( 90 "merge", parents=objs, comment=f"#pts {msh.dataset.GetNumberOfPoints()}" 91 ) 92 return msh 93 94 95def _rotate_points(points, n0=None, n1=(0, 0, 1)) -> Union[np.ndarray, tuple]: 96 # Rotate a set of 3D points from direction n0 to direction n1. 97 # Return the rotated points and the normal to the fitting plane (if n0 is None). 98 # The pointing direction of the normal in this case is arbitrary. 99 points = np.asarray(points) 100 101 if points.ndim == 1: 102 points = points[np.newaxis, :] 103 104 if len(points[0]) == 2: 105 return points, (0, 0, 1) 106 107 if n0 is None: # fit plane 108 datamean = points.mean(axis=0) 109 vv = np.linalg.svd(points - datamean)[2] 110 n0 = np.cross(vv[0], vv[1]) 111 112 n0 = n0 / np.linalg.norm(n0) 113 n1 = n1 / np.linalg.norm(n1) 114 k = np.cross(n0, n1) 115 l = np.linalg.norm(k) 116 if not l: 117 k = n0 118 k /= np.linalg.norm(k) 119 120 ct = np.dot(n0, n1) 121 theta = np.arccos(ct) 122 st = np.sin(theta) 123 v = k * (1 - ct) 124 125 rpoints = [] 126 for p in points: 127 a = p * ct 128 b = np.cross(k, p) * st 129 c = v * np.dot(k, p) 130 rpoints.append(a + b + c) 131 132 return np.array(rpoints), n0 133 134 135def fit_line(points: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Line": 136 """ 137 Fits a line through points. 138 139 Extra info is stored in `Line.slope`, `Line.center`, `Line.variances`. 140 141 Examples: 142 - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py) 143 144 ![](https://vedo.embl.es/images/advanced/fitline.png) 145 """ 146 if isinstance(points, Points): 147 points = points.vertices 148 data = np.asarray(points) 149 datamean = data.mean(axis=0) 150 _, dd, vv = np.linalg.svd(data - datamean) 151 vv = vv[0] / np.linalg.norm(vv[0]) 152 # vv contains the first principal component, i.e. the direction 153 # vector of the best fit line in the least squares sense. 154 xyz_min = data.min(axis=0) 155 xyz_max = data.max(axis=0) 156 a = np.linalg.norm(xyz_min - datamean) 157 b = np.linalg.norm(xyz_max - datamean) 158 p1 = datamean - a * vv 159 p2 = datamean + b * vv 160 line = vedo.shapes.Line(p1, p2, lw=1) 161 line.slope = vv 162 line.center = datamean 163 line.variances = dd 164 return line 165 166 167def fit_circle(points: Union[np.ndarray, "vedo.Points"]) -> tuple: 168 """ 169 Fits a circle through a set of 3D points, with a very fast non-iterative method. 170 171 Returns the tuple `(center, radius, normal_to_circle)`. 172 173 .. warning:: 174 trying to fit s-shaped points will inevitably lead to instabilities and 175 circles of small radius. 176 177 References: 178 *J.F. Crawford, Nucl. Instr. Meth. 211, 1983, 223-225.* 179 """ 180 if isinstance(points, Points): 181 points = points.vertices 182 data = np.asarray(points) 183 184 offs = data.mean(axis=0) 185 data, n0 = _rotate_points(data - offs) 186 187 xi = data[:, 0] 188 yi = data[:, 1] 189 190 x = sum(xi) 191 xi2 = xi * xi 192 xx = sum(xi2) 193 xxx = sum(xi2 * xi) 194 195 y = sum(yi) 196 yi2 = yi * yi 197 yy = sum(yi2) 198 yyy = sum(yi2 * yi) 199 200 xiyi = xi * yi 201 xy = sum(xiyi) 202 xyy = sum(xiyi * yi) 203 xxy = sum(xi * xiyi) 204 205 N = len(xi) 206 k = (xx + yy) / N 207 208 a1 = xx - x * x / N 209 b1 = xy - x * y / N 210 c1 = 0.5 * (xxx + xyy - x * k) 211 212 a2 = xy - x * y / N 213 b2 = yy - y * y / N 214 c2 = 0.5 * (xxy + yyy - y * k) 215 216 d = a2 * b1 - a1 * b2 217 if not d: 218 return offs, 0, n0 219 x0 = (b1 * c2 - b2 * c1) / d 220 y0 = (c1 - a1 * x0) / b1 221 222 R = np.sqrt(x0 * x0 + y0 * y0 - 1 / N * (2 * x0 * x + 2 * y0 * y - xx - yy)) 223 224 c, _ = _rotate_points([x0, y0, 0], (0, 0, 1), n0) 225 226 return c[0] + offs, R, n0 227 228 229def fit_plane(points: Union[np.ndarray, "vedo.Points"], signed=False) -> "vedo.shapes.Plane": 230 """ 231 Fits a plane to a set of points. 232 233 Extra info is stored in `Plane.normal`, `Plane.center`, `Plane.variance`. 234 235 Arguments: 236 signed : (bool) 237 if True flip sign of the normal based on the ordering of the points 238 239 Examples: 240 - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py) 241 242 ![](https://vedo.embl.es/images/advanced/fitline.png) 243 """ 244 if isinstance(points, Points): 245 points = points.vertices 246 data = np.asarray(points) 247 datamean = data.mean(axis=0) 248 pts = data - datamean 249 res = np.linalg.svd(pts) 250 dd, vv = res[1], res[2] 251 n = np.cross(vv[0], vv[1]) 252 if signed: 253 v = np.zeros_like(pts) 254 for i in range(len(pts) - 1): 255 vi = np.cross(pts[i], pts[i + 1]) 256 v[i] = vi / np.linalg.norm(vi) 257 ns = np.mean(v, axis=0) # normal to the points plane 258 if np.dot(n, ns) < 0: 259 n = -n 260 xyz_min = data.min(axis=0) 261 xyz_max = data.max(axis=0) 262 s = np.linalg.norm(xyz_max - xyz_min) 263 pla = vedo.shapes.Plane(datamean, n, s=[s, s]) 264 pla.variance = dd[2] 265 pla.name = "FitPlane" 266 return pla 267 268 269def fit_sphere(coords: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Sphere": 270 """ 271 Fits a sphere to a set of points. 272 273 Extra info is stored in `Sphere.radius`, `Sphere.center`, `Sphere.residue`. 274 275 Examples: 276 - [fitspheres1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitspheres1.py) 277 278 ![](https://vedo.embl.es/images/advanced/fitspheres1.jpg) 279 """ 280 if isinstance(coords, Points): 281 coords = coords.vertices 282 coords = np.array(coords) 283 n = len(coords) 284 A = np.zeros((n, 4)) 285 A[:, :-1] = coords * 2 286 A[:, 3] = 1 287 f = np.zeros((n, 1)) 288 x = coords[:, 0] 289 y = coords[:, 1] 290 z = coords[:, 2] 291 f[:, 0] = x * x + y * y + z * z 292 try: 293 C, residue, rank, _ = np.linalg.lstsq(A, f, rcond=-1) # solve AC=f 294 except: 295 C, residue, rank, _ = np.linalg.lstsq(A, f) # solve AC=f 296 if rank < 4: 297 return None 298 t = (C[0] * C[0]) + (C[1] * C[1]) + (C[2] * C[2]) + C[3] 299 radius = np.sqrt(t)[0] 300 center = np.array([C[0][0], C[1][0], C[2][0]]) 301 if len(residue) > 0: 302 residue = np.sqrt(residue[0]) / n 303 else: 304 residue = 0 305 sph = vedo.shapes.Sphere(center, radius, c=(1, 0, 0)).wireframe(1) 306 sph.radius = radius 307 sph.center = center 308 sph.residue = residue 309 sph.name = "FitSphere" 310 return sph 311 312 313def pca_ellipse(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=60) -> Union["vedo.shapes.Circle", None]: 314 """ 315 Create the oriented 2D ellipse that contains the fraction `pvalue` of points. 316 PCA (Principal Component Analysis) is used to compute the ellipse orientation. 317 318 Parameter `pvalue` sets the specified fraction of points inside the ellipse. 319 Normalized directions are stored in `ellipse.axis1`, `ellipse.axis2`. 320 Axes sizes are stored in `ellipse.va`, `ellipse.vb` 321 322 Arguments: 323 pvalue : (float) 324 ellipse will include this fraction of points 325 res : (int) 326 resolution of the ellipse 327 328 Examples: 329 - [pca_ellipse.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipse.py) 330 - [histo_pca.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_pca.py) 331 332 ![](https://vedo.embl.es/images/pyplot/histo_pca.png) 333 """ 334 from scipy.stats import f 335 336 if isinstance(points, Points): 337 coords = points.vertices 338 else: 339 coords = points 340 if len(coords) < 4: 341 vedo.logger.warning("in pca_ellipse(), there are not enough points!") 342 return None 343 344 P = np.array(coords, dtype=float)[:, (0, 1)] 345 cov = np.cov(P, rowvar=0) # type: ignore 346 _, s, R = np.linalg.svd(cov) # singular value decomposition 347 p, n = s.size, P.shape[0] 348 fppf = f.ppf(pvalue, p, n - p) # f % point function 349 u = np.sqrt(s * fppf / 2) * 2 # semi-axes (largest first) 350 ua, ub = u 351 center = utils.make3d(np.mean(P, axis=0)) # centroid of the ellipse 352 353 t = LinearTransform(R.T * u).translate(center) 354 elli = vedo.shapes.Circle(alpha=0.75, res=res) 355 elli.apply_transform(t) 356 elli.properties.LightingOff() 357 358 elli.pvalue = pvalue 359 elli.center = np.array([center[0], center[1], 0]) 360 elli.nr_of_points = n 361 elli.va = ua 362 elli.vb = ub 363 364 # we subtract center because it's in t 365 elli.axis1 = t.move([1, 0, 0]) - center 366 elli.axis2 = t.move([0, 1, 0]) - center 367 368 elli.axis1 /= np.linalg.norm(elli.axis1) 369 elli.axis2 /= np.linalg.norm(elli.axis2) 370 elli.name = "PCAEllipse" 371 return elli 372 373 374def pca_ellipsoid(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=24) -> Union["vedo.shapes.Ellipsoid", None]: 375 """ 376 Create the oriented ellipsoid that contains the fraction `pvalue` of points. 377 PCA (Principal Component Analysis) is used to compute the ellipsoid orientation. 378 379 Axes sizes can be accessed in `ellips.va`, `ellips.vb`, `ellips.vc`, 380 normalized directions are stored in `ellips.axis1`, `ellips.axis2` and `ellips.axis3`. 381 Center of mass is stored in `ellips.center`. 382 383 Asphericity can be accessed in `ellips.asphericity()` and ellips.asphericity_error(). 384 A value of 0 means a perfect sphere. 385 386 Arguments: 387 pvalue : (float) 388 ellipsoid will include this fraction of points 389 390 Examples: 391 [pca_ellipsoid.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipsoid.py) 392 393 ![](https://vedo.embl.es/images/basic/pca.png) 394 395 See also: 396 `pca_ellipse()` for a 2D ellipse. 397 """ 398 from scipy.stats import f 399 400 if isinstance(points, Points): 401 coords = points.vertices 402 else: 403 coords = points 404 if len(coords) < 4: 405 vedo.logger.warning("in pca_ellipsoid(), not enough input points!") 406 return None 407 408 P = np.array(coords, ndmin=2, dtype=float) 409 cov = np.cov(P, rowvar=0) # type: ignore 410 _, s, R = np.linalg.svd(cov) # singular value decomposition 411 p, n = s.size, P.shape[0] 412 fppf = f.ppf(pvalue, p, n-p)*(n-1)*p*(n+1)/n/(n-p) # f % point function 413 u = np.sqrt(s*fppf) 414 ua, ub, uc = u # semi-axes (largest first) 415 center = np.mean(P, axis=0) # centroid of the hyperellipsoid 416 417 t = LinearTransform(R.T * u).translate(center) 418 elli = vedo.shapes.Ellipsoid((0,0,0), (1,0,0), (0,1,0), (0,0,1), res=res) 419 elli.apply_transform(t) 420 elli.alpha(0.25) 421 elli.properties.LightingOff() 422 423 elli.pvalue = pvalue 424 elli.nr_of_points = n 425 elli.center = center 426 elli.va = ua 427 elli.vb = ub 428 elli.vc = uc 429 # we subtract center because it's in t 430 elli.axis1 = np.array(t.move([1, 0, 0])) - center 431 elli.axis2 = np.array(t.move([0, 1, 0])) - center 432 elli.axis3 = np.array(t.move([0, 0, 1])) - center 433 elli.axis1 /= np.linalg.norm(elli.axis1) 434 elli.axis2 /= np.linalg.norm(elli.axis2) 435 elli.axis3 /= np.linalg.norm(elli.axis3) 436 elli.name = "PCAEllipsoid" 437 return elli 438 439 440################################################### 441def Point(pos=(0, 0, 0), r=12, c="red", alpha=1.0) -> Self: 442 """ 443 Create a simple point in space. 444 445 .. note:: if you are creating many points you should use class `Points` instead! 446 """ 447 pt = Points([[0,0,0]], r, c, alpha).pos(pos) 448 pt.name = "Point" 449 return pt 450 451 452################################################### 453class Points(PointsVisual, PointAlgorithms): 454 """Work with point clouds.""" 455 456 def __init__(self, inputobj=None, r=4, c=(0.2, 0.2, 0.2), alpha=1): 457 """ 458 Build an object made of only vertex points for a list of 2D/3D points. 459 Both shapes (N, 3) or (3, N) are accepted as input, if N>3. 460 461 Arguments: 462 inputobj : (list, tuple) 463 r : (int) 464 Point radius in units of pixels. 465 c : (str, list) 466 Color name or rgb tuple. 467 alpha : (float) 468 Transparency in range [0,1]. 469 470 Example: 471 ```python 472 from vedo import * 473 474 def fibonacci_sphere(n): 475 s = np.linspace(0, n, num=n, endpoint=False) 476 theta = s * 2.399963229728653 477 y = 1 - s * (2/(n-1)) 478 r = np.sqrt(1 - y * y) 479 x = np.cos(theta) * r 480 z = np.sin(theta) * r 481 return np._c[x,y,z] 482 483 Points(fibonacci_sphere(1000)).show(axes=1).close() 484 ``` 485 ![](https://vedo.embl.es/images/feats/fibonacci.png) 486 """ 487 # print("INIT POINTS") 488 super().__init__() 489 490 self.name = "" 491 self.filename = "" 492 self.file_size = "" 493 494 self.info = {} 495 self.time = time.time() 496 497 self.transform = LinearTransform() 498 self.point_locator = None 499 self.cell_locator = None 500 self.line_locator = None 501 502 self.actor = vtki.vtkActor() 503 self.properties = self.actor.GetProperty() 504 self.properties_backface = self.actor.GetBackfaceProperty() 505 self.mapper = vtki.new("PolyDataMapper") 506 self.dataset = vtki.vtkPolyData() 507 508 # Create weakref so actor can access this object (eg to pick/remove): 509 self.actor.retrieve_object = weak_ref_to(self) 510 511 try: 512 self.properties.RenderPointsAsSpheresOn() 513 except AttributeError: 514 pass 515 516 if inputobj is None: #################### 517 return 518 ########################################## 519 520 self.name = "Points" 521 522 ###### 523 if isinstance(inputobj, vtki.vtkActor): 524 self.dataset.DeepCopy(inputobj.GetMapper().GetInput()) 525 pr = vtki.vtkProperty() 526 pr.DeepCopy(inputobj.GetProperty()) 527 self.actor.SetProperty(pr) 528 self.properties = pr 529 self.mapper.SetScalarVisibility(inputobj.GetMapper().GetScalarVisibility()) 530 531 elif isinstance(inputobj, vtki.vtkPolyData): 532 self.dataset = inputobj 533 if self.dataset.GetNumberOfCells() == 0: 534 carr = vtki.vtkCellArray() 535 for i in range(self.dataset.GetNumberOfPoints()): 536 carr.InsertNextCell(1) 537 carr.InsertCellPoint(i) 538 self.dataset.SetVerts(carr) 539 540 elif isinstance(inputobj, Points): 541 self.dataset = inputobj.dataset 542 self.copy_properties_from(inputobj) 543 544 elif utils.is_sequence(inputobj): # passing point coords 545 self.dataset = utils.buildPolyData(utils.make3d(inputobj)) 546 547 elif isinstance(inputobj, str): 548 verts = vedo.file_io.load(inputobj) 549 self.filename = inputobj 550 self.dataset = verts.dataset 551 552 elif "meshlib" in str(type(inputobj)): 553 from meshlib import mrmeshnumpy as mn 554 self.dataset = utils.buildPolyData(mn.toNumpyArray(inputobj.points)) 555 556 else: 557 # try to extract the points from a generic VTK input data object 558 if hasattr(inputobj, "dataset"): 559 inputobj = inputobj.dataset 560 try: 561 vvpts = inputobj.GetPoints() 562 self.dataset = vtki.vtkPolyData() 563 self.dataset.SetPoints(vvpts) 564 for i in range(inputobj.GetPointData().GetNumberOfArrays()): 565 arr = inputobj.GetPointData().GetArray(i) 566 self.dataset.GetPointData().AddArray(arr) 567 carr = vtki.vtkCellArray() 568 for i in range(self.dataset.GetNumberOfPoints()): 569 carr.InsertNextCell(1) 570 carr.InsertCellPoint(i) 571 self.dataset.SetVerts(carr) 572 except: 573 vedo.logger.error(f"cannot build Points from type {type(inputobj)}") 574 raise RuntimeError() 575 576 self.actor.SetMapper(self.mapper) 577 self.mapper.SetInputData(self.dataset) 578 579 self.properties.SetColor(colors.get_color(c)) 580 self.properties.SetOpacity(alpha) 581 self.properties.SetRepresentationToPoints() 582 self.properties.SetPointSize(r) 583 self.properties.LightingOff() 584 585 self.pipeline = utils.OperationNode( 586 self, parents=[], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 587 ) 588 589 def _update(self, polydata, reset_locators=True) -> Self: 590 """Overwrite the polygonal dataset with a new vtkPolyData.""" 591 self.dataset = polydata 592 self.mapper.SetInputData(self.dataset) 593 self.mapper.Modified() 594 if reset_locators: 595 self.point_locator = None 596 self.line_locator = None 597 self.cell_locator = None 598 return self 599 600 def __str__(self): 601 """Print a description of the Points/Mesh.""" 602 module = self.__class__.__module__ 603 name = self.__class__.__name__ 604 out = vedo.printc( 605 f"{module}.{name} at ({hex(self.memory_address())})".ljust(75), 606 c="g", bold=True, invert=True, return_string=True, 607 ) 608 out += "\x1b[0m\x1b[32;1m" 609 610 if self.name: 611 out += "name".ljust(14) + ": " + self.name 612 if "legend" in self.info.keys() and self.info["legend"]: 613 out+= f", legend='{self.info['legend']}'" 614 out += "\n" 615 616 if self.filename: 617 out+= "file name".ljust(14) + ": " + self.filename + "\n" 618 619 if not self.mapper.GetScalarVisibility(): 620 col = utils.precision(self.properties.GetColor(), 3) 621 cname = vedo.colors.get_color_name(self.properties.GetColor()) 622 out+= "color".ljust(14) + ": " + cname 623 out+= f", rgb={col}, alpha={self.properties.GetOpacity()}\n" 624 if self.actor.GetBackfaceProperty(): 625 bcol = self.actor.GetBackfaceProperty().GetDiffuseColor() 626 cname = vedo.colors.get_color_name(bcol) 627 out+= "backface color".ljust(14) + ": " 628 out+= f"{cname}, rgb={utils.precision(bcol,3)}\n" 629 630 npt = self.dataset.GetNumberOfPoints() 631 npo, nln = self.dataset.GetNumberOfPolys(), self.dataset.GetNumberOfLines() 632 out+= "elements".ljust(14) + f": vertices={npt:,} polygons={npo:,} lines={nln:,}" 633 if self.dataset.GetNumberOfStrips(): 634 out+= f", strips={self.dataset.GetNumberOfStrips():,}" 635 out+= "\n" 636 if self.dataset.GetNumberOfPieces() > 1: 637 out+= "pieces".ljust(14) + ": " + str(self.dataset.GetNumberOfPieces()) + "\n" 638 639 out+= "position".ljust(14) + ": " + f"{utils.precision(self.pos(), 6)}\n" 640 try: 641 sc = self.transform.get_scale() 642 out+= "scaling".ljust(14) + ": " 643 out+= utils.precision(sc, 6) + "\n" 644 except AttributeError: 645 pass 646 647 if self.npoints: 648 out+="size".ljust(14)+ ": average=" + utils.precision(self.average_size(),6) 649 out+=", diagonal="+ utils.precision(self.diagonal_size(), 6)+ "\n" 650 out+="center of mass".ljust(14) + ": " + utils.precision(self.center_of_mass(),6)+"\n" 651 652 bnds = self.bounds() 653 bx1, bx2 = utils.precision(bnds[0], 3), utils.precision(bnds[1], 3) 654 by1, by2 = utils.precision(bnds[2], 3), utils.precision(bnds[3], 3) 655 bz1, bz2 = utils.precision(bnds[4], 3), utils.precision(bnds[5], 3) 656 out+= "bounds".ljust(14) + ":" 657 out+= " x=(" + bx1 + ", " + bx2 + ")," 658 out+= " y=(" + by1 + ", " + by2 + ")," 659 out+= " z=(" + bz1 + ", " + bz2 + ")\n" 660 661 for key in self.pointdata.keys(): 662 arr = self.pointdata[key] 663 dim = arr.shape[1] if arr.ndim > 1 else 1 664 mark_active = "pointdata" 665 a_scalars = self.dataset.GetPointData().GetScalars() 666 a_vectors = self.dataset.GetPointData().GetVectors() 667 a_tensors = self.dataset.GetPointData().GetTensors() 668 if a_scalars and a_scalars.GetName() == key: 669 mark_active += " *" 670 elif a_vectors and a_vectors.GetName() == key: 671 mark_active += " **" 672 elif a_tensors and a_tensors.GetName() == key: 673 mark_active += " ***" 674 out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), dim={dim}' 675 if dim == 1 and len(arr): 676 rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3) 677 out += f", range=({rng})\n" 678 else: 679 out += "\n" 680 681 for key in self.celldata.keys(): 682 arr = self.celldata[key] 683 dim = arr.shape[1] if arr.ndim > 1 else 1 684 mark_active = "celldata" 685 a_scalars = self.dataset.GetCellData().GetScalars() 686 a_vectors = self.dataset.GetCellData().GetVectors() 687 a_tensors = self.dataset.GetCellData().GetTensors() 688 if a_scalars and a_scalars.GetName() == key: 689 mark_active += " *" 690 elif a_vectors and a_vectors.GetName() == key: 691 mark_active += " **" 692 elif a_tensors and a_tensors.GetName() == key: 693 mark_active += " ***" 694 out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), dim={dim}' 695 if dim == 1 and len(arr): 696 rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3) 697 out += f", range=({rng})\n" 698 else: 699 out += "\n" 700 701 for key in self.metadata.keys(): 702 arr = self.metadata[key] 703 if len(arr) > 3: 704 out+= "metadata".ljust(14) + ": " + f'"{key}" ({len(arr)} values)\n' 705 else: 706 out+= "metadata".ljust(14) + ": " + f'"{key}" = {arr}\n' 707 708 if self.picked3d is not None: 709 idp = self.closest_point(self.picked3d, return_point_id=True) 710 idc = self.closest_point(self.picked3d, return_cell_id=True) 711 out+= "clicked point".ljust(14) + ": " + utils.precision(self.picked3d, 6) 712 out+= f", pointID={idp}, cellID={idc}\n" 713 714 return out.rstrip() + "\x1b[0m" 715 716 def _repr_html_(self): 717 """ 718 HTML representation of the Point cloud object for Jupyter Notebooks. 719 720 Returns: 721 HTML text with the image and some properties. 722 """ 723 import io 724 import base64 725 from PIL import Image 726 727 library_name = "vedo.pointcloud.Points" 728 help_url = "https://vedo.embl.es/docs/vedo/pointcloud.html#Points" 729 730 arr = self.thumbnail() 731 im = Image.fromarray(arr) 732 buffered = io.BytesIO() 733 im.save(buffered, format="PNG", quality=100) 734 encoded = base64.b64encode(buffered.getvalue()).decode("utf-8") 735 url = "data:image/png;base64," + encoded 736 image = f"<img src='{url}'></img>" 737 738 bounds = "<br/>".join( 739 [ 740 utils.precision(min_x, 4) + " ... " + utils.precision(max_x, 4) 741 for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2]) 742 ] 743 ) 744 average_size = "{size:.3f}".format(size=self.average_size()) 745 746 help_text = "" 747 if self.name: 748 help_text += f"<b> {self.name}:   </b>" 749 help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>" 750 if self.filename: 751 dots = "" 752 if len(self.filename) > 30: 753 dots = "..." 754 help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>" 755 756 pdata = "" 757 if self.dataset.GetPointData().GetScalars(): 758 if self.dataset.GetPointData().GetScalars().GetName(): 759 name = self.dataset.GetPointData().GetScalars().GetName() 760 pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>" 761 762 cdata = "" 763 if self.dataset.GetCellData().GetScalars(): 764 if self.dataset.GetCellData().GetScalars().GetName(): 765 name = self.dataset.GetCellData().GetScalars().GetName() 766 cdata = "<tr><td><b> cell data array </b></td><td>" + name + "</td></tr>" 767 768 allt = [ 769 "<table>", 770 "<tr>", 771 "<td>", 772 image, 773 "</td>", 774 "<td style='text-align: center; vertical-align: center;'><br/>", 775 help_text, 776 "<table>", 777 "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>", 778 "<tr><td><b> center of mass </b></td><td>" 779 + utils.precision(self.center_of_mass(), 3) 780 + "</td></tr>", 781 "<tr><td><b> average size </b></td><td>" + str(average_size) + "</td></tr>", 782 "<tr><td><b> nr. points </b></td><td>" + str(self.npoints) + "</td></tr>", 783 pdata, 784 cdata, 785 "</table>", 786 "</table>", 787 ] 788 return "\n".join(allt) 789 790 ################################################################################## 791 def __add__(self, meshs): 792 """ 793 Add two meshes or a list of meshes together to form an `Assembly` object. 794 """ 795 if isinstance(meshs, list): 796 alist = [self] 797 for l in meshs: 798 if isinstance(l, vedo.Assembly): 799 alist += l.unpack() 800 else: 801 alist += l 802 return vedo.assembly.Assembly(alist) 803 804 if isinstance(meshs, vedo.Assembly): 805 return meshs + self # use Assembly.__add__ 806 807 return vedo.assembly.Assembly([self, meshs]) 808 809 def polydata(self, **kwargs): 810 """ 811 Obsolete. Use property `.dataset` instead. 812 Returns the underlying `vtkPolyData` object. 813 """ 814 colors.printc( 815 "WARNING: call to .polydata() is obsolete, use property .dataset instead.", 816 c="y") 817 return self.dataset 818 819 def __copy__(self): 820 return self.clone(deep=False) 821 822 def __deepcopy__(self, memo): 823 return self.clone(deep=memo) 824 825 def copy(self, deep=True) -> Self: 826 """Return a copy of the object. Alias of `clone()`.""" 827 return self.clone(deep=deep) 828 829 def clone(self, deep=True) -> Self: 830 """ 831 Clone a `PointCloud` or `Mesh` object to make an exact copy of it. 832 Alias of `copy()`. 833 834 Arguments: 835 deep : (bool) 836 if False return a shallow copy of the mesh without copying the points array. 837 838 Examples: 839 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 840 841 ![](https://vedo.embl.es/images/basic/mirror.png) 842 """ 843 poly = vtki.vtkPolyData() 844 if deep or isinstance(deep, dict): # if a memo object is passed this checks as True 845 poly.DeepCopy(self.dataset) 846 else: 847 poly.ShallowCopy(self.dataset) 848 849 if isinstance(self, vedo.Mesh): 850 cloned = vedo.Mesh(poly) 851 else: 852 cloned = Points(poly) 853 # print([self], self.__class__) 854 # cloned = self.__class__(poly) 855 856 cloned.transform = self.transform.clone() 857 858 cloned.copy_properties_from(self) 859 860 cloned.name = str(self.name) 861 cloned.filename = str(self.filename) 862 cloned.info = dict(self.info) 863 cloned.pipeline = utils.OperationNode("clone", parents=[self], shape="diamond", c="#edede9") 864 865 if isinstance(deep, dict): 866 deep[id(self)] = cloned 867 868 return cloned 869 870 def compute_normals_with_pca(self, n=20, orientation_point=None, invert=False) -> Self: 871 """ 872 Generate point normals using PCA (principal component analysis). 873 This algorithm estimates a local tangent plane around each sample point p 874 by considering a small neighborhood of points around p, and fitting a plane 875 to the neighborhood (via PCA). 876 877 Arguments: 878 n : (int) 879 neighborhood size to calculate the normal 880 orientation_point : (list) 881 adjust the +/- sign of the normals so that 882 the normals all point towards a specified point. If None, perform a traversal 883 of the point cloud and flip neighboring normals so that they are mutually consistent. 884 invert : (bool) 885 flip all normals 886 """ 887 poly = self.dataset 888 pcan = vtki.new("PCANormalEstimation") 889 pcan.SetInputData(poly) 890 pcan.SetSampleSize(n) 891 892 if orientation_point is not None: 893 pcan.SetNormalOrientationToPoint() 894 pcan.SetOrientationPoint(orientation_point) 895 else: 896 pcan.SetNormalOrientationToGraphTraversal() 897 898 if invert: 899 pcan.FlipNormalsOn() 900 pcan.Update() 901 902 varr = pcan.GetOutput().GetPointData().GetNormals() 903 varr.SetName("Normals") 904 self.dataset.GetPointData().SetNormals(varr) 905 self.dataset.GetPointData().Modified() 906 return self 907 908 def compute_acoplanarity(self, n=25, radius=None, on="points") -> Self: 909 """ 910 Compute acoplanarity which is a measure of how much a local region of the mesh 911 differs from a plane. 912 913 The information is stored in a `pointdata` or `celldata` array with name 'Acoplanarity'. 914 915 Either `n` (number of neighbour points) or `radius` (radius of local search) can be specified. 916 If a radius value is given and not enough points fall inside it, then a -1 is stored. 917 918 Example: 919 ```python 920 from vedo import * 921 msh = ParametricShape('RandomHills') 922 msh.compute_acoplanarity(radius=0.1, on='cells') 923 msh.cmap("coolwarm", on='cells').add_scalarbar() 924 msh.show(axes=1).close() 925 ``` 926 ![](https://vedo.embl.es/images/feats/acoplanarity.jpg) 927 """ 928 acoplanarities = [] 929 if "point" in on: 930 pts = self.vertices 931 elif "cell" in on: 932 pts = self.cell_centers 933 else: 934 raise ValueError(f"In compute_acoplanarity() set on to either 'cells' or 'points', not {on}") 935 936 for p in utils.progressbar(pts, delay=5, width=15, title=f"{on} acoplanarity"): 937 if n: 938 data = self.closest_point(p, n=n) 939 npts = n 940 elif radius: 941 data = self.closest_point(p, radius=radius) 942 npts = len(data) 943 944 try: 945 center = data.mean(axis=0) 946 res = np.linalg.svd(data - center) 947 acoplanarities.append(res[1][2] / npts) 948 except: 949 acoplanarities.append(-1.0) 950 951 if "point" in on: 952 self.pointdata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 953 else: 954 self.celldata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 955 return self 956 957 def distance_to(self, pcloud, signed=False, invert=False, name="Distance") -> np.ndarray: 958 """ 959 Computes the distance from one point cloud or mesh to another point cloud or mesh. 960 This new `pointdata` array is saved with default name "Distance". 961 962 Keywords `signed` and `invert` are used to compute signed distance, 963 but the mesh in that case must have polygonal faces (not a simple point cloud), 964 and normals must also be computed. 965 966 Examples: 967 - [distance2mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/distance2mesh.py) 968 969 ![](https://vedo.embl.es/images/basic/distance2mesh.png) 970 """ 971 if pcloud.dataset.GetNumberOfPolys(): 972 973 poly1 = self.dataset 974 poly2 = pcloud.dataset 975 df = vtki.new("DistancePolyDataFilter") 976 df.ComputeSecondDistanceOff() 977 df.SetInputData(0, poly1) 978 df.SetInputData(1, poly2) 979 df.SetSignedDistance(signed) 980 df.SetNegateDistance(invert) 981 df.Update() 982 scals = df.GetOutput().GetPointData().GetScalars() 983 dists = utils.vtk2numpy(scals) 984 985 else: # has no polygons 986 987 if signed: 988 vedo.logger.warning("distance_to() called with signed=True but input object has no polygons") 989 990 if not pcloud.point_locator: 991 pcloud.point_locator = vtki.new("PointLocator") 992 pcloud.point_locator.SetDataSet(pcloud.dataset) 993 pcloud.point_locator.BuildLocator() 994 995 ids = [] 996 ps1 = self.vertices 997 ps2 = pcloud.vertices 998 for p in ps1: 999 pid = pcloud.point_locator.FindClosestPoint(p) 1000 ids.append(pid) 1001 1002 deltas = ps2[ids] - ps1 1003 dists = np.linalg.norm(deltas, axis=1).astype(np.float32) 1004 scals = utils.numpy2vtk(dists) 1005 1006 scals.SetName(name) 1007 self.dataset.GetPointData().AddArray(scals) 1008 self.dataset.GetPointData().SetActiveScalars(scals.GetName()) 1009 rng = scals.GetRange() 1010 self.mapper.SetScalarRange(rng[0], rng[1]) 1011 self.mapper.ScalarVisibilityOn() 1012 1013 self.pipeline = utils.OperationNode( 1014 "distance_to", 1015 parents=[self, pcloud], 1016 shape="cylinder", 1017 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1018 ) 1019 return dists 1020 1021 def clean(self) -> Self: 1022 """Clean pointcloud or mesh by removing coincident points.""" 1023 cpd = vtki.new("CleanPolyData") 1024 cpd.PointMergingOn() 1025 cpd.ConvertLinesToPointsOff() 1026 cpd.ConvertPolysToLinesOff() 1027 cpd.ConvertStripsToPolysOff() 1028 cpd.SetInputData(self.dataset) 1029 cpd.Update() 1030 self._update(cpd.GetOutput()) 1031 self.pipeline = utils.OperationNode( 1032 "clean", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1033 ) 1034 return self 1035 1036 def subsample(self, fraction: float, absolute=False) -> Self: 1037 """ 1038 Subsample a point cloud by requiring that the points 1039 or vertices are far apart at least by the specified fraction of the object size. 1040 If a Mesh is passed the polygonal faces are not removed 1041 but holes can appear as their vertices are removed. 1042 1043 Examples: 1044 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1045 1046 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1047 1048 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1049 1050 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1051 """ 1052 if not absolute: 1053 if fraction > 1: 1054 vedo.logger.warning( 1055 f"subsample(fraction=...), fraction must be < 1, but is {fraction}" 1056 ) 1057 if fraction <= 0: 1058 return self 1059 1060 cpd = vtki.new("CleanPolyData") 1061 cpd.PointMergingOn() 1062 cpd.ConvertLinesToPointsOn() 1063 cpd.ConvertPolysToLinesOn() 1064 cpd.ConvertStripsToPolysOn() 1065 cpd.SetInputData(self.dataset) 1066 if absolute: 1067 cpd.SetTolerance(fraction / self.diagonal_size()) 1068 # cpd.SetToleranceIsAbsolute(absolute) 1069 else: 1070 cpd.SetTolerance(fraction) 1071 cpd.Update() 1072 1073 ps = 2 1074 if self.properties.GetRepresentation() == 0: 1075 ps = self.properties.GetPointSize() 1076 1077 self._update(cpd.GetOutput()) 1078 self.ps(ps) 1079 1080 self.pipeline = utils.OperationNode( 1081 "subsample", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1082 ) 1083 return self 1084 1085 def threshold(self, scalars: str, above=None, below=None, on="points") -> Self: 1086 """ 1087 Extracts cells where scalar value satisfies threshold criterion. 1088 1089 Arguments: 1090 scalars : (str) 1091 name of the scalars array. 1092 above : (float) 1093 minimum value of the scalar 1094 below : (float) 1095 maximum value of the scalar 1096 on : (str) 1097 if 'cells' assume array of scalars refers to cell data. 1098 1099 Examples: 1100 - [mesh_threshold.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_threshold.py) 1101 """ 1102 thres = vtki.new("Threshold") 1103 thres.SetInputData(self.dataset) 1104 1105 if on.startswith("c"): 1106 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_CELLS 1107 else: 1108 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_POINTS 1109 1110 thres.SetInputArrayToProcess(0, 0, 0, asso, scalars) 1111 1112 if above is None and below is not None: 1113 try: # vtk 9.2 1114 thres.ThresholdByLower(below) 1115 except AttributeError: # vtk 9.3 1116 thres.SetUpperThreshold(below) 1117 1118 elif below is None and above is not None: 1119 try: 1120 thres.ThresholdByUpper(above) 1121 except AttributeError: 1122 thres.SetLowerThreshold(above) 1123 else: 1124 try: 1125 thres.ThresholdBetween(above, below) 1126 except AttributeError: 1127 thres.SetUpperThreshold(below) 1128 thres.SetLowerThreshold(above) 1129 1130 thres.Update() 1131 1132 gf = vtki.new("GeometryFilter") 1133 gf.SetInputData(thres.GetOutput()) 1134 gf.Update() 1135 self._update(gf.GetOutput()) 1136 self.pipeline = utils.OperationNode("threshold", parents=[self]) 1137 return self 1138 1139 def quantize(self, value: float) -> Self: 1140 """ 1141 The user should input a value and all {x,y,z} coordinates 1142 will be quantized to that absolute grain size. 1143 """ 1144 qp = vtki.new("QuantizePolyDataPoints") 1145 qp.SetInputData(self.dataset) 1146 qp.SetQFactor(value) 1147 qp.Update() 1148 self._update(qp.GetOutput()) 1149 self.pipeline = utils.OperationNode("quantize", parents=[self]) 1150 return self 1151 1152 @property 1153 def vertex_normals(self) -> np.ndarray: 1154 """ 1155 Retrieve vertex normals as a numpy array. Same as `point_normals`. 1156 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1157 """ 1158 vtknormals = self.dataset.GetPointData().GetNormals() 1159 return utils.vtk2numpy(vtknormals) 1160 1161 @property 1162 def point_normals(self) -> np.ndarray: 1163 """ 1164 Retrieve vertex normals as a numpy array. Same as `vertex_normals`. 1165 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1166 """ 1167 vtknormals = self.dataset.GetPointData().GetNormals() 1168 return utils.vtk2numpy(vtknormals) 1169 1170 def align_to(self, target, iters=100, rigid=False, invert=False, use_centroids=False) -> Self: 1171 """ 1172 Aligned to target mesh through the `Iterative Closest Point` algorithm. 1173 1174 The core of the algorithm is to match each vertex in one surface with 1175 the closest surface point on the other, then apply the transformation 1176 that modify one surface to best match the other (in the least-square sense). 1177 1178 Arguments: 1179 rigid : (bool) 1180 if True do not allow scaling 1181 invert : (bool) 1182 if True start by aligning the target to the source but 1183 invert the transformation finally. Useful when the target is smaller 1184 than the source. 1185 use_centroids : (bool) 1186 start by matching the centroids of the two objects. 1187 1188 Examples: 1189 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1190 1191 ![](https://vedo.embl.es/images/basic/align1.png) 1192 1193 - [align2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align2.py) 1194 1195 ![](https://vedo.embl.es/images/basic/align2.png) 1196 """ 1197 icp = vtki.new("IterativeClosestPointTransform") 1198 icp.SetSource(self.dataset) 1199 icp.SetTarget(target.dataset) 1200 if invert: 1201 icp.Inverse() 1202 icp.SetMaximumNumberOfIterations(iters) 1203 if rigid: 1204 icp.GetLandmarkTransform().SetModeToRigidBody() 1205 icp.SetStartByMatchingCentroids(use_centroids) 1206 icp.Update() 1207 1208 self.apply_transform(icp.GetMatrix()) 1209 1210 self.pipeline = utils.OperationNode( 1211 "align_to", parents=[self, target], comment=f"rigid = {rigid}" 1212 ) 1213 return self 1214 1215 def align_to_bounding_box(self, msh, rigid=False) -> Self: 1216 """ 1217 Align the current object's bounding box to the bounding box 1218 of the input object. 1219 1220 Use `rigid=True` to disable scaling. 1221 1222 Example: 1223 [align6.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align6.py) 1224 """ 1225 lmt = vtki.vtkLandmarkTransform() 1226 ss = vtki.vtkPoints() 1227 xss0, xss1, yss0, yss1, zss0, zss1 = self.bounds() 1228 for p in [ 1229 [xss0, yss0, zss0], 1230 [xss1, yss0, zss0], 1231 [xss1, yss1, zss0], 1232 [xss0, yss1, zss0], 1233 [xss0, yss0, zss1], 1234 [xss1, yss0, zss1], 1235 [xss1, yss1, zss1], 1236 [xss0, yss1, zss1], 1237 ]: 1238 ss.InsertNextPoint(p) 1239 st = vtki.vtkPoints() 1240 xst0, xst1, yst0, yst1, zst0, zst1 = msh.bounds() 1241 for p in [ 1242 [xst0, yst0, zst0], 1243 [xst1, yst0, zst0], 1244 [xst1, yst1, zst0], 1245 [xst0, yst1, zst0], 1246 [xst0, yst0, zst1], 1247 [xst1, yst0, zst1], 1248 [xst1, yst1, zst1], 1249 [xst0, yst1, zst1], 1250 ]: 1251 st.InsertNextPoint(p) 1252 1253 lmt.SetSourceLandmarks(ss) 1254 lmt.SetTargetLandmarks(st) 1255 lmt.SetModeToAffine() 1256 if rigid: 1257 lmt.SetModeToRigidBody() 1258 lmt.Update() 1259 1260 LT = LinearTransform(lmt) 1261 self.apply_transform(LT) 1262 return self 1263 1264 def align_with_landmarks( 1265 self, 1266 source_landmarks, 1267 target_landmarks, 1268 rigid=False, 1269 affine=False, 1270 least_squares=False, 1271 ) -> Self: 1272 """ 1273 Transform mesh orientation and position based on a set of landmarks points. 1274 The algorithm finds the best matching of source points to target points 1275 in the mean least square sense, in one single step. 1276 1277 If `affine` is True the x, y and z axes can scale independently but stay collinear. 1278 With least_squares they can vary orientation. 1279 1280 Examples: 1281 - [align5.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align5.py) 1282 1283 ![](https://vedo.embl.es/images/basic/align5.png) 1284 """ 1285 1286 if utils.is_sequence(source_landmarks): 1287 ss = vtki.vtkPoints() 1288 for p in source_landmarks: 1289 ss.InsertNextPoint(p) 1290 else: 1291 ss = source_landmarks.dataset.GetPoints() 1292 if least_squares: 1293 source_landmarks = source_landmarks.vertices 1294 1295 if utils.is_sequence(target_landmarks): 1296 st = vtki.vtkPoints() 1297 for p in target_landmarks: 1298 st.InsertNextPoint(p) 1299 else: 1300 st = target_landmarks.GetPoints() 1301 if least_squares: 1302 target_landmarks = target_landmarks.vertices 1303 1304 if ss.GetNumberOfPoints() != st.GetNumberOfPoints(): 1305 n1 = ss.GetNumberOfPoints() 1306 n2 = st.GetNumberOfPoints() 1307 vedo.logger.error(f"source and target have different nr of points {n1} vs {n2}") 1308 raise RuntimeError() 1309 1310 if int(rigid) + int(affine) + int(least_squares) > 1: 1311 vedo.logger.error( 1312 "only one of rigid, affine, least_squares can be True at a time" 1313 ) 1314 raise RuntimeError() 1315 1316 lmt = vtki.vtkLandmarkTransform() 1317 lmt.SetSourceLandmarks(ss) 1318 lmt.SetTargetLandmarks(st) 1319 lmt.SetModeToSimilarity() 1320 1321 if rigid: 1322 lmt.SetModeToRigidBody() 1323 lmt.Update() 1324 1325 elif affine: 1326 lmt.SetModeToAffine() 1327 lmt.Update() 1328 1329 elif least_squares: 1330 cms = source_landmarks.mean(axis=0) 1331 cmt = target_landmarks.mean(axis=0) 1332 m = np.linalg.lstsq(source_landmarks - cms, target_landmarks - cmt, rcond=None)[0] 1333 M = vtki.vtkMatrix4x4() 1334 for i in range(3): 1335 for j in range(3): 1336 M.SetElement(j, i, m[i][j]) 1337 lmt = vtki.vtkTransform() 1338 lmt.Translate(cmt) 1339 lmt.Concatenate(M) 1340 lmt.Translate(-cms) 1341 1342 else: 1343 lmt.Update() 1344 1345 self.apply_transform(lmt) 1346 self.pipeline = utils.OperationNode("transform_with_landmarks", parents=[self]) 1347 return self 1348 1349 def normalize(self) -> Self: 1350 """Scale average size to unit. The scaling is performed around the center of mass.""" 1351 coords = self.vertices 1352 if not coords.shape[0]: 1353 return self 1354 cm = np.mean(coords, axis=0) 1355 pts = coords - cm 1356 xyz2 = np.sum(pts * pts, axis=0) 1357 scale = 1 / np.sqrt(np.sum(xyz2) / len(pts)) 1358 self.scale(scale, origin=cm) 1359 self.pipeline = utils.OperationNode("normalize", parents=[self]) 1360 return self 1361 1362 def mirror(self, axis="x", origin=True) -> Self: 1363 """ 1364 Mirror reflect along one of the cartesian axes 1365 1366 Arguments: 1367 axis : (str) 1368 axis to use for mirroring, must be set to `x, y, z`. 1369 Or any combination of those. 1370 origin : (list) 1371 use this point as the origin of the mirroring transformation. 1372 1373 Examples: 1374 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 1375 1376 ![](https://vedo.embl.es/images/basic/mirror.png) 1377 """ 1378 sx, sy, sz = 1, 1, 1 1379 if "x" in axis.lower(): sx = -1 1380 if "y" in axis.lower(): sy = -1 1381 if "z" in axis.lower(): sz = -1 1382 1383 self.scale([sx, sy, sz], origin=origin) 1384 1385 self.pipeline = utils.OperationNode( 1386 "mirror", comment=f"axis = {axis}", parents=[self]) 1387 1388 if sx * sy * sz < 0: 1389 if hasattr(self, "reverse"): 1390 self.reverse() 1391 return self 1392 1393 def flip_normals(self) -> Self: 1394 """Flip all normals orientation.""" 1395 rs = vtki.new("ReverseSense") 1396 rs.SetInputData(self.dataset) 1397 rs.ReverseCellsOff() 1398 rs.ReverseNormalsOn() 1399 rs.Update() 1400 self._update(rs.GetOutput()) 1401 self.pipeline = utils.OperationNode("flip_normals", parents=[self]) 1402 return self 1403 1404 def add_gaussian_noise(self, sigma=1.0) -> Self: 1405 """ 1406 Add gaussian noise to point positions. 1407 An extra array is added named "GaussianNoise" with the displacements. 1408 1409 Arguments: 1410 sigma : (float) 1411 nr. of standard deviations, expressed in percent of the diagonal size of mesh. 1412 Can also be a list `[sigma_x, sigma_y, sigma_z]`. 1413 1414 Example: 1415 ```python 1416 from vedo import Sphere 1417 Sphere().add_gaussian_noise(1.0).point_size(8).show().close() 1418 ``` 1419 """ 1420 sz = self.diagonal_size() 1421 pts = self.vertices 1422 n = len(pts) 1423 ns = (np.random.randn(n, 3) * sigma) * (sz / 100) 1424 vpts = vtki.vtkPoints() 1425 vpts.SetNumberOfPoints(n) 1426 vpts.SetData(utils.numpy2vtk(pts + ns, dtype=np.float32)) 1427 self.dataset.SetPoints(vpts) 1428 self.dataset.GetPoints().Modified() 1429 self.pointdata["GaussianNoise"] = -ns 1430 self.pipeline = utils.OperationNode( 1431 "gaussian_noise", parents=[self], shape="egg", comment=f"sigma = {sigma}" 1432 ) 1433 return self 1434 1435 def closest_point( 1436 self, pt, n=1, radius=None, return_point_id=False, return_cell_id=False 1437 ) -> Union[List[int], int, np.ndarray]: 1438 """ 1439 Find the closest point(s) on a mesh given from the input point `pt`. 1440 1441 Arguments: 1442 n : (int) 1443 if greater than 1, return a list of n ordered closest points 1444 radius : (float) 1445 if given, get all points within that radius. Then n is ignored. 1446 return_point_id : (bool) 1447 return point ID instead of coordinates 1448 return_cell_id : (bool) 1449 return cell ID in which the closest point sits 1450 1451 Examples: 1452 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1453 - [fitplanes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitplanes.py) 1454 - [quadratic_morphing.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/quadratic_morphing.py) 1455 1456 .. note:: 1457 The appropriate tree search locator is built on the fly and cached for speed. 1458 1459 If you want to reset it use `mymesh.point_locator=None` 1460 and / or `mymesh.cell_locator=None`. 1461 """ 1462 if len(pt) != 3: 1463 pt = [pt[0], pt[1], 0] 1464 1465 # NB: every time the mesh moves or is warped the locators are set to None 1466 if ((n > 1 or radius) or (n == 1 and return_point_id)) and not return_cell_id: 1467 poly = None 1468 if not self.point_locator: 1469 poly = self.dataset 1470 self.point_locator = vtki.new("StaticPointLocator") 1471 self.point_locator.SetDataSet(poly) 1472 self.point_locator.BuildLocator() 1473 1474 ########## 1475 if radius: 1476 vtklist = vtki.vtkIdList() 1477 self.point_locator.FindPointsWithinRadius(radius, pt, vtklist) 1478 elif n > 1: 1479 vtklist = vtki.vtkIdList() 1480 self.point_locator.FindClosestNPoints(n, pt, vtklist) 1481 else: # n==1 hence return_point_id==True 1482 ######## 1483 return self.point_locator.FindClosestPoint(pt) 1484 ######## 1485 1486 if return_point_id: 1487 ######## 1488 return utils.vtk2numpy(vtklist) 1489 ######## 1490 1491 if not poly: 1492 poly = self.dataset 1493 trgp = [] 1494 for i in range(vtklist.GetNumberOfIds()): 1495 trgp_ = [0, 0, 0] 1496 vi = vtklist.GetId(i) 1497 poly.GetPoints().GetPoint(vi, trgp_) 1498 trgp.append(trgp_) 1499 ######## 1500 return np.array(trgp) 1501 ######## 1502 1503 else: 1504 1505 if not self.cell_locator: 1506 poly = self.dataset 1507 1508 # As per Miquel example with limbs the vtkStaticCellLocator doesnt work !! 1509 # https://discourse.vtk.org/t/vtkstaticcelllocator-problem-vtk9-0-3/7854/4 1510 if vedo.vtk_version[0] >= 9 and vedo.vtk_version[1] > 0: 1511 self.cell_locator = vtki.new("StaticCellLocator") 1512 else: 1513 self.cell_locator = vtki.new("CellLocator") 1514 1515 self.cell_locator.SetDataSet(poly) 1516 self.cell_locator.BuildLocator() 1517 1518 if radius is not None: 1519 vedo.printc("Warning: closest_point() with radius is not implemented for cells.", c='r') 1520 1521 if n != 1: 1522 vedo.printc("Warning: closest_point() with n>1 is not implemented for cells.", c='r') 1523 1524 trgp = [0, 0, 0] 1525 cid = vtki.mutable(0) 1526 dist2 = vtki.mutable(0) 1527 subid = vtki.mutable(0) 1528 self.cell_locator.FindClosestPoint(pt, trgp, cid, subid, dist2) 1529 1530 if return_cell_id: 1531 return int(cid) 1532 1533 return np.array(trgp) 1534 1535 def auto_distance(self) -> np.ndarray: 1536 """ 1537 Calculate the distance to the closest point in the same cloud of points. 1538 The output is stored in a new pointdata array called "AutoDistance", 1539 and it is also returned by the function. 1540 """ 1541 points = self.vertices 1542 if not self.point_locator: 1543 self.point_locator = vtki.new("StaticPointLocator") 1544 self.point_locator.SetDataSet(self.dataset) 1545 self.point_locator.BuildLocator() 1546 qs = [] 1547 vtklist = vtki.vtkIdList() 1548 vtkpoints = self.dataset.GetPoints() 1549 for p in points: 1550 self.point_locator.FindClosestNPoints(2, p, vtklist) 1551 q = [0, 0, 0] 1552 pid = vtklist.GetId(1) 1553 vtkpoints.GetPoint(pid, q) 1554 qs.append(q) 1555 dists = np.linalg.norm(points - np.array(qs), axis=1) 1556 self.pointdata["AutoDistance"] = dists 1557 return dists 1558 1559 def hausdorff_distance(self, points) -> float: 1560 """ 1561 Compute the Hausdorff distance to the input point set. 1562 Returns a single `float`. 1563 1564 Example: 1565 ```python 1566 from vedo import * 1567 t = np.linspace(0, 2*np.pi, 100) 1568 x = 4/3 * sin(t)**3 1569 y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12 1570 pol1 = Line(np.c_[x,y], closed=True).triangulate() 1571 pol2 = Polygon(nsides=5).pos(2,2) 1572 d12 = pol1.distance_to(pol2) 1573 d21 = pol2.distance_to(pol1) 1574 pol1.lw(0).cmap("viridis") 1575 pol2.lw(0).cmap("viridis") 1576 print("distance d12, d21 :", min(d12), min(d21)) 1577 print("hausdorff distance:", pol1.hausdorff_distance(pol2)) 1578 print("chamfer distance :", pol1.chamfer_distance(pol2)) 1579 show(pol1, pol2, axes=1) 1580 ``` 1581 ![](https://vedo.embl.es/images/feats/heart.png) 1582 """ 1583 hp = vtki.new("HausdorffDistancePointSetFilter") 1584 hp.SetInputData(0, self.dataset) 1585 hp.SetInputData(1, points.dataset) 1586 hp.SetTargetDistanceMethodToPointToCell() 1587 hp.Update() 1588 return hp.GetHausdorffDistance() 1589 1590 def chamfer_distance(self, pcloud) -> float: 1591 """ 1592 Compute the Chamfer distance to the input point set. 1593 1594 Example: 1595 ```python 1596 from vedo import * 1597 cloud1 = np.random.randn(1000, 3) 1598 cloud2 = np.random.randn(1000, 3) + [1, 2, 3] 1599 c1 = Points(cloud1, r=5, c="red") 1600 c2 = Points(cloud2, r=5, c="green") 1601 d = c1.chamfer_distance(c2) 1602 show(f"Chamfer distance = {d}", c1, c2, axes=1).close() 1603 ``` 1604 """ 1605 # Definition of Chamfer distance may vary, here we use the average 1606 if not pcloud.point_locator: 1607 pcloud.point_locator = vtki.new("PointLocator") 1608 pcloud.point_locator.SetDataSet(pcloud.dataset) 1609 pcloud.point_locator.BuildLocator() 1610 if not self.point_locator: 1611 self.point_locator = vtki.new("PointLocator") 1612 self.point_locator.SetDataSet(self.dataset) 1613 self.point_locator.BuildLocator() 1614 1615 ps1 = self.vertices 1616 ps2 = pcloud.vertices 1617 1618 ids12 = [] 1619 for p in ps1: 1620 pid12 = pcloud.point_locator.FindClosestPoint(p) 1621 ids12.append(pid12) 1622 deltav = ps2[ids12] - ps1 1623 da = np.mean(np.linalg.norm(deltav, axis=1)) 1624 1625 ids21 = [] 1626 for p in ps2: 1627 pid21 = self.point_locator.FindClosestPoint(p) 1628 ids21.append(pid21) 1629 deltav = ps1[ids21] - ps2 1630 db = np.mean(np.linalg.norm(deltav, axis=1)) 1631 return (da + db) / 2 1632 1633 def remove_outliers(self, radius: float, neighbors=5) -> Self: 1634 """ 1635 Remove outliers from a cloud of points within the specified `radius` search. 1636 1637 Arguments: 1638 radius : (float) 1639 Specify the local search radius. 1640 neighbors : (int) 1641 Specify the number of neighbors that a point must have, 1642 within the specified radius, for the point to not be considered isolated. 1643 1644 Examples: 1645 - [clustering.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/clustering.py) 1646 1647 ![](https://vedo.embl.es/images/basic/clustering.png) 1648 """ 1649 removal = vtki.new("RadiusOutlierRemoval") 1650 removal.SetInputData(self.dataset) 1651 removal.SetRadius(radius) 1652 removal.SetNumberOfNeighbors(neighbors) 1653 removal.GenerateOutliersOff() 1654 removal.Update() 1655 inputobj = removal.GetOutput() 1656 if inputobj.GetNumberOfCells() == 0: 1657 carr = vtki.vtkCellArray() 1658 for i in range(inputobj.GetNumberOfPoints()): 1659 carr.InsertNextCell(1) 1660 carr.InsertCellPoint(i) 1661 inputobj.SetVerts(carr) 1662 self._update(removal.GetOutput()) 1663 self.pipeline = utils.OperationNode("remove_outliers", parents=[self]) 1664 return self 1665 1666 def relax_point_positions( 1667 self, 1668 n=10, 1669 iters=10, 1670 sub_iters=10, 1671 packing_factor=1, 1672 max_step=0, 1673 constraints=(), 1674 ) -> Self: 1675 """ 1676 Smooth mesh or points with a 1677 [Laplacian algorithm](https://vtk.org/doc/nightly/html/classvtkPointSmoothingFilter.html) 1678 variant. This modifies the coordinates of the input points by adjusting their positions 1679 to create a smooth distribution (and thereby form a pleasing packing of the points). 1680 Smoothing is performed by considering the effects of neighboring points on one another 1681 it uses a cubic cutoff function to produce repulsive forces between close points 1682 and attractive forces that are a little further away. 1683 1684 In general, the larger the neighborhood size, the greater the reduction in high frequency 1685 information. The memory and computational requirements of the algorithm may also 1686 significantly increase. 1687 1688 The algorithm incrementally adjusts the point positions through an iterative process. 1689 Basically points are moved due to the influence of neighboring points. 1690 1691 As points move, both the local connectivity and data attributes associated with each point 1692 must be updated. Rather than performing these expensive operations after every iteration, 1693 a number of sub-iterations can be specified. If so, then the neighborhood and attribute 1694 value updates occur only every sub iteration, which can improve performance significantly. 1695 1696 Arguments: 1697 n : (int) 1698 neighborhood size to calculate the Laplacian. 1699 iters : (int) 1700 number of iterations. 1701 sub_iters : (int) 1702 number of sub-iterations, i.e. the number of times the neighborhood and attribute 1703 value updates occur during each iteration. 1704 packing_factor : (float) 1705 adjust convergence speed. 1706 max_step : (float) 1707 Specify the maximum smoothing step size for each smoothing iteration. 1708 This limits the the distance over which a point can move in each iteration. 1709 As in all iterative methods, the stability of the process is sensitive to this parameter. 1710 In general, small step size and large numbers of iterations are more stable than a larger 1711 step size and a smaller numbers of iterations. 1712 constraints : (dict) 1713 dictionary of constraints. 1714 Point constraints are used to prevent points from moving, 1715 or to move only on a plane. This can prevent shrinking or growing point clouds. 1716 If enabled, a local topological analysis is performed to determine whether a point 1717 should be marked as fixed" i.e., never moves, or the point only moves on a plane, 1718 or the point can move freely. 1719 If all points in the neighborhood surrounding a point are in the cone defined by 1720 `fixed_angle`, then the point is classified as fixed. 1721 If all points in the neighborhood surrounding a point are in the cone defined by 1722 `boundary_angle`, then the point is classified as lying on a plane. 1723 Angles are expressed in degrees. 1724 1725 Example: 1726 ```py 1727 import numpy as np 1728 from vedo import Points, show 1729 from vedo.pyplot import histogram 1730 1731 vpts1 = Points(np.random.rand(10_000, 3)) 1732 dists = vpts1.auto_distance() 1733 h1 = histogram(dists, xlim=(0,0.08)).clone2d() 1734 1735 vpts2 = vpts1.clone().relax_point_positions(n=100, iters=20, sub_iters=10) 1736 dists = vpts2.auto_distance() 1737 h2 = histogram(dists, xlim=(0,0.08)).clone2d() 1738 1739 show([[vpts1, h1], [vpts2, h2]], N=2).close() 1740 ``` 1741 """ 1742 smooth = vtki.new("PointSmoothingFilter") 1743 smooth.SetInputData(self.dataset) 1744 smooth.SetSmoothingModeToUniform() 1745 smooth.SetNumberOfIterations(iters) 1746 smooth.SetNumberOfSubIterations(sub_iters) 1747 smooth.SetPackingFactor(packing_factor) 1748 if self.point_locator: 1749 smooth.SetLocator(self.point_locator) 1750 if not max_step: 1751 max_step = self.diagonal_size() / 100 1752 smooth.SetMaximumStepSize(max_step) 1753 smooth.SetNeighborhoodSize(n) 1754 if constraints: 1755 fixed_angle = constraints.get("fixed_angle", 45) 1756 boundary_angle = constraints.get("boundary_angle", 110) 1757 smooth.EnableConstraintsOn() 1758 smooth.SetFixedAngle(fixed_angle) 1759 smooth.SetBoundaryAngle(boundary_angle) 1760 smooth.GenerateConstraintScalarsOn() 1761 smooth.GenerateConstraintNormalsOn() 1762 smooth.Update() 1763 self._update(smooth.GetOutput()) 1764 self.metadata["PackingRadius"] = smooth.GetPackingRadius() 1765 self.pipeline = utils.OperationNode("relax_point_positions", parents=[self]) 1766 return self 1767 1768 def smooth_mls_1d(self, f=0.2, radius=None, n=0) -> Self: 1769 """ 1770 Smooth mesh or points with a `Moving Least Squares` variant. 1771 The point data array "Variances" will contain the residue calculated for each point. 1772 1773 Arguments: 1774 f : (float) 1775 smoothing factor - typical range is [0,2]. 1776 radius : (float) 1777 radius search in absolute units. 1778 If set then `f` is ignored. 1779 n : (int) 1780 number of neighbours to be used for the fit. 1781 If set then `f` and `radius` are ignored. 1782 1783 Examples: 1784 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1785 - [skeletonize.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/skeletonize.py) 1786 1787 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1788 """ 1789 coords = self.vertices 1790 ncoords = len(coords) 1791 1792 if n: 1793 Ncp = n 1794 elif radius: 1795 Ncp = 1 1796 else: 1797 Ncp = int(ncoords * f / 10) 1798 if Ncp < 5: 1799 vedo.logger.warning(f"Please choose a fraction higher than {f}") 1800 Ncp = 5 1801 1802 variances, newline = [], [] 1803 for p in coords: 1804 points = self.closest_point(p, n=Ncp, radius=radius) 1805 if len(points) < 4: 1806 continue 1807 1808 points = np.array(points) 1809 pointsmean = points.mean(axis=0) # plane center 1810 _, dd, vv = np.linalg.svd(points - pointsmean) 1811 newp = np.dot(p - pointsmean, vv[0]) * vv[0] + pointsmean 1812 variances.append(dd[1] + dd[2]) 1813 newline.append(newp) 1814 1815 self.pointdata["Variances"] = np.array(variances).astype(np.float32) 1816 self.vertices = newline 1817 self.pipeline = utils.OperationNode("smooth_mls_1d", parents=[self]) 1818 return self 1819 1820 def smooth_mls_2d(self, f=0.2, radius=None, n=0) -> Self: 1821 """ 1822 Smooth mesh or points with a `Moving Least Squares` algorithm variant. 1823 1824 The `mesh.pointdata['MLSVariance']` array will contain the residue calculated for each point. 1825 When a radius is specified, points that are isolated will not be moved and will get 1826 a 0 entry in array `mesh.pointdata['MLSValidPoint']`. 1827 1828 Arguments: 1829 f : (float) 1830 smoothing factor - typical range is [0, 2]. 1831 radius : (float | array) 1832 radius search in absolute units. Can be single value (float) or sequence 1833 for adaptive smoothing. If set then `f` is ignored. 1834 n : (int) 1835 number of neighbours to be used for the fit. 1836 If set then `f` and `radius` are ignored. 1837 1838 Examples: 1839 - [moving_least_squares2D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares2D.py) 1840 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1841 1842 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1843 """ 1844 coords = self.vertices 1845 ncoords = len(coords) 1846 1847 if n: 1848 Ncp = n 1849 radius = None 1850 elif radius is not None: 1851 Ncp = 1 1852 else: 1853 Ncp = int(ncoords * f / 100) 1854 if Ncp < 4: 1855 vedo.logger.error(f"please choose a f-value higher than {f}") 1856 Ncp = 4 1857 1858 variances, newpts, valid = [], [], [] 1859 radius_is_sequence = utils.is_sequence(radius) 1860 1861 pb = None 1862 if ncoords > 10000: 1863 pb = utils.ProgressBar(0, ncoords, delay=3) 1864 1865 for i, p in enumerate(coords): 1866 if pb: 1867 pb.print("smooth_mls_2d working ...") 1868 1869 # if a radius was provided for each point 1870 if radius_is_sequence: 1871 pts = self.closest_point(p, n=Ncp, radius=radius[i]) 1872 else: 1873 pts = self.closest_point(p, n=Ncp, radius=radius) 1874 1875 if len(pts) > 3: 1876 ptsmean = pts.mean(axis=0) # plane center 1877 _, dd, vv = np.linalg.svd(pts - ptsmean) 1878 cv = np.cross(vv[0], vv[1]) 1879 t = (np.dot(cv, ptsmean) - np.dot(cv, p)) / np.dot(cv, cv) 1880 newpts.append(p + cv * t) 1881 variances.append(dd[2]) 1882 if radius is not None: 1883 valid.append(1) 1884 else: 1885 newpts.append(p) 1886 variances.append(0) 1887 if radius is not None: 1888 valid.append(0) 1889 1890 if radius is not None: 1891 self.pointdata["MLSValidPoint"] = np.array(valid).astype(np.uint8) 1892 self.pointdata["MLSVariance"] = np.array(variances).astype(np.float32) 1893 1894 self.vertices = newpts 1895 1896 self.pipeline = utils.OperationNode("smooth_mls_2d", parents=[self]) 1897 return self 1898 1899 def smooth_lloyd_2d(self, iterations=2, bounds=None, options="Qbb Qc Qx") -> Self: 1900 """ 1901 Lloyd relaxation of a 2D pointcloud. 1902 1903 Arguments: 1904 iterations : (int) 1905 number of iterations. 1906 bounds : (list) 1907 bounding box of the domain. 1908 options : (str) 1909 options for the Qhull algorithm. 1910 """ 1911 # Credits: https://hatarilabs.com/ih-en/ 1912 # tutorial-to-create-a-geospatial-voronoi-sh-mesh-with-python-scipy-and-geopandas 1913 from scipy.spatial import Voronoi as scipy_voronoi 1914 1915 def _constrain_points(points): 1916 # Update any points that have drifted beyond the boundaries of this space 1917 if bounds is not None: 1918 for point in points: 1919 if point[0] < bounds[0]: point[0] = bounds[0] 1920 if point[0] > bounds[1]: point[0] = bounds[1] 1921 if point[1] < bounds[2]: point[1] = bounds[2] 1922 if point[1] > bounds[3]: point[1] = bounds[3] 1923 return points 1924 1925 def _find_centroid(vertices): 1926 # The equation for the method used here to find the centroid of a 1927 # 2D polygon is given here: https://en.wikipedia.org/wiki/Centroid#Of_a_polygon 1928 area = 0 1929 centroid_x = 0 1930 centroid_y = 0 1931 for i in range(len(vertices) - 1): 1932 step = (vertices[i, 0] * vertices[i + 1, 1]) - (vertices[i + 1, 0] * vertices[i, 1]) 1933 centroid_x += (vertices[i, 0] + vertices[i + 1, 0]) * step 1934 centroid_y += (vertices[i, 1] + vertices[i + 1, 1]) * step 1935 area += step 1936 if area: 1937 centroid_x = (1.0 / (3.0 * area)) * centroid_x 1938 centroid_y = (1.0 / (3.0 * area)) * centroid_y 1939 # prevent centroids from escaping bounding box 1940 return _constrain_points([[centroid_x, centroid_y]])[0] 1941 1942 def _relax(voron): 1943 # Moves each point to the centroid of its cell in the voronoi 1944 # map to "relax" the points (i.e. jitter the points so as 1945 # to spread them out within the space). 1946 centroids = [] 1947 for idx in voron.point_region: 1948 # the region is a series of indices into voronoi.vertices 1949 # remove point at infinity, designated by index -1 1950 region = [i for i in voron.regions[idx] if i != -1] 1951 # enclose the polygon 1952 region = region + [region[0]] 1953 verts = voron.vertices[region] 1954 # find the centroid of those vertices 1955 centroids.append(_find_centroid(verts)) 1956 return _constrain_points(centroids) 1957 1958 if bounds is None: 1959 bounds = self.bounds() 1960 1961 pts = self.vertices[:, (0, 1)] 1962 for i in range(iterations): 1963 vor = scipy_voronoi(pts, qhull_options=options) 1964 _constrain_points(vor.vertices) 1965 pts = _relax(vor) 1966 out = Points(pts) 1967 out.name = "MeshSmoothLloyd2D" 1968 out.pipeline = utils.OperationNode("smooth_lloyd", parents=[self]) 1969 return out 1970 1971 def project_on_plane(self, plane="z", point=None, direction=None) -> Self: 1972 """ 1973 Project the mesh on one of the Cartesian planes. 1974 1975 Arguments: 1976 plane : (str, Plane) 1977 if plane is `str`, plane can be one of ['x', 'y', 'z'], 1978 represents x-plane, y-plane and z-plane, respectively. 1979 Otherwise, plane should be an instance of `vedo.shapes.Plane`. 1980 point : (float, array) 1981 if plane is `str`, point should be a float represents the intercept. 1982 Otherwise, point is the camera point of perspective projection 1983 direction : (array) 1984 direction of oblique projection 1985 1986 Note: 1987 Parameters `point` and `direction` are only used if the given plane 1988 is an instance of `vedo.shapes.Plane`. And one of these two params 1989 should be left as `None` to specify the projection type. 1990 1991 Example: 1992 ```python 1993 s.project_on_plane(plane='z') # project to z-plane 1994 plane = Plane(pos=(4, 8, -4), normal=(-1, 0, 1), s=(5,5)) 1995 s.project_on_plane(plane=plane) # orthogonal projection 1996 s.project_on_plane(plane=plane, point=(6, 6, 6)) # perspective projection 1997 s.project_on_plane(plane=plane, direction=(1, 2, -1)) # oblique projection 1998 ``` 1999 2000 Examples: 2001 - [silhouette2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette2.py) 2002 2003 ![](https://vedo.embl.es/images/basic/silhouette2.png) 2004 """ 2005 coords = self.vertices 2006 2007 if plane == "x": 2008 coords[:, 0] = self.transform.position[0] 2009 intercept = self.xbounds()[0] if point is None else point 2010 self.x(intercept) 2011 elif plane == "y": 2012 coords[:, 1] = self.transform.position[1] 2013 intercept = self.ybounds()[0] if point is None else point 2014 self.y(intercept) 2015 elif plane == "z": 2016 coords[:, 2] = self.transform.position[2] 2017 intercept = self.zbounds()[0] if point is None else point 2018 self.z(intercept) 2019 2020 elif isinstance(plane, vedo.shapes.Plane): 2021 normal = plane.normal / np.linalg.norm(plane.normal) 2022 pl = np.hstack((normal, -np.dot(plane.pos(), normal))).reshape(4, 1) 2023 if direction is None and point is None: 2024 # orthogonal projection 2025 pt = np.hstack((normal, [0])).reshape(4, 1) 2026 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T # python3 only 2027 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2028 2029 elif direction is None: 2030 # perspective projection 2031 pt = np.hstack((np.array(point), [1])).reshape(4, 1) 2032 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2033 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2034 2035 elif point is None: 2036 # oblique projection 2037 pt = np.hstack((np.array(direction), [0])).reshape(4, 1) 2038 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2039 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2040 2041 coords = np.concatenate([coords, np.ones((coords.shape[:-1] + (1,)))], axis=-1) 2042 # coords = coords @ proj_mat.T 2043 coords = np.matmul(coords, proj_mat.T) 2044 coords = coords[:, :3] / coords[:, 3:] 2045 2046 else: 2047 vedo.logger.error(f"unknown plane {plane}") 2048 raise RuntimeError() 2049 2050 self.alpha(0.1) 2051 self.vertices = coords 2052 return self 2053 2054 def warp(self, source, target, sigma=1.0, mode="3d") -> Self: 2055 """ 2056 "Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set 2057 of source and target landmarks. Any point on the mesh close to a source landmark will 2058 be moved to a place close to the corresponding target landmark. 2059 The points in between are interpolated smoothly using 2060 Bookstein's Thin Plate Spline algorithm. 2061 2062 Transformation object can be accessed with `mesh.transform`. 2063 2064 Arguments: 2065 sigma : (float) 2066 specify the 'stiffness' of the spline. 2067 mode : (str) 2068 set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes) 2069 2070 Examples: 2071 - [interpolate_field.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/interpolate_field.py) 2072 - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py) 2073 - [warp2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp2.py) 2074 - [warp3.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp3.py) 2075 - [warp4a.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4a.py) 2076 - [warp4b.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4b.py) 2077 - [warp6.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp6.py) 2078 2079 ![](https://vedo.embl.es/images/advanced/warp2.png) 2080 """ 2081 parents = [self] 2082 2083 try: 2084 source = source.vertices 2085 parents.append(source) 2086 except AttributeError: 2087 source = utils.make3d(source) 2088 2089 try: 2090 target = target.vertices 2091 parents.append(target) 2092 except AttributeError: 2093 target = utils.make3d(target) 2094 2095 ns = len(source) 2096 nt = len(target) 2097 if ns != nt: 2098 vedo.logger.error(f"#source {ns} != {nt} #target points") 2099 raise RuntimeError() 2100 2101 NLT = NonLinearTransform() 2102 NLT.source_points = source 2103 NLT.target_points = target 2104 self.apply_transform(NLT) 2105 2106 self.pipeline = utils.OperationNode("warp", parents=parents) 2107 return self 2108 2109 def cut_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0), invert=False) -> Self: 2110 """ 2111 Cut the mesh with the plane defined by a point and a normal. 2112 2113 Arguments: 2114 origin : (array) 2115 the cutting plane goes through this point 2116 normal : (array) 2117 normal of the cutting plane 2118 2119 Example: 2120 ```python 2121 from vedo import Cube 2122 cube = Cube().cut_with_plane(normal=(1,1,1)) 2123 cube.back_color('pink').show().close() 2124 ``` 2125 ![](https://vedo.embl.es/images/feats/cut_with_plane_cube.png) 2126 2127 Examples: 2128 - [trail.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/trail.py) 2129 2130 ![](https://vedo.embl.es/images/simulations/trail.gif) 2131 2132 Check out also: 2133 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`. 2134 """ 2135 s = str(normal) 2136 if "x" in s: 2137 normal = (1, 0, 0) 2138 if "-" in s: 2139 normal = -np.array(normal) 2140 elif "y" in s: 2141 normal = (0, 1, 0) 2142 if "-" in s: 2143 normal = -np.array(normal) 2144 elif "z" in s: 2145 normal = (0, 0, 1) 2146 if "-" in s: 2147 normal = -np.array(normal) 2148 plane = vtki.vtkPlane() 2149 plane.SetOrigin(origin) 2150 plane.SetNormal(normal) 2151 2152 clipper = vtki.new("ClipPolyData") 2153 clipper.SetInputData(self.dataset) 2154 clipper.SetClipFunction(plane) 2155 clipper.GenerateClippedOutputOff() 2156 clipper.GenerateClipScalarsOff() 2157 clipper.SetInsideOut(invert) 2158 clipper.SetValue(0) 2159 clipper.Update() 2160 2161 self._update(clipper.GetOutput()) 2162 2163 self.pipeline = utils.OperationNode("cut_with_plane", parents=[self]) 2164 return self 2165 2166 def cut_with_planes(self, origins, normals, invert=False) -> Self: 2167 """ 2168 Cut the mesh with a convex set of planes defined by points and normals. 2169 2170 Arguments: 2171 origins : (array) 2172 each cutting plane goes through this point 2173 normals : (array) 2174 normal of each of the cutting planes 2175 invert : (bool) 2176 if True, cut outside instead of inside 2177 2178 Check out also: 2179 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()` 2180 """ 2181 2182 vpoints = vtki.vtkPoints() 2183 for p in utils.make3d(origins): 2184 vpoints.InsertNextPoint(p) 2185 normals = utils.make3d(normals) 2186 2187 planes = vtki.vtkPlanes() 2188 planes.SetPoints(vpoints) 2189 planes.SetNormals(utils.numpy2vtk(normals, dtype=float)) 2190 2191 clipper = vtki.new("ClipPolyData") 2192 clipper.SetInputData(self.dataset) 2193 clipper.SetInsideOut(invert) 2194 clipper.SetClipFunction(planes) 2195 clipper.GenerateClippedOutputOff() 2196 clipper.GenerateClipScalarsOff() 2197 clipper.SetValue(0) 2198 clipper.Update() 2199 2200 self._update(clipper.GetOutput()) 2201 2202 self.pipeline = utils.OperationNode("cut_with_planes", parents=[self]) 2203 return self 2204 2205 def cut_with_box(self, bounds, invert=False) -> Self: 2206 """ 2207 Cut the current mesh with a box or a set of boxes. 2208 This is much faster than `cut_with_mesh()`. 2209 2210 Input `bounds` can be either: 2211 - a Mesh or Points object 2212 - a list of 6 number representing a bounding box `[xmin,xmax, ymin,ymax, zmin,zmax]` 2213 - a list of bounding boxes like the above: `[[xmin1,...], [xmin2,...], ...]` 2214 2215 Example: 2216 ```python 2217 from vedo import Sphere, Cube, show 2218 mesh = Sphere(r=1, res=50) 2219 box = Cube(side=1.5).wireframe() 2220 mesh.cut_with_box(box) 2221 show(mesh, box, axes=1).close() 2222 ``` 2223 ![](https://vedo.embl.es/images/feats/cut_with_box_cube.png) 2224 2225 Check out also: 2226 `cut_with_line()`, `cut_with_plane()`, `cut_with_cylinder()` 2227 """ 2228 if isinstance(bounds, Points): 2229 bounds = bounds.bounds() 2230 2231 box = vtki.new("Box") 2232 if utils.is_sequence(bounds[0]): 2233 for bs in bounds: 2234 box.AddBounds(bs) 2235 else: 2236 box.SetBounds(bounds) 2237 2238 clipper = vtki.new("ClipPolyData") 2239 clipper.SetInputData(self.dataset) 2240 clipper.SetClipFunction(box) 2241 clipper.SetInsideOut(not invert) 2242 clipper.GenerateClippedOutputOff() 2243 clipper.GenerateClipScalarsOff() 2244 clipper.SetValue(0) 2245 clipper.Update() 2246 self._update(clipper.GetOutput()) 2247 2248 self.pipeline = utils.OperationNode("cut_with_box", parents=[self]) 2249 return self 2250 2251 def cut_with_line(self, points, invert=False, closed=True) -> Self: 2252 """ 2253 Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter. 2254 The polyline is defined by a set of points (z-coordinates are ignored). 2255 This is much faster than `cut_with_mesh()`. 2256 2257 Check out also: 2258 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2259 """ 2260 pplane = vtki.new("PolyPlane") 2261 if isinstance(points, Points): 2262 points = points.vertices.tolist() 2263 2264 if closed: 2265 if isinstance(points, np.ndarray): 2266 points = points.tolist() 2267 points.append(points[0]) 2268 2269 vpoints = vtki.vtkPoints() 2270 for p in points: 2271 if len(p) == 2: 2272 p = [p[0], p[1], 0.0] 2273 vpoints.InsertNextPoint(p) 2274 2275 n = len(points) 2276 polyline = vtki.new("PolyLine") 2277 polyline.Initialize(n, vpoints) 2278 polyline.GetPointIds().SetNumberOfIds(n) 2279 for i in range(n): 2280 polyline.GetPointIds().SetId(i, i) 2281 pplane.SetPolyLine(polyline) 2282 2283 clipper = vtki.new("ClipPolyData") 2284 clipper.SetInputData(self.dataset) 2285 clipper.SetClipFunction(pplane) 2286 clipper.SetInsideOut(invert) 2287 clipper.GenerateClippedOutputOff() 2288 clipper.GenerateClipScalarsOff() 2289 clipper.SetValue(0) 2290 clipper.Update() 2291 self._update(clipper.GetOutput()) 2292 2293 self.pipeline = utils.OperationNode("cut_with_line", parents=[self]) 2294 return self 2295 2296 def cut_with_cookiecutter(self, lines) -> Self: 2297 """ 2298 Cut the current mesh with a single line or a set of lines. 2299 2300 Input `lines` can be either: 2301 - a `Mesh` or `Points` object 2302 - a list of 3D points: `[(x1,y1,z1), (x2,y2,z2), ...]` 2303 - a list of 2D points: `[(x1,y1), (x2,y2), ...]` 2304 2305 Example: 2306 ```python 2307 from vedo import * 2308 grid = Mesh(dataurl + "dolfin_fine.vtk") 2309 grid.compute_quality().cmap("Greens") 2310 pols = merge( 2311 Polygon(nsides=10, r=0.3).pos(0.7, 0.3), 2312 Polygon(nsides=10, r=0.2).pos(0.3, 0.7), 2313 ) 2314 lines = pols.boundaries() 2315 cgrid = grid.clone().cut_with_cookiecutter(lines) 2316 grid.alpha(0.1).wireframe() 2317 show(grid, cgrid, lines, axes=8, bg='blackboard').close() 2318 ``` 2319 ![](https://vedo.embl.es/images/feats/cookiecutter.png) 2320 2321 Check out also: 2322 `cut_with_line()` and `cut_with_point_loop()` 2323 2324 Note: 2325 In case of a warning message like: 2326 "Mesh and trim loop point data attributes are different" 2327 consider interpolating the mesh point data to the loop points, 2328 Eg. (in the above example): 2329 ```python 2330 lines = pols.boundaries().interpolate_data_from(grid, n=2) 2331 ``` 2332 2333 Note: 2334 trying to invert the selection by reversing the loop order 2335 will have no effect in this method, hence it does not have 2336 the `invert` option. 2337 """ 2338 if utils.is_sequence(lines): 2339 lines = utils.make3d(lines) 2340 iline = list(range(len(lines))) + [0] 2341 poly = utils.buildPolyData(lines, lines=[iline]) 2342 else: 2343 poly = lines.dataset 2344 2345 # if invert: # not working 2346 # rev = vtki.new("ReverseSense") 2347 # rev.ReverseCellsOn() 2348 # rev.SetInputData(poly) 2349 # rev.Update() 2350 # poly = rev.GetOutput() 2351 2352 # Build loops from the polyline 2353 build_loops = vtki.new("ContourLoopExtraction") 2354 build_loops.SetGlobalWarningDisplay(0) 2355 build_loops.SetInputData(poly) 2356 build_loops.Update() 2357 boundary_poly = build_loops.GetOutput() 2358 2359 ccut = vtki.new("CookieCutter") 2360 ccut.SetInputData(self.dataset) 2361 ccut.SetLoopsData(boundary_poly) 2362 ccut.SetPointInterpolationToMeshEdges() 2363 # ccut.SetPointInterpolationToLoopEdges() 2364 ccut.PassCellDataOn() 2365 ccut.PassPointDataOn() 2366 ccut.Update() 2367 self._update(ccut.GetOutput()) 2368 2369 self.pipeline = utils.OperationNode("cut_with_cookiecutter", parents=[self]) 2370 return self 2371 2372 def cut_with_cylinder(self, center=(0, 0, 0), axis=(0, 0, 1), r=1, invert=False) -> Self: 2373 """ 2374 Cut the current mesh with an infinite cylinder. 2375 This is much faster than `cut_with_mesh()`. 2376 2377 Arguments: 2378 center : (array) 2379 the center of the cylinder 2380 normal : (array) 2381 direction of the cylinder axis 2382 r : (float) 2383 radius of the cylinder 2384 2385 Example: 2386 ```python 2387 from vedo import Disc, show 2388 disc = Disc(r1=1, r2=1.2) 2389 mesh = disc.extrude(3, res=50).linewidth(1) 2390 mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True) 2391 show(mesh, axes=1).close() 2392 ``` 2393 ![](https://vedo.embl.es/images/feats/cut_with_cylinder.png) 2394 2395 Examples: 2396 - [optics_main1.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/optics_main1.py) 2397 2398 Check out also: 2399 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2400 """ 2401 s = str(axis) 2402 if "x" in s: 2403 axis = (1, 0, 0) 2404 elif "y" in s: 2405 axis = (0, 1, 0) 2406 elif "z" in s: 2407 axis = (0, 0, 1) 2408 cyl = vtki.new("Cylinder") 2409 cyl.SetCenter(center) 2410 cyl.SetAxis(axis[0], axis[1], axis[2]) 2411 cyl.SetRadius(r) 2412 2413 clipper = vtki.new("ClipPolyData") 2414 clipper.SetInputData(self.dataset) 2415 clipper.SetClipFunction(cyl) 2416 clipper.SetInsideOut(not invert) 2417 clipper.GenerateClippedOutputOff() 2418 clipper.GenerateClipScalarsOff() 2419 clipper.SetValue(0) 2420 clipper.Update() 2421 self._update(clipper.GetOutput()) 2422 2423 self.pipeline = utils.OperationNode("cut_with_cylinder", parents=[self]) 2424 return self 2425 2426 def cut_with_sphere(self, center=(0, 0, 0), r=1.0, invert=False) -> Self: 2427 """ 2428 Cut the current mesh with an sphere. 2429 This is much faster than `cut_with_mesh()`. 2430 2431 Arguments: 2432 center : (array) 2433 the center of the sphere 2434 r : (float) 2435 radius of the sphere 2436 2437 Example: 2438 ```python 2439 from vedo import Disc, show 2440 disc = Disc(r1=1, r2=1.2) 2441 mesh = disc.extrude(3, res=50).linewidth(1) 2442 mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True) 2443 show(mesh, axes=1).close() 2444 ``` 2445 ![](https://vedo.embl.es/images/feats/cut_with_sphere.png) 2446 2447 Check out also: 2448 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2449 """ 2450 sph = vtki.new("Sphere") 2451 sph.SetCenter(center) 2452 sph.SetRadius(r) 2453 2454 clipper = vtki.new("ClipPolyData") 2455 clipper.SetInputData(self.dataset) 2456 clipper.SetClipFunction(sph) 2457 clipper.SetInsideOut(not invert) 2458 clipper.GenerateClippedOutputOff() 2459 clipper.GenerateClipScalarsOff() 2460 clipper.SetValue(0) 2461 clipper.Update() 2462 self._update(clipper.GetOutput()) 2463 self.pipeline = utils.OperationNode("cut_with_sphere", parents=[self]) 2464 return self 2465 2466 def cut_with_mesh(self, mesh, invert=False, keep=False) -> Union[Self, "vedo.Assembly"]: 2467 """ 2468 Cut an `Mesh` mesh with another `Mesh`. 2469 2470 Use `invert` to invert the selection. 2471 2472 Use `keep` to keep the cutoff part, in this case an `Assembly` is returned: 2473 the "cut" object and the "discarded" part of the original object. 2474 You can access both via `assembly.unpack()` method. 2475 2476 Example: 2477 ```python 2478 from vedo import * 2479 arr = np.random.randn(100000, 3)/2 2480 pts = Points(arr).c('red3').pos(5,0,0) 2481 cube = Cube().pos(4,0.5,0) 2482 assem = pts.cut_with_mesh(cube, keep=True) 2483 show(assem.unpack(), axes=1).close() 2484 ``` 2485 ![](https://vedo.embl.es/images/feats/cut_with_mesh.png) 2486 2487 Check out also: 2488 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2489 """ 2490 polymesh = mesh.dataset 2491 poly = self.dataset 2492 2493 # Create an array to hold distance information 2494 signed_distances = vtki.vtkFloatArray() 2495 signed_distances.SetNumberOfComponents(1) 2496 signed_distances.SetName("SignedDistances") 2497 2498 # implicit function that will be used to slice the mesh 2499 ippd = vtki.new("ImplicitPolyDataDistance") 2500 ippd.SetInput(polymesh) 2501 2502 # Evaluate the signed distance function at all of the grid points 2503 for pointId in range(poly.GetNumberOfPoints()): 2504 p = poly.GetPoint(pointId) 2505 signed_distance = ippd.EvaluateFunction(p) 2506 signed_distances.InsertNextValue(signed_distance) 2507 2508 currentscals = poly.GetPointData().GetScalars() 2509 if currentscals: 2510 currentscals = currentscals.GetName() 2511 2512 poly.GetPointData().AddArray(signed_distances) 2513 poly.GetPointData().SetActiveScalars("SignedDistances") 2514 2515 clipper = vtki.new("ClipPolyData") 2516 clipper.SetInputData(poly) 2517 clipper.SetInsideOut(not invert) 2518 clipper.SetGenerateClippedOutput(keep) 2519 clipper.SetValue(0.0) 2520 clipper.Update() 2521 cpoly = clipper.GetOutput() 2522 2523 if keep: 2524 kpoly = clipper.GetOutput(1) 2525 2526 vis = False 2527 if currentscals: 2528 cpoly.GetPointData().SetActiveScalars(currentscals) 2529 vis = self.mapper.GetScalarVisibility() 2530 2531 self._update(cpoly) 2532 2533 self.pointdata.remove("SignedDistances") 2534 self.mapper.SetScalarVisibility(vis) 2535 if keep: 2536 if isinstance(self, vedo.Mesh): 2537 cutoff = vedo.Mesh(kpoly) 2538 else: 2539 cutoff = vedo.Points(kpoly) 2540 # cutoff = self.__class__(kpoly) # this does not work properly 2541 cutoff.properties = vtki.vtkProperty() 2542 cutoff.properties.DeepCopy(self.properties) 2543 cutoff.actor.SetProperty(cutoff.properties) 2544 cutoff.c("k5").alpha(0.2) 2545 return vedo.Assembly([self, cutoff]) 2546 2547 self.pipeline = utils.OperationNode("cut_with_mesh", parents=[self, mesh]) 2548 return self 2549 2550 def cut_with_point_loop( 2551 self, points, invert=False, on="points", include_boundary=False 2552 ) -> Self: 2553 """ 2554 Cut an `Mesh` object with a set of points forming a closed loop. 2555 2556 Arguments: 2557 invert : (bool) 2558 invert selection (inside-out) 2559 on : (str) 2560 if 'cells' will extract the whole cells lying inside (or outside) the point loop 2561 include_boundary : (bool) 2562 include cells lying exactly on the boundary line. Only relevant on 'cells' mode 2563 2564 Examples: 2565 - [cut_with_points1.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points1.py) 2566 2567 ![](https://vedo.embl.es/images/advanced/cutWithPoints1.png) 2568 2569 - [cut_with_points2.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points2.py) 2570 2571 ![](https://vedo.embl.es/images/advanced/cutWithPoints2.png) 2572 """ 2573 if isinstance(points, Points): 2574 parents = [points] 2575 vpts = points.dataset.GetPoints() 2576 points = points.vertices 2577 else: 2578 parents = [self] 2579 vpts = vtki.vtkPoints() 2580 points = utils.make3d(points) 2581 for p in points: 2582 vpts.InsertNextPoint(p) 2583 2584 if "cell" in on: 2585 ippd = vtki.new("ImplicitSelectionLoop") 2586 ippd.SetLoop(vpts) 2587 ippd.AutomaticNormalGenerationOn() 2588 clipper = vtki.new("ExtractPolyDataGeometry") 2589 clipper.SetInputData(self.dataset) 2590 clipper.SetImplicitFunction(ippd) 2591 clipper.SetExtractInside(not invert) 2592 clipper.SetExtractBoundaryCells(include_boundary) 2593 else: 2594 spol = vtki.new("SelectPolyData") 2595 spol.SetLoop(vpts) 2596 spol.GenerateSelectionScalarsOn() 2597 spol.GenerateUnselectedOutputOff() 2598 spol.SetInputData(self.dataset) 2599 spol.Update() 2600 clipper = vtki.new("ClipPolyData") 2601 clipper.SetInputData(spol.GetOutput()) 2602 clipper.SetInsideOut(not invert) 2603 clipper.SetValue(0.0) 2604 clipper.Update() 2605 self._update(clipper.GetOutput()) 2606 2607 self.pipeline = utils.OperationNode("cut_with_pointloop", parents=parents) 2608 return self 2609 2610 def cut_with_scalar(self, value: float, name="", invert=False) -> Self: 2611 """ 2612 Cut a mesh or point cloud with some input scalar point-data. 2613 2614 Arguments: 2615 value : (float) 2616 cutting value 2617 name : (str) 2618 array name of the scalars to be used 2619 invert : (bool) 2620 flip selection 2621 2622 Example: 2623 ```python 2624 from vedo import * 2625 s = Sphere().lw(1) 2626 pts = s.vertices 2627 scalars = np.sin(3*pts[:,2]) + pts[:,0] 2628 s.pointdata["somevalues"] = scalars 2629 s.cut_with_scalar(0.3) 2630 s.cmap("Spectral", "somevalues").add_scalarbar() 2631 s.show(axes=1).close() 2632 ``` 2633 ![](https://vedo.embl.es/images/feats/cut_with_scalars.png) 2634 """ 2635 if name: 2636 self.pointdata.select(name) 2637 clipper = vtki.new("ClipPolyData") 2638 clipper.SetInputData(self.dataset) 2639 clipper.SetValue(value) 2640 clipper.GenerateClippedOutputOff() 2641 clipper.SetInsideOut(not invert) 2642 clipper.Update() 2643 self._update(clipper.GetOutput()) 2644 self.pipeline = utils.OperationNode("cut_with_scalar", parents=[self]) 2645 return self 2646 2647 def crop(self, 2648 top=None, bottom=None, right=None, left=None, front=None, back=None, 2649 bounds=()) -> Self: 2650 """ 2651 Crop an `Mesh` object. 2652 2653 Arguments: 2654 top : (float) 2655 fraction to crop from the top plane (positive z) 2656 bottom : (float) 2657 fraction to crop from the bottom plane (negative z) 2658 front : (float) 2659 fraction to crop from the front plane (positive y) 2660 back : (float) 2661 fraction to crop from the back plane (negative y) 2662 right : (float) 2663 fraction to crop from the right plane (positive x) 2664 left : (float) 2665 fraction to crop from the left plane (negative x) 2666 bounds : (list) 2667 bounding box of the crop region as `[x0,x1, y0,y1, z0,z1]` 2668 2669 Example: 2670 ```python 2671 from vedo import Sphere 2672 Sphere().crop(right=0.3, left=0.1).show() 2673 ``` 2674 ![](https://user-images.githubusercontent.com/32848391/57081955-0ef1e800-6cf6-11e9-99de-b45220939bc9.png) 2675 """ 2676 if not len(bounds): 2677 pos = np.array(self.pos()) 2678 x0, x1, y0, y1, z0, z1 = self.bounds() 2679 x0, y0, z0 = [x0, y0, z0] - pos 2680 x1, y1, z1 = [x1, y1, z1] - pos 2681 2682 dx, dy, dz = x1 - x0, y1 - y0, z1 - z0 2683 if top: 2684 z1 = z1 - top * dz 2685 if bottom: 2686 z0 = z0 + bottom * dz 2687 if front: 2688 y1 = y1 - front * dy 2689 if back: 2690 y0 = y0 + back * dy 2691 if right: 2692 x1 = x1 - right * dx 2693 if left: 2694 x0 = x0 + left * dx 2695 bounds = (x0, x1, y0, y1, z0, z1) 2696 2697 cu = vtki.new("Box") 2698 cu.SetBounds(bounds) 2699 2700 clipper = vtki.new("ClipPolyData") 2701 clipper.SetInputData(self.dataset) 2702 clipper.SetClipFunction(cu) 2703 clipper.InsideOutOn() 2704 clipper.GenerateClippedOutputOff() 2705 clipper.GenerateClipScalarsOff() 2706 clipper.SetValue(0) 2707 clipper.Update() 2708 self._update(clipper.GetOutput()) 2709 2710 self.pipeline = utils.OperationNode( 2711 "crop", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 2712 ) 2713 return self 2714 2715 def generate_surface_halo( 2716 self, 2717 distance=0.05, 2718 res=(50, 50, 50), 2719 bounds=(), 2720 maxdist=None, 2721 ) -> "vedo.Mesh": 2722 """ 2723 Generate the surface halo which sits at the specified distance from the input one. 2724 2725 Arguments: 2726 distance : (float) 2727 distance from the input surface 2728 res : (int) 2729 resolution of the surface 2730 bounds : (list) 2731 bounding box of the surface 2732 maxdist : (float) 2733 maximum distance to generate the surface 2734 """ 2735 if not bounds: 2736 bounds = self.bounds() 2737 2738 if not maxdist: 2739 maxdist = self.diagonal_size() / 2 2740 2741 imp = vtki.new("ImplicitModeller") 2742 imp.SetInputData(self.dataset) 2743 imp.SetSampleDimensions(res) 2744 if maxdist: 2745 imp.SetMaximumDistance(maxdist) 2746 if len(bounds) == 6: 2747 imp.SetModelBounds(bounds) 2748 contour = vtki.new("ContourFilter") 2749 contour.SetInputConnection(imp.GetOutputPort()) 2750 contour.SetValue(0, distance) 2751 contour.Update() 2752 out = vedo.Mesh(contour.GetOutput()) 2753 out.c("lightblue").alpha(0.25).lighting("off") 2754 out.pipeline = utils.OperationNode("generate_surface_halo", parents=[self]) 2755 return out 2756 2757 def generate_mesh( 2758 self, 2759 line_resolution=None, 2760 mesh_resolution=None, 2761 smooth=0.0, 2762 jitter=0.001, 2763 grid=None, 2764 quads=False, 2765 invert=False, 2766 ) -> Self: 2767 """ 2768 Generate a polygonal Mesh from a closed contour line. 2769 If line is not closed it will be closed with a straight segment. 2770 2771 Check also `generate_delaunay2d()`. 2772 2773 Arguments: 2774 line_resolution : (int) 2775 resolution of the contour line. The default is None, in this case 2776 the contour is not resampled. 2777 mesh_resolution : (int) 2778 resolution of the internal triangles not touching the boundary. 2779 smooth : (float) 2780 smoothing of the contour before meshing. 2781 jitter : (float) 2782 add a small noise to the internal points. 2783 grid : (Grid) 2784 manually pass a Grid object. The default is True. 2785 quads : (bool) 2786 generate a mesh of quads instead of triangles. 2787 invert : (bool) 2788 flip the line orientation. The default is False. 2789 2790 Examples: 2791 - [line2mesh_tri.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_tri.py) 2792 2793 ![](https://vedo.embl.es/images/advanced/line2mesh_tri.jpg) 2794 2795 - [line2mesh_quads.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_quads.py) 2796 2797 ![](https://vedo.embl.es/images/advanced/line2mesh_quads.png) 2798 """ 2799 if line_resolution is None: 2800 contour = vedo.shapes.Line(self.vertices) 2801 else: 2802 contour = vedo.shapes.Spline(self.vertices, smooth=smooth, res=line_resolution) 2803 contour.clean() 2804 2805 length = contour.length() 2806 density = length / contour.npoints 2807 # print(f"tomesh():\n\tline length = {length}") 2808 # print(f"\tdensity = {density} length/pt_separation") 2809 2810 x0, x1 = contour.xbounds() 2811 y0, y1 = contour.ybounds() 2812 2813 if grid is None: 2814 if mesh_resolution is None: 2815 resx = int((x1 - x0) / density + 0.5) 2816 resy = int((y1 - y0) / density + 0.5) 2817 # print(f"tmesh_resolution = {[resx, resy]}") 2818 else: 2819 if utils.is_sequence(mesh_resolution): 2820 resx, resy = mesh_resolution 2821 else: 2822 resx, resy = mesh_resolution, mesh_resolution 2823 grid = vedo.shapes.Grid( 2824 [(x0 + x1) / 2, (y0 + y1) / 2, 0], 2825 s=((x1 - x0) * 1.025, (y1 - y0) * 1.025), 2826 res=(resx, resy), 2827 ) 2828 else: 2829 grid = grid.clone() 2830 2831 cpts = contour.vertices 2832 2833 # make sure it's closed 2834 p0, p1 = cpts[0], cpts[-1] 2835 nj = max(2, int(utils.mag(p1 - p0) / density + 0.5)) 2836 joinline = vedo.shapes.Line(p1, p0, res=nj) 2837 contour = vedo.merge(contour, joinline).subsample(0.0001) 2838 2839 ####################################### quads 2840 if quads: 2841 cmesh = grid.clone().cut_with_point_loop(contour, on="cells", invert=invert) 2842 cmesh.wireframe(False).lw(0.5) 2843 cmesh.pipeline = utils.OperationNode( 2844 "generate_mesh", 2845 parents=[self, contour], 2846 comment=f"#quads {cmesh.dataset.GetNumberOfCells()}", 2847 ) 2848 return cmesh 2849 ############################################# 2850 2851 grid_tmp = grid.vertices.copy() 2852 2853 if jitter: 2854 np.random.seed(0) 2855 sigma = 1.0 / np.sqrt(grid.npoints) * grid.diagonal_size() * jitter 2856 # print(f"\tsigma jittering = {sigma}") 2857 grid_tmp += np.random.rand(grid.npoints, 3) * sigma 2858 grid_tmp[:, 2] = 0.0 2859 2860 todel = [] 2861 density /= np.sqrt(3) 2862 vgrid_tmp = Points(grid_tmp) 2863 2864 for p in contour.vertices: 2865 out = vgrid_tmp.closest_point(p, radius=density, return_point_id=True) 2866 todel += out.tolist() 2867 2868 grid_tmp = grid_tmp.tolist() 2869 for index in sorted(list(set(todel)), reverse=True): 2870 del grid_tmp[index] 2871 2872 points = contour.vertices.tolist() + grid_tmp 2873 if invert: 2874 boundary = list(reversed(range(contour.npoints))) 2875 else: 2876 boundary = list(range(contour.npoints)) 2877 2878 dln = Points(points).generate_delaunay2d(mode="xy", boundaries=[boundary]) 2879 dln.compute_normals(points=False) # fixes reversd faces 2880 dln.lw(1) 2881 2882 dln.pipeline = utils.OperationNode( 2883 "generate_mesh", 2884 parents=[self, contour], 2885 comment=f"#cells {dln.dataset.GetNumberOfCells()}", 2886 ) 2887 return dln 2888 2889 def reconstruct_surface( 2890 self, 2891 dims=(100, 100, 100), 2892 radius=None, 2893 sample_size=None, 2894 hole_filling=True, 2895 bounds=(), 2896 padding=0.05, 2897 ) -> "vedo.Mesh": 2898 """ 2899 Surface reconstruction from a scattered cloud of points. 2900 2901 Arguments: 2902 dims : (int) 2903 number of voxels in x, y and z to control precision. 2904 radius : (float) 2905 radius of influence of each point. 2906 Smaller values generally improve performance markedly. 2907 Note that after the signed distance function is computed, 2908 any voxel taking on the value >= radius 2909 is presumed to be "unseen" or uninitialized. 2910 sample_size : (int) 2911 if normals are not present 2912 they will be calculated using this sample size per point. 2913 hole_filling : (bool) 2914 enables hole filling, this generates 2915 separating surfaces between the empty and unseen portions of the volume. 2916 bounds : (list) 2917 region in space in which to perform the sampling 2918 in format (xmin,xmax, ymin,ymax, zim, zmax) 2919 padding : (float) 2920 increase by this fraction the bounding box 2921 2922 Examples: 2923 - [recosurface.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/recosurface.py) 2924 2925 ![](https://vedo.embl.es/images/advanced/recosurface.png) 2926 """ 2927 if not utils.is_sequence(dims): 2928 dims = (dims, dims, dims) 2929 2930 sdf = vtki.new("SignedDistance") 2931 2932 if len(bounds) == 6: 2933 sdf.SetBounds(bounds) 2934 else: 2935 x0, x1, y0, y1, z0, z1 = self.bounds() 2936 sdf.SetBounds( 2937 x0 - (x1 - x0) * padding, 2938 x1 + (x1 - x0) * padding, 2939 y0 - (y1 - y0) * padding, 2940 y1 + (y1 - y0) * padding, 2941 z0 - (z1 - z0) * padding, 2942 z1 + (z1 - z0) * padding, 2943 ) 2944 2945 bb = sdf.GetBounds() 2946 if bb[0]==bb[1]: 2947 vedo.logger.warning("reconstruct_surface(): zero x-range") 2948 if bb[2]==bb[3]: 2949 vedo.logger.warning("reconstruct_surface(): zero y-range") 2950 if bb[4]==bb[5]: 2951 vedo.logger.warning("reconstruct_surface(): zero z-range") 2952 2953 pd = self.dataset 2954 2955 if pd.GetPointData().GetNormals(): 2956 sdf.SetInputData(pd) 2957 else: 2958 normals = vtki.new("PCANormalEstimation") 2959 normals.SetInputData(pd) 2960 if not sample_size: 2961 sample_size = int(pd.GetNumberOfPoints() / 50) 2962 normals.SetSampleSize(sample_size) 2963 normals.SetNormalOrientationToGraphTraversal() 2964 sdf.SetInputConnection(normals.GetOutputPort()) 2965 # print("Recalculating normals with sample size =", sample_size) 2966 2967 if radius is None: 2968 radius = self.diagonal_size() / (sum(dims) / 3) * 5 2969 # print("Calculating mesh from points with radius =", radius) 2970 2971 sdf.SetRadius(radius) 2972 sdf.SetDimensions(dims) 2973 sdf.Update() 2974 2975 surface = vtki.new("ExtractSurface") 2976 surface.SetRadius(radius * 0.99) 2977 surface.SetHoleFilling(hole_filling) 2978 surface.ComputeNormalsOff() 2979 surface.ComputeGradientsOff() 2980 surface.SetInputConnection(sdf.GetOutputPort()) 2981 surface.Update() 2982 m = vedo.mesh.Mesh(surface.GetOutput(), c=self.color()) 2983 2984 m.pipeline = utils.OperationNode( 2985 "reconstruct_surface", 2986 parents=[self], 2987 comment=f"#pts {m.dataset.GetNumberOfPoints()}", 2988 ) 2989 return m 2990 2991 def compute_clustering(self, radius: float) -> Self: 2992 """ 2993 Cluster points in space. The `radius` is the radius of local search. 2994 2995 An array named "ClusterId" is added to `pointdata`. 2996 2997 Examples: 2998 - [clustering.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/clustering.py) 2999 3000 ![](https://vedo.embl.es/images/basic/clustering.png) 3001 """ 3002 cluster = vtki.new("EuclideanClusterExtraction") 3003 cluster.SetInputData(self.dataset) 3004 cluster.SetExtractionModeToAllClusters() 3005 cluster.SetRadius(radius) 3006 cluster.ColorClustersOn() 3007 cluster.Update() 3008 idsarr = cluster.GetOutput().GetPointData().GetArray("ClusterId") 3009 self.dataset.GetPointData().AddArray(idsarr) 3010 self.pipeline = utils.OperationNode( 3011 "compute_clustering", parents=[self], comment=f"radius = {radius}" 3012 ) 3013 return self 3014 3015 def compute_connections(self, radius, mode=0, regions=(), vrange=(0, 1), seeds=(), angle=0.0) -> Self: 3016 """ 3017 Extracts and/or segments points from a point cloud based on geometric distance measures 3018 (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range. 3019 The default operation is to segment the points into "connected" regions where the connection 3020 is determined by an appropriate distance measure. Each region is given a region id. 3021 3022 Optionally, the filter can output the largest connected region of points; a particular region 3023 (via id specification); those regions that are seeded using a list of input point ids; 3024 or the region of points closest to a specified position. 3025 3026 The key parameter of this filter is the radius defining a sphere around each point which defines 3027 a local neighborhood: any other points in the local neighborhood are assumed connected to the point. 3028 Note that the radius is defined in absolute terms. 3029 3030 Other parameters are used to further qualify what it means to be a neighboring point. 3031 For example, scalar range and/or point normals can be used to further constrain the neighborhood. 3032 Also the extraction mode defines how the filter operates. 3033 By default, all regions are extracted but it is possible to extract particular regions; 3034 the region closest to a seed point; seeded regions; or the largest region found while processing. 3035 By default, all regions are extracted. 3036 3037 On output, all points are labeled with a region number. 3038 However note that the number of input and output points may not be the same: 3039 if not extracting all regions then the output size may be less than the input size. 3040 3041 Arguments: 3042 radius : (float) 3043 variable specifying a local sphere used to define local point neighborhood 3044 mode : (int) 3045 - 0, Extract all regions 3046 - 1, Extract point seeded regions 3047 - 2, Extract largest region 3048 - 3, Test specified regions 3049 - 4, Extract all regions with scalar connectivity 3050 - 5, Extract point seeded regions 3051 regions : (list) 3052 a list of non-negative regions id to extract 3053 vrange : (list) 3054 scalar range to use to extract points based on scalar connectivity 3055 seeds : (list) 3056 a list of non-negative point seed ids 3057 angle : (list) 3058 points are connected if the angle between their normals is 3059 within this angle threshold (expressed in degrees). 3060 """ 3061 # https://vtk.org/doc/nightly/html/classvtkConnectedPointsFilter.html 3062 cpf = vtki.new("ConnectedPointsFilter") 3063 cpf.SetInputData(self.dataset) 3064 cpf.SetRadius(radius) 3065 if mode == 0: # Extract all regions 3066 pass 3067 3068 elif mode == 1: # Extract point seeded regions 3069 cpf.SetExtractionModeToPointSeededRegions() 3070 for s in seeds: 3071 cpf.AddSeed(s) 3072 3073 elif mode == 2: # Test largest region 3074 cpf.SetExtractionModeToLargestRegion() 3075 3076 elif mode == 3: # Test specified regions 3077 cpf.SetExtractionModeToSpecifiedRegions() 3078 for r in regions: 3079 cpf.AddSpecifiedRegion(r) 3080 3081 elif mode == 4: # Extract all regions with scalar connectivity 3082 cpf.SetExtractionModeToLargestRegion() 3083 cpf.ScalarConnectivityOn() 3084 cpf.SetScalarRange(vrange[0], vrange[1]) 3085 3086 elif mode == 5: # Extract point seeded regions 3087 cpf.SetExtractionModeToLargestRegion() 3088 cpf.ScalarConnectivityOn() 3089 cpf.SetScalarRange(vrange[0], vrange[1]) 3090 cpf.AlignedNormalsOn() 3091 cpf.SetNormalAngle(angle) 3092 3093 cpf.Update() 3094 self._update(cpf.GetOutput(), reset_locators=False) 3095 return self 3096 3097 def compute_camera_distance(self) -> np.ndarray: 3098 """ 3099 Calculate the distance from points to the camera. 3100 3101 A pointdata array is created with name 'DistanceToCamera' and returned. 3102 """ 3103 if vedo.plotter_instance and vedo.plotter_instance.renderer: 3104 poly = self.dataset 3105 dc = vtki.new("DistanceToCamera") 3106 dc.SetInputData(poly) 3107 dc.SetRenderer(vedo.plotter_instance.renderer) 3108 dc.Update() 3109 self._update(dc.GetOutput(), reset_locators=False) 3110 return self.pointdata["DistanceToCamera"] 3111 return np.array([]) 3112 3113 def densify(self, target_distance=0.1, nclosest=6, radius=None, niter=1, nmax=None) -> Self: 3114 """ 3115 Return a copy of the cloud with new added points. 3116 The new points are created in such a way that all points in any local neighborhood are 3117 within a target distance of one another. 3118 3119 For each input point, the distance to all points in its neighborhood is computed. 3120 If any of its neighbors is further than the target distance, 3121 the edge connecting the point and its neighbor is bisected and 3122 a new point is inserted at the bisection point. 3123 A single pass is completed once all the input points are visited. 3124 Then the process repeats to the number of iterations. 3125 3126 Examples: 3127 - [densifycloud.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/densifycloud.py) 3128 3129 ![](https://vedo.embl.es/images/volumetric/densifycloud.png) 3130 3131 .. note:: 3132 Points will be created in an iterative fashion until all points in their 3133 local neighborhood are the target distance apart or less. 3134 Note that the process may terminate early due to the 3135 number of iterations. By default the target distance is set to 0.5. 3136 Note that the target_distance should be less than the radius 3137 or nothing will change on output. 3138 3139 .. warning:: 3140 This class can generate a lot of points very quickly. 3141 The maximum number of iterations is by default set to =1.0 for this reason. 3142 Increase the number of iterations very carefully. 3143 Also, `nmax` can be set to limit the explosion of points. 3144 It is also recommended that a N closest neighborhood is used. 3145 3146 """ 3147 src = vtki.new("ProgrammableSource") 3148 opts = self.vertices 3149 # zeros = np.zeros(3) 3150 3151 def _read_points(): 3152 output = src.GetPolyDataOutput() 3153 points = vtki.vtkPoints() 3154 for p in opts: 3155 # print(p) 3156 # if not np.array_equal(p, zeros): 3157 points.InsertNextPoint(p) 3158 output.SetPoints(points) 3159 3160 src.SetExecuteMethod(_read_points) 3161 3162 dens = vtki.new("DensifyPointCloudFilter") 3163 dens.SetInputConnection(src.GetOutputPort()) 3164 # dens.SetInputData(self.dataset) # this does not work 3165 dens.InterpolateAttributeDataOn() 3166 dens.SetTargetDistance(target_distance) 3167 dens.SetMaximumNumberOfIterations(niter) 3168 if nmax: 3169 dens.SetMaximumNumberOfPoints(nmax) 3170 3171 if radius: 3172 dens.SetNeighborhoodTypeToRadius() 3173 dens.SetRadius(radius) 3174 elif nclosest: 3175 dens.SetNeighborhoodTypeToNClosest() 3176 dens.SetNumberOfClosestPoints(nclosest) 3177 else: 3178 vedo.logger.error("set either radius or nclosest") 3179 raise RuntimeError() 3180 dens.Update() 3181 3182 cld = Points(dens.GetOutput()) 3183 cld.copy_properties_from(self) 3184 cld.interpolate_data_from(self, n=nclosest, radius=radius) 3185 cld.name = "DensifiedCloud" 3186 cld.pipeline = utils.OperationNode( 3187 "densify", 3188 parents=[self], 3189 c="#e9c46a:", 3190 comment=f"#pts {cld.dataset.GetNumberOfPoints()}", 3191 ) 3192 return cld 3193 3194 ############################################################################### 3195 ## stuff returning a Volume 3196 ############################################################################### 3197 3198 def density( 3199 self, dims=(40, 40, 40), bounds=None, radius=None, compute_gradient=False, locator=None 3200 ) -> "vedo.Volume": 3201 """ 3202 Generate a density field from a point cloud. Input can also be a set of 3D coordinates. 3203 Output is a `Volume`. 3204 3205 The local neighborhood is specified as the `radius` around each sample position (each voxel). 3206 If left to None, the radius is automatically computed as the diagonal of the bounding box 3207 and can be accessed via `vol.metadata["radius"]`. 3208 The density is expressed as the number of counts in the radius search. 3209 3210 Arguments: 3211 dims : (int, list) 3212 number of voxels in x, y and z of the output Volume. 3213 compute_gradient : (bool) 3214 Turn on/off the generation of the gradient vector, 3215 gradient magnitude scalar, and function classification scalar. 3216 By default this is off. Note that this will increase execution time 3217 and the size of the output. (The names of these point data arrays are: 3218 "Gradient", "Gradient Magnitude", and "Classification") 3219 locator : (vtkPointLocator) 3220 can be assigned from a previous call for speed (access it via `object.point_locator`). 3221 3222 Examples: 3223 - [plot_density3d.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/plot_density3d.py) 3224 3225 ![](https://vedo.embl.es/images/pyplot/plot_density3d.png) 3226 """ 3227 pdf = vtki.new("PointDensityFilter") 3228 pdf.SetInputData(self.dataset) 3229 3230 if not utils.is_sequence(dims): 3231 dims = [dims, dims, dims] 3232 3233 if bounds is None: 3234 bounds = list(self.bounds()) 3235 elif len(bounds) == 4: 3236 bounds = [*bounds, 0, 0] 3237 3238 if bounds[5] - bounds[4] == 0 or len(dims) == 2: # its 2D 3239 dims = list(dims) 3240 dims = [dims[0], dims[1], 2] 3241 diag = self.diagonal_size() 3242 bounds[5] = bounds[4] + diag / 1000 3243 pdf.SetModelBounds(bounds) 3244 3245 pdf.SetSampleDimensions(dims) 3246 3247 if locator: 3248 pdf.SetLocator(locator) 3249 3250 pdf.SetDensityEstimateToFixedRadius() 3251 if radius is None: 3252 radius = self.diagonal_size() / 20 3253 pdf.SetRadius(radius) 3254 pdf.SetComputeGradient(compute_gradient) 3255 pdf.Update() 3256 3257 vol = vedo.Volume(pdf.GetOutput()).mode(1) 3258 vol.name = "PointDensity" 3259 vol.metadata["radius"] = radius 3260 vol.locator = pdf.GetLocator() 3261 vol.pipeline = utils.OperationNode( 3262 "density", parents=[self], comment=f"dims={tuple(vol.dimensions())}" 3263 ) 3264 return vol 3265 3266 3267 def tovolume( 3268 self, 3269 kernel="shepard", 3270 radius=None, 3271 n=None, 3272 bounds=None, 3273 null_value=None, 3274 dims=(25, 25, 25), 3275 ) -> "vedo.Volume": 3276 """ 3277 Generate a `Volume` by interpolating a scalar 3278 or vector field which is only known on a scattered set of points or mesh. 3279 Available interpolation kernels are: shepard, gaussian, or linear. 3280 3281 Arguments: 3282 kernel : (str) 3283 interpolation kernel type [shepard] 3284 radius : (float) 3285 radius of the local search 3286 n : (int) 3287 number of point to use for interpolation 3288 bounds : (list) 3289 bounding box of the output Volume object 3290 dims : (list) 3291 dimensions of the output Volume object 3292 null_value : (float) 3293 value to be assigned to invalid points 3294 3295 Examples: 3296 - [interpolate_volume.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/interpolate_volume.py) 3297 3298 ![](https://vedo.embl.es/images/volumetric/59095175-1ec5a300-8918-11e9-8bc0-fd35c8981e2b.jpg) 3299 """ 3300 if radius is None and not n: 3301 vedo.logger.error("please set either radius or n") 3302 raise RuntimeError 3303 3304 poly = self.dataset 3305 3306 # Create a probe volume 3307 probe = vtki.vtkImageData() 3308 probe.SetDimensions(dims) 3309 if bounds is None: 3310 bounds = self.bounds() 3311 probe.SetOrigin(bounds[0], bounds[2], bounds[4]) 3312 probe.SetSpacing( 3313 (bounds[1] - bounds[0]) / dims[0], 3314 (bounds[3] - bounds[2]) / dims[1], 3315 (bounds[5] - bounds[4]) / dims[2], 3316 ) 3317 3318 if not self.point_locator: 3319 self.point_locator = vtki.new("PointLocator") 3320 self.point_locator.SetDataSet(poly) 3321 self.point_locator.BuildLocator() 3322 3323 if kernel == "shepard": 3324 kern = vtki.new("ShepardKernel") 3325 kern.SetPowerParameter(2) 3326 elif kernel == "gaussian": 3327 kern = vtki.new("GaussianKernel") 3328 elif kernel == "linear": 3329 kern = vtki.new("LinearKernel") 3330 else: 3331 vedo.logger.error("Error in tovolume(), available kernels are:") 3332 vedo.logger.error(" [shepard, gaussian, linear]") 3333 raise RuntimeError() 3334 3335 if radius: 3336 kern.SetRadius(radius) 3337 3338 interpolator = vtki.new("PointInterpolator") 3339 interpolator.SetInputData(probe) 3340 interpolator.SetSourceData(poly) 3341 interpolator.SetKernel(kern) 3342 interpolator.SetLocator(self.point_locator) 3343 3344 if n: 3345 kern.SetNumberOfPoints(n) 3346 kern.SetKernelFootprintToNClosest() 3347 else: 3348 kern.SetRadius(radius) 3349 3350 if null_value is not None: 3351 interpolator.SetNullValue(null_value) 3352 else: 3353 interpolator.SetNullPointsStrategyToClosestPoint() 3354 interpolator.Update() 3355 3356 vol = vedo.Volume(interpolator.GetOutput()) 3357 3358 vol.pipeline = utils.OperationNode( 3359 "signed_distance", 3360 parents=[self], 3361 comment=f"dims={tuple(vol.dimensions())}", 3362 c="#e9c46a:#0096c7", 3363 ) 3364 return vol 3365 3366 ################################################################################# 3367 def generate_segments(self, istart=0, rmax=1e30, niter=3) -> "vedo.shapes.Lines": 3368 """ 3369 Generate a line segments from a set of points. 3370 The algorithm is based on the closest point search. 3371 3372 Returns a `Line` object. 3373 This object contains the a metadata array of used vertex counts in "UsedVertexCount" 3374 and the sum of the length of the segments in "SegmentsLengthSum". 3375 3376 Arguments: 3377 istart : (int) 3378 index of the starting point 3379 rmax : (float) 3380 maximum length of a segment 3381 niter : (int) 3382 number of iterations or passes through the points 3383 3384 Examples: 3385 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 3386 """ 3387 points = self.vertices 3388 segments = [] 3389 dists = [] 3390 n = len(points) 3391 used = np.zeros(n, dtype=int) 3392 for _ in range(niter): 3393 i = istart 3394 for _ in range(n): 3395 p = points[i] 3396 ids = self.closest_point(p, n=4, return_point_id=True) 3397 j = ids[1] 3398 if used[j] > 1 or [j, i] in segments: 3399 j = ids[2] 3400 if used[j] > 1: 3401 j = ids[3] 3402 d = np.linalg.norm(p - points[j]) 3403 if used[j] > 1 or used[i] > 1 or d > rmax: 3404 i += 1 3405 if i >= n: 3406 i = 0 3407 continue 3408 used[i] += 1 3409 used[j] += 1 3410 segments.append([i, j]) 3411 dists.append(d) 3412 i = j 3413 segments = np.array(segments, dtype=int) 3414 3415 lines = vedo.shapes.Lines(points[segments], c="k", lw=3) 3416 lines.metadata["UsedVertexCount"] = used 3417 lines.metadata["SegmentsLengthSum"] = np.sum(dists) 3418 lines.pipeline = utils.OperationNode("generate_segments", parents=[self]) 3419 lines.name = "Segments" 3420 return lines 3421 3422 def generate_delaunay2d( 3423 self, 3424 mode="scipy", 3425 boundaries=(), 3426 tol=None, 3427 alpha=0.0, 3428 offset=0.0, 3429 transform=None, 3430 ) -> "vedo.mesh.Mesh": 3431 """ 3432 Create a mesh from points in the XY plane. 3433 If `mode='fit'` then the filter computes a best fitting 3434 plane and projects the points onto it. 3435 3436 Check also `generate_mesh()`. 3437 3438 Arguments: 3439 tol : (float) 3440 specify a tolerance to control discarding of closely spaced points. 3441 This tolerance is specified as a fraction of the diagonal length of the bounding box of the points. 3442 alpha : (float) 3443 for a non-zero alpha value, only edges or triangles contained 3444 within a sphere centered at mesh vertices will be output. 3445 Otherwise, only triangles will be output. 3446 offset : (float) 3447 multiplier to control the size of the initial, bounding Delaunay triangulation. 3448 transform: (LinearTransform, NonLinearTransform) 3449 a transformation which is applied to points to generate a 2D problem. 3450 This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane. 3451 The points are transformed and triangulated. 3452 The topology of triangulated points is used as the output topology. 3453 3454 Examples: 3455 - [delaunay2d.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delaunay2d.py) 3456 3457 ![](https://vedo.embl.es/images/basic/delaunay2d.png) 3458 """ 3459 plist = self.vertices.copy() 3460 3461 ######################################################### 3462 if mode == "scipy": 3463 from scipy.spatial import Delaunay as scipy_delaunay 3464 3465 tri = scipy_delaunay(plist[:, 0:2]) 3466 return vedo.mesh.Mesh([plist, tri.simplices]) 3467 ########################################################## 3468 3469 pd = vtki.vtkPolyData() 3470 vpts = vtki.vtkPoints() 3471 vpts.SetData(utils.numpy2vtk(plist, dtype=np.float32)) 3472 pd.SetPoints(vpts) 3473 3474 delny = vtki.new("Delaunay2D") 3475 delny.SetInputData(pd) 3476 if tol: 3477 delny.SetTolerance(tol) 3478 delny.SetAlpha(alpha) 3479 delny.SetOffset(offset) 3480 3481 if transform: 3482 delny.SetTransform(transform.T) 3483 elif mode == "fit": 3484 delny.SetProjectionPlaneMode(vtki.get_class("VTK_BEST_FITTING_PLANE")) 3485 elif mode == "xy" and boundaries: 3486 boundary = vtki.vtkPolyData() 3487 boundary.SetPoints(vpts) 3488 cell_array = vtki.vtkCellArray() 3489 for b in boundaries: 3490 cpolygon = vtki.vtkPolygon() 3491 for idd in b: 3492 cpolygon.GetPointIds().InsertNextId(idd) 3493 cell_array.InsertNextCell(cpolygon) 3494 boundary.SetPolys(cell_array) 3495 delny.SetSourceData(boundary) 3496 3497 delny.Update() 3498 3499 msh = vedo.mesh.Mesh(delny.GetOutput()) 3500 msh.name = "Delaunay2D" 3501 msh.clean().lighting("off") 3502 msh.pipeline = utils.OperationNode( 3503 "delaunay2d", 3504 parents=[self], 3505 comment=f"#cells {msh.dataset.GetNumberOfCells()}", 3506 ) 3507 return msh 3508 3509 def generate_voronoi(self, padding=0.0, fit=False, method="vtk") -> "vedo.Mesh": 3510 """ 3511 Generate the 2D Voronoi convex tiling of the input points (z is ignored). 3512 The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon. 3513 3514 A cell array named "VoronoiID" is added to the output Mesh. 3515 3516 The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest 3517 to one of the input points. Voronoi tessellations are important in computational geometry 3518 (and many other fields), and are the dual of Delaunay triangulations. 3519 3520 Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored 3521 (although carried through to the output). 3522 If you desire to triangulate in a different plane, you can use fit=True. 3523 3524 A brief summary is as follows. Each (generating) input point is associated with 3525 an initial Voronoi tile, which is simply the bounding box of the point set. 3526 A locator is then used to identify nearby points: each neighbor in turn generates a 3527 clipping line positioned halfway between the generating point and the neighboring point, 3528 and orthogonal to the line connecting them. Clips are readily performed by evaluationg the 3529 vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line. 3530 If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip 3531 line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs, 3532 the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region 3533 containing the neighboring clip points. The clip region (along with the points contained in it) is grown 3534 by careful expansion (e.g., outward spiraling iterator over all candidate clip points). 3535 When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi 3536 tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi 3537 tessellation. Note that topological and geometric information is used to generate a valid triangulation 3538 (e.g., merging points and validating topology). 3539 3540 Arguments: 3541 pts : (list) 3542 list of input points. 3543 padding : (float) 3544 padding distance. The default is 0. 3545 fit : (bool) 3546 detect automatically the best fitting plane. The default is False. 3547 3548 Examples: 3549 - [voronoi1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi1.py) 3550 3551 ![](https://vedo.embl.es/images/basic/voronoi1.png) 3552 3553 - [voronoi2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi2.py) 3554 3555 ![](https://vedo.embl.es/images/advanced/voronoi2.png) 3556 """ 3557 pts = self.vertices 3558 3559 if method == "scipy": 3560 from scipy.spatial import Voronoi as scipy_voronoi 3561 3562 pts = np.asarray(pts)[:, (0, 1)] 3563 vor = scipy_voronoi(pts) 3564 regs = [] # filter out invalid indices 3565 for r in vor.regions: 3566 flag = True 3567 for x in r: 3568 if x < 0: 3569 flag = False 3570 break 3571 if flag and len(r) > 0: 3572 regs.append(r) 3573 3574 m = vedo.Mesh([vor.vertices, regs]) 3575 m.celldata["VoronoiID"] = np.array(list(range(len(regs)))).astype(int) 3576 3577 elif method == "vtk": 3578 vor = vtki.new("Voronoi2D") 3579 if isinstance(pts, Points): 3580 vor.SetInputData(pts) 3581 else: 3582 pts = np.asarray(pts) 3583 if pts.shape[1] == 2: 3584 pts = np.c_[pts, np.zeros(len(pts))] 3585 pd = vtki.vtkPolyData() 3586 vpts = vtki.vtkPoints() 3587 vpts.SetData(utils.numpy2vtk(pts, dtype=np.float32)) 3588 pd.SetPoints(vpts) 3589 vor.SetInputData(pd) 3590 vor.SetPadding(padding) 3591 vor.SetGenerateScalarsToPointIds() 3592 if fit: 3593 vor.SetProjectionPlaneModeToBestFittingPlane() 3594 else: 3595 vor.SetProjectionPlaneModeToXYPlane() 3596 vor.Update() 3597 poly = vor.GetOutput() 3598 arr = poly.GetCellData().GetArray(0) 3599 if arr: 3600 arr.SetName("VoronoiID") 3601 m = vedo.Mesh(poly, c="orange5") 3602 3603 else: 3604 vedo.logger.error(f"Unknown method {method} in voronoi()") 3605 raise RuntimeError 3606 3607 m.lw(2).lighting("off").wireframe() 3608 m.name = "Voronoi" 3609 return m 3610 3611 ########################################################################## 3612 def generate_delaunay3d(self, radius=0, tol=None) -> "vedo.TetMesh": 3613 """ 3614 Create 3D Delaunay triangulation of input points. 3615 3616 Arguments: 3617 radius : (float) 3618 specify distance (or "alpha") value to control output. 3619 For a non-zero values, only tetra contained within the circumsphere 3620 will be output. 3621 tol : (float) 3622 Specify a tolerance to control discarding of closely spaced points. 3623 This tolerance is specified as a fraction of the diagonal length of 3624 the bounding box of the points. 3625 """ 3626 deln = vtki.new("Delaunay3D") 3627 deln.SetInputData(self.dataset) 3628 deln.SetAlpha(radius) 3629 deln.AlphaTetsOn() 3630 deln.AlphaTrisOff() 3631 deln.AlphaLinesOff() 3632 deln.AlphaVertsOff() 3633 deln.BoundingTriangulationOff() 3634 if tol: 3635 deln.SetTolerance(tol) 3636 deln.Update() 3637 m = vedo.TetMesh(deln.GetOutput()) 3638 m.pipeline = utils.OperationNode( 3639 "generate_delaunay3d", c="#e9c46a:#edabab", parents=[self], 3640 ) 3641 m.name = "Delaunay3D" 3642 return m 3643 3644 #################################################### 3645 def visible_points(self, area=(), tol=None, invert=False) -> Union[Self, None]: 3646 """ 3647 Extract points based on whether they are visible or not. 3648 Visibility is determined by accessing the z-buffer of a rendering window. 3649 The position of each input point is converted into display coordinates, 3650 and then the z-value at that point is obtained. 3651 If within the user-specified tolerance, the point is considered visible. 3652 Associated data attributes are passed to the output as well. 3653 3654 This filter also allows you to specify a rectangular window in display (pixel) 3655 coordinates in which the visible points must lie. 3656 3657 Arguments: 3658 area : (list) 3659 specify a rectangular region as (xmin,xmax,ymin,ymax) 3660 tol : (float) 3661 a tolerance in normalized display coordinate system 3662 invert : (bool) 3663 select invisible points instead. 3664 3665 Example: 3666 ```python 3667 from vedo import Ellipsoid, show 3668 s = Ellipsoid().rotate_y(30) 3669 3670 # Camera options: pos, focal_point, viewup, distance 3671 camopts = dict(pos=(0,0,25), focal_point=(0,0,0)) 3672 show(s, camera=camopts, offscreen=True) 3673 3674 m = s.visible_points() 3675 # print('visible pts:', m.vertices) # numpy array 3676 show(m, new=True, axes=1).close() # optionally draw result in a new window 3677 ``` 3678 ![](https://vedo.embl.es/images/feats/visible_points.png) 3679 """ 3680 svp = vtki.new("SelectVisiblePoints") 3681 svp.SetInputData(self.dataset) 3682 3683 ren = None 3684 if vedo.plotter_instance: 3685 if vedo.plotter_instance.renderer: 3686 ren = vedo.plotter_instance.renderer 3687 svp.SetRenderer(ren) 3688 if not ren: 3689 vedo.logger.warning( 3690 "visible_points() can only be used after a rendering step" 3691 ) 3692 return None 3693 3694 if len(area) == 2: 3695 area = utils.flatten(area) 3696 if len(area) == 4: 3697 # specify a rectangular region 3698 svp.SetSelection(area[0], area[1], area[2], area[3]) 3699 if tol is not None: 3700 svp.SetTolerance(tol) 3701 if invert: 3702 svp.SelectInvisibleOn() 3703 svp.Update() 3704 3705 m = Points(svp.GetOutput()) 3706 m.name = "VisiblePoints" 3707 return m 3708 3709#################################################### 3710class CellCenters(Points): 3711 def __init__(self, pcloud): 3712 """ 3713 Generate `Points` at the center of the cells of any type of object. 3714 3715 Check out also `cell_centers()`. 3716 """ 3717 vcen = vtki.new("CellCenters") 3718 vcen.CopyArraysOn() 3719 vcen.VertexCellsOn() 3720 # vcen.ConvertGhostCellsToGhostPointsOn() 3721 try: 3722 vcen.SetInputData(pcloud.dataset) 3723 except AttributeError: 3724 vcen.SetInputData(pcloud) 3725 vcen.Update() 3726 super().__init__(vcen.GetOutput()) 3727 self.name = "CellCenters"
454class Points(PointsVisual, PointAlgorithms): 455 """Work with point clouds.""" 456 457 def __init__(self, inputobj=None, r=4, c=(0.2, 0.2, 0.2), alpha=1): 458 """ 459 Build an object made of only vertex points for a list of 2D/3D points. 460 Both shapes (N, 3) or (3, N) are accepted as input, if N>3. 461 462 Arguments: 463 inputobj : (list, tuple) 464 r : (int) 465 Point radius in units of pixels. 466 c : (str, list) 467 Color name or rgb tuple. 468 alpha : (float) 469 Transparency in range [0,1]. 470 471 Example: 472 ```python 473 from vedo import * 474 475 def fibonacci_sphere(n): 476 s = np.linspace(0, n, num=n, endpoint=False) 477 theta = s * 2.399963229728653 478 y = 1 - s * (2/(n-1)) 479 r = np.sqrt(1 - y * y) 480 x = np.cos(theta) * r 481 z = np.sin(theta) * r 482 return np._c[x,y,z] 483 484 Points(fibonacci_sphere(1000)).show(axes=1).close() 485 ``` 486 ![](https://vedo.embl.es/images/feats/fibonacci.png) 487 """ 488 # print("INIT POINTS") 489 super().__init__() 490 491 self.name = "" 492 self.filename = "" 493 self.file_size = "" 494 495 self.info = {} 496 self.time = time.time() 497 498 self.transform = LinearTransform() 499 self.point_locator = None 500 self.cell_locator = None 501 self.line_locator = None 502 503 self.actor = vtki.vtkActor() 504 self.properties = self.actor.GetProperty() 505 self.properties_backface = self.actor.GetBackfaceProperty() 506 self.mapper = vtki.new("PolyDataMapper") 507 self.dataset = vtki.vtkPolyData() 508 509 # Create weakref so actor can access this object (eg to pick/remove): 510 self.actor.retrieve_object = weak_ref_to(self) 511 512 try: 513 self.properties.RenderPointsAsSpheresOn() 514 except AttributeError: 515 pass 516 517 if inputobj is None: #################### 518 return 519 ########################################## 520 521 self.name = "Points" 522 523 ###### 524 if isinstance(inputobj, vtki.vtkActor): 525 self.dataset.DeepCopy(inputobj.GetMapper().GetInput()) 526 pr = vtki.vtkProperty() 527 pr.DeepCopy(inputobj.GetProperty()) 528 self.actor.SetProperty(pr) 529 self.properties = pr 530 self.mapper.SetScalarVisibility(inputobj.GetMapper().GetScalarVisibility()) 531 532 elif isinstance(inputobj, vtki.vtkPolyData): 533 self.dataset = inputobj 534 if self.dataset.GetNumberOfCells() == 0: 535 carr = vtki.vtkCellArray() 536 for i in range(self.dataset.GetNumberOfPoints()): 537 carr.InsertNextCell(1) 538 carr.InsertCellPoint(i) 539 self.dataset.SetVerts(carr) 540 541 elif isinstance(inputobj, Points): 542 self.dataset = inputobj.dataset 543 self.copy_properties_from(inputobj) 544 545 elif utils.is_sequence(inputobj): # passing point coords 546 self.dataset = utils.buildPolyData(utils.make3d(inputobj)) 547 548 elif isinstance(inputobj, str): 549 verts = vedo.file_io.load(inputobj) 550 self.filename = inputobj 551 self.dataset = verts.dataset 552 553 elif "meshlib" in str(type(inputobj)): 554 from meshlib import mrmeshnumpy as mn 555 self.dataset = utils.buildPolyData(mn.toNumpyArray(inputobj.points)) 556 557 else: 558 # try to extract the points from a generic VTK input data object 559 if hasattr(inputobj, "dataset"): 560 inputobj = inputobj.dataset 561 try: 562 vvpts = inputobj.GetPoints() 563 self.dataset = vtki.vtkPolyData() 564 self.dataset.SetPoints(vvpts) 565 for i in range(inputobj.GetPointData().GetNumberOfArrays()): 566 arr = inputobj.GetPointData().GetArray(i) 567 self.dataset.GetPointData().AddArray(arr) 568 carr = vtki.vtkCellArray() 569 for i in range(self.dataset.GetNumberOfPoints()): 570 carr.InsertNextCell(1) 571 carr.InsertCellPoint(i) 572 self.dataset.SetVerts(carr) 573 except: 574 vedo.logger.error(f"cannot build Points from type {type(inputobj)}") 575 raise RuntimeError() 576 577 self.actor.SetMapper(self.mapper) 578 self.mapper.SetInputData(self.dataset) 579 580 self.properties.SetColor(colors.get_color(c)) 581 self.properties.SetOpacity(alpha) 582 self.properties.SetRepresentationToPoints() 583 self.properties.SetPointSize(r) 584 self.properties.LightingOff() 585 586 self.pipeline = utils.OperationNode( 587 self, parents=[], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 588 ) 589 590 def _update(self, polydata, reset_locators=True) -> Self: 591 """Overwrite the polygonal dataset with a new vtkPolyData.""" 592 self.dataset = polydata 593 self.mapper.SetInputData(self.dataset) 594 self.mapper.Modified() 595 if reset_locators: 596 self.point_locator = None 597 self.line_locator = None 598 self.cell_locator = None 599 return self 600 601 def __str__(self): 602 """Print a description of the Points/Mesh.""" 603 module = self.__class__.__module__ 604 name = self.__class__.__name__ 605 out = vedo.printc( 606 f"{module}.{name} at ({hex(self.memory_address())})".ljust(75), 607 c="g", bold=True, invert=True, return_string=True, 608 ) 609 out += "\x1b[0m\x1b[32;1m" 610 611 if self.name: 612 out += "name".ljust(14) + ": " + self.name 613 if "legend" in self.info.keys() and self.info["legend"]: 614 out+= f", legend='{self.info['legend']}'" 615 out += "\n" 616 617 if self.filename: 618 out+= "file name".ljust(14) + ": " + self.filename + "\n" 619 620 if not self.mapper.GetScalarVisibility(): 621 col = utils.precision(self.properties.GetColor(), 3) 622 cname = vedo.colors.get_color_name(self.properties.GetColor()) 623 out+= "color".ljust(14) + ": " + cname 624 out+= f", rgb={col}, alpha={self.properties.GetOpacity()}\n" 625 if self.actor.GetBackfaceProperty(): 626 bcol = self.actor.GetBackfaceProperty().GetDiffuseColor() 627 cname = vedo.colors.get_color_name(bcol) 628 out+= "backface color".ljust(14) + ": " 629 out+= f"{cname}, rgb={utils.precision(bcol,3)}\n" 630 631 npt = self.dataset.GetNumberOfPoints() 632 npo, nln = self.dataset.GetNumberOfPolys(), self.dataset.GetNumberOfLines() 633 out+= "elements".ljust(14) + f": vertices={npt:,} polygons={npo:,} lines={nln:,}" 634 if self.dataset.GetNumberOfStrips(): 635 out+= f", strips={self.dataset.GetNumberOfStrips():,}" 636 out+= "\n" 637 if self.dataset.GetNumberOfPieces() > 1: 638 out+= "pieces".ljust(14) + ": " + str(self.dataset.GetNumberOfPieces()) + "\n" 639 640 out+= "position".ljust(14) + ": " + f"{utils.precision(self.pos(), 6)}\n" 641 try: 642 sc = self.transform.get_scale() 643 out+= "scaling".ljust(14) + ": " 644 out+= utils.precision(sc, 6) + "\n" 645 except AttributeError: 646 pass 647 648 if self.npoints: 649 out+="size".ljust(14)+ ": average=" + utils.precision(self.average_size(),6) 650 out+=", diagonal="+ utils.precision(self.diagonal_size(), 6)+ "\n" 651 out+="center of mass".ljust(14) + ": " + utils.precision(self.center_of_mass(),6)+"\n" 652 653 bnds = self.bounds() 654 bx1, bx2 = utils.precision(bnds[0], 3), utils.precision(bnds[1], 3) 655 by1, by2 = utils.precision(bnds[2], 3), utils.precision(bnds[3], 3) 656 bz1, bz2 = utils.precision(bnds[4], 3), utils.precision(bnds[5], 3) 657 out+= "bounds".ljust(14) + ":" 658 out+= " x=(" + bx1 + ", " + bx2 + ")," 659 out+= " y=(" + by1 + ", " + by2 + ")," 660 out+= " z=(" + bz1 + ", " + bz2 + ")\n" 661 662 for key in self.pointdata.keys(): 663 arr = self.pointdata[key] 664 dim = arr.shape[1] if arr.ndim > 1 else 1 665 mark_active = "pointdata" 666 a_scalars = self.dataset.GetPointData().GetScalars() 667 a_vectors = self.dataset.GetPointData().GetVectors() 668 a_tensors = self.dataset.GetPointData().GetTensors() 669 if a_scalars and a_scalars.GetName() == key: 670 mark_active += " *" 671 elif a_vectors and a_vectors.GetName() == key: 672 mark_active += " **" 673 elif a_tensors and a_tensors.GetName() == key: 674 mark_active += " ***" 675 out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), dim={dim}' 676 if dim == 1 and len(arr): 677 rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3) 678 out += f", range=({rng})\n" 679 else: 680 out += "\n" 681 682 for key in self.celldata.keys(): 683 arr = self.celldata[key] 684 dim = arr.shape[1] if arr.ndim > 1 else 1 685 mark_active = "celldata" 686 a_scalars = self.dataset.GetCellData().GetScalars() 687 a_vectors = self.dataset.GetCellData().GetVectors() 688 a_tensors = self.dataset.GetCellData().GetTensors() 689 if a_scalars and a_scalars.GetName() == key: 690 mark_active += " *" 691 elif a_vectors and a_vectors.GetName() == key: 692 mark_active += " **" 693 elif a_tensors and a_tensors.GetName() == key: 694 mark_active += " ***" 695 out += mark_active.ljust(14) + f': "{key}" ({arr.dtype}), dim={dim}' 696 if dim == 1 and len(arr): 697 rng = utils.precision(arr.min(), 3) + ", " + utils.precision(arr.max(), 3) 698 out += f", range=({rng})\n" 699 else: 700 out += "\n" 701 702 for key in self.metadata.keys(): 703 arr = self.metadata[key] 704 if len(arr) > 3: 705 out+= "metadata".ljust(14) + ": " + f'"{key}" ({len(arr)} values)\n' 706 else: 707 out+= "metadata".ljust(14) + ": " + f'"{key}" = {arr}\n' 708 709 if self.picked3d is not None: 710 idp = self.closest_point(self.picked3d, return_point_id=True) 711 idc = self.closest_point(self.picked3d, return_cell_id=True) 712 out+= "clicked point".ljust(14) + ": " + utils.precision(self.picked3d, 6) 713 out+= f", pointID={idp}, cellID={idc}\n" 714 715 return out.rstrip() + "\x1b[0m" 716 717 def _repr_html_(self): 718 """ 719 HTML representation of the Point cloud object for Jupyter Notebooks. 720 721 Returns: 722 HTML text with the image and some properties. 723 """ 724 import io 725 import base64 726 from PIL import Image 727 728 library_name = "vedo.pointcloud.Points" 729 help_url = "https://vedo.embl.es/docs/vedo/pointcloud.html#Points" 730 731 arr = self.thumbnail() 732 im = Image.fromarray(arr) 733 buffered = io.BytesIO() 734 im.save(buffered, format="PNG", quality=100) 735 encoded = base64.b64encode(buffered.getvalue()).decode("utf-8") 736 url = "data:image/png;base64," + encoded 737 image = f"<img src='{url}'></img>" 738 739 bounds = "<br/>".join( 740 [ 741 utils.precision(min_x, 4) + " ... " + utils.precision(max_x, 4) 742 for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2]) 743 ] 744 ) 745 average_size = "{size:.3f}".format(size=self.average_size()) 746 747 help_text = "" 748 if self.name: 749 help_text += f"<b> {self.name}:   </b>" 750 help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>" 751 if self.filename: 752 dots = "" 753 if len(self.filename) > 30: 754 dots = "..." 755 help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>" 756 757 pdata = "" 758 if self.dataset.GetPointData().GetScalars(): 759 if self.dataset.GetPointData().GetScalars().GetName(): 760 name = self.dataset.GetPointData().GetScalars().GetName() 761 pdata = "<tr><td><b> point data array </b></td><td>" + name + "</td></tr>" 762 763 cdata = "" 764 if self.dataset.GetCellData().GetScalars(): 765 if self.dataset.GetCellData().GetScalars().GetName(): 766 name = self.dataset.GetCellData().GetScalars().GetName() 767 cdata = "<tr><td><b> cell data array </b></td><td>" + name + "</td></tr>" 768 769 allt = [ 770 "<table>", 771 "<tr>", 772 "<td>", 773 image, 774 "</td>", 775 "<td style='text-align: center; vertical-align: center;'><br/>", 776 help_text, 777 "<table>", 778 "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>", 779 "<tr><td><b> center of mass </b></td><td>" 780 + utils.precision(self.center_of_mass(), 3) 781 + "</td></tr>", 782 "<tr><td><b> average size </b></td><td>" + str(average_size) + "</td></tr>", 783 "<tr><td><b> nr. points </b></td><td>" + str(self.npoints) + "</td></tr>", 784 pdata, 785 cdata, 786 "</table>", 787 "</table>", 788 ] 789 return "\n".join(allt) 790 791 ################################################################################## 792 def __add__(self, meshs): 793 """ 794 Add two meshes or a list of meshes together to form an `Assembly` object. 795 """ 796 if isinstance(meshs, list): 797 alist = [self] 798 for l in meshs: 799 if isinstance(l, vedo.Assembly): 800 alist += l.unpack() 801 else: 802 alist += l 803 return vedo.assembly.Assembly(alist) 804 805 if isinstance(meshs, vedo.Assembly): 806 return meshs + self # use Assembly.__add__ 807 808 return vedo.assembly.Assembly([self, meshs]) 809 810 def polydata(self, **kwargs): 811 """ 812 Obsolete. Use property `.dataset` instead. 813 Returns the underlying `vtkPolyData` object. 814 """ 815 colors.printc( 816 "WARNING: call to .polydata() is obsolete, use property .dataset instead.", 817 c="y") 818 return self.dataset 819 820 def __copy__(self): 821 return self.clone(deep=False) 822 823 def __deepcopy__(self, memo): 824 return self.clone(deep=memo) 825 826 def copy(self, deep=True) -> Self: 827 """Return a copy of the object. Alias of `clone()`.""" 828 return self.clone(deep=deep) 829 830 def clone(self, deep=True) -> Self: 831 """ 832 Clone a `PointCloud` or `Mesh` object to make an exact copy of it. 833 Alias of `copy()`. 834 835 Arguments: 836 deep : (bool) 837 if False return a shallow copy of the mesh without copying the points array. 838 839 Examples: 840 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 841 842 ![](https://vedo.embl.es/images/basic/mirror.png) 843 """ 844 poly = vtki.vtkPolyData() 845 if deep or isinstance(deep, dict): # if a memo object is passed this checks as True 846 poly.DeepCopy(self.dataset) 847 else: 848 poly.ShallowCopy(self.dataset) 849 850 if isinstance(self, vedo.Mesh): 851 cloned = vedo.Mesh(poly) 852 else: 853 cloned = Points(poly) 854 # print([self], self.__class__) 855 # cloned = self.__class__(poly) 856 857 cloned.transform = self.transform.clone() 858 859 cloned.copy_properties_from(self) 860 861 cloned.name = str(self.name) 862 cloned.filename = str(self.filename) 863 cloned.info = dict(self.info) 864 cloned.pipeline = utils.OperationNode("clone", parents=[self], shape="diamond", c="#edede9") 865 866 if isinstance(deep, dict): 867 deep[id(self)] = cloned 868 869 return cloned 870 871 def compute_normals_with_pca(self, n=20, orientation_point=None, invert=False) -> Self: 872 """ 873 Generate point normals using PCA (principal component analysis). 874 This algorithm estimates a local tangent plane around each sample point p 875 by considering a small neighborhood of points around p, and fitting a plane 876 to the neighborhood (via PCA). 877 878 Arguments: 879 n : (int) 880 neighborhood size to calculate the normal 881 orientation_point : (list) 882 adjust the +/- sign of the normals so that 883 the normals all point towards a specified point. If None, perform a traversal 884 of the point cloud and flip neighboring normals so that they are mutually consistent. 885 invert : (bool) 886 flip all normals 887 """ 888 poly = self.dataset 889 pcan = vtki.new("PCANormalEstimation") 890 pcan.SetInputData(poly) 891 pcan.SetSampleSize(n) 892 893 if orientation_point is not None: 894 pcan.SetNormalOrientationToPoint() 895 pcan.SetOrientationPoint(orientation_point) 896 else: 897 pcan.SetNormalOrientationToGraphTraversal() 898 899 if invert: 900 pcan.FlipNormalsOn() 901 pcan.Update() 902 903 varr = pcan.GetOutput().GetPointData().GetNormals() 904 varr.SetName("Normals") 905 self.dataset.GetPointData().SetNormals(varr) 906 self.dataset.GetPointData().Modified() 907 return self 908 909 def compute_acoplanarity(self, n=25, radius=None, on="points") -> Self: 910 """ 911 Compute acoplanarity which is a measure of how much a local region of the mesh 912 differs from a plane. 913 914 The information is stored in a `pointdata` or `celldata` array with name 'Acoplanarity'. 915 916 Either `n` (number of neighbour points) or `radius` (radius of local search) can be specified. 917 If a radius value is given and not enough points fall inside it, then a -1 is stored. 918 919 Example: 920 ```python 921 from vedo import * 922 msh = ParametricShape('RandomHills') 923 msh.compute_acoplanarity(radius=0.1, on='cells') 924 msh.cmap("coolwarm", on='cells').add_scalarbar() 925 msh.show(axes=1).close() 926 ``` 927 ![](https://vedo.embl.es/images/feats/acoplanarity.jpg) 928 """ 929 acoplanarities = [] 930 if "point" in on: 931 pts = self.vertices 932 elif "cell" in on: 933 pts = self.cell_centers 934 else: 935 raise ValueError(f"In compute_acoplanarity() set on to either 'cells' or 'points', not {on}") 936 937 for p in utils.progressbar(pts, delay=5, width=15, title=f"{on} acoplanarity"): 938 if n: 939 data = self.closest_point(p, n=n) 940 npts = n 941 elif radius: 942 data = self.closest_point(p, radius=radius) 943 npts = len(data) 944 945 try: 946 center = data.mean(axis=0) 947 res = np.linalg.svd(data - center) 948 acoplanarities.append(res[1][2] / npts) 949 except: 950 acoplanarities.append(-1.0) 951 952 if "point" in on: 953 self.pointdata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 954 else: 955 self.celldata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 956 return self 957 958 def distance_to(self, pcloud, signed=False, invert=False, name="Distance") -> np.ndarray: 959 """ 960 Computes the distance from one point cloud or mesh to another point cloud or mesh. 961 This new `pointdata` array is saved with default name "Distance". 962 963 Keywords `signed` and `invert` are used to compute signed distance, 964 but the mesh in that case must have polygonal faces (not a simple point cloud), 965 and normals must also be computed. 966 967 Examples: 968 - [distance2mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/distance2mesh.py) 969 970 ![](https://vedo.embl.es/images/basic/distance2mesh.png) 971 """ 972 if pcloud.dataset.GetNumberOfPolys(): 973 974 poly1 = self.dataset 975 poly2 = pcloud.dataset 976 df = vtki.new("DistancePolyDataFilter") 977 df.ComputeSecondDistanceOff() 978 df.SetInputData(0, poly1) 979 df.SetInputData(1, poly2) 980 df.SetSignedDistance(signed) 981 df.SetNegateDistance(invert) 982 df.Update() 983 scals = df.GetOutput().GetPointData().GetScalars() 984 dists = utils.vtk2numpy(scals) 985 986 else: # has no polygons 987 988 if signed: 989 vedo.logger.warning("distance_to() called with signed=True but input object has no polygons") 990 991 if not pcloud.point_locator: 992 pcloud.point_locator = vtki.new("PointLocator") 993 pcloud.point_locator.SetDataSet(pcloud.dataset) 994 pcloud.point_locator.BuildLocator() 995 996 ids = [] 997 ps1 = self.vertices 998 ps2 = pcloud.vertices 999 for p in ps1: 1000 pid = pcloud.point_locator.FindClosestPoint(p) 1001 ids.append(pid) 1002 1003 deltas = ps2[ids] - ps1 1004 dists = np.linalg.norm(deltas, axis=1).astype(np.float32) 1005 scals = utils.numpy2vtk(dists) 1006 1007 scals.SetName(name) 1008 self.dataset.GetPointData().AddArray(scals) 1009 self.dataset.GetPointData().SetActiveScalars(scals.GetName()) 1010 rng = scals.GetRange() 1011 self.mapper.SetScalarRange(rng[0], rng[1]) 1012 self.mapper.ScalarVisibilityOn() 1013 1014 self.pipeline = utils.OperationNode( 1015 "distance_to", 1016 parents=[self, pcloud], 1017 shape="cylinder", 1018 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1019 ) 1020 return dists 1021 1022 def clean(self) -> Self: 1023 """Clean pointcloud or mesh by removing coincident points.""" 1024 cpd = vtki.new("CleanPolyData") 1025 cpd.PointMergingOn() 1026 cpd.ConvertLinesToPointsOff() 1027 cpd.ConvertPolysToLinesOff() 1028 cpd.ConvertStripsToPolysOff() 1029 cpd.SetInputData(self.dataset) 1030 cpd.Update() 1031 self._update(cpd.GetOutput()) 1032 self.pipeline = utils.OperationNode( 1033 "clean", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1034 ) 1035 return self 1036 1037 def subsample(self, fraction: float, absolute=False) -> Self: 1038 """ 1039 Subsample a point cloud by requiring that the points 1040 or vertices are far apart at least by the specified fraction of the object size. 1041 If a Mesh is passed the polygonal faces are not removed 1042 but holes can appear as their vertices are removed. 1043 1044 Examples: 1045 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1046 1047 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1048 1049 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1050 1051 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1052 """ 1053 if not absolute: 1054 if fraction > 1: 1055 vedo.logger.warning( 1056 f"subsample(fraction=...), fraction must be < 1, but is {fraction}" 1057 ) 1058 if fraction <= 0: 1059 return self 1060 1061 cpd = vtki.new("CleanPolyData") 1062 cpd.PointMergingOn() 1063 cpd.ConvertLinesToPointsOn() 1064 cpd.ConvertPolysToLinesOn() 1065 cpd.ConvertStripsToPolysOn() 1066 cpd.SetInputData(self.dataset) 1067 if absolute: 1068 cpd.SetTolerance(fraction / self.diagonal_size()) 1069 # cpd.SetToleranceIsAbsolute(absolute) 1070 else: 1071 cpd.SetTolerance(fraction) 1072 cpd.Update() 1073 1074 ps = 2 1075 if self.properties.GetRepresentation() == 0: 1076 ps = self.properties.GetPointSize() 1077 1078 self._update(cpd.GetOutput()) 1079 self.ps(ps) 1080 1081 self.pipeline = utils.OperationNode( 1082 "subsample", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1083 ) 1084 return self 1085 1086 def threshold(self, scalars: str, above=None, below=None, on="points") -> Self: 1087 """ 1088 Extracts cells where scalar value satisfies threshold criterion. 1089 1090 Arguments: 1091 scalars : (str) 1092 name of the scalars array. 1093 above : (float) 1094 minimum value of the scalar 1095 below : (float) 1096 maximum value of the scalar 1097 on : (str) 1098 if 'cells' assume array of scalars refers to cell data. 1099 1100 Examples: 1101 - [mesh_threshold.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_threshold.py) 1102 """ 1103 thres = vtki.new("Threshold") 1104 thres.SetInputData(self.dataset) 1105 1106 if on.startswith("c"): 1107 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_CELLS 1108 else: 1109 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_POINTS 1110 1111 thres.SetInputArrayToProcess(0, 0, 0, asso, scalars) 1112 1113 if above is None and below is not None: 1114 try: # vtk 9.2 1115 thres.ThresholdByLower(below) 1116 except AttributeError: # vtk 9.3 1117 thres.SetUpperThreshold(below) 1118 1119 elif below is None and above is not None: 1120 try: 1121 thres.ThresholdByUpper(above) 1122 except AttributeError: 1123 thres.SetLowerThreshold(above) 1124 else: 1125 try: 1126 thres.ThresholdBetween(above, below) 1127 except AttributeError: 1128 thres.SetUpperThreshold(below) 1129 thres.SetLowerThreshold(above) 1130 1131 thres.Update() 1132 1133 gf = vtki.new("GeometryFilter") 1134 gf.SetInputData(thres.GetOutput()) 1135 gf.Update() 1136 self._update(gf.GetOutput()) 1137 self.pipeline = utils.OperationNode("threshold", parents=[self]) 1138 return self 1139 1140 def quantize(self, value: float) -> Self: 1141 """ 1142 The user should input a value and all {x,y,z} coordinates 1143 will be quantized to that absolute grain size. 1144 """ 1145 qp = vtki.new("QuantizePolyDataPoints") 1146 qp.SetInputData(self.dataset) 1147 qp.SetQFactor(value) 1148 qp.Update() 1149 self._update(qp.GetOutput()) 1150 self.pipeline = utils.OperationNode("quantize", parents=[self]) 1151 return self 1152 1153 @property 1154 def vertex_normals(self) -> np.ndarray: 1155 """ 1156 Retrieve vertex normals as a numpy array. Same as `point_normals`. 1157 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1158 """ 1159 vtknormals = self.dataset.GetPointData().GetNormals() 1160 return utils.vtk2numpy(vtknormals) 1161 1162 @property 1163 def point_normals(self) -> np.ndarray: 1164 """ 1165 Retrieve vertex normals as a numpy array. Same as `vertex_normals`. 1166 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1167 """ 1168 vtknormals = self.dataset.GetPointData().GetNormals() 1169 return utils.vtk2numpy(vtknormals) 1170 1171 def align_to(self, target, iters=100, rigid=False, invert=False, use_centroids=False) -> Self: 1172 """ 1173 Aligned to target mesh through the `Iterative Closest Point` algorithm. 1174 1175 The core of the algorithm is to match each vertex in one surface with 1176 the closest surface point on the other, then apply the transformation 1177 that modify one surface to best match the other (in the least-square sense). 1178 1179 Arguments: 1180 rigid : (bool) 1181 if True do not allow scaling 1182 invert : (bool) 1183 if True start by aligning the target to the source but 1184 invert the transformation finally. Useful when the target is smaller 1185 than the source. 1186 use_centroids : (bool) 1187 start by matching the centroids of the two objects. 1188 1189 Examples: 1190 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1191 1192 ![](https://vedo.embl.es/images/basic/align1.png) 1193 1194 - [align2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align2.py) 1195 1196 ![](https://vedo.embl.es/images/basic/align2.png) 1197 """ 1198 icp = vtki.new("IterativeClosestPointTransform") 1199 icp.SetSource(self.dataset) 1200 icp.SetTarget(target.dataset) 1201 if invert: 1202 icp.Inverse() 1203 icp.SetMaximumNumberOfIterations(iters) 1204 if rigid: 1205 icp.GetLandmarkTransform().SetModeToRigidBody() 1206 icp.SetStartByMatchingCentroids(use_centroids) 1207 icp.Update() 1208 1209 self.apply_transform(icp.GetMatrix()) 1210 1211 self.pipeline = utils.OperationNode( 1212 "align_to", parents=[self, target], comment=f"rigid = {rigid}" 1213 ) 1214 return self 1215 1216 def align_to_bounding_box(self, msh, rigid=False) -> Self: 1217 """ 1218 Align the current object's bounding box to the bounding box 1219 of the input object. 1220 1221 Use `rigid=True` to disable scaling. 1222 1223 Example: 1224 [align6.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align6.py) 1225 """ 1226 lmt = vtki.vtkLandmarkTransform() 1227 ss = vtki.vtkPoints() 1228 xss0, xss1, yss0, yss1, zss0, zss1 = self.bounds() 1229 for p in [ 1230 [xss0, yss0, zss0], 1231 [xss1, yss0, zss0], 1232 [xss1, yss1, zss0], 1233 [xss0, yss1, zss0], 1234 [xss0, yss0, zss1], 1235 [xss1, yss0, zss1], 1236 [xss1, yss1, zss1], 1237 [xss0, yss1, zss1], 1238 ]: 1239 ss.InsertNextPoint(p) 1240 st = vtki.vtkPoints() 1241 xst0, xst1, yst0, yst1, zst0, zst1 = msh.bounds() 1242 for p in [ 1243 [xst0, yst0, zst0], 1244 [xst1, yst0, zst0], 1245 [xst1, yst1, zst0], 1246 [xst0, yst1, zst0], 1247 [xst0, yst0, zst1], 1248 [xst1, yst0, zst1], 1249 [xst1, yst1, zst1], 1250 [xst0, yst1, zst1], 1251 ]: 1252 st.InsertNextPoint(p) 1253 1254 lmt.SetSourceLandmarks(ss) 1255 lmt.SetTargetLandmarks(st) 1256 lmt.SetModeToAffine() 1257 if rigid: 1258 lmt.SetModeToRigidBody() 1259 lmt.Update() 1260 1261 LT = LinearTransform(lmt) 1262 self.apply_transform(LT) 1263 return self 1264 1265 def align_with_landmarks( 1266 self, 1267 source_landmarks, 1268 target_landmarks, 1269 rigid=False, 1270 affine=False, 1271 least_squares=False, 1272 ) -> Self: 1273 """ 1274 Transform mesh orientation and position based on a set of landmarks points. 1275 The algorithm finds the best matching of source points to target points 1276 in the mean least square sense, in one single step. 1277 1278 If `affine` is True the x, y and z axes can scale independently but stay collinear. 1279 With least_squares they can vary orientation. 1280 1281 Examples: 1282 - [align5.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align5.py) 1283 1284 ![](https://vedo.embl.es/images/basic/align5.png) 1285 """ 1286 1287 if utils.is_sequence(source_landmarks): 1288 ss = vtki.vtkPoints() 1289 for p in source_landmarks: 1290 ss.InsertNextPoint(p) 1291 else: 1292 ss = source_landmarks.dataset.GetPoints() 1293 if least_squares: 1294 source_landmarks = source_landmarks.vertices 1295 1296 if utils.is_sequence(target_landmarks): 1297 st = vtki.vtkPoints() 1298 for p in target_landmarks: 1299 st.InsertNextPoint(p) 1300 else: 1301 st = target_landmarks.GetPoints() 1302 if least_squares: 1303 target_landmarks = target_landmarks.vertices 1304 1305 if ss.GetNumberOfPoints() != st.GetNumberOfPoints(): 1306 n1 = ss.GetNumberOfPoints() 1307 n2 = st.GetNumberOfPoints() 1308 vedo.logger.error(f"source and target have different nr of points {n1} vs {n2}") 1309 raise RuntimeError() 1310 1311 if int(rigid) + int(affine) + int(least_squares) > 1: 1312 vedo.logger.error( 1313 "only one of rigid, affine, least_squares can be True at a time" 1314 ) 1315 raise RuntimeError() 1316 1317 lmt = vtki.vtkLandmarkTransform() 1318 lmt.SetSourceLandmarks(ss) 1319 lmt.SetTargetLandmarks(st) 1320 lmt.SetModeToSimilarity() 1321 1322 if rigid: 1323 lmt.SetModeToRigidBody() 1324 lmt.Update() 1325 1326 elif affine: 1327 lmt.SetModeToAffine() 1328 lmt.Update() 1329 1330 elif least_squares: 1331 cms = source_landmarks.mean(axis=0) 1332 cmt = target_landmarks.mean(axis=0) 1333 m = np.linalg.lstsq(source_landmarks - cms, target_landmarks - cmt, rcond=None)[0] 1334 M = vtki.vtkMatrix4x4() 1335 for i in range(3): 1336 for j in range(3): 1337 M.SetElement(j, i, m[i][j]) 1338 lmt = vtki.vtkTransform() 1339 lmt.Translate(cmt) 1340 lmt.Concatenate(M) 1341 lmt.Translate(-cms) 1342 1343 else: 1344 lmt.Update() 1345 1346 self.apply_transform(lmt) 1347 self.pipeline = utils.OperationNode("transform_with_landmarks", parents=[self]) 1348 return self 1349 1350 def normalize(self) -> Self: 1351 """Scale average size to unit. The scaling is performed around the center of mass.""" 1352 coords = self.vertices 1353 if not coords.shape[0]: 1354 return self 1355 cm = np.mean(coords, axis=0) 1356 pts = coords - cm 1357 xyz2 = np.sum(pts * pts, axis=0) 1358 scale = 1 / np.sqrt(np.sum(xyz2) / len(pts)) 1359 self.scale(scale, origin=cm) 1360 self.pipeline = utils.OperationNode("normalize", parents=[self]) 1361 return self 1362 1363 def mirror(self, axis="x", origin=True) -> Self: 1364 """ 1365 Mirror reflect along one of the cartesian axes 1366 1367 Arguments: 1368 axis : (str) 1369 axis to use for mirroring, must be set to `x, y, z`. 1370 Or any combination of those. 1371 origin : (list) 1372 use this point as the origin of the mirroring transformation. 1373 1374 Examples: 1375 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 1376 1377 ![](https://vedo.embl.es/images/basic/mirror.png) 1378 """ 1379 sx, sy, sz = 1, 1, 1 1380 if "x" in axis.lower(): sx = -1 1381 if "y" in axis.lower(): sy = -1 1382 if "z" in axis.lower(): sz = -1 1383 1384 self.scale([sx, sy, sz], origin=origin) 1385 1386 self.pipeline = utils.OperationNode( 1387 "mirror", comment=f"axis = {axis}", parents=[self]) 1388 1389 if sx * sy * sz < 0: 1390 if hasattr(self, "reverse"): 1391 self.reverse() 1392 return self 1393 1394 def flip_normals(self) -> Self: 1395 """Flip all normals orientation.""" 1396 rs = vtki.new("ReverseSense") 1397 rs.SetInputData(self.dataset) 1398 rs.ReverseCellsOff() 1399 rs.ReverseNormalsOn() 1400 rs.Update() 1401 self._update(rs.GetOutput()) 1402 self.pipeline = utils.OperationNode("flip_normals", parents=[self]) 1403 return self 1404 1405 def add_gaussian_noise(self, sigma=1.0) -> Self: 1406 """ 1407 Add gaussian noise to point positions. 1408 An extra array is added named "GaussianNoise" with the displacements. 1409 1410 Arguments: 1411 sigma : (float) 1412 nr. of standard deviations, expressed in percent of the diagonal size of mesh. 1413 Can also be a list `[sigma_x, sigma_y, sigma_z]`. 1414 1415 Example: 1416 ```python 1417 from vedo import Sphere 1418 Sphere().add_gaussian_noise(1.0).point_size(8).show().close() 1419 ``` 1420 """ 1421 sz = self.diagonal_size() 1422 pts = self.vertices 1423 n = len(pts) 1424 ns = (np.random.randn(n, 3) * sigma) * (sz / 100) 1425 vpts = vtki.vtkPoints() 1426 vpts.SetNumberOfPoints(n) 1427 vpts.SetData(utils.numpy2vtk(pts + ns, dtype=np.float32)) 1428 self.dataset.SetPoints(vpts) 1429 self.dataset.GetPoints().Modified() 1430 self.pointdata["GaussianNoise"] = -ns 1431 self.pipeline = utils.OperationNode( 1432 "gaussian_noise", parents=[self], shape="egg", comment=f"sigma = {sigma}" 1433 ) 1434 return self 1435 1436 def closest_point( 1437 self, pt, n=1, radius=None, return_point_id=False, return_cell_id=False 1438 ) -> Union[List[int], int, np.ndarray]: 1439 """ 1440 Find the closest point(s) on a mesh given from the input point `pt`. 1441 1442 Arguments: 1443 n : (int) 1444 if greater than 1, return a list of n ordered closest points 1445 radius : (float) 1446 if given, get all points within that radius. Then n is ignored. 1447 return_point_id : (bool) 1448 return point ID instead of coordinates 1449 return_cell_id : (bool) 1450 return cell ID in which the closest point sits 1451 1452 Examples: 1453 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1454 - [fitplanes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitplanes.py) 1455 - [quadratic_morphing.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/quadratic_morphing.py) 1456 1457 .. note:: 1458 The appropriate tree search locator is built on the fly and cached for speed. 1459 1460 If you want to reset it use `mymesh.point_locator=None` 1461 and / or `mymesh.cell_locator=None`. 1462 """ 1463 if len(pt) != 3: 1464 pt = [pt[0], pt[1], 0] 1465 1466 # NB: every time the mesh moves or is warped the locators are set to None 1467 if ((n > 1 or radius) or (n == 1 and return_point_id)) and not return_cell_id: 1468 poly = None 1469 if not self.point_locator: 1470 poly = self.dataset 1471 self.point_locator = vtki.new("StaticPointLocator") 1472 self.point_locator.SetDataSet(poly) 1473 self.point_locator.BuildLocator() 1474 1475 ########## 1476 if radius: 1477 vtklist = vtki.vtkIdList() 1478 self.point_locator.FindPointsWithinRadius(radius, pt, vtklist) 1479 elif n > 1: 1480 vtklist = vtki.vtkIdList() 1481 self.point_locator.FindClosestNPoints(n, pt, vtklist) 1482 else: # n==1 hence return_point_id==True 1483 ######## 1484 return self.point_locator.FindClosestPoint(pt) 1485 ######## 1486 1487 if return_point_id: 1488 ######## 1489 return utils.vtk2numpy(vtklist) 1490 ######## 1491 1492 if not poly: 1493 poly = self.dataset 1494 trgp = [] 1495 for i in range(vtklist.GetNumberOfIds()): 1496 trgp_ = [0, 0, 0] 1497 vi = vtklist.GetId(i) 1498 poly.GetPoints().GetPoint(vi, trgp_) 1499 trgp.append(trgp_) 1500 ######## 1501 return np.array(trgp) 1502 ######## 1503 1504 else: 1505 1506 if not self.cell_locator: 1507 poly = self.dataset 1508 1509 # As per Miquel example with limbs the vtkStaticCellLocator doesnt work !! 1510 # https://discourse.vtk.org/t/vtkstaticcelllocator-problem-vtk9-0-3/7854/4 1511 if vedo.vtk_version[0] >= 9 and vedo.vtk_version[1] > 0: 1512 self.cell_locator = vtki.new("StaticCellLocator") 1513 else: 1514 self.cell_locator = vtki.new("CellLocator") 1515 1516 self.cell_locator.SetDataSet(poly) 1517 self.cell_locator.BuildLocator() 1518 1519 if radius is not None: 1520 vedo.printc("Warning: closest_point() with radius is not implemented for cells.", c='r') 1521 1522 if n != 1: 1523 vedo.printc("Warning: closest_point() with n>1 is not implemented for cells.", c='r') 1524 1525 trgp = [0, 0, 0] 1526 cid = vtki.mutable(0) 1527 dist2 = vtki.mutable(0) 1528 subid = vtki.mutable(0) 1529 self.cell_locator.FindClosestPoint(pt, trgp, cid, subid, dist2) 1530 1531 if return_cell_id: 1532 return int(cid) 1533 1534 return np.array(trgp) 1535 1536 def auto_distance(self) -> np.ndarray: 1537 """ 1538 Calculate the distance to the closest point in the same cloud of points. 1539 The output is stored in a new pointdata array called "AutoDistance", 1540 and it is also returned by the function. 1541 """ 1542 points = self.vertices 1543 if not self.point_locator: 1544 self.point_locator = vtki.new("StaticPointLocator") 1545 self.point_locator.SetDataSet(self.dataset) 1546 self.point_locator.BuildLocator() 1547 qs = [] 1548 vtklist = vtki.vtkIdList() 1549 vtkpoints = self.dataset.GetPoints() 1550 for p in points: 1551 self.point_locator.FindClosestNPoints(2, p, vtklist) 1552 q = [0, 0, 0] 1553 pid = vtklist.GetId(1) 1554 vtkpoints.GetPoint(pid, q) 1555 qs.append(q) 1556 dists = np.linalg.norm(points - np.array(qs), axis=1) 1557 self.pointdata["AutoDistance"] = dists 1558 return dists 1559 1560 def hausdorff_distance(self, points) -> float: 1561 """ 1562 Compute the Hausdorff distance to the input point set. 1563 Returns a single `float`. 1564 1565 Example: 1566 ```python 1567 from vedo import * 1568 t = np.linspace(0, 2*np.pi, 100) 1569 x = 4/3 * sin(t)**3 1570 y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12 1571 pol1 = Line(np.c_[x,y], closed=True).triangulate() 1572 pol2 = Polygon(nsides=5).pos(2,2) 1573 d12 = pol1.distance_to(pol2) 1574 d21 = pol2.distance_to(pol1) 1575 pol1.lw(0).cmap("viridis") 1576 pol2.lw(0).cmap("viridis") 1577 print("distance d12, d21 :", min(d12), min(d21)) 1578 print("hausdorff distance:", pol1.hausdorff_distance(pol2)) 1579 print("chamfer distance :", pol1.chamfer_distance(pol2)) 1580 show(pol1, pol2, axes=1) 1581 ``` 1582 ![](https://vedo.embl.es/images/feats/heart.png) 1583 """ 1584 hp = vtki.new("HausdorffDistancePointSetFilter") 1585 hp.SetInputData(0, self.dataset) 1586 hp.SetInputData(1, points.dataset) 1587 hp.SetTargetDistanceMethodToPointToCell() 1588 hp.Update() 1589 return hp.GetHausdorffDistance() 1590 1591 def chamfer_distance(self, pcloud) -> float: 1592 """ 1593 Compute the Chamfer distance to the input point set. 1594 1595 Example: 1596 ```python 1597 from vedo import * 1598 cloud1 = np.random.randn(1000, 3) 1599 cloud2 = np.random.randn(1000, 3) + [1, 2, 3] 1600 c1 = Points(cloud1, r=5, c="red") 1601 c2 = Points(cloud2, r=5, c="green") 1602 d = c1.chamfer_distance(c2) 1603 show(f"Chamfer distance = {d}", c1, c2, axes=1).close() 1604 ``` 1605 """ 1606 # Definition of Chamfer distance may vary, here we use the average 1607 if not pcloud.point_locator: 1608 pcloud.point_locator = vtki.new("PointLocator") 1609 pcloud.point_locator.SetDataSet(pcloud.dataset) 1610 pcloud.point_locator.BuildLocator() 1611 if not self.point_locator: 1612 self.point_locator = vtki.new("PointLocator") 1613 self.point_locator.SetDataSet(self.dataset) 1614 self.point_locator.BuildLocator() 1615 1616 ps1 = self.vertices 1617 ps2 = pcloud.vertices 1618 1619 ids12 = [] 1620 for p in ps1: 1621 pid12 = pcloud.point_locator.FindClosestPoint(p) 1622 ids12.append(pid12) 1623 deltav = ps2[ids12] - ps1 1624 da = np.mean(np.linalg.norm(deltav, axis=1)) 1625 1626 ids21 = [] 1627 for p in ps2: 1628 pid21 = self.point_locator.FindClosestPoint(p) 1629 ids21.append(pid21) 1630 deltav = ps1[ids21] - ps2 1631 db = np.mean(np.linalg.norm(deltav, axis=1)) 1632 return (da + db) / 2 1633 1634 def remove_outliers(self, radius: float, neighbors=5) -> Self: 1635 """ 1636 Remove outliers from a cloud of points within the specified `radius` search. 1637 1638 Arguments: 1639 radius : (float) 1640 Specify the local search radius. 1641 neighbors : (int) 1642 Specify the number of neighbors that a point must have, 1643 within the specified radius, for the point to not be considered isolated. 1644 1645 Examples: 1646 - [clustering.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/clustering.py) 1647 1648 ![](https://vedo.embl.es/images/basic/clustering.png) 1649 """ 1650 removal = vtki.new("RadiusOutlierRemoval") 1651 removal.SetInputData(self.dataset) 1652 removal.SetRadius(radius) 1653 removal.SetNumberOfNeighbors(neighbors) 1654 removal.GenerateOutliersOff() 1655 removal.Update() 1656 inputobj = removal.GetOutput() 1657 if inputobj.GetNumberOfCells() == 0: 1658 carr = vtki.vtkCellArray() 1659 for i in range(inputobj.GetNumberOfPoints()): 1660 carr.InsertNextCell(1) 1661 carr.InsertCellPoint(i) 1662 inputobj.SetVerts(carr) 1663 self._update(removal.GetOutput()) 1664 self.pipeline = utils.OperationNode("remove_outliers", parents=[self]) 1665 return self 1666 1667 def relax_point_positions( 1668 self, 1669 n=10, 1670 iters=10, 1671 sub_iters=10, 1672 packing_factor=1, 1673 max_step=0, 1674 constraints=(), 1675 ) -> Self: 1676 """ 1677 Smooth mesh or points with a 1678 [Laplacian algorithm](https://vtk.org/doc/nightly/html/classvtkPointSmoothingFilter.html) 1679 variant. This modifies the coordinates of the input points by adjusting their positions 1680 to create a smooth distribution (and thereby form a pleasing packing of the points). 1681 Smoothing is performed by considering the effects of neighboring points on one another 1682 it uses a cubic cutoff function to produce repulsive forces between close points 1683 and attractive forces that are a little further away. 1684 1685 In general, the larger the neighborhood size, the greater the reduction in high frequency 1686 information. The memory and computational requirements of the algorithm may also 1687 significantly increase. 1688 1689 The algorithm incrementally adjusts the point positions through an iterative process. 1690 Basically points are moved due to the influence of neighboring points. 1691 1692 As points move, both the local connectivity and data attributes associated with each point 1693 must be updated. Rather than performing these expensive operations after every iteration, 1694 a number of sub-iterations can be specified. If so, then the neighborhood and attribute 1695 value updates occur only every sub iteration, which can improve performance significantly. 1696 1697 Arguments: 1698 n : (int) 1699 neighborhood size to calculate the Laplacian. 1700 iters : (int) 1701 number of iterations. 1702 sub_iters : (int) 1703 number of sub-iterations, i.e. the number of times the neighborhood and attribute 1704 value updates occur during each iteration. 1705 packing_factor : (float) 1706 adjust convergence speed. 1707 max_step : (float) 1708 Specify the maximum smoothing step size for each smoothing iteration. 1709 This limits the the distance over which a point can move in each iteration. 1710 As in all iterative methods, the stability of the process is sensitive to this parameter. 1711 In general, small step size and large numbers of iterations are more stable than a larger 1712 step size and a smaller numbers of iterations. 1713 constraints : (dict) 1714 dictionary of constraints. 1715 Point constraints are used to prevent points from moving, 1716 or to move only on a plane. This can prevent shrinking or growing point clouds. 1717 If enabled, a local topological analysis is performed to determine whether a point 1718 should be marked as fixed" i.e., never moves, or the point only moves on a plane, 1719 or the point can move freely. 1720 If all points in the neighborhood surrounding a point are in the cone defined by 1721 `fixed_angle`, then the point is classified as fixed. 1722 If all points in the neighborhood surrounding a point are in the cone defined by 1723 `boundary_angle`, then the point is classified as lying on a plane. 1724 Angles are expressed in degrees. 1725 1726 Example: 1727 ```py 1728 import numpy as np 1729 from vedo import Points, show 1730 from vedo.pyplot import histogram 1731 1732 vpts1 = Points(np.random.rand(10_000, 3)) 1733 dists = vpts1.auto_distance() 1734 h1 = histogram(dists, xlim=(0,0.08)).clone2d() 1735 1736 vpts2 = vpts1.clone().relax_point_positions(n=100, iters=20, sub_iters=10) 1737 dists = vpts2.auto_distance() 1738 h2 = histogram(dists, xlim=(0,0.08)).clone2d() 1739 1740 show([[vpts1, h1], [vpts2, h2]], N=2).close() 1741 ``` 1742 """ 1743 smooth = vtki.new("PointSmoothingFilter") 1744 smooth.SetInputData(self.dataset) 1745 smooth.SetSmoothingModeToUniform() 1746 smooth.SetNumberOfIterations(iters) 1747 smooth.SetNumberOfSubIterations(sub_iters) 1748 smooth.SetPackingFactor(packing_factor) 1749 if self.point_locator: 1750 smooth.SetLocator(self.point_locator) 1751 if not max_step: 1752 max_step = self.diagonal_size() / 100 1753 smooth.SetMaximumStepSize(max_step) 1754 smooth.SetNeighborhoodSize(n) 1755 if constraints: 1756 fixed_angle = constraints.get("fixed_angle", 45) 1757 boundary_angle = constraints.get("boundary_angle", 110) 1758 smooth.EnableConstraintsOn() 1759 smooth.SetFixedAngle(fixed_angle) 1760 smooth.SetBoundaryAngle(boundary_angle) 1761 smooth.GenerateConstraintScalarsOn() 1762 smooth.GenerateConstraintNormalsOn() 1763 smooth.Update() 1764 self._update(smooth.GetOutput()) 1765 self.metadata["PackingRadius"] = smooth.GetPackingRadius() 1766 self.pipeline = utils.OperationNode("relax_point_positions", parents=[self]) 1767 return self 1768 1769 def smooth_mls_1d(self, f=0.2, radius=None, n=0) -> Self: 1770 """ 1771 Smooth mesh or points with a `Moving Least Squares` variant. 1772 The point data array "Variances" will contain the residue calculated for each point. 1773 1774 Arguments: 1775 f : (float) 1776 smoothing factor - typical range is [0,2]. 1777 radius : (float) 1778 radius search in absolute units. 1779 If set then `f` is ignored. 1780 n : (int) 1781 number of neighbours to be used for the fit. 1782 If set then `f` and `radius` are ignored. 1783 1784 Examples: 1785 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1786 - [skeletonize.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/skeletonize.py) 1787 1788 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1789 """ 1790 coords = self.vertices 1791 ncoords = len(coords) 1792 1793 if n: 1794 Ncp = n 1795 elif radius: 1796 Ncp = 1 1797 else: 1798 Ncp = int(ncoords * f / 10) 1799 if Ncp < 5: 1800 vedo.logger.warning(f"Please choose a fraction higher than {f}") 1801 Ncp = 5 1802 1803 variances, newline = [], [] 1804 for p in coords: 1805 points = self.closest_point(p, n=Ncp, radius=radius) 1806 if len(points) < 4: 1807 continue 1808 1809 points = np.array(points) 1810 pointsmean = points.mean(axis=0) # plane center 1811 _, dd, vv = np.linalg.svd(points - pointsmean) 1812 newp = np.dot(p - pointsmean, vv[0]) * vv[0] + pointsmean 1813 variances.append(dd[1] + dd[2]) 1814 newline.append(newp) 1815 1816 self.pointdata["Variances"] = np.array(variances).astype(np.float32) 1817 self.vertices = newline 1818 self.pipeline = utils.OperationNode("smooth_mls_1d", parents=[self]) 1819 return self 1820 1821 def smooth_mls_2d(self, f=0.2, radius=None, n=0) -> Self: 1822 """ 1823 Smooth mesh or points with a `Moving Least Squares` algorithm variant. 1824 1825 The `mesh.pointdata['MLSVariance']` array will contain the residue calculated for each point. 1826 When a radius is specified, points that are isolated will not be moved and will get 1827 a 0 entry in array `mesh.pointdata['MLSValidPoint']`. 1828 1829 Arguments: 1830 f : (float) 1831 smoothing factor - typical range is [0, 2]. 1832 radius : (float | array) 1833 radius search in absolute units. Can be single value (float) or sequence 1834 for adaptive smoothing. If set then `f` is ignored. 1835 n : (int) 1836 number of neighbours to be used for the fit. 1837 If set then `f` and `radius` are ignored. 1838 1839 Examples: 1840 - [moving_least_squares2D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares2D.py) 1841 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1842 1843 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1844 """ 1845 coords = self.vertices 1846 ncoords = len(coords) 1847 1848 if n: 1849 Ncp = n 1850 radius = None 1851 elif radius is not None: 1852 Ncp = 1 1853 else: 1854 Ncp = int(ncoords * f / 100) 1855 if Ncp < 4: 1856 vedo.logger.error(f"please choose a f-value higher than {f}") 1857 Ncp = 4 1858 1859 variances, newpts, valid = [], [], [] 1860 radius_is_sequence = utils.is_sequence(radius) 1861 1862 pb = None 1863 if ncoords > 10000: 1864 pb = utils.ProgressBar(0, ncoords, delay=3) 1865 1866 for i, p in enumerate(coords): 1867 if pb: 1868 pb.print("smooth_mls_2d working ...") 1869 1870 # if a radius was provided for each point 1871 if radius_is_sequence: 1872 pts = self.closest_point(p, n=Ncp, radius=radius[i]) 1873 else: 1874 pts = self.closest_point(p, n=Ncp, radius=radius) 1875 1876 if len(pts) > 3: 1877 ptsmean = pts.mean(axis=0) # plane center 1878 _, dd, vv = np.linalg.svd(pts - ptsmean) 1879 cv = np.cross(vv[0], vv[1]) 1880 t = (np.dot(cv, ptsmean) - np.dot(cv, p)) / np.dot(cv, cv) 1881 newpts.append(p + cv * t) 1882 variances.append(dd[2]) 1883 if radius is not None: 1884 valid.append(1) 1885 else: 1886 newpts.append(p) 1887 variances.append(0) 1888 if radius is not None: 1889 valid.append(0) 1890 1891 if radius is not None: 1892 self.pointdata["MLSValidPoint"] = np.array(valid).astype(np.uint8) 1893 self.pointdata["MLSVariance"] = np.array(variances).astype(np.float32) 1894 1895 self.vertices = newpts 1896 1897 self.pipeline = utils.OperationNode("smooth_mls_2d", parents=[self]) 1898 return self 1899 1900 def smooth_lloyd_2d(self, iterations=2, bounds=None, options="Qbb Qc Qx") -> Self: 1901 """ 1902 Lloyd relaxation of a 2D pointcloud. 1903 1904 Arguments: 1905 iterations : (int) 1906 number of iterations. 1907 bounds : (list) 1908 bounding box of the domain. 1909 options : (str) 1910 options for the Qhull algorithm. 1911 """ 1912 # Credits: https://hatarilabs.com/ih-en/ 1913 # tutorial-to-create-a-geospatial-voronoi-sh-mesh-with-python-scipy-and-geopandas 1914 from scipy.spatial import Voronoi as scipy_voronoi 1915 1916 def _constrain_points(points): 1917 # Update any points that have drifted beyond the boundaries of this space 1918 if bounds is not None: 1919 for point in points: 1920 if point[0] < bounds[0]: point[0] = bounds[0] 1921 if point[0] > bounds[1]: point[0] = bounds[1] 1922 if point[1] < bounds[2]: point[1] = bounds[2] 1923 if point[1] > bounds[3]: point[1] = bounds[3] 1924 return points 1925 1926 def _find_centroid(vertices): 1927 # The equation for the method used here to find the centroid of a 1928 # 2D polygon is given here: https://en.wikipedia.org/wiki/Centroid#Of_a_polygon 1929 area = 0 1930 centroid_x = 0 1931 centroid_y = 0 1932 for i in range(len(vertices) - 1): 1933 step = (vertices[i, 0] * vertices[i + 1, 1]) - (vertices[i + 1, 0] * vertices[i, 1]) 1934 centroid_x += (vertices[i, 0] + vertices[i + 1, 0]) * step 1935 centroid_y += (vertices[i, 1] + vertices[i + 1, 1]) * step 1936 area += step 1937 if area: 1938 centroid_x = (1.0 / (3.0 * area)) * centroid_x 1939 centroid_y = (1.0 / (3.0 * area)) * centroid_y 1940 # prevent centroids from escaping bounding box 1941 return _constrain_points([[centroid_x, centroid_y]])[0] 1942 1943 def _relax(voron): 1944 # Moves each point to the centroid of its cell in the voronoi 1945 # map to "relax" the points (i.e. jitter the points so as 1946 # to spread them out within the space). 1947 centroids = [] 1948 for idx in voron.point_region: 1949 # the region is a series of indices into voronoi.vertices 1950 # remove point at infinity, designated by index -1 1951 region = [i for i in voron.regions[idx] if i != -1] 1952 # enclose the polygon 1953 region = region + [region[0]] 1954 verts = voron.vertices[region] 1955 # find the centroid of those vertices 1956 centroids.append(_find_centroid(verts)) 1957 return _constrain_points(centroids) 1958 1959 if bounds is None: 1960 bounds = self.bounds() 1961 1962 pts = self.vertices[:, (0, 1)] 1963 for i in range(iterations): 1964 vor = scipy_voronoi(pts, qhull_options=options) 1965 _constrain_points(vor.vertices) 1966 pts = _relax(vor) 1967 out = Points(pts) 1968 out.name = "MeshSmoothLloyd2D" 1969 out.pipeline = utils.OperationNode("smooth_lloyd", parents=[self]) 1970 return out 1971 1972 def project_on_plane(self, plane="z", point=None, direction=None) -> Self: 1973 """ 1974 Project the mesh on one of the Cartesian planes. 1975 1976 Arguments: 1977 plane : (str, Plane) 1978 if plane is `str`, plane can be one of ['x', 'y', 'z'], 1979 represents x-plane, y-plane and z-plane, respectively. 1980 Otherwise, plane should be an instance of `vedo.shapes.Plane`. 1981 point : (float, array) 1982 if plane is `str`, point should be a float represents the intercept. 1983 Otherwise, point is the camera point of perspective projection 1984 direction : (array) 1985 direction of oblique projection 1986 1987 Note: 1988 Parameters `point` and `direction` are only used if the given plane 1989 is an instance of `vedo.shapes.Plane`. And one of these two params 1990 should be left as `None` to specify the projection type. 1991 1992 Example: 1993 ```python 1994 s.project_on_plane(plane='z') # project to z-plane 1995 plane = Plane(pos=(4, 8, -4), normal=(-1, 0, 1), s=(5,5)) 1996 s.project_on_plane(plane=plane) # orthogonal projection 1997 s.project_on_plane(plane=plane, point=(6, 6, 6)) # perspective projection 1998 s.project_on_plane(plane=plane, direction=(1, 2, -1)) # oblique projection 1999 ``` 2000 2001 Examples: 2002 - [silhouette2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette2.py) 2003 2004 ![](https://vedo.embl.es/images/basic/silhouette2.png) 2005 """ 2006 coords = self.vertices 2007 2008 if plane == "x": 2009 coords[:, 0] = self.transform.position[0] 2010 intercept = self.xbounds()[0] if point is None else point 2011 self.x(intercept) 2012 elif plane == "y": 2013 coords[:, 1] = self.transform.position[1] 2014 intercept = self.ybounds()[0] if point is None else point 2015 self.y(intercept) 2016 elif plane == "z": 2017 coords[:, 2] = self.transform.position[2] 2018 intercept = self.zbounds()[0] if point is None else point 2019 self.z(intercept) 2020 2021 elif isinstance(plane, vedo.shapes.Plane): 2022 normal = plane.normal / np.linalg.norm(plane.normal) 2023 pl = np.hstack((normal, -np.dot(plane.pos(), normal))).reshape(4, 1) 2024 if direction is None and point is None: 2025 # orthogonal projection 2026 pt = np.hstack((normal, [0])).reshape(4, 1) 2027 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T # python3 only 2028 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2029 2030 elif direction is None: 2031 # perspective projection 2032 pt = np.hstack((np.array(point), [1])).reshape(4, 1) 2033 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2034 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2035 2036 elif point is None: 2037 # oblique projection 2038 pt = np.hstack((np.array(direction), [0])).reshape(4, 1) 2039 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2040 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2041 2042 coords = np.concatenate([coords, np.ones((coords.shape[:-1] + (1,)))], axis=-1) 2043 # coords = coords @ proj_mat.T 2044 coords = np.matmul(coords, proj_mat.T) 2045 coords = coords[:, :3] / coords[:, 3:] 2046 2047 else: 2048 vedo.logger.error(f"unknown plane {plane}") 2049 raise RuntimeError() 2050 2051 self.alpha(0.1) 2052 self.vertices = coords 2053 return self 2054 2055 def warp(self, source, target, sigma=1.0, mode="3d") -> Self: 2056 """ 2057 "Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set 2058 of source and target landmarks. Any point on the mesh close to a source landmark will 2059 be moved to a place close to the corresponding target landmark. 2060 The points in between are interpolated smoothly using 2061 Bookstein's Thin Plate Spline algorithm. 2062 2063 Transformation object can be accessed with `mesh.transform`. 2064 2065 Arguments: 2066 sigma : (float) 2067 specify the 'stiffness' of the spline. 2068 mode : (str) 2069 set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes) 2070 2071 Examples: 2072 - [interpolate_field.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/interpolate_field.py) 2073 - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py) 2074 - [warp2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp2.py) 2075 - [warp3.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp3.py) 2076 - [warp4a.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4a.py) 2077 - [warp4b.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4b.py) 2078 - [warp6.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp6.py) 2079 2080 ![](https://vedo.embl.es/images/advanced/warp2.png) 2081 """ 2082 parents = [self] 2083 2084 try: 2085 source = source.vertices 2086 parents.append(source) 2087 except AttributeError: 2088 source = utils.make3d(source) 2089 2090 try: 2091 target = target.vertices 2092 parents.append(target) 2093 except AttributeError: 2094 target = utils.make3d(target) 2095 2096 ns = len(source) 2097 nt = len(target) 2098 if ns != nt: 2099 vedo.logger.error(f"#source {ns} != {nt} #target points") 2100 raise RuntimeError() 2101 2102 NLT = NonLinearTransform() 2103 NLT.source_points = source 2104 NLT.target_points = target 2105 self.apply_transform(NLT) 2106 2107 self.pipeline = utils.OperationNode("warp", parents=parents) 2108 return self 2109 2110 def cut_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0), invert=False) -> Self: 2111 """ 2112 Cut the mesh with the plane defined by a point and a normal. 2113 2114 Arguments: 2115 origin : (array) 2116 the cutting plane goes through this point 2117 normal : (array) 2118 normal of the cutting plane 2119 2120 Example: 2121 ```python 2122 from vedo import Cube 2123 cube = Cube().cut_with_plane(normal=(1,1,1)) 2124 cube.back_color('pink').show().close() 2125 ``` 2126 ![](https://vedo.embl.es/images/feats/cut_with_plane_cube.png) 2127 2128 Examples: 2129 - [trail.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/trail.py) 2130 2131 ![](https://vedo.embl.es/images/simulations/trail.gif) 2132 2133 Check out also: 2134 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`. 2135 """ 2136 s = str(normal) 2137 if "x" in s: 2138 normal = (1, 0, 0) 2139 if "-" in s: 2140 normal = -np.array(normal) 2141 elif "y" in s: 2142 normal = (0, 1, 0) 2143 if "-" in s: 2144 normal = -np.array(normal) 2145 elif "z" in s: 2146 normal = (0, 0, 1) 2147 if "-" in s: 2148 normal = -np.array(normal) 2149 plane = vtki.vtkPlane() 2150 plane.SetOrigin(origin) 2151 plane.SetNormal(normal) 2152 2153 clipper = vtki.new("ClipPolyData") 2154 clipper.SetInputData(self.dataset) 2155 clipper.SetClipFunction(plane) 2156 clipper.GenerateClippedOutputOff() 2157 clipper.GenerateClipScalarsOff() 2158 clipper.SetInsideOut(invert) 2159 clipper.SetValue(0) 2160 clipper.Update() 2161 2162 self._update(clipper.GetOutput()) 2163 2164 self.pipeline = utils.OperationNode("cut_with_plane", parents=[self]) 2165 return self 2166 2167 def cut_with_planes(self, origins, normals, invert=False) -> Self: 2168 """ 2169 Cut the mesh with a convex set of planes defined by points and normals. 2170 2171 Arguments: 2172 origins : (array) 2173 each cutting plane goes through this point 2174 normals : (array) 2175 normal of each of the cutting planes 2176 invert : (bool) 2177 if True, cut outside instead of inside 2178 2179 Check out also: 2180 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()` 2181 """ 2182 2183 vpoints = vtki.vtkPoints() 2184 for p in utils.make3d(origins): 2185 vpoints.InsertNextPoint(p) 2186 normals = utils.make3d(normals) 2187 2188 planes = vtki.vtkPlanes() 2189 planes.SetPoints(vpoints) 2190 planes.SetNormals(utils.numpy2vtk(normals, dtype=float)) 2191 2192 clipper = vtki.new("ClipPolyData") 2193 clipper.SetInputData(self.dataset) 2194 clipper.SetInsideOut(invert) 2195 clipper.SetClipFunction(planes) 2196 clipper.GenerateClippedOutputOff() 2197 clipper.GenerateClipScalarsOff() 2198 clipper.SetValue(0) 2199 clipper.Update() 2200 2201 self._update(clipper.GetOutput()) 2202 2203 self.pipeline = utils.OperationNode("cut_with_planes", parents=[self]) 2204 return self 2205 2206 def cut_with_box(self, bounds, invert=False) -> Self: 2207 """ 2208 Cut the current mesh with a box or a set of boxes. 2209 This is much faster than `cut_with_mesh()`. 2210 2211 Input `bounds` can be either: 2212 - a Mesh or Points object 2213 - a list of 6 number representing a bounding box `[xmin,xmax, ymin,ymax, zmin,zmax]` 2214 - a list of bounding boxes like the above: `[[xmin1,...], [xmin2,...], ...]` 2215 2216 Example: 2217 ```python 2218 from vedo import Sphere, Cube, show 2219 mesh = Sphere(r=1, res=50) 2220 box = Cube(side=1.5).wireframe() 2221 mesh.cut_with_box(box) 2222 show(mesh, box, axes=1).close() 2223 ``` 2224 ![](https://vedo.embl.es/images/feats/cut_with_box_cube.png) 2225 2226 Check out also: 2227 `cut_with_line()`, `cut_with_plane()`, `cut_with_cylinder()` 2228 """ 2229 if isinstance(bounds, Points): 2230 bounds = bounds.bounds() 2231 2232 box = vtki.new("Box") 2233 if utils.is_sequence(bounds[0]): 2234 for bs in bounds: 2235 box.AddBounds(bs) 2236 else: 2237 box.SetBounds(bounds) 2238 2239 clipper = vtki.new("ClipPolyData") 2240 clipper.SetInputData(self.dataset) 2241 clipper.SetClipFunction(box) 2242 clipper.SetInsideOut(not invert) 2243 clipper.GenerateClippedOutputOff() 2244 clipper.GenerateClipScalarsOff() 2245 clipper.SetValue(0) 2246 clipper.Update() 2247 self._update(clipper.GetOutput()) 2248 2249 self.pipeline = utils.OperationNode("cut_with_box", parents=[self]) 2250 return self 2251 2252 def cut_with_line(self, points, invert=False, closed=True) -> Self: 2253 """ 2254 Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter. 2255 The polyline is defined by a set of points (z-coordinates are ignored). 2256 This is much faster than `cut_with_mesh()`. 2257 2258 Check out also: 2259 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2260 """ 2261 pplane = vtki.new("PolyPlane") 2262 if isinstance(points, Points): 2263 points = points.vertices.tolist() 2264 2265 if closed: 2266 if isinstance(points, np.ndarray): 2267 points = points.tolist() 2268 points.append(points[0]) 2269 2270 vpoints = vtki.vtkPoints() 2271 for p in points: 2272 if len(p) == 2: 2273 p = [p[0], p[1], 0.0] 2274 vpoints.InsertNextPoint(p) 2275 2276 n = len(points) 2277 polyline = vtki.new("PolyLine") 2278 polyline.Initialize(n, vpoints) 2279 polyline.GetPointIds().SetNumberOfIds(n) 2280 for i in range(n): 2281 polyline.GetPointIds().SetId(i, i) 2282 pplane.SetPolyLine(polyline) 2283 2284 clipper = vtki.new("ClipPolyData") 2285 clipper.SetInputData(self.dataset) 2286 clipper.SetClipFunction(pplane) 2287 clipper.SetInsideOut(invert) 2288 clipper.GenerateClippedOutputOff() 2289 clipper.GenerateClipScalarsOff() 2290 clipper.SetValue(0) 2291 clipper.Update() 2292 self._update(clipper.GetOutput()) 2293 2294 self.pipeline = utils.OperationNode("cut_with_line", parents=[self]) 2295 return self 2296 2297 def cut_with_cookiecutter(self, lines) -> Self: 2298 """ 2299 Cut the current mesh with a single line or a set of lines. 2300 2301 Input `lines` can be either: 2302 - a `Mesh` or `Points` object 2303 - a list of 3D points: `[(x1,y1,z1), (x2,y2,z2), ...]` 2304 - a list of 2D points: `[(x1,y1), (x2,y2), ...]` 2305 2306 Example: 2307 ```python 2308 from vedo import * 2309 grid = Mesh(dataurl + "dolfin_fine.vtk") 2310 grid.compute_quality().cmap("Greens") 2311 pols = merge( 2312 Polygon(nsides=10, r=0.3).pos(0.7, 0.3), 2313 Polygon(nsides=10, r=0.2).pos(0.3, 0.7), 2314 ) 2315 lines = pols.boundaries() 2316 cgrid = grid.clone().cut_with_cookiecutter(lines) 2317 grid.alpha(0.1).wireframe() 2318 show(grid, cgrid, lines, axes=8, bg='blackboard').close() 2319 ``` 2320 ![](https://vedo.embl.es/images/feats/cookiecutter.png) 2321 2322 Check out also: 2323 `cut_with_line()` and `cut_with_point_loop()` 2324 2325 Note: 2326 In case of a warning message like: 2327 "Mesh and trim loop point data attributes are different" 2328 consider interpolating the mesh point data to the loop points, 2329 Eg. (in the above example): 2330 ```python 2331 lines = pols.boundaries().interpolate_data_from(grid, n=2) 2332 ``` 2333 2334 Note: 2335 trying to invert the selection by reversing the loop order 2336 will have no effect in this method, hence it does not have 2337 the `invert` option. 2338 """ 2339 if utils.is_sequence(lines): 2340 lines = utils.make3d(lines) 2341 iline = list(range(len(lines))) + [0] 2342 poly = utils.buildPolyData(lines, lines=[iline]) 2343 else: 2344 poly = lines.dataset 2345 2346 # if invert: # not working 2347 # rev = vtki.new("ReverseSense") 2348 # rev.ReverseCellsOn() 2349 # rev.SetInputData(poly) 2350 # rev.Update() 2351 # poly = rev.GetOutput() 2352 2353 # Build loops from the polyline 2354 build_loops = vtki.new("ContourLoopExtraction") 2355 build_loops.SetGlobalWarningDisplay(0) 2356 build_loops.SetInputData(poly) 2357 build_loops.Update() 2358 boundary_poly = build_loops.GetOutput() 2359 2360 ccut = vtki.new("CookieCutter") 2361 ccut.SetInputData(self.dataset) 2362 ccut.SetLoopsData(boundary_poly) 2363 ccut.SetPointInterpolationToMeshEdges() 2364 # ccut.SetPointInterpolationToLoopEdges() 2365 ccut.PassCellDataOn() 2366 ccut.PassPointDataOn() 2367 ccut.Update() 2368 self._update(ccut.GetOutput()) 2369 2370 self.pipeline = utils.OperationNode("cut_with_cookiecutter", parents=[self]) 2371 return self 2372 2373 def cut_with_cylinder(self, center=(0, 0, 0), axis=(0, 0, 1), r=1, invert=False) -> Self: 2374 """ 2375 Cut the current mesh with an infinite cylinder. 2376 This is much faster than `cut_with_mesh()`. 2377 2378 Arguments: 2379 center : (array) 2380 the center of the cylinder 2381 normal : (array) 2382 direction of the cylinder axis 2383 r : (float) 2384 radius of the cylinder 2385 2386 Example: 2387 ```python 2388 from vedo import Disc, show 2389 disc = Disc(r1=1, r2=1.2) 2390 mesh = disc.extrude(3, res=50).linewidth(1) 2391 mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True) 2392 show(mesh, axes=1).close() 2393 ``` 2394 ![](https://vedo.embl.es/images/feats/cut_with_cylinder.png) 2395 2396 Examples: 2397 - [optics_main1.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/optics_main1.py) 2398 2399 Check out also: 2400 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2401 """ 2402 s = str(axis) 2403 if "x" in s: 2404 axis = (1, 0, 0) 2405 elif "y" in s: 2406 axis = (0, 1, 0) 2407 elif "z" in s: 2408 axis = (0, 0, 1) 2409 cyl = vtki.new("Cylinder") 2410 cyl.SetCenter(center) 2411 cyl.SetAxis(axis[0], axis[1], axis[2]) 2412 cyl.SetRadius(r) 2413 2414 clipper = vtki.new("ClipPolyData") 2415 clipper.SetInputData(self.dataset) 2416 clipper.SetClipFunction(cyl) 2417 clipper.SetInsideOut(not invert) 2418 clipper.GenerateClippedOutputOff() 2419 clipper.GenerateClipScalarsOff() 2420 clipper.SetValue(0) 2421 clipper.Update() 2422 self._update(clipper.GetOutput()) 2423 2424 self.pipeline = utils.OperationNode("cut_with_cylinder", parents=[self]) 2425 return self 2426 2427 def cut_with_sphere(self, center=(0, 0, 0), r=1.0, invert=False) -> Self: 2428 """ 2429 Cut the current mesh with an sphere. 2430 This is much faster than `cut_with_mesh()`. 2431 2432 Arguments: 2433 center : (array) 2434 the center of the sphere 2435 r : (float) 2436 radius of the sphere 2437 2438 Example: 2439 ```python 2440 from vedo import Disc, show 2441 disc = Disc(r1=1, r2=1.2) 2442 mesh = disc.extrude(3, res=50).linewidth(1) 2443 mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True) 2444 show(mesh, axes=1).close() 2445 ``` 2446 ![](https://vedo.embl.es/images/feats/cut_with_sphere.png) 2447 2448 Check out also: 2449 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2450 """ 2451 sph = vtki.new("Sphere") 2452 sph.SetCenter(center) 2453 sph.SetRadius(r) 2454 2455 clipper = vtki.new("ClipPolyData") 2456 clipper.SetInputData(self.dataset) 2457 clipper.SetClipFunction(sph) 2458 clipper.SetInsideOut(not invert) 2459 clipper.GenerateClippedOutputOff() 2460 clipper.GenerateClipScalarsOff() 2461 clipper.SetValue(0) 2462 clipper.Update() 2463 self._update(clipper.GetOutput()) 2464 self.pipeline = utils.OperationNode("cut_with_sphere", parents=[self]) 2465 return self 2466 2467 def cut_with_mesh(self, mesh, invert=False, keep=False) -> Union[Self, "vedo.Assembly"]: 2468 """ 2469 Cut an `Mesh` mesh with another `Mesh`. 2470 2471 Use `invert` to invert the selection. 2472 2473 Use `keep` to keep the cutoff part, in this case an `Assembly` is returned: 2474 the "cut" object and the "discarded" part of the original object. 2475 You can access both via `assembly.unpack()` method. 2476 2477 Example: 2478 ```python 2479 from vedo import * 2480 arr = np.random.randn(100000, 3)/2 2481 pts = Points(arr).c('red3').pos(5,0,0) 2482 cube = Cube().pos(4,0.5,0) 2483 assem = pts.cut_with_mesh(cube, keep=True) 2484 show(assem.unpack(), axes=1).close() 2485 ``` 2486 ![](https://vedo.embl.es/images/feats/cut_with_mesh.png) 2487 2488 Check out also: 2489 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2490 """ 2491 polymesh = mesh.dataset 2492 poly = self.dataset 2493 2494 # Create an array to hold distance information 2495 signed_distances = vtki.vtkFloatArray() 2496 signed_distances.SetNumberOfComponents(1) 2497 signed_distances.SetName("SignedDistances") 2498 2499 # implicit function that will be used to slice the mesh 2500 ippd = vtki.new("ImplicitPolyDataDistance") 2501 ippd.SetInput(polymesh) 2502 2503 # Evaluate the signed distance function at all of the grid points 2504 for pointId in range(poly.GetNumberOfPoints()): 2505 p = poly.GetPoint(pointId) 2506 signed_distance = ippd.EvaluateFunction(p) 2507 signed_distances.InsertNextValue(signed_distance) 2508 2509 currentscals = poly.GetPointData().GetScalars() 2510 if currentscals: 2511 currentscals = currentscals.GetName() 2512 2513 poly.GetPointData().AddArray(signed_distances) 2514 poly.GetPointData().SetActiveScalars("SignedDistances") 2515 2516 clipper = vtki.new("ClipPolyData") 2517 clipper.SetInputData(poly) 2518 clipper.SetInsideOut(not invert) 2519 clipper.SetGenerateClippedOutput(keep) 2520 clipper.SetValue(0.0) 2521 clipper.Update() 2522 cpoly = clipper.GetOutput() 2523 2524 if keep: 2525 kpoly = clipper.GetOutput(1) 2526 2527 vis = False 2528 if currentscals: 2529 cpoly.GetPointData().SetActiveScalars(currentscals) 2530 vis = self.mapper.GetScalarVisibility() 2531 2532 self._update(cpoly) 2533 2534 self.pointdata.remove("SignedDistances") 2535 self.mapper.SetScalarVisibility(vis) 2536 if keep: 2537 if isinstance(self, vedo.Mesh): 2538 cutoff = vedo.Mesh(kpoly) 2539 else: 2540 cutoff = vedo.Points(kpoly) 2541 # cutoff = self.__class__(kpoly) # this does not work properly 2542 cutoff.properties = vtki.vtkProperty() 2543 cutoff.properties.DeepCopy(self.properties) 2544 cutoff.actor.SetProperty(cutoff.properties) 2545 cutoff.c("k5").alpha(0.2) 2546 return vedo.Assembly([self, cutoff]) 2547 2548 self.pipeline = utils.OperationNode("cut_with_mesh", parents=[self, mesh]) 2549 return self 2550 2551 def cut_with_point_loop( 2552 self, points, invert=False, on="points", include_boundary=False 2553 ) -> Self: 2554 """ 2555 Cut an `Mesh` object with a set of points forming a closed loop. 2556 2557 Arguments: 2558 invert : (bool) 2559 invert selection (inside-out) 2560 on : (str) 2561 if 'cells' will extract the whole cells lying inside (or outside) the point loop 2562 include_boundary : (bool) 2563 include cells lying exactly on the boundary line. Only relevant on 'cells' mode 2564 2565 Examples: 2566 - [cut_with_points1.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points1.py) 2567 2568 ![](https://vedo.embl.es/images/advanced/cutWithPoints1.png) 2569 2570 - [cut_with_points2.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points2.py) 2571 2572 ![](https://vedo.embl.es/images/advanced/cutWithPoints2.png) 2573 """ 2574 if isinstance(points, Points): 2575 parents = [points] 2576 vpts = points.dataset.GetPoints() 2577 points = points.vertices 2578 else: 2579 parents = [self] 2580 vpts = vtki.vtkPoints() 2581 points = utils.make3d(points) 2582 for p in points: 2583 vpts.InsertNextPoint(p) 2584 2585 if "cell" in on: 2586 ippd = vtki.new("ImplicitSelectionLoop") 2587 ippd.SetLoop(vpts) 2588 ippd.AutomaticNormalGenerationOn() 2589 clipper = vtki.new("ExtractPolyDataGeometry") 2590 clipper.SetInputData(self.dataset) 2591 clipper.SetImplicitFunction(ippd) 2592 clipper.SetExtractInside(not invert) 2593 clipper.SetExtractBoundaryCells(include_boundary) 2594 else: 2595 spol = vtki.new("SelectPolyData") 2596 spol.SetLoop(vpts) 2597 spol.GenerateSelectionScalarsOn() 2598 spol.GenerateUnselectedOutputOff() 2599 spol.SetInputData(self.dataset) 2600 spol.Update() 2601 clipper = vtki.new("ClipPolyData") 2602 clipper.SetInputData(spol.GetOutput()) 2603 clipper.SetInsideOut(not invert) 2604 clipper.SetValue(0.0) 2605 clipper.Update() 2606 self._update(clipper.GetOutput()) 2607 2608 self.pipeline = utils.OperationNode("cut_with_pointloop", parents=parents) 2609 return self 2610 2611 def cut_with_scalar(self, value: float, name="", invert=False) -> Self: 2612 """ 2613 Cut a mesh or point cloud with some input scalar point-data. 2614 2615 Arguments: 2616 value : (float) 2617 cutting value 2618 name : (str) 2619 array name of the scalars to be used 2620 invert : (bool) 2621 flip selection 2622 2623 Example: 2624 ```python 2625 from vedo import * 2626 s = Sphere().lw(1) 2627 pts = s.vertices 2628 scalars = np.sin(3*pts[:,2]) + pts[:,0] 2629 s.pointdata["somevalues"] = scalars 2630 s.cut_with_scalar(0.3) 2631 s.cmap("Spectral", "somevalues").add_scalarbar() 2632 s.show(axes=1).close() 2633 ``` 2634 ![](https://vedo.embl.es/images/feats/cut_with_scalars.png) 2635 """ 2636 if name: 2637 self.pointdata.select(name) 2638 clipper = vtki.new("ClipPolyData") 2639 clipper.SetInputData(self.dataset) 2640 clipper.SetValue(value) 2641 clipper.GenerateClippedOutputOff() 2642 clipper.SetInsideOut(not invert) 2643 clipper.Update() 2644 self._update(clipper.GetOutput()) 2645 self.pipeline = utils.OperationNode("cut_with_scalar", parents=[self]) 2646 return self 2647 2648 def crop(self, 2649 top=None, bottom=None, right=None, left=None, front=None, back=None, 2650 bounds=()) -> Self: 2651 """ 2652 Crop an `Mesh` object. 2653 2654 Arguments: 2655 top : (float) 2656 fraction to crop from the top plane (positive z) 2657 bottom : (float) 2658 fraction to crop from the bottom plane (negative z) 2659 front : (float) 2660 fraction to crop from the front plane (positive y) 2661 back : (float) 2662 fraction to crop from the back plane (negative y) 2663 right : (float) 2664 fraction to crop from the right plane (positive x) 2665 left : (float) 2666 fraction to crop from the left plane (negative x) 2667 bounds : (list) 2668 bounding box of the crop region as `[x0,x1, y0,y1, z0,z1]` 2669 2670 Example: 2671 ```python 2672 from vedo import Sphere 2673 Sphere().crop(right=0.3, left=0.1).show() 2674 ``` 2675 ![](https://user-images.githubusercontent.com/32848391/57081955-0ef1e800-6cf6-11e9-99de-b45220939bc9.png) 2676 """ 2677 if not len(bounds): 2678 pos = np.array(self.pos()) 2679 x0, x1, y0, y1, z0, z1 = self.bounds() 2680 x0, y0, z0 = [x0, y0, z0] - pos 2681 x1, y1, z1 = [x1, y1, z1] - pos 2682 2683 dx, dy, dz = x1 - x0, y1 - y0, z1 - z0 2684 if top: 2685 z1 = z1 - top * dz 2686 if bottom: 2687 z0 = z0 + bottom * dz 2688 if front: 2689 y1 = y1 - front * dy 2690 if back: 2691 y0 = y0 + back * dy 2692 if right: 2693 x1 = x1 - right * dx 2694 if left: 2695 x0 = x0 + left * dx 2696 bounds = (x0, x1, y0, y1, z0, z1) 2697 2698 cu = vtki.new("Box") 2699 cu.SetBounds(bounds) 2700 2701 clipper = vtki.new("ClipPolyData") 2702 clipper.SetInputData(self.dataset) 2703 clipper.SetClipFunction(cu) 2704 clipper.InsideOutOn() 2705 clipper.GenerateClippedOutputOff() 2706 clipper.GenerateClipScalarsOff() 2707 clipper.SetValue(0) 2708 clipper.Update() 2709 self._update(clipper.GetOutput()) 2710 2711 self.pipeline = utils.OperationNode( 2712 "crop", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 2713 ) 2714 return self 2715 2716 def generate_surface_halo( 2717 self, 2718 distance=0.05, 2719 res=(50, 50, 50), 2720 bounds=(), 2721 maxdist=None, 2722 ) -> "vedo.Mesh": 2723 """ 2724 Generate the surface halo which sits at the specified distance from the input one. 2725 2726 Arguments: 2727 distance : (float) 2728 distance from the input surface 2729 res : (int) 2730 resolution of the surface 2731 bounds : (list) 2732 bounding box of the surface 2733 maxdist : (float) 2734 maximum distance to generate the surface 2735 """ 2736 if not bounds: 2737 bounds = self.bounds() 2738 2739 if not maxdist: 2740 maxdist = self.diagonal_size() / 2 2741 2742 imp = vtki.new("ImplicitModeller") 2743 imp.SetInputData(self.dataset) 2744 imp.SetSampleDimensions(res) 2745 if maxdist: 2746 imp.SetMaximumDistance(maxdist) 2747 if len(bounds) == 6: 2748 imp.SetModelBounds(bounds) 2749 contour = vtki.new("ContourFilter") 2750 contour.SetInputConnection(imp.GetOutputPort()) 2751 contour.SetValue(0, distance) 2752 contour.Update() 2753 out = vedo.Mesh(contour.GetOutput()) 2754 out.c("lightblue").alpha(0.25).lighting("off") 2755 out.pipeline = utils.OperationNode("generate_surface_halo", parents=[self]) 2756 return out 2757 2758 def generate_mesh( 2759 self, 2760 line_resolution=None, 2761 mesh_resolution=None, 2762 smooth=0.0, 2763 jitter=0.001, 2764 grid=None, 2765 quads=False, 2766 invert=False, 2767 ) -> Self: 2768 """ 2769 Generate a polygonal Mesh from a closed contour line. 2770 If line is not closed it will be closed with a straight segment. 2771 2772 Check also `generate_delaunay2d()`. 2773 2774 Arguments: 2775 line_resolution : (int) 2776 resolution of the contour line. The default is None, in this case 2777 the contour is not resampled. 2778 mesh_resolution : (int) 2779 resolution of the internal triangles not touching the boundary. 2780 smooth : (float) 2781 smoothing of the contour before meshing. 2782 jitter : (float) 2783 add a small noise to the internal points. 2784 grid : (Grid) 2785 manually pass a Grid object. The default is True. 2786 quads : (bool) 2787 generate a mesh of quads instead of triangles. 2788 invert : (bool) 2789 flip the line orientation. The default is False. 2790 2791 Examples: 2792 - [line2mesh_tri.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_tri.py) 2793 2794 ![](https://vedo.embl.es/images/advanced/line2mesh_tri.jpg) 2795 2796 - [line2mesh_quads.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_quads.py) 2797 2798 ![](https://vedo.embl.es/images/advanced/line2mesh_quads.png) 2799 """ 2800 if line_resolution is None: 2801 contour = vedo.shapes.Line(self.vertices) 2802 else: 2803 contour = vedo.shapes.Spline(self.vertices, smooth=smooth, res=line_resolution) 2804 contour.clean() 2805 2806 length = contour.length() 2807 density = length / contour.npoints 2808 # print(f"tomesh():\n\tline length = {length}") 2809 # print(f"\tdensity = {density} length/pt_separation") 2810 2811 x0, x1 = contour.xbounds() 2812 y0, y1 = contour.ybounds() 2813 2814 if grid is None: 2815 if mesh_resolution is None: 2816 resx = int((x1 - x0) / density + 0.5) 2817 resy = int((y1 - y0) / density + 0.5) 2818 # print(f"tmesh_resolution = {[resx, resy]}") 2819 else: 2820 if utils.is_sequence(mesh_resolution): 2821 resx, resy = mesh_resolution 2822 else: 2823 resx, resy = mesh_resolution, mesh_resolution 2824 grid = vedo.shapes.Grid( 2825 [(x0 + x1) / 2, (y0 + y1) / 2, 0], 2826 s=((x1 - x0) * 1.025, (y1 - y0) * 1.025), 2827 res=(resx, resy), 2828 ) 2829 else: 2830 grid = grid.clone() 2831 2832 cpts = contour.vertices 2833 2834 # make sure it's closed 2835 p0, p1 = cpts[0], cpts[-1] 2836 nj = max(2, int(utils.mag(p1 - p0) / density + 0.5)) 2837 joinline = vedo.shapes.Line(p1, p0, res=nj) 2838 contour = vedo.merge(contour, joinline).subsample(0.0001) 2839 2840 ####################################### quads 2841 if quads: 2842 cmesh = grid.clone().cut_with_point_loop(contour, on="cells", invert=invert) 2843 cmesh.wireframe(False).lw(0.5) 2844 cmesh.pipeline = utils.OperationNode( 2845 "generate_mesh", 2846 parents=[self, contour], 2847 comment=f"#quads {cmesh.dataset.GetNumberOfCells()}", 2848 ) 2849 return cmesh 2850 ############################################# 2851 2852 grid_tmp = grid.vertices.copy() 2853 2854 if jitter: 2855 np.random.seed(0) 2856 sigma = 1.0 / np.sqrt(grid.npoints) * grid.diagonal_size() * jitter 2857 # print(f"\tsigma jittering = {sigma}") 2858 grid_tmp += np.random.rand(grid.npoints, 3) * sigma 2859 grid_tmp[:, 2] = 0.0 2860 2861 todel = [] 2862 density /= np.sqrt(3) 2863 vgrid_tmp = Points(grid_tmp) 2864 2865 for p in contour.vertices: 2866 out = vgrid_tmp.closest_point(p, radius=density, return_point_id=True) 2867 todel += out.tolist() 2868 2869 grid_tmp = grid_tmp.tolist() 2870 for index in sorted(list(set(todel)), reverse=True): 2871 del grid_tmp[index] 2872 2873 points = contour.vertices.tolist() + grid_tmp 2874 if invert: 2875 boundary = list(reversed(range(contour.npoints))) 2876 else: 2877 boundary = list(range(contour.npoints)) 2878 2879 dln = Points(points).generate_delaunay2d(mode="xy", boundaries=[boundary]) 2880 dln.compute_normals(points=False) # fixes reversd faces 2881 dln.lw(1) 2882 2883 dln.pipeline = utils.OperationNode( 2884 "generate_mesh", 2885 parents=[self, contour], 2886 comment=f"#cells {dln.dataset.GetNumberOfCells()}", 2887 ) 2888 return dln 2889 2890 def reconstruct_surface( 2891 self, 2892 dims=(100, 100, 100), 2893 radius=None, 2894 sample_size=None, 2895 hole_filling=True, 2896 bounds=(), 2897 padding=0.05, 2898 ) -> "vedo.Mesh": 2899 """ 2900 Surface reconstruction from a scattered cloud of points. 2901 2902 Arguments: 2903 dims : (int) 2904 number of voxels in x, y and z to control precision. 2905 radius : (float) 2906 radius of influence of each point. 2907 Smaller values generally improve performance markedly. 2908 Note that after the signed distance function is computed, 2909 any voxel taking on the value >= radius 2910 is presumed to be "unseen" or uninitialized. 2911 sample_size : (int) 2912 if normals are not present 2913 they will be calculated using this sample size per point. 2914 hole_filling : (bool) 2915 enables hole filling, this generates 2916 separating surfaces between the empty and unseen portions of the volume. 2917 bounds : (list) 2918 region in space in which to perform the sampling 2919 in format (xmin,xmax, ymin,ymax, zim, zmax) 2920 padding : (float) 2921 increase by this fraction the bounding box 2922 2923 Examples: 2924 - [recosurface.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/recosurface.py) 2925 2926 ![](https://vedo.embl.es/images/advanced/recosurface.png) 2927 """ 2928 if not utils.is_sequence(dims): 2929 dims = (dims, dims, dims) 2930 2931 sdf = vtki.new("SignedDistance") 2932 2933 if len(bounds) == 6: 2934 sdf.SetBounds(bounds) 2935 else: 2936 x0, x1, y0, y1, z0, z1 = self.bounds() 2937 sdf.SetBounds( 2938 x0 - (x1 - x0) * padding, 2939 x1 + (x1 - x0) * padding, 2940 y0 - (y1 - y0) * padding, 2941 y1 + (y1 - y0) * padding, 2942 z0 - (z1 - z0) * padding, 2943 z1 + (z1 - z0) * padding, 2944 ) 2945 2946 bb = sdf.GetBounds() 2947 if bb[0]==bb[1]: 2948 vedo.logger.warning("reconstruct_surface(): zero x-range") 2949 if bb[2]==bb[3]: 2950 vedo.logger.warning("reconstruct_surface(): zero y-range") 2951 if bb[4]==bb[5]: 2952 vedo.logger.warning("reconstruct_surface(): zero z-range") 2953 2954 pd = self.dataset 2955 2956 if pd.GetPointData().GetNormals(): 2957 sdf.SetInputData(pd) 2958 else: 2959 normals = vtki.new("PCANormalEstimation") 2960 normals.SetInputData(pd) 2961 if not sample_size: 2962 sample_size = int(pd.GetNumberOfPoints() / 50) 2963 normals.SetSampleSize(sample_size) 2964 normals.SetNormalOrientationToGraphTraversal() 2965 sdf.SetInputConnection(normals.GetOutputPort()) 2966 # print("Recalculating normals with sample size =", sample_size) 2967 2968 if radius is None: 2969 radius = self.diagonal_size() / (sum(dims) / 3) * 5 2970 # print("Calculating mesh from points with radius =", radius) 2971 2972 sdf.SetRadius(radius) 2973 sdf.SetDimensions(dims) 2974 sdf.Update() 2975 2976 surface = vtki.new("ExtractSurface") 2977 surface.SetRadius(radius * 0.99) 2978 surface.SetHoleFilling(hole_filling) 2979 surface.ComputeNormalsOff() 2980 surface.ComputeGradientsOff() 2981 surface.SetInputConnection(sdf.GetOutputPort()) 2982 surface.Update() 2983 m = vedo.mesh.Mesh(surface.GetOutput(), c=self.color()) 2984 2985 m.pipeline = utils.OperationNode( 2986 "reconstruct_surface", 2987 parents=[self], 2988 comment=f"#pts {m.dataset.GetNumberOfPoints()}", 2989 ) 2990 return m 2991 2992 def compute_clustering(self, radius: float) -> Self: 2993 """ 2994 Cluster points in space. The `radius` is the radius of local search. 2995 2996 An array named "ClusterId" is added to `pointdata`. 2997 2998 Examples: 2999 - [clustering.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/clustering.py) 3000 3001 ![](https://vedo.embl.es/images/basic/clustering.png) 3002 """ 3003 cluster = vtki.new("EuclideanClusterExtraction") 3004 cluster.SetInputData(self.dataset) 3005 cluster.SetExtractionModeToAllClusters() 3006 cluster.SetRadius(radius) 3007 cluster.ColorClustersOn() 3008 cluster.Update() 3009 idsarr = cluster.GetOutput().GetPointData().GetArray("ClusterId") 3010 self.dataset.GetPointData().AddArray(idsarr) 3011 self.pipeline = utils.OperationNode( 3012 "compute_clustering", parents=[self], comment=f"radius = {radius}" 3013 ) 3014 return self 3015 3016 def compute_connections(self, radius, mode=0, regions=(), vrange=(0, 1), seeds=(), angle=0.0) -> Self: 3017 """ 3018 Extracts and/or segments points from a point cloud based on geometric distance measures 3019 (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range. 3020 The default operation is to segment the points into "connected" regions where the connection 3021 is determined by an appropriate distance measure. Each region is given a region id. 3022 3023 Optionally, the filter can output the largest connected region of points; a particular region 3024 (via id specification); those regions that are seeded using a list of input point ids; 3025 or the region of points closest to a specified position. 3026 3027 The key parameter of this filter is the radius defining a sphere around each point which defines 3028 a local neighborhood: any other points in the local neighborhood are assumed connected to the point. 3029 Note that the radius is defined in absolute terms. 3030 3031 Other parameters are used to further qualify what it means to be a neighboring point. 3032 For example, scalar range and/or point normals can be used to further constrain the neighborhood. 3033 Also the extraction mode defines how the filter operates. 3034 By default, all regions are extracted but it is possible to extract particular regions; 3035 the region closest to a seed point; seeded regions; or the largest region found while processing. 3036 By default, all regions are extracted. 3037 3038 On output, all points are labeled with a region number. 3039 However note that the number of input and output points may not be the same: 3040 if not extracting all regions then the output size may be less than the input size. 3041 3042 Arguments: 3043 radius : (float) 3044 variable specifying a local sphere used to define local point neighborhood 3045 mode : (int) 3046 - 0, Extract all regions 3047 - 1, Extract point seeded regions 3048 - 2, Extract largest region 3049 - 3, Test specified regions 3050 - 4, Extract all regions with scalar connectivity 3051 - 5, Extract point seeded regions 3052 regions : (list) 3053 a list of non-negative regions id to extract 3054 vrange : (list) 3055 scalar range to use to extract points based on scalar connectivity 3056 seeds : (list) 3057 a list of non-negative point seed ids 3058 angle : (list) 3059 points are connected if the angle between their normals is 3060 within this angle threshold (expressed in degrees). 3061 """ 3062 # https://vtk.org/doc/nightly/html/classvtkConnectedPointsFilter.html 3063 cpf = vtki.new("ConnectedPointsFilter") 3064 cpf.SetInputData(self.dataset) 3065 cpf.SetRadius(radius) 3066 if mode == 0: # Extract all regions 3067 pass 3068 3069 elif mode == 1: # Extract point seeded regions 3070 cpf.SetExtractionModeToPointSeededRegions() 3071 for s in seeds: 3072 cpf.AddSeed(s) 3073 3074 elif mode == 2: # Test largest region 3075 cpf.SetExtractionModeToLargestRegion() 3076 3077 elif mode == 3: # Test specified regions 3078 cpf.SetExtractionModeToSpecifiedRegions() 3079 for r in regions: 3080 cpf.AddSpecifiedRegion(r) 3081 3082 elif mode == 4: # Extract all regions with scalar connectivity 3083 cpf.SetExtractionModeToLargestRegion() 3084 cpf.ScalarConnectivityOn() 3085 cpf.SetScalarRange(vrange[0], vrange[1]) 3086 3087 elif mode == 5: # Extract point seeded regions 3088 cpf.SetExtractionModeToLargestRegion() 3089 cpf.ScalarConnectivityOn() 3090 cpf.SetScalarRange(vrange[0], vrange[1]) 3091 cpf.AlignedNormalsOn() 3092 cpf.SetNormalAngle(angle) 3093 3094 cpf.Update() 3095 self._update(cpf.GetOutput(), reset_locators=False) 3096 return self 3097 3098 def compute_camera_distance(self) -> np.ndarray: 3099 """ 3100 Calculate the distance from points to the camera. 3101 3102 A pointdata array is created with name 'DistanceToCamera' and returned. 3103 """ 3104 if vedo.plotter_instance and vedo.plotter_instance.renderer: 3105 poly = self.dataset 3106 dc = vtki.new("DistanceToCamera") 3107 dc.SetInputData(poly) 3108 dc.SetRenderer(vedo.plotter_instance.renderer) 3109 dc.Update() 3110 self._update(dc.GetOutput(), reset_locators=False) 3111 return self.pointdata["DistanceToCamera"] 3112 return np.array([]) 3113 3114 def densify(self, target_distance=0.1, nclosest=6, radius=None, niter=1, nmax=None) -> Self: 3115 """ 3116 Return a copy of the cloud with new added points. 3117 The new points are created in such a way that all points in any local neighborhood are 3118 within a target distance of one another. 3119 3120 For each input point, the distance to all points in its neighborhood is computed. 3121 If any of its neighbors is further than the target distance, 3122 the edge connecting the point and its neighbor is bisected and 3123 a new point is inserted at the bisection point. 3124 A single pass is completed once all the input points are visited. 3125 Then the process repeats to the number of iterations. 3126 3127 Examples: 3128 - [densifycloud.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/densifycloud.py) 3129 3130 ![](https://vedo.embl.es/images/volumetric/densifycloud.png) 3131 3132 .. note:: 3133 Points will be created in an iterative fashion until all points in their 3134 local neighborhood are the target distance apart or less. 3135 Note that the process may terminate early due to the 3136 number of iterations. By default the target distance is set to 0.5. 3137 Note that the target_distance should be less than the radius 3138 or nothing will change on output. 3139 3140 .. warning:: 3141 This class can generate a lot of points very quickly. 3142 The maximum number of iterations is by default set to =1.0 for this reason. 3143 Increase the number of iterations very carefully. 3144 Also, `nmax` can be set to limit the explosion of points. 3145 It is also recommended that a N closest neighborhood is used. 3146 3147 """ 3148 src = vtki.new("ProgrammableSource") 3149 opts = self.vertices 3150 # zeros = np.zeros(3) 3151 3152 def _read_points(): 3153 output = src.GetPolyDataOutput() 3154 points = vtki.vtkPoints() 3155 for p in opts: 3156 # print(p) 3157 # if not np.array_equal(p, zeros): 3158 points.InsertNextPoint(p) 3159 output.SetPoints(points) 3160 3161 src.SetExecuteMethod(_read_points) 3162 3163 dens = vtki.new("DensifyPointCloudFilter") 3164 dens.SetInputConnection(src.GetOutputPort()) 3165 # dens.SetInputData(self.dataset) # this does not work 3166 dens.InterpolateAttributeDataOn() 3167 dens.SetTargetDistance(target_distance) 3168 dens.SetMaximumNumberOfIterations(niter) 3169 if nmax: 3170 dens.SetMaximumNumberOfPoints(nmax) 3171 3172 if radius: 3173 dens.SetNeighborhoodTypeToRadius() 3174 dens.SetRadius(radius) 3175 elif nclosest: 3176 dens.SetNeighborhoodTypeToNClosest() 3177 dens.SetNumberOfClosestPoints(nclosest) 3178 else: 3179 vedo.logger.error("set either radius or nclosest") 3180 raise RuntimeError() 3181 dens.Update() 3182 3183 cld = Points(dens.GetOutput()) 3184 cld.copy_properties_from(self) 3185 cld.interpolate_data_from(self, n=nclosest, radius=radius) 3186 cld.name = "DensifiedCloud" 3187 cld.pipeline = utils.OperationNode( 3188 "densify", 3189 parents=[self], 3190 c="#e9c46a:", 3191 comment=f"#pts {cld.dataset.GetNumberOfPoints()}", 3192 ) 3193 return cld 3194 3195 ############################################################################### 3196 ## stuff returning a Volume 3197 ############################################################################### 3198 3199 def density( 3200 self, dims=(40, 40, 40), bounds=None, radius=None, compute_gradient=False, locator=None 3201 ) -> "vedo.Volume": 3202 """ 3203 Generate a density field from a point cloud. Input can also be a set of 3D coordinates. 3204 Output is a `Volume`. 3205 3206 The local neighborhood is specified as the `radius` around each sample position (each voxel). 3207 If left to None, the radius is automatically computed as the diagonal of the bounding box 3208 and can be accessed via `vol.metadata["radius"]`. 3209 The density is expressed as the number of counts in the radius search. 3210 3211 Arguments: 3212 dims : (int, list) 3213 number of voxels in x, y and z of the output Volume. 3214 compute_gradient : (bool) 3215 Turn on/off the generation of the gradient vector, 3216 gradient magnitude scalar, and function classification scalar. 3217 By default this is off. Note that this will increase execution time 3218 and the size of the output. (The names of these point data arrays are: 3219 "Gradient", "Gradient Magnitude", and "Classification") 3220 locator : (vtkPointLocator) 3221 can be assigned from a previous call for speed (access it via `object.point_locator`). 3222 3223 Examples: 3224 - [plot_density3d.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/plot_density3d.py) 3225 3226 ![](https://vedo.embl.es/images/pyplot/plot_density3d.png) 3227 """ 3228 pdf = vtki.new("PointDensityFilter") 3229 pdf.SetInputData(self.dataset) 3230 3231 if not utils.is_sequence(dims): 3232 dims = [dims, dims, dims] 3233 3234 if bounds is None: 3235 bounds = list(self.bounds()) 3236 elif len(bounds) == 4: 3237 bounds = [*bounds, 0, 0] 3238 3239 if bounds[5] - bounds[4] == 0 or len(dims) == 2: # its 2D 3240 dims = list(dims) 3241 dims = [dims[0], dims[1], 2] 3242 diag = self.diagonal_size() 3243 bounds[5] = bounds[4] + diag / 1000 3244 pdf.SetModelBounds(bounds) 3245 3246 pdf.SetSampleDimensions(dims) 3247 3248 if locator: 3249 pdf.SetLocator(locator) 3250 3251 pdf.SetDensityEstimateToFixedRadius() 3252 if radius is None: 3253 radius = self.diagonal_size() / 20 3254 pdf.SetRadius(radius) 3255 pdf.SetComputeGradient(compute_gradient) 3256 pdf.Update() 3257 3258 vol = vedo.Volume(pdf.GetOutput()).mode(1) 3259 vol.name = "PointDensity" 3260 vol.metadata["radius"] = radius 3261 vol.locator = pdf.GetLocator() 3262 vol.pipeline = utils.OperationNode( 3263 "density", parents=[self], comment=f"dims={tuple(vol.dimensions())}" 3264 ) 3265 return vol 3266 3267 3268 def tovolume( 3269 self, 3270 kernel="shepard", 3271 radius=None, 3272 n=None, 3273 bounds=None, 3274 null_value=None, 3275 dims=(25, 25, 25), 3276 ) -> "vedo.Volume": 3277 """ 3278 Generate a `Volume` by interpolating a scalar 3279 or vector field which is only known on a scattered set of points or mesh. 3280 Available interpolation kernels are: shepard, gaussian, or linear. 3281 3282 Arguments: 3283 kernel : (str) 3284 interpolation kernel type [shepard] 3285 radius : (float) 3286 radius of the local search 3287 n : (int) 3288 number of point to use for interpolation 3289 bounds : (list) 3290 bounding box of the output Volume object 3291 dims : (list) 3292 dimensions of the output Volume object 3293 null_value : (float) 3294 value to be assigned to invalid points 3295 3296 Examples: 3297 - [interpolate_volume.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/interpolate_volume.py) 3298 3299 ![](https://vedo.embl.es/images/volumetric/59095175-1ec5a300-8918-11e9-8bc0-fd35c8981e2b.jpg) 3300 """ 3301 if radius is None and not n: 3302 vedo.logger.error("please set either radius or n") 3303 raise RuntimeError 3304 3305 poly = self.dataset 3306 3307 # Create a probe volume 3308 probe = vtki.vtkImageData() 3309 probe.SetDimensions(dims) 3310 if bounds is None: 3311 bounds = self.bounds() 3312 probe.SetOrigin(bounds[0], bounds[2], bounds[4]) 3313 probe.SetSpacing( 3314 (bounds[1] - bounds[0]) / dims[0], 3315 (bounds[3] - bounds[2]) / dims[1], 3316 (bounds[5] - bounds[4]) / dims[2], 3317 ) 3318 3319 if not self.point_locator: 3320 self.point_locator = vtki.new("PointLocator") 3321 self.point_locator.SetDataSet(poly) 3322 self.point_locator.BuildLocator() 3323 3324 if kernel == "shepard": 3325 kern = vtki.new("ShepardKernel") 3326 kern.SetPowerParameter(2) 3327 elif kernel == "gaussian": 3328 kern = vtki.new("GaussianKernel") 3329 elif kernel == "linear": 3330 kern = vtki.new("LinearKernel") 3331 else: 3332 vedo.logger.error("Error in tovolume(), available kernels are:") 3333 vedo.logger.error(" [shepard, gaussian, linear]") 3334 raise RuntimeError() 3335 3336 if radius: 3337 kern.SetRadius(radius) 3338 3339 interpolator = vtki.new("PointInterpolator") 3340 interpolator.SetInputData(probe) 3341 interpolator.SetSourceData(poly) 3342 interpolator.SetKernel(kern) 3343 interpolator.SetLocator(self.point_locator) 3344 3345 if n: 3346 kern.SetNumberOfPoints(n) 3347 kern.SetKernelFootprintToNClosest() 3348 else: 3349 kern.SetRadius(radius) 3350 3351 if null_value is not None: 3352 interpolator.SetNullValue(null_value) 3353 else: 3354 interpolator.SetNullPointsStrategyToClosestPoint() 3355 interpolator.Update() 3356 3357 vol = vedo.Volume(interpolator.GetOutput()) 3358 3359 vol.pipeline = utils.OperationNode( 3360 "signed_distance", 3361 parents=[self], 3362 comment=f"dims={tuple(vol.dimensions())}", 3363 c="#e9c46a:#0096c7", 3364 ) 3365 return vol 3366 3367 ################################################################################# 3368 def generate_segments(self, istart=0, rmax=1e30, niter=3) -> "vedo.shapes.Lines": 3369 """ 3370 Generate a line segments from a set of points. 3371 The algorithm is based on the closest point search. 3372 3373 Returns a `Line` object. 3374 This object contains the a metadata array of used vertex counts in "UsedVertexCount" 3375 and the sum of the length of the segments in "SegmentsLengthSum". 3376 3377 Arguments: 3378 istart : (int) 3379 index of the starting point 3380 rmax : (float) 3381 maximum length of a segment 3382 niter : (int) 3383 number of iterations or passes through the points 3384 3385 Examples: 3386 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 3387 """ 3388 points = self.vertices 3389 segments = [] 3390 dists = [] 3391 n = len(points) 3392 used = np.zeros(n, dtype=int) 3393 for _ in range(niter): 3394 i = istart 3395 for _ in range(n): 3396 p = points[i] 3397 ids = self.closest_point(p, n=4, return_point_id=True) 3398 j = ids[1] 3399 if used[j] > 1 or [j, i] in segments: 3400 j = ids[2] 3401 if used[j] > 1: 3402 j = ids[3] 3403 d = np.linalg.norm(p - points[j]) 3404 if used[j] > 1 or used[i] > 1 or d > rmax: 3405 i += 1 3406 if i >= n: 3407 i = 0 3408 continue 3409 used[i] += 1 3410 used[j] += 1 3411 segments.append([i, j]) 3412 dists.append(d) 3413 i = j 3414 segments = np.array(segments, dtype=int) 3415 3416 lines = vedo.shapes.Lines(points[segments], c="k", lw=3) 3417 lines.metadata["UsedVertexCount"] = used 3418 lines.metadata["SegmentsLengthSum"] = np.sum(dists) 3419 lines.pipeline = utils.OperationNode("generate_segments", parents=[self]) 3420 lines.name = "Segments" 3421 return lines 3422 3423 def generate_delaunay2d( 3424 self, 3425 mode="scipy", 3426 boundaries=(), 3427 tol=None, 3428 alpha=0.0, 3429 offset=0.0, 3430 transform=None, 3431 ) -> "vedo.mesh.Mesh": 3432 """ 3433 Create a mesh from points in the XY plane. 3434 If `mode='fit'` then the filter computes a best fitting 3435 plane and projects the points onto it. 3436 3437 Check also `generate_mesh()`. 3438 3439 Arguments: 3440 tol : (float) 3441 specify a tolerance to control discarding of closely spaced points. 3442 This tolerance is specified as a fraction of the diagonal length of the bounding box of the points. 3443 alpha : (float) 3444 for a non-zero alpha value, only edges or triangles contained 3445 within a sphere centered at mesh vertices will be output. 3446 Otherwise, only triangles will be output. 3447 offset : (float) 3448 multiplier to control the size of the initial, bounding Delaunay triangulation. 3449 transform: (LinearTransform, NonLinearTransform) 3450 a transformation which is applied to points to generate a 2D problem. 3451 This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane. 3452 The points are transformed and triangulated. 3453 The topology of triangulated points is used as the output topology. 3454 3455 Examples: 3456 - [delaunay2d.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delaunay2d.py) 3457 3458 ![](https://vedo.embl.es/images/basic/delaunay2d.png) 3459 """ 3460 plist = self.vertices.copy() 3461 3462 ######################################################### 3463 if mode == "scipy": 3464 from scipy.spatial import Delaunay as scipy_delaunay 3465 3466 tri = scipy_delaunay(plist[:, 0:2]) 3467 return vedo.mesh.Mesh([plist, tri.simplices]) 3468 ########################################################## 3469 3470 pd = vtki.vtkPolyData() 3471 vpts = vtki.vtkPoints() 3472 vpts.SetData(utils.numpy2vtk(plist, dtype=np.float32)) 3473 pd.SetPoints(vpts) 3474 3475 delny = vtki.new("Delaunay2D") 3476 delny.SetInputData(pd) 3477 if tol: 3478 delny.SetTolerance(tol) 3479 delny.SetAlpha(alpha) 3480 delny.SetOffset(offset) 3481 3482 if transform: 3483 delny.SetTransform(transform.T) 3484 elif mode == "fit": 3485 delny.SetProjectionPlaneMode(vtki.get_class("VTK_BEST_FITTING_PLANE")) 3486 elif mode == "xy" and boundaries: 3487 boundary = vtki.vtkPolyData() 3488 boundary.SetPoints(vpts) 3489 cell_array = vtki.vtkCellArray() 3490 for b in boundaries: 3491 cpolygon = vtki.vtkPolygon() 3492 for idd in b: 3493 cpolygon.GetPointIds().InsertNextId(idd) 3494 cell_array.InsertNextCell(cpolygon) 3495 boundary.SetPolys(cell_array) 3496 delny.SetSourceData(boundary) 3497 3498 delny.Update() 3499 3500 msh = vedo.mesh.Mesh(delny.GetOutput()) 3501 msh.name = "Delaunay2D" 3502 msh.clean().lighting("off") 3503 msh.pipeline = utils.OperationNode( 3504 "delaunay2d", 3505 parents=[self], 3506 comment=f"#cells {msh.dataset.GetNumberOfCells()}", 3507 ) 3508 return msh 3509 3510 def generate_voronoi(self, padding=0.0, fit=False, method="vtk") -> "vedo.Mesh": 3511 """ 3512 Generate the 2D Voronoi convex tiling of the input points (z is ignored). 3513 The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon. 3514 3515 A cell array named "VoronoiID" is added to the output Mesh. 3516 3517 The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest 3518 to one of the input points. Voronoi tessellations are important in computational geometry 3519 (and many other fields), and are the dual of Delaunay triangulations. 3520 3521 Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored 3522 (although carried through to the output). 3523 If you desire to triangulate in a different plane, you can use fit=True. 3524 3525 A brief summary is as follows. Each (generating) input point is associated with 3526 an initial Voronoi tile, which is simply the bounding box of the point set. 3527 A locator is then used to identify nearby points: each neighbor in turn generates a 3528 clipping line positioned halfway between the generating point and the neighboring point, 3529 and orthogonal to the line connecting them. Clips are readily performed by evaluationg the 3530 vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line. 3531 If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip 3532 line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs, 3533 the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region 3534 containing the neighboring clip points. The clip region (along with the points contained in it) is grown 3535 by careful expansion (e.g., outward spiraling iterator over all candidate clip points). 3536 When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi 3537 tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi 3538 tessellation. Note that topological and geometric information is used to generate a valid triangulation 3539 (e.g., merging points and validating topology). 3540 3541 Arguments: 3542 pts : (list) 3543 list of input points. 3544 padding : (float) 3545 padding distance. The default is 0. 3546 fit : (bool) 3547 detect automatically the best fitting plane. The default is False. 3548 3549 Examples: 3550 - [voronoi1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi1.py) 3551 3552 ![](https://vedo.embl.es/images/basic/voronoi1.png) 3553 3554 - [voronoi2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi2.py) 3555 3556 ![](https://vedo.embl.es/images/advanced/voronoi2.png) 3557 """ 3558 pts = self.vertices 3559 3560 if method == "scipy": 3561 from scipy.spatial import Voronoi as scipy_voronoi 3562 3563 pts = np.asarray(pts)[:, (0, 1)] 3564 vor = scipy_voronoi(pts) 3565 regs = [] # filter out invalid indices 3566 for r in vor.regions: 3567 flag = True 3568 for x in r: 3569 if x < 0: 3570 flag = False 3571 break 3572 if flag and len(r) > 0: 3573 regs.append(r) 3574 3575 m = vedo.Mesh([vor.vertices, regs]) 3576 m.celldata["VoronoiID"] = np.array(list(range(len(regs)))).astype(int) 3577 3578 elif method == "vtk": 3579 vor = vtki.new("Voronoi2D") 3580 if isinstance(pts, Points): 3581 vor.SetInputData(pts) 3582 else: 3583 pts = np.asarray(pts) 3584 if pts.shape[1] == 2: 3585 pts = np.c_[pts, np.zeros(len(pts))] 3586 pd = vtki.vtkPolyData() 3587 vpts = vtki.vtkPoints() 3588 vpts.SetData(utils.numpy2vtk(pts, dtype=np.float32)) 3589 pd.SetPoints(vpts) 3590 vor.SetInputData(pd) 3591 vor.SetPadding(padding) 3592 vor.SetGenerateScalarsToPointIds() 3593 if fit: 3594 vor.SetProjectionPlaneModeToBestFittingPlane() 3595 else: 3596 vor.SetProjectionPlaneModeToXYPlane() 3597 vor.Update() 3598 poly = vor.GetOutput() 3599 arr = poly.GetCellData().GetArray(0) 3600 if arr: 3601 arr.SetName("VoronoiID") 3602 m = vedo.Mesh(poly, c="orange5") 3603 3604 else: 3605 vedo.logger.error(f"Unknown method {method} in voronoi()") 3606 raise RuntimeError 3607 3608 m.lw(2).lighting("off").wireframe() 3609 m.name = "Voronoi" 3610 return m 3611 3612 ########################################################################## 3613 def generate_delaunay3d(self, radius=0, tol=None) -> "vedo.TetMesh": 3614 """ 3615 Create 3D Delaunay triangulation of input points. 3616 3617 Arguments: 3618 radius : (float) 3619 specify distance (or "alpha") value to control output. 3620 For a non-zero values, only tetra contained within the circumsphere 3621 will be output. 3622 tol : (float) 3623 Specify a tolerance to control discarding of closely spaced points. 3624 This tolerance is specified as a fraction of the diagonal length of 3625 the bounding box of the points. 3626 """ 3627 deln = vtki.new("Delaunay3D") 3628 deln.SetInputData(self.dataset) 3629 deln.SetAlpha(radius) 3630 deln.AlphaTetsOn() 3631 deln.AlphaTrisOff() 3632 deln.AlphaLinesOff() 3633 deln.AlphaVertsOff() 3634 deln.BoundingTriangulationOff() 3635 if tol: 3636 deln.SetTolerance(tol) 3637 deln.Update() 3638 m = vedo.TetMesh(deln.GetOutput()) 3639 m.pipeline = utils.OperationNode( 3640 "generate_delaunay3d", c="#e9c46a:#edabab", parents=[self], 3641 ) 3642 m.name = "Delaunay3D" 3643 return m 3644 3645 #################################################### 3646 def visible_points(self, area=(), tol=None, invert=False) -> Union[Self, None]: 3647 """ 3648 Extract points based on whether they are visible or not. 3649 Visibility is determined by accessing the z-buffer of a rendering window. 3650 The position of each input point is converted into display coordinates, 3651 and then the z-value at that point is obtained. 3652 If within the user-specified tolerance, the point is considered visible. 3653 Associated data attributes are passed to the output as well. 3654 3655 This filter also allows you to specify a rectangular window in display (pixel) 3656 coordinates in which the visible points must lie. 3657 3658 Arguments: 3659 area : (list) 3660 specify a rectangular region as (xmin,xmax,ymin,ymax) 3661 tol : (float) 3662 a tolerance in normalized display coordinate system 3663 invert : (bool) 3664 select invisible points instead. 3665 3666 Example: 3667 ```python 3668 from vedo import Ellipsoid, show 3669 s = Ellipsoid().rotate_y(30) 3670 3671 # Camera options: pos, focal_point, viewup, distance 3672 camopts = dict(pos=(0,0,25), focal_point=(0,0,0)) 3673 show(s, camera=camopts, offscreen=True) 3674 3675 m = s.visible_points() 3676 # print('visible pts:', m.vertices) # numpy array 3677 show(m, new=True, axes=1).close() # optionally draw result in a new window 3678 ``` 3679 ![](https://vedo.embl.es/images/feats/visible_points.png) 3680 """ 3681 svp = vtki.new("SelectVisiblePoints") 3682 svp.SetInputData(self.dataset) 3683 3684 ren = None 3685 if vedo.plotter_instance: 3686 if vedo.plotter_instance.renderer: 3687 ren = vedo.plotter_instance.renderer 3688 svp.SetRenderer(ren) 3689 if not ren: 3690 vedo.logger.warning( 3691 "visible_points() can only be used after a rendering step" 3692 ) 3693 return None 3694 3695 if len(area) == 2: 3696 area = utils.flatten(area) 3697 if len(area) == 4: 3698 # specify a rectangular region 3699 svp.SetSelection(area[0], area[1], area[2], area[3]) 3700 if tol is not None: 3701 svp.SetTolerance(tol) 3702 if invert: 3703 svp.SelectInvisibleOn() 3704 svp.Update() 3705 3706 m = Points(svp.GetOutput()) 3707 m.name = "VisiblePoints" 3708 return m
Work with point clouds.
457 def __init__(self, inputobj=None, r=4, c=(0.2, 0.2, 0.2), alpha=1): 458 """ 459 Build an object made of only vertex points for a list of 2D/3D points. 460 Both shapes (N, 3) or (3, N) are accepted as input, if N>3. 461 462 Arguments: 463 inputobj : (list, tuple) 464 r : (int) 465 Point radius in units of pixels. 466 c : (str, list) 467 Color name or rgb tuple. 468 alpha : (float) 469 Transparency in range [0,1]. 470 471 Example: 472 ```python 473 from vedo import * 474 475 def fibonacci_sphere(n): 476 s = np.linspace(0, n, num=n, endpoint=False) 477 theta = s * 2.399963229728653 478 y = 1 - s * (2/(n-1)) 479 r = np.sqrt(1 - y * y) 480 x = np.cos(theta) * r 481 z = np.sin(theta) * r 482 return np._c[x,y,z] 483 484 Points(fibonacci_sphere(1000)).show(axes=1).close() 485 ``` 486 ![](https://vedo.embl.es/images/feats/fibonacci.png) 487 """ 488 # print("INIT POINTS") 489 super().__init__() 490 491 self.name = "" 492 self.filename = "" 493 self.file_size = "" 494 495 self.info = {} 496 self.time = time.time() 497 498 self.transform = LinearTransform() 499 self.point_locator = None 500 self.cell_locator = None 501 self.line_locator = None 502 503 self.actor = vtki.vtkActor() 504 self.properties = self.actor.GetProperty() 505 self.properties_backface = self.actor.GetBackfaceProperty() 506 self.mapper = vtki.new("PolyDataMapper") 507 self.dataset = vtki.vtkPolyData() 508 509 # Create weakref so actor can access this object (eg to pick/remove): 510 self.actor.retrieve_object = weak_ref_to(self) 511 512 try: 513 self.properties.RenderPointsAsSpheresOn() 514 except AttributeError: 515 pass 516 517 if inputobj is None: #################### 518 return 519 ########################################## 520 521 self.name = "Points" 522 523 ###### 524 if isinstance(inputobj, vtki.vtkActor): 525 self.dataset.DeepCopy(inputobj.GetMapper().GetInput()) 526 pr = vtki.vtkProperty() 527 pr.DeepCopy(inputobj.GetProperty()) 528 self.actor.SetProperty(pr) 529 self.properties = pr 530 self.mapper.SetScalarVisibility(inputobj.GetMapper().GetScalarVisibility()) 531 532 elif isinstance(inputobj, vtki.vtkPolyData): 533 self.dataset = inputobj 534 if self.dataset.GetNumberOfCells() == 0: 535 carr = vtki.vtkCellArray() 536 for i in range(self.dataset.GetNumberOfPoints()): 537 carr.InsertNextCell(1) 538 carr.InsertCellPoint(i) 539 self.dataset.SetVerts(carr) 540 541 elif isinstance(inputobj, Points): 542 self.dataset = inputobj.dataset 543 self.copy_properties_from(inputobj) 544 545 elif utils.is_sequence(inputobj): # passing point coords 546 self.dataset = utils.buildPolyData(utils.make3d(inputobj)) 547 548 elif isinstance(inputobj, str): 549 verts = vedo.file_io.load(inputobj) 550 self.filename = inputobj 551 self.dataset = verts.dataset 552 553 elif "meshlib" in str(type(inputobj)): 554 from meshlib import mrmeshnumpy as mn 555 self.dataset = utils.buildPolyData(mn.toNumpyArray(inputobj.points)) 556 557 else: 558 # try to extract the points from a generic VTK input data object 559 if hasattr(inputobj, "dataset"): 560 inputobj = inputobj.dataset 561 try: 562 vvpts = inputobj.GetPoints() 563 self.dataset = vtki.vtkPolyData() 564 self.dataset.SetPoints(vvpts) 565 for i in range(inputobj.GetPointData().GetNumberOfArrays()): 566 arr = inputobj.GetPointData().GetArray(i) 567 self.dataset.GetPointData().AddArray(arr) 568 carr = vtki.vtkCellArray() 569 for i in range(self.dataset.GetNumberOfPoints()): 570 carr.InsertNextCell(1) 571 carr.InsertCellPoint(i) 572 self.dataset.SetVerts(carr) 573 except: 574 vedo.logger.error(f"cannot build Points from type {type(inputobj)}") 575 raise RuntimeError() 576 577 self.actor.SetMapper(self.mapper) 578 self.mapper.SetInputData(self.dataset) 579 580 self.properties.SetColor(colors.get_color(c)) 581 self.properties.SetOpacity(alpha) 582 self.properties.SetRepresentationToPoints() 583 self.properties.SetPointSize(r) 584 self.properties.LightingOff() 585 586 self.pipeline = utils.OperationNode( 587 self, parents=[], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 588 )
Build an object made of only vertex points for a list of 2D/3D points. Both shapes (N, 3) or (3, N) are accepted as input, if N>3.
Arguments:
- inputobj : (list, tuple)
- r : (int) Point radius in units of pixels.
- c : (str, list) Color name or rgb tuple.
- alpha : (float) Transparency in range [0,1].
Example:
from vedo import * def fibonacci_sphere(n): s = np.linspace(0, n, num=n, endpoint=False) theta = s * 2.399963229728653 y = 1 - s * (2/(n-1)) r = np.sqrt(1 - y * y) x = np.cos(theta) * r z = np.sin(theta) * r return np._c[x,y,z] Points(fibonacci_sphere(1000)).show(axes=1).close()
810 def polydata(self, **kwargs): 811 """ 812 Obsolete. Use property `.dataset` instead. 813 Returns the underlying `vtkPolyData` object. 814 """ 815 colors.printc( 816 "WARNING: call to .polydata() is obsolete, use property .dataset instead.", 817 c="y") 818 return self.dataset
Obsolete. Use property .dataset
instead.
Returns the underlying vtkPolyData
object.
826 def copy(self, deep=True) -> Self: 827 """Return a copy of the object. Alias of `clone()`.""" 828 return self.clone(deep=deep)
Return a copy of the object. Alias of clone()
.
830 def clone(self, deep=True) -> Self: 831 """ 832 Clone a `PointCloud` or `Mesh` object to make an exact copy of it. 833 Alias of `copy()`. 834 835 Arguments: 836 deep : (bool) 837 if False return a shallow copy of the mesh without copying the points array. 838 839 Examples: 840 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 841 842 ![](https://vedo.embl.es/images/basic/mirror.png) 843 """ 844 poly = vtki.vtkPolyData() 845 if deep or isinstance(deep, dict): # if a memo object is passed this checks as True 846 poly.DeepCopy(self.dataset) 847 else: 848 poly.ShallowCopy(self.dataset) 849 850 if isinstance(self, vedo.Mesh): 851 cloned = vedo.Mesh(poly) 852 else: 853 cloned = Points(poly) 854 # print([self], self.__class__) 855 # cloned = self.__class__(poly) 856 857 cloned.transform = self.transform.clone() 858 859 cloned.copy_properties_from(self) 860 861 cloned.name = str(self.name) 862 cloned.filename = str(self.filename) 863 cloned.info = dict(self.info) 864 cloned.pipeline = utils.OperationNode("clone", parents=[self], shape="diamond", c="#edede9") 865 866 if isinstance(deep, dict): 867 deep[id(self)] = cloned 868 869 return cloned
871 def compute_normals_with_pca(self, n=20, orientation_point=None, invert=False) -> Self: 872 """ 873 Generate point normals using PCA (principal component analysis). 874 This algorithm estimates a local tangent plane around each sample point p 875 by considering a small neighborhood of points around p, and fitting a plane 876 to the neighborhood (via PCA). 877 878 Arguments: 879 n : (int) 880 neighborhood size to calculate the normal 881 orientation_point : (list) 882 adjust the +/- sign of the normals so that 883 the normals all point towards a specified point. If None, perform a traversal 884 of the point cloud and flip neighboring normals so that they are mutually consistent. 885 invert : (bool) 886 flip all normals 887 """ 888 poly = self.dataset 889 pcan = vtki.new("PCANormalEstimation") 890 pcan.SetInputData(poly) 891 pcan.SetSampleSize(n) 892 893 if orientation_point is not None: 894 pcan.SetNormalOrientationToPoint() 895 pcan.SetOrientationPoint(orientation_point) 896 else: 897 pcan.SetNormalOrientationToGraphTraversal() 898 899 if invert: 900 pcan.FlipNormalsOn() 901 pcan.Update() 902 903 varr = pcan.GetOutput().GetPointData().GetNormals() 904 varr.SetName("Normals") 905 self.dataset.GetPointData().SetNormals(varr) 906 self.dataset.GetPointData().Modified() 907 return self
Generate point normals using PCA (principal component analysis). This algorithm estimates a local tangent plane around each sample point p by considering a small neighborhood of points around p, and fitting a plane to the neighborhood (via PCA).
Arguments:
- n : (int) neighborhood size to calculate the normal
- orientation_point : (list) adjust the +/- sign of the normals so that the normals all point towards a specified point. If None, perform a traversal of the point cloud and flip neighboring normals so that they are mutually consistent.
- invert : (bool) flip all normals
909 def compute_acoplanarity(self, n=25, radius=None, on="points") -> Self: 910 """ 911 Compute acoplanarity which is a measure of how much a local region of the mesh 912 differs from a plane. 913 914 The information is stored in a `pointdata` or `celldata` array with name 'Acoplanarity'. 915 916 Either `n` (number of neighbour points) or `radius` (radius of local search) can be specified. 917 If a radius value is given and not enough points fall inside it, then a -1 is stored. 918 919 Example: 920 ```python 921 from vedo import * 922 msh = ParametricShape('RandomHills') 923 msh.compute_acoplanarity(radius=0.1, on='cells') 924 msh.cmap("coolwarm", on='cells').add_scalarbar() 925 msh.show(axes=1).close() 926 ``` 927 ![](https://vedo.embl.es/images/feats/acoplanarity.jpg) 928 """ 929 acoplanarities = [] 930 if "point" in on: 931 pts = self.vertices 932 elif "cell" in on: 933 pts = self.cell_centers 934 else: 935 raise ValueError(f"In compute_acoplanarity() set on to either 'cells' or 'points', not {on}") 936 937 for p in utils.progressbar(pts, delay=5, width=15, title=f"{on} acoplanarity"): 938 if n: 939 data = self.closest_point(p, n=n) 940 npts = n 941 elif radius: 942 data = self.closest_point(p, radius=radius) 943 npts = len(data) 944 945 try: 946 center = data.mean(axis=0) 947 res = np.linalg.svd(data - center) 948 acoplanarities.append(res[1][2] / npts) 949 except: 950 acoplanarities.append(-1.0) 951 952 if "point" in on: 953 self.pointdata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 954 else: 955 self.celldata["Acoplanarity"] = np.array(acoplanarities, dtype=float) 956 return self
Compute acoplanarity which is a measure of how much a local region of the mesh differs from a plane.
The information is stored in a pointdata
or celldata
array with name 'Acoplanarity'.
Either n
(number of neighbour points) or radius
(radius of local search) can be specified.
If a radius value is given and not enough points fall inside it, then a -1 is stored.
Example:
from vedo import * msh = ParametricShape('RandomHills') msh.compute_acoplanarity(radius=0.1, on='cells') msh.cmap("coolwarm", on='cells').add_scalarbar() msh.show(axes=1).close()
958 def distance_to(self, pcloud, signed=False, invert=False, name="Distance") -> np.ndarray: 959 """ 960 Computes the distance from one point cloud or mesh to another point cloud or mesh. 961 This new `pointdata` array is saved with default name "Distance". 962 963 Keywords `signed` and `invert` are used to compute signed distance, 964 but the mesh in that case must have polygonal faces (not a simple point cloud), 965 and normals must also be computed. 966 967 Examples: 968 - [distance2mesh.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/distance2mesh.py) 969 970 ![](https://vedo.embl.es/images/basic/distance2mesh.png) 971 """ 972 if pcloud.dataset.GetNumberOfPolys(): 973 974 poly1 = self.dataset 975 poly2 = pcloud.dataset 976 df = vtki.new("DistancePolyDataFilter") 977 df.ComputeSecondDistanceOff() 978 df.SetInputData(0, poly1) 979 df.SetInputData(1, poly2) 980 df.SetSignedDistance(signed) 981 df.SetNegateDistance(invert) 982 df.Update() 983 scals = df.GetOutput().GetPointData().GetScalars() 984 dists = utils.vtk2numpy(scals) 985 986 else: # has no polygons 987 988 if signed: 989 vedo.logger.warning("distance_to() called with signed=True but input object has no polygons") 990 991 if not pcloud.point_locator: 992 pcloud.point_locator = vtki.new("PointLocator") 993 pcloud.point_locator.SetDataSet(pcloud.dataset) 994 pcloud.point_locator.BuildLocator() 995 996 ids = [] 997 ps1 = self.vertices 998 ps2 = pcloud.vertices 999 for p in ps1: 1000 pid = pcloud.point_locator.FindClosestPoint(p) 1001 ids.append(pid) 1002 1003 deltas = ps2[ids] - ps1 1004 dists = np.linalg.norm(deltas, axis=1).astype(np.float32) 1005 scals = utils.numpy2vtk(dists) 1006 1007 scals.SetName(name) 1008 self.dataset.GetPointData().AddArray(scals) 1009 self.dataset.GetPointData().SetActiveScalars(scals.GetName()) 1010 rng = scals.GetRange() 1011 self.mapper.SetScalarRange(rng[0], rng[1]) 1012 self.mapper.ScalarVisibilityOn() 1013 1014 self.pipeline = utils.OperationNode( 1015 "distance_to", 1016 parents=[self, pcloud], 1017 shape="cylinder", 1018 comment=f"#pts {self.dataset.GetNumberOfPoints()}", 1019 ) 1020 return dists
Computes the distance from one point cloud or mesh to another point cloud or mesh.
This new pointdata
array is saved with default name "Distance".
Keywords signed
and invert
are used to compute signed distance,
but the mesh in that case must have polygonal faces (not a simple point cloud),
and normals must also be computed.
Examples:
1022 def clean(self) -> Self: 1023 """Clean pointcloud or mesh by removing coincident points.""" 1024 cpd = vtki.new("CleanPolyData") 1025 cpd.PointMergingOn() 1026 cpd.ConvertLinesToPointsOff() 1027 cpd.ConvertPolysToLinesOff() 1028 cpd.ConvertStripsToPolysOff() 1029 cpd.SetInputData(self.dataset) 1030 cpd.Update() 1031 self._update(cpd.GetOutput()) 1032 self.pipeline = utils.OperationNode( 1033 "clean", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1034 ) 1035 return self
Clean pointcloud or mesh by removing coincident points.
1037 def subsample(self, fraction: float, absolute=False) -> Self: 1038 """ 1039 Subsample a point cloud by requiring that the points 1040 or vertices are far apart at least by the specified fraction of the object size. 1041 If a Mesh is passed the polygonal faces are not removed 1042 but holes can appear as their vertices are removed. 1043 1044 Examples: 1045 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1046 1047 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1048 1049 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1050 1051 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1052 """ 1053 if not absolute: 1054 if fraction > 1: 1055 vedo.logger.warning( 1056 f"subsample(fraction=...), fraction must be < 1, but is {fraction}" 1057 ) 1058 if fraction <= 0: 1059 return self 1060 1061 cpd = vtki.new("CleanPolyData") 1062 cpd.PointMergingOn() 1063 cpd.ConvertLinesToPointsOn() 1064 cpd.ConvertPolysToLinesOn() 1065 cpd.ConvertStripsToPolysOn() 1066 cpd.SetInputData(self.dataset) 1067 if absolute: 1068 cpd.SetTolerance(fraction / self.diagonal_size()) 1069 # cpd.SetToleranceIsAbsolute(absolute) 1070 else: 1071 cpd.SetTolerance(fraction) 1072 cpd.Update() 1073 1074 ps = 2 1075 if self.properties.GetRepresentation() == 0: 1076 ps = self.properties.GetPointSize() 1077 1078 self._update(cpd.GetOutput()) 1079 self.ps(ps) 1080 1081 self.pipeline = utils.OperationNode( 1082 "subsample", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 1083 ) 1084 return self
Subsample a point cloud by requiring that the points or vertices are far apart at least by the specified fraction of the object size. If a Mesh is passed the polygonal faces are not removed but holes can appear as their vertices are removed.
Examples:
1086 def threshold(self, scalars: str, above=None, below=None, on="points") -> Self: 1087 """ 1088 Extracts cells where scalar value satisfies threshold criterion. 1089 1090 Arguments: 1091 scalars : (str) 1092 name of the scalars array. 1093 above : (float) 1094 minimum value of the scalar 1095 below : (float) 1096 maximum value of the scalar 1097 on : (str) 1098 if 'cells' assume array of scalars refers to cell data. 1099 1100 Examples: 1101 - [mesh_threshold.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mesh_threshold.py) 1102 """ 1103 thres = vtki.new("Threshold") 1104 thres.SetInputData(self.dataset) 1105 1106 if on.startswith("c"): 1107 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_CELLS 1108 else: 1109 asso = vtki.vtkDataObject.FIELD_ASSOCIATION_POINTS 1110 1111 thres.SetInputArrayToProcess(0, 0, 0, asso, scalars) 1112 1113 if above is None and below is not None: 1114 try: # vtk 9.2 1115 thres.ThresholdByLower(below) 1116 except AttributeError: # vtk 9.3 1117 thres.SetUpperThreshold(below) 1118 1119 elif below is None and above is not None: 1120 try: 1121 thres.ThresholdByUpper(above) 1122 except AttributeError: 1123 thres.SetLowerThreshold(above) 1124 else: 1125 try: 1126 thres.ThresholdBetween(above, below) 1127 except AttributeError: 1128 thres.SetUpperThreshold(below) 1129 thres.SetLowerThreshold(above) 1130 1131 thres.Update() 1132 1133 gf = vtki.new("GeometryFilter") 1134 gf.SetInputData(thres.GetOutput()) 1135 gf.Update() 1136 self._update(gf.GetOutput()) 1137 self.pipeline = utils.OperationNode("threshold", parents=[self]) 1138 return self
Extracts cells where scalar value satisfies threshold criterion.
Arguments:
- scalars : (str) name of the scalars array.
- above : (float) minimum value of the scalar
- below : (float) maximum value of the scalar
- on : (str) if 'cells' assume array of scalars refers to cell data.
Examples:
1140 def quantize(self, value: float) -> Self: 1141 """ 1142 The user should input a value and all {x,y,z} coordinates 1143 will be quantized to that absolute grain size. 1144 """ 1145 qp = vtki.new("QuantizePolyDataPoints") 1146 qp.SetInputData(self.dataset) 1147 qp.SetQFactor(value) 1148 qp.Update() 1149 self._update(qp.GetOutput()) 1150 self.pipeline = utils.OperationNode("quantize", parents=[self]) 1151 return self
The user should input a value and all {x,y,z} coordinates will be quantized to that absolute grain size.
1153 @property 1154 def vertex_normals(self) -> np.ndarray: 1155 """ 1156 Retrieve vertex normals as a numpy array. Same as `point_normals`. 1157 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1158 """ 1159 vtknormals = self.dataset.GetPointData().GetNormals() 1160 return utils.vtk2numpy(vtknormals)
Retrieve vertex normals as a numpy array. Same as point_normals
.
Check out also compute_normals()
and compute_normals_with_pca()
.
1162 @property 1163 def point_normals(self) -> np.ndarray: 1164 """ 1165 Retrieve vertex normals as a numpy array. Same as `vertex_normals`. 1166 Check out also `compute_normals()` and `compute_normals_with_pca()`. 1167 """ 1168 vtknormals = self.dataset.GetPointData().GetNormals() 1169 return utils.vtk2numpy(vtknormals)
Retrieve vertex normals as a numpy array. Same as vertex_normals
.
Check out also compute_normals()
and compute_normals_with_pca()
.
1171 def align_to(self, target, iters=100, rigid=False, invert=False, use_centroids=False) -> Self: 1172 """ 1173 Aligned to target mesh through the `Iterative Closest Point` algorithm. 1174 1175 The core of the algorithm is to match each vertex in one surface with 1176 the closest surface point on the other, then apply the transformation 1177 that modify one surface to best match the other (in the least-square sense). 1178 1179 Arguments: 1180 rigid : (bool) 1181 if True do not allow scaling 1182 invert : (bool) 1183 if True start by aligning the target to the source but 1184 invert the transformation finally. Useful when the target is smaller 1185 than the source. 1186 use_centroids : (bool) 1187 start by matching the centroids of the two objects. 1188 1189 Examples: 1190 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1191 1192 ![](https://vedo.embl.es/images/basic/align1.png) 1193 1194 - [align2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align2.py) 1195 1196 ![](https://vedo.embl.es/images/basic/align2.png) 1197 """ 1198 icp = vtki.new("IterativeClosestPointTransform") 1199 icp.SetSource(self.dataset) 1200 icp.SetTarget(target.dataset) 1201 if invert: 1202 icp.Inverse() 1203 icp.SetMaximumNumberOfIterations(iters) 1204 if rigid: 1205 icp.GetLandmarkTransform().SetModeToRigidBody() 1206 icp.SetStartByMatchingCentroids(use_centroids) 1207 icp.Update() 1208 1209 self.apply_transform(icp.GetMatrix()) 1210 1211 self.pipeline = utils.OperationNode( 1212 "align_to", parents=[self, target], comment=f"rigid = {rigid}" 1213 ) 1214 return self
Aligned to target mesh through the Iterative Closest Point
algorithm.
The core of the algorithm is to match each vertex in one surface with the closest surface point on the other, then apply the transformation that modify one surface to best match the other (in the least-square sense).
Arguments:
- rigid : (bool) if True do not allow scaling
- invert : (bool) if True start by aligning the target to the source but invert the transformation finally. Useful when the target is smaller than the source.
- use_centroids : (bool) start by matching the centroids of the two objects.
Examples:
1216 def align_to_bounding_box(self, msh, rigid=False) -> Self: 1217 """ 1218 Align the current object's bounding box to the bounding box 1219 of the input object. 1220 1221 Use `rigid=True` to disable scaling. 1222 1223 Example: 1224 [align6.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align6.py) 1225 """ 1226 lmt = vtki.vtkLandmarkTransform() 1227 ss = vtki.vtkPoints() 1228 xss0, xss1, yss0, yss1, zss0, zss1 = self.bounds() 1229 for p in [ 1230 [xss0, yss0, zss0], 1231 [xss1, yss0, zss0], 1232 [xss1, yss1, zss0], 1233 [xss0, yss1, zss0], 1234 [xss0, yss0, zss1], 1235 [xss1, yss0, zss1], 1236 [xss1, yss1, zss1], 1237 [xss0, yss1, zss1], 1238 ]: 1239 ss.InsertNextPoint(p) 1240 st = vtki.vtkPoints() 1241 xst0, xst1, yst0, yst1, zst0, zst1 = msh.bounds() 1242 for p in [ 1243 [xst0, yst0, zst0], 1244 [xst1, yst0, zst0], 1245 [xst1, yst1, zst0], 1246 [xst0, yst1, zst0], 1247 [xst0, yst0, zst1], 1248 [xst1, yst0, zst1], 1249 [xst1, yst1, zst1], 1250 [xst0, yst1, zst1], 1251 ]: 1252 st.InsertNextPoint(p) 1253 1254 lmt.SetSourceLandmarks(ss) 1255 lmt.SetTargetLandmarks(st) 1256 lmt.SetModeToAffine() 1257 if rigid: 1258 lmt.SetModeToRigidBody() 1259 lmt.Update() 1260 1261 LT = LinearTransform(lmt) 1262 self.apply_transform(LT) 1263 return self
Align the current object's bounding box to the bounding box of the input object.
Use rigid=True
to disable scaling.
Example:
1265 def align_with_landmarks( 1266 self, 1267 source_landmarks, 1268 target_landmarks, 1269 rigid=False, 1270 affine=False, 1271 least_squares=False, 1272 ) -> Self: 1273 """ 1274 Transform mesh orientation and position based on a set of landmarks points. 1275 The algorithm finds the best matching of source points to target points 1276 in the mean least square sense, in one single step. 1277 1278 If `affine` is True the x, y and z axes can scale independently but stay collinear. 1279 With least_squares they can vary orientation. 1280 1281 Examples: 1282 - [align5.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align5.py) 1283 1284 ![](https://vedo.embl.es/images/basic/align5.png) 1285 """ 1286 1287 if utils.is_sequence(source_landmarks): 1288 ss = vtki.vtkPoints() 1289 for p in source_landmarks: 1290 ss.InsertNextPoint(p) 1291 else: 1292 ss = source_landmarks.dataset.GetPoints() 1293 if least_squares: 1294 source_landmarks = source_landmarks.vertices 1295 1296 if utils.is_sequence(target_landmarks): 1297 st = vtki.vtkPoints() 1298 for p in target_landmarks: 1299 st.InsertNextPoint(p) 1300 else: 1301 st = target_landmarks.GetPoints() 1302 if least_squares: 1303 target_landmarks = target_landmarks.vertices 1304 1305 if ss.GetNumberOfPoints() != st.GetNumberOfPoints(): 1306 n1 = ss.GetNumberOfPoints() 1307 n2 = st.GetNumberOfPoints() 1308 vedo.logger.error(f"source and target have different nr of points {n1} vs {n2}") 1309 raise RuntimeError() 1310 1311 if int(rigid) + int(affine) + int(least_squares) > 1: 1312 vedo.logger.error( 1313 "only one of rigid, affine, least_squares can be True at a time" 1314 ) 1315 raise RuntimeError() 1316 1317 lmt = vtki.vtkLandmarkTransform() 1318 lmt.SetSourceLandmarks(ss) 1319 lmt.SetTargetLandmarks(st) 1320 lmt.SetModeToSimilarity() 1321 1322 if rigid: 1323 lmt.SetModeToRigidBody() 1324 lmt.Update() 1325 1326 elif affine: 1327 lmt.SetModeToAffine() 1328 lmt.Update() 1329 1330 elif least_squares: 1331 cms = source_landmarks.mean(axis=0) 1332 cmt = target_landmarks.mean(axis=0) 1333 m = np.linalg.lstsq(source_landmarks - cms, target_landmarks - cmt, rcond=None)[0] 1334 M = vtki.vtkMatrix4x4() 1335 for i in range(3): 1336 for j in range(3): 1337 M.SetElement(j, i, m[i][j]) 1338 lmt = vtki.vtkTransform() 1339 lmt.Translate(cmt) 1340 lmt.Concatenate(M) 1341 lmt.Translate(-cms) 1342 1343 else: 1344 lmt.Update() 1345 1346 self.apply_transform(lmt) 1347 self.pipeline = utils.OperationNode("transform_with_landmarks", parents=[self]) 1348 return self
Transform mesh orientation and position based on a set of landmarks points. The algorithm finds the best matching of source points to target points in the mean least square sense, in one single step.
If affine
is True the x, y and z axes can scale independently but stay collinear.
With least_squares they can vary orientation.
Examples:
1350 def normalize(self) -> Self: 1351 """Scale average size to unit. The scaling is performed around the center of mass.""" 1352 coords = self.vertices 1353 if not coords.shape[0]: 1354 return self 1355 cm = np.mean(coords, axis=0) 1356 pts = coords - cm 1357 xyz2 = np.sum(pts * pts, axis=0) 1358 scale = 1 / np.sqrt(np.sum(xyz2) / len(pts)) 1359 self.scale(scale, origin=cm) 1360 self.pipeline = utils.OperationNode("normalize", parents=[self]) 1361 return self
Scale average size to unit. The scaling is performed around the center of mass.
1363 def mirror(self, axis="x", origin=True) -> Self: 1364 """ 1365 Mirror reflect along one of the cartesian axes 1366 1367 Arguments: 1368 axis : (str) 1369 axis to use for mirroring, must be set to `x, y, z`. 1370 Or any combination of those. 1371 origin : (list) 1372 use this point as the origin of the mirroring transformation. 1373 1374 Examples: 1375 - [mirror.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/mirror.py) 1376 1377 ![](https://vedo.embl.es/images/basic/mirror.png) 1378 """ 1379 sx, sy, sz = 1, 1, 1 1380 if "x" in axis.lower(): sx = -1 1381 if "y" in axis.lower(): sy = -1 1382 if "z" in axis.lower(): sz = -1 1383 1384 self.scale([sx, sy, sz], origin=origin) 1385 1386 self.pipeline = utils.OperationNode( 1387 "mirror", comment=f"axis = {axis}", parents=[self]) 1388 1389 if sx * sy * sz < 0: 1390 if hasattr(self, "reverse"): 1391 self.reverse() 1392 return self
Mirror reflect along one of the cartesian axes
Arguments:
- axis : (str)
axis to use for mirroring, must be set to
x, y, z
. Or any combination of those. - origin : (list) use this point as the origin of the mirroring transformation.
Examples:
1394 def flip_normals(self) -> Self: 1395 """Flip all normals orientation.""" 1396 rs = vtki.new("ReverseSense") 1397 rs.SetInputData(self.dataset) 1398 rs.ReverseCellsOff() 1399 rs.ReverseNormalsOn() 1400 rs.Update() 1401 self._update(rs.GetOutput()) 1402 self.pipeline = utils.OperationNode("flip_normals", parents=[self]) 1403 return self
Flip all normals orientation.
1405 def add_gaussian_noise(self, sigma=1.0) -> Self: 1406 """ 1407 Add gaussian noise to point positions. 1408 An extra array is added named "GaussianNoise" with the displacements. 1409 1410 Arguments: 1411 sigma : (float) 1412 nr. of standard deviations, expressed in percent of the diagonal size of mesh. 1413 Can also be a list `[sigma_x, sigma_y, sigma_z]`. 1414 1415 Example: 1416 ```python 1417 from vedo import Sphere 1418 Sphere().add_gaussian_noise(1.0).point_size(8).show().close() 1419 ``` 1420 """ 1421 sz = self.diagonal_size() 1422 pts = self.vertices 1423 n = len(pts) 1424 ns = (np.random.randn(n, 3) * sigma) * (sz / 100) 1425 vpts = vtki.vtkPoints() 1426 vpts.SetNumberOfPoints(n) 1427 vpts.SetData(utils.numpy2vtk(pts + ns, dtype=np.float32)) 1428 self.dataset.SetPoints(vpts) 1429 self.dataset.GetPoints().Modified() 1430 self.pointdata["GaussianNoise"] = -ns 1431 self.pipeline = utils.OperationNode( 1432 "gaussian_noise", parents=[self], shape="egg", comment=f"sigma = {sigma}" 1433 ) 1434 return self
Add gaussian noise to point positions. An extra array is added named "GaussianNoise" with the displacements.
Arguments:
- sigma : (float)
nr. of standard deviations, expressed in percent of the diagonal size of mesh.
Can also be a list
[sigma_x, sigma_y, sigma_z]
.
Example:
from vedo import Sphere Sphere().add_gaussian_noise(1.0).point_size(8).show().close()
1436 def closest_point( 1437 self, pt, n=1, radius=None, return_point_id=False, return_cell_id=False 1438 ) -> Union[List[int], int, np.ndarray]: 1439 """ 1440 Find the closest point(s) on a mesh given from the input point `pt`. 1441 1442 Arguments: 1443 n : (int) 1444 if greater than 1, return a list of n ordered closest points 1445 radius : (float) 1446 if given, get all points within that radius. Then n is ignored. 1447 return_point_id : (bool) 1448 return point ID instead of coordinates 1449 return_cell_id : (bool) 1450 return cell ID in which the closest point sits 1451 1452 Examples: 1453 - [align1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align1.py) 1454 - [fitplanes.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitplanes.py) 1455 - [quadratic_morphing.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/quadratic_morphing.py) 1456 1457 .. note:: 1458 The appropriate tree search locator is built on the fly and cached for speed. 1459 1460 If you want to reset it use `mymesh.point_locator=None` 1461 and / or `mymesh.cell_locator=None`. 1462 """ 1463 if len(pt) != 3: 1464 pt = [pt[0], pt[1], 0] 1465 1466 # NB: every time the mesh moves or is warped the locators are set to None 1467 if ((n > 1 or radius) or (n == 1 and return_point_id)) and not return_cell_id: 1468 poly = None 1469 if not self.point_locator: 1470 poly = self.dataset 1471 self.point_locator = vtki.new("StaticPointLocator") 1472 self.point_locator.SetDataSet(poly) 1473 self.point_locator.BuildLocator() 1474 1475 ########## 1476 if radius: 1477 vtklist = vtki.vtkIdList() 1478 self.point_locator.FindPointsWithinRadius(radius, pt, vtklist) 1479 elif n > 1: 1480 vtklist = vtki.vtkIdList() 1481 self.point_locator.FindClosestNPoints(n, pt, vtklist) 1482 else: # n==1 hence return_point_id==True 1483 ######## 1484 return self.point_locator.FindClosestPoint(pt) 1485 ######## 1486 1487 if return_point_id: 1488 ######## 1489 return utils.vtk2numpy(vtklist) 1490 ######## 1491 1492 if not poly: 1493 poly = self.dataset 1494 trgp = [] 1495 for i in range(vtklist.GetNumberOfIds()): 1496 trgp_ = [0, 0, 0] 1497 vi = vtklist.GetId(i) 1498 poly.GetPoints().GetPoint(vi, trgp_) 1499 trgp.append(trgp_) 1500 ######## 1501 return np.array(trgp) 1502 ######## 1503 1504 else: 1505 1506 if not self.cell_locator: 1507 poly = self.dataset 1508 1509 # As per Miquel example with limbs the vtkStaticCellLocator doesnt work !! 1510 # https://discourse.vtk.org/t/vtkstaticcelllocator-problem-vtk9-0-3/7854/4 1511 if vedo.vtk_version[0] >= 9 and vedo.vtk_version[1] > 0: 1512 self.cell_locator = vtki.new("StaticCellLocator") 1513 else: 1514 self.cell_locator = vtki.new("CellLocator") 1515 1516 self.cell_locator.SetDataSet(poly) 1517 self.cell_locator.BuildLocator() 1518 1519 if radius is not None: 1520 vedo.printc("Warning: closest_point() with radius is not implemented for cells.", c='r') 1521 1522 if n != 1: 1523 vedo.printc("Warning: closest_point() with n>1 is not implemented for cells.", c='r') 1524 1525 trgp = [0, 0, 0] 1526 cid = vtki.mutable(0) 1527 dist2 = vtki.mutable(0) 1528 subid = vtki.mutable(0) 1529 self.cell_locator.FindClosestPoint(pt, trgp, cid, subid, dist2) 1530 1531 if return_cell_id: 1532 return int(cid) 1533 1534 return np.array(trgp)
Find the closest point(s) on a mesh given from the input point pt
.
Arguments:
- n : (int) if greater than 1, return a list of n ordered closest points
- radius : (float) if given, get all points within that radius. Then n is ignored.
- return_point_id : (bool) return point ID instead of coordinates
- return_cell_id : (bool) return cell ID in which the closest point sits
Examples:
The appropriate tree search locator is built on the fly and cached for speed.
If you want to reset it use mymesh.point_locator=None
and / or mymesh.cell_locator=None
.
1536 def auto_distance(self) -> np.ndarray: 1537 """ 1538 Calculate the distance to the closest point in the same cloud of points. 1539 The output is stored in a new pointdata array called "AutoDistance", 1540 and it is also returned by the function. 1541 """ 1542 points = self.vertices 1543 if not self.point_locator: 1544 self.point_locator = vtki.new("StaticPointLocator") 1545 self.point_locator.SetDataSet(self.dataset) 1546 self.point_locator.BuildLocator() 1547 qs = [] 1548 vtklist = vtki.vtkIdList() 1549 vtkpoints = self.dataset.GetPoints() 1550 for p in points: 1551 self.point_locator.FindClosestNPoints(2, p, vtklist) 1552 q = [0, 0, 0] 1553 pid = vtklist.GetId(1) 1554 vtkpoints.GetPoint(pid, q) 1555 qs.append(q) 1556 dists = np.linalg.norm(points - np.array(qs), axis=1) 1557 self.pointdata["AutoDistance"] = dists 1558 return dists
Calculate the distance to the closest point in the same cloud of points. The output is stored in a new pointdata array called "AutoDistance", and it is also returned by the function.
1560 def hausdorff_distance(self, points) -> float: 1561 """ 1562 Compute the Hausdorff distance to the input point set. 1563 Returns a single `float`. 1564 1565 Example: 1566 ```python 1567 from vedo import * 1568 t = np.linspace(0, 2*np.pi, 100) 1569 x = 4/3 * sin(t)**3 1570 y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12 1571 pol1 = Line(np.c_[x,y], closed=True).triangulate() 1572 pol2 = Polygon(nsides=5).pos(2,2) 1573 d12 = pol1.distance_to(pol2) 1574 d21 = pol2.distance_to(pol1) 1575 pol1.lw(0).cmap("viridis") 1576 pol2.lw(0).cmap("viridis") 1577 print("distance d12, d21 :", min(d12), min(d21)) 1578 print("hausdorff distance:", pol1.hausdorff_distance(pol2)) 1579 print("chamfer distance :", pol1.chamfer_distance(pol2)) 1580 show(pol1, pol2, axes=1) 1581 ``` 1582 ![](https://vedo.embl.es/images/feats/heart.png) 1583 """ 1584 hp = vtki.new("HausdorffDistancePointSetFilter") 1585 hp.SetInputData(0, self.dataset) 1586 hp.SetInputData(1, points.dataset) 1587 hp.SetTargetDistanceMethodToPointToCell() 1588 hp.Update() 1589 return hp.GetHausdorffDistance()
Compute the Hausdorff distance to the input point set.
Returns a single float
.
Example:
from vedo import * t = np.linspace(0, 2*np.pi, 100) x = 4/3 * sin(t)**3 y = cos(t) - cos(2*t)/3 - cos(3*t)/6 - cos(4*t)/12 pol1 = Line(np.c_[x,y], closed=True).triangulate() pol2 = Polygon(nsides=5).pos(2,2) d12 = pol1.distance_to(pol2) d21 = pol2.distance_to(pol1) pol1.lw(0).cmap("viridis") pol2.lw(0).cmap("viridis") print("distance d12, d21 :", min(d12), min(d21)) print("hausdorff distance:", pol1.hausdorff_distance(pol2)) print("chamfer distance :", pol1.chamfer_distance(pol2)) show(pol1, pol2, axes=1)
1591 def chamfer_distance(self, pcloud) -> float: 1592 """ 1593 Compute the Chamfer distance to the input point set. 1594 1595 Example: 1596 ```python 1597 from vedo import * 1598 cloud1 = np.random.randn(1000, 3) 1599 cloud2 = np.random.randn(1000, 3) + [1, 2, 3] 1600 c1 = Points(cloud1, r=5, c="red") 1601 c2 = Points(cloud2, r=5, c="green") 1602 d = c1.chamfer_distance(c2) 1603 show(f"Chamfer distance = {d}", c1, c2, axes=1).close() 1604 ``` 1605 """ 1606 # Definition of Chamfer distance may vary, here we use the average 1607 if not pcloud.point_locator: 1608 pcloud.point_locator = vtki.new("PointLocator") 1609 pcloud.point_locator.SetDataSet(pcloud.dataset) 1610 pcloud.point_locator.BuildLocator() 1611 if not self.point_locator: 1612 self.point_locator = vtki.new("PointLocator") 1613 self.point_locator.SetDataSet(self.dataset) 1614 self.point_locator.BuildLocator() 1615 1616 ps1 = self.vertices 1617 ps2 = pcloud.vertices 1618 1619 ids12 = [] 1620 for p in ps1: 1621 pid12 = pcloud.point_locator.FindClosestPoint(p) 1622 ids12.append(pid12) 1623 deltav = ps2[ids12] - ps1 1624 da = np.mean(np.linalg.norm(deltav, axis=1)) 1625 1626 ids21 = [] 1627 for p in ps2: 1628 pid21 = self.point_locator.FindClosestPoint(p) 1629 ids21.append(pid21) 1630 deltav = ps1[ids21] - ps2 1631 db = np.mean(np.linalg.norm(deltav, axis=1)) 1632 return (da + db) / 2
Compute the Chamfer distance to the input point set.
Example:
from vedo import * cloud1 = np.random.randn(1000, 3) cloud2 = np.random.randn(1000, 3) + [1, 2, 3] c1 = Points(cloud1, r=5, c="red") c2 = Points(cloud2, r=5, c="green") d = c1.chamfer_distance(c2) show(f"Chamfer distance = {d}", c1, c2, axes=1).close()
1634 def remove_outliers(self, radius: float, neighbors=5) -> Self: 1635 """ 1636 Remove outliers from a cloud of points within the specified `radius` search. 1637 1638 Arguments: 1639 radius : (float) 1640 Specify the local search radius. 1641 neighbors : (int) 1642 Specify the number of neighbors that a point must have, 1643 within the specified radius, for the point to not be considered isolated. 1644 1645 Examples: 1646 - [clustering.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/clustering.py) 1647 1648 ![](https://vedo.embl.es/images/basic/clustering.png) 1649 """ 1650 removal = vtki.new("RadiusOutlierRemoval") 1651 removal.SetInputData(self.dataset) 1652 removal.SetRadius(radius) 1653 removal.SetNumberOfNeighbors(neighbors) 1654 removal.GenerateOutliersOff() 1655 removal.Update() 1656 inputobj = removal.GetOutput() 1657 if inputobj.GetNumberOfCells() == 0: 1658 carr = vtki.vtkCellArray() 1659 for i in range(inputobj.GetNumberOfPoints()): 1660 carr.InsertNextCell(1) 1661 carr.InsertCellPoint(i) 1662 inputobj.SetVerts(carr) 1663 self._update(removal.GetOutput()) 1664 self.pipeline = utils.OperationNode("remove_outliers", parents=[self]) 1665 return self
Remove outliers from a cloud of points within the specified radius
search.
Arguments:
- radius : (float) Specify the local search radius.
- neighbors : (int) Specify the number of neighbors that a point must have, within the specified radius, for the point to not be considered isolated.
Examples:
1667 def relax_point_positions( 1668 self, 1669 n=10, 1670 iters=10, 1671 sub_iters=10, 1672 packing_factor=1, 1673 max_step=0, 1674 constraints=(), 1675 ) -> Self: 1676 """ 1677 Smooth mesh or points with a 1678 [Laplacian algorithm](https://vtk.org/doc/nightly/html/classvtkPointSmoothingFilter.html) 1679 variant. This modifies the coordinates of the input points by adjusting their positions 1680 to create a smooth distribution (and thereby form a pleasing packing of the points). 1681 Smoothing is performed by considering the effects of neighboring points on one another 1682 it uses a cubic cutoff function to produce repulsive forces between close points 1683 and attractive forces that are a little further away. 1684 1685 In general, the larger the neighborhood size, the greater the reduction in high frequency 1686 information. The memory and computational requirements of the algorithm may also 1687 significantly increase. 1688 1689 The algorithm incrementally adjusts the point positions through an iterative process. 1690 Basically points are moved due to the influence of neighboring points. 1691 1692 As points move, both the local connectivity and data attributes associated with each point 1693 must be updated. Rather than performing these expensive operations after every iteration, 1694 a number of sub-iterations can be specified. If so, then the neighborhood and attribute 1695 value updates occur only every sub iteration, which can improve performance significantly. 1696 1697 Arguments: 1698 n : (int) 1699 neighborhood size to calculate the Laplacian. 1700 iters : (int) 1701 number of iterations. 1702 sub_iters : (int) 1703 number of sub-iterations, i.e. the number of times the neighborhood and attribute 1704 value updates occur during each iteration. 1705 packing_factor : (float) 1706 adjust convergence speed. 1707 max_step : (float) 1708 Specify the maximum smoothing step size for each smoothing iteration. 1709 This limits the the distance over which a point can move in each iteration. 1710 As in all iterative methods, the stability of the process is sensitive to this parameter. 1711 In general, small step size and large numbers of iterations are more stable than a larger 1712 step size and a smaller numbers of iterations. 1713 constraints : (dict) 1714 dictionary of constraints. 1715 Point constraints are used to prevent points from moving, 1716 or to move only on a plane. This can prevent shrinking or growing point clouds. 1717 If enabled, a local topological analysis is performed to determine whether a point 1718 should be marked as fixed" i.e., never moves, or the point only moves on a plane, 1719 or the point can move freely. 1720 If all points in the neighborhood surrounding a point are in the cone defined by 1721 `fixed_angle`, then the point is classified as fixed. 1722 If all points in the neighborhood surrounding a point are in the cone defined by 1723 `boundary_angle`, then the point is classified as lying on a plane. 1724 Angles are expressed in degrees. 1725 1726 Example: 1727 ```py 1728 import numpy as np 1729 from vedo import Points, show 1730 from vedo.pyplot import histogram 1731 1732 vpts1 = Points(np.random.rand(10_000, 3)) 1733 dists = vpts1.auto_distance() 1734 h1 = histogram(dists, xlim=(0,0.08)).clone2d() 1735 1736 vpts2 = vpts1.clone().relax_point_positions(n=100, iters=20, sub_iters=10) 1737 dists = vpts2.auto_distance() 1738 h2 = histogram(dists, xlim=(0,0.08)).clone2d() 1739 1740 show([[vpts1, h1], [vpts2, h2]], N=2).close() 1741 ``` 1742 """ 1743 smooth = vtki.new("PointSmoothingFilter") 1744 smooth.SetInputData(self.dataset) 1745 smooth.SetSmoothingModeToUniform() 1746 smooth.SetNumberOfIterations(iters) 1747 smooth.SetNumberOfSubIterations(sub_iters) 1748 smooth.SetPackingFactor(packing_factor) 1749 if self.point_locator: 1750 smooth.SetLocator(self.point_locator) 1751 if not max_step: 1752 max_step = self.diagonal_size() / 100 1753 smooth.SetMaximumStepSize(max_step) 1754 smooth.SetNeighborhoodSize(n) 1755 if constraints: 1756 fixed_angle = constraints.get("fixed_angle", 45) 1757 boundary_angle = constraints.get("boundary_angle", 110) 1758 smooth.EnableConstraintsOn() 1759 smooth.SetFixedAngle(fixed_angle) 1760 smooth.SetBoundaryAngle(boundary_angle) 1761 smooth.GenerateConstraintScalarsOn() 1762 smooth.GenerateConstraintNormalsOn() 1763 smooth.Update() 1764 self._update(smooth.GetOutput()) 1765 self.metadata["PackingRadius"] = smooth.GetPackingRadius() 1766 self.pipeline = utils.OperationNode("relax_point_positions", parents=[self]) 1767 return self
Smooth mesh or points with a Laplacian algorithm variant. This modifies the coordinates of the input points by adjusting their positions to create a smooth distribution (and thereby form a pleasing packing of the points). Smoothing is performed by considering the effects of neighboring points on one another it uses a cubic cutoff function to produce repulsive forces between close points and attractive forces that are a little further away.
In general, the larger the neighborhood size, the greater the reduction in high frequency information. The memory and computational requirements of the algorithm may also significantly increase.
The algorithm incrementally adjusts the point positions through an iterative process. Basically points are moved due to the influence of neighboring points.
As points move, both the local connectivity and data attributes associated with each point must be updated. Rather than performing these expensive operations after every iteration, a number of sub-iterations can be specified. If so, then the neighborhood and attribute value updates occur only every sub iteration, which can improve performance significantly.
Arguments:
- n : (int) neighborhood size to calculate the Laplacian.
- iters : (int) number of iterations.
- sub_iters : (int) number of sub-iterations, i.e. the number of times the neighborhood and attribute value updates occur during each iteration.
- packing_factor : (float) adjust convergence speed.
- max_step : (float) Specify the maximum smoothing step size for each smoothing iteration. This limits the the distance over which a point can move in each iteration. As in all iterative methods, the stability of the process is sensitive to this parameter. In general, small step size and large numbers of iterations are more stable than a larger step size and a smaller numbers of iterations.
- constraints : (dict)
dictionary of constraints.
Point constraints are used to prevent points from moving,
or to move only on a plane. This can prevent shrinking or growing point clouds.
If enabled, a local topological analysis is performed to determine whether a point
should be marked as fixed" i.e., never moves, or the point only moves on a plane,
or the point can move freely.
If all points in the neighborhood surrounding a point are in the cone defined by
fixed_angle
, then the point is classified as fixed. If all points in the neighborhood surrounding a point are in the cone defined 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()
1769 def smooth_mls_1d(self, f=0.2, radius=None, n=0) -> Self: 1770 """ 1771 Smooth mesh or points with a `Moving Least Squares` variant. 1772 The point data array "Variances" will contain the residue calculated for each point. 1773 1774 Arguments: 1775 f : (float) 1776 smoothing factor - typical range is [0,2]. 1777 radius : (float) 1778 radius search in absolute units. 1779 If set then `f` is ignored. 1780 n : (int) 1781 number of neighbours to be used for the fit. 1782 If set then `f` and `radius` are ignored. 1783 1784 Examples: 1785 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 1786 - [skeletonize.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/skeletonize.py) 1787 1788 ![](https://vedo.embl.es/images/advanced/moving_least_squares1D.png) 1789 """ 1790 coords = self.vertices 1791 ncoords = len(coords) 1792 1793 if n: 1794 Ncp = n 1795 elif radius: 1796 Ncp = 1 1797 else: 1798 Ncp = int(ncoords * f / 10) 1799 if Ncp < 5: 1800 vedo.logger.warning(f"Please choose a fraction higher than {f}") 1801 Ncp = 5 1802 1803 variances, newline = [], [] 1804 for p in coords: 1805 points = self.closest_point(p, n=Ncp, radius=radius) 1806 if len(points) < 4: 1807 continue 1808 1809 points = np.array(points) 1810 pointsmean = points.mean(axis=0) # plane center 1811 _, dd, vv = np.linalg.svd(points - pointsmean) 1812 newp = np.dot(p - pointsmean, vv[0]) * vv[0] + pointsmean 1813 variances.append(dd[1] + dd[2]) 1814 newline.append(newp) 1815 1816 self.pointdata["Variances"] = np.array(variances).astype(np.float32) 1817 self.vertices = newline 1818 self.pipeline = utils.OperationNode("smooth_mls_1d", parents=[self]) 1819 return self
Smooth mesh or points with a Moving Least Squares
variant.
The point data array "Variances" will contain the residue calculated for each point.
Arguments:
- f : (float) smoothing factor - typical range is [0,2].
- radius : (float)
radius search in absolute units.
If set then
f
is ignored. - n : (int)
number of neighbours to be used for the fit.
If set then
f
andradius
are ignored.
Examples:
1821 def smooth_mls_2d(self, f=0.2, radius=None, n=0) -> Self: 1822 """ 1823 Smooth mesh or points with a `Moving Least Squares` algorithm variant. 1824 1825 The `mesh.pointdata['MLSVariance']` array will contain the residue calculated for each point. 1826 When a radius is specified, points that are isolated will not be moved and will get 1827 a 0 entry in array `mesh.pointdata['MLSValidPoint']`. 1828 1829 Arguments: 1830 f : (float) 1831 smoothing factor - typical range is [0, 2]. 1832 radius : (float | array) 1833 radius search in absolute units. Can be single value (float) or sequence 1834 for adaptive smoothing. If set then `f` is ignored. 1835 n : (int) 1836 number of neighbours to be used for the fit. 1837 If set then `f` and `radius` are ignored. 1838 1839 Examples: 1840 - [moving_least_squares2D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares2D.py) 1841 - [recosurface.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/recosurface.py) 1842 1843 ![](https://vedo.embl.es/images/advanced/recosurface.png) 1844 """ 1845 coords = self.vertices 1846 ncoords = len(coords) 1847 1848 if n: 1849 Ncp = n 1850 radius = None 1851 elif radius is not None: 1852 Ncp = 1 1853 else: 1854 Ncp = int(ncoords * f / 100) 1855 if Ncp < 4: 1856 vedo.logger.error(f"please choose a f-value higher than {f}") 1857 Ncp = 4 1858 1859 variances, newpts, valid = [], [], [] 1860 radius_is_sequence = utils.is_sequence(radius) 1861 1862 pb = None 1863 if ncoords > 10000: 1864 pb = utils.ProgressBar(0, ncoords, delay=3) 1865 1866 for i, p in enumerate(coords): 1867 if pb: 1868 pb.print("smooth_mls_2d working ...") 1869 1870 # if a radius was provided for each point 1871 if radius_is_sequence: 1872 pts = self.closest_point(p, n=Ncp, radius=radius[i]) 1873 else: 1874 pts = self.closest_point(p, n=Ncp, radius=radius) 1875 1876 if len(pts) > 3: 1877 ptsmean = pts.mean(axis=0) # plane center 1878 _, dd, vv = np.linalg.svd(pts - ptsmean) 1879 cv = np.cross(vv[0], vv[1]) 1880 t = (np.dot(cv, ptsmean) - np.dot(cv, p)) / np.dot(cv, cv) 1881 newpts.append(p + cv * t) 1882 variances.append(dd[2]) 1883 if radius is not None: 1884 valid.append(1) 1885 else: 1886 newpts.append(p) 1887 variances.append(0) 1888 if radius is not None: 1889 valid.append(0) 1890 1891 if radius is not None: 1892 self.pointdata["MLSValidPoint"] = np.array(valid).astype(np.uint8) 1893 self.pointdata["MLSVariance"] = np.array(variances).astype(np.float32) 1894 1895 self.vertices = newpts 1896 1897 self.pipeline = utils.OperationNode("smooth_mls_2d", parents=[self]) 1898 return self
Smooth mesh or points with a Moving Least Squares
algorithm variant.
The mesh.pointdata['MLSVariance']
array will contain the residue calculated for each point.
When a radius is specified, points that are isolated will not be moved and will get
a 0 entry in array mesh.pointdata['MLSValidPoint']
.
Arguments:
- f : (float) smoothing factor - typical range is [0, 2].
- radius : (float | array)
radius search in absolute units. Can be single value (float) or sequence
for adaptive smoothing. If set then
f
is ignored. - n : (int)
number of neighbours to be used for the fit.
If set then
f
andradius
are ignored.
Examples:
1900 def smooth_lloyd_2d(self, iterations=2, bounds=None, options="Qbb Qc Qx") -> Self: 1901 """ 1902 Lloyd relaxation of a 2D pointcloud. 1903 1904 Arguments: 1905 iterations : (int) 1906 number of iterations. 1907 bounds : (list) 1908 bounding box of the domain. 1909 options : (str) 1910 options for the Qhull algorithm. 1911 """ 1912 # Credits: https://hatarilabs.com/ih-en/ 1913 # tutorial-to-create-a-geospatial-voronoi-sh-mesh-with-python-scipy-and-geopandas 1914 from scipy.spatial import Voronoi as scipy_voronoi 1915 1916 def _constrain_points(points): 1917 # Update any points that have drifted beyond the boundaries of this space 1918 if bounds is not None: 1919 for point in points: 1920 if point[0] < bounds[0]: point[0] = bounds[0] 1921 if point[0] > bounds[1]: point[0] = bounds[1] 1922 if point[1] < bounds[2]: point[1] = bounds[2] 1923 if point[1] > bounds[3]: point[1] = bounds[3] 1924 return points 1925 1926 def _find_centroid(vertices): 1927 # The equation for the method used here to find the centroid of a 1928 # 2D polygon is given here: https://en.wikipedia.org/wiki/Centroid#Of_a_polygon 1929 area = 0 1930 centroid_x = 0 1931 centroid_y = 0 1932 for i in range(len(vertices) - 1): 1933 step = (vertices[i, 0] * vertices[i + 1, 1]) - (vertices[i + 1, 0] * vertices[i, 1]) 1934 centroid_x += (vertices[i, 0] + vertices[i + 1, 0]) * step 1935 centroid_y += (vertices[i, 1] + vertices[i + 1, 1]) * step 1936 area += step 1937 if area: 1938 centroid_x = (1.0 / (3.0 * area)) * centroid_x 1939 centroid_y = (1.0 / (3.0 * area)) * centroid_y 1940 # prevent centroids from escaping bounding box 1941 return _constrain_points([[centroid_x, centroid_y]])[0] 1942 1943 def _relax(voron): 1944 # Moves each point to the centroid of its cell in the voronoi 1945 # map to "relax" the points (i.e. jitter the points so as 1946 # to spread them out within the space). 1947 centroids = [] 1948 for idx in voron.point_region: 1949 # the region is a series of indices into voronoi.vertices 1950 # remove point at infinity, designated by index -1 1951 region = [i for i in voron.regions[idx] if i != -1] 1952 # enclose the polygon 1953 region = region + [region[0]] 1954 verts = voron.vertices[region] 1955 # find the centroid of those vertices 1956 centroids.append(_find_centroid(verts)) 1957 return _constrain_points(centroids) 1958 1959 if bounds is None: 1960 bounds = self.bounds() 1961 1962 pts = self.vertices[:, (0, 1)] 1963 for i in range(iterations): 1964 vor = scipy_voronoi(pts, qhull_options=options) 1965 _constrain_points(vor.vertices) 1966 pts = _relax(vor) 1967 out = Points(pts) 1968 out.name = "MeshSmoothLloyd2D" 1969 out.pipeline = utils.OperationNode("smooth_lloyd", parents=[self]) 1970 return out
Lloyd relaxation of a 2D pointcloud.
Arguments:
- iterations : (int) number of iterations.
- bounds : (list) bounding box of the domain.
- options : (str) options for the Qhull algorithm.
1972 def project_on_plane(self, plane="z", point=None, direction=None) -> Self: 1973 """ 1974 Project the mesh on one of the Cartesian planes. 1975 1976 Arguments: 1977 plane : (str, Plane) 1978 if plane is `str`, plane can be one of ['x', 'y', 'z'], 1979 represents x-plane, y-plane and z-plane, respectively. 1980 Otherwise, plane should be an instance of `vedo.shapes.Plane`. 1981 point : (float, array) 1982 if plane is `str`, point should be a float represents the intercept. 1983 Otherwise, point is the camera point of perspective projection 1984 direction : (array) 1985 direction of oblique projection 1986 1987 Note: 1988 Parameters `point` and `direction` are only used if the given plane 1989 is an instance of `vedo.shapes.Plane`. And one of these two params 1990 should be left as `None` to specify the projection type. 1991 1992 Example: 1993 ```python 1994 s.project_on_plane(plane='z') # project to z-plane 1995 plane = Plane(pos=(4, 8, -4), normal=(-1, 0, 1), s=(5,5)) 1996 s.project_on_plane(plane=plane) # orthogonal projection 1997 s.project_on_plane(plane=plane, point=(6, 6, 6)) # perspective projection 1998 s.project_on_plane(plane=plane, direction=(1, 2, -1)) # oblique projection 1999 ``` 2000 2001 Examples: 2002 - [silhouette2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/silhouette2.py) 2003 2004 ![](https://vedo.embl.es/images/basic/silhouette2.png) 2005 """ 2006 coords = self.vertices 2007 2008 if plane == "x": 2009 coords[:, 0] = self.transform.position[0] 2010 intercept = self.xbounds()[0] if point is None else point 2011 self.x(intercept) 2012 elif plane == "y": 2013 coords[:, 1] = self.transform.position[1] 2014 intercept = self.ybounds()[0] if point is None else point 2015 self.y(intercept) 2016 elif plane == "z": 2017 coords[:, 2] = self.transform.position[2] 2018 intercept = self.zbounds()[0] if point is None else point 2019 self.z(intercept) 2020 2021 elif isinstance(plane, vedo.shapes.Plane): 2022 normal = plane.normal / np.linalg.norm(plane.normal) 2023 pl = np.hstack((normal, -np.dot(plane.pos(), normal))).reshape(4, 1) 2024 if direction is None and point is None: 2025 # orthogonal projection 2026 pt = np.hstack((normal, [0])).reshape(4, 1) 2027 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T # python3 only 2028 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2029 2030 elif direction is None: 2031 # perspective projection 2032 pt = np.hstack((np.array(point), [1])).reshape(4, 1) 2033 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2034 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2035 2036 elif point is None: 2037 # oblique projection 2038 pt = np.hstack((np.array(direction), [0])).reshape(4, 1) 2039 # proj_mat = pt.T @ pl * np.eye(4) - pt @ pl.T 2040 proj_mat = np.matmul(pt.T, pl) * np.eye(4) - np.matmul(pt, pl.T) 2041 2042 coords = np.concatenate([coords, np.ones((coords.shape[:-1] + (1,)))], axis=-1) 2043 # coords = coords @ proj_mat.T 2044 coords = np.matmul(coords, proj_mat.T) 2045 coords = coords[:, :3] / coords[:, 3:] 2046 2047 else: 2048 vedo.logger.error(f"unknown plane {plane}") 2049 raise RuntimeError() 2050 2051 self.alpha(0.1) 2052 self.vertices = coords 2053 return self
Project the mesh on one of the Cartesian planes.
Arguments:
- plane : (str, Plane)
if plane is
str
, plane can be one of ['x', 'y', 'z'], represents x-plane, y-plane and z-plane, respectively. Otherwise, plane should be an instance 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:
2055 def warp(self, source, target, sigma=1.0, mode="3d") -> Self: 2056 """ 2057 "Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set 2058 of source and target landmarks. Any point on the mesh close to a source landmark will 2059 be moved to a place close to the corresponding target landmark. 2060 The points in between are interpolated smoothly using 2061 Bookstein's Thin Plate Spline algorithm. 2062 2063 Transformation object can be accessed with `mesh.transform`. 2064 2065 Arguments: 2066 sigma : (float) 2067 specify the 'stiffness' of the spline. 2068 mode : (str) 2069 set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes) 2070 2071 Examples: 2072 - [interpolate_field.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/interpolate_field.py) 2073 - [warp1.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp1.py) 2074 - [warp2.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp2.py) 2075 - [warp3.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp3.py) 2076 - [warp4a.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4a.py) 2077 - [warp4b.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp4b.py) 2078 - [warp6.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/warp6.py) 2079 2080 ![](https://vedo.embl.es/images/advanced/warp2.png) 2081 """ 2082 parents = [self] 2083 2084 try: 2085 source = source.vertices 2086 parents.append(source) 2087 except AttributeError: 2088 source = utils.make3d(source) 2089 2090 try: 2091 target = target.vertices 2092 parents.append(target) 2093 except AttributeError: 2094 target = utils.make3d(target) 2095 2096 ns = len(source) 2097 nt = len(target) 2098 if ns != nt: 2099 vedo.logger.error(f"#source {ns} != {nt} #target points") 2100 raise RuntimeError() 2101 2102 NLT = NonLinearTransform() 2103 NLT.source_points = source 2104 NLT.target_points = target 2105 self.apply_transform(NLT) 2106 2107 self.pipeline = utils.OperationNode("warp", parents=parents) 2108 return self
"Thin Plate Spline" transformations describe a nonlinear warp transform defined by a set of source and target landmarks. Any point on the mesh close to a source landmark will be moved to a place close to the corresponding target landmark. The points in between are interpolated smoothly using Bookstein's Thin Plate Spline algorithm.
Transformation object can be accessed with mesh.transform
.
Arguments:
- sigma : (float) specify the 'stiffness' of the spline.
- mode : (str) set the basis function to either abs(R) (for 3d) or R2LogR (for 2d meshes)
Examples:
2110 def cut_with_plane(self, origin=(0, 0, 0), normal=(1, 0, 0), invert=False) -> Self: 2111 """ 2112 Cut the mesh with the plane defined by a point and a normal. 2113 2114 Arguments: 2115 origin : (array) 2116 the cutting plane goes through this point 2117 normal : (array) 2118 normal of the cutting plane 2119 2120 Example: 2121 ```python 2122 from vedo import Cube 2123 cube = Cube().cut_with_plane(normal=(1,1,1)) 2124 cube.back_color('pink').show().close() 2125 ``` 2126 ![](https://vedo.embl.es/images/feats/cut_with_plane_cube.png) 2127 2128 Examples: 2129 - [trail.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/trail.py) 2130 2131 ![](https://vedo.embl.es/images/simulations/trail.gif) 2132 2133 Check out also: 2134 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()`. 2135 """ 2136 s = str(normal) 2137 if "x" in s: 2138 normal = (1, 0, 0) 2139 if "-" in s: 2140 normal = -np.array(normal) 2141 elif "y" in s: 2142 normal = (0, 1, 0) 2143 if "-" in s: 2144 normal = -np.array(normal) 2145 elif "z" in s: 2146 normal = (0, 0, 1) 2147 if "-" in s: 2148 normal = -np.array(normal) 2149 plane = vtki.vtkPlane() 2150 plane.SetOrigin(origin) 2151 plane.SetNormal(normal) 2152 2153 clipper = vtki.new("ClipPolyData") 2154 clipper.SetInputData(self.dataset) 2155 clipper.SetClipFunction(plane) 2156 clipper.GenerateClippedOutputOff() 2157 clipper.GenerateClipScalarsOff() 2158 clipper.SetInsideOut(invert) 2159 clipper.SetValue(0) 2160 clipper.Update() 2161 2162 self._update(clipper.GetOutput()) 2163 2164 self.pipeline = utils.OperationNode("cut_with_plane", parents=[self]) 2165 return self
Cut the mesh with the plane defined by a point and a normal.
Arguments:
- origin : (array) the cutting plane goes through this point
- normal : (array) normal of the cutting plane
Example:
from vedo import Cube cube = Cube().cut_with_plane(normal=(1,1,1)) cube.back_color('pink').show().close()
Examples:
Check out also:
2167 def cut_with_planes(self, origins, normals, invert=False) -> Self: 2168 """ 2169 Cut the mesh with a convex set of planes defined by points and normals. 2170 2171 Arguments: 2172 origins : (array) 2173 each cutting plane goes through this point 2174 normals : (array) 2175 normal of each of the cutting planes 2176 invert : (bool) 2177 if True, cut outside instead of inside 2178 2179 Check out also: 2180 `cut_with_box()`, `cut_with_cylinder()`, `cut_with_sphere()` 2181 """ 2182 2183 vpoints = vtki.vtkPoints() 2184 for p in utils.make3d(origins): 2185 vpoints.InsertNextPoint(p) 2186 normals = utils.make3d(normals) 2187 2188 planes = vtki.vtkPlanes() 2189 planes.SetPoints(vpoints) 2190 planes.SetNormals(utils.numpy2vtk(normals, dtype=float)) 2191 2192 clipper = vtki.new("ClipPolyData") 2193 clipper.SetInputData(self.dataset) 2194 clipper.SetInsideOut(invert) 2195 clipper.SetClipFunction(planes) 2196 clipper.GenerateClippedOutputOff() 2197 clipper.GenerateClipScalarsOff() 2198 clipper.SetValue(0) 2199 clipper.Update() 2200 2201 self._update(clipper.GetOutput()) 2202 2203 self.pipeline = utils.OperationNode("cut_with_planes", parents=[self]) 2204 return self
Cut the mesh with a convex set of planes defined by points and normals.
Arguments:
- origins : (array) each cutting plane goes through this point
- normals : (array) normal of each of the cutting planes
- invert : (bool) if True, cut outside instead of inside
Check out also:
2206 def cut_with_box(self, bounds, invert=False) -> Self: 2207 """ 2208 Cut the current mesh with a box or a set of boxes. 2209 This is much faster than `cut_with_mesh()`. 2210 2211 Input `bounds` can be either: 2212 - a Mesh or Points object 2213 - a list of 6 number representing a bounding box `[xmin,xmax, ymin,ymax, zmin,zmax]` 2214 - a list of bounding boxes like the above: `[[xmin1,...], [xmin2,...], ...]` 2215 2216 Example: 2217 ```python 2218 from vedo import Sphere, Cube, show 2219 mesh = Sphere(r=1, res=50) 2220 box = Cube(side=1.5).wireframe() 2221 mesh.cut_with_box(box) 2222 show(mesh, box, axes=1).close() 2223 ``` 2224 ![](https://vedo.embl.es/images/feats/cut_with_box_cube.png) 2225 2226 Check out also: 2227 `cut_with_line()`, `cut_with_plane()`, `cut_with_cylinder()` 2228 """ 2229 if isinstance(bounds, Points): 2230 bounds = bounds.bounds() 2231 2232 box = vtki.new("Box") 2233 if utils.is_sequence(bounds[0]): 2234 for bs in bounds: 2235 box.AddBounds(bs) 2236 else: 2237 box.SetBounds(bounds) 2238 2239 clipper = vtki.new("ClipPolyData") 2240 clipper.SetInputData(self.dataset) 2241 clipper.SetClipFunction(box) 2242 clipper.SetInsideOut(not invert) 2243 clipper.GenerateClippedOutputOff() 2244 clipper.GenerateClipScalarsOff() 2245 clipper.SetValue(0) 2246 clipper.Update() 2247 self._update(clipper.GetOutput()) 2248 2249 self.pipeline = utils.OperationNode("cut_with_box", parents=[self]) 2250 return self
Cut the current mesh with a box or a set of boxes.
This is much faster than cut_with_mesh()
.
Input bounds
can be either:
- a Mesh or Points object
- a list of 6 number representing a bounding box
[xmin,xmax, ymin,ymax, zmin,zmax]
- a list of bounding boxes like the above:
[[xmin1,...], [xmin2,...], ...]
Example:
from vedo import Sphere, Cube, show mesh = Sphere(r=1, res=50) box = Cube(side=1.5).wireframe() mesh.cut_with_box(box) show(mesh, box, axes=1).close()
Check out also:
2252 def cut_with_line(self, points, invert=False, closed=True) -> Self: 2253 """ 2254 Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter. 2255 The polyline is defined by a set of points (z-coordinates are ignored). 2256 This is much faster than `cut_with_mesh()`. 2257 2258 Check out also: 2259 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2260 """ 2261 pplane = vtki.new("PolyPlane") 2262 if isinstance(points, Points): 2263 points = points.vertices.tolist() 2264 2265 if closed: 2266 if isinstance(points, np.ndarray): 2267 points = points.tolist() 2268 points.append(points[0]) 2269 2270 vpoints = vtki.vtkPoints() 2271 for p in points: 2272 if len(p) == 2: 2273 p = [p[0], p[1], 0.0] 2274 vpoints.InsertNextPoint(p) 2275 2276 n = len(points) 2277 polyline = vtki.new("PolyLine") 2278 polyline.Initialize(n, vpoints) 2279 polyline.GetPointIds().SetNumberOfIds(n) 2280 for i in range(n): 2281 polyline.GetPointIds().SetId(i, i) 2282 pplane.SetPolyLine(polyline) 2283 2284 clipper = vtki.new("ClipPolyData") 2285 clipper.SetInputData(self.dataset) 2286 clipper.SetClipFunction(pplane) 2287 clipper.SetInsideOut(invert) 2288 clipper.GenerateClippedOutputOff() 2289 clipper.GenerateClipScalarsOff() 2290 clipper.SetValue(0) 2291 clipper.Update() 2292 self._update(clipper.GetOutput()) 2293 2294 self.pipeline = utils.OperationNode("cut_with_line", parents=[self]) 2295 return self
Cut the current mesh with a line vertically in the z-axis direction like a cookie cutter.
The polyline is defined by a set of points (z-coordinates are ignored).
This is much faster than cut_with_mesh()
.
Check out also:
2373 def cut_with_cylinder(self, center=(0, 0, 0), axis=(0, 0, 1), r=1, invert=False) -> Self: 2374 """ 2375 Cut the current mesh with an infinite cylinder. 2376 This is much faster than `cut_with_mesh()`. 2377 2378 Arguments: 2379 center : (array) 2380 the center of the cylinder 2381 normal : (array) 2382 direction of the cylinder axis 2383 r : (float) 2384 radius of the cylinder 2385 2386 Example: 2387 ```python 2388 from vedo import Disc, show 2389 disc = Disc(r1=1, r2=1.2) 2390 mesh = disc.extrude(3, res=50).linewidth(1) 2391 mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True) 2392 show(mesh, axes=1).close() 2393 ``` 2394 ![](https://vedo.embl.es/images/feats/cut_with_cylinder.png) 2395 2396 Examples: 2397 - [optics_main1.py](https://github.com/marcomusy/vedo/blob/master/examples/simulations/optics_main1.py) 2398 2399 Check out also: 2400 `cut_with_box()`, `cut_with_plane()`, `cut_with_sphere()` 2401 """ 2402 s = str(axis) 2403 if "x" in s: 2404 axis = (1, 0, 0) 2405 elif "y" in s: 2406 axis = (0, 1, 0) 2407 elif "z" in s: 2408 axis = (0, 0, 1) 2409 cyl = vtki.new("Cylinder") 2410 cyl.SetCenter(center) 2411 cyl.SetAxis(axis[0], axis[1], axis[2]) 2412 cyl.SetRadius(r) 2413 2414 clipper = vtki.new("ClipPolyData") 2415 clipper.SetInputData(self.dataset) 2416 clipper.SetClipFunction(cyl) 2417 clipper.SetInsideOut(not invert) 2418 clipper.GenerateClippedOutputOff() 2419 clipper.GenerateClipScalarsOff() 2420 clipper.SetValue(0) 2421 clipper.Update() 2422 self._update(clipper.GetOutput()) 2423 2424 self.pipeline = utils.OperationNode("cut_with_cylinder", parents=[self]) 2425 return self
Cut the current mesh with an infinite cylinder.
This is much faster than cut_with_mesh()
.
Arguments:
- center : (array) the center of the cylinder
- normal : (array) direction of the cylinder axis
- r : (float) radius of the cylinder
Example:
from vedo import Disc, show disc = Disc(r1=1, r2=1.2) mesh = disc.extrude(3, res=50).linewidth(1) mesh.cut_with_cylinder([0,0,2], r=0.4, axis='y', invert=True) show(mesh, axes=1).close()
Examples:
Check out also:
2427 def cut_with_sphere(self, center=(0, 0, 0), r=1.0, invert=False) -> Self: 2428 """ 2429 Cut the current mesh with an sphere. 2430 This is much faster than `cut_with_mesh()`. 2431 2432 Arguments: 2433 center : (array) 2434 the center of the sphere 2435 r : (float) 2436 radius of the sphere 2437 2438 Example: 2439 ```python 2440 from vedo import Disc, show 2441 disc = Disc(r1=1, r2=1.2) 2442 mesh = disc.extrude(3, res=50).linewidth(1) 2443 mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True) 2444 show(mesh, axes=1).close() 2445 ``` 2446 ![](https://vedo.embl.es/images/feats/cut_with_sphere.png) 2447 2448 Check out also: 2449 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2450 """ 2451 sph = vtki.new("Sphere") 2452 sph.SetCenter(center) 2453 sph.SetRadius(r) 2454 2455 clipper = vtki.new("ClipPolyData") 2456 clipper.SetInputData(self.dataset) 2457 clipper.SetClipFunction(sph) 2458 clipper.SetInsideOut(not invert) 2459 clipper.GenerateClippedOutputOff() 2460 clipper.GenerateClipScalarsOff() 2461 clipper.SetValue(0) 2462 clipper.Update() 2463 self._update(clipper.GetOutput()) 2464 self.pipeline = utils.OperationNode("cut_with_sphere", parents=[self]) 2465 return self
Cut the current mesh with an sphere.
This is much faster than cut_with_mesh()
.
Arguments:
- center : (array) the center of the sphere
- r : (float) radius of the sphere
Example:
from vedo import Disc, show disc = Disc(r1=1, r2=1.2) mesh = disc.extrude(3, res=50).linewidth(1) mesh.cut_with_sphere([1,-0.7,2], r=1.5, invert=True) show(mesh, axes=1).close()
Check out also:
2467 def cut_with_mesh(self, mesh, invert=False, keep=False) -> Union[Self, "vedo.Assembly"]: 2468 """ 2469 Cut an `Mesh` mesh with another `Mesh`. 2470 2471 Use `invert` to invert the selection. 2472 2473 Use `keep` to keep the cutoff part, in this case an `Assembly` is returned: 2474 the "cut" object and the "discarded" part of the original object. 2475 You can access both via `assembly.unpack()` method. 2476 2477 Example: 2478 ```python 2479 from vedo import * 2480 arr = np.random.randn(100000, 3)/2 2481 pts = Points(arr).c('red3').pos(5,0,0) 2482 cube = Cube().pos(4,0.5,0) 2483 assem = pts.cut_with_mesh(cube, keep=True) 2484 show(assem.unpack(), axes=1).close() 2485 ``` 2486 ![](https://vedo.embl.es/images/feats/cut_with_mesh.png) 2487 2488 Check out also: 2489 `cut_with_box()`, `cut_with_plane()`, `cut_with_cylinder()` 2490 """ 2491 polymesh = mesh.dataset 2492 poly = self.dataset 2493 2494 # Create an array to hold distance information 2495 signed_distances = vtki.vtkFloatArray() 2496 signed_distances.SetNumberOfComponents(1) 2497 signed_distances.SetName("SignedDistances") 2498 2499 # implicit function that will be used to slice the mesh 2500 ippd = vtki.new("ImplicitPolyDataDistance") 2501 ippd.SetInput(polymesh) 2502 2503 # Evaluate the signed distance function at all of the grid points 2504 for pointId in range(poly.GetNumberOfPoints()): 2505 p = poly.GetPoint(pointId) 2506 signed_distance = ippd.EvaluateFunction(p) 2507 signed_distances.InsertNextValue(signed_distance) 2508 2509 currentscals = poly.GetPointData().GetScalars() 2510 if currentscals: 2511 currentscals = currentscals.GetName() 2512 2513 poly.GetPointData().AddArray(signed_distances) 2514 poly.GetPointData().SetActiveScalars("SignedDistances") 2515 2516 clipper = vtki.new("ClipPolyData") 2517 clipper.SetInputData(poly) 2518 clipper.SetInsideOut(not invert) 2519 clipper.SetGenerateClippedOutput(keep) 2520 clipper.SetValue(0.0) 2521 clipper.Update() 2522 cpoly = clipper.GetOutput() 2523 2524 if keep: 2525 kpoly = clipper.GetOutput(1) 2526 2527 vis = False 2528 if currentscals: 2529 cpoly.GetPointData().SetActiveScalars(currentscals) 2530 vis = self.mapper.GetScalarVisibility() 2531 2532 self._update(cpoly) 2533 2534 self.pointdata.remove("SignedDistances") 2535 self.mapper.SetScalarVisibility(vis) 2536 if keep: 2537 if isinstance(self, vedo.Mesh): 2538 cutoff = vedo.Mesh(kpoly) 2539 else: 2540 cutoff = vedo.Points(kpoly) 2541 # cutoff = self.__class__(kpoly) # this does not work properly 2542 cutoff.properties = vtki.vtkProperty() 2543 cutoff.properties.DeepCopy(self.properties) 2544 cutoff.actor.SetProperty(cutoff.properties) 2545 cutoff.c("k5").alpha(0.2) 2546 return vedo.Assembly([self, cutoff]) 2547 2548 self.pipeline = utils.OperationNode("cut_with_mesh", parents=[self, mesh]) 2549 return self
Cut an Mesh
mesh with another Mesh
.
Use invert
to invert the selection.
Use keep
to keep the cutoff part, in this case an Assembly
is returned:
the "cut" object and the "discarded" part of the original object.
You can access both via assembly.unpack()
method.
Example:
from vedo import *
arr = np.random.randn(100000, 3)/2
pts = Points(arr).c('red3').pos(5,0,0)
cube = Cube().pos(4,0.5,0)
assem = pts.cut_with_mesh(cube, keep=True)
show(assem.unpack(), axes=1).close()
Check out also:
2551 def cut_with_point_loop( 2552 self, points, invert=False, on="points", include_boundary=False 2553 ) -> Self: 2554 """ 2555 Cut an `Mesh` object with a set of points forming a closed loop. 2556 2557 Arguments: 2558 invert : (bool) 2559 invert selection (inside-out) 2560 on : (str) 2561 if 'cells' will extract the whole cells lying inside (or outside) the point loop 2562 include_boundary : (bool) 2563 include cells lying exactly on the boundary line. Only relevant on 'cells' mode 2564 2565 Examples: 2566 - [cut_with_points1.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points1.py) 2567 2568 ![](https://vedo.embl.es/images/advanced/cutWithPoints1.png) 2569 2570 - [cut_with_points2.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/cut_with_points2.py) 2571 2572 ![](https://vedo.embl.es/images/advanced/cutWithPoints2.png) 2573 """ 2574 if isinstance(points, Points): 2575 parents = [points] 2576 vpts = points.dataset.GetPoints() 2577 points = points.vertices 2578 else: 2579 parents = [self] 2580 vpts = vtki.vtkPoints() 2581 points = utils.make3d(points) 2582 for p in points: 2583 vpts.InsertNextPoint(p) 2584 2585 if "cell" in on: 2586 ippd = vtki.new("ImplicitSelectionLoop") 2587 ippd.SetLoop(vpts) 2588 ippd.AutomaticNormalGenerationOn() 2589 clipper = vtki.new("ExtractPolyDataGeometry") 2590 clipper.SetInputData(self.dataset) 2591 clipper.SetImplicitFunction(ippd) 2592 clipper.SetExtractInside(not invert) 2593 clipper.SetExtractBoundaryCells(include_boundary) 2594 else: 2595 spol = vtki.new("SelectPolyData") 2596 spol.SetLoop(vpts) 2597 spol.GenerateSelectionScalarsOn() 2598 spol.GenerateUnselectedOutputOff() 2599 spol.SetInputData(self.dataset) 2600 spol.Update() 2601 clipper = vtki.new("ClipPolyData") 2602 clipper.SetInputData(spol.GetOutput()) 2603 clipper.SetInsideOut(not invert) 2604 clipper.SetValue(0.0) 2605 clipper.Update() 2606 self._update(clipper.GetOutput()) 2607 2608 self.pipeline = utils.OperationNode("cut_with_pointloop", parents=parents) 2609 return self
Cut an Mesh
object with a set of points forming a closed loop.
Arguments:
- invert : (bool) invert selection (inside-out)
- on : (str) if 'cells' will extract the whole cells lying inside (or outside) the point loop
- include_boundary : (bool) include cells lying exactly on the boundary line. Only relevant on 'cells' mode
Examples:
2611 def cut_with_scalar(self, value: float, name="", invert=False) -> Self: 2612 """ 2613 Cut a mesh or point cloud with some input scalar point-data. 2614 2615 Arguments: 2616 value : (float) 2617 cutting value 2618 name : (str) 2619 array name of the scalars to be used 2620 invert : (bool) 2621 flip selection 2622 2623 Example: 2624 ```python 2625 from vedo import * 2626 s = Sphere().lw(1) 2627 pts = s.vertices 2628 scalars = np.sin(3*pts[:,2]) + pts[:,0] 2629 s.pointdata["somevalues"] = scalars 2630 s.cut_with_scalar(0.3) 2631 s.cmap("Spectral", "somevalues").add_scalarbar() 2632 s.show(axes=1).close() 2633 ``` 2634 ![](https://vedo.embl.es/images/feats/cut_with_scalars.png) 2635 """ 2636 if name: 2637 self.pointdata.select(name) 2638 clipper = vtki.new("ClipPolyData") 2639 clipper.SetInputData(self.dataset) 2640 clipper.SetValue(value) 2641 clipper.GenerateClippedOutputOff() 2642 clipper.SetInsideOut(not invert) 2643 clipper.Update() 2644 self._update(clipper.GetOutput()) 2645 self.pipeline = utils.OperationNode("cut_with_scalar", parents=[self]) 2646 return self
Cut a mesh or point cloud with some input scalar point-data.
Arguments:
- value : (float) cutting value
- name : (str) array name of the scalars to be used
- invert : (bool) flip selection
Example:
from vedo import * s = Sphere().lw(1) pts = s.vertices scalars = np.sin(3*pts[:,2]) + pts[:,0] s.pointdata["somevalues"] = scalars s.cut_with_scalar(0.3) s.cmap("Spectral", "somevalues").add_scalarbar() s.show(axes=1).close()
2648 def crop(self, 2649 top=None, bottom=None, right=None, left=None, front=None, back=None, 2650 bounds=()) -> Self: 2651 """ 2652 Crop an `Mesh` object. 2653 2654 Arguments: 2655 top : (float) 2656 fraction to crop from the top plane (positive z) 2657 bottom : (float) 2658 fraction to crop from the bottom plane (negative z) 2659 front : (float) 2660 fraction to crop from the front plane (positive y) 2661 back : (float) 2662 fraction to crop from the back plane (negative y) 2663 right : (float) 2664 fraction to crop from the right plane (positive x) 2665 left : (float) 2666 fraction to crop from the left plane (negative x) 2667 bounds : (list) 2668 bounding box of the crop region as `[x0,x1, y0,y1, z0,z1]` 2669 2670 Example: 2671 ```python 2672 from vedo import Sphere 2673 Sphere().crop(right=0.3, left=0.1).show() 2674 ``` 2675 ![](https://user-images.githubusercontent.com/32848391/57081955-0ef1e800-6cf6-11e9-99de-b45220939bc9.png) 2676 """ 2677 if not len(bounds): 2678 pos = np.array(self.pos()) 2679 x0, x1, y0, y1, z0, z1 = self.bounds() 2680 x0, y0, z0 = [x0, y0, z0] - pos 2681 x1, y1, z1 = [x1, y1, z1] - pos 2682 2683 dx, dy, dz = x1 - x0, y1 - y0, z1 - z0 2684 if top: 2685 z1 = z1 - top * dz 2686 if bottom: 2687 z0 = z0 + bottom * dz 2688 if front: 2689 y1 = y1 - front * dy 2690 if back: 2691 y0 = y0 + back * dy 2692 if right: 2693 x1 = x1 - right * dx 2694 if left: 2695 x0 = x0 + left * dx 2696 bounds = (x0, x1, y0, y1, z0, z1) 2697 2698 cu = vtki.new("Box") 2699 cu.SetBounds(bounds) 2700 2701 clipper = vtki.new("ClipPolyData") 2702 clipper.SetInputData(self.dataset) 2703 clipper.SetClipFunction(cu) 2704 clipper.InsideOutOn() 2705 clipper.GenerateClippedOutputOff() 2706 clipper.GenerateClipScalarsOff() 2707 clipper.SetValue(0) 2708 clipper.Update() 2709 self._update(clipper.GetOutput()) 2710 2711 self.pipeline = utils.OperationNode( 2712 "crop", parents=[self], comment=f"#pts {self.dataset.GetNumberOfPoints()}" 2713 ) 2714 return self
Crop an Mesh
object.
Arguments:
- top : (float) fraction to crop from the top plane (positive z)
- bottom : (float) fraction to crop from the bottom plane (negative z)
- front : (float) fraction to crop from the front plane (positive y)
- back : (float) fraction to crop from the back plane (negative y)
- right : (float) fraction to crop from the right plane (positive x)
- left : (float) fraction to crop from the left plane (negative x)
- bounds : (list)
bounding box of the crop region as
[x0,x1, y0,y1, z0,z1]
Example:
from vedo import Sphere Sphere().crop(right=0.3, left=0.1).show()
2716 def generate_surface_halo( 2717 self, 2718 distance=0.05, 2719 res=(50, 50, 50), 2720 bounds=(), 2721 maxdist=None, 2722 ) -> "vedo.Mesh": 2723 """ 2724 Generate the surface halo which sits at the specified distance from the input one. 2725 2726 Arguments: 2727 distance : (float) 2728 distance from the input surface 2729 res : (int) 2730 resolution of the surface 2731 bounds : (list) 2732 bounding box of the surface 2733 maxdist : (float) 2734 maximum distance to generate the surface 2735 """ 2736 if not bounds: 2737 bounds = self.bounds() 2738 2739 if not maxdist: 2740 maxdist = self.diagonal_size() / 2 2741 2742 imp = vtki.new("ImplicitModeller") 2743 imp.SetInputData(self.dataset) 2744 imp.SetSampleDimensions(res) 2745 if maxdist: 2746 imp.SetMaximumDistance(maxdist) 2747 if len(bounds) == 6: 2748 imp.SetModelBounds(bounds) 2749 contour = vtki.new("ContourFilter") 2750 contour.SetInputConnection(imp.GetOutputPort()) 2751 contour.SetValue(0, distance) 2752 contour.Update() 2753 out = vedo.Mesh(contour.GetOutput()) 2754 out.c("lightblue").alpha(0.25).lighting("off") 2755 out.pipeline = utils.OperationNode("generate_surface_halo", parents=[self]) 2756 return out
Generate the surface halo which sits at the specified distance from the input one.
Arguments:
- distance : (float) distance from the input surface
- res : (int) resolution of the surface
- bounds : (list) bounding box of the surface
- maxdist : (float) maximum distance to generate the surface
2758 def generate_mesh( 2759 self, 2760 line_resolution=None, 2761 mesh_resolution=None, 2762 smooth=0.0, 2763 jitter=0.001, 2764 grid=None, 2765 quads=False, 2766 invert=False, 2767 ) -> Self: 2768 """ 2769 Generate a polygonal Mesh from a closed contour line. 2770 If line is not closed it will be closed with a straight segment. 2771 2772 Check also `generate_delaunay2d()`. 2773 2774 Arguments: 2775 line_resolution : (int) 2776 resolution of the contour line. The default is None, in this case 2777 the contour is not resampled. 2778 mesh_resolution : (int) 2779 resolution of the internal triangles not touching the boundary. 2780 smooth : (float) 2781 smoothing of the contour before meshing. 2782 jitter : (float) 2783 add a small noise to the internal points. 2784 grid : (Grid) 2785 manually pass a Grid object. The default is True. 2786 quads : (bool) 2787 generate a mesh of quads instead of triangles. 2788 invert : (bool) 2789 flip the line orientation. The default is False. 2790 2791 Examples: 2792 - [line2mesh_tri.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_tri.py) 2793 2794 ![](https://vedo.embl.es/images/advanced/line2mesh_tri.jpg) 2795 2796 - [line2mesh_quads.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/line2mesh_quads.py) 2797 2798 ![](https://vedo.embl.es/images/advanced/line2mesh_quads.png) 2799 """ 2800 if line_resolution is None: 2801 contour = vedo.shapes.Line(self.vertices) 2802 else: 2803 contour = vedo.shapes.Spline(self.vertices, smooth=smooth, res=line_resolution) 2804 contour.clean() 2805 2806 length = contour.length() 2807 density = length / contour.npoints 2808 # print(f"tomesh():\n\tline length = {length}") 2809 # print(f"\tdensity = {density} length/pt_separation") 2810 2811 x0, x1 = contour.xbounds() 2812 y0, y1 = contour.ybounds() 2813 2814 if grid is None: 2815 if mesh_resolution is None: 2816 resx = int((x1 - x0) / density + 0.5) 2817 resy = int((y1 - y0) / density + 0.5) 2818 # print(f"tmesh_resolution = {[resx, resy]}") 2819 else: 2820 if utils.is_sequence(mesh_resolution): 2821 resx, resy = mesh_resolution 2822 else: 2823 resx, resy = mesh_resolution, mesh_resolution 2824 grid = vedo.shapes.Grid( 2825 [(x0 + x1) / 2, (y0 + y1) / 2, 0], 2826 s=((x1 - x0) * 1.025, (y1 - y0) * 1.025), 2827 res=(resx, resy), 2828 ) 2829 else: 2830 grid = grid.clone() 2831 2832 cpts = contour.vertices 2833 2834 # make sure it's closed 2835 p0, p1 = cpts[0], cpts[-1] 2836 nj = max(2, int(utils.mag(p1 - p0) / density + 0.5)) 2837 joinline = vedo.shapes.Line(p1, p0, res=nj) 2838 contour = vedo.merge(contour, joinline).subsample(0.0001) 2839 2840 ####################################### quads 2841 if quads: 2842 cmesh = grid.clone().cut_with_point_loop(contour, on="cells", invert=invert) 2843 cmesh.wireframe(False).lw(0.5) 2844 cmesh.pipeline = utils.OperationNode( 2845 "generate_mesh", 2846 parents=[self, contour], 2847 comment=f"#quads {cmesh.dataset.GetNumberOfCells()}", 2848 ) 2849 return cmesh 2850 ############################################# 2851 2852 grid_tmp = grid.vertices.copy() 2853 2854 if jitter: 2855 np.random.seed(0) 2856 sigma = 1.0 / np.sqrt(grid.npoints) * grid.diagonal_size() * jitter 2857 # print(f"\tsigma jittering = {sigma}") 2858 grid_tmp += np.random.rand(grid.npoints, 3) * sigma 2859 grid_tmp[:, 2] = 0.0 2860 2861 todel = [] 2862 density /= np.sqrt(3) 2863 vgrid_tmp = Points(grid_tmp) 2864 2865 for p in contour.vertices: 2866 out = vgrid_tmp.closest_point(p, radius=density, return_point_id=True) 2867 todel += out.tolist() 2868 2869 grid_tmp = grid_tmp.tolist() 2870 for index in sorted(list(set(todel)), reverse=True): 2871 del grid_tmp[index] 2872 2873 points = contour.vertices.tolist() + grid_tmp 2874 if invert: 2875 boundary = list(reversed(range(contour.npoints))) 2876 else: 2877 boundary = list(range(contour.npoints)) 2878 2879 dln = Points(points).generate_delaunay2d(mode="xy", boundaries=[boundary]) 2880 dln.compute_normals(points=False) # fixes reversd faces 2881 dln.lw(1) 2882 2883 dln.pipeline = utils.OperationNode( 2884 "generate_mesh", 2885 parents=[self, contour], 2886 comment=f"#cells {dln.dataset.GetNumberOfCells()}", 2887 ) 2888 return dln
Generate a polygonal Mesh from a closed contour line. If line is not closed it will be closed with a straight segment.
Check also generate_delaunay2d()
.
Arguments:
- line_resolution : (int) resolution of the contour line. The default is None, in this case the contour is not resampled.
- mesh_resolution : (int) resolution of the internal triangles not touching the boundary.
- smooth : (float) smoothing of the contour before meshing.
- jitter : (float) add a small noise to the internal points.
- grid : (Grid) manually pass a Grid object. The default is True.
- quads : (bool) generate a mesh of quads instead of triangles.
- invert : (bool) flip the line orientation. The default is False.
Examples:
2890 def reconstruct_surface( 2891 self, 2892 dims=(100, 100, 100), 2893 radius=None, 2894 sample_size=None, 2895 hole_filling=True, 2896 bounds=(), 2897 padding=0.05, 2898 ) -> "vedo.Mesh": 2899 """ 2900 Surface reconstruction from a scattered cloud of points. 2901 2902 Arguments: 2903 dims : (int) 2904 number of voxels in x, y and z to control precision. 2905 radius : (float) 2906 radius of influence of each point. 2907 Smaller values generally improve performance markedly. 2908 Note that after the signed distance function is computed, 2909 any voxel taking on the value >= radius 2910 is presumed to be "unseen" or uninitialized. 2911 sample_size : (int) 2912 if normals are not present 2913 they will be calculated using this sample size per point. 2914 hole_filling : (bool) 2915 enables hole filling, this generates 2916 separating surfaces between the empty and unseen portions of the volume. 2917 bounds : (list) 2918 region in space in which to perform the sampling 2919 in format (xmin,xmax, ymin,ymax, zim, zmax) 2920 padding : (float) 2921 increase by this fraction the bounding box 2922 2923 Examples: 2924 - [recosurface.py](https://github.com/marcomusy/vedo/blob/master/examples/advanced/recosurface.py) 2925 2926 ![](https://vedo.embl.es/images/advanced/recosurface.png) 2927 """ 2928 if not utils.is_sequence(dims): 2929 dims = (dims, dims, dims) 2930 2931 sdf = vtki.new("SignedDistance") 2932 2933 if len(bounds) == 6: 2934 sdf.SetBounds(bounds) 2935 else: 2936 x0, x1, y0, y1, z0, z1 = self.bounds() 2937 sdf.SetBounds( 2938 x0 - (x1 - x0) * padding, 2939 x1 + (x1 - x0) * padding, 2940 y0 - (y1 - y0) * padding, 2941 y1 + (y1 - y0) * padding, 2942 z0 - (z1 - z0) * padding, 2943 z1 + (z1 - z0) * padding, 2944 ) 2945 2946 bb = sdf.GetBounds() 2947 if bb[0]==bb[1]: 2948 vedo.logger.warning("reconstruct_surface(): zero x-range") 2949 if bb[2]==bb[3]: 2950 vedo.logger.warning("reconstruct_surface(): zero y-range") 2951 if bb[4]==bb[5]: 2952 vedo.logger.warning("reconstruct_surface(): zero z-range") 2953 2954 pd = self.dataset 2955 2956 if pd.GetPointData().GetNormals(): 2957 sdf.SetInputData(pd) 2958 else: 2959 normals = vtki.new("PCANormalEstimation") 2960 normals.SetInputData(pd) 2961 if not sample_size: 2962 sample_size = int(pd.GetNumberOfPoints() / 50) 2963 normals.SetSampleSize(sample_size) 2964 normals.SetNormalOrientationToGraphTraversal() 2965 sdf.SetInputConnection(normals.GetOutputPort()) 2966 # print("Recalculating normals with sample size =", sample_size) 2967 2968 if radius is None: 2969 radius = self.diagonal_size() / (sum(dims) / 3) * 5 2970 # print("Calculating mesh from points with radius =", radius) 2971 2972 sdf.SetRadius(radius) 2973 sdf.SetDimensions(dims) 2974 sdf.Update() 2975 2976 surface = vtki.new("ExtractSurface") 2977 surface.SetRadius(radius * 0.99) 2978 surface.SetHoleFilling(hole_filling) 2979 surface.ComputeNormalsOff() 2980 surface.ComputeGradientsOff() 2981 surface.SetInputConnection(sdf.GetOutputPort()) 2982 surface.Update() 2983 m = vedo.mesh.Mesh(surface.GetOutput(), c=self.color()) 2984 2985 m.pipeline = utils.OperationNode( 2986 "reconstruct_surface", 2987 parents=[self], 2988 comment=f"#pts {m.dataset.GetNumberOfPoints()}", 2989 ) 2990 return m
Surface reconstruction from a scattered cloud of points.
Arguments:
- dims : (int) number of voxels in x, y and z to control precision.
- radius : (float) radius of influence of each point. Smaller values generally improve performance markedly. Note that after the signed distance function is computed, any voxel taking on the value >= radius is presumed to be "unseen" or uninitialized.
- sample_size : (int) if normals are not present they will be calculated using this sample size per point.
- hole_filling : (bool) enables hole filling, this generates separating surfaces between the empty and unseen portions of the volume.
- bounds : (list) region in space in which to perform the sampling in format (xmin,xmax, ymin,ymax, zim, zmax)
- padding : (float) increase by this fraction the bounding box
Examples:
2992 def compute_clustering(self, radius: float) -> Self: 2993 """ 2994 Cluster points in space. The `radius` is the radius of local search. 2995 2996 An array named "ClusterId" is added to `pointdata`. 2997 2998 Examples: 2999 - [clustering.py](https://github.com/marcomusy/vedo/blob/master/examples/basic/clustering.py) 3000 3001 ![](https://vedo.embl.es/images/basic/clustering.png) 3002 """ 3003 cluster = vtki.new("EuclideanClusterExtraction") 3004 cluster.SetInputData(self.dataset) 3005 cluster.SetExtractionModeToAllClusters() 3006 cluster.SetRadius(radius) 3007 cluster.ColorClustersOn() 3008 cluster.Update() 3009 idsarr = cluster.GetOutput().GetPointData().GetArray("ClusterId") 3010 self.dataset.GetPointData().AddArray(idsarr) 3011 self.pipeline = utils.OperationNode( 3012 "compute_clustering", parents=[self], comment=f"radius = {radius}" 3013 ) 3014 return self
Cluster points in space. The radius
is the radius of local search.
An array named "ClusterId" is added to pointdata
.
Examples:
3016 def compute_connections(self, radius, mode=0, regions=(), vrange=(0, 1), seeds=(), angle=0.0) -> Self: 3017 """ 3018 Extracts and/or segments points from a point cloud based on geometric distance measures 3019 (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range. 3020 The default operation is to segment the points into "connected" regions where the connection 3021 is determined by an appropriate distance measure. Each region is given a region id. 3022 3023 Optionally, the filter can output the largest connected region of points; a particular region 3024 (via id specification); those regions that are seeded using a list of input point ids; 3025 or the region of points closest to a specified position. 3026 3027 The key parameter of this filter is the radius defining a sphere around each point which defines 3028 a local neighborhood: any other points in the local neighborhood are assumed connected to the point. 3029 Note that the radius is defined in absolute terms. 3030 3031 Other parameters are used to further qualify what it means to be a neighboring point. 3032 For example, scalar range and/or point normals can be used to further constrain the neighborhood. 3033 Also the extraction mode defines how the filter operates. 3034 By default, all regions are extracted but it is possible to extract particular regions; 3035 the region closest to a seed point; seeded regions; or the largest region found while processing. 3036 By default, all regions are extracted. 3037 3038 On output, all points are labeled with a region number. 3039 However note that the number of input and output points may not be the same: 3040 if not extracting all regions then the output size may be less than the input size. 3041 3042 Arguments: 3043 radius : (float) 3044 variable specifying a local sphere used to define local point neighborhood 3045 mode : (int) 3046 - 0, Extract all regions 3047 - 1, Extract point seeded regions 3048 - 2, Extract largest region 3049 - 3, Test specified regions 3050 - 4, Extract all regions with scalar connectivity 3051 - 5, Extract point seeded regions 3052 regions : (list) 3053 a list of non-negative regions id to extract 3054 vrange : (list) 3055 scalar range to use to extract points based on scalar connectivity 3056 seeds : (list) 3057 a list of non-negative point seed ids 3058 angle : (list) 3059 points are connected if the angle between their normals is 3060 within this angle threshold (expressed in degrees). 3061 """ 3062 # https://vtk.org/doc/nightly/html/classvtkConnectedPointsFilter.html 3063 cpf = vtki.new("ConnectedPointsFilter") 3064 cpf.SetInputData(self.dataset) 3065 cpf.SetRadius(radius) 3066 if mode == 0: # Extract all regions 3067 pass 3068 3069 elif mode == 1: # Extract point seeded regions 3070 cpf.SetExtractionModeToPointSeededRegions() 3071 for s in seeds: 3072 cpf.AddSeed(s) 3073 3074 elif mode == 2: # Test largest region 3075 cpf.SetExtractionModeToLargestRegion() 3076 3077 elif mode == 3: # Test specified regions 3078 cpf.SetExtractionModeToSpecifiedRegions() 3079 for r in regions: 3080 cpf.AddSpecifiedRegion(r) 3081 3082 elif mode == 4: # Extract all regions with scalar connectivity 3083 cpf.SetExtractionModeToLargestRegion() 3084 cpf.ScalarConnectivityOn() 3085 cpf.SetScalarRange(vrange[0], vrange[1]) 3086 3087 elif mode == 5: # Extract point seeded regions 3088 cpf.SetExtractionModeToLargestRegion() 3089 cpf.ScalarConnectivityOn() 3090 cpf.SetScalarRange(vrange[0], vrange[1]) 3091 cpf.AlignedNormalsOn() 3092 cpf.SetNormalAngle(angle) 3093 3094 cpf.Update() 3095 self._update(cpf.GetOutput(), reset_locators=False) 3096 return self
Extracts and/or segments points from a point cloud based on geometric distance measures (e.g., proximity, normal alignments, etc.) and optional measures such as scalar range. The default operation is to segment the points into "connected" regions where the connection is determined by an appropriate distance measure. Each region is given a region id.
Optionally, the filter can output the largest connected region of points; a particular region (via id specification); those regions that are seeded using a list of input point ids; or the region of points closest to a specified position.
The key parameter of this filter is the radius defining a sphere around each point which defines a local neighborhood: any other points in the local neighborhood are assumed connected to the point. Note that the radius is defined in absolute terms.
Other parameters are used to further qualify what it means to be a neighboring point. For example, scalar range and/or point normals can be used to further constrain the neighborhood. Also the extraction mode defines how the filter operates. By default, all regions are extracted but it is possible to extract particular regions; the region closest to a seed point; seeded regions; or the largest region found while processing. By default, all regions are extracted.
On output, all points are labeled with a region number. However note that the number of input and output points may not be the same: if not extracting all regions then the output size may be less than the input size.
Arguments:
- radius : (float) variable specifying a local sphere used to define local point neighborhood
- mode : (int)
- 0, Extract all regions
- 1, Extract point seeded regions
- 2, Extract largest region
- 3, Test specified regions
- 4, Extract all regions with scalar connectivity
- 5, Extract point seeded regions
- regions : (list) a list of non-negative regions id to extract
- vrange : (list) scalar range to use to extract points based on scalar connectivity
- seeds : (list) a list of non-negative point seed ids
- angle : (list) points are connected if the angle between their normals is within this angle threshold (expressed in degrees).
3098 def compute_camera_distance(self) -> np.ndarray: 3099 """ 3100 Calculate the distance from points to the camera. 3101 3102 A pointdata array is created with name 'DistanceToCamera' and returned. 3103 """ 3104 if vedo.plotter_instance and vedo.plotter_instance.renderer: 3105 poly = self.dataset 3106 dc = vtki.new("DistanceToCamera") 3107 dc.SetInputData(poly) 3108 dc.SetRenderer(vedo.plotter_instance.renderer) 3109 dc.Update() 3110 self._update(dc.GetOutput(), reset_locators=False) 3111 return self.pointdata["DistanceToCamera"] 3112 return np.array([])
Calculate the distance from points to the camera.
A pointdata array is created with name 'DistanceToCamera' and returned.
3114 def densify(self, target_distance=0.1, nclosest=6, radius=None, niter=1, nmax=None) -> Self: 3115 """ 3116 Return a copy of the cloud with new added points. 3117 The new points are created in such a way that all points in any local neighborhood are 3118 within a target distance of one another. 3119 3120 For each input point, the distance to all points in its neighborhood is computed. 3121 If any of its neighbors is further than the target distance, 3122 the edge connecting the point and its neighbor is bisected and 3123 a new point is inserted at the bisection point. 3124 A single pass is completed once all the input points are visited. 3125 Then the process repeats to the number of iterations. 3126 3127 Examples: 3128 - [densifycloud.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/densifycloud.py) 3129 3130 ![](https://vedo.embl.es/images/volumetric/densifycloud.png) 3131 3132 .. note:: 3133 Points will be created in an iterative fashion until all points in their 3134 local neighborhood are the target distance apart or less. 3135 Note that the process may terminate early due to the 3136 number of iterations. By default the target distance is set to 0.5. 3137 Note that the target_distance should be less than the radius 3138 or nothing will change on output. 3139 3140 .. warning:: 3141 This class can generate a lot of points very quickly. 3142 The maximum number of iterations is by default set to =1.0 for this reason. 3143 Increase the number of iterations very carefully. 3144 Also, `nmax` can be set to limit the explosion of points. 3145 It is also recommended that a N closest neighborhood is used. 3146 3147 """ 3148 src = vtki.new("ProgrammableSource") 3149 opts = self.vertices 3150 # zeros = np.zeros(3) 3151 3152 def _read_points(): 3153 output = src.GetPolyDataOutput() 3154 points = vtki.vtkPoints() 3155 for p in opts: 3156 # print(p) 3157 # if not np.array_equal(p, zeros): 3158 points.InsertNextPoint(p) 3159 output.SetPoints(points) 3160 3161 src.SetExecuteMethod(_read_points) 3162 3163 dens = vtki.new("DensifyPointCloudFilter") 3164 dens.SetInputConnection(src.GetOutputPort()) 3165 # dens.SetInputData(self.dataset) # this does not work 3166 dens.InterpolateAttributeDataOn() 3167 dens.SetTargetDistance(target_distance) 3168 dens.SetMaximumNumberOfIterations(niter) 3169 if nmax: 3170 dens.SetMaximumNumberOfPoints(nmax) 3171 3172 if radius: 3173 dens.SetNeighborhoodTypeToRadius() 3174 dens.SetRadius(radius) 3175 elif nclosest: 3176 dens.SetNeighborhoodTypeToNClosest() 3177 dens.SetNumberOfClosestPoints(nclosest) 3178 else: 3179 vedo.logger.error("set either radius or nclosest") 3180 raise RuntimeError() 3181 dens.Update() 3182 3183 cld = Points(dens.GetOutput()) 3184 cld.copy_properties_from(self) 3185 cld.interpolate_data_from(self, n=nclosest, radius=radius) 3186 cld.name = "DensifiedCloud" 3187 cld.pipeline = utils.OperationNode( 3188 "densify", 3189 parents=[self], 3190 c="#e9c46a:", 3191 comment=f"#pts {cld.dataset.GetNumberOfPoints()}", 3192 ) 3193 return cld
Return a copy of the cloud with new added points. The new points are created in such a way that all points in any local neighborhood are within a target distance of one another.
For each input point, the distance to all points in its neighborhood is computed. If any of its neighbors is further than the target distance, the edge connecting the point and its neighbor is bisected and a new point is inserted at the bisection point. A single pass is completed once all the input points are visited. Then the process repeats to the number of iterations.
Examples:
Points will be created in an iterative fashion until all points in their local neighborhood are the target distance apart or less. Note that the process may terminate early due to the number of iterations. By default the target distance is set to 0.5. Note that the target_distance should be less than the radius or nothing will change on output.
This class can generate a lot of points very quickly.
The maximum number of iterations is by default set to =1.0 for this reason.
Increase the number of iterations very carefully.
Also, nmax
can be set to limit the explosion of points.
It is also recommended that a N closest neighborhood is used.
3199 def density( 3200 self, dims=(40, 40, 40), bounds=None, radius=None, compute_gradient=False, locator=None 3201 ) -> "vedo.Volume": 3202 """ 3203 Generate a density field from a point cloud. Input can also be a set of 3D coordinates. 3204 Output is a `Volume`. 3205 3206 The local neighborhood is specified as the `radius` around each sample position (each voxel). 3207 If left to None, the radius is automatically computed as the diagonal of the bounding box 3208 and can be accessed via `vol.metadata["radius"]`. 3209 The density is expressed as the number of counts in the radius search. 3210 3211 Arguments: 3212 dims : (int, list) 3213 number of voxels in x, y and z of the output Volume. 3214 compute_gradient : (bool) 3215 Turn on/off the generation of the gradient vector, 3216 gradient magnitude scalar, and function classification scalar. 3217 By default this is off. Note that this will increase execution time 3218 and the size of the output. (The names of these point data arrays are: 3219 "Gradient", "Gradient Magnitude", and "Classification") 3220 locator : (vtkPointLocator) 3221 can be assigned from a previous call for speed (access it via `object.point_locator`). 3222 3223 Examples: 3224 - [plot_density3d.py](https://github.com/marcomusy/vedo/blob/master/examples/pyplot/plot_density3d.py) 3225 3226 ![](https://vedo.embl.es/images/pyplot/plot_density3d.png) 3227 """ 3228 pdf = vtki.new("PointDensityFilter") 3229 pdf.SetInputData(self.dataset) 3230 3231 if not utils.is_sequence(dims): 3232 dims = [dims, dims, dims] 3233 3234 if bounds is None: 3235 bounds = list(self.bounds()) 3236 elif len(bounds) == 4: 3237 bounds = [*bounds, 0, 0] 3238 3239 if bounds[5] - bounds[4] == 0 or len(dims) == 2: # its 2D 3240 dims = list(dims) 3241 dims = [dims[0], dims[1], 2] 3242 diag = self.diagonal_size() 3243 bounds[5] = bounds[4] + diag / 1000 3244 pdf.SetModelBounds(bounds) 3245 3246 pdf.SetSampleDimensions(dims) 3247 3248 if locator: 3249 pdf.SetLocator(locator) 3250 3251 pdf.SetDensityEstimateToFixedRadius() 3252 if radius is None: 3253 radius = self.diagonal_size() / 20 3254 pdf.SetRadius(radius) 3255 pdf.SetComputeGradient(compute_gradient) 3256 pdf.Update() 3257 3258 vol = vedo.Volume(pdf.GetOutput()).mode(1) 3259 vol.name = "PointDensity" 3260 vol.metadata["radius"] = radius 3261 vol.locator = pdf.GetLocator() 3262 vol.pipeline = utils.OperationNode( 3263 "density", parents=[self], comment=f"dims={tuple(vol.dimensions())}" 3264 ) 3265 return vol
Generate a density field from a point cloud. Input can also be a set of 3D coordinates.
Output is a Volume
.
The local neighborhood is specified as the radius
around each sample position (each voxel).
If left to None, the radius is automatically computed as the diagonal of the bounding box
and can be accessed via vol.metadata["radius"]
.
The density is expressed as the number of counts in the radius search.
Arguments:
- dims : (int, list) number of voxels in x, y and z of the output Volume.
- compute_gradient : (bool) Turn on/off the generation of the gradient vector, gradient magnitude scalar, and function classification scalar. By default this is off. Note that this will increase execution time and the size of the output. (The names of these point data arrays are: "Gradient", "Gradient Magnitude", and "Classification")
- locator : (vtkPointLocator)
can be assigned from a previous call for speed (access it via
object.point_locator
).
Examples:
3268 def tovolume( 3269 self, 3270 kernel="shepard", 3271 radius=None, 3272 n=None, 3273 bounds=None, 3274 null_value=None, 3275 dims=(25, 25, 25), 3276 ) -> "vedo.Volume": 3277 """ 3278 Generate a `Volume` by interpolating a scalar 3279 or vector field which is only known on a scattered set of points or mesh. 3280 Available interpolation kernels are: shepard, gaussian, or linear. 3281 3282 Arguments: 3283 kernel : (str) 3284 interpolation kernel type [shepard] 3285 radius : (float) 3286 radius of the local search 3287 n : (int) 3288 number of point to use for interpolation 3289 bounds : (list) 3290 bounding box of the output Volume object 3291 dims : (list) 3292 dimensions of the output Volume object 3293 null_value : (float) 3294 value to be assigned to invalid points 3295 3296 Examples: 3297 - [interpolate_volume.py](https://github.com/marcomusy/vedo/blob/master/examples/volumetric/interpolate_volume.py) 3298 3299 ![](https://vedo.embl.es/images/volumetric/59095175-1ec5a300-8918-11e9-8bc0-fd35c8981e2b.jpg) 3300 """ 3301 if radius is None and not n: 3302 vedo.logger.error("please set either radius or n") 3303 raise RuntimeError 3304 3305 poly = self.dataset 3306 3307 # Create a probe volume 3308 probe = vtki.vtkImageData() 3309 probe.SetDimensions(dims) 3310 if bounds is None: 3311 bounds = self.bounds() 3312 probe.SetOrigin(bounds[0], bounds[2], bounds[4]) 3313 probe.SetSpacing( 3314 (bounds[1] - bounds[0]) / dims[0], 3315 (bounds[3] - bounds[2]) / dims[1], 3316 (bounds[5] - bounds[4]) / dims[2], 3317 ) 3318 3319 if not self.point_locator: 3320 self.point_locator = vtki.new("PointLocator") 3321 self.point_locator.SetDataSet(poly) 3322 self.point_locator.BuildLocator() 3323 3324 if kernel == "shepard": 3325 kern = vtki.new("ShepardKernel") 3326 kern.SetPowerParameter(2) 3327 elif kernel == "gaussian": 3328 kern = vtki.new("GaussianKernel") 3329 elif kernel == "linear": 3330 kern = vtki.new("LinearKernel") 3331 else: 3332 vedo.logger.error("Error in tovolume(), available kernels are:") 3333 vedo.logger.error(" [shepard, gaussian, linear]") 3334 raise RuntimeError() 3335 3336 if radius: 3337 kern.SetRadius(radius) 3338 3339 interpolator = vtki.new("PointInterpolator") 3340 interpolator.SetInputData(probe) 3341 interpolator.SetSourceData(poly) 3342 interpolator.SetKernel(kern) 3343 interpolator.SetLocator(self.point_locator) 3344 3345 if n: 3346 kern.SetNumberOfPoints(n) 3347 kern.SetKernelFootprintToNClosest() 3348 else: 3349 kern.SetRadius(radius) 3350 3351 if null_value is not None: 3352 interpolator.SetNullValue(null_value) 3353 else: 3354 interpolator.SetNullPointsStrategyToClosestPoint() 3355 interpolator.Update() 3356 3357 vol = vedo.Volume(interpolator.GetOutput()) 3358 3359 vol.pipeline = utils.OperationNode( 3360 "signed_distance", 3361 parents=[self], 3362 comment=f"dims={tuple(vol.dimensions())}", 3363 c="#e9c46a:#0096c7", 3364 ) 3365 return vol
Generate a Volume
by interpolating a scalar
or vector field which is only known on a scattered set of points or mesh.
Available interpolation kernels are: shepard, gaussian, or linear.
Arguments:
- kernel : (str) interpolation kernel type [shepard]
- radius : (float) radius of the local search
- n : (int) number of point to use for interpolation
- bounds : (list) bounding box of the output Volume object
- dims : (list) dimensions of the output Volume object
- null_value : (float) value to be assigned to invalid points
Examples:
3368 def generate_segments(self, istart=0, rmax=1e30, niter=3) -> "vedo.shapes.Lines": 3369 """ 3370 Generate a line segments from a set of points. 3371 The algorithm is based on the closest point search. 3372 3373 Returns a `Line` object. 3374 This object contains the a metadata array of used vertex counts in "UsedVertexCount" 3375 and the sum of the length of the segments in "SegmentsLengthSum". 3376 3377 Arguments: 3378 istart : (int) 3379 index of the starting point 3380 rmax : (float) 3381 maximum length of a segment 3382 niter : (int) 3383 number of iterations or passes through the points 3384 3385 Examples: 3386 - [moving_least_squares1D.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/moving_least_squares1D.py) 3387 """ 3388 points = self.vertices 3389 segments = [] 3390 dists = [] 3391 n = len(points) 3392 used = np.zeros(n, dtype=int) 3393 for _ in range(niter): 3394 i = istart 3395 for _ in range(n): 3396 p = points[i] 3397 ids = self.closest_point(p, n=4, return_point_id=True) 3398 j = ids[1] 3399 if used[j] > 1 or [j, i] in segments: 3400 j = ids[2] 3401 if used[j] > 1: 3402 j = ids[3] 3403 d = np.linalg.norm(p - points[j]) 3404 if used[j] > 1 or used[i] > 1 or d > rmax: 3405 i += 1 3406 if i >= n: 3407 i = 0 3408 continue 3409 used[i] += 1 3410 used[j] += 1 3411 segments.append([i, j]) 3412 dists.append(d) 3413 i = j 3414 segments = np.array(segments, dtype=int) 3415 3416 lines = vedo.shapes.Lines(points[segments], c="k", lw=3) 3417 lines.metadata["UsedVertexCount"] = used 3418 lines.metadata["SegmentsLengthSum"] = np.sum(dists) 3419 lines.pipeline = utils.OperationNode("generate_segments", parents=[self]) 3420 lines.name = "Segments" 3421 return lines
Generate a line segments from a set of points. The algorithm is based on the closest point search.
Returns a Line
object.
This object contains the a metadata array of used vertex counts in "UsedVertexCount"
and the sum of the length of the segments in "SegmentsLengthSum".
Arguments:
- istart : (int) index of the starting point
- rmax : (float) maximum length of a segment
- niter : (int) number of iterations or passes through the points
Examples:
3423 def generate_delaunay2d( 3424 self, 3425 mode="scipy", 3426 boundaries=(), 3427 tol=None, 3428 alpha=0.0, 3429 offset=0.0, 3430 transform=None, 3431 ) -> "vedo.mesh.Mesh": 3432 """ 3433 Create a mesh from points in the XY plane. 3434 If `mode='fit'` then the filter computes a best fitting 3435 plane and projects the points onto it. 3436 3437 Check also `generate_mesh()`. 3438 3439 Arguments: 3440 tol : (float) 3441 specify a tolerance to control discarding of closely spaced points. 3442 This tolerance is specified as a fraction of the diagonal length of the bounding box of the points. 3443 alpha : (float) 3444 for a non-zero alpha value, only edges or triangles contained 3445 within a sphere centered at mesh vertices will be output. 3446 Otherwise, only triangles will be output. 3447 offset : (float) 3448 multiplier to control the size of the initial, bounding Delaunay triangulation. 3449 transform: (LinearTransform, NonLinearTransform) 3450 a transformation which is applied to points to generate a 2D problem. 3451 This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane. 3452 The points are transformed and triangulated. 3453 The topology of triangulated points is used as the output topology. 3454 3455 Examples: 3456 - [delaunay2d.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/delaunay2d.py) 3457 3458 ![](https://vedo.embl.es/images/basic/delaunay2d.png) 3459 """ 3460 plist = self.vertices.copy() 3461 3462 ######################################################### 3463 if mode == "scipy": 3464 from scipy.spatial import Delaunay as scipy_delaunay 3465 3466 tri = scipy_delaunay(plist[:, 0:2]) 3467 return vedo.mesh.Mesh([plist, tri.simplices]) 3468 ########################################################## 3469 3470 pd = vtki.vtkPolyData() 3471 vpts = vtki.vtkPoints() 3472 vpts.SetData(utils.numpy2vtk(plist, dtype=np.float32)) 3473 pd.SetPoints(vpts) 3474 3475 delny = vtki.new("Delaunay2D") 3476 delny.SetInputData(pd) 3477 if tol: 3478 delny.SetTolerance(tol) 3479 delny.SetAlpha(alpha) 3480 delny.SetOffset(offset) 3481 3482 if transform: 3483 delny.SetTransform(transform.T) 3484 elif mode == "fit": 3485 delny.SetProjectionPlaneMode(vtki.get_class("VTK_BEST_FITTING_PLANE")) 3486 elif mode == "xy" and boundaries: 3487 boundary = vtki.vtkPolyData() 3488 boundary.SetPoints(vpts) 3489 cell_array = vtki.vtkCellArray() 3490 for b in boundaries: 3491 cpolygon = vtki.vtkPolygon() 3492 for idd in b: 3493 cpolygon.GetPointIds().InsertNextId(idd) 3494 cell_array.InsertNextCell(cpolygon) 3495 boundary.SetPolys(cell_array) 3496 delny.SetSourceData(boundary) 3497 3498 delny.Update() 3499 3500 msh = vedo.mesh.Mesh(delny.GetOutput()) 3501 msh.name = "Delaunay2D" 3502 msh.clean().lighting("off") 3503 msh.pipeline = utils.OperationNode( 3504 "delaunay2d", 3505 parents=[self], 3506 comment=f"#cells {msh.dataset.GetNumberOfCells()}", 3507 ) 3508 return msh
Create a mesh from points in the XY plane.
If mode='fit'
then the filter computes a best fitting
plane and projects the points onto it.
Check also generate_mesh()
.
Arguments:
- tol : (float) specify a tolerance to control discarding of closely spaced points. This tolerance is specified as a fraction of the diagonal length of the bounding box of the points.
- alpha : (float) for a non-zero alpha value, only edges or triangles contained within a sphere centered at mesh vertices will be output. Otherwise, only triangles will be output.
- offset : (float) multiplier to control the size of the initial, bounding Delaunay triangulation.
- transform: (LinearTransform, NonLinearTransform) a transformation which is applied to points to generate a 2D problem. This maps a 3D dataset into a 2D dataset where triangulation can be done on the XY plane. The points are transformed and triangulated. The topology of triangulated points is used as the output topology.
Examples:
3510 def generate_voronoi(self, padding=0.0, fit=False, method="vtk") -> "vedo.Mesh": 3511 """ 3512 Generate the 2D Voronoi convex tiling of the input points (z is ignored). 3513 The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon. 3514 3515 A cell array named "VoronoiID" is added to the output Mesh. 3516 3517 The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest 3518 to one of the input points. Voronoi tessellations are important in computational geometry 3519 (and many other fields), and are the dual of Delaunay triangulations. 3520 3521 Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored 3522 (although carried through to the output). 3523 If you desire to triangulate in a different plane, you can use fit=True. 3524 3525 A brief summary is as follows. Each (generating) input point is associated with 3526 an initial Voronoi tile, which is simply the bounding box of the point set. 3527 A locator is then used to identify nearby points: each neighbor in turn generates a 3528 clipping line positioned halfway between the generating point and the neighboring point, 3529 and orthogonal to the line connecting them. Clips are readily performed by evaluationg the 3530 vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line. 3531 If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip 3532 line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs, 3533 the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region 3534 containing the neighboring clip points. The clip region (along with the points contained in it) is grown 3535 by careful expansion (e.g., outward spiraling iterator over all candidate clip points). 3536 When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi 3537 tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi 3538 tessellation. Note that topological and geometric information is used to generate a valid triangulation 3539 (e.g., merging points and validating topology). 3540 3541 Arguments: 3542 pts : (list) 3543 list of input points. 3544 padding : (float) 3545 padding distance. The default is 0. 3546 fit : (bool) 3547 detect automatically the best fitting plane. The default is False. 3548 3549 Examples: 3550 - [voronoi1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi1.py) 3551 3552 ![](https://vedo.embl.es/images/basic/voronoi1.png) 3553 3554 - [voronoi2.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/voronoi2.py) 3555 3556 ![](https://vedo.embl.es/images/advanced/voronoi2.png) 3557 """ 3558 pts = self.vertices 3559 3560 if method == "scipy": 3561 from scipy.spatial import Voronoi as scipy_voronoi 3562 3563 pts = np.asarray(pts)[:, (0, 1)] 3564 vor = scipy_voronoi(pts) 3565 regs = [] # filter out invalid indices 3566 for r in vor.regions: 3567 flag = True 3568 for x in r: 3569 if x < 0: 3570 flag = False 3571 break 3572 if flag and len(r) > 0: 3573 regs.append(r) 3574 3575 m = vedo.Mesh([vor.vertices, regs]) 3576 m.celldata["VoronoiID"] = np.array(list(range(len(regs)))).astype(int) 3577 3578 elif method == "vtk": 3579 vor = vtki.new("Voronoi2D") 3580 if isinstance(pts, Points): 3581 vor.SetInputData(pts) 3582 else: 3583 pts = np.asarray(pts) 3584 if pts.shape[1] == 2: 3585 pts = np.c_[pts, np.zeros(len(pts))] 3586 pd = vtki.vtkPolyData() 3587 vpts = vtki.vtkPoints() 3588 vpts.SetData(utils.numpy2vtk(pts, dtype=np.float32)) 3589 pd.SetPoints(vpts) 3590 vor.SetInputData(pd) 3591 vor.SetPadding(padding) 3592 vor.SetGenerateScalarsToPointIds() 3593 if fit: 3594 vor.SetProjectionPlaneModeToBestFittingPlane() 3595 else: 3596 vor.SetProjectionPlaneModeToXYPlane() 3597 vor.Update() 3598 poly = vor.GetOutput() 3599 arr = poly.GetCellData().GetArray(0) 3600 if arr: 3601 arr.SetName("VoronoiID") 3602 m = vedo.Mesh(poly, c="orange5") 3603 3604 else: 3605 vedo.logger.error(f"Unknown method {method} in voronoi()") 3606 raise RuntimeError 3607 3608 m.lw(2).lighting("off").wireframe() 3609 m.name = "Voronoi" 3610 return m
Generate the 2D Voronoi convex tiling of the input points (z is ignored). The points are assumed to lie in a plane. The output is a Mesh. Each output cell is a convex polygon.
A cell array named "VoronoiID" is added to the output Mesh.
The 2D Voronoi tessellation is a tiling of space, where each Voronoi tile represents the region nearest to one of the input points. Voronoi tessellations are important in computational geometry (and many other fields), and are the dual of Delaunay triangulations.
Thus the triangulation is constructed in the x-y plane, and the z coordinate is ignored (although carried through to the output). If you desire to triangulate in a different plane, you can use fit=True.
A brief summary is as follows. Each (generating) input point is associated with an initial Voronoi tile, which is simply the bounding box of the point set. A locator is then used to identify nearby points: each neighbor in turn generates a clipping line positioned halfway between the generating point and the neighboring point, and orthogonal to the line connecting them. Clips are readily performed by evaluationg the vertices of the convex Voronoi tile as being on either side (inside,outside) of the clip line. If two intersections of the Voronoi tile are found, the portion of the tile "outside" the clip line is discarded, resulting in a new convex, Voronoi tile. As each clip occurs, the Voronoi "Flower" error metric (the union of error spheres) is compared to the extent of the region containing the neighboring clip points. The clip region (along with the points contained in it) is grown by careful expansion (e.g., outward spiraling iterator over all candidate clip points). When the Voronoi Flower is contained within the clip region, the algorithm terminates and the Voronoi tile is output. Once complete, it is possible to construct the Delaunay triangulation from the Voronoi tessellation. Note that topological and geometric information is used to generate a valid triangulation (e.g., merging points and validating topology).
Arguments:
- pts : (list) list of input points.
- padding : (float) padding distance. The default is 0.
- fit : (bool) detect automatically the best fitting plane. The default is False.
Examples:
3613 def generate_delaunay3d(self, radius=0, tol=None) -> "vedo.TetMesh": 3614 """ 3615 Create 3D Delaunay triangulation of input points. 3616 3617 Arguments: 3618 radius : (float) 3619 specify distance (or "alpha") value to control output. 3620 For a non-zero values, only tetra contained within the circumsphere 3621 will be output. 3622 tol : (float) 3623 Specify a tolerance to control discarding of closely spaced points. 3624 This tolerance is specified as a fraction of the diagonal length of 3625 the bounding box of the points. 3626 """ 3627 deln = vtki.new("Delaunay3D") 3628 deln.SetInputData(self.dataset) 3629 deln.SetAlpha(radius) 3630 deln.AlphaTetsOn() 3631 deln.AlphaTrisOff() 3632 deln.AlphaLinesOff() 3633 deln.AlphaVertsOff() 3634 deln.BoundingTriangulationOff() 3635 if tol: 3636 deln.SetTolerance(tol) 3637 deln.Update() 3638 m = vedo.TetMesh(deln.GetOutput()) 3639 m.pipeline = utils.OperationNode( 3640 "generate_delaunay3d", c="#e9c46a:#edabab", parents=[self], 3641 ) 3642 m.name = "Delaunay3D" 3643 return m
Create 3D Delaunay triangulation of input points.
Arguments:
- radius : (float) specify distance (or "alpha") value to control output. For a non-zero values, only tetra contained within the circumsphere will be output.
- tol : (float) Specify a tolerance to control discarding of closely spaced points. This tolerance is specified as a fraction of the diagonal length of the bounding box of the points.
3646 def visible_points(self, area=(), tol=None, invert=False) -> Union[Self, None]: 3647 """ 3648 Extract points based on whether they are visible or not. 3649 Visibility is determined by accessing the z-buffer of a rendering window. 3650 The position of each input point is converted into display coordinates, 3651 and then the z-value at that point is obtained. 3652 If within the user-specified tolerance, the point is considered visible. 3653 Associated data attributes are passed to the output as well. 3654 3655 This filter also allows you to specify a rectangular window in display (pixel) 3656 coordinates in which the visible points must lie. 3657 3658 Arguments: 3659 area : (list) 3660 specify a rectangular region as (xmin,xmax,ymin,ymax) 3661 tol : (float) 3662 a tolerance in normalized display coordinate system 3663 invert : (bool) 3664 select invisible points instead. 3665 3666 Example: 3667 ```python 3668 from vedo import Ellipsoid, show 3669 s = Ellipsoid().rotate_y(30) 3670 3671 # Camera options: pos, focal_point, viewup, distance 3672 camopts = dict(pos=(0,0,25), focal_point=(0,0,0)) 3673 show(s, camera=camopts, offscreen=True) 3674 3675 m = s.visible_points() 3676 # print('visible pts:', m.vertices) # numpy array 3677 show(m, new=True, axes=1).close() # optionally draw result in a new window 3678 ``` 3679 ![](https://vedo.embl.es/images/feats/visible_points.png) 3680 """ 3681 svp = vtki.new("SelectVisiblePoints") 3682 svp.SetInputData(self.dataset) 3683 3684 ren = None 3685 if vedo.plotter_instance: 3686 if vedo.plotter_instance.renderer: 3687 ren = vedo.plotter_instance.renderer 3688 svp.SetRenderer(ren) 3689 if not ren: 3690 vedo.logger.warning( 3691 "visible_points() can only be used after a rendering step" 3692 ) 3693 return None 3694 3695 if len(area) == 2: 3696 area = utils.flatten(area) 3697 if len(area) == 4: 3698 # specify a rectangular region 3699 svp.SetSelection(area[0], area[1], area[2], area[3]) 3700 if tol is not None: 3701 svp.SetTolerance(tol) 3702 if invert: 3703 svp.SelectInvisibleOn() 3704 svp.Update() 3705 3706 m = Points(svp.GetOutput()) 3707 m.name = "VisiblePoints" 3708 return m
Extract points based on whether they are visible or not. Visibility is determined by accessing the z-buffer of a rendering window. The position of each input point is converted into display coordinates, and then the z-value at that point is obtained. If within the user-specified tolerance, the point is considered visible. Associated data attributes are passed to the output as well.
This filter also allows you to specify a rectangular window in display (pixel) coordinates in which the visible points must lie.
Arguments:
- area : (list) specify a rectangular region as (xmin,xmax,ymin,ymax)
- tol : (float) a tolerance in normalized display coordinate system
- invert : (bool) select invisible points instead.
Example:
from vedo import Ellipsoid, show s = Ellipsoid().rotate_y(30) # Camera options: pos, focal_point, viewup, distance camopts = dict(pos=(0,0,25), focal_point=(0,0,0)) show(s, camera=camopts, offscreen=True) m = s.visible_points() # print('visible pts:', m.vertices) # numpy array show(m, new=True, axes=1).close() # optionally draw result in a new window
Inherited Members
- vedo.visual.PointsVisual
- clone2d
- copy_properties_from
- color
- c
- alpha
- lut_color_at
- opacity
- force_opaque
- force_translucent
- point_size
- ps
- render_points_as_spheres
- lighting
- point_blurring
- cellcolors
- pointcolors
- cmap
- add_trail
- update_trail
- add_shadow
- update_shadows
- labels
- labels2d
- legend
- flagpole
- flagpost
- vedo.visual.CommonVisual
- LUT
- scalar_range
- add_observer
- invoke_event
- show
- thumbnail
- pickable
- use_bounds
- draggable
- on
- off
- toggle
- add_scalarbar
- add_scalarbar3d
- vedo.core.PointAlgorithms
- apply_transform
- apply_transform_from_actor
- pos
- shift
- x
- y
- z
- rotate
- rotate_x
- rotate_y
- rotate_z
- reorient
- scale
- vedo.core.CommonAlgorithms
- pointdata
- celldata
- metadata
- memory_address
- memory_size
- modified
- box
- update_dataset
- bounds
- xbounds
- ybounds
- zbounds
- diagonal_size
- average_size
- center_of_mass
- copy_data_from
- inputdata
- npoints
- nvertices
- ncells
- points
- cell_centers
- lines
- lines_as_flat_array
- mark_boundaries
- find_cells_in_bounds
- find_cells_along_line
- find_cells_along_plane
- keep_cell_types
- map_cells_to_points
- vertices
- coordinates
- cells_as_flat_array
- cells
- cell_edge_neighbors
- map_points_to_cells
- resample_data_from
- interpolate_data_from
- add_ids
- gradient
- divergence
- vorticity
- probe
- compute_cell_size
- generate_random_data
- integrate_data
- write
- tomesh
- signed_distance
- unsigned_distance
- smooth_data
- compute_streamlines
442def Point(pos=(0, 0, 0), r=12, c="red", alpha=1.0) -> Self: 443 """ 444 Create a simple point in space. 445 446 .. note:: if you are creating many points you should use class `Points` instead! 447 """ 448 pt = Points([[0,0,0]], r, c, alpha).pos(pos) 449 pt.name = "Point" 450 return pt
Create a simple point in space.
if you are creating many points you should use class Points
instead!
3711class CellCenters(Points): 3712 def __init__(self, pcloud): 3713 """ 3714 Generate `Points` at the center of the cells of any type of object. 3715 3716 Check out also `cell_centers()`. 3717 """ 3718 vcen = vtki.new("CellCenters") 3719 vcen.CopyArraysOn() 3720 vcen.VertexCellsOn() 3721 # vcen.ConvertGhostCellsToGhostPointsOn() 3722 try: 3723 vcen.SetInputData(pcloud.dataset) 3724 except AttributeError: 3725 vcen.SetInputData(pcloud) 3726 vcen.Update() 3727 super().__init__(vcen.GetOutput()) 3728 self.name = "CellCenters"
Work with point clouds.
3712 def __init__(self, pcloud): 3713 """ 3714 Generate `Points` at the center of the cells of any type of object. 3715 3716 Check out also `cell_centers()`. 3717 """ 3718 vcen = vtki.new("CellCenters") 3719 vcen.CopyArraysOn() 3720 vcen.VertexCellsOn() 3721 # vcen.ConvertGhostCellsToGhostPointsOn() 3722 try: 3723 vcen.SetInputData(pcloud.dataset) 3724 except AttributeError: 3725 vcen.SetInputData(pcloud) 3726 vcen.Update() 3727 super().__init__(vcen.GetOutput()) 3728 self.name = "CellCenters"
Generate Points
at the center of the cells of any type of object.
Check out also cell_centers()
.
Inherited Members
- Points
- polydata
- copy
- clone
- compute_normals_with_pca
- compute_acoplanarity
- distance_to
- clean
- subsample
- threshold
- quantize
- vertex_normals
- point_normals
- align_to
- align_to_bounding_box
- align_with_landmarks
- normalize
- mirror
- flip_normals
- add_gaussian_noise
- closest_point
- auto_distance
- hausdorff_distance
- chamfer_distance
- remove_outliers
- relax_point_positions
- smooth_mls_1d
- smooth_mls_2d
- smooth_lloyd_2d
- project_on_plane
- warp
- cut_with_plane
- cut_with_planes
- cut_with_box
- cut_with_line
- cut_with_cylinder
- cut_with_sphere
- cut_with_mesh
- cut_with_point_loop
- cut_with_scalar
- crop
- generate_surface_halo
- generate_mesh
- reconstruct_surface
- compute_clustering
- compute_connections
- compute_camera_distance
- densify
- density
- tovolume
- generate_segments
- generate_delaunay2d
- generate_voronoi
- generate_delaunay3d
- visible_points
- vedo.visual.PointsVisual
- clone2d
- copy_properties_from
- color
- c
- alpha
- lut_color_at
- opacity
- force_opaque
- force_translucent
- point_size
- ps
- render_points_as_spheres
- lighting
- point_blurring
- cellcolors
- pointcolors
- cmap
- add_trail
- update_trail
- add_shadow
- update_shadows
- labels
- labels2d
- legend
- flagpole
- flagpost
- vedo.visual.CommonVisual
- LUT
- scalar_range
- add_observer
- invoke_event
- show
- thumbnail
- pickable
- use_bounds
- draggable
- on
- off
- toggle
- add_scalarbar
- add_scalarbar3d
- vedo.core.PointAlgorithms
- apply_transform
- apply_transform_from_actor
- pos
- shift
- x
- y
- z
- rotate
- rotate_x
- rotate_y
- rotate_z
- reorient
- scale
- vedo.core.CommonAlgorithms
- pointdata
- celldata
- metadata
- memory_address
- memory_size
- modified
- box
- update_dataset
- bounds
- xbounds
- ybounds
- zbounds
- diagonal_size
- average_size
- center_of_mass
- copy_data_from
- inputdata
- npoints
- nvertices
- ncells
- points
- cell_centers
- lines
- lines_as_flat_array
- mark_boundaries
- find_cells_in_bounds
- find_cells_along_line
- find_cells_along_plane
- keep_cell_types
- map_cells_to_points
- vertices
- coordinates
- cells_as_flat_array
- cells
- cell_edge_neighbors
- map_points_to_cells
- resample_data_from
- interpolate_data_from
- add_ids
- gradient
- divergence
- vorticity
- probe
- compute_cell_size
- generate_random_data
- integrate_data
- write
- tomesh
- signed_distance
- unsigned_distance
- smooth_data
- compute_streamlines
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:
136def fit_line(points: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Line": 137 """ 138 Fits a line through points. 139 140 Extra info is stored in `Line.slope`, `Line.center`, `Line.variances`. 141 142 Examples: 143 - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py) 144 145 ![](https://vedo.embl.es/images/advanced/fitline.png) 146 """ 147 if isinstance(points, Points): 148 points = points.vertices 149 data = np.asarray(points) 150 datamean = data.mean(axis=0) 151 _, dd, vv = np.linalg.svd(data - datamean) 152 vv = vv[0] / np.linalg.norm(vv[0]) 153 # vv contains the first principal component, i.e. the direction 154 # vector of the best fit line in the least squares sense. 155 xyz_min = data.min(axis=0) 156 xyz_max = data.max(axis=0) 157 a = np.linalg.norm(xyz_min - datamean) 158 b = np.linalg.norm(xyz_max - datamean) 159 p1 = datamean - a * vv 160 p2 = datamean + b * vv 161 line = vedo.shapes.Line(p1, p2, lw=1) 162 line.slope = vv 163 line.center = datamean 164 line.variances = dd 165 return line
Fits a line through points.
Extra info is stored in Line.slope
, Line.center
, Line.variances
.
Examples:
168def fit_circle(points: Union[np.ndarray, "vedo.Points"]) -> tuple: 169 """ 170 Fits a circle through a set of 3D points, with a very fast non-iterative method. 171 172 Returns the tuple `(center, radius, normal_to_circle)`. 173 174 .. warning:: 175 trying to fit s-shaped points will inevitably lead to instabilities and 176 circles of small radius. 177 178 References: 179 *J.F. Crawford, Nucl. Instr. Meth. 211, 1983, 223-225.* 180 """ 181 if isinstance(points, Points): 182 points = points.vertices 183 data = np.asarray(points) 184 185 offs = data.mean(axis=0) 186 data, n0 = _rotate_points(data - offs) 187 188 xi = data[:, 0] 189 yi = data[:, 1] 190 191 x = sum(xi) 192 xi2 = xi * xi 193 xx = sum(xi2) 194 xxx = sum(xi2 * xi) 195 196 y = sum(yi) 197 yi2 = yi * yi 198 yy = sum(yi2) 199 yyy = sum(yi2 * yi) 200 201 xiyi = xi * yi 202 xy = sum(xiyi) 203 xyy = sum(xiyi * yi) 204 xxy = sum(xi * xiyi) 205 206 N = len(xi) 207 k = (xx + yy) / N 208 209 a1 = xx - x * x / N 210 b1 = xy - x * y / N 211 c1 = 0.5 * (xxx + xyy - x * k) 212 213 a2 = xy - x * y / N 214 b2 = yy - y * y / N 215 c2 = 0.5 * (xxy + yyy - y * k) 216 217 d = a2 * b1 - a1 * b2 218 if not d: 219 return offs, 0, n0 220 x0 = (b1 * c2 - b2 * c1) / d 221 y0 = (c1 - a1 * x0) / b1 222 223 R = np.sqrt(x0 * x0 + y0 * y0 - 1 / N * (2 * x0 * x + 2 * y0 * y - xx - yy)) 224 225 c, _ = _rotate_points([x0, y0, 0], (0, 0, 1), n0) 226 227 return c[0] + offs, R, n0
Fits a circle through a set of 3D points, with a very fast non-iterative method.
Returns the tuple (center, radius, normal_to_circle)
.
trying to fit s-shaped points will inevitably lead to instabilities and circles of small radius.
References:
J.F. Crawford, Nucl. Instr. Meth. 211, 1983, 223-225.
230def fit_plane(points: Union[np.ndarray, "vedo.Points"], signed=False) -> "vedo.shapes.Plane": 231 """ 232 Fits a plane to a set of points. 233 234 Extra info is stored in `Plane.normal`, `Plane.center`, `Plane.variance`. 235 236 Arguments: 237 signed : (bool) 238 if True flip sign of the normal based on the ordering of the points 239 240 Examples: 241 - [fitline.py](https://github.com/marcomusy/vedo/tree/master/examples/advanced/fitline.py) 242 243 ![](https://vedo.embl.es/images/advanced/fitline.png) 244 """ 245 if isinstance(points, Points): 246 points = points.vertices 247 data = np.asarray(points) 248 datamean = data.mean(axis=0) 249 pts = data - datamean 250 res = np.linalg.svd(pts) 251 dd, vv = res[1], res[2] 252 n = np.cross(vv[0], vv[1]) 253 if signed: 254 v = np.zeros_like(pts) 255 for i in range(len(pts) - 1): 256 vi = np.cross(pts[i], pts[i + 1]) 257 v[i] = vi / np.linalg.norm(vi) 258 ns = np.mean(v, axis=0) # normal to the points plane 259 if np.dot(n, ns) < 0: 260 n = -n 261 xyz_min = data.min(axis=0) 262 xyz_max = data.max(axis=0) 263 s = np.linalg.norm(xyz_max - xyz_min) 264 pla = vedo.shapes.Plane(datamean, n, s=[s, s]) 265 pla.variance = dd[2] 266 pla.name = "FitPlane" 267 return pla
Fits a plane to a set of points.
Extra info is stored in Plane.normal
, Plane.center
, Plane.variance
.
Arguments:
- signed : (bool) if True flip sign of the normal based on the ordering of the points
Examples:
270def fit_sphere(coords: Union[np.ndarray, "vedo.Points"]) -> "vedo.shapes.Sphere": 271 """ 272 Fits a sphere to a set of points. 273 274 Extra info is stored in `Sphere.radius`, `Sphere.center`, `Sphere.residue`. 275 276 Examples: 277 - [fitspheres1.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/fitspheres1.py) 278 279 ![](https://vedo.embl.es/images/advanced/fitspheres1.jpg) 280 """ 281 if isinstance(coords, Points): 282 coords = coords.vertices 283 coords = np.array(coords) 284 n = len(coords) 285 A = np.zeros((n, 4)) 286 A[:, :-1] = coords * 2 287 A[:, 3] = 1 288 f = np.zeros((n, 1)) 289 x = coords[:, 0] 290 y = coords[:, 1] 291 z = coords[:, 2] 292 f[:, 0] = x * x + y * y + z * z 293 try: 294 C, residue, rank, _ = np.linalg.lstsq(A, f, rcond=-1) # solve AC=f 295 except: 296 C, residue, rank, _ = np.linalg.lstsq(A, f) # solve AC=f 297 if rank < 4: 298 return None 299 t = (C[0] * C[0]) + (C[1] * C[1]) + (C[2] * C[2]) + C[3] 300 radius = np.sqrt(t)[0] 301 center = np.array([C[0][0], C[1][0], C[2][0]]) 302 if len(residue) > 0: 303 residue = np.sqrt(residue[0]) / n 304 else: 305 residue = 0 306 sph = vedo.shapes.Sphere(center, radius, c=(1, 0, 0)).wireframe(1) 307 sph.radius = radius 308 sph.center = center 309 sph.residue = residue 310 sph.name = "FitSphere" 311 return sph
Fits a sphere to a set of points.
Extra info is stored in Sphere.radius
, Sphere.center
, Sphere.residue
.
Examples:
314def pca_ellipse(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=60) -> Union["vedo.shapes.Circle", None]: 315 """ 316 Create the oriented 2D ellipse that contains the fraction `pvalue` of points. 317 PCA (Principal Component Analysis) is used to compute the ellipse orientation. 318 319 Parameter `pvalue` sets the specified fraction of points inside the ellipse. 320 Normalized directions are stored in `ellipse.axis1`, `ellipse.axis2`. 321 Axes sizes are stored in `ellipse.va`, `ellipse.vb` 322 323 Arguments: 324 pvalue : (float) 325 ellipse will include this fraction of points 326 res : (int) 327 resolution of the ellipse 328 329 Examples: 330 - [pca_ellipse.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipse.py) 331 - [histo_pca.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/histo_pca.py) 332 333 ![](https://vedo.embl.es/images/pyplot/histo_pca.png) 334 """ 335 from scipy.stats import f 336 337 if isinstance(points, Points): 338 coords = points.vertices 339 else: 340 coords = points 341 if len(coords) < 4: 342 vedo.logger.warning("in pca_ellipse(), there are not enough points!") 343 return None 344 345 P = np.array(coords, dtype=float)[:, (0, 1)] 346 cov = np.cov(P, rowvar=0) # type: ignore 347 _, s, R = np.linalg.svd(cov) # singular value decomposition 348 p, n = s.size, P.shape[0] 349 fppf = f.ppf(pvalue, p, n - p) # f % point function 350 u = np.sqrt(s * fppf / 2) * 2 # semi-axes (largest first) 351 ua, ub = u 352 center = utils.make3d(np.mean(P, axis=0)) # centroid of the ellipse 353 354 t = LinearTransform(R.T * u).translate(center) 355 elli = vedo.shapes.Circle(alpha=0.75, res=res) 356 elli.apply_transform(t) 357 elli.properties.LightingOff() 358 359 elli.pvalue = pvalue 360 elli.center = np.array([center[0], center[1], 0]) 361 elli.nr_of_points = n 362 elli.va = ua 363 elli.vb = ub 364 365 # we subtract center because it's in t 366 elli.axis1 = t.move([1, 0, 0]) - center 367 elli.axis2 = t.move([0, 1, 0]) - center 368 369 elli.axis1 /= np.linalg.norm(elli.axis1) 370 elli.axis2 /= np.linalg.norm(elli.axis2) 371 elli.name = "PCAEllipse" 372 return elli
Create the oriented 2D ellipse that contains the fraction pvalue
of points.
PCA (Principal Component Analysis) is used to compute the ellipse orientation.
Parameter pvalue
sets the specified fraction of points inside the ellipse.
Normalized directions are stored in ellipse.axis1
, ellipse.axis2
.
Axes sizes are stored in ellipse.va
, ellipse.vb
Arguments:
- pvalue : (float) ellipse will include this fraction of points
- res : (int) resolution of the ellipse
Examples:
375def pca_ellipsoid(points: Union[np.ndarray, "vedo.Points"], pvalue=0.673, res=24) -> Union["vedo.shapes.Ellipsoid", None]: 376 """ 377 Create the oriented ellipsoid that contains the fraction `pvalue` of points. 378 PCA (Principal Component Analysis) is used to compute the ellipsoid orientation. 379 380 Axes sizes can be accessed in `ellips.va`, `ellips.vb`, `ellips.vc`, 381 normalized directions are stored in `ellips.axis1`, `ellips.axis2` and `ellips.axis3`. 382 Center of mass is stored in `ellips.center`. 383 384 Asphericity can be accessed in `ellips.asphericity()` and ellips.asphericity_error(). 385 A value of 0 means a perfect sphere. 386 387 Arguments: 388 pvalue : (float) 389 ellipsoid will include this fraction of points 390 391 Examples: 392 [pca_ellipsoid.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/pca_ellipsoid.py) 393 394 ![](https://vedo.embl.es/images/basic/pca.png) 395 396 See also: 397 `pca_ellipse()` for a 2D ellipse. 398 """ 399 from scipy.stats import f 400 401 if isinstance(points, Points): 402 coords = points.vertices 403 else: 404 coords = points 405 if len(coords) < 4: 406 vedo.logger.warning("in pca_ellipsoid(), not enough input points!") 407 return None 408 409 P = np.array(coords, ndmin=2, dtype=float) 410 cov = np.cov(P, rowvar=0) # type: ignore 411 _, s, R = np.linalg.svd(cov) # singular value decomposition 412 p, n = s.size, P.shape[0] 413 fppf = f.ppf(pvalue, p, n-p)*(n-1)*p*(n+1)/n/(n-p) # f % point function 414 u = np.sqrt(s*fppf) 415 ua, ub, uc = u # semi-axes (largest first) 416 center = np.mean(P, axis=0) # centroid of the hyperellipsoid 417 418 t = LinearTransform(R.T * u).translate(center) 419 elli = vedo.shapes.Ellipsoid((0,0,0), (1,0,0), (0,1,0), (0,0,1), res=res) 420 elli.apply_transform(t) 421 elli.alpha(0.25) 422 elli.properties.LightingOff() 423 424 elli.pvalue = pvalue 425 elli.nr_of_points = n 426 elli.center = center 427 elli.va = ua 428 elli.vb = ub 429 elli.vc = uc 430 # we subtract center because it's in t 431 elli.axis1 = np.array(t.move([1, 0, 0])) - center 432 elli.axis2 = np.array(t.move([0, 1, 0])) - center 433 elli.axis3 = np.array(t.move([0, 0, 1])) - center 434 elli.axis1 /= np.linalg.norm(elli.axis1) 435 elli.axis2 /= np.linalg.norm(elli.axis2) 436 elli.axis3 /= np.linalg.norm(elli.axis3) 437 elli.name = "PCAEllipsoid" 438 return elli
Create the oriented ellipsoid that contains the fraction pvalue
of points.
PCA (Principal Component Analysis) is used to compute the ellipsoid orientation.
Axes sizes can be accessed in ellips.va
, ellips.vb
, ellips.vc
,
normalized directions are stored in ellips.axis1
, ellips.axis2
and ellips.axis3
.
Center of mass is stored in ellips.center
.
Asphericity can be accessed in ellips.asphericity()
and ellips.asphericity_error().
A value of 0 means a perfect sphere.
Arguments:
- pvalue : (float) ellipsoid will include this fraction of points
Examples:
![](https://vedo.embl.es/images/basic/pca.png)
See also:
pca_ellipse()
for a 2D ellipse.