Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bea3ef0

Browse files
committed
Implement Affine3D
1 parent a44e7d8 commit bea3ef0

File tree

2 files changed

+328
-4
lines changed

2 files changed

+328
-4
lines changed

lib/matplotlib/transforms.py

Lines changed: 301 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,7 +1289,8 @@ class Transform(TransformNode):
12891289
actually perform a transformation.
12901290
12911291
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`.
12931294
12941295
Subclasses of this class should override the following members (at
12951296
minimum):
@@ -1510,7 +1511,7 @@ def transform(self, values):
15101511
return res[0, 0]
15111512
if ndim == 1:
15121513
return res.reshape(-1)
1513-
elif ndim == 2:
1514+
elif ndim == 2 or ndim == 3:
15141515
return res
15151516
raise ValueError(
15161517
"Input values must have shape (N, {dims}) or ({dims},)"
@@ -1839,8 +1840,8 @@ class AffineImmutable(AffineBase):
18391840
b d f
18401841
0 0 1
18411842
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`.
18441845
18451846
Subclasses of this class will generally only need to override a
18461847
constructor and `~.Transform.get_matrix` that generates a custom matrix
@@ -1920,6 +1921,8 @@ class Affine2DBase(AffineImmutable):
19201921
def _affine_factory(mtx, dims, *args, **kwargs):
19211922
if dims == 2:
19221923
return Affine2D(mtx, *args, **kwargs)
1924+
elif dims == 3:
1925+
return Affine3D(mtx, *args, **kwargs)
19231926
else:
19241927
return NotImplemented
19251928

@@ -2147,6 +2150,298 @@ def skew_deg(self, xShear, yShear):
21472150
return self.skew(math.radians(xShear), math.radians(yShear))
21482151

21492152

2153+
class Affine3D(AffineImmutable):
2154+
"""
2155+
A mutable 3D affine transformation.
2156+
"""
2157+
2158+
def __init__(self, matrix=None, **kwargs):
2159+
"""
2160+
Initialize an Affine transform from a 4x4 numpy float array::
2161+
2162+
a d g j
2163+
b e h k
2164+
c f i l
2165+
0 0 0 1
2166+
2167+
If *matrix* is None, initialize with the identity transform.
2168+
"""
2169+
super().__init__(dims=3, **kwargs)
2170+
if matrix is None:
2171+
matrix = np.identity(4)
2172+
self._mtx = matrix.copy()
2173+
self._invalid = 0
2174+
2175+
_base_str = _make_str_method("_mtx")
2176+
2177+
def __str__(self):
2178+
return (self._base_str()
2179+
if (self._mtx != np.diag(np.diag(self._mtx))).any()
2180+
else f"Affine3D().scale("
2181+
f"{self._mtx[0, 0]}, "
2182+
f"{self._mtx[1, 1]}, "
2183+
f"{self._mtx[2, 2]})"
2184+
if self._mtx[0, 0] != self._mtx[1, 1] or
2185+
self._mtx[0, 0] != self._mtx[2, 2]
2186+
else f"Affine3D().scale({self._mtx[0, 0]})")
2187+
2188+
@staticmethod
2189+
def from_values(a, b, c, d, e, f, g, h, i, j, k, l):
2190+
"""
2191+
Create a new Affine2D instance from the given values::
2192+
2193+
a d g j
2194+
b e h k
2195+
c f i l
2196+
0 0 0 1
2197+
2198+
.
2199+
"""
2200+
return Affine3D(np.array([
2201+
a, d, g, j,
2202+
b, e, h, k,
2203+
c, f, i, l,
2204+
0.0, 0.0, 0.0, 1.0
2205+
], float).reshape((4, 4)))
2206+
2207+
def get_matrix(self):
2208+
"""
2209+
Get the underlying transformation matrix as a 4x4 array::
2210+
2211+
a d g j
2212+
b e h k
2213+
c f i l
2214+
0 0 0 1
2215+
2216+
.
2217+
"""
2218+
if self._invalid:
2219+
self._inverted = None
2220+
self._invalid = 0
2221+
return self._mtx
2222+
2223+
def set_matrix(self, mtx):
2224+
"""
2225+
Set the underlying transformation matrix from 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+
self._mtx = mtx
2235+
self.invalidate()
2236+
2237+
def set(self, other):
2238+
"""
2239+
Set this transformation from the frozen copy of another
2240+
`AffineImmutable` object with input and output dimension of 3.
2241+
"""
2242+
_api.check_isinstance(AffineImmutable, other=other)
2243+
if (other.input_dims != 3):
2244+
raise TypeError("Mismatch between dimensions of AffineImmutable"
2245+
"and Affine3D")
2246+
self._mtx = other.get_matrix()
2247+
self.invalidate()
2248+
2249+
def clear(self):
2250+
"""
2251+
Reset the underlying matrix to the identity transform.
2252+
"""
2253+
self._mtx = np.identity(4)
2254+
self.invalidate()
2255+
return self
2256+
2257+
def rotate(self, theta, dim=0):
2258+
"""
2259+
Add a rotation (in radians) to this transform in place, along
2260+
the dimension denoted by *dim*.
2261+
2262+
Returns *self*, so this method can easily be chained with more
2263+
calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2264+
and :meth:`scale`.
2265+
"""
2266+
if dim == 0:
2267+
return self.rotate_around_vector([1, 0, 0], theta)
2268+
elif dim == 1:
2269+
return self.rotate_around_vector([0, 1, 0], theta)
2270+
elif dim == 2:
2271+
return self.rotate_around_vector([0, 0, 1], theta)
2272+
2273+
self.invalidate()
2274+
return self
2275+
2276+
def rotate_deg(self, degrees, dim=0):
2277+
"""
2278+
Add a rotation (in degrees) to this transform in place, along
2279+
the dimension denoted by *dim*.
2280+
2281+
Returns *self*, so this method can easily be chained with more
2282+
calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2283+
and :meth:`scale`.
2284+
"""
2285+
return self.rotate(math.radians(degrees), dim)
2286+
2287+
def rotate_around(self, x, y, z, theta, dim=0):
2288+
"""
2289+
Add a rotation (in radians) around the point (x, y, z) in place,
2290+
along the dimension denoted by *dim*.
2291+
2292+
Returns *self*, so this method can easily be chained with more
2293+
calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2294+
and :meth:`scale`.
2295+
"""
2296+
return self.translate(-x, -y, -z).rotate(theta, dim).translate(x, y, z)
2297+
2298+
def rotate_deg_around(self, x, y, z, degrees, dim=0):
2299+
"""
2300+
Add a rotation (in degrees) around the point (x, y, z) in place,
2301+
along 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+
# Cast to float to avoid wraparound issues with uint8's
2308+
x, y = float(x), float(y)
2309+
return self.translate(-x, -y, -z).rotate_deg(degrees, dim).translate(x, y, z)
2310+
2311+
def rotate_around_vector(self, vector, theta):
2312+
"""
2313+
Add a rotation (in radians) around the vector (vx, vy, vz) in place.
2314+
2315+
Returns *self*, so this method can easily be chained with more
2316+
calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2317+
and :meth:`scale`.
2318+
"""
2319+
vx, vy, vz = vector / np.linalg.norm(vector)
2320+
s = np.sin(theta)
2321+
c = np.cos(theta)
2322+
t = 2*np.sin(theta/2)**2 # more numerically stable than t = 1-c
2323+
rot = [[t*vx*vx + c, t*vx*vy - vz*s, t*vx*vz + vy*s, 0],
2324+
[t*vy*vx + vz*s, t*vy*vy + c, t*vy*vz - vx*s, 0],
2325+
[t*vz*vx - vy*s, t*vz*vy + vx*s, t*vz*vz + c, 0],
2326+
[0, 0, 0, 1]]
2327+
np.matmul(rot, self._mtx, out=self._mtx)
2328+
return self
2329+
2330+
def rotate_deg_around_vector(self, vector, degrees):
2331+
"""
2332+
Add a rotation (in radians) around the vector (vx, vy, vz) in place.
2333+
2334+
Returns *self*, so this method can easily be chained with more
2335+
calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2336+
and :meth:`scale`.
2337+
"""
2338+
return self.rotate_around_vector(vector, math.radians(degrees))
2339+
2340+
def translate(self, tx, ty, tz):
2341+
"""
2342+
Add a translation in place.
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+
self._mtx[0, 3] += tx
2349+
self._mtx[1, 3] += ty
2350+
self._mtx[2, 3] += tz
2351+
self.invalidate()
2352+
return self
2353+
2354+
def scale(self, sx, sy=None, sz=None):
2355+
"""
2356+
Add a scale in place.
2357+
2358+
If a scale is not provided in the *y* or *z* directions, *sx*
2359+
will be applied for that direction.
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+
if sy is None:
2366+
sy = sx
2367+
2368+
if sz is None:
2369+
sz = sx
2370+
# explicit element-wise scaling is fastest
2371+
self._mtx[0, 0] *= sx
2372+
self._mtx[0, 1] *= sx
2373+
self._mtx[0, 2] *= sx
2374+
self._mtx[0, 3] *= sx
2375+
self._mtx[1, 0] *= sy
2376+
self._mtx[1, 1] *= sy
2377+
self._mtx[1, 2] *= sy
2378+
self._mtx[1, 3] *= sy
2379+
self._mtx[2, 0] *= sz
2380+
self._mtx[2, 1] *= sz
2381+
self._mtx[2, 2] *= sz
2382+
self._mtx[2, 3] *= sz
2383+
2384+
self.invalidate()
2385+
return self
2386+
2387+
def skew(self, xyShear, xzShear, yxShear, yzShear, zxShear, zyShear):
2388+
"""
2389+
Add a skew in place along for each plane in the 3rd dimension.
2390+
2391+
For example *zxShear* is the shear angle along the *zx* plane,
2392+
in radians.
2393+
2394+
Returns *self*, so this method can easily be chained with more
2395+
calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2396+
and :meth:`scale`.
2397+
"""
2398+
rxy = math.tan(xyShear)
2399+
rxz = math.tan(xzShear)
2400+
ryx = math.tan(yxShear)
2401+
ryz = math.tan(yzShear)
2402+
rzx = math.tan(zxShear)
2403+
rzy = math.tan(zyShear)
2404+
mtx = self._mtx
2405+
# Operating and assigning one scalar at a time is much faster.
2406+
(xx, xy, xz, x0), (yx, yy, yz, y0), (zx, zy, zz, z0), _ = mtx.tolist()
2407+
# mtx = [[1 rx 0], [ry 1 0], [0 0 1]] * mtx
2408+
2409+
mtx[0, 0] += (rxy * yx) + (rxz * zx)
2410+
mtx[0, 1] += (rxy * yy) + (rxz * zy)
2411+
mtx[0, 2] += (rxy * yz) + (rxz * zz)
2412+
mtx[0, 3] += (rxy * y0) + (rxz * z0)
2413+
mtx[1, 0] = (ryx * xx) + yx + (ryz * zx)
2414+
mtx[1, 1] = (ryx * xy) + yy + (ryz * zy)
2415+
mtx[1, 2] = (ryx * xz) + yz + (ryz * zz)
2416+
mtx[1, 3] = (ryx * x0) + y0 + (ryz * z0)
2417+
mtx[2, 0] = (rzx * xx) + (rzy * yx) + zx
2418+
mtx[2, 1] = (rzx * xy) + (rzy * yy) + zy
2419+
mtx[2, 2] = (rzx * xz) + (rzy * yz) + zz
2420+
mtx[2, 3] = (rzx * x0) + (rzy * y0) + z0
2421+
2422+
self.invalidate()
2423+
return self
2424+
2425+
def skew_deg(self, xyShear, xzShear, yxShear, yzShear, zxShear, zyShear):
2426+
"""
2427+
Add a skew in place along for each plane in the 3rd dimension.
2428+
2429+
For example *zxShear* is the shear angle along the *zx* plane,
2430+
in radians.
2431+
2432+
Returns *self*, so this method can easily be chained with more
2433+
calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2434+
and :meth:`scale`.
2435+
"""
2436+
return self.skew(
2437+
math.radians(xyShear),
2438+
math.radians(xzShear),
2439+
math.radians(yxShear),
2440+
math.radians(yzShear),
2441+
math.radians(zxShear),
2442+
math.radians(zyShear))
2443+
2444+
21502445
class IdentityTransform(AffineImmutable):
21512446
"""
21522447
A special class that does one thing, the identity transform, in a
@@ -2595,6 +2890,8 @@ def composite_transform_factory(a, b):
25952890
return a
25962891
elif isinstance(a, Affine2D) and isinstance(b, Affine2D):
25972892
return CompositeAffine(a, b)
2893+
elif isinstance(a, Affine3D) and isinstance(b, Affine3D):
2894+
return CompositeAffine(a, b)
25982895
return CompositeGenericTransform(a, b)
25992896

26002897

lib/matplotlib/transforms.pyi

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,33 @@ class Affine2D(AffineImmutable):
251251
def skew(self, xShear: float, yShear: float) -> Affine2D: ...
252252
def skew_deg(self, xShear: float, yShear: float) -> Affine2D: ...
253253

254+
class Affine3D(AffineImmutable):
255+
def __init__(self, matrix: ArrayLike | None = ..., **kwargs) -> None: ...
256+
@staticmethod
257+
def from_values(
258+
a: float, b: float, c: float, d: float, e: float, f: float, g: float,
259+
h: float, i: float, j: float, k: float, l: float
260+
) -> Affine3D: ...
261+
def set_matrix(self, mtx: ArrayLike) -> None: ...
262+
def clear(self) -> Affine3D: ...
263+
def rotate(self, theta: float, dim: int = ...) -> Affine3D: ...
264+
def rotate_deg(self, degrees: float, dim: int = ...) -> Affine3D: ...
265+
def rotate_around(self, x: float, y: float, z: float, theta: float, dim: int = ...
266+
) -> Affine3D: ...
267+
def rotate_deg_around(
268+
self, x: float, y: float, z: float, degrees: float, dim: int = ...
269+
) -> Affine3D: ...
270+
def rotate_around_vector(self, vector: ArrayLike, theta: float) -> Affine3D: ...
271+
def rotate_deg_around_vector(self, vector: ArrayLike, degrees: float
272+
) -> Affine3D: ...
273+
def translate(self, tx: float, ty: float, tz: float) -> Affine3D: ...
274+
def scale(self, sx: float, sy: float | None = ..., sz: float | None = ...
275+
) -> Affine3D: ...
276+
def skew(self, xyShear: float, xzShear: float, yxShear: float, yzShear: float,
277+
zxShear: float, zyShear: float) -> Affine3D: ...
278+
def skew_deg(self, xyShear: float, xzShear: float, yxShear: float, yzShear: float,
279+
zxShear: float, zyShear: float) -> Affine3D: ...
280+
254281
class IdentityTransform(AffineImmutable): ...
255282

256283
class _BlendedMixin:

0 commit comments

Comments
 (0)