@@ -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,299 @@ 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
+ a = math .cos (theta )
2289
+ b = math .sin (theta )
2290
+ mtx = self ._mtx
2291
+ # Operating and assigning one scalar at a time is much faster.
2292
+ (xx , xy , xz , x0 ), (yx , yy , yz , y0 ), (zx , zy , zz , z0 ), _ = mtx .tolist ()
2293
+ # mtx = [[a -b 0], [b a 0], [0 0 1]] * mtx
2294
+
2295
+ if dim == 0 :
2296
+ mtx [1 , 0 ] = (a * yx ) - (b * zx )
2297
+ mtx [1 , 1 ] = (a * yy ) - (b * zy )
2298
+ mtx [1 , 2 ] = (a * yz ) - (b * zz )
2299
+ mtx [1 , 3 ] = (a * y0 ) - (b * z0 )
2300
+ mtx [2 , 0 ] = (b * yx ) + (a * zx )
2301
+ mtx [2 , 1 ] = (b * yy ) + (a * zy )
2302
+ mtx [2 , 2 ] = (b * yz ) + (a * zz )
2303
+ mtx [2 , 3 ] = (b * y0 ) + (a * z0 )
2304
+
2305
+ elif dim == 1 :
2306
+ mtx [0 , 0 ] = (a * xx ) + (b * zx )
2307
+ mtx [0 , 1 ] = (a * xy ) + (b * zy )
2308
+ mtx [0 , 2 ] = (a * xz ) + (b * zz )
2309
+ mtx [0 , 3 ] = (a * x0 ) + (b * z0 )
2310
+ mtx [2 , 0 ] = (a * zx ) - (b * xx )
2311
+ mtx [2 , 1 ] = (a * zy ) - (b * xy )
2312
+ mtx [2 , 2 ] = (a * zz ) - (b * xz )
2313
+ mtx [2 , 3 ] = (a * z0 ) - (b * x0 )
2314
+
2315
+ elif dim == 2 :
2316
+ mtx [0 , 0 ] = (a * xx ) - (b * yx )
2317
+ mtx [0 , 1 ] = (a * xy ) - (b * yy )
2318
+ mtx [0 , 2 ] = (a * xz ) - (b * yz )
2319
+ mtx [0 , 3 ] = (a * x0 ) - (b * y0 )
2320
+ mtx [1 , 0 ] = (b * xx ) + (a * yx )
2321
+ mtx [1 , 1 ] = (b * xy ) + (a * yy )
2322
+ mtx [1 , 2 ] = (b * xz ) + (a * yz )
2323
+ mtx [1 , 3 ] = (b * x0 ) + (a * y0 )
2324
+
2325
+ self .invalidate ()
2326
+ return self
2327
+
2328
+ def rotate_deg (self , degrees , dim = 0 ):
2329
+ """
2330
+ Add a rotation (in degrees) to this transform in place, along
2331
+ the dimension denoted by *dim*.
2332
+
2333
+ Returns *self*, so this method can easily be chained with more
2334
+ calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2335
+ and :meth:`scale`.
2336
+ """
2337
+ return self .rotate (math .radians (degrees ), dim )
2338
+
2339
+ def rotate_around (self , x , y , z , theta , dim = 0 ):
2340
+ """
2341
+ Add a rotation (in radians) around the point (x, y, z) in place,
2342
+ along the dimension denoted by *dim*.
2343
+
2344
+ Returns *self*, so this method can easily be chained with more
2345
+ calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2346
+ and :meth:`scale`.
2347
+ """
2348
+ return self .translate (- x , - y , - z ).rotate (theta , dim ).translate (x , y , z )
2349
+
2350
+ def rotate_deg_around (self , x , y , z , degrees , dim = 0 ):
2351
+ """
2352
+ Add a rotation (in degrees) around the point (x, y, z) in place,
2353
+ along the dimension denoted by *dim*.
2354
+
2355
+ Returns *self*, so this method can easily be chained with more
2356
+ calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2357
+ and :meth:`scale`.
2358
+ """
2359
+ # Cast to float to avoid wraparound issues with uint8's
2360
+ x , y = float (x ), float (y )
2361
+ return self .translate (- x , - y , - z ).rotate_deg (degrees , dim ).translate (x , y , z )
2362
+
2363
+ def translate (self , tx , ty , tz ):
2364
+ """
2365
+ Add a translation in place.
2366
+
2367
+ Returns *self*, so this method can easily be chained with more
2368
+ calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2369
+ and :meth:`scale`.
2370
+ """
2371
+ self ._mtx [0 , 3 ] += tx
2372
+ self ._mtx [1 , 3 ] += ty
2373
+ self ._mtx [2 , 3 ] += tz
2374
+ self .invalidate ()
2375
+ return self
2376
+
2377
+ def scale (self , sx , sy = None , sz = None ):
2378
+ """
2379
+ Add a scale in place.
2380
+
2381
+ If a scale is not provided in the *y* or *z* directions, *sx*
2382
+ will be applied for that direction.
2383
+
2384
+ Returns *self*, so this method can easily be chained with more
2385
+ calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2386
+ and :meth:`scale`.
2387
+ """
2388
+ if sy is None :
2389
+ sy = sx
2390
+
2391
+ if sz is None :
2392
+ sz = sx
2393
+ # explicit element-wise scaling is fastest
2394
+ self ._mtx [0 , 0 ] *= sx
2395
+ self ._mtx [0 , 1 ] *= sx
2396
+ self ._mtx [0 , 2 ] *= sx
2397
+ self ._mtx [0 , 3 ] *= sx
2398
+ self ._mtx [1 , 0 ] *= sy
2399
+ self ._mtx [1 , 1 ] *= sy
2400
+ self ._mtx [1 , 2 ] *= sy
2401
+ self ._mtx [1 , 3 ] *= sy
2402
+ self ._mtx [2 , 0 ] *= sz
2403
+ self ._mtx [2 , 1 ] *= sz
2404
+ self ._mtx [2 , 2 ] *= sz
2405
+ self ._mtx [2 , 3 ] *= sz
2406
+
2407
+ self .invalidate ()
2408
+ return self
2409
+
2410
+ def skew (self , xyShear , xzShear , yxShear , yzShear , zxShear , zyShear ):
2411
+ """
2412
+ Add a skew in place along for each plane in the 3rd dimension.
2413
+
2414
+ For example *zxShear* is the shear angle along the *zx* plane,
2415
+ in radians.
2416
+
2417
+ Returns *self*, so this method can easily be chained with more
2418
+ calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2419
+ and :meth:`scale`.
2420
+ """
2421
+ rxy = math .tan (xyShear )
2422
+ rxz = math .tan (xzShear )
2423
+ ryx = math .tan (yxShear )
2424
+ ryz = math .tan (yzShear )
2425
+ rzx = math .tan (zxShear )
2426
+ rzy = math .tan (zyShear )
2427
+ mtx = self ._mtx
2428
+ # Operating and assigning one scalar at a time is much faster.
2429
+ (xx , xy , xz , x0 ), (yx , yy , yz , y0 ), (zx , zy , zz , z0 ), _ = mtx .tolist ()
2430
+ # mtx = [[1 rx 0], [ry 1 0], [0 0 1]] * mtx
2431
+
2432
+ mtx [0 , 0 ] += (rxy * yx ) + (rxz * zx )
2433
+ mtx [0 , 1 ] += (rxy * yy ) + (rxz * zy )
2434
+ mtx [0 , 2 ] += (rxy * yz ) + (rxz * zz )
2435
+ mtx [0 , 3 ] += (rxy * y0 ) + (rxz * z0 )
2436
+ mtx [1 , 0 ] = (ryx * xx ) + yx + (ryz * zx )
2437
+ mtx [1 , 1 ] = (ryx * xy ) + yy + (ryz * zy )
2438
+ mtx [1 , 2 ] = (ryx * xz ) + yz + (ryz * zz )
2439
+ mtx [1 , 3 ] = (ryx * x0 ) + y0 + (ryz * z0 )
2440
+ mtx [2 , 0 ] = (rzx * xx ) + (rzy * yx ) + zx
2441
+ mtx [2 , 1 ] = (rzx * xy ) + (rzy * yy ) + zy
2442
+ mtx [2 , 2 ] = (rzx * xz ) + (rzy * yz ) + zz
2443
+ mtx [2 , 3 ] = (rzx * x0 ) + (rzy * y0 ) + z0
2444
+
2445
+ self .invalidate ()
2446
+ return self
2447
+
2448
+ def skew_deg (self , xyShear , xzShear , yxShear , yzShear , zxShear , zyShear ):
2449
+ """
2450
+ Add a skew in place along for each plane in the 3rd dimension.
2451
+
2452
+ For example *zxShear* is the shear angle along the *zx* plane,
2453
+ in radians.
2454
+
2455
+ Returns *self*, so this method can easily be chained with more
2456
+ calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2457
+ and :meth:`scale`.
2458
+ """
2459
+ return self .skew (
2460
+ math .radians (xyShear ),
2461
+ math .radians (xzShear ),
2462
+ math .radians (yxShear ),
2463
+ math .radians (yzShear ),
2464
+ math .radians (zxShear ),
2465
+ math .radians (zyShear ))
2466
+
2467
+
2172
2468
class IdentityTransform (AffineImmutable ):
2173
2469
"""
2174
2470
A special class that does one thing, the identity transform, in a
@@ -2615,6 +2911,8 @@ def composite_transform_factory(a, b):
2615
2911
return a
2616
2912
elif isinstance (a , Affine2D ) and isinstance (b , Affine2D ):
2617
2913
return CompositeAffine (a , b )
2914
+ elif isinstance (a , Affine3D ) and isinstance (b , Affine3D ):
2915
+ return CompositeAffine (a , b )
2618
2916
return CompositeGenericTransform (a , b )
2619
2917
2620
2918
0 commit comments