vedo.transformations

Submodule to work with linear and non-linear transformations

  1#!/usr/bin/env python3
  2# -*- coding: utf-8 -*-
  3import numpy as np
  4
  5import vedo.vtkclasses as vtk
  6
  7__docformat__ = "google"
  8
  9__doc__ = """
 10Submodule to work with linear and non-linear transformations<br>
 11
 12![](https://vedo.embl.es/images/feats/transforms.png)
 13"""
 14
 15__all__ = [
 16    "LinearTransform",
 17    "NonLinearTransform",
 18    "spher2cart",
 19    "cart2spher",
 20    "cart2cyl",
 21    "cyl2cart",
 22    "cyl2spher",
 23    "spher2cyl",
 24    "cart2pol",
 25    "pol2cart",
 26]
 27
 28###################################################
 29def _is_sequence(arg):
 30    if hasattr(arg, "strip"):
 31        return False
 32    if hasattr(arg, "__getslice__"):
 33        return True
 34    if hasattr(arg, "__iter__"):
 35        return True
 36    return False
 37
 38
 39###################################################
 40class LinearTransform:
 41    """Work with linear transformations."""
 42
 43    def __init__(self, T=None):
 44        """
 45        Define a linear transformation.
 46        Can be saved to file and reloaded.
 47        
 48        Arguments:
 49            T : (vtkTransform, numpy array)
 50                input transformation. Defaults to unit.
 51        
 52        Example:
 53            ```python
 54            from vedo import *
 55            settings.use_parallel_projection = True
 56
 57            LT = LinearTransform()
 58            LT.translate([3,0,1]).rotate_z(45)
 59            LT.comment = "shifting by (3,0,1) and rotating by 45 deg"
 60            print(LT)
 61
 62            sph = Sphere(r=0.2)
 63            sph.apply_transform(LT) # same as: LT.move(s1)
 64            print(sph.transform)
 65
 66            show(Point([0,0,0]), sph, str(LT.matrix), axes=1).close()
 67            ```
 68        """
 69        self.name = "LinearTransform"
 70        self.filename = ""
 71        self.comment = ""
 72
 73        if T is None:
 74            T = vtk.vtkTransform()
 75
 76        elif isinstance(T, vtk.vtkMatrix4x4):
 77            S = vtk.vtkTransform()
 78            S.SetMatrix(T)
 79            T = S
 80
 81        elif isinstance(T, vtk.vtkLandmarkTransform):
 82            S = vtk.vtkTransform()
 83            S.SetMatrix(T.GetMatrix())
 84            T = S
 85
 86        elif _is_sequence(T):
 87            S = vtk.vtkTransform()
 88            M = vtk.vtkMatrix4x4()
 89            n = len(T)
 90            for i in range(n):
 91                for j in range(n):
 92                    M.SetElement(i, j, T[i][j])
 93            S.SetMatrix(M)
 94            T = S
 95
 96        elif isinstance(T, vtk.vtkLinearTransform):
 97            S = vtk.vtkTransform()
 98            S.DeepCopy(T)
 99            T = S
100
101        elif isinstance(T, LinearTransform):
102            S = vtk.vtkTransform()
103            S.DeepCopy(T.T)
104            T = S
105        
106        elif isinstance(T, str):
107            import json
108            self.filename = str(T)
109            try:
110                with open(self.filename, "r") as read_file:
111                    D = json.load(read_file)
112                self.name = D["name"]
113                self.comment = D["comment"]
114                matrix = np.array(D["matrix"])
115            except json.decoder.JSONDecodeError:
116                ### assuming legacy vedo format E.g.:
117                #aligned by manual_align.py
118                # 0.8026854838223 -0.0789823873914 -0.508476844097  38.17377632072
119                # 0.0679734082661  0.9501827489452 -0.040289803376 -69.53864247951
120                # 0.5100652300642 -0.0023313569781  0.805555043665 -81.20317788519
121                # 0.0 0.0 0.0 1.0
122                with open(self.filename, "r", encoding="UTF-8") as read_file:
123                    lines = read_file.readlines()
124                    i = 0
125                    matrix = np.eye(4)
126                    for l in lines:
127                        if l.startswith("#"):
128                            self.comment = l.replace("#", "").strip()
129                            continue
130                        vals = l.split(" ")
131                        for j in range(len(vals)):
132                            v = vals[j].replace("\n", "")
133                            if v != "":
134                                matrix[i, j] = float(v)
135                        i += 1
136            T = vtk.vtkTransform()
137            m = vtk.vtkMatrix4x4()
138            for i in range(4):
139                for j in range(4):
140                    m.SetElement(i, j, matrix[i][j])
141            T.SetMatrix(m)
142
143        self.T = T
144        self.T.PostMultiply()
145        self.inverse_flag = False
146
147    def __str__(self):
148        module = self.__class__.__module__
149        name = self.__class__.__name__
150        s = f"\x1b[7m\x1b[1m{module}.{name} at ({hex(id(self))})".ljust(75) + "\x1b[0m"
151        s += "\nname".ljust(15) + ": "  + self.name
152        if self.filename:
153            s += "\nfilename".ljust(15) + ": "  + self.filename
154        if self.comment:
155            s += "\ncomment".ljust(15) + f': \x1b[3m"{self.comment}"\x1b[0m'
156        s += f"\nconcatenations".ljust(15) + f": {self.n_concatenated_transforms}"
157        s += "\ninverse flag".ljust(15) + f": {bool(self.inverse_flag)}"
158        arr = np.array2string(self.matrix,
159            separator=', ', precision=6, suppress_small=True)
160        s += "\nmatrix 4x4".ljust(15) + f":\n{arr}"
161        return s
162
163    def __repr__(self):
164        return self.__str__()
165    
166    def print(self):
167        """Print transformation."""
168        print(self.__str__())
169        return self
170
171    def move(self, obj):
172        """
173        Apply transformation to object or single point.
174
175        Note:
176            When applying a transformation to a mesh, the mesh is modified in place.
177            If you want to keep the original mesh unchanged, use `clone()` method.
178        
179        Example:
180            ```python
181            from vedo import *
182            settings.use_parallel_projection = True
183
184            LT = LinearTransform()
185            LT.translate([3,0,1]).rotate_z(45)
186            print(LT)
187
188            s = Sphere(r=0.2)
189            LT.move(s)
190            # same as:
191            # s.apply_transform(LT)
192
193            zero = Point([0,0,0])
194            show(s, zero, axes=1).close()
195            ```
196        """
197        if _is_sequence(obj):
198            n = len(obj)
199            if n == 2:
200                obj = [obj[0], obj[1], 0]
201            return np.array(self.T.TransformFloatPoint(obj))
202
203        obj.apply_transform(self)
204        return obj
205
206    def reset(self):
207        """Reset transformation."""
208        self.T.Identity()
209        return self
210
211    def pop(self):
212        """Delete the transformation on the top of the stack 
213        and sets the top to the next transformation on the stack."""
214        self.T.Pop()
215        return self
216
217    def is_identity(self):
218        """Check if identity."""
219        m = self.T.GetMatrix()
220        M = [[m.GetElement(i, j) for j in range(4)] for i in range(4)]
221        if np.allclose(M - np.eye(4), 0):
222            return True
223        return False
224
225    def invert(self):
226        """Invert transformation."""
227        self.T.Inverse()
228        self.inverse_flag = bool(self.T.GetInverseFlag())
229        return self
230
231    def compute_inverse(self):
232        """Compute inverse."""
233        t = self.clone()
234        t.invert()
235        return t
236
237    def copy(self):
238        """Return a copy of the transformation. Alias of `clone()`."""
239        return self.clone()
240
241    def clone(self):
242        """Clone transformation to make an exact copy."""
243        return LinearTransform(self.T)
244
245    def concatenate(self, T, pre_multiply=False):
246        """
247        Post-multiply (by default) 2 transfomations.
248        T can also be a 4x4 matrix or 3x3 matrix.
249        
250        Example:
251            ```python
252            from vedo import LinearTransform
253
254            A = LinearTransform()
255            A.rotate_x(45)
256            A.translate([7,8,9])
257            A.translate([10,10,10])
258            A.name = "My transformation A"
259            print(A)
260
261            B = A.compute_inverse()
262            B.shift([1,2,3])
263            B.name = "My transformation B (shifted inverse of A)"
264            print(B)
265
266            # A is applied first, then B
267            # print("A.concatenate(B)", A.concatenate(B))
268
269            # B is applied first, then A
270            print(B*A)
271            ```
272        """
273        if _is_sequence(T):
274            S = vtk.vtkTransform()
275            M = vtk.vtkMatrix4x4()
276            n = len(T)
277            for i in range(n):
278                for j in range(n):
279                    M.SetElement(i, j, T[i][j])
280            S.SetMatrix(M)
281            T = S
282
283        if pre_multiply:
284            self.T.PreMultiply()
285        try:
286            self.T.Concatenate(T)
287        except:
288            self.T.Concatenate(T.T)
289        self.T.PostMultiply()
290        return self
291    
292    def __mul__(self, A):
293        """Pre-multiply 2 transfomations."""
294        return self.concatenate(A, pre_multiply=True)
295
296    def get_concatenated_transform(self, i):
297        """Get intermediate matrix by concatenation index."""
298        return LinearTransform(self.T.GetConcatenatedTransform(i))
299
300    @property
301    def n_concatenated_transforms(self):
302        """Get number of concatenated transforms."""
303        return self.T.GetNumberOfConcatenatedTransforms()
304
305    def translate(self, p):
306        """Translate, same as `shift`."""
307        if len(p) == 2:
308            p = [p[0], p[1], 0]
309        self.T.Translate(p)
310        return self
311
312    def shift(self, p):
313        """Shift, same as `translate`."""
314        return self.translate(p)
315
316    def scale(self, s, origin=True):
317        """Scale."""
318        if not _is_sequence(s):
319            s = [s, s, s]
320
321        if origin is True:
322            p = np.array(self.T.GetPosition())
323            if np.linalg.norm(p) > 0:
324                self.T.Translate(-p)
325                self.T.Scale(*s)
326                self.T.Translate(p)
327            else:
328                self.T.Scale(*s)
329
330        elif _is_sequence(origin):
331            origin = np.asarray(origin)
332            self.T.Translate(-origin)
333            self.T.Scale(*s)
334            self.T.Translate(origin)
335
336        else:
337            self.T.Scale(*s)
338        return self
339
340    def rotate(self, angle, axis=(1, 0, 0), point=(0, 0, 0), rad=False):
341        """
342        Rotate around an arbitrary `axis` passing through `point`.
343
344        Example:
345            ```python
346            from vedo import *
347            c1 = Cube()
348            c2 = c1.clone().c('violet').alpha(0.5) # copy of c1
349            v = vector(0.2, 1, 0)
350            p = vector(1.0, 0, 0)  # axis passes through this point
351            c2.rotate(90, axis=v, point=p)
352            l = Line(p-v, p+v).c('red5').lw(3)
353            show(c1, l, c2, axes=1).close()
354            ```
355            ![](https://vedo.embl.es/images/feats/rotate_axis.png)
356        """
357        if not angle:
358            return self
359        if rad:
360            anglerad = angle
361        else:
362            anglerad = np.deg2rad(angle)
363        axis = np.asarray(axis) / np.linalg.norm(axis)
364        a = np.cos(anglerad / 2)
365        b, c, d = -axis * np.sin(anglerad / 2)
366        aa, bb, cc, dd = a * a, b * b, c * c, d * d
367        bc, ad, ac, ab, bd, cd = b * c, a * d, a * c, a * b, b * d, c * d
368        R = np.array(
369            [
370                [aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)],
371                [2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)],
372                [2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc],
373            ]
374        )
375        rv = np.dot(R, self.T.GetPosition() - np.asarray(point)) + point
376
377        if rad:
378            angle *= 180.0 / np.pi
379        # this vtk method only rotates in the origin of the object:
380        self.T.RotateWXYZ(angle, axis[0], axis[1], axis[2])
381        self.T.Translate(rv - np.array(self.T.GetPosition()))
382        return self
383
384    def _rotatexyz(self, axe, angle, rad, around):
385        if not angle:
386            return self
387        if rad:
388            angle *= 180 / np.pi
389
390        rot = dict(x=self.T.RotateX, y=self.T.RotateY, z=self.T.RotateZ)
391
392        if around is None:
393            # rotate around its origin
394            rot[axe](angle)
395        else:
396            # displacement needed to bring it back to the origin
397            self.T.Translate(-np.asarray(around))
398            rot[axe](angle)
399            self.T.Translate(around)
400        return self
401
402    def rotate_x(self, angle, rad=False, around=None):
403        """
404        Rotate around x-axis. If angle is in radians set `rad=True`.
405
406        Use `around` to define a pivoting point.
407        """
408        return self._rotatexyz("x", angle, rad, around)
409
410    def rotate_y(self, angle, rad=False, around=None):
411        """
412        Rotate around y-axis. If angle is in radians set `rad=True`.
413
414        Use `around` to define a pivoting point.
415        """
416        return self._rotatexyz("y", angle, rad, around)
417
418    def rotate_z(self, angle, rad=False, around=None):
419        """
420        Rotate around z-axis. If angle is in radians set `rad=True`.
421
422        Use `around` to define a pivoting point.
423        """
424        return self._rotatexyz("z", angle, rad, around)
425
426    def set_position(self, p):
427        """Set position."""
428        if len(p) == 2:
429            p = np.array([p[0], p[1], 0])
430        q = np.array(self.T.GetPosition())
431        self.T.Translate(p - q)
432        return self
433
434    # def set_scale(self, s):
435    #     """Set absolute scale."""
436    #     if not _is_sequence(s):
437    #         s = [s, s, s]
438    #     s0, s1, s2 = 1, 1, 1
439    #     b = self.T.GetScale()
440    #     print(b)
441    #     if b[0]:
442    #         s0 = s[0] / b[0]
443    #     if b[1]:
444    #         s1 = s[1] / b[1]
445    #     if b[2]:
446    #         s2 = s[2] / b[2]
447    #     self.T.Scale(s0, s1, s2)
448    #     print()
449    #     return self
450
451    def get_scale(self):
452        """Get current scale."""
453        return np.array(self.T.GetScale())
454
455    @property
456    def orientation(self):
457        """Compute orientation."""
458        return np.array(self.T.GetOrientation())
459
460    @property
461    def position(self):
462        """Compute position."""
463        return np.array(self.T.GetPosition())
464
465    @property
466    def matrix(self):
467        """Get the 4x4 trasformation matrix."""
468        m = self.T.GetMatrix()
469        M = [[m.GetElement(i, j) for j in range(4)] for i in range(4)]
470        return np.array(M)
471
472    @matrix.setter
473    def matrix(self, M):
474        """Set trasformation by assigning a 4x4 or 3x3 numpy matrix."""
475        m = vtk.vtkMatrix4x4()
476        n = len(M)
477        for i in range(n):
478            for j in range(n):
479                m.SetElement(i, j, M[i][j])
480        self.T.SetMatrix(m)
481
482    @property
483    def matrix3x3(self):
484        """Get the 3x3 trasformation matrix."""
485        m = self.T.GetMatrix()
486        M = [[m.GetElement(i, j) for j in range(3)] for i in range(3)]
487        return np.array(M)
488
489    def write(self, filename="transform.mat"):
490        """Save transformation to ASCII file."""
491        import json
492        m = self.T.GetMatrix()
493        M = [[m.GetElement(i, j) for j in range(4)] for i in range(4)]
494        arr = np.array(M)
495        dictionary = {
496            "name": self.name,
497            "comment": self.comment,
498            "matrix": arr.astype(float).tolist(),
499            "n_concatenated_transforms": self.n_concatenated_transforms,
500        }
501        with open(filename, "w") as outfile:
502            json.dump(dictionary, outfile, sort_keys=True, indent=2)
503
504    def reorient(
505        self, initaxis, newaxis, around=(0, 0, 0), rotation=0, rad=False, xyplane=True
506    ):
507        """
508        Set/Get object orientation.
509
510        Arguments:
511            rotation : (float)
512                rotate object around newaxis.
513            concatenate : (bool)
514                concatenate the orientation operation with the previous existing transform (if any)
515            rad : (bool)
516                set to True if angle is expressed in radians.
517            xyplane : (bool)
518                make an extra rotation to keep the object aligned to the xy-plane
519        """
520        newaxis  = np.asarray(newaxis) / np.linalg.norm(newaxis)
521        initaxis = np.asarray(initaxis) / np.linalg.norm(initaxis)
522
523        if not np.any(initaxis - newaxis):
524            return self
525
526        if not np.any(initaxis + newaxis):
527            print("Warning: in reorient() initaxis and newaxis are parallel")
528            newaxis += np.array([0.0000001, 0.0000002, 0])
529            angleth = np.pi
530        else:
531            angleth = np.arccos(np.dot(initaxis, newaxis))
532        crossvec = np.cross(initaxis, newaxis)
533
534        p = np.asarray(around)
535        self.T.Translate(-p)
536        if rotation:
537            if rad:
538                rotation = np.rad2deg(rotation)
539            self.T.RotateWXYZ(rotation, initaxis)
540
541        self.T.RotateWXYZ(np.rad2deg(angleth), crossvec)
542
543        if xyplane:
544            self.T.RotateWXYZ(-self.orientation[0] * 1.4142, newaxis)
545
546        self.T.Translate(p)
547        return self
548
549
550###################################################
551class NonLinearTransform:
552    """Work with non-linear transformations."""
553    
554    def __init__(self, T=None, **kwargs):
555        """
556        Define a non-linear transformation.
557        Can be saved to file and reloaded.
558
559        Arguments:
560            T : (vtkThinPlateSplineTransform, str, dict)
561                vtk transformation.
562                If T is a string, it is assumed to be a filename.
563                If T is a dictionary, it is assumed to be a set of keyword arguments.
564                Defaults to None.
565            **kwargs : (dict)
566                keyword arguments to define the transformation.
567                The following keywords are accepted:
568                - name : (str) name of the transformation
569                - comment : (str) comment
570                - source_points : (list) source points
571                - target_points : (list) target points
572                - mode : (str) either '2d' or '3d'
573                - sigma : (float) sigma parameter
574        
575        Example:
576            ```python
577            from vedo import *
578            settings.use_parallel_projection = True
579
580            NLT = NonLinearTransform()
581            NLT.source_points = [[-2,0,0], [1,2,1], [2,-2,2]]
582            NLT.target_points = NLT.source_points + np.random.randn(3,3)*0.5
583            NLT.mode = '3d'
584            print(NLT)
585
586            s1 = Sphere()
587            NLT.move(s1)
588            # same as:
589            # s1.apply_transform(NLT)
590
591            arrs = Arrows(NLT.source_points, NLT.target_points)
592            show(s1, arrs, Sphere().alpha(0.1), axes=1).close()
593            ```
594        """
595
596        self.name = "NonLinearTransform"
597        self.filename = ""
598        self.comment = ""
599
600        if T is None and len(kwargs) == 0:
601            T = vtk.vtkThinPlateSplineTransform()
602
603        elif isinstance(T, vtk.vtkThinPlateSplineTransform):
604            S = vtk.vtkThinPlateSplineTransform()
605            S.DeepCopy(T)
606            T = S
607
608        elif isinstance(T, NonLinearTransform):
609            S = vtk.vtkThinPlateSplineTransform()
610            S.DeepCopy(T.T)
611            T = S
612
613        elif isinstance(T, str):
614            import json
615            filename = str(T)
616            self.filename = filename
617            with open(filename, "r") as read_file:
618                D = json.load(read_file)
619            self.name = D["name"]
620            self.comment = D["comment"]
621            source = D["source_points"]
622            target = D["target_points"]
623            mode = D["mode"]
624            sigma = D["sigma"]
625
626            T = vtk.vtkThinPlateSplineTransform()
627            vptss = vtk.vtkPoints()
628            for p in source:
629                if len(p) == 2:
630                    p = [p[0], p[1], 0.0]
631                vptss.InsertNextPoint(p)
632            T.SetSourceLandmarks(vptss)
633            vptst = vtk.vtkPoints()
634            for p in target:
635                if len(p) == 2:
636                    p = [p[0], p[1], 0.0]
637                vptst.InsertNextPoint(p)
638            T.SetTargetLandmarks(vptst)
639            T.SetSigma(sigma)
640            if mode == "2d":
641                T.SetBasisToR2LogR()
642            elif mode == "3d":
643                T.SetBasisToR()
644            else:
645                print(f'In {filename} mode can be either "2d" or "3d"')
646
647        elif len(kwargs) > 0:
648            T = kwargs.copy()
649            self.name = T.pop("name", "NonLinearTransform")
650            self.comment = T.pop("comment", "")
651            source = T.pop("source_points", [])
652            target = T.pop("target_points", [])
653            mode = T.pop("mode", "3d")
654            sigma = T.pop("sigma", 1.0)
655            if len(T) > 0:
656                print("Warning: NonLinearTransform got unexpected keyword arguments:")
657                print(T)
658
659            T = vtk.vtkThinPlateSplineTransform()
660            vptss = vtk.vtkPoints()
661            for p in source:
662                if len(p) == 2:
663                    p = [p[0], p[1], 0.0]
664                vptss.InsertNextPoint(p)
665            T.SetSourceLandmarks(vptss)
666            vptst = vtk.vtkPoints()
667            for p in target:
668                if len(p) == 2:
669                    p = [p[0], p[1], 0.0]
670                vptst.InsertNextPoint(p)
671            T.SetTargetLandmarks(vptst)
672            T.SetSigma(sigma)
673            if mode == "2d":
674                T.SetBasisToR2LogR()
675            elif mode == "3d":
676                T.SetBasisToR()
677            else:
678                print(f'In {filename} mode can be either "2d" or "3d"')
679
680        self.T = T
681        self.inverse_flag = False
682
683    def __str__(self):
684        module = self.__class__.__module__
685        name = self.__class__.__name__
686        s = f"\x1b[7m\x1b[1m{module}.{name} at ({hex(id(self))})".ljust(75) + "\x1b[0m\n"
687        s += "name".ljust(9) + ": "  + self.name + "\n"
688        if self.filename:
689            s += "filename".ljust(9) + ": "  + self.filename + "\n"
690        if self.comment:
691            s += "comment".ljust(9) + f': \x1b[3m"{self.comment}"\x1b[0m\n'
692        s += f"mode".ljust(9)  + f": {self.mode}\n"
693        s += f"sigma".ljust(9) + f": {self.sigma}\n"
694        p = self.source_points
695        q = self.target_points
696        s += f"sources".ljust(9) + f": {p.size}, bounds {np.min(p, axis=0)}, {np.max(p, axis=0)}\n"
697        s += f"targets".ljust(9) + f": {q.size}, bounds {np.min(q, axis=0)}, {np.max(q, axis=0)}"
698        return s
699    
700    def __repr__(self):
701        return self.__str__()
702
703    def update(self):
704        """Update transformation."""
705        self.T.Update()
706        return self
707
708    @property
709    def position(self):
710        """
711        Trying to get the position of a `NonLinearTransform` always returns [0,0,0].
712        """
713        return np.array([0.0, 0.0, 0.0], dtype=np.float32)
714    
715    @position.setter
716    def position(self, p):
717        """
718        Trying to set position of a `NonLinearTransform` 
719        has no effect and prints a warning.
720
721        Use clone() method to create a copy of the object,
722        or reset it with 'object.transform = vedo.LinearTransform()'
723        """
724        print("Warning: NonLinearTransform has no position.")
725        print("  Use clone() method to create a copy of the object,")
726        print("  or reset it with 'object.transform = vedo.LinearTransform()'")
727
728    @property
729    def source_points(self):
730        """Get the source points."""
731        pts = self.T.GetSourceLandmarks()
732        vpts = []
733        if pts:
734            for i in range(pts.GetNumberOfPoints()):
735                vpts.append(pts.GetPoint(i))
736        return np.array(vpts, dtype=np.float32)
737    
738    @property
739    def target_points(self):
740        """Get the target points."""
741        pts = self.T.GetTargetLandmarks()
742        vpts = []
743        for i in range(pts.GetNumberOfPoints()):
744            vpts.append(pts.GetPoint(i))
745        return np.array(vpts, dtype=np.float32)
746 
747    @source_points.setter
748    def source_points(self, pts):
749        """Set source points."""
750        if _is_sequence(pts):
751            pass
752        else:
753            pts = pts.vertices
754        vpts = vtk.vtkPoints()
755        for p in pts:
756            if len(p) == 2:
757                p = [p[0], p[1], 0.0]
758            vpts.InsertNextPoint(p)
759        self.T.SetSourceLandmarks(vpts)
760    
761    @target_points.setter
762    def target_points(self, pts):
763        """Set target points."""
764        if _is_sequence(pts):
765            pass
766        else:
767            pts = pts.vertices
768        vpts = vtk.vtkPoints()
769        for p in pts:
770            if len(p) == 2:
771                p = [p[0], p[1], 0.0]
772            vpts.InsertNextPoint(p)
773        self.T.SetTargetLandmarks(vpts)
774       
775    @property
776    def sigma(self) -> float:
777        """Set sigma."""
778        return self.T.GetSigma()
779
780    @sigma.setter
781    def sigma(self, s):
782        """Get sigma."""
783        self.T.SetSigma(s)
784
785    @property
786    def mode(self) -> str:
787        """Get mode."""
788        m = self.T.GetBasis()
789        # print("T.GetBasis()", m, self.T.GetBasisAsString())
790        if m == 2:
791            return "2d"
792        elif m == 1:
793            return "3d"
794        else:
795            print("Warning: NonLinearTransform has no valid mode.")
796
797    @mode.setter
798    def mode(self, m):
799        """Set mode."""
800        if m=='3d':
801            self.T.SetBasisToR()
802        elif m=='2d':
803            self.T.SetBasisToR2LogR()
804        else:
805            print('In NonLinearTransform mode can be either "2d" or "3d"')
806
807    def clone(self):
808        """Clone transformation to make an exact copy."""
809        return NonLinearTransform(self.T)
810        
811    def write(self, filename):
812        """Save transformation to ASCII file."""
813        import json
814        dictionary = {
815            "name": self.name,
816            "comment": self.comment,
817            "mode": self.mode,
818            "sigma": self.sigma,
819            "source_points": self.source_points.astype(float).tolist(),
820            "target_points": self.target_points.astype(float).tolist(),
821        }
822        with open(filename, "w") as outfile:
823            json.dump(dictionary, outfile, sort_keys=True, indent=2)
824        
825    def invert(self):
826        """Invert transformation."""
827        self.T.Inverse()
828        self.inverse_flag = bool(self.T.GetInverseFlag())
829        return self
830    
831    def compute_inverse(self):
832        """Compute inverse."""
833        t = self.clone()
834        t.invert()
835        return t   
836    
837    def move(self, obj):
838        """
839        Apply transformation to object or single point.
840        
841        Note:
842            When applying a transformation to a mesh, the mesh is modified in place.
843            If you want to keep the original mesh unchanged, use `clone()` method.
844        
845        Example:
846            ```python
847            from vedo import *
848            np.random.seed(0)
849            settings.use_parallel_projection = True
850
851            NLT = NonLinearTransform()
852            NLT.source_points = [[-2,0,0], [1,2,1], [2,-2,2]]
853            NLT.target_points = NLT.source_points + np.random.randn(3,3)*0.5
854            NLT.mode = '3d'
855            print(NLT)
856
857            s1 = Sphere()
858            NLT.move(s1)
859            # same as:
860            # s1.apply_transform(NLT)
861
862            arrs = Arrows(NLT.source_points, NLT.target_points)
863            show(s1, arrs, Sphere().alpha(0.1), axes=1).close()
864            ```
865        """
866        if _is_sequence(obj):
867            if len(obj) == 2:
868                obj = [obj[0], obj[1], 0]
869            return np.array(self.T.TransformFloatPoint(obj))
870
871        obj.apply_transform(self)
872        return obj
873
874########################################################################
875# 2d ######
876def cart2pol(x, y):
877    """2D Cartesian to Polar coordinates conversion."""
878    theta = np.arctan2(y, x)
879    rho = np.hypot(x, y)
880    return np.array([rho, theta])
881
882
883def pol2cart(rho, theta):
884    """2D Polar to Cartesian coordinates conversion."""
885    x = rho * np.cos(theta)
886    y = rho * np.sin(theta)
887    return np.array([x, y])
888
889
890########################################################################
891# 3d ######
892def cart2spher(x, y, z):
893    """3D Cartesian to Spherical coordinate conversion."""
894    hxy = np.hypot(x, y)
895    rho = np.hypot(hxy, z)
896    theta = np.arctan2(hxy, z)
897    phi = np.arctan2(y, x)
898    return np.array([rho, theta, phi])
899
900
901def spher2cart(rho, theta, phi):
902    """3D Spherical to Cartesian coordinate conversion."""
903    st = np.sin(theta)
904    sp = np.sin(phi)
905    ct = np.cos(theta)
906    cp = np.cos(phi)
907    rst = rho * st
908    x = rst * cp
909    y = rst * sp
910    z = rho * ct
911    return np.array([x, y, z])
912
913
914def cart2cyl(x, y, z):
915    """3D Cartesian to Cylindrical coordinate conversion."""
916    rho = np.sqrt(x * x + y * y)
917    theta = np.arctan2(y, x)
918    return np.array([rho, theta, z])
919
920
921def cyl2cart(rho, theta, z):
922    """3D Cylindrical to Cartesian coordinate conversion."""
923    x = rho * np.cos(theta)
924    y = rho * np.sin(theta)
925    return np.array([x, y, z])
926
927
928def cyl2spher(rho, theta, z):
929    """3D Cylindrical to Spherical coordinate conversion."""
930    rhos = np.sqrt(rho * rho + z * z)
931    phi = np.arctan2(rho, z)
932    return np.array([rhos, phi, theta])
933
934
935def spher2cyl(rho, theta, phi):
936    """3D Spherical to Cylindrical coordinate conversion."""
937    rhoc = rho * np.sin(theta)
938    z = rho * np.cos(theta)
939    return np.array([rhoc, phi, z])
class LinearTransform:
 41class LinearTransform:
 42    """Work with linear transformations."""
 43
 44    def __init__(self, T=None):
 45        """
 46        Define a linear transformation.
 47        Can be saved to file and reloaded.
 48        
 49        Arguments:
 50            T : (vtkTransform, numpy array)
 51                input transformation. Defaults to unit.
 52        
 53        Example:
 54            ```python
 55            from vedo import *
 56            settings.use_parallel_projection = True
 57
 58            LT = LinearTransform()
 59            LT.translate([3,0,1]).rotate_z(45)
 60            LT.comment = "shifting by (3,0,1) and rotating by 45 deg"
 61            print(LT)
 62
 63            sph = Sphere(r=0.2)
 64            sph.apply_transform(LT) # same as: LT.move(s1)
 65            print(sph.transform)
 66
 67            show(Point([0,0,0]), sph, str(LT.matrix), axes=1).close()
 68            ```
 69        """
 70        self.name = "LinearTransform"
 71        self.filename = ""
 72        self.comment = ""
 73
 74        if T is None:
 75            T = vtk.vtkTransform()
 76
 77        elif isinstance(T, vtk.vtkMatrix4x4):
 78            S = vtk.vtkTransform()
 79            S.SetMatrix(T)
 80            T = S
 81
 82        elif isinstance(T, vtk.vtkLandmarkTransform):
 83            S = vtk.vtkTransform()
 84            S.SetMatrix(T.GetMatrix())
 85            T = S
 86
 87        elif _is_sequence(T):
 88            S = vtk.vtkTransform()
 89            M = vtk.vtkMatrix4x4()
 90            n = len(T)
 91            for i in range(n):
 92                for j in range(n):
 93                    M.SetElement(i, j, T[i][j])
 94            S.SetMatrix(M)
 95            T = S
 96
 97        elif isinstance(T, vtk.vtkLinearTransform):
 98            S = vtk.vtkTransform()
 99            S.DeepCopy(T)
100            T = S
101
102        elif isinstance(T, LinearTransform):
103            S = vtk.vtkTransform()
104            S.DeepCopy(T.T)
105            T = S
106        
107        elif isinstance(T, str):
108            import json
109            self.filename = str(T)
110            try:
111                with open(self.filename, "r") as read_file:
112                    D = json.load(read_file)
113                self.name = D["name"]
114                self.comment = D["comment"]
115                matrix = np.array(D["matrix"])
116            except json.decoder.JSONDecodeError:
117                ### assuming legacy vedo format E.g.:
118                #aligned by manual_align.py
119                # 0.8026854838223 -0.0789823873914 -0.508476844097  38.17377632072
120                # 0.0679734082661  0.9501827489452 -0.040289803376 -69.53864247951
121                # 0.5100652300642 -0.0023313569781  0.805555043665 -81.20317788519
122                # 0.0 0.0 0.0 1.0
123                with open(self.filename, "r", encoding="UTF-8") as read_file:
124                    lines = read_file.readlines()
125                    i = 0
126                    matrix = np.eye(4)
127                    for l in lines:
128                        if l.startswith("#"):
129                            self.comment = l.replace("#", "").strip()
130                            continue
131                        vals = l.split(" ")
132                        for j in range(len(vals)):
133                            v = vals[j].replace("\n", "")
134                            if v != "":
135                                matrix[i, j] = float(v)
136                        i += 1
137            T = vtk.vtkTransform()
138            m = vtk.vtkMatrix4x4()
139            for i in range(4):
140                for j in range(4):
141                    m.SetElement(i, j, matrix[i][j])
142            T.SetMatrix(m)
143
144        self.T = T
145        self.T.PostMultiply()
146        self.inverse_flag = False
147
148    def __str__(self):
149        module = self.__class__.__module__
150        name = self.__class__.__name__
151        s = f"\x1b[7m\x1b[1m{module}.{name} at ({hex(id(self))})".ljust(75) + "\x1b[0m"
152        s += "\nname".ljust(15) + ": "  + self.name
153        if self.filename:
154            s += "\nfilename".ljust(15) + ": "  + self.filename
155        if self.comment:
156            s += "\ncomment".ljust(15) + f': \x1b[3m"{self.comment}"\x1b[0m'
157        s += f"\nconcatenations".ljust(15) + f": {self.n_concatenated_transforms}"
158        s += "\ninverse flag".ljust(15) + f": {bool(self.inverse_flag)}"
159        arr = np.array2string(self.matrix,
160            separator=', ', precision=6, suppress_small=True)
161        s += "\nmatrix 4x4".ljust(15) + f":\n{arr}"
162        return s
163
164    def __repr__(self):
165        return self.__str__()
166    
167    def print(self):
168        """Print transformation."""
169        print(self.__str__())
170        return self
171
172    def move(self, obj):
173        """
174        Apply transformation to object or single point.
175
176        Note:
177            When applying a transformation to a mesh, the mesh is modified in place.
178            If you want to keep the original mesh unchanged, use `clone()` method.
179        
180        Example:
181            ```python
182            from vedo import *
183            settings.use_parallel_projection = True
184
185            LT = LinearTransform()
186            LT.translate([3,0,1]).rotate_z(45)
187            print(LT)
188
189            s = Sphere(r=0.2)
190            LT.move(s)
191            # same as:
192            # s.apply_transform(LT)
193
194            zero = Point([0,0,0])
195            show(s, zero, axes=1).close()
196            ```
197        """
198        if _is_sequence(obj):
199            n = len(obj)
200            if n == 2:
201                obj = [obj[0], obj[1], 0]
202            return np.array(self.T.TransformFloatPoint(obj))
203
204        obj.apply_transform(self)
205        return obj
206
207    def reset(self):
208        """Reset transformation."""
209        self.T.Identity()
210        return self
211
212    def pop(self):
213        """Delete the transformation on the top of the stack 
214        and sets the top to the next transformation on the stack."""
215        self.T.Pop()
216        return self
217
218    def is_identity(self):
219        """Check if identity."""
220        m = self.T.GetMatrix()
221        M = [[m.GetElement(i, j) for j in range(4)] for i in range(4)]
222        if np.allclose(M - np.eye(4), 0):
223            return True
224        return False
225
226    def invert(self):
227        """Invert transformation."""
228        self.T.Inverse()
229        self.inverse_flag = bool(self.T.GetInverseFlag())
230        return self
231
232    def compute_inverse(self):
233        """Compute inverse."""
234        t = self.clone()
235        t.invert()
236        return t
237
238    def copy(self):
239        """Return a copy of the transformation. Alias of `clone()`."""
240        return self.clone()
241
242    def clone(self):
243        """Clone transformation to make an exact copy."""
244        return LinearTransform(self.T)
245
246    def concatenate(self, T, pre_multiply=False):
247        """
248        Post-multiply (by default) 2 transfomations.
249        T can also be a 4x4 matrix or 3x3 matrix.
250        
251        Example:
252            ```python
253            from vedo import LinearTransform
254
255            A = LinearTransform()
256            A.rotate_x(45)
257            A.translate([7,8,9])
258            A.translate([10,10,10])
259            A.name = "My transformation A"
260            print(A)
261
262            B = A.compute_inverse()
263            B.shift([1,2,3])
264            B.name = "My transformation B (shifted inverse of A)"
265            print(B)
266
267            # A is applied first, then B
268            # print("A.concatenate(B)", A.concatenate(B))
269
270            # B is applied first, then A
271            print(B*A)
272            ```
273        """
274        if _is_sequence(T):
275            S = vtk.vtkTransform()
276            M = vtk.vtkMatrix4x4()
277            n = len(T)
278            for i in range(n):
279                for j in range(n):
280                    M.SetElement(i, j, T[i][j])
281            S.SetMatrix(M)
282            T = S
283
284        if pre_multiply:
285            self.T.PreMultiply()
286        try:
287            self.T.Concatenate(T)
288        except:
289            self.T.Concatenate(T.T)
290        self.T.PostMultiply()
291        return self
292    
293    def __mul__(self, A):
294        """Pre-multiply 2 transfomations."""
295        return self.concatenate(A, pre_multiply=True)
296
297    def get_concatenated_transform(self, i):
298        """Get intermediate matrix by concatenation index."""
299        return LinearTransform(self.T.GetConcatenatedTransform(i))
300
301    @property
302    def n_concatenated_transforms(self):
303        """Get number of concatenated transforms."""
304        return self.T.GetNumberOfConcatenatedTransforms()
305
306    def translate(self, p):
307        """Translate, same as `shift`."""
308        if len(p) == 2:
309            p = [p[0], p[1], 0]
310        self.T.Translate(p)
311        return self
312
313    def shift(self, p):
314        """Shift, same as `translate`."""
315        return self.translate(p)
316
317    def scale(self, s, origin=True):
318        """Scale."""
319        if not _is_sequence(s):
320            s = [s, s, s]
321
322        if origin is True:
323            p = np.array(self.T.GetPosition())
324            if np.linalg.norm(p) > 0:
325                self.T.Translate(-p)
326                self.T.Scale(*s)
327                self.T.Translate(p)
328            else:
329                self.T.Scale(*s)
330
331        elif _is_sequence(origin):
332            origin = np.asarray(origin)
333            self.T.Translate(-origin)
334            self.T.Scale(*s)
335            self.T.Translate(origin)
336
337        else:
338            self.T.Scale(*s)
339        return self
340
341    def rotate(self, angle, axis=(1, 0, 0), point=(0, 0, 0), rad=False):
342        """
343        Rotate around an arbitrary `axis` passing through `point`.
344
345        Example:
346            ```python
347            from vedo import *
348            c1 = Cube()
349            c2 = c1.clone().c('violet').alpha(0.5) # copy of c1
350            v = vector(0.2, 1, 0)
351            p = vector(1.0, 0, 0)  # axis passes through this point
352            c2.rotate(90, axis=v, point=p)
353            l = Line(p-v, p+v).c('red5').lw(3)
354            show(c1, l, c2, axes=1).close()
355            ```
356            ![](https://vedo.embl.es/images/feats/rotate_axis.png)
357        """
358        if not angle:
359            return self
360        if rad:
361            anglerad = angle
362        else:
363            anglerad = np.deg2rad(angle)
364        axis = np.asarray(axis) / np.linalg.norm(axis)
365        a = np.cos(anglerad / 2)
366        b, c, d = -axis * np.sin(anglerad / 2)
367        aa, bb, cc, dd = a * a, b * b, c * c, d * d
368        bc, ad, ac, ab, bd, cd = b * c, a * d, a * c, a * b, b * d, c * d
369        R = np.array(
370            [
371                [aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)],
372                [2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)],
373                [2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc],
374            ]
375        )
376        rv = np.dot(R, self.T.GetPosition() - np.asarray(point)) + point
377
378        if rad:
379            angle *= 180.0 / np.pi
380        # this vtk method only rotates in the origin of the object:
381        self.T.RotateWXYZ(angle, axis[0], axis[1], axis[2])
382        self.T.Translate(rv - np.array(self.T.GetPosition()))
383        return self
384
385    def _rotatexyz(self, axe, angle, rad, around):
386        if not angle:
387            return self
388        if rad:
389            angle *= 180 / np.pi
390
391        rot = dict(x=self.T.RotateX, y=self.T.RotateY, z=self.T.RotateZ)
392
393        if around is None:
394            # rotate around its origin
395            rot[axe](angle)
396        else:
397            # displacement needed to bring it back to the origin
398            self.T.Translate(-np.asarray(around))
399            rot[axe](angle)
400            self.T.Translate(around)
401        return self
402
403    def rotate_x(self, angle, rad=False, around=None):
404        """
405        Rotate around x-axis. If angle is in radians set `rad=True`.
406
407        Use `around` to define a pivoting point.
408        """
409        return self._rotatexyz("x", angle, rad, around)
410
411    def rotate_y(self, angle, rad=False, around=None):
412        """
413        Rotate around y-axis. If angle is in radians set `rad=True`.
414
415        Use `around` to define a pivoting point.
416        """
417        return self._rotatexyz("y", angle, rad, around)
418
419    def rotate_z(self, angle, rad=False, around=None):
420        """
421        Rotate around z-axis. If angle is in radians set `rad=True`.
422
423        Use `around` to define a pivoting point.
424        """
425        return self._rotatexyz("z", angle, rad, around)
426
427    def set_position(self, p):
428        """Set position."""
429        if len(p) == 2:
430            p = np.array([p[0], p[1], 0])
431        q = np.array(self.T.GetPosition())
432        self.T.Translate(p - q)
433        return self
434
435    # def set_scale(self, s):
436    #     """Set absolute scale."""
437    #     if not _is_sequence(s):
438    #         s = [s, s, s]
439    #     s0, s1, s2 = 1, 1, 1
440    #     b = self.T.GetScale()
441    #     print(b)
442    #     if b[0]:
443    #         s0 = s[0] / b[0]
444    #     if b[1]:
445    #         s1 = s[1] / b[1]
446    #     if b[2]:
447    #         s2 = s[2] / b[2]
448    #     self.T.Scale(s0, s1, s2)
449    #     print()
450    #     return self
451
452    def get_scale(self):
453        """Get current scale."""
454        return np.array(self.T.GetScale())
455
456    @property
457    def orientation(self):
458        """Compute orientation."""
459        return np.array(self.T.GetOrientation())
460
461    @property
462    def position(self):
463        """Compute position."""
464        return np.array(self.T.GetPosition())
465
466    @property
467    def matrix(self):
468        """Get the 4x4 trasformation matrix."""
469        m = self.T.GetMatrix()
470        M = [[m.GetElement(i, j) for j in range(4)] for i in range(4)]
471        return np.array(M)
472
473    @matrix.setter
474    def matrix(self, M):
475        """Set trasformation by assigning a 4x4 or 3x3 numpy matrix."""
476        m = vtk.vtkMatrix4x4()
477        n = len(M)
478        for i in range(n):
479            for j in range(n):
480                m.SetElement(i, j, M[i][j])
481        self.T.SetMatrix(m)
482
483    @property
484    def matrix3x3(self):
485        """Get the 3x3 trasformation matrix."""
486        m = self.T.GetMatrix()
487        M = [[m.GetElement(i, j) for j in range(3)] for i in range(3)]
488        return np.array(M)
489
490    def write(self, filename="transform.mat"):
491        """Save transformation to ASCII file."""
492        import json
493        m = self.T.GetMatrix()
494        M = [[m.GetElement(i, j) for j in range(4)] for i in range(4)]
495        arr = np.array(M)
496        dictionary = {
497            "name": self.name,
498            "comment": self.comment,
499            "matrix": arr.astype(float).tolist(),
500            "n_concatenated_transforms": self.n_concatenated_transforms,
501        }
502        with open(filename, "w") as outfile:
503            json.dump(dictionary, outfile, sort_keys=True, indent=2)
504
505    def reorient(
506        self, initaxis, newaxis, around=(0, 0, 0), rotation=0, rad=False, xyplane=True
507    ):
508        """
509        Set/Get object orientation.
510
511        Arguments:
512            rotation : (float)
513                rotate object around newaxis.
514            concatenate : (bool)
515                concatenate the orientation operation with the previous existing transform (if any)
516            rad : (bool)
517                set to True if angle is expressed in radians.
518            xyplane : (bool)
519                make an extra rotation to keep the object aligned to the xy-plane
520        """
521        newaxis  = np.asarray(newaxis) / np.linalg.norm(newaxis)
522        initaxis = np.asarray(initaxis) / np.linalg.norm(initaxis)
523
524        if not np.any(initaxis - newaxis):
525            return self
526
527        if not np.any(initaxis + newaxis):
528            print("Warning: in reorient() initaxis and newaxis are parallel")
529            newaxis += np.array([0.0000001, 0.0000002, 0])
530            angleth = np.pi
531        else:
532            angleth = np.arccos(np.dot(initaxis, newaxis))
533        crossvec = np.cross(initaxis, newaxis)
534
535        p = np.asarray(around)
536        self.T.Translate(-p)
537        if rotation:
538            if rad:
539                rotation = np.rad2deg(rotation)
540            self.T.RotateWXYZ(rotation, initaxis)
541
542        self.T.RotateWXYZ(np.rad2deg(angleth), crossvec)
543
544        if xyplane:
545            self.T.RotateWXYZ(-self.orientation[0] * 1.4142, newaxis)
546
547        self.T.Translate(p)
548        return self

Work with linear transformations.

LinearTransform(T=None)
 44    def __init__(self, T=None):
 45        """
 46        Define a linear transformation.
 47        Can be saved to file and reloaded.
 48        
 49        Arguments:
 50            T : (vtkTransform, numpy array)
 51                input transformation. Defaults to unit.
 52        
 53        Example:
 54            ```python
 55            from vedo import *
 56            settings.use_parallel_projection = True
 57
 58            LT = LinearTransform()
 59            LT.translate([3,0,1]).rotate_z(45)
 60            LT.comment = "shifting by (3,0,1) and rotating by 45 deg"
 61            print(LT)
 62
 63            sph = Sphere(r=0.2)
 64            sph.apply_transform(LT) # same as: LT.move(s1)
 65            print(sph.transform)
 66
 67            show(Point([0,0,0]), sph, str(LT.matrix), axes=1).close()
 68            ```
 69        """
 70        self.name = "LinearTransform"
 71        self.filename = ""
 72        self.comment = ""
 73
 74        if T is None:
 75            T = vtk.vtkTransform()
 76
 77        elif isinstance(T, vtk.vtkMatrix4x4):
 78            S = vtk.vtkTransform()
 79            S.SetMatrix(T)
 80            T = S
 81
 82        elif isinstance(T, vtk.vtkLandmarkTransform):
 83            S = vtk.vtkTransform()
 84            S.SetMatrix(T.GetMatrix())
 85            T = S
 86
 87        elif _is_sequence(T):
 88            S = vtk.vtkTransform()
 89            M = vtk.vtkMatrix4x4()
 90            n = len(T)
 91            for i in range(n):
 92                for j in range(n):
 93                    M.SetElement(i, j, T[i][j])
 94            S.SetMatrix(M)
 95            T = S
 96
 97        elif isinstance(T, vtk.vtkLinearTransform):
 98            S = vtk.vtkTransform()
 99            S.DeepCopy(T)
100            T = S
101
102        elif isinstance(T, LinearTransform):
103            S = vtk.vtkTransform()
104            S.DeepCopy(T.T)
105            T = S
106        
107        elif isinstance(T, str):
108            import json
109            self.filename = str(T)
110            try:
111                with open(self.filename, "r") as read_file:
112                    D = json.load(read_file)
113                self.name = D["name"]
114                self.comment = D["comment"]
115                matrix = np.array(D["matrix"])
116            except json.decoder.JSONDecodeError:
117                ### assuming legacy vedo format E.g.:
118                #aligned by manual_align.py
119                # 0.8026854838223 -0.0789823873914 -0.508476844097  38.17377632072
120                # 0.0679734082661  0.9501827489452 -0.040289803376 -69.53864247951
121                # 0.5100652300642 -0.0023313569781  0.805555043665 -81.20317788519
122                # 0.0 0.0 0.0 1.0
123                with open(self.filename, "r", encoding="UTF-8") as read_file:
124                    lines = read_file.readlines()
125                    i = 0
126                    matrix = np.eye(4)
127                    for l in lines:
128                        if l.startswith("#"):
129                            self.comment = l.replace("#", "").strip()
130                            continue
131                        vals = l.split(" ")
132                        for j in range(len(vals)):
133                            v = vals[j].replace("\n", "")
134                            if v != "":
135                                matrix[i, j] = float(v)
136                        i += 1
137            T = vtk.vtkTransform()
138            m = vtk.vtkMatrix4x4()
139            for i in range(4):
140                for j in range(4):
141                    m.SetElement(i, j, matrix[i][j])
142            T.SetMatrix(m)
143
144        self.T = T
145        self.T.PostMultiply()
146        self.inverse_flag = False

Define a linear transformation. Can be saved to file and reloaded.

Arguments:
  • T : (vtkTransform, numpy array) input transformation. Defaults to unit.
Example:
from vedo import *
settings.use_parallel_projection = True

LT = LinearTransform()
LT.translate([3,0,1]).rotate_z(45)
LT.comment = "shifting by (3,0,1) and rotating by 45 deg"
print(LT)

sph = Sphere(r=0.2)
sph.apply_transform(LT) # same as: LT.move(s1)
print(sph.transform)

show(Point([0,0,0]), sph, str(LT.matrix), axes=1).close()
def print(self):
167    def print(self):
168        """Print transformation."""
169        print(self.__str__())
170        return self

Print transformation.

def move(self, obj):
172    def move(self, obj):
173        """
174        Apply transformation to object or single point.
175
176        Note:
177            When applying a transformation to a mesh, the mesh is modified in place.
178            If you want to keep the original mesh unchanged, use `clone()` method.
179        
180        Example:
181            ```python
182            from vedo import *
183            settings.use_parallel_projection = True
184
185            LT = LinearTransform()
186            LT.translate([3,0,1]).rotate_z(45)
187            print(LT)
188
189            s = Sphere(r=0.2)
190            LT.move(s)
191            # same as:
192            # s.apply_transform(LT)
193
194            zero = Point([0,0,0])
195            show(s, zero, axes=1).close()
196            ```
197        """
198        if _is_sequence(obj):
199            n = len(obj)
200            if n == 2:
201                obj = [obj[0], obj[1], 0]
202            return np.array(self.T.TransformFloatPoint(obj))
203
204        obj.apply_transform(self)
205        return obj

Apply transformation to object or single point.

Note:

When applying a transformation to a mesh, the mesh is modified in place. If you want to keep the original mesh unchanged, use clone() method.

Example:
from vedo import *
settings.use_parallel_projection = True

LT = LinearTransform()
LT.translate([3,0,1]).rotate_z(45)
print(LT)

s = Sphere(r=0.2)
LT.move(s)
# same as:
# s.apply_transform(LT)

zero = Point([0,0,0])
show(s, zero, axes=1).close()
def reset(self):
207    def reset(self):
208        """Reset transformation."""
209        self.T.Identity()
210        return self

Reset transformation.

def pop(self):
212    def pop(self):
213        """Delete the transformation on the top of the stack 
214        and sets the top to the next transformation on the stack."""
215        self.T.Pop()
216        return self

Delete the transformation on the top of the stack and sets the top to the next transformation on the stack.

def is_identity(self):
218    def is_identity(self):
219        """Check if identity."""
220        m = self.T.GetMatrix()
221        M = [[m.GetElement(i, j) for j in range(4)] for i in range(4)]
222        if np.allclose(M - np.eye(4), 0):
223            return True
224        return False

Check if identity.

def invert(self):
226    def invert(self):
227        """Invert transformation."""
228        self.T.Inverse()
229        self.inverse_flag = bool(self.T.GetInverseFlag())
230        return self

Invert transformation.

def compute_inverse(self):
232    def compute_inverse(self):
233        """Compute inverse."""
234        t = self.clone()
235        t.invert()
236        return t

Compute inverse.

def copy(self):
238    def copy(self):
239        """Return a copy of the transformation. Alias of `clone()`."""
240        return self.clone()

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

def clone(self):
242    def clone(self):
243        """Clone transformation to make an exact copy."""
244        return LinearTransform(self.T)

Clone transformation to make an exact copy.

def concatenate(self, T, pre_multiply=False):
246    def concatenate(self, T, pre_multiply=False):
247        """
248        Post-multiply (by default) 2 transfomations.
249        T can also be a 4x4 matrix or 3x3 matrix.
250        
251        Example:
252            ```python
253            from vedo import LinearTransform
254
255            A = LinearTransform()
256            A.rotate_x(45)
257            A.translate([7,8,9])
258            A.translate([10,10,10])
259            A.name = "My transformation A"
260            print(A)
261
262            B = A.compute_inverse()
263            B.shift([1,2,3])
264            B.name = "My transformation B (shifted inverse of A)"
265            print(B)
266
267            # A is applied first, then B
268            # print("A.concatenate(B)", A.concatenate(B))
269
270            # B is applied first, then A
271            print(B*A)
272            ```
273        """
274        if _is_sequence(T):
275            S = vtk.vtkTransform()
276            M = vtk.vtkMatrix4x4()
277            n = len(T)
278            for i in range(n):
279                for j in range(n):
280                    M.SetElement(i, j, T[i][j])
281            S.SetMatrix(M)
282            T = S
283
284        if pre_multiply:
285            self.T.PreMultiply()
286        try:
287            self.T.Concatenate(T)
288        except:
289            self.T.Concatenate(T.T)
290        self.T.PostMultiply()
291        return self

Post-multiply (by default) 2 transfomations. T can also be a 4x4 matrix or 3x3 matrix.

Example:
from vedo import LinearTransform

A = LinearTransform()
A.rotate_x(45)
A.translate([7,8,9])
A.translate([10,10,10])
A.name = "My transformation A"
print(A)

B = A.compute_inverse()
B.shift([1,2,3])
B.name = "My transformation B (shifted inverse of A)"
print(B)

# A is applied first, then B
# print("A.concatenate(B)", A.concatenate(B))

# B is applied first, then A
print(B*A)
def get_concatenated_transform(self, i):
297    def get_concatenated_transform(self, i):
298        """Get intermediate matrix by concatenation index."""
299        return LinearTransform(self.T.GetConcatenatedTransform(i))

Get intermediate matrix by concatenation index.

n_concatenated_transforms

Get number of concatenated transforms.

def translate(self, p):
306    def translate(self, p):
307        """Translate, same as `shift`."""
308        if len(p) == 2:
309            p = [p[0], p[1], 0]
310        self.T.Translate(p)
311        return self

Translate, same as shift.

def shift(self, p):
313    def shift(self, p):
314        """Shift, same as `translate`."""
315        return self.translate(p)

Shift, same as translate.

def scale(self, s, origin=True):
317    def scale(self, s, origin=True):
318        """Scale."""
319        if not _is_sequence(s):
320            s = [s, s, s]
321
322        if origin is True:
323            p = np.array(self.T.GetPosition())
324            if np.linalg.norm(p) > 0:
325                self.T.Translate(-p)
326                self.T.Scale(*s)
327                self.T.Translate(p)
328            else:
329                self.T.Scale(*s)
330
331        elif _is_sequence(origin):
332            origin = np.asarray(origin)
333            self.T.Translate(-origin)
334            self.T.Scale(*s)
335            self.T.Translate(origin)
336
337        else:
338            self.T.Scale(*s)
339        return self

Scale.

def rotate(self, angle, axis=(1, 0, 0), point=(0, 0, 0), rad=False):
341    def rotate(self, angle, axis=(1, 0, 0), point=(0, 0, 0), rad=False):
342        """
343        Rotate around an arbitrary `axis` passing through `point`.
344
345        Example:
346            ```python
347            from vedo import *
348            c1 = Cube()
349            c2 = c1.clone().c('violet').alpha(0.5) # copy of c1
350            v = vector(0.2, 1, 0)
351            p = vector(1.0, 0, 0)  # axis passes through this point
352            c2.rotate(90, axis=v, point=p)
353            l = Line(p-v, p+v).c('red5').lw(3)
354            show(c1, l, c2, axes=1).close()
355            ```
356            ![](https://vedo.embl.es/images/feats/rotate_axis.png)
357        """
358        if not angle:
359            return self
360        if rad:
361            anglerad = angle
362        else:
363            anglerad = np.deg2rad(angle)
364        axis = np.asarray(axis) / np.linalg.norm(axis)
365        a = np.cos(anglerad / 2)
366        b, c, d = -axis * np.sin(anglerad / 2)
367        aa, bb, cc, dd = a * a, b * b, c * c, d * d
368        bc, ad, ac, ab, bd, cd = b * c, a * d, a * c, a * b, b * d, c * d
369        R = np.array(
370            [
371                [aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)],
372                [2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)],
373                [2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc],
374            ]
375        )
376        rv = np.dot(R, self.T.GetPosition() - np.asarray(point)) + point
377
378        if rad:
379            angle *= 180.0 / np.pi
380        # this vtk method only rotates in the origin of the object:
381        self.T.RotateWXYZ(angle, axis[0], axis[1], axis[2])
382        self.T.Translate(rv - np.array(self.T.GetPosition()))
383        return self

Rotate around an arbitrary axis passing through point.

Example:
from vedo import *
c1 = Cube()
c2 = c1.clone().c('violet').alpha(0.5) # copy of c1
v = vector(0.2, 1, 0)
p = vector(1.0, 0, 0)  # axis passes through this point
c2.rotate(90, axis=v, point=p)
l = Line(p-v, p+v).c('red5').lw(3)
show(c1, l, c2, axes=1).close()

def rotate_x(self, angle, rad=False, around=None):
403    def rotate_x(self, angle, rad=False, around=None):
404        """
405        Rotate around x-axis. If angle is in radians set `rad=True`.
406
407        Use `around` to define a pivoting point.
408        """
409        return self._rotatexyz("x", angle, rad, around)

Rotate around x-axis. If angle is in radians set rad=True.

Use around to define a pivoting point.

def rotate_y(self, angle, rad=False, around=None):
411    def rotate_y(self, angle, rad=False, around=None):
412        """
413        Rotate around y-axis. If angle is in radians set `rad=True`.
414
415        Use `around` to define a pivoting point.
416        """
417        return self._rotatexyz("y", angle, rad, around)

Rotate around y-axis. If angle is in radians set rad=True.

Use around to define a pivoting point.

def rotate_z(self, angle, rad=False, around=None):
419    def rotate_z(self, angle, rad=False, around=None):
420        """
421        Rotate around z-axis. If angle is in radians set `rad=True`.
422
423        Use `around` to define a pivoting point.
424        """
425        return self._rotatexyz("z", angle, rad, around)

Rotate around z-axis. If angle is in radians set rad=True.

Use around to define a pivoting point.

def set_position(self, p):
427    def set_position(self, p):
428        """Set position."""
429        if len(p) == 2:
430            p = np.array([p[0], p[1], 0])
431        q = np.array(self.T.GetPosition())
432        self.T.Translate(p - q)
433        return self

Set position.

def get_scale(self):
452    def get_scale(self):
453        """Get current scale."""
454        return np.array(self.T.GetScale())

Get current scale.

orientation

Compute orientation.

position

Compute position.

matrix

Get the 4x4 trasformation matrix.

matrix3x3

Get the 3x3 trasformation matrix.

def write(self, filename='transform.mat'):
490    def write(self, filename="transform.mat"):
491        """Save transformation to ASCII file."""
492        import json
493        m = self.T.GetMatrix()
494        M = [[m.GetElement(i, j) for j in range(4)] for i in range(4)]
495        arr = np.array(M)
496        dictionary = {
497            "name": self.name,
498            "comment": self.comment,
499            "matrix": arr.astype(float).tolist(),
500            "n_concatenated_transforms": self.n_concatenated_transforms,
501        }
502        with open(filename, "w") as outfile:
503            json.dump(dictionary, outfile, sort_keys=True, indent=2)

Save transformation to ASCII file.

def reorient( self, initaxis, newaxis, around=(0, 0, 0), rotation=0, rad=False, xyplane=True):
505    def reorient(
506        self, initaxis, newaxis, around=(0, 0, 0), rotation=0, rad=False, xyplane=True
507    ):
508        """
509        Set/Get object orientation.
510
511        Arguments:
512            rotation : (float)
513                rotate object around newaxis.
514            concatenate : (bool)
515                concatenate the orientation operation with the previous existing transform (if any)
516            rad : (bool)
517                set to True if angle is expressed in radians.
518            xyplane : (bool)
519                make an extra rotation to keep the object aligned to the xy-plane
520        """
521        newaxis  = np.asarray(newaxis) / np.linalg.norm(newaxis)
522        initaxis = np.asarray(initaxis) / np.linalg.norm(initaxis)
523
524        if not np.any(initaxis - newaxis):
525            return self
526
527        if not np.any(initaxis + newaxis):
528            print("Warning: in reorient() initaxis and newaxis are parallel")
529            newaxis += np.array([0.0000001, 0.0000002, 0])
530            angleth = np.pi
531        else:
532            angleth = np.arccos(np.dot(initaxis, newaxis))
533        crossvec = np.cross(initaxis, newaxis)
534
535        p = np.asarray(around)
536        self.T.Translate(-p)
537        if rotation:
538            if rad:
539                rotation = np.rad2deg(rotation)
540            self.T.RotateWXYZ(rotation, initaxis)
541
542        self.T.RotateWXYZ(np.rad2deg(angleth), crossvec)
543
544        if xyplane:
545            self.T.RotateWXYZ(-self.orientation[0] * 1.4142, newaxis)
546
547        self.T.Translate(p)
548        return self

Set/Get object orientation.

Arguments:
  • rotation : (float) rotate object around newaxis.
  • concatenate : (bool) concatenate the orientation operation with the previous existing transform (if any)
  • rad : (bool) set to True if angle is expressed in radians.
  • xyplane : (bool) make an extra rotation to keep the object aligned to the xy-plane
class NonLinearTransform:
552class NonLinearTransform:
553    """Work with non-linear transformations."""
554    
555    def __init__(self, T=None, **kwargs):
556        """
557        Define a non-linear transformation.
558        Can be saved to file and reloaded.
559
560        Arguments:
561            T : (vtkThinPlateSplineTransform, str, dict)
562                vtk transformation.
563                If T is a string, it is assumed to be a filename.
564                If T is a dictionary, it is assumed to be a set of keyword arguments.
565                Defaults to None.
566            **kwargs : (dict)
567                keyword arguments to define the transformation.
568                The following keywords are accepted:
569                - name : (str) name of the transformation
570                - comment : (str) comment
571                - source_points : (list) source points
572                - target_points : (list) target points
573                - mode : (str) either '2d' or '3d'
574                - sigma : (float) sigma parameter
575        
576        Example:
577            ```python
578            from vedo import *
579            settings.use_parallel_projection = True
580
581            NLT = NonLinearTransform()
582            NLT.source_points = [[-2,0,0], [1,2,1], [2,-2,2]]
583            NLT.target_points = NLT.source_points + np.random.randn(3,3)*0.5
584            NLT.mode = '3d'
585            print(NLT)
586
587            s1 = Sphere()
588            NLT.move(s1)
589            # same as:
590            # s1.apply_transform(NLT)
591
592            arrs = Arrows(NLT.source_points, NLT.target_points)
593            show(s1, arrs, Sphere().alpha(0.1), axes=1).close()
594            ```
595        """
596
597        self.name = "NonLinearTransform"
598        self.filename = ""
599        self.comment = ""
600
601        if T is None and len(kwargs) == 0:
602            T = vtk.vtkThinPlateSplineTransform()
603
604        elif isinstance(T, vtk.vtkThinPlateSplineTransform):
605            S = vtk.vtkThinPlateSplineTransform()
606            S.DeepCopy(T)
607            T = S
608
609        elif isinstance(T, NonLinearTransform):
610            S = vtk.vtkThinPlateSplineTransform()
611            S.DeepCopy(T.T)
612            T = S
613
614        elif isinstance(T, str):
615            import json
616            filename = str(T)
617            self.filename = filename
618            with open(filename, "r") as read_file:
619                D = json.load(read_file)
620            self.name = D["name"]
621            self.comment = D["comment"]
622            source = D["source_points"]
623            target = D["target_points"]
624            mode = D["mode"]
625            sigma = D["sigma"]
626
627            T = vtk.vtkThinPlateSplineTransform()
628            vptss = vtk.vtkPoints()
629            for p in source:
630                if len(p) == 2:
631                    p = [p[0], p[1], 0.0]
632                vptss.InsertNextPoint(p)
633            T.SetSourceLandmarks(vptss)
634            vptst = vtk.vtkPoints()
635            for p in target:
636                if len(p) == 2:
637                    p = [p[0], p[1], 0.0]
638                vptst.InsertNextPoint(p)
639            T.SetTargetLandmarks(vptst)
640            T.SetSigma(sigma)
641            if mode == "2d":
642                T.SetBasisToR2LogR()
643            elif mode == "3d":
644                T.SetBasisToR()
645            else:
646                print(f'In {filename} mode can be either "2d" or "3d"')
647
648        elif len(kwargs) > 0:
649            T = kwargs.copy()
650            self.name = T.pop("name", "NonLinearTransform")
651            self.comment = T.pop("comment", "")
652            source = T.pop("source_points", [])
653            target = T.pop("target_points", [])
654            mode = T.pop("mode", "3d")
655            sigma = T.pop("sigma", 1.0)
656            if len(T) > 0:
657                print("Warning: NonLinearTransform got unexpected keyword arguments:")
658                print(T)
659
660            T = vtk.vtkThinPlateSplineTransform()
661            vptss = vtk.vtkPoints()
662            for p in source:
663                if len(p) == 2:
664                    p = [p[0], p[1], 0.0]
665                vptss.InsertNextPoint(p)
666            T.SetSourceLandmarks(vptss)
667            vptst = vtk.vtkPoints()
668            for p in target:
669                if len(p) == 2:
670                    p = [p[0], p[1], 0.0]
671                vptst.InsertNextPoint(p)
672            T.SetTargetLandmarks(vptst)
673            T.SetSigma(sigma)
674            if mode == "2d":
675                T.SetBasisToR2LogR()
676            elif mode == "3d":
677                T.SetBasisToR()
678            else:
679                print(f'In {filename} mode can be either "2d" or "3d"')
680
681        self.T = T
682        self.inverse_flag = False
683
684    def __str__(self):
685        module = self.__class__.__module__
686        name = self.__class__.__name__
687        s = f"\x1b[7m\x1b[1m{module}.{name} at ({hex(id(self))})".ljust(75) + "\x1b[0m\n"
688        s += "name".ljust(9) + ": "  + self.name + "\n"
689        if self.filename:
690            s += "filename".ljust(9) + ": "  + self.filename + "\n"
691        if self.comment:
692            s += "comment".ljust(9) + f': \x1b[3m"{self.comment}"\x1b[0m\n'
693        s += f"mode".ljust(9)  + f": {self.mode}\n"
694        s += f"sigma".ljust(9) + f": {self.sigma}\n"
695        p = self.source_points
696        q = self.target_points
697        s += f"sources".ljust(9) + f": {p.size}, bounds {np.min(p, axis=0)}, {np.max(p, axis=0)}\n"
698        s += f"targets".ljust(9) + f": {q.size}, bounds {np.min(q, axis=0)}, {np.max(q, axis=0)}"
699        return s
700    
701    def __repr__(self):
702        return self.__str__()
703
704    def update(self):
705        """Update transformation."""
706        self.T.Update()
707        return self
708
709    @property
710    def position(self):
711        """
712        Trying to get the position of a `NonLinearTransform` always returns [0,0,0].
713        """
714        return np.array([0.0, 0.0, 0.0], dtype=np.float32)
715    
716    @position.setter
717    def position(self, p):
718        """
719        Trying to set position of a `NonLinearTransform` 
720        has no effect and prints a warning.
721
722        Use clone() method to create a copy of the object,
723        or reset it with 'object.transform = vedo.LinearTransform()'
724        """
725        print("Warning: NonLinearTransform has no position.")
726        print("  Use clone() method to create a copy of the object,")
727        print("  or reset it with 'object.transform = vedo.LinearTransform()'")
728
729    @property
730    def source_points(self):
731        """Get the source points."""
732        pts = self.T.GetSourceLandmarks()
733        vpts = []
734        if pts:
735            for i in range(pts.GetNumberOfPoints()):
736                vpts.append(pts.GetPoint(i))
737        return np.array(vpts, dtype=np.float32)
738    
739    @property
740    def target_points(self):
741        """Get the target points."""
742        pts = self.T.GetTargetLandmarks()
743        vpts = []
744        for i in range(pts.GetNumberOfPoints()):
745            vpts.append(pts.GetPoint(i))
746        return np.array(vpts, dtype=np.float32)
747 
748    @source_points.setter
749    def source_points(self, pts):
750        """Set source points."""
751        if _is_sequence(pts):
752            pass
753        else:
754            pts = pts.vertices
755        vpts = vtk.vtkPoints()
756        for p in pts:
757            if len(p) == 2:
758                p = [p[0], p[1], 0.0]
759            vpts.InsertNextPoint(p)
760        self.T.SetSourceLandmarks(vpts)
761    
762    @target_points.setter
763    def target_points(self, pts):
764        """Set target points."""
765        if _is_sequence(pts):
766            pass
767        else:
768            pts = pts.vertices
769        vpts = vtk.vtkPoints()
770        for p in pts:
771            if len(p) == 2:
772                p = [p[0], p[1], 0.0]
773            vpts.InsertNextPoint(p)
774        self.T.SetTargetLandmarks(vpts)
775       
776    @property
777    def sigma(self) -> float:
778        """Set sigma."""
779        return self.T.GetSigma()
780
781    @sigma.setter
782    def sigma(self, s):
783        """Get sigma."""
784        self.T.SetSigma(s)
785
786    @property
787    def mode(self) -> str:
788        """Get mode."""
789        m = self.T.GetBasis()
790        # print("T.GetBasis()", m, self.T.GetBasisAsString())
791        if m == 2:
792            return "2d"
793        elif m == 1:
794            return "3d"
795        else:
796            print("Warning: NonLinearTransform has no valid mode.")
797
798    @mode.setter
799    def mode(self, m):
800        """Set mode."""
801        if m=='3d':
802            self.T.SetBasisToR()
803        elif m=='2d':
804            self.T.SetBasisToR2LogR()
805        else:
806            print('In NonLinearTransform mode can be either "2d" or "3d"')
807
808    def clone(self):
809        """Clone transformation to make an exact copy."""
810        return NonLinearTransform(self.T)
811        
812    def write(self, filename):
813        """Save transformation to ASCII file."""
814        import json
815        dictionary = {
816            "name": self.name,
817            "comment": self.comment,
818            "mode": self.mode,
819            "sigma": self.sigma,
820            "source_points": self.source_points.astype(float).tolist(),
821            "target_points": self.target_points.astype(float).tolist(),
822        }
823        with open(filename, "w") as outfile:
824            json.dump(dictionary, outfile, sort_keys=True, indent=2)
825        
826    def invert(self):
827        """Invert transformation."""
828        self.T.Inverse()
829        self.inverse_flag = bool(self.T.GetInverseFlag())
830        return self
831    
832    def compute_inverse(self):
833        """Compute inverse."""
834        t = self.clone()
835        t.invert()
836        return t   
837    
838    def move(self, obj):
839        """
840        Apply transformation to object or single point.
841        
842        Note:
843            When applying a transformation to a mesh, the mesh is modified in place.
844            If you want to keep the original mesh unchanged, use `clone()` method.
845        
846        Example:
847            ```python
848            from vedo import *
849            np.random.seed(0)
850            settings.use_parallel_projection = True
851
852            NLT = NonLinearTransform()
853            NLT.source_points = [[-2,0,0], [1,2,1], [2,-2,2]]
854            NLT.target_points = NLT.source_points + np.random.randn(3,3)*0.5
855            NLT.mode = '3d'
856            print(NLT)
857
858            s1 = Sphere()
859            NLT.move(s1)
860            # same as:
861            # s1.apply_transform(NLT)
862
863            arrs = Arrows(NLT.source_points, NLT.target_points)
864            show(s1, arrs, Sphere().alpha(0.1), axes=1).close()
865            ```
866        """
867        if _is_sequence(obj):
868            if len(obj) == 2:
869                obj = [obj[0], obj[1], 0]
870            return np.array(self.T.TransformFloatPoint(obj))
871
872        obj.apply_transform(self)
873        return obj

Work with non-linear transformations.

NonLinearTransform(T=None, **kwargs)
555    def __init__(self, T=None, **kwargs):
556        """
557        Define a non-linear transformation.
558        Can be saved to file and reloaded.
559
560        Arguments:
561            T : (vtkThinPlateSplineTransform, str, dict)
562                vtk transformation.
563                If T is a string, it is assumed to be a filename.
564                If T is a dictionary, it is assumed to be a set of keyword arguments.
565                Defaults to None.
566            **kwargs : (dict)
567                keyword arguments to define the transformation.
568                The following keywords are accepted:
569                - name : (str) name of the transformation
570                - comment : (str) comment
571                - source_points : (list) source points
572                - target_points : (list) target points
573                - mode : (str) either '2d' or '3d'
574                - sigma : (float) sigma parameter
575        
576        Example:
577            ```python
578            from vedo import *
579            settings.use_parallel_projection = True
580
581            NLT = NonLinearTransform()
582            NLT.source_points = [[-2,0,0], [1,2,1], [2,-2,2]]
583            NLT.target_points = NLT.source_points + np.random.randn(3,3)*0.5
584            NLT.mode = '3d'
585            print(NLT)
586
587            s1 = Sphere()
588            NLT.move(s1)
589            # same as:
590            # s1.apply_transform(NLT)
591
592            arrs = Arrows(NLT.source_points, NLT.target_points)
593            show(s1, arrs, Sphere().alpha(0.1), axes=1).close()
594            ```
595        """
596
597        self.name = "NonLinearTransform"
598        self.filename = ""
599        self.comment = ""
600
601        if T is None and len(kwargs) == 0:
602            T = vtk.vtkThinPlateSplineTransform()
603
604        elif isinstance(T, vtk.vtkThinPlateSplineTransform):
605            S = vtk.vtkThinPlateSplineTransform()
606            S.DeepCopy(T)
607            T = S
608
609        elif isinstance(T, NonLinearTransform):
610            S = vtk.vtkThinPlateSplineTransform()
611            S.DeepCopy(T.T)
612            T = S
613
614        elif isinstance(T, str):
615            import json
616            filename = str(T)
617            self.filename = filename
618            with open(filename, "r") as read_file:
619                D = json.load(read_file)
620            self.name = D["name"]
621            self.comment = D["comment"]
622            source = D["source_points"]
623            target = D["target_points"]
624            mode = D["mode"]
625            sigma = D["sigma"]
626
627            T = vtk.vtkThinPlateSplineTransform()
628            vptss = vtk.vtkPoints()
629            for p in source:
630                if len(p) == 2:
631                    p = [p[0], p[1], 0.0]
632                vptss.InsertNextPoint(p)
633            T.SetSourceLandmarks(vptss)
634            vptst = vtk.vtkPoints()
635            for p in target:
636                if len(p) == 2:
637                    p = [p[0], p[1], 0.0]
638                vptst.InsertNextPoint(p)
639            T.SetTargetLandmarks(vptst)
640            T.SetSigma(sigma)
641            if mode == "2d":
642                T.SetBasisToR2LogR()
643            elif mode == "3d":
644                T.SetBasisToR()
645            else:
646                print(f'In {filename} mode can be either "2d" or "3d"')
647
648        elif len(kwargs) > 0:
649            T = kwargs.copy()
650            self.name = T.pop("name", "NonLinearTransform")
651            self.comment = T.pop("comment", "")
652            source = T.pop("source_points", [])
653            target = T.pop("target_points", [])
654            mode = T.pop("mode", "3d")
655            sigma = T.pop("sigma", 1.0)
656            if len(T) > 0:
657                print("Warning: NonLinearTransform got unexpected keyword arguments:")
658                print(T)
659
660            T = vtk.vtkThinPlateSplineTransform()
661            vptss = vtk.vtkPoints()
662            for p in source:
663                if len(p) == 2:
664                    p = [p[0], p[1], 0.0]
665                vptss.InsertNextPoint(p)
666            T.SetSourceLandmarks(vptss)
667            vptst = vtk.vtkPoints()
668            for p in target:
669                if len(p) == 2:
670                    p = [p[0], p[1], 0.0]
671                vptst.InsertNextPoint(p)
672            T.SetTargetLandmarks(vptst)
673            T.SetSigma(sigma)
674            if mode == "2d":
675                T.SetBasisToR2LogR()
676            elif mode == "3d":
677                T.SetBasisToR()
678            else:
679                print(f'In {filename} mode can be either "2d" or "3d"')
680
681        self.T = T
682        self.inverse_flag = False

Define a non-linear transformation. Can be saved to file and reloaded.

Arguments:
  • T : (vtkThinPlateSplineTransform, str, dict) vtk transformation. If T is a string, it is assumed to be a filename. If T is a dictionary, it is assumed to be a set of keyword arguments. Defaults to None.
  • **kwargs : (dict) keyword arguments to define the transformation. The following keywords are accepted:
    • name : (str) name of the transformation
    • comment : (str) comment
    • source_points : (list) source points
    • target_points : (list) target points
    • mode : (str) either '2d' or '3d'
    • sigma : (float) sigma parameter
Example:
from vedo import *
settings.use_parallel_projection = True

NLT = NonLinearTransform()
NLT.source_points = [[-2,0,0], [1,2,1], [2,-2,2]]
NLT.target_points = NLT.source_points + np.random.randn(3,3)*0.5
NLT.mode = '3d'
print(NLT)

s1 = Sphere()
NLT.move(s1)
# same as:
# s1.apply_transform(NLT)

arrs = Arrows(NLT.source_points, NLT.target_points)
show(s1, arrs, Sphere().alpha(0.1), axes=1).close()
def update(self):
704    def update(self):
705        """Update transformation."""
706        self.T.Update()
707        return self

Update transformation.

position

Trying to get the position of a NonLinearTransform always returns [0,0,0].

source_points

Get the source points.

target_points

Get the target points.

sigma: float

Set sigma.

mode: str

Get mode.

def clone(self):
808    def clone(self):
809        """Clone transformation to make an exact copy."""
810        return NonLinearTransform(self.T)

Clone transformation to make an exact copy.

def write(self, filename):
812    def write(self, filename):
813        """Save transformation to ASCII file."""
814        import json
815        dictionary = {
816            "name": self.name,
817            "comment": self.comment,
818            "mode": self.mode,
819            "sigma": self.sigma,
820            "source_points": self.source_points.astype(float).tolist(),
821            "target_points": self.target_points.astype(float).tolist(),
822        }
823        with open(filename, "w") as outfile:
824            json.dump(dictionary, outfile, sort_keys=True, indent=2)

Save transformation to ASCII file.

def invert(self):
826    def invert(self):
827        """Invert transformation."""
828        self.T.Inverse()
829        self.inverse_flag = bool(self.T.GetInverseFlag())
830        return self

Invert transformation.

def compute_inverse(self):
832    def compute_inverse(self):
833        """Compute inverse."""
834        t = self.clone()
835        t.invert()
836        return t   

Compute inverse.

def move(self, obj):
838    def move(self, obj):
839        """
840        Apply transformation to object or single point.
841        
842        Note:
843            When applying a transformation to a mesh, the mesh is modified in place.
844            If you want to keep the original mesh unchanged, use `clone()` method.
845        
846        Example:
847            ```python
848            from vedo import *
849            np.random.seed(0)
850            settings.use_parallel_projection = True
851
852            NLT = NonLinearTransform()
853            NLT.source_points = [[-2,0,0], [1,2,1], [2,-2,2]]
854            NLT.target_points = NLT.source_points + np.random.randn(3,3)*0.5
855            NLT.mode = '3d'
856            print(NLT)
857
858            s1 = Sphere()
859            NLT.move(s1)
860            # same as:
861            # s1.apply_transform(NLT)
862
863            arrs = Arrows(NLT.source_points, NLT.target_points)
864            show(s1, arrs, Sphere().alpha(0.1), axes=1).close()
865            ```
866        """
867        if _is_sequence(obj):
868            if len(obj) == 2:
869                obj = [obj[0], obj[1], 0]
870            return np.array(self.T.TransformFloatPoint(obj))
871
872        obj.apply_transform(self)
873        return obj

Apply transformation to object or single point.

Note:

When applying a transformation to a mesh, the mesh is modified in place. If you want to keep the original mesh unchanged, use clone() method.

Example:
from vedo import *
np.random.seed(0)
settings.use_parallel_projection = True

NLT = NonLinearTransform()
NLT.source_points = [[-2,0,0], [1,2,1], [2,-2,2]]
NLT.target_points = NLT.source_points + np.random.randn(3,3)*0.5
NLT.mode = '3d'
print(NLT)

s1 = Sphere()
NLT.move(s1)
# same as:
# s1.apply_transform(NLT)

arrs = Arrows(NLT.source_points, NLT.target_points)
show(s1, arrs, Sphere().alpha(0.1), axes=1).close()
def spher2cart(rho, theta, phi):
902def spher2cart(rho, theta, phi):
903    """3D Spherical to Cartesian coordinate conversion."""
904    st = np.sin(theta)
905    sp = np.sin(phi)
906    ct = np.cos(theta)
907    cp = np.cos(phi)
908    rst = rho * st
909    x = rst * cp
910    y = rst * sp
911    z = rho * ct
912    return np.array([x, y, z])

3D Spherical to Cartesian coordinate conversion.

def cart2spher(x, y, z):
893def cart2spher(x, y, z):
894    """3D Cartesian to Spherical coordinate conversion."""
895    hxy = np.hypot(x, y)
896    rho = np.hypot(hxy, z)
897    theta = np.arctan2(hxy, z)
898    phi = np.arctan2(y, x)
899    return np.array([rho, theta, phi])

3D Cartesian to Spherical coordinate conversion.

def cart2cyl(x, y, z):
915def cart2cyl(x, y, z):
916    """3D Cartesian to Cylindrical coordinate conversion."""
917    rho = np.sqrt(x * x + y * y)
918    theta = np.arctan2(y, x)
919    return np.array([rho, theta, z])

3D Cartesian to Cylindrical coordinate conversion.

def cyl2cart(rho, theta, z):
922def cyl2cart(rho, theta, z):
923    """3D Cylindrical to Cartesian coordinate conversion."""
924    x = rho * np.cos(theta)
925    y = rho * np.sin(theta)
926    return np.array([x, y, z])

3D Cylindrical to Cartesian coordinate conversion.

def cyl2spher(rho, theta, z):
929def cyl2spher(rho, theta, z):
930    """3D Cylindrical to Spherical coordinate conversion."""
931    rhos = np.sqrt(rho * rho + z * z)
932    phi = np.arctan2(rho, z)
933    return np.array([rhos, phi, theta])

3D Cylindrical to Spherical coordinate conversion.

def spher2cyl(rho, theta, phi):
936def spher2cyl(rho, theta, phi):
937    """3D Spherical to Cylindrical coordinate conversion."""
938    rhoc = rho * np.sin(theta)
939    z = rho * np.cos(theta)
940    return np.array([rhoc, phi, z])

3D Spherical to Cylindrical coordinate conversion.

def cart2pol(x, y):
877def cart2pol(x, y):
878    """2D Cartesian to Polar coordinates conversion."""
879    theta = np.arctan2(y, x)
880    rho = np.hypot(x, y)
881    return np.array([rho, theta])

2D Cartesian to Polar coordinates conversion.

def pol2cart(rho, theta):
884def pol2cart(rho, theta):
885    """2D Polar to Cartesian coordinates conversion."""
886    x = rho * np.cos(theta)
887    y = rho * np.sin(theta)
888    return np.array([x, y])

2D Polar to Cartesian coordinates conversion.