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