@@ -1289,7 +1289,8 @@ class Transform(TransformNode):
1289
1289
actually perform a transformation.
1290
1290
1291
1291
All non-affine transformations should be subclasses of this class.
1292
- New affine transformations should be subclasses of `Affine2D`.
1292
+ New affine transformations should be subclasses of `Affine2D` or
1293
+ `Affine3D`.
1293
1294
1294
1295
Subclasses of this class should override the following members (at
1295
1296
minimum):
@@ -1510,7 +1511,7 @@ def transform(self, values):
1510
1511
return res [0 , 0 ]
1511
1512
if ndim == 1 :
1512
1513
return res .reshape (- 1 )
1513
- elif ndim == 2 :
1514
+ elif ndim == 2 or ndim == 3 :
1514
1515
return res
1515
1516
raise ValueError (
1516
1517
"Input values must have shape (N, {dims}) or ({dims},)"
@@ -1839,8 +1840,8 @@ class AffineImmutable(AffineBase):
1839
1840
b d f
1840
1841
0 0 1
1841
1842
1842
- This class provides the read-only interface. For a mutable 2D
1843
- affine transformation, use `Affine2D`.
1843
+ This class provides the read-only interface. For a mutable
1844
+ affine transformation, use `Affine2D` or `Affine3D` .
1844
1845
1845
1846
Subclasses of this class will generally only need to override a
1846
1847
constructor and `~.Transform.get_matrix` that generates a custom matrix
@@ -1942,6 +1943,8 @@ class Affine2DBase(AffineImmutable):
1942
1943
def _affine_factory (mtx , dims , * args , ** kwargs ):
1943
1944
if dims == 2 :
1944
1945
return Affine2D (mtx , * args , ** kwargs )
1946
+ elif dims == 3 :
1947
+ return Affine3D (mtx , * args , ** kwargs )
1945
1948
else :
1946
1949
return NotImplemented
1947
1950
@@ -2169,6 +2172,298 @@ def skew_deg(self, xShear, yShear):
2169
2172
return self .skew (math .radians (xShear ), math .radians (yShear ))
2170
2173
2171
2174
2175
+ class Affine3D (AffineImmutable ):
2176
+ """
2177
+ A mutable 3D affine transformation.
2178
+ """
2179
+
2180
+ def __init__ (self , matrix = None , ** kwargs ):
2181
+ """
2182
+ Initialize an Affine transform from a 4x4 numpy float array::
2183
+
2184
+ a d g j
2185
+ b e h k
2186
+ c f i l
2187
+ 0 0 0 1
2188
+
2189
+ If *matrix* is None, initialize with the identity transform.
2190
+ """
2191
+ super ().__init__ (dims = 3 , ** kwargs )
2192
+ if matrix is None :
2193
+ matrix = np .identity (4 )
2194
+ self ._mtx = matrix .copy ()
2195
+ self ._invalid = 0
2196
+
2197
+ _base_str = _make_str_method ("_mtx" )
2198
+
2199
+ def __str__ (self ):
2200
+ return (self ._base_str ()
2201
+ if (self ._mtx != np .diag (np .diag (self ._mtx ))).any ()
2202
+ else f"Affine3D().scale("
2203
+ f"{ self ._mtx [0 , 0 ]} , "
2204
+ f"{ self ._mtx [1 , 1 ]} , "
2205
+ f"{ self ._mtx [2 , 2 ]} )"
2206
+ if self ._mtx [0 , 0 ] != self ._mtx [1 , 1 ] or
2207
+ self ._mtx [0 , 0 ] != self ._mtx [2 , 2 ]
2208
+ else f"Affine3D().scale({ self ._mtx [0 , 0 ]} )" )
2209
+
2210
+ @staticmethod
2211
+ def from_values (a , b , c , d , e , f , g , h , i , j , k , l ):
2212
+ """
2213
+ Create a new Affine2D instance from the given values::
2214
+
2215
+ a d g j
2216
+ b e h k
2217
+ c f i l
2218
+ 0 0 0 1
2219
+
2220
+ .
2221
+ """
2222
+ return Affine3D (np .array ([
2223
+ a , d , g , j ,
2224
+ b , e , h , k ,
2225
+ c , f , i , l ,
2226
+ 0.0 , 0.0 , 0.0 , 1.0
2227
+ ], float ).reshape ((4 , 4 )))
2228
+
2229
+ def get_matrix (self ):
2230
+ """
2231
+ Get the underlying transformation matrix as a 4x4 array::
2232
+
2233
+ a d g j
2234
+ b e h k
2235
+ c f i l
2236
+ 0 0 0 1
2237
+
2238
+ .
2239
+ """
2240
+ if self ._invalid :
2241
+ self ._inverted = None
2242
+ self ._invalid = 0
2243
+ return self ._mtx
2244
+
2245
+ def set_matrix (self , mtx ):
2246
+ """
2247
+ Set the underlying transformation matrix from a 4x4 array::
2248
+
2249
+ a d g j
2250
+ b e h k
2251
+ c f i l
2252
+ 0 0 0 1
2253
+
2254
+ .
2255
+ """
2256
+ self ._mtx = mtx
2257
+ self .invalidate ()
2258
+
2259
+ def set (self , other ):
2260
+ """
2261
+ Set this transformation from the frozen copy of another
2262
+ `AffineImmutable` object with input and output dimension of 3.
2263
+ """
2264
+ _api .check_isinstance (AffineImmutable , other = other )
2265
+ if (other .input_dims != 3 ):
2266
+ raise TypeError ("Mismatch between dimensions of AffineImmutable"
2267
+ "and Affine3D" )
2268
+ self ._mtx = other .get_matrix ()
2269
+ self .invalidate ()
2270
+
2271
+ def clear (self ):
2272
+ """
2273
+ Reset the underlying matrix to the identity transform.
2274
+ """
2275
+ self ._mtx = np .identity (4 )
2276
+ self .invalidate ()
2277
+ return self
2278
+
2279
+ def rotate (self , theta , dim = 0 ):
2280
+ """
2281
+ Add a rotation (in radians) to this transform in place, along
2282
+ the dimension denoted by *dim*.
2283
+
2284
+ Returns *self*, so this method can easily be chained with more
2285
+ calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2286
+ and :meth:`scale`.
2287
+ """
2288
+ if dim == 0 :
2289
+ return self .rotate_around_vector ([1 , 0 , 0 ], theta )
2290
+ elif dim == 1 :
2291
+ return self .rotate_around_vector ([0 , 1 , 0 ], theta )
2292
+ elif dim == 2 :
2293
+ return self .rotate_around_vector ([0 , 0 , 1 ], theta )
2294
+
2295
+ self .invalidate ()
2296
+ return self
2297
+
2298
+ def rotate_deg (self , degrees , dim = 0 ):
2299
+ """
2300
+ Add a rotation (in degrees) to this transform in place, along
2301
+ the dimension denoted by *dim*.
2302
+
2303
+ Returns *self*, so this method can easily be chained with more
2304
+ calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2305
+ and :meth:`scale`.
2306
+ """
2307
+ return self .rotate (math .radians (degrees ), dim )
2308
+
2309
+ def rotate_around (self , x , y , z , theta , dim = 0 ):
2310
+ """
2311
+ Add a rotation (in radians) around the point (x, y, z) in place,
2312
+ along the dimension denoted by *dim*.
2313
+
2314
+ Returns *self*, so this method can easily be chained with more
2315
+ calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2316
+ and :meth:`scale`.
2317
+ """
2318
+ return self .translate (- x , - y , - z ).rotate (theta , dim ).translate (x , y , z )
2319
+
2320
+ def rotate_deg_around (self , x , y , z , degrees , dim = 0 ):
2321
+ """
2322
+ Add a rotation (in degrees) around the point (x, y, z) in place,
2323
+ along the dimension denoted by *dim*.
2324
+
2325
+ Returns *self*, so this method can easily be chained with more
2326
+ calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2327
+ and :meth:`scale`.
2328
+ """
2329
+ # Cast to float to avoid wraparound issues with uint8's
2330
+ x , y = float (x ), float (y )
2331
+ return self .translate (- x , - y , - z ).rotate_deg (degrees , dim ).translate (x , y , z )
2332
+
2333
+ def rotate_around_vector (self , vector , theta ):
2334
+ """
2335
+ Add a rotation (in radians) around the vector (vx, vy, vz) in place.
2336
+
2337
+ Returns *self*, so this method can easily be chained with more
2338
+ calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2339
+ and :meth:`scale`.
2340
+ """
2341
+ vx , vy , vz = vector / np .linalg .norm (vector )
2342
+ s = np .sin (theta )
2343
+ c = np .cos (theta )
2344
+ t = 2 * np .sin (theta / 2 )** 2 # more numerically stable than t = 1-c
2345
+ rot = [[t * vx * vx + c , t * vx * vy - vz * s , t * vx * vz + vy * s , 0 ],
2346
+ [t * vy * vx + vz * s , t * vy * vy + c , t * vy * vz - vx * s , 0 ],
2347
+ [t * vz * vx - vy * s , t * vz * vy + vx * s , t * vz * vz + c , 0 ],
2348
+ [0 , 0 , 0 , 1 ]]
2349
+ np .matmul (rot , self ._mtx , out = self ._mtx )
2350
+ return self
2351
+
2352
+ def rotate_around_vector_deg (self , vector , degrees ):
2353
+ """
2354
+ Add a rotation (in radians) around the vector (vx, vy, vz) in place.
2355
+
2356
+ Returns *self*, so this method can easily be chained with more
2357
+ calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2358
+ and :meth:`scale`.
2359
+ """
2360
+ return self .roate_around_vector (vector , math .radians (degrees ))
2361
+
2362
+ def translate (self , tx , ty , tz ):
2363
+ """
2364
+ Add a translation in place.
2365
+
2366
+ Returns *self*, so this method can easily be chained with more
2367
+ calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2368
+ and :meth:`scale`.
2369
+ """
2370
+ self ._mtx [0 , 3 ] += tx
2371
+ self ._mtx [1 , 3 ] += ty
2372
+ self ._mtx [2 , 3 ] += tz
2373
+ self .invalidate ()
2374
+ return self
2375
+
2376
+ def scale (self , sx , sy = None , sz = None ):
2377
+ """
2378
+ Add a scale in place.
2379
+
2380
+ If a scale is not provided in the *y* or *z* directions, *sx*
2381
+ will be applied for that direction.
2382
+
2383
+ Returns *self*, so this method can easily be chained with more
2384
+ calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2385
+ and :meth:`scale`.
2386
+ """
2387
+ if sy is None :
2388
+ sy = sx
2389
+
2390
+ if sz is None :
2391
+ sz = sx
2392
+ # explicit element-wise scaling is fastest
2393
+ self ._mtx [0 , 0 ] *= sx
2394
+ self ._mtx [0 , 1 ] *= sx
2395
+ self ._mtx [0 , 2 ] *= sx
2396
+ self ._mtx [0 , 3 ] *= sx
2397
+ self ._mtx [1 , 0 ] *= sy
2398
+ self ._mtx [1 , 1 ] *= sy
2399
+ self ._mtx [1 , 2 ] *= sy
2400
+ self ._mtx [1 , 3 ] *= sy
2401
+ self ._mtx [2 , 0 ] *= sz
2402
+ self ._mtx [2 , 1 ] *= sz
2403
+ self ._mtx [2 , 2 ] *= sz
2404
+ self ._mtx [2 , 3 ] *= sz
2405
+
2406
+ self .invalidate ()
2407
+ return self
2408
+
2409
+ def skew (self , xyShear , xzShear , yxShear , yzShear , zxShear , zyShear ):
2410
+ """
2411
+ Add a skew in place along for each plane in the 3rd dimension.
2412
+
2413
+ For example *zxShear* is the shear angle along the *zx* plane,
2414
+ in radians.
2415
+
2416
+ Returns *self*, so this method can easily be chained with more
2417
+ calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2418
+ and :meth:`scale`.
2419
+ """
2420
+ rxy = math .tan (xyShear )
2421
+ rxz = math .tan (xzShear )
2422
+ ryx = math .tan (yxShear )
2423
+ ryz = math .tan (yzShear )
2424
+ rzx = math .tan (zxShear )
2425
+ rzy = math .tan (zyShear )
2426
+ mtx = self ._mtx
2427
+ # Operating and assigning one scalar at a time is much faster.
2428
+ (xx , xy , xz , x0 ), (yx , yy , yz , y0 ), (zx , zy , zz , z0 ), _ = mtx .tolist ()
2429
+ # mtx = [[1 rx 0], [ry 1 0], [0 0 1]] * mtx
2430
+
2431
+ mtx [0 , 0 ] += (rxy * yx ) + (rxz * zx )
2432
+ mtx [0 , 1 ] += (rxy * yy ) + (rxz * zy )
2433
+ mtx [0 , 2 ] += (rxy * yz ) + (rxz * zz )
2434
+ mtx [0 , 3 ] += (rxy * y0 ) + (rxz * z0 )
2435
+ mtx [1 , 0 ] = (ryx * xx ) + yx + (ryz * zx )
2436
+ mtx [1 , 1 ] = (ryx * xy ) + yy + (ryz * zy )
2437
+ mtx [1 , 2 ] = (ryx * xz ) + yz + (ryz * zz )
2438
+ mtx [1 , 3 ] = (ryx * x0 ) + y0 + (ryz * z0 )
2439
+ mtx [2 , 0 ] = (rzx * xx ) + (rzy * yx ) + zx
2440
+ mtx [2 , 1 ] = (rzx * xy ) + (rzy * yy ) + zy
2441
+ mtx [2 , 2 ] = (rzx * xz ) + (rzy * yz ) + zz
2442
+ mtx [2 , 3 ] = (rzx * x0 ) + (rzy * y0 ) + z0
2443
+
2444
+ self .invalidate ()
2445
+ return self
2446
+
2447
+ def skew_deg (self , xyShear , xzShear , yxShear , yzShear , zxShear , zyShear ):
2448
+ """
2449
+ Add a skew in place along for each plane in the 3rd dimension.
2450
+
2451
+ For example *zxShear* is the shear angle along the *zx* plane,
2452
+ in radians.
2453
+
2454
+ Returns *self*, so this method can easily be chained with more
2455
+ calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2456
+ and :meth:`scale`.
2457
+ """
2458
+ return self .skew (
2459
+ math .radians (xyShear ),
2460
+ math .radians (xzShear ),
2461
+ math .radians (yxShear ),
2462
+ math .radians (yzShear ),
2463
+ math .radians (zxShear ),
2464
+ math .radians (zyShear ))
2465
+
2466
+
2172
2467
class IdentityTransform (AffineImmutable ):
2173
2468
"""
2174
2469
A special class that does one thing, the identity transform, in a
@@ -2615,6 +2910,8 @@ def composite_transform_factory(a, b):
2615
2910
return a
2616
2911
elif isinstance (a , Affine2D ) and isinstance (b , Affine2D ):
2617
2912
return CompositeAffine (a , b )
2913
+ elif isinstance (a , Affine3D ) and isinstance (b , Affine3D ):
2914
+ return CompositeAffine (a , b )
2618
2915
return CompositeGenericTransform (a , b )
2619
2916
2620
2917
0 commit comments