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