vedo.assembly

Submodule for managing groups of vedo objects

  1#!/usr/bin/env python3
  2# -*- coding: utf-8 -*-
  3import numpy as np
  4from weakref import ref as weak_ref_to
  5
  6import vedo.vtkclasses as vtk
  7
  8import vedo
  9from vedo.transformations import LinearTransform
 10from vedo.visual import CommonVisual, Actor3DHelper
 11
 12__docformat__ = "google"
 13
 14__doc__ = """
 15Submodule for managing groups of vedo objects
 16
 17![](https://vedo.embl.es/images/basic/align4.png)
 18"""
 19
 20__all__ = ["Group", "Assembly", "procrustes_alignment"]
 21
 22
 23#################################################
 24def procrustes_alignment(sources, rigid=False):
 25    """
 26    Return an `Assembly` of aligned source meshes with the `Procrustes` algorithm.
 27    The output `Assembly` is normalized in size.
 28
 29    The `Procrustes` algorithm takes N set of points and aligns them in a least-squares sense
 30    to their mutual mean. The algorithm is iterated until convergence,
 31    as the mean must be recomputed after each alignment.
 32
 33    The set of average points generated by the algorithm can be accessed with
 34    `algoutput.info['mean']` as a numpy array.
 35
 36    Arguments:
 37        rigid : bool
 38            if `True` scaling is disabled.
 39
 40    Examples:
 41        - [align4.py](https://github.com/marcomusy/vedo/tree/master/examples/basic/align4.py)
 42
 43        ![](https://vedo.embl.es/images/basic/align4.png)
 44    """
 45
 46    group = vtk.new("MultiBlockDataGroupFilter")
 47    for source in sources:
 48        if sources[0].npoints != source.npoints:
 49            vedo.logger.error("sources have different nr of points")
 50            raise RuntimeError()
 51        group.AddInputData(source.dataset)
 52    procrustes = vtk.new("ProcrustesAlignmentFilter")
 53    procrustes.StartFromCentroidOn()
 54    procrustes.SetInputConnection(group.GetOutputPort())
 55    if rigid:
 56        procrustes.GetLandmarkTransform().SetModeToRigidBody()
 57    procrustes.Update()
 58
 59    acts = []
 60    for i, s in enumerate(sources):
 61        poly = procrustes.GetOutput().GetBlock(i)
 62        mesh = vedo.mesh.Mesh(poly)
 63        mesh.actor.SetProperty(s.actor.GetProperty())
 64        mesh.properties = s.actor.GetProperty()
 65        if hasattr(s, "name"):
 66            mesh.name = s.name
 67        acts.append(mesh)
 68    assem = Assembly(acts)
 69    assem.transform = procrustes.GetLandmarkTransform()
 70    assem.info["mean"] = vedo.utils.vtk2numpy(procrustes.GetMeanPoints().GetData())
 71    return assem
 72
 73
 74#################################################
 75class Group(CommonVisual, vtk.vtkPropAssembly):
 76    """Form groups of generic objects (not necessarily meshes)."""
 77
 78    def __init__(self, objects=()):
 79        """Form groups of generic objects (not necessarily meshes)."""
 80
 81        super().__init__()
 82
 83        self.actor = self
 84
 85        self.name = "Group"
 86        self.filename = ""
 87        self.trail = None
 88        self.trail_points = []
 89        self.trail_segment_size = 0
 90        self.trail_offset = None
 91        self.shadows = []
 92        self.info = {}
 93        self.rendered_at = set()
 94        self.scalarbar = None
 95
 96        for a in vedo.utils.flatten(objects):
 97            if a:
 98                self.AddPart(a.actor)
 99
100        self.PickableOff()
101
102
103    def __iadd__(self, obj):
104        """
105        Add an object to the group
106        """
107        if not vedo.utils.is_sequence(obj):
108            obj = [obj]
109        for a in obj:
110            if a:
111                self.AddPart(a)
112        return self
113
114    def unpack(self):
115        """Unpack the group into its elements"""
116        elements = []
117        self.InitPathTraversal()
118        parts = self.GetParts()
119        parts.InitTraversal()
120        for i in range(parts.GetNumberOfItems()):
121            ele = parts.GetItemAsObject(i)
122            elements.append(ele)
123
124        # gr.InitPathTraversal()
125        # for _ in range(gr.GetNumberOfPaths()):
126        #     path  = gr.GetNextPath()
127        #     print([path])
128        #     path.InitTraversal()
129        #     for i in range(path.GetNumberOfItems()):
130        #         a = path.GetItemAsObject(i).GetViewProp()
131        #         print([a])
132
133        return elements
134
135    def clear(self):
136        """Remove all parts"""
137        for a in self.unpack():
138            self.RemovePart(a)
139        return self
140
141    def on(self):
142        """Switch on visibility"""
143        self.VisibilityOn()
144        return self
145
146    def off(self):
147        """Switch off visibility"""
148        self.VisibilityOff()
149        return self
150
151    def pickable(self, value=None):
152        """Set/get the pickability property of an object."""
153        if value is None:
154            return self.GetPickable()
155        self.SetPickable(value)
156        return self
157
158    def draggable(self, value=None):
159        """Set/get the draggability property of an object."""
160        if value is None:
161            return self.GetDragable()
162        self.SetDragable(value)
163        return self
164
165    def pos(self, x=None, y=None):
166        """Set/Get object 2D position on the screen."""
167        if x is None:  # get functionality
168            return np.array(self.GetPosition())
169
170        if y is None:  # assume x is of the form (x,y)
171            x, y = x
172        self.SetPosition(x, y)
173        return self
174
175    def shift(self, ds):
176        """Add a shift to the current object position on the screen."""
177        p = np.array(self.GetPosition())
178        self.SetPosition(p + ds)
179        return self
180
181    def bounds(self):
182        """
183        Get the object 2D bounds.
184        Returns a list in format [xmin,xmax, ymin,ymax].
185        """
186        return self.GetBounds()
187
188
189#################################################
190class Assembly(CommonVisual, Actor3DHelper, vtk.vtkAssembly):
191    """
192    Group many objects and treat them as a single new object.
193    """
194
195    def __init__(self, *meshs):
196        """
197        Group many objects and treat them as a single new object,
198        keeping track of internal transformations.
199
200        Examples:
201            - [gyroscope1.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/gyroscope1.py)
202
203            ![](https://vedo.embl.es/images/simulations/39766016-85c1c1d6-52e3-11e8-8575-d167b7ce5217.gif)
204        """
205        super().__init__()
206
207        # Init by filename
208        if len(meshs) == 1 and isinstance(meshs[0], str):
209            filename = vedo.file_io.download(meshs[0], verbose=False)
210            data = np.load(filename, allow_pickle=True)
211            meshs = [vedo.file_io._from_numpy(dd) for dd in data]
212
213        if len(meshs) == 1:
214            meshs = meshs[0]
215        else:
216            meshs = vedo.utils.flatten(meshs)
217
218        self.actor = self
219        self.actor.retrieve_object = weak_ref_to(self)
220
221        self.name = "Assembly"
222        self.filename = ""
223        self.rendered_at = set()
224        self.scalarbar = None
225        self.info = {}
226        self.time = 0
227
228        self.transform = LinearTransform()
229
230        self.objects = [m for m in meshs if m]
231        self.actors  = [m.actor for m in self.objects]
232
233        scalarbars = []
234        for a in self.actors:
235            if isinstance(a, vtk.get_class("Prop3D")): # and a.GetNumberOfPoints():
236                self.AddPart(a)
237            if hasattr(a, "scalarbar") and a.scalarbar is not None:
238                scalarbars.append(a.scalarbar)
239
240        if len(scalarbars) > 1:
241            self.scalarbar = Group(scalarbars)
242        elif len(scalarbars) == 1:
243            self.scalarbar = scalarbars[0]
244
245        self.pipeline = vedo.utils.OperationNode(
246            "Assembly",
247            parents=self.objects,
248            comment=f"#meshes {len(self.objects)}",
249            c="#f08080",
250        )
251        ##########################################
252
253    def __str__(self):
254        """Print info about Assembly object."""
255        module = self.__class__.__module__
256        name = self.__class__.__name__
257        out = vedo.printc(
258            f"{module}.{name} at ({hex(id(self))})".ljust(75),
259            bold=True, invert=True, return_string=True,
260        )
261        out += "\x1b[0m"
262
263        if self.name:
264            out += "name".ljust(14) + ": " + self.name
265            if "legend" in self.info.keys() and self.info["legend"]:
266                out+= f", legend='{self.info['legend']}'"
267            out += "\n"
268
269        n = len(self.unpack())
270        out += "n. of objects".ljust(14) + ": " + str(n) + " "
271        names = [a.name for a in self.unpack() if a.name]
272        if names:
273            out += str(names).replace("'","")[:56]
274        out += "\n"
275
276        pos = self.GetPosition()
277        out += "position".ljust(14) + ": " + str(pos) + "\n"
278
279        bnds = self.GetBounds()
280        bx1, bx2 = vedo.utils.precision(bnds[0], 3), vedo.utils.precision(bnds[1], 3)
281        by1, by2 = vedo.utils.precision(bnds[2], 3), vedo.utils.precision(bnds[3], 3)
282        bz1, bz2 = vedo.utils.precision(bnds[4], 3), vedo.utils.precision(bnds[5], 3)
283        out+= "bounds".ljust(14) + ":"
284        out+= " x=(" + bx1 + ", " + bx2 + "),"
285        out+= " y=(" + by1 + ", " + by2 + "),"
286        out+= " z=(" + bz1 + ", " + bz2 + ")\n"
287        return out.rstrip() + "\x1b[0m"
288
289    def _repr_html_(self):
290        """
291        HTML representation of the Assembly object for Jupyter Notebooks.
292
293        Returns:
294            HTML text with the image and some properties.
295        """
296        import io
297        import base64
298        from PIL import Image
299
300        library_name = "vedo.assembly.Assembly"
301        help_url = "https://vedo.embl.es/docs/vedo/assembly.html"
302
303        arr = self.thumbnail(zoom=1.1, elevation=-60)
304
305        im = Image.fromarray(arr)
306        buffered = io.BytesIO()
307        im.save(buffered, format="PNG", quality=100)
308        encoded = base64.b64encode(buffered.getvalue()).decode("utf-8")
309        url = "data:image/png;base64," + encoded
310        image = f"<img src='{url}'></img>"
311
312        # statisitics
313        bounds = "<br/>".join(
314            [
315                vedo.utils.precision(min_x, 4) + " ... " + vedo.utils.precision(max_x, 4)
316                for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2])
317            ]
318        )
319
320        help_text = ""
321        if self.name:
322            help_text += f"<b> {self.name}: &nbsp&nbsp</b>"
323        help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>"
324        if self.filename:
325            dots = ""
326            if len(self.filename) > 30:
327                dots = "..."
328            help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>"
329
330        allt = [
331            "<table>",
332            "<tr>",
333            "<td>",
334            image,
335            "</td>",
336            "<td style='text-align: center; vertical-align: center;'><br/>",
337            help_text,
338            "<table>",
339            "<tr><td><b> nr. of objects </b></td><td>"
340            + str(self.GetNumberOfPaths())
341            + "</td></tr>",
342            "<tr><td><b> position </b></td><td>" + str(self.GetPosition()) + "</td></tr>",
343            "<tr><td><b> diagonal size </b></td><td>"
344            + vedo.utils.precision(self.diagonal_size(), 5)
345            + "</td></tr>",
346            "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>",
347            "</table>",
348            "</table>",
349        ]
350        return "\n".join(allt)
351
352    def __add__(self, obj):
353        """
354        Add an object to the assembly
355        """
356        if isinstance(obj, vtk.get_class("Prop3D")):
357
358            self.objects.append(obj)
359            self.actors.append(obj.actor)
360            self.AddPart(obj.actor)
361
362            if hasattr(obj, "scalarbar") and obj.scalarbar is not None:
363                if self.scalarbar is None:
364                    self.scalarbar = obj.scalarbar
365                    return self
366
367                def unpack_group(scalarbar):
368                    if isinstance(scalarbar, Group):
369                        return scalarbar.unpack()
370                    else:
371                        return scalarbar
372
373                if isinstance(self.scalarbar, Group):
374                    self.scalarbar += unpack_group(obj.scalarbar)
375                else:
376                    self.scalarbar = Group([unpack_group(self.scalarbar), unpack_group(obj.scalarbar)])
377            self.pipeline = vedo.utils.OperationNode("add mesh", parents=[self, obj], c="#f08080")
378        return self
379
380    def __contains__(self, obj):
381        """Allows to use `in` to check if an object is in the `Assembly`."""
382        return obj in self.objects
383
384    def __getitem__(self, i):
385        """Return i-th object."""
386        if isinstance(i, int):
387            return self.objects[i]
388        elif isinstance(i, str):
389            for m in self.objects:
390                if i in m.name:
391                    return m
392        return None
393
394    def __len__(self):
395        """Return nr. of objects in the assembly."""
396        return len(self.objects)
397
398    # TODO ####
399    # def propagate_transform(self):
400    #     """Propagate the transformation to all parts."""
401    #     # navigate the assembly and apply the transform to all parts
402    #     # and reset position, orientation and scale of the assembly
403    #     for i in range(self.GetNumberOfPaths()):
404    #         path = self.GetPath(i)
405    #         obj = path.GetLastNode().GetViewProp()
406    #         obj.SetUserTransform(self.transform.T)
407    #         obj.SetPosition(0, 0, 0)
408    #         obj.SetOrientation(0, 0, 0)
409    #         obj.SetScale(1, 1, 1)
410    #     raise NotImplementedError()
411
412    def unpack(self, i=None):
413        """Unpack the list of objects from a `Assembly`.
414
415        If `i` is given, get `i-th` object from a `Assembly`.
416        Input can be a string, in this case returns the first object
417        whose name contains the given string.
418
419        Examples:
420            - [custom_axes4.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes4.py)
421        """
422        if i is None:
423            return self.objects
424        elif isinstance(i, int):
425            return self.objects[i]
426        elif isinstance(i, str):
427            for m in self.objects:
428                if i in m.name:
429                    return m
430
431    def recursive_unpack(self):
432        """Flatten out an Assembly."""
433
434        def _genflatten(lst):
435            if not lst:
436                return []
437            ##
438            if isinstance(lst[0], Assembly):
439                lst = lst[0].unpack()
440            ##
441            for elem in lst:
442                if isinstance(elem, Assembly):
443                    apos = elem.GetPosition()
444                    asum = np.sum(apos)
445                    for x in elem.unpack():
446                        if asum:
447                            yield x.clone().shift(apos)
448                        else:
449                            yield x
450                else:
451                    yield elem
452
453        return list(_genflatten([self]))
454
455    def pickable(self, value=True):
456        """Set/get the pickability property of an assembly and its elements"""
457        self.SetPickable(value)
458        # set property to each element
459        for elem in self.recursive_unpack():
460            elem.pickable(value)
461        return self
462
463    def clone(self):
464        """Make a clone copy of the object. Same as `copy()`."""
465        newlist = []
466        for a in self.objects:
467            newlist.append(a.clone())
468        return Assembly(newlist)
469    
470    def copy(self):
471        """Return a copy of the object. Alias of `clone()`."""
472        return self.clone()
473
474    def write(self, filename="assembly.npy"):
475        """
476        Write the object to file in `numpy` format.
477        """
478        objs = []
479        for ob in self.unpack():
480            d = vedo.file_io._to_numpy(ob)
481            objs.append(d)
482        np.save(filename, objs)
483        return self
class Group(vedo.visual.CommonVisual, vtkmodules.vtkRenderingCore.vtkPropAssembly):
 76class Group(CommonVisual, vtk.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
 82        super().__init__()
 83
 84        self.actor = self
 85
 86        self.name = "Group"
 87        self.filename = ""
 88        self.trail = None
 89        self.trail_points = []
 90        self.trail_segment_size = 0
 91        self.trail_offset = None
 92        self.shadows = []
 93        self.info = {}
 94        self.rendered_at = set()
 95        self.scalarbar = None
 96
 97        for a in vedo.utils.flatten(objects):
 98            if a:
 99                self.AddPart(a.actor)
100
101        self.PickableOff()
102
103
104    def __iadd__(self, obj):
105        """
106        Add an object to the group
107        """
108        if not vedo.utils.is_sequence(obj):
109            obj = [obj]
110        for a in obj:
111            if a:
112                self.AddPart(a)
113        return self
114
115    def unpack(self):
116        """Unpack the group into its elements"""
117        elements = []
118        self.InitPathTraversal()
119        parts = self.GetParts()
120        parts.InitTraversal()
121        for i in range(parts.GetNumberOfItems()):
122            ele = parts.GetItemAsObject(i)
123            elements.append(ele)
124
125        # gr.InitPathTraversal()
126        # for _ in range(gr.GetNumberOfPaths()):
127        #     path  = gr.GetNextPath()
128        #     print([path])
129        #     path.InitTraversal()
130        #     for i in range(path.GetNumberOfItems()):
131        #         a = path.GetItemAsObject(i).GetViewProp()
132        #         print([a])
133
134        return elements
135
136    def clear(self):
137        """Remove all parts"""
138        for a in self.unpack():
139            self.RemovePart(a)
140        return self
141
142    def on(self):
143        """Switch on visibility"""
144        self.VisibilityOn()
145        return self
146
147    def off(self):
148        """Switch off visibility"""
149        self.VisibilityOff()
150        return self
151
152    def pickable(self, value=None):
153        """Set/get the pickability property of an object."""
154        if value is None:
155            return self.GetPickable()
156        self.SetPickable(value)
157        return self
158
159    def draggable(self, value=None):
160        """Set/get the draggability property of an object."""
161        if value is None:
162            return self.GetDragable()
163        self.SetDragable(value)
164        return self
165
166    def pos(self, x=None, y=None):
167        """Set/Get object 2D position on the screen."""
168        if x is None:  # get functionality
169            return np.array(self.GetPosition())
170
171        if y is None:  # assume x is of the form (x,y)
172            x, y = x
173        self.SetPosition(x, y)
174        return self
175
176    def shift(self, ds):
177        """Add a shift to the current object position on the screen."""
178        p = np.array(self.GetPosition())
179        self.SetPosition(p + ds)
180        return self
181
182    def bounds(self):
183        """
184        Get the object 2D bounds.
185        Returns a list in format [xmin,xmax, ymin,ymax].
186        """
187        return self.GetBounds()

Form groups of generic objects (not necessarily meshes).

Group(objects=())
 79    def __init__(self, objects=()):
 80        """Form groups of generic objects (not necessarily meshes)."""
 81
 82        super().__init__()
 83
 84        self.actor = self
 85
 86        self.name = "Group"
 87        self.filename = ""
 88        self.trail = None
 89        self.trail_points = []
 90        self.trail_segment_size = 0
 91        self.trail_offset = None
 92        self.shadows = []
 93        self.info = {}
 94        self.rendered_at = set()
 95        self.scalarbar = None
 96
 97        for a in vedo.utils.flatten(objects):
 98            if a:
 99                self.AddPart(a.actor)
100
101        self.PickableOff()

Form groups of generic objects (not necessarily meshes).

def unpack(self):
115    def unpack(self):
116        """Unpack the group into its elements"""
117        elements = []
118        self.InitPathTraversal()
119        parts = self.GetParts()
120        parts.InitTraversal()
121        for i in range(parts.GetNumberOfItems()):
122            ele = parts.GetItemAsObject(i)
123            elements.append(ele)
124
125        # gr.InitPathTraversal()
126        # for _ in range(gr.GetNumberOfPaths()):
127        #     path  = gr.GetNextPath()
128        #     print([path])
129        #     path.InitTraversal()
130        #     for i in range(path.GetNumberOfItems()):
131        #         a = path.GetItemAsObject(i).GetViewProp()
132        #         print([a])
133
134        return elements

Unpack the group into its elements

def clear(self):
136    def clear(self):
137        """Remove all parts"""
138        for a in self.unpack():
139            self.RemovePart(a)
140        return self

Remove all parts

def on(self):
142    def on(self):
143        """Switch on visibility"""
144        self.VisibilityOn()
145        return self

Switch on visibility

def off(self):
147    def off(self):
148        """Switch off visibility"""
149        self.VisibilityOff()
150        return self

Switch off visibility

def pickable(self, value=None):
152    def pickable(self, value=None):
153        """Set/get the pickability property of an object."""
154        if value is None:
155            return self.GetPickable()
156        self.SetPickable(value)
157        return self

Set/get the pickability property of an object.

def draggable(self, value=None):
159    def draggable(self, value=None):
160        """Set/get the draggability property of an object."""
161        if value is None:
162            return self.GetDragable()
163        self.SetDragable(value)
164        return self

Set/get the draggability property of an object.

def pos(self, x=None, y=None):
166    def pos(self, x=None, y=None):
167        """Set/Get object 2D position on the screen."""
168        if x is None:  # get functionality
169            return np.array(self.GetPosition())
170
171        if y is None:  # assume x is of the form (x,y)
172            x, y = x
173        self.SetPosition(x, y)
174        return self

Set/Get object 2D position on the screen.

def shift(self, ds):
176    def shift(self, ds):
177        """Add a shift to the current object position on the screen."""
178        p = np.array(self.GetPosition())
179        self.SetPosition(p + ds)
180        return self

Add a shift to the current object position on the screen.

def bounds(self):
182    def bounds(self):
183        """
184        Get the object 2D bounds.
185        Returns a list in format [xmin,xmax, ymin,ymax].
186        """
187        return self.GetBounds()

Get the object 2D bounds. Returns a list in format [xmin,xmax, ymin,ymax].

class Assembly(vedo.visual.CommonVisual, vedo.visual.Actor3DHelper, vtkmodules.vtkRenderingCore.vtkAssembly):
191class Assembly(CommonVisual, Actor3DHelper, vtk.vtkAssembly):
192    """
193    Group many objects and treat them as a single new object.
194    """
195
196    def __init__(self, *meshs):
197        """
198        Group many objects and treat them as a single new object,
199        keeping track of internal transformations.
200
201        Examples:
202            - [gyroscope1.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/gyroscope1.py)
203
204            ![](https://vedo.embl.es/images/simulations/39766016-85c1c1d6-52e3-11e8-8575-d167b7ce5217.gif)
205        """
206        super().__init__()
207
208        # Init by filename
209        if len(meshs) == 1 and isinstance(meshs[0], str):
210            filename = vedo.file_io.download(meshs[0], verbose=False)
211            data = np.load(filename, allow_pickle=True)
212            meshs = [vedo.file_io._from_numpy(dd) for dd in data]
213
214        if len(meshs) == 1:
215            meshs = meshs[0]
216        else:
217            meshs = vedo.utils.flatten(meshs)
218
219        self.actor = self
220        self.actor.retrieve_object = weak_ref_to(self)
221
222        self.name = "Assembly"
223        self.filename = ""
224        self.rendered_at = set()
225        self.scalarbar = None
226        self.info = {}
227        self.time = 0
228
229        self.transform = LinearTransform()
230
231        self.objects = [m for m in meshs if m]
232        self.actors  = [m.actor for m in self.objects]
233
234        scalarbars = []
235        for a in self.actors:
236            if isinstance(a, vtk.get_class("Prop3D")): # and a.GetNumberOfPoints():
237                self.AddPart(a)
238            if hasattr(a, "scalarbar") and a.scalarbar is not None:
239                scalarbars.append(a.scalarbar)
240
241        if len(scalarbars) > 1:
242            self.scalarbar = Group(scalarbars)
243        elif len(scalarbars) == 1:
244            self.scalarbar = scalarbars[0]
245
246        self.pipeline = vedo.utils.OperationNode(
247            "Assembly",
248            parents=self.objects,
249            comment=f"#meshes {len(self.objects)}",
250            c="#f08080",
251        )
252        ##########################################
253
254    def __str__(self):
255        """Print info about Assembly object."""
256        module = self.__class__.__module__
257        name = self.__class__.__name__
258        out = vedo.printc(
259            f"{module}.{name} at ({hex(id(self))})".ljust(75),
260            bold=True, invert=True, return_string=True,
261        )
262        out += "\x1b[0m"
263
264        if self.name:
265            out += "name".ljust(14) + ": " + self.name
266            if "legend" in self.info.keys() and self.info["legend"]:
267                out+= f", legend='{self.info['legend']}'"
268            out += "\n"
269
270        n = len(self.unpack())
271        out += "n. of objects".ljust(14) + ": " + str(n) + " "
272        names = [a.name for a in self.unpack() if a.name]
273        if names:
274            out += str(names).replace("'","")[:56]
275        out += "\n"
276
277        pos = self.GetPosition()
278        out += "position".ljust(14) + ": " + str(pos) + "\n"
279
280        bnds = self.GetBounds()
281        bx1, bx2 = vedo.utils.precision(bnds[0], 3), vedo.utils.precision(bnds[1], 3)
282        by1, by2 = vedo.utils.precision(bnds[2], 3), vedo.utils.precision(bnds[3], 3)
283        bz1, bz2 = vedo.utils.precision(bnds[4], 3), vedo.utils.precision(bnds[5], 3)
284        out+= "bounds".ljust(14) + ":"
285        out+= " x=(" + bx1 + ", " + bx2 + "),"
286        out+= " y=(" + by1 + ", " + by2 + "),"
287        out+= " z=(" + bz1 + ", " + bz2 + ")\n"
288        return out.rstrip() + "\x1b[0m"
289
290    def _repr_html_(self):
291        """
292        HTML representation of the Assembly object for Jupyter Notebooks.
293
294        Returns:
295            HTML text with the image and some properties.
296        """
297        import io
298        import base64
299        from PIL import Image
300
301        library_name = "vedo.assembly.Assembly"
302        help_url = "https://vedo.embl.es/docs/vedo/assembly.html"
303
304        arr = self.thumbnail(zoom=1.1, elevation=-60)
305
306        im = Image.fromarray(arr)
307        buffered = io.BytesIO()
308        im.save(buffered, format="PNG", quality=100)
309        encoded = base64.b64encode(buffered.getvalue()).decode("utf-8")
310        url = "data:image/png;base64," + encoded
311        image = f"<img src='{url}'></img>"
312
313        # statisitics
314        bounds = "<br/>".join(
315            [
316                vedo.utils.precision(min_x, 4) + " ... " + vedo.utils.precision(max_x, 4)
317                for min_x, max_x in zip(self.bounds()[::2], self.bounds()[1::2])
318            ]
319        )
320
321        help_text = ""
322        if self.name:
323            help_text += f"<b> {self.name}: &nbsp&nbsp</b>"
324        help_text += '<b><a href="' + help_url + '" target="_blank">' + library_name + "</a></b>"
325        if self.filename:
326            dots = ""
327            if len(self.filename) > 30:
328                dots = "..."
329            help_text += f"<br/><code><i>({dots}{self.filename[-30:]})</i></code>"
330
331        allt = [
332            "<table>",
333            "<tr>",
334            "<td>",
335            image,
336            "</td>",
337            "<td style='text-align: center; vertical-align: center;'><br/>",
338            help_text,
339            "<table>",
340            "<tr><td><b> nr. of objects </b></td><td>"
341            + str(self.GetNumberOfPaths())
342            + "</td></tr>",
343            "<tr><td><b> position </b></td><td>" + str(self.GetPosition()) + "</td></tr>",
344            "<tr><td><b> diagonal size </b></td><td>"
345            + vedo.utils.precision(self.diagonal_size(), 5)
346            + "</td></tr>",
347            "<tr><td><b> bounds </b> <br/> (x/y/z) </td><td>" + str(bounds) + "</td></tr>",
348            "</table>",
349            "</table>",
350        ]
351        return "\n".join(allt)
352
353    def __add__(self, obj):
354        """
355        Add an object to the assembly
356        """
357        if isinstance(obj, vtk.get_class("Prop3D")):
358
359            self.objects.append(obj)
360            self.actors.append(obj.actor)
361            self.AddPart(obj.actor)
362
363            if hasattr(obj, "scalarbar") and obj.scalarbar is not None:
364                if self.scalarbar is None:
365                    self.scalarbar = obj.scalarbar
366                    return self
367
368                def unpack_group(scalarbar):
369                    if isinstance(scalarbar, Group):
370                        return scalarbar.unpack()
371                    else:
372                        return scalarbar
373
374                if isinstance(self.scalarbar, Group):
375                    self.scalarbar += unpack_group(obj.scalarbar)
376                else:
377                    self.scalarbar = Group([unpack_group(self.scalarbar), unpack_group(obj.scalarbar)])
378            self.pipeline = vedo.utils.OperationNode("add mesh", parents=[self, obj], c="#f08080")
379        return self
380
381    def __contains__(self, obj):
382        """Allows to use `in` to check if an object is in the `Assembly`."""
383        return obj in self.objects
384
385    def __getitem__(self, i):
386        """Return i-th object."""
387        if isinstance(i, int):
388            return self.objects[i]
389        elif isinstance(i, str):
390            for m in self.objects:
391                if i in m.name:
392                    return m
393        return None
394
395    def __len__(self):
396        """Return nr. of objects in the assembly."""
397        return len(self.objects)
398
399    # TODO ####
400    # def propagate_transform(self):
401    #     """Propagate the transformation to all parts."""
402    #     # navigate the assembly and apply the transform to all parts
403    #     # and reset position, orientation and scale of the assembly
404    #     for i in range(self.GetNumberOfPaths()):
405    #         path = self.GetPath(i)
406    #         obj = path.GetLastNode().GetViewProp()
407    #         obj.SetUserTransform(self.transform.T)
408    #         obj.SetPosition(0, 0, 0)
409    #         obj.SetOrientation(0, 0, 0)
410    #         obj.SetScale(1, 1, 1)
411    #     raise NotImplementedError()
412
413    def unpack(self, i=None):
414        """Unpack the list of objects from a `Assembly`.
415
416        If `i` is given, get `i-th` object from a `Assembly`.
417        Input can be a string, in this case returns the first object
418        whose name contains the given string.
419
420        Examples:
421            - [custom_axes4.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes4.py)
422        """
423        if i is None:
424            return self.objects
425        elif isinstance(i, int):
426            return self.objects[i]
427        elif isinstance(i, str):
428            for m in self.objects:
429                if i in m.name:
430                    return m
431
432    def recursive_unpack(self):
433        """Flatten out an Assembly."""
434
435        def _genflatten(lst):
436            if not lst:
437                return []
438            ##
439            if isinstance(lst[0], Assembly):
440                lst = lst[0].unpack()
441            ##
442            for elem in lst:
443                if isinstance(elem, Assembly):
444                    apos = elem.GetPosition()
445                    asum = np.sum(apos)
446                    for x in elem.unpack():
447                        if asum:
448                            yield x.clone().shift(apos)
449                        else:
450                            yield x
451                else:
452                    yield elem
453
454        return list(_genflatten([self]))
455
456    def pickable(self, value=True):
457        """Set/get the pickability property of an assembly and its elements"""
458        self.SetPickable(value)
459        # set property to each element
460        for elem in self.recursive_unpack():
461            elem.pickable(value)
462        return self
463
464    def clone(self):
465        """Make a clone copy of the object. Same as `copy()`."""
466        newlist = []
467        for a in self.objects:
468            newlist.append(a.clone())
469        return Assembly(newlist)
470    
471    def copy(self):
472        """Return a copy of the object. Alias of `clone()`."""
473        return self.clone()
474
475    def write(self, filename="assembly.npy"):
476        """
477        Write the object to file in `numpy` format.
478        """
479        objs = []
480        for ob in self.unpack():
481            d = vedo.file_io._to_numpy(ob)
482            objs.append(d)
483        np.save(filename, objs)
484        return self

Group many objects and treat them as a single new object.

Assembly(*meshs)
196    def __init__(self, *meshs):
197        """
198        Group many objects and treat them as a single new object,
199        keeping track of internal transformations.
200
201        Examples:
202            - [gyroscope1.py](https://github.com/marcomusy/vedo/tree/master/examples/simulations/gyroscope1.py)
203
204            ![](https://vedo.embl.es/images/simulations/39766016-85c1c1d6-52e3-11e8-8575-d167b7ce5217.gif)
205        """
206        super().__init__()
207
208        # Init by filename
209        if len(meshs) == 1 and isinstance(meshs[0], str):
210            filename = vedo.file_io.download(meshs[0], verbose=False)
211            data = np.load(filename, allow_pickle=True)
212            meshs = [vedo.file_io._from_numpy(dd) for dd in data]
213
214        if len(meshs) == 1:
215            meshs = meshs[0]
216        else:
217            meshs = vedo.utils.flatten(meshs)
218
219        self.actor = self
220        self.actor.retrieve_object = weak_ref_to(self)
221
222        self.name = "Assembly"
223        self.filename = ""
224        self.rendered_at = set()
225        self.scalarbar = None
226        self.info = {}
227        self.time = 0
228
229        self.transform = LinearTransform()
230
231        self.objects = [m for m in meshs if m]
232        self.actors  = [m.actor for m in self.objects]
233
234        scalarbars = []
235        for a in self.actors:
236            if isinstance(a, vtk.get_class("Prop3D")): # and a.GetNumberOfPoints():
237                self.AddPart(a)
238            if hasattr(a, "scalarbar") and a.scalarbar is not None:
239                scalarbars.append(a.scalarbar)
240
241        if len(scalarbars) > 1:
242            self.scalarbar = Group(scalarbars)
243        elif len(scalarbars) == 1:
244            self.scalarbar = scalarbars[0]
245
246        self.pipeline = vedo.utils.OperationNode(
247            "Assembly",
248            parents=self.objects,
249            comment=f"#meshes {len(self.objects)}",
250            c="#f08080",
251        )
252        ##########################################

Group many objects and treat them as a single new object, keeping track of internal transformations.

Examples:

def unpack(self, i=None):
413    def unpack(self, i=None):
414        """Unpack the list of objects from a `Assembly`.
415
416        If `i` is given, get `i-th` object from a `Assembly`.
417        Input can be a string, in this case returns the first object
418        whose name contains the given string.
419
420        Examples:
421            - [custom_axes4.py](https://github.com/marcomusy/vedo/tree/master/examples/pyplot/custom_axes4.py)
422        """
423        if i is None:
424            return self.objects
425        elif isinstance(i, int):
426            return self.objects[i]
427        elif isinstance(i, str):
428            for m in self.objects:
429                if i in m.name:
430                    return m

Unpack the list of objects from a Assembly.

If i is given, get i-th object from a Assembly. Input can be a string, in this case returns the first object whose name contains the given string.

Examples:
def recursive_unpack(self):
432    def recursive_unpack(self):
433        """Flatten out an Assembly."""
434
435        def _genflatten(lst):
436            if not lst:
437                return []
438            ##
439            if isinstance(lst[0], Assembly):
440                lst = lst[0].unpack()
441            ##
442            for elem in lst:
443                if isinstance(elem, Assembly):
444                    apos = elem.GetPosition()
445                    asum = np.sum(apos)
446                    for x in elem.unpack():
447                        if asum:
448                            yield x.clone().shift(apos)
449                        else:
450                            yield x
451                else:
452                    yield elem
453
454        return list(_genflatten([self]))

Flatten out an Assembly.

def pickable(self, value=True):
456    def pickable(self, value=True):
457        """Set/get the pickability property of an assembly and its elements"""
458        self.SetPickable(value)
459        # set property to each element
460        for elem in self.recursive_unpack():
461            elem.pickable(value)
462        return self

Set/get the pickability property of an assembly and its elements

def clone(self):
464    def clone(self):
465        """Make a clone copy of the object. Same as `copy()`."""
466        newlist = []
467        for a in self.objects:
468            newlist.append(a.clone())
469        return Assembly(newlist)

Make a clone copy of the object. Same as copy().

def copy(self):
471    def copy(self):
472        """Return a copy of the object. Alias of `clone()`."""
473        return self.clone()

Return a copy of the object. Alias of clone().

def write(self, filename='assembly.npy'):
475    def write(self, filename="assembly.npy"):
476        """
477        Write the object to file in `numpy` format.
478        """
479        objs = []
480        for ob in self.unpack():
481            d = vedo.file_io._to_numpy(ob)
482            objs.append(d)
483        np.save(filename, objs)
484        return self

Write the object to file in numpy format.

def procrustes_alignment(sources, rigid=False):
25def procrustes_alignment(sources, rigid=False):
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 = vtk.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 = vtk.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

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.
Examples: