vedo.assembly
Submodule for managing groups of vedo objects
1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3from weakref import ref as weak_ref_to 4from typing import List, Union, Any 5import numpy as np 6 7import vedo.vtkclasses as vtki # a wrapper for lazy imports 8 9import vedo 10from vedo.transformations import LinearTransform 11from vedo.visual import CommonVisual, Actor3DHelper 12 13__docformat__ = "google" 14 15__doc__ = """ 16Submodule for managing groups of vedo objects 17 18![](https://vedo.embl.es/images/basic/align4.png) 19""" 20 21__all__ = ["Group", "Assembly", "procrustes_alignment"] 22 23 24################################################# 25def procrustes_alignment(sources: List["vedo.Mesh"], rigid=False) -> "Assembly": 26 """ 27 Return an `Assembly` of aligned source meshes with the `Procrustes` algorithm. 28 The output `Assembly` is normalized in size. 29 30 The `Procrustes` algorithm takes N set of points and aligns them in a least-squares sense 31 to their mutual mean. The algorithm is iterated until convergence, 32 as the mean must be recomputed after each alignment. 33 34 The set of average points generated by the algorithm can be accessed with 35 `algoutput.info['mean']` as a numpy array. 36 37 Arguments: 38 rigid : bool 39 if `True` scaling is disabled. 40 41 Examples: 42 - [align4.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align4.py) 43 44 ![](https://vedo.embl.es/images/basic/align4.png) 45 """ 46 47 group = vtki.new("MultiBlockDataGroupFilter") 48 for source in sources: 49 if sources[0].npoints != source.npoints: 50 vedo.logger.error("sources have different nr of points") 51 raise RuntimeError() 52 group.AddInputData(source.dataset) 53 procrustes = vtki.new("ProcrustesAlignmentFilter") 54 procrustes.StartFromCentroidOn() 55 procrustes.SetInputConnection(group.GetOutputPort()) 56 if rigid: 57 procrustes.GetLandmarkTransform().SetModeToRigidBody() 58 procrustes.Update() 59 60 acts = [] 61 for i, s in enumerate(sources): 62 poly = procrustes.GetOutput().GetBlock(i) 63 mesh = vedo.mesh.Mesh(poly) 64 mesh.actor.SetProperty(s.actor.GetProperty()) 65 mesh.properties = s.actor.GetProperty() 66 if hasattr(s, "name"): 67 mesh.name = s.name 68 acts.append(mesh) 69 assem = Assembly(acts) 70 assem.transform = procrustes.GetLandmarkTransform() 71 assem.info["mean"] = vedo.utils.vtk2numpy(procrustes.GetMeanPoints().GetData()) 72 return assem 73 74 75################################################# 76class Group(vtki.vtkPropAssembly): 77 """Form groups of generic objects (not necessarily meshes).""" 78 79 def __init__(self, objects=()): 80 """Form groups of generic objects (not necessarily meshes).""" 81 super().__init__() 82 83 self.objects = [] 84 85 if isinstance(objects, dict): 86 for name in objects: 87 objects[name].name = name 88 objects = list(objects.values()) 89 elif vedo.utils.is_sequence(objects): 90 self.objects = objects 91 92 93 self.actor = self 94 95 self.name = "Group" 96 self.filename = "" 97 self.trail = None 98 self.trail_points = [] 99 self.trail_segment_size = 0 100 self.trail_offset = None 101 self.shadows = [] 102 self.info = {} 103 self.rendered_at = set() 104 self.scalarbar = None 105 106 for a in vedo.utils.flatten(objects): 107 if a: 108 self.AddPart(a.actor) 109 110 self.PickableOff() 111 112 113 def __str__(self): 114 """Print info about Group object.""" 115 module = self.__class__.__module__ 116 name = self.__class__.__name__ 117 out = vedo.printc( 118 f"{module}.{name} at ({hex(id(self))})".ljust(75), 119 bold=True, invert=True, return_string=True, 120 ) 121 out += "\x1b[0m" 122 if self.name: 123 out += "name".ljust(14) + ": " + self.name 124 if "legend" in self.info.keys() and self.info["legend"]: 125 out+= f", legend='{self.info['legend']}'" 126 out += "\n" 127 128 n = len(self.unpack()) 129 out += "n. of objects".ljust(14) + ": " + str(n) + " " 130 names = [a.name for a in self.unpack() if a.name] 131 if names: 132 out += str(names).replace("'","")[:56] 133 return out.rstrip() + "\x1b[0m" 134 135 def __iadd__(self, obj): 136 """ 137 Add an object to the group 138 """ 139 if not vedo.utils.is_sequence(obj): 140 obj = [obj] 141 for a in obj: 142 if a: 143 try: 144 self.AddPart(a) 145 except TypeError: 146 self.AddPart(a.actor) 147 self.objcects.append(a) 148 return self 149 150 def _unpack(self): 151 """Unpack the group into its elements""" 152 elements = [] 153 self.InitPathTraversal() 154 parts = self.GetParts() 155 parts.InitTraversal() 156 for i in range(parts.GetNumberOfItems()): 157 ele = parts.GetItemAsObject(i) 158 elements.append(ele) 159 160 # gr.InitPathTraversal() 161 # for _ in range(gr.GetNumberOfPaths()): 162 # path = gr.GetNextPath() 163 # print([path]) 164 # path.InitTraversal() 165 # for i in range(path.GetNumberOfItems()): 166 # a = path.GetItemAsObject(i).GetViewProp() 167 # print([a]) 168 169 return elements 170 171 def clear(self) -> "Group": 172 """Remove all parts""" 173 for a in self._unpack(): 174 self.RemovePart(a) 175 self.objects = [] 176 return self 177 178 def on(self) -> "Group": 179 """Switch on visibility""" 180 self.VisibilityOn() 181 return self 182 183 def off(self) -> "Group": 184 """Switch off visibility""" 185 self.VisibilityOff() 186 return self 187 188 def pickable(self, value=True) -> "Group": 189 """The pickability property of the Group.""" 190 self.SetPickable(value) 191 return self 192 193 def use_bounds(self, value=True) -> "Group": 194 """Set the use bounds property of the Group.""" 195 self.SetUseBounds(value) 196 return self 197 198 def print(self) -> "Group": 199 """Print info about the object.""" 200 print(self) 201 return self 202 203 def objects(self) -> List["vedo.Mesh"]: 204 """Return the list of objects in the group.""" 205 return self.objects 206 207 208################################################# 209class Assembly(CommonVisual, Actor3DHelper, vtki.vtkAssembly): 210 """ 211 Group many objects and treat them as a single new object. 212 """ 213 214 def __init__(self, *meshs): 215 """ 216 Group many objects and treat them as a single new object, 217 keeping track of internal transformations. 218 219 Examples: 220 - [gyroscope1.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/gyroscope1.py) 221 222 ![](https://vedo.embl.es/images/simulations/39766016-85c1c1d6-52e3-11e8-8575-d167b7ce5217.gif) 223 """ 224 super().__init__() 225 226 # Init by filename 227 if len(meshs) == 1 and isinstance(meshs[0], str): 228 filename = vedo.file_io.download(meshs[0], verbose=False) 229 data = np.load(filename, allow_pickle=True) 230 meshs = [vedo.file_io._from_numpy(dd) for dd in data] 231 # Name and load from dictionary 232 if len(meshs) == 1 and isinstance(meshs[0], dict): 233 meshs = meshs[0] 234 for name in meshs: 235 meshs[name].name = name 236 meshs = list(meshs.values()) 237 else: 238 if len(meshs) == 1: 239 meshs = meshs[0] 240 else: 241 meshs = vedo.utils.flatten(meshs) 242 243 self.actor = self 244 self.actor.retrieve_object = weak_ref_to(self) 245 246 self.name = "Assembly" 247 self.filename = "" 248 self.rendered_at = set() 249 self.scalarbar = None 250 self.info = {} 251 self.time = 0 252 253 self.transform = LinearTransform() 254 255 self.objects = [m for m in meshs if m] 256 self.actors = [m.actor for m in self.objects] 257 258 scalarbars = [] 259 for a in self.actors: 260 if isinstance(a, vtki.get_class("Prop3D")): # and a.GetNumberOfPoints(): 261 self.AddPart(a) 262 if hasattr(a, "scalarbar") and a.scalarbar is not None: 263 scalarbars.append(a.scalarbar) 264 265 if len(scalarbars) > 1: 266 self.scalarbar = Group(scalarbars) 267 elif len(scalarbars) == 1: 268 self.scalarbar = scalarbars[0] 269 270 self.pipeline = vedo.utils.OperationNode( 271 "Assembly", 272 parents=self.objects, 273 comment=f"#meshes {len(self.objects)}", 274 c="#f08080", 275 ) 276 ########################################## 277 278 def __str__(self): 279 """Print info about Assembly object.""" 280 module = self.__class__.__module__ 281 name = self.__class__.__name__ 282 out = vedo.printc( 283 f"{module}.{name} at ({hex(id(self))})".ljust(75), 284 bold=True, invert=True, return_string=True, 285 ) 286 out += "\x1b[0m" 287 288 if self.name: 289 out += "name".ljust(14) + ": " + self.name 290 if "legend" in self.info.keys() and self.info["legend"]: 291 out+= f", legend='{self.info['legend']}'" 292 out += "\n" 293 294 n = len(self.unpack()) 295 out += "n. of objects".ljust(14) + ": " + str(n) + " " 296 names = [a.name for a in self.unpack() if a.name] 297 if names: 298 out += str(names).replace("'","")[:56] 299 out += "\n" 300 301 pos = self.GetPosition() 302 out += "position".ljust(14) + ": " + str(pos) + "\n" 303 304 bnds = self.GetBounds() 305 bx1, bx2 = vedo.utils.precision(bnds[0], 3), vedo.utils.precision(bnds[1], 3) 306 by1, by2 = vedo.utils.precision(bnds[2], 3), vedo.utils.precision(bnds[3], 3) 307 bz1, bz2 = vedo.utils.precision(bnds[4], 3), vedo.utils.precision(bnds[5], 3) 308 out += "bounds".ljust(14) + ":" 309 out += " x=(" + bx1 + ", " + bx2 + ")," 310 out += " y=(" + by1 + ", " + by2 + ")," 311 out += " z=(" + bz1 + ", " + bz2 + ")\n" 312 return out.rstrip() + "\x1b[0m" 313 314 def _repr_html_(self): 315 """ 316 HTML representation of the Assembly object for Jupyter Notebooks. 317 318 Returns: 319 HTML text with the image and some properties. 320 """ 321 import io 322 import base64 323 from PIL import Image 324 325 library_name = "vedo.assembly.Assembly" 326 help_url = "https://vedo.embl.es/docs/vedo/assembly.html" 327 328 arr = self.thumbnail(zoom=1.1, elevation=-60) 329 330 im = Image.fromarray(arr) 331 buffered = io.BytesIO() 332 im.save(buffered, format="PNG", quality=100) 333 encoded = base64.b64encode(buffered.getvalue()).decode("utf-8") 334 url = "data:image/png;base64," + encoded 335 image = f"<img src='{url}'></img>" 336 337 # statisitics 338 bounds = "<br/>".join( 339 [ 340 vedo.utils.precision(min_x, 4) + " ... " + vedo.utils.precision(max_x, 4) 341 for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2]) 342 ] 343 ) 344 345 help_text = "" 346 if self.name: 347 help_text += f"<b> {self.name}:   </b>" 348 help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>" 349 if self.filename: 350 dots = "" 351 if len(self.filename) > 30: 352 dots = "..." 353 help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>" 354 355 allt = [ 356 "<table>", 357 "<tr>", 358 "<td>", 359 image, 360 "</td>", 361 "<td style='text-align: center; vertical-align: center;'><br/>", 362 help_text, 363 "<table>", 364 "<tr><td><b> nr. of objects </b></td><td>" 365 + str(self.GetNumberOfPaths()) 366 + "</td></tr>", 367 "<tr><td><b> position </b></td><td>" + str(self.GetPosition()) + "</td></tr>", 368 "<tr><td><b> diagonal size </b></td><td>" 369 + vedo.utils.precision(self.diagonal_size(), 5) 370 + "</td></tr>", 371 "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>", 372 "</table>", 373 "</table>", 374 ] 375 return "\n".join(allt) 376 377 def __add__(self, obj): 378 """ 379 Add an object to the assembly 380 """ 381 if isinstance(getattr(obj, "actor", None), vtki.get_class("Prop3D")): 382 383 self.objects.append(obj) 384 self.actors.append(obj.actor) 385 self.AddPart(obj.actor) 386 387 if hasattr(obj, "scalarbar") and obj.scalarbar is not None: 388 if self.scalarbar is None: 389 self.scalarbar = obj.scalarbar 390 return self 391 392 def unpack_group(scalarbar): 393 if isinstance(scalarbar, Group): 394 return scalarbar.unpack() 395 else: 396 return scalarbar 397 398 if isinstance(self.scalarbar, Group): 399 self.scalarbar += unpack_group(obj.scalarbar) 400 else: 401 self.scalarbar = Group([unpack_group(self.scalarbar), unpack_group(obj.scalarbar)]) 402 self.pipeline = vedo.utils.OperationNode("add mesh", parents=[self, obj], c="#f08080") 403 return self 404 405 def __contains__(self, obj): 406 """Allows to use `in` to check if an object is in the `Assembly`.""" 407 return obj in self.objects 408 409 def __getitem__(self, i): 410 """Return i-th object.""" 411 if isinstance(i, int): 412 return self.objects[i] 413 elif isinstance(i, str): 414 for m in self.objects: 415 if i == m.name: 416 return m 417 return None 418 419 def __len__(self): 420 """Return nr. of objects in the assembly.""" 421 return len(self.objects) 422 423 # TODO #### 424 # def propagate_transform(self): 425 # """Propagate the transformation to all parts.""" 426 # # navigate the assembly and apply the transform to all parts 427 # # and reset position, orientation and scale of the assembly 428 # for i in range(self.GetNumberOfPaths()): 429 # path = self.GetPath(i) 430 # obj = path.GetLastNode().GetViewProp() 431 # obj.SetUserTransform(self.transform.T) 432 # obj.SetPosition(0, 0, 0) 433 # obj.SetOrientation(0, 0, 0) 434 # obj.SetScale(1, 1, 1) 435 # raise NotImplementedError() 436 437 def unpack(self, i=None) -> Union[List["vedo.Mesh"], "vedo.Mesh"]: 438 """Unpack the list of objects from a `Assembly`. 439 440 If `i` is given, get `i-th` object from a `Assembly`. 441 Input can be a string, in this case returns the first object 442 whose name contains the given string. 443 444 Examples: 445 - [custom_axes4.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes4.py) 446 """ 447 if i is None: 448 return self.objects 449 elif isinstance(i, int): 450 return self.objects[i] 451 elif isinstance(i, str): 452 for m in self.objects: 453 if i == m.name: 454 return m 455 return [] 456 457 def recursive_unpack(self) -> List["vedo.Mesh"]: 458 """Flatten out an Assembly.""" 459 460 def _genflatten(lst): 461 if not lst: 462 return [] 463 ## 464 if isinstance(lst[0], Assembly): 465 lst = lst[0].unpack() 466 ## 467 for elem in lst: 468 if isinstance(elem, Assembly): 469 apos = elem.GetPosition() 470 asum = np.sum(apos) 471 for x in elem.unpack(): 472 if asum: 473 yield x.clone().shift(apos) 474 else: 475 yield x 476 else: 477 yield elem 478 479 return list(_genflatten([self])) 480 481 def pickable(self, value=True) -> "Assembly": 482 """Set/get the pickability property of an assembly and its elements""" 483 self.SetPickable(value) 484 # set property to each element 485 for elem in self.recursive_unpack(): 486 elem.pickable(value) 487 return self 488 489 def clone(self) -> "Assembly": 490 """Make a clone copy of the object. Same as `copy()`.""" 491 newlist = [] 492 for a in self.objects: 493 newlist.append(a.clone()) 494 return Assembly(newlist) 495 496 def clone2d(self, pos="bottom-left", size=1, rotation=0, ontop=False, scale=None) -> Group: 497 """ 498 Convert the `Assembly` into a `Group` of 2D objects. 499 500 Arguments: 501 pos : (list, str) 502 Position in 2D, as a string or list (x,y). 503 The center of the renderer is [0,0] while top-right is [1,1]. 504 Any combination of "center", "top", "bottom", "left" and "right" will work. 505 size : (float) 506 global scaling factor for the 2D object. 507 The scaling is normalized to the x-range of the original object. 508 rotation : (float) 509 rotation angle in degrees. 510 ontop : (bool) 511 if `True` the now 2D object is rendered on top of the 3D scene. 512 scale : (float) 513 deprecated, use `size` instead. 514 515 Returns: 516 `Group` object. 517 """ 518 if scale is not None: 519 vedo.logger.warning("clone2d(scale=...) is deprecated, use clone2d(size=...) instead") 520 size = scale 521 522 padding = 0.05 523 x0, x1 = self.xbounds() 524 y0, y1 = self.ybounds() 525 pp = self.pos() 526 x0 -= pp[0] 527 x1 -= pp[0] 528 y0 -= pp[1] 529 y1 -= pp[1] 530 531 offset = [x0, y0] 532 if "cent" in pos: 533 offset = [(x0 + x1) / 2, (y0 + y1) / 2] 534 position = [0., 0.] 535 if "right" in pos: 536 offset[0] = x1 537 position = [1 - padding, 0] 538 if "left" in pos: 539 offset[0] = x0 540 position = [-1 + padding, 0] 541 if "top" in pos: 542 offset[1] = y1 543 position = [0, 1 - padding] 544 if "bottom" in pos: 545 offset[1] = y0 546 position = [0, -1 + padding] 547 elif "top" in pos: 548 if "right" in pos: 549 offset = [x1, y1] 550 position = [1 - padding, 1 - padding] 551 elif "left" in pos: 552 offset = [x0, y1] 553 position = [-1 + padding, 1 - padding] 554 else: 555 raise ValueError(f"incomplete position pos='{pos}'") 556 elif "bottom" in pos: 557 if "right" in pos: 558 offset = [x1, y0] 559 position = [1 - padding, -1 + padding] 560 elif "left" in pos: 561 offset = [x0, y0] 562 position = [-1 + padding, -1 + padding] 563 else: 564 raise ValueError(f"incomplete position pos='{pos}'") 565 else: 566 position = pos 567 568 scanned : List[Any] = [] 569 group = Group() 570 for a in self.recursive_unpack(): 571 if a in scanned: 572 continue 573 if not isinstance(a, vedo.Points): 574 continue 575 if a.npoints == 0: 576 continue 577 578 s = size * 500 / (x1 - x0) 579 if a.properties.GetRepresentation() == 1: 580 # wireframe is not rendered correctly in 2d 581 b = a.boundaries().lw(1).c(a.color(), a.alpha()) 582 if rotation: 583 b.rotate_z(rotation, around=self.origin()) 584 a2d = b.clone2d(size=s, offset=offset) 585 else: 586 if rotation: 587 # around=self.actor.GetCenter() 588 a.rotate_z(rotation, around=self.origin()) 589 a2d = a.clone2d(size=s, offset=offset) 590 a2d.pos(position).ontop(ontop) 591 group += a2d 592 593 try: # copy info from Histogram1D 594 group.entries = self.entries 595 group.frequencies = self.frequencies 596 group.errors = self.errors 597 group.edges = self.edges 598 group.centers = self.centers 599 group.mean = self.mean 600 group.mode = self.mode 601 group.std = self.std 602 except AttributeError: 603 pass 604 605 group.name = self.name 606 return group 607 608 def copy(self) -> "Assembly": 609 """Return a copy of the object. Alias of `clone()`.""" 610 return self.clone() 611 612 # def write(self, filename="assembly.npy") -> "Assembly": 613 # """ 614 # Write the object to file in `numpy` format. 615 # """ 616 # objs = [] 617 # for ob in self.unpack(): 618 # d = vedo.file_io._to_numpy(ob) 619 # objs.append(d) 620 # np.save(filename, objs) 621 # return self
77class Group(vtki.vtkPropAssembly): 78 """Form groups of generic objects (not necessarily meshes).""" 79 80 def __init__(self, objects=()): 81 """Form groups of generic objects (not necessarily meshes).""" 82 super().__init__() 83 84 self.objects = [] 85 86 if isinstance(objects, dict): 87 for name in objects: 88 objects[name].name = name 89 objects = list(objects.values()) 90 elif vedo.utils.is_sequence(objects): 91 self.objects = objects 92 93 94 self.actor = self 95 96 self.name = "Group" 97 self.filename = "" 98 self.trail = None 99 self.trail_points = [] 100 self.trail_segment_size = 0 101 self.trail_offset = None 102 self.shadows = [] 103 self.info = {} 104 self.rendered_at = set() 105 self.scalarbar = None 106 107 for a in vedo.utils.flatten(objects): 108 if a: 109 self.AddPart(a.actor) 110 111 self.PickableOff() 112 113 114 def __str__(self): 115 """Print info about Group object.""" 116 module = self.__class__.__module__ 117 name = self.__class__.__name__ 118 out = vedo.printc( 119 f"{module}.{name} at ({hex(id(self))})".ljust(75), 120 bold=True, invert=True, return_string=True, 121 ) 122 out += "\x1b[0m" 123 if self.name: 124 out += "name".ljust(14) + ": " + self.name 125 if "legend" in self.info.keys() and self.info["legend"]: 126 out+= f", legend='{self.info['legend']}'" 127 out += "\n" 128 129 n = len(self.unpack()) 130 out += "n. of objects".ljust(14) + ": " + str(n) + " " 131 names = [a.name for a in self.unpack() if a.name] 132 if names: 133 out += str(names).replace("'","")[:56] 134 return out.rstrip() + "\x1b[0m" 135 136 def __iadd__(self, obj): 137 """ 138 Add an object to the group 139 """ 140 if not vedo.utils.is_sequence(obj): 141 obj = [obj] 142 for a in obj: 143 if a: 144 try: 145 self.AddPart(a) 146 except TypeError: 147 self.AddPart(a.actor) 148 self.objcects.append(a) 149 return self 150 151 def _unpack(self): 152 """Unpack the group into its elements""" 153 elements = [] 154 self.InitPathTraversal() 155 parts = self.GetParts() 156 parts.InitTraversal() 157 for i in range(parts.GetNumberOfItems()): 158 ele = parts.GetItemAsObject(i) 159 elements.append(ele) 160 161 # gr.InitPathTraversal() 162 # for _ in range(gr.GetNumberOfPaths()): 163 # path = gr.GetNextPath() 164 # print([path]) 165 # path.InitTraversal() 166 # for i in range(path.GetNumberOfItems()): 167 # a = path.GetItemAsObject(i).GetViewProp() 168 # print([a]) 169 170 return elements 171 172 def clear(self) -> "Group": 173 """Remove all parts""" 174 for a in self._unpack(): 175 self.RemovePart(a) 176 self.objects = [] 177 return self 178 179 def on(self) -> "Group": 180 """Switch on visibility""" 181 self.VisibilityOn() 182 return self 183 184 def off(self) -> "Group": 185 """Switch off visibility""" 186 self.VisibilityOff() 187 return self 188 189 def pickable(self, value=True) -> "Group": 190 """The pickability property of the Group.""" 191 self.SetPickable(value) 192 return self 193 194 def use_bounds(self, value=True) -> "Group": 195 """Set the use bounds property of the Group.""" 196 self.SetUseBounds(value) 197 return self 198 199 def print(self) -> "Group": 200 """Print info about the object.""" 201 print(self) 202 return self 203 204 def objects(self) -> List["vedo.Mesh"]: 205 """Return the list of objects in the group.""" 206 return self.objects
Form groups of generic objects (not necessarily meshes).
80 def __init__(self, objects=()): 81 """Form groups of generic objects (not necessarily meshes).""" 82 super().__init__() 83 84 self.objects = [] 85 86 if isinstance(objects, dict): 87 for name in objects: 88 objects[name].name = name 89 objects = list(objects.values()) 90 elif vedo.utils.is_sequence(objects): 91 self.objects = objects 92 93 94 self.actor = self 95 96 self.name = "Group" 97 self.filename = "" 98 self.trail = None 99 self.trail_points = [] 100 self.trail_segment_size = 0 101 self.trail_offset = None 102 self.shadows = [] 103 self.info = {} 104 self.rendered_at = set() 105 self.scalarbar = None 106 107 for a in vedo.utils.flatten(objects): 108 if a: 109 self.AddPart(a.actor) 110 111 self.PickableOff()
Form groups of generic objects (not necessarily meshes).
204 def objects(self) -> List["vedo.Mesh"]: 205 """Return the list of objects in the group.""" 206 return self.objects
Return the list of objects in the group.
172 def clear(self) -> "Group": 173 """Remove all parts""" 174 for a in self._unpack(): 175 self.RemovePart(a) 176 self.objects = [] 177 return self
Remove all parts
184 def off(self) -> "Group": 185 """Switch off visibility""" 186 self.VisibilityOff() 187 return self
Switch off visibility
189 def pickable(self, value=True) -> "Group": 190 """The pickability property of the Group.""" 191 self.SetPickable(value) 192 return self
The pickability property of the Group.
210class Assembly(CommonVisual, Actor3DHelper, vtki.vtkAssembly): 211 """ 212 Group many objects and treat them as a single new object. 213 """ 214 215 def __init__(self, *meshs): 216 """ 217 Group many objects and treat them as a single new object, 218 keeping track of internal transformations. 219 220 Examples: 221 - [gyroscope1.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/gyroscope1.py) 222 223 ![](https://vedo.embl.es/images/simulations/39766016-85c1c1d6-52e3-11e8-8575-d167b7ce5217.gif) 224 """ 225 super().__init__() 226 227 # Init by filename 228 if len(meshs) == 1 and isinstance(meshs[0], str): 229 filename = vedo.file_io.download(meshs[0], verbose=False) 230 data = np.load(filename, allow_pickle=True) 231 meshs = [vedo.file_io._from_numpy(dd) for dd in data] 232 # Name and load from dictionary 233 if len(meshs) == 1 and isinstance(meshs[0], dict): 234 meshs = meshs[0] 235 for name in meshs: 236 meshs[name].name = name 237 meshs = list(meshs.values()) 238 else: 239 if len(meshs) == 1: 240 meshs = meshs[0] 241 else: 242 meshs = vedo.utils.flatten(meshs) 243 244 self.actor = self 245 self.actor.retrieve_object = weak_ref_to(self) 246 247 self.name = "Assembly" 248 self.filename = "" 249 self.rendered_at = set() 250 self.scalarbar = None 251 self.info = {} 252 self.time = 0 253 254 self.transform = LinearTransform() 255 256 self.objects = [m for m in meshs if m] 257 self.actors = [m.actor for m in self.objects] 258 259 scalarbars = [] 260 for a in self.actors: 261 if isinstance(a, vtki.get_class("Prop3D")): # and a.GetNumberOfPoints(): 262 self.AddPart(a) 263 if hasattr(a, "scalarbar") and a.scalarbar is not None: 264 scalarbars.append(a.scalarbar) 265 266 if len(scalarbars) > 1: 267 self.scalarbar = Group(scalarbars) 268 elif len(scalarbars) == 1: 269 self.scalarbar = scalarbars[0] 270 271 self.pipeline = vedo.utils.OperationNode( 272 "Assembly", 273 parents=self.objects, 274 comment=f"#meshes {len(self.objects)}", 275 c="#f08080", 276 ) 277 ########################################## 278 279 def __str__(self): 280 """Print info about Assembly object.""" 281 module = self.__class__.__module__ 282 name = self.__class__.__name__ 283 out = vedo.printc( 284 f"{module}.{name} at ({hex(id(self))})".ljust(75), 285 bold=True, invert=True, return_string=True, 286 ) 287 out += "\x1b[0m" 288 289 if self.name: 290 out += "name".ljust(14) + ": " + self.name 291 if "legend" in self.info.keys() and self.info["legend"]: 292 out+= f", legend='{self.info['legend']}'" 293 out += "\n" 294 295 n = len(self.unpack()) 296 out += "n. of objects".ljust(14) + ": " + str(n) + " " 297 names = [a.name for a in self.unpack() if a.name] 298 if names: 299 out += str(names).replace("'","")[:56] 300 out += "\n" 301 302 pos = self.GetPosition() 303 out += "position".ljust(14) + ": " + str(pos) + "\n" 304 305 bnds = self.GetBounds() 306 bx1, bx2 = vedo.utils.precision(bnds[0], 3), vedo.utils.precision(bnds[1], 3) 307 by1, by2 = vedo.utils.precision(bnds[2], 3), vedo.utils.precision(bnds[3], 3) 308 bz1, bz2 = vedo.utils.precision(bnds[4], 3), vedo.utils.precision(bnds[5], 3) 309 out += "bounds".ljust(14) + ":" 310 out += " x=(" + bx1 + ", " + bx2 + ")," 311 out += " y=(" + by1 + ", " + by2 + ")," 312 out += " z=(" + bz1 + ", " + bz2 + ")\n" 313 return out.rstrip() + "\x1b[0m" 314 315 def _repr_html_(self): 316 """ 317 HTML representation of the Assembly object for Jupyter Notebooks. 318 319 Returns: 320 HTML text with the image and some properties. 321 """ 322 import io 323 import base64 324 from PIL import Image 325 326 library_name = "vedo.assembly.Assembly" 327 help_url = "https://vedo.embl.es/docs/vedo/assembly.html" 328 329 arr = self.thumbnail(zoom=1.1, elevation=-60) 330 331 im = Image.fromarray(arr) 332 buffered = io.BytesIO() 333 im.save(buffered, format="PNG", quality=100) 334 encoded = base64.b64encode(buffered.getvalue()).decode("utf-8") 335 url = "data:image/png;base64," + encoded 336 image = f"<img src='{url}'></img>" 337 338 # statisitics 339 bounds = "<br/>".join( 340 [ 341 vedo.utils.precision(min_x, 4) + " ... " + vedo.utils.precision(max_x, 4) 342 for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2]) 343 ] 344 ) 345 346 help_text = "" 347 if self.name: 348 help_text += f"<b> {self.name}:   </b>" 349 help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>" 350 if self.filename: 351 dots = "" 352 if len(self.filename) > 30: 353 dots = "..." 354 help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>" 355 356 allt = [ 357 "<table>", 358 "<tr>", 359 "<td>", 360 image, 361 "</td>", 362 "<td style='text-align: center; vertical-align: center;'><br/>", 363 help_text, 364 "<table>", 365 "<tr><td><b> nr. of objects </b></td><td>" 366 + str(self.GetNumberOfPaths()) 367 + "</td></tr>", 368 "<tr><td><b> position </b></td><td>" + str(self.GetPosition()) + "</td></tr>", 369 "<tr><td><b> diagonal size </b></td><td>" 370 + vedo.utils.precision(self.diagonal_size(), 5) 371 + "</td></tr>", 372 "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>", 373 "</table>", 374 "</table>", 375 ] 376 return "\n".join(allt) 377 378 def __add__(self, obj): 379 """ 380 Add an object to the assembly 381 """ 382 if isinstance(getattr(obj, "actor", None), vtki.get_class("Prop3D")): 383 384 self.objects.append(obj) 385 self.actors.append(obj.actor) 386 self.AddPart(obj.actor) 387 388 if hasattr(obj, "scalarbar") and obj.scalarbar is not None: 389 if self.scalarbar is None: 390 self.scalarbar = obj.scalarbar 391 return self 392 393 def unpack_group(scalarbar): 394 if isinstance(scalarbar, Group): 395 return scalarbar.unpack() 396 else: 397 return scalarbar 398 399 if isinstance(self.scalarbar, Group): 400 self.scalarbar += unpack_group(obj.scalarbar) 401 else: 402 self.scalarbar = Group([unpack_group(self.scalarbar), unpack_group(obj.scalarbar)]) 403 self.pipeline = vedo.utils.OperationNode("add mesh", parents=[self, obj], c="#f08080") 404 return self 405 406 def __contains__(self, obj): 407 """Allows to use `in` to check if an object is in the `Assembly`.""" 408 return obj in self.objects 409 410 def __getitem__(self, i): 411 """Return i-th object.""" 412 if isinstance(i, int): 413 return self.objects[i] 414 elif isinstance(i, str): 415 for m in self.objects: 416 if i == m.name: 417 return m 418 return None 419 420 def __len__(self): 421 """Return nr. of objects in the assembly.""" 422 return len(self.objects) 423 424 # TODO #### 425 # def propagate_transform(self): 426 # """Propagate the transformation to all parts.""" 427 # # navigate the assembly and apply the transform to all parts 428 # # and reset position, orientation and scale of the assembly 429 # for i in range(self.GetNumberOfPaths()): 430 # path = self.GetPath(i) 431 # obj = path.GetLastNode().GetViewProp() 432 # obj.SetUserTransform(self.transform.T) 433 # obj.SetPosition(0, 0, 0) 434 # obj.SetOrientation(0, 0, 0) 435 # obj.SetScale(1, 1, 1) 436 # raise NotImplementedError() 437 438 def unpack(self, i=None) -> Union[List["vedo.Mesh"], "vedo.Mesh"]: 439 """Unpack the list of objects from a `Assembly`. 440 441 If `i` is given, get `i-th` object from a `Assembly`. 442 Input can be a string, in this case returns the first object 443 whose name contains the given string. 444 445 Examples: 446 - [custom_axes4.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes4.py) 447 """ 448 if i is None: 449 return self.objects 450 elif isinstance(i, int): 451 return self.objects[i] 452 elif isinstance(i, str): 453 for m in self.objects: 454 if i == m.name: 455 return m 456 return [] 457 458 def recursive_unpack(self) -> List["vedo.Mesh"]: 459 """Flatten out an Assembly.""" 460 461 def _genflatten(lst): 462 if not lst: 463 return [] 464 ## 465 if isinstance(lst[0], Assembly): 466 lst = lst[0].unpack() 467 ## 468 for elem in lst: 469 if isinstance(elem, Assembly): 470 apos = elem.GetPosition() 471 asum = np.sum(apos) 472 for x in elem.unpack(): 473 if asum: 474 yield x.clone().shift(apos) 475 else: 476 yield x 477 else: 478 yield elem 479 480 return list(_genflatten([self])) 481 482 def pickable(self, value=True) -> "Assembly": 483 """Set/get the pickability property of an assembly and its elements""" 484 self.SetPickable(value) 485 # set property to each element 486 for elem in self.recursive_unpack(): 487 elem.pickable(value) 488 return self 489 490 def clone(self) -> "Assembly": 491 """Make a clone copy of the object. Same as `copy()`.""" 492 newlist = [] 493 for a in self.objects: 494 newlist.append(a.clone()) 495 return Assembly(newlist) 496 497 def clone2d(self, pos="bottom-left", size=1, rotation=0, ontop=False, scale=None) -> Group: 498 """ 499 Convert the `Assembly` into a `Group` of 2D objects. 500 501 Arguments: 502 pos : (list, str) 503 Position in 2D, as a string or list (x,y). 504 The center of the renderer is [0,0] while top-right is [1,1]. 505 Any combination of "center", "top", "bottom", "left" and "right" will work. 506 size : (float) 507 global scaling factor for the 2D object. 508 The scaling is normalized to the x-range of the original object. 509 rotation : (float) 510 rotation angle in degrees. 511 ontop : (bool) 512 if `True` the now 2D object is rendered on top of the 3D scene. 513 scale : (float) 514 deprecated, use `size` instead. 515 516 Returns: 517 `Group` object. 518 """ 519 if scale is not None: 520 vedo.logger.warning("clone2d(scale=...) is deprecated, use clone2d(size=...) instead") 521 size = scale 522 523 padding = 0.05 524 x0, x1 = self.xbounds() 525 y0, y1 = self.ybounds() 526 pp = self.pos() 527 x0 -= pp[0] 528 x1 -= pp[0] 529 y0 -= pp[1] 530 y1 -= pp[1] 531 532 offset = [x0, y0] 533 if "cent" in pos: 534 offset = [(x0 + x1) / 2, (y0 + y1) / 2] 535 position = [0., 0.] 536 if "right" in pos: 537 offset[0] = x1 538 position = [1 - padding, 0] 539 if "left" in pos: 540 offset[0] = x0 541 position = [-1 + padding, 0] 542 if "top" in pos: 543 offset[1] = y1 544 position = [0, 1 - padding] 545 if "bottom" in pos: 546 offset[1] = y0 547 position = [0, -1 + padding] 548 elif "top" in pos: 549 if "right" in pos: 550 offset = [x1, y1] 551 position = [1 - padding, 1 - padding] 552 elif "left" in pos: 553 offset = [x0, y1] 554 position = [-1 + padding, 1 - padding] 555 else: 556 raise ValueError(f"incomplete position pos='{pos}'") 557 elif "bottom" in pos: 558 if "right" in pos: 559 offset = [x1, y0] 560 position = [1 - padding, -1 + padding] 561 elif "left" in pos: 562 offset = [x0, y0] 563 position = [-1 + padding, -1 + padding] 564 else: 565 raise ValueError(f"incomplete position pos='{pos}'") 566 else: 567 position = pos 568 569 scanned : List[Any] = [] 570 group = Group() 571 for a in self.recursive_unpack(): 572 if a in scanned: 573 continue 574 if not isinstance(a, vedo.Points): 575 continue 576 if a.npoints == 0: 577 continue 578 579 s = size * 500 / (x1 - x0) 580 if a.properties.GetRepresentation() == 1: 581 # wireframe is not rendered correctly in 2d 582 b = a.boundaries().lw(1).c(a.color(), a.alpha()) 583 if rotation: 584 b.rotate_z(rotation, around=self.origin()) 585 a2d = b.clone2d(size=s, offset=offset) 586 else: 587 if rotation: 588 # around=self.actor.GetCenter() 589 a.rotate_z(rotation, around=self.origin()) 590 a2d = a.clone2d(size=s, offset=offset) 591 a2d.pos(position).ontop(ontop) 592 group += a2d 593 594 try: # copy info from Histogram1D 595 group.entries = self.entries 596 group.frequencies = self.frequencies 597 group.errors = self.errors 598 group.edges = self.edges 599 group.centers = self.centers 600 group.mean = self.mean 601 group.mode = self.mode 602 group.std = self.std 603 except AttributeError: 604 pass 605 606 group.name = self.name 607 return group 608 609 def copy(self) -> "Assembly": 610 """Return a copy of the object. Alias of `clone()`.""" 611 return self.clone() 612 613 # def write(self, filename="assembly.npy") -> "Assembly": 614 # """ 615 # Write the object to file in `numpy` format. 616 # """ 617 # objs = [] 618 # for ob in self.unpack(): 619 # d = vedo.file_io._to_numpy(ob) 620 # objs.append(d) 621 # np.save(filename, objs) 622 # return self
Group many objects and treat them as a single new object.
215 def __init__(self, *meshs): 216 """ 217 Group many objects and treat them as a single new object, 218 keeping track of internal transformations. 219 220 Examples: 221 - [gyroscope1.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/gyroscope1.py) 222 223 ![](https://vedo.embl.es/images/simulations/39766016-85c1c1d6-52e3-11e8-8575-d167b7ce5217.gif) 224 """ 225 super().__init__() 226 227 # Init by filename 228 if len(meshs) == 1 and isinstance(meshs[0], str): 229 filename = vedo.file_io.download(meshs[0], verbose=False) 230 data = np.load(filename, allow_pickle=True) 231 meshs = [vedo.file_io._from_numpy(dd) for dd in data] 232 # Name and load from dictionary 233 if len(meshs) == 1 and isinstance(meshs[0], dict): 234 meshs = meshs[0] 235 for name in meshs: 236 meshs[name].name = name 237 meshs = list(meshs.values()) 238 else: 239 if len(meshs) == 1: 240 meshs = meshs[0] 241 else: 242 meshs = vedo.utils.flatten(meshs) 243 244 self.actor = self 245 self.actor.retrieve_object = weak_ref_to(self) 246 247 self.name = "Assembly" 248 self.filename = "" 249 self.rendered_at = set() 250 self.scalarbar = None 251 self.info = {} 252 self.time = 0 253 254 self.transform = LinearTransform() 255 256 self.objects = [m for m in meshs if m] 257 self.actors = [m.actor for m in self.objects] 258 259 scalarbars = [] 260 for a in self.actors: 261 if isinstance(a, vtki.get_class("Prop3D")): # and a.GetNumberOfPoints(): 262 self.AddPart(a) 263 if hasattr(a, "scalarbar") and a.scalarbar is not None: 264 scalarbars.append(a.scalarbar) 265 266 if len(scalarbars) > 1: 267 self.scalarbar = Group(scalarbars) 268 elif len(scalarbars) == 1: 269 self.scalarbar = scalarbars[0] 270 271 self.pipeline = vedo.utils.OperationNode( 272 "Assembly", 273 parents=self.objects, 274 comment=f"#meshes {len(self.objects)}", 275 c="#f08080", 276 ) 277 ##########################################
Group many objects and treat them as a single new object, keeping track of internal transformations.
Examples:
438 def unpack(self, i=None) -> Union[List["vedo.Mesh"], "vedo.Mesh"]: 439 """Unpack the list of objects from a `Assembly`. 440 441 If `i` is given, get `i-th` object from a `Assembly`. 442 Input can be a string, in this case returns the first object 443 whose name contains the given string. 444 445 Examples: 446 - [custom_axes4.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes4.py) 447 """ 448 if i is None: 449 return self.objects 450 elif isinstance(i, int): 451 return self.objects[i] 452 elif isinstance(i, str): 453 for m in self.objects: 454 if i == m.name: 455 return m 456 return []
458 def recursive_unpack(self) -> List["vedo.Mesh"]: 459 """Flatten out an Assembly.""" 460 461 def _genflatten(lst): 462 if not lst: 463 return [] 464 ## 465 if isinstance(lst[0], Assembly): 466 lst = lst[0].unpack() 467 ## 468 for elem in lst: 469 if isinstance(elem, Assembly): 470 apos = elem.GetPosition() 471 asum = np.sum(apos) 472 for x in elem.unpack(): 473 if asum: 474 yield x.clone().shift(apos) 475 else: 476 yield x 477 else: 478 yield elem 479 480 return list(_genflatten([self]))
Flatten out an Assembly.
482 def pickable(self, value=True) -> "Assembly": 483 """Set/get the pickability property of an assembly and its elements""" 484 self.SetPickable(value) 485 # set property to each element 486 for elem in self.recursive_unpack(): 487 elem.pickable(value) 488 return self
Set/get the pickability property of an assembly and its elements
490 def clone(self) -> "Assembly": 491 """Make a clone copy of the object. Same as `copy()`.""" 492 newlist = [] 493 for a in self.objects: 494 newlist.append(a.clone()) 495 return Assembly(newlist)
Make a clone copy of the object. Same as copy()
.
497 def clone2d(self, pos="bottom-left", size=1, rotation=0, ontop=False, scale=None) -> Group: 498 """ 499 Convert the `Assembly` into a `Group` of 2D objects. 500 501 Arguments: 502 pos : (list, str) 503 Position in 2D, as a string or list (x,y). 504 The center of the renderer is [0,0] while top-right is [1,1]. 505 Any combination of "center", "top", "bottom", "left" and "right" will work. 506 size : (float) 507 global scaling factor for the 2D object. 508 The scaling is normalized to the x-range of the original object. 509 rotation : (float) 510 rotation angle in degrees. 511 ontop : (bool) 512 if `True` the now 2D object is rendered on top of the 3D scene. 513 scale : (float) 514 deprecated, use `size` instead. 515 516 Returns: 517 `Group` object. 518 """ 519 if scale is not None: 520 vedo.logger.warning("clone2d(scale=...) is deprecated, use clone2d(size=...) instead") 521 size = scale 522 523 padding = 0.05 524 x0, x1 = self.xbounds() 525 y0, y1 = self.ybounds() 526 pp = self.pos() 527 x0 -= pp[0] 528 x1 -= pp[0] 529 y0 -= pp[1] 530 y1 -= pp[1] 531 532 offset = [x0, y0] 533 if "cent" in pos: 534 offset = [(x0 + x1) / 2, (y0 + y1) / 2] 535 position = [0., 0.] 536 if "right" in pos: 537 offset[0] = x1 538 position = [1 - padding, 0] 539 if "left" in pos: 540 offset[0] = x0 541 position = [-1 + padding, 0] 542 if "top" in pos: 543 offset[1] = y1 544 position = [0, 1 - padding] 545 if "bottom" in pos: 546 offset[1] = y0 547 position = [0, -1 + padding] 548 elif "top" in pos: 549 if "right" in pos: 550 offset = [x1, y1] 551 position = [1 - padding, 1 - padding] 552 elif "left" in pos: 553 offset = [x0, y1] 554 position = [-1 + padding, 1 - padding] 555 else: 556 raise ValueError(f"incomplete position pos='{pos}'") 557 elif "bottom" in pos: 558 if "right" in pos: 559 offset = [x1, y0] 560 position = [1 - padding, -1 + padding] 561 elif "left" in pos: 562 offset = [x0, y0] 563 position = [-1 + padding, -1 + padding] 564 else: 565 raise ValueError(f"incomplete position pos='{pos}'") 566 else: 567 position = pos 568 569 scanned : List[Any] = [] 570 group = Group() 571 for a in self.recursive_unpack(): 572 if a in scanned: 573 continue 574 if not isinstance(a, vedo.Points): 575 continue 576 if a.npoints == 0: 577 continue 578 579 s = size * 500 / (x1 - x0) 580 if a.properties.GetRepresentation() == 1: 581 # wireframe is not rendered correctly in 2d 582 b = a.boundaries().lw(1).c(a.color(), a.alpha()) 583 if rotation: 584 b.rotate_z(rotation, around=self.origin()) 585 a2d = b.clone2d(size=s, offset=offset) 586 else: 587 if rotation: 588 # around=self.actor.GetCenter() 589 a.rotate_z(rotation, around=self.origin()) 590 a2d = a.clone2d(size=s, offset=offset) 591 a2d.pos(position).ontop(ontop) 592 group += a2d 593 594 try: # copy info from Histogram1D 595 group.entries = self.entries 596 group.frequencies = self.frequencies 597 group.errors = self.errors 598 group.edges = self.edges 599 group.centers = self.centers 600 group.mean = self.mean 601 group.mode = self.mode 602 group.std = self.std 603 except AttributeError: 604 pass 605 606 group.name = self.name 607 return group
Convert the Assembly
into a Group
of 2D objects.
Arguments:
- pos : (list, str) Position in 2D, as a string or list (x,y). The center of the renderer is [0,0] while top-right is [1,1]. Any combination of "center", "top", "bottom", "left" and "right" will work.
- size : (float) global scaling factor for the 2D object. The scaling is normalized to the x-range of the original object.
- rotation : (float) rotation angle in degrees.
- ontop : (bool)
if
True
the now 2D object is rendered on top of the 3D scene. - scale : (float)
deprecated, use
size
instead.
Returns:
Group
object.
609 def copy(self) -> "Assembly": 610 """Return a copy of the object. Alias of `clone()`.""" 611 return self.clone()
Return a copy of the object. Alias of clone()
.
Inherited Members
26def procrustes_alignment(sources: List["vedo.Mesh"], rigid=False) -> "Assembly": 27 """ 28 Return an `Assembly` of aligned source meshes with the `Procrustes` algorithm. 29 The output `Assembly` is normalized in size. 30 31 The `Procrustes` algorithm takes N set of points and aligns them in a least-squares sense 32 to their mutual mean. The algorithm is iterated until convergence, 33 as the mean must be recomputed after each alignment. 34 35 The set of average points generated by the algorithm can be accessed with 36 `algoutput.info['mean']` as a numpy array. 37 38 Arguments: 39 rigid : bool 40 if `True` scaling is disabled. 41 42 Examples: 43 - [align4.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align4.py) 44 45 ![](https://vedo.embl.es/images/basic/align4.png) 46 """ 47 48 group = vtki.new("MultiBlockDataGroupFilter") 49 for source in sources: 50 if sources[0].npoints != source.npoints: 51 vedo.logger.error("sources have different nr of points") 52 raise RuntimeError() 53 group.AddInputData(source.dataset) 54 procrustes = vtki.new("ProcrustesAlignmentFilter") 55 procrustes.StartFromCentroidOn() 56 procrustes.SetInputConnection(group.GetOutputPort()) 57 if rigid: 58 procrustes.GetLandmarkTransform().SetModeToRigidBody() 59 procrustes.Update() 60 61 acts = [] 62 for i, s in enumerate(sources): 63 poly = procrustes.GetOutput().GetBlock(i) 64 mesh = vedo.mesh.Mesh(poly) 65 mesh.actor.SetProperty(s.actor.GetProperty()) 66 mesh.properties = s.actor.GetProperty() 67 if hasattr(s, "name"): 68 mesh.name = s.name 69 acts.append(mesh) 70 assem = Assembly(acts) 71 assem.transform = procrustes.GetLandmarkTransform() 72 assem.info["mean"] = vedo.utils.vtk2numpy(procrustes.GetMeanPoints().GetData()) 73 return assem
Return an Assembly
of aligned source meshes with the Procrustes
algorithm.
The output Assembly
is normalized in size.
The Procrustes
algorithm takes N set of points and aligns them in a least-squares sense
to their mutual mean. The algorithm is iterated until convergence,
as the mean must be recomputed after each alignment.
The set of average points generated by the algorithm can be accessed with
algoutput.info['mean']
as a numpy array.
Arguments:
- rigid : bool
if
True
scaling is disabled.