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