# transformations.py# Modified for inclusion in the `trimesh` library# https://github.com/mikedh/trimesh# -----------------------------------------------------------------------## Copyright (c) 2006-2017, Christoph Gohlke# Copyright (c) 2006-2017, The Regents of the University of California# Produced at the Laboratory for Fluorescence Dynamics# All rights reserved.## Redistribution and use in source and binary forms, with or without# modification, are permitted provided that the following conditions are met:## * Redistributions of source code must retain the above copyright# notice, this list of conditions and the following disclaimer.# * Redistributions in binary form must reproduce the above copyright# notice, this list of conditions and the following disclaimer in the# documentation and/or other materials provided with the distribution.# * Neither the name of the copyright holders nor the names of any# contributors may be used to endorse or promote products derived# from this software without specific prior written permission.## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE# POSSIBILITY OF SUCH DAMAGE."""Homogeneous Transformation Matrices and Quaternions.A library for calculating 4x4 matrices for translating, rotating, reflecting,scaling, shearing, projecting, orthogonalizing, and superimposing arrays of3D homogeneous coordinates as well as for converting between rotation matrices,Euler angles, and quaternions. Also includes an Arcball control object andfunctions to decompose transformation matrices.:Author: `Christoph Gohlke <http://www.lfd.uci.edu/~gohlke/>`_:Organization: Laboratory for Fluorescence Dynamics, University of California, Irvine:Version: 2017.02.17Requirements------------* `CPython 2.7 or 3.4 <http://www.python.org>`_* `numpy 1.9 <http://www.np.org>`_* `Transformations.c 2015.03.19 <http://www.lfd.uci.edu/~gohlke/>`_ (recommended for speedup of some functions)Notes-----The API is not stable yet and is expected to change between revisions.This Python code is not optimized for speed. Refer to the transformations.cmodule for a faster implementation of some functions.Documentation in HTML format can be generated with epydoc.Matrices (M) can be inverted using np.linalg.inv(M), be concatenated usingnp.dot(M0, M1), or transform homogeneous coordinate arrays (v) usingnp.dot(M, v) for shape (4, *) column vectors, respectivelynp.dot(v, M.T) for shape (*, 4) row vectors ("array of points").This module follows the "column vectors on the right" and "row major storage"(C contiguous) conventions. The translation components are in the right columnof the transformation matrix, i.e. M[:3, 3].The transpose of the transformation matrices may have to be used to interfacewith other graphics systems, e.g. with OpenGL's glMultMatrixd(). See also [16].Calculations are carried out with np.float64 precision.Vector, point, quaternion, and matrix function arguments are expected to be"array like", i.e. tuple, list, or numpy arrays.Return types are numpy arrays unless specified otherwise.Angles are in radians unless specified otherwise.Quaternions w+ix+jy+kz are represented as [w, x, y, z].A triple of Euler angles can be applied/interpreted in 24 ways, which canbe specified using a 4 character string or encoded 4-tuple: *Axes 4-string*: e.g. 'sxyz' or 'ryxy' - first character : rotations are applied to 's'tatic or 'r'otating frame - remaining characters : successive rotation axis 'x', 'y', or 'z' *Axes 4-tuple*: e.g. (0, 0, 0, 0) or (1, 1, 1, 1) - inner axis: code of axis ('x':0, 'y':1, 'z':2) of rightmost matrix. - parity : even (0) if inner axis 'x' is followed by 'y', 'y' is followed by 'z', or 'z' is followed by 'x'. Otherwise odd (1). - repetition : first and last axis are same (1) or different (0). - frame : rotations are applied to static (0) or rotating (1) frame.Other Python packages and modules for 3D transformations and quaternions:* `Transforms3d <https://pypi.python.org/pypi/transforms3d>`_ includes most code of this module.* `Blender.mathutils <http://www.blender.org/api/blender_python_api>`_* `numpy-dtypes <https://github.com/numpy/numpy-dtypes>`_References----------(1) Matrices and transformations. Ronald Goldman. In "Graphics Gems I", pp 472-475. Morgan Kaufmann, 1990.(2) More matrices and transformations: shear and pseudo-perspective. Ronald Goldman. In "Graphics Gems II", pp 320-323. Morgan Kaufmann, 1991.(3) Decomposing a matrix into simple transformations. Spencer Thomas. In "Graphics Gems II", pp 320-323. Morgan Kaufmann, 1991.(4) Recovering the data from the transformation matrix. Ronald Goldman. In "Graphics Gems II", pp 324-331. Morgan Kaufmann, 1991.(5) Euler angle conversion. Ken Shoemake. In "Graphics Gems IV", pp 222-229. Morgan Kaufmann, 1994.(6) Arcball rotation control. Ken Shoemake. In "Graphics Gems IV", pp 175-192. Morgan Kaufmann, 1994.(7) Representing attitude: Euler angles, unit quaternions, and rotation vectors. James Diebel. 2006.(8) A discussion of the solution for the best rotation to relate two sets of vectors. W Kabsch. Acta Cryst. 1978. A34, 827-828.(9) Closed-form solution of absolute orientation using unit quaternions. BKP Horn. J Opt Soc Am A. 1987. 4(4):629-642.(10) Quaternions. Ken Shoemake. http://www.sfu.ca/~jwa3/cmpt461/files/quatut.pdf(11) From quaternion to matrix and back. JMP van Waveren. 2005. http://www.intel.com/cd/ids/developer/asmo-na/eng/293748.htm(12) Uniform random rotations. Ken Shoemake. In "Graphics Gems III", pp 124-132. Morgan Kaufmann, 1992.(13) Quaternion in molecular modeling. CFF Karney. J Mol Graph Mod, 25(5):595-604(14) New method for extracting the quaternion from a rotation matrix. Itzhack Y Bar-Itzhack, J Guid Contr Dynam. 2000. 23(6): 1085-1087.(15) Multiple View Geometry in Computer Vision. Hartley and Zissermann. Cambridge University Press; 2nd Ed. 2004. Chapter 4, Algorithm 4.7, p 130.(16) Column Vectors vs. Row Vectors. http://steve.hollasch.net/cgindex/math/matrix/column-vec.htmlExamples-------->>> alpha, beta, gamma = 0.123, -1.234, 2.345>>> origin, xaxis, yaxis, zaxis = [0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]>>> I = identity_matrix()>>> Rx = rotation_matrix(alpha, xaxis)>>> Ry = rotation_matrix(beta, yaxis)>>> Rz = rotation_matrix(gamma, zaxis)>>> R = concatenate_matrices(Rx, Ry, Rz)>>> euler = euler_from_matrix(R, 'rxyz')>>> np.allclose([alpha, beta, gamma], euler)True>>> Re = euler_matrix(alpha, beta, gamma, 'rxyz')>>> is_same_transform(R, Re)True>>> al, be, ga = euler_from_matrix(Re, 'rxyz')>>> is_same_transform(Re, euler_matrix(al, be, ga, 'rxyz'))True>>> qx = quaternion_about_axis(alpha, xaxis)>>> qy = quaternion_about_axis(beta, yaxis)>>> qz = quaternion_about_axis(gamma, zaxis)>>> q = quaternion_multiply(qx, qy)>>> q = quaternion_multiply(q, qz)>>> Rq = quaternion_matrix(q)>>> is_same_transform(R, Rq)True>>> S = scale_matrix(1.23, origin)>>> T = translation_matrix([1, 2, 3])>>> Z = shear_matrix(beta, xaxis, origin, zaxis)>>> R = random_rotation_matrix(np.random.rand(3))>>> M = concatenate_matrices(T, R, Z, S)>>> scale, shear, angles, trans, persp = decompose_matrix(M)>>> np.allclose(scale, 1.23)True>>> np.allclose(trans, [1, 2, 3])True>>> np.allclose(shear, [0, np.tan(beta), 0])True>>> is_same_transform(R, euler_matrix(axes='sxyz', *angles))True>>> M1 = compose_matrix(scale, shear, angles, trans, persp)>>> is_same_transform(M, M1)True>>> v0, v1 = random_vector(3), random_vector(3)>>> M = rotation_matrix(angle_between_vectors(v0, v1), vector_product(v0, v1))>>> v2 = np.dot(v0, M[:3,:3].T)>>> np.allclose(unit_vector(v1), unit_vector(v2))True"""importnumpyasnpfromnumpy.typingimportArrayLike,NDArray_IDENTITY=np.eye(4)_IDENTITY.flags["WRITEABLE"]=Falsedefidentity_matrix():"""Return 4x4 identity/unit matrix. >>> I = identity_matrix() >>> np.allclose(I, np.dot(I, I)) True >>> float(np.sum(I)), float(np.trace(I)) (4.0, 4.0) >>> np.allclose(I, np.identity(4)) True """returnnp.identity(4)deftranslation_matrix(direction):""" Return matrix to translate by direction vector. >>> v = np.random.random(3) - 0.5 >>> np.allclose(v, translation_matrix(v)[:3, 3]) True """# are we 2D or 3Ddim=len(direction)# start with identity matrixifany("sympy"instr(type(v))forvindirection):# if we have been passed input values as sympy.SymbolfromsympyimporteyeM=eye(dim+1)else:M=np.eye(dim+1)# apply the offsetM[:dim,dim]=direction[:dim]returnMdeftranslation_from_matrix(matrix):"""Return translation vector from translation matrix. >>> v0 = np.random.random(3) - 0.5 >>> v1 = translation_from_matrix(translation_matrix(v0)) >>> np.allclose(v0, v1) True """returnnp.asarray(matrix)[:3,3].copy()defreflection_matrix(point,normal):"""Return matrix to mirror at plane defined by point and normal vector. >>> v0 = np.random.random(4) - 0.5 >>> v0[3] = 1. >>> v1 = np.random.random(3) - 0.5 >>> R = reflection_matrix(v0, v1) >>> np.allclose(2, np.trace(R)) True >>> np.allclose(v0, np.dot(R, v0)) True >>> v2 = v0.copy() >>> v2[:3] += v1 >>> v3 = v0.copy() >>> v2[:3] -= v1 >>> np.allclose(v2, np.dot(R, v3)) True """normal=unit_vector(normal[:3])M=np.identity(4)M[:3,:3]-=2.0*np.outer(normal,normal)M[:3,3]=(2.0*np.dot(point[:3],normal))*normalreturnMdefreflection_from_matrix(matrix):"""Return mirror plane point and normal vector from reflection matrix. >>> v0 = np.random.random(3) - 0.5 >>> v1 = np.random.random(3) - 0.5 >>> M0 = reflection_matrix(v0, v1) >>> point, normal = reflection_from_matrix(M0) >>> M1 = reflection_matrix(point, normal) >>> is_same_transform(M0, M1) True """M=np.asarray(matrix,dtype=np.float64)# normal: unit eigenvector corresponding to eigenvalue -1w,V=np.linalg.eig(M[:3,:3])i=np.where(abs(np.real(w)+1.0)<1e-8)[0]ifnotlen(i):raiseValueError("no unit eigenvector corresponding to eigenvalue -1")normal=np.real(V[:,i[0]]).squeeze()# point: any unit eigenvector corresponding to eigenvalue 1w,V=np.linalg.eig(M)i=np.where(abs(np.real(w)-1.0)<1e-8)[0]ifnotlen(i):raiseValueError("no unit eigenvector corresponding to eigenvalue 1")point=np.real(V[:,i[-1]]).squeeze()point/=point[3]returnpoint,normaldefrotation_matrix(angle,direction,point=None):""" Return matrix to rotate about axis defined by point and direction. Parameters ------------- angle : float, or sympy.Symbol Angle, in radians or symbolic angle direction : (3,) float Any vector along rotation axis point : (3, ) float, or None Origin point of rotation axis Returns ------------- matrix : (4, 4) float, or (4, 4) sympy.Matrix Homogeneous transformation matrix Examples ------------- >>> R = rotation_matrix(np.pi/2, [0, 0, 1], [1, 0, 0]) >>> np.allclose(np.dot(R, [0, 0, 0, 1]), [1, -1, 0, 1]) True >>> angle = (random.random() - 0.5) * (2*np.pi) >>> direc = np.random.random(3) - 0.5 >>> point = np.random.random(3) - 0.5 >>> R0 = rotation_matrix(angle, direc, point) >>> R1 = rotation_matrix(angle-2*np.pi, direc, point) >>> is_same_transform(R0, R1) True >>> R0 = rotation_matrix(angle, direc, point) >>> R1 = rotation_matrix(-angle, -direc, point) >>> is_same_transform(R0, R1) True >>> I = np.identity(4, np.float64) >>> np.allclose(I, rotation_matrix(np.pi*2, direc)) True >>> np.allclose(2, np.trace(rotation_matrix(np.pi/2,direc,point))) True """if"sympy"instr(type(angle)):# special case sympy symbolic anglesimportsympyasspsymbolic=Truesina=sp.sin(angle)cosa=sp.cos(angle)else:symbolic=Falsesina=np.sin(angle)cosa=np.cos(angle)direction=unit_vector(direction[:3])# rotation matrix around unit vectorM=np.diag([cosa,cosa,cosa,1.0])M[:3,:3]+=np.outer(direction,direction)*(1.0-cosa)direction=direction*sinaM[:3,:3]+=np.array([[0.0,-direction[2],direction[1]],[direction[2],0.0,-direction[0]],[-direction[1],direction[0],0.0],])# if point is specified, rotation is not around originifpointisnotNone:point=np.asarray(point[:3],dtype=np.float64)M[:3,3]=point-np.dot(M[:3,:3],point)# return symbolic angles as sympy Matrix objectsifsymbolic:returnsp.Matrix(M)returnMdefrotation_from_matrix(matrix):"""Return rotation angle and axis from rotation matrix. >>> angle = (random.random() - 0.5) * (2*np.pi) >>> direc = np.random.random(3) - 0.5 >>> point = np.random.random(3) - 0.5 >>> R0 = rotation_matrix(angle, direc, point) >>> angle, direc, point = rotation_from_matrix(R0) >>> R1 = rotation_matrix(angle, direc, point) >>> is_same_transform(R0, R1) True """R=np.asarray(matrix,dtype=np.float64)R33=R[:3,:3]# direction: unit eigenvector of R33 corresponding to eigenvalue of 1w,W=np.linalg.eig(R33.T)i=np.where(abs(np.real(w)-1.0)<1e-8)[0]ifnotlen(i):raiseValueError("no unit eigenvector corresponding to eigenvalue 1")direction=np.real(W[:,i[-1]]).squeeze()# point: unit eigenvector of R33 corresponding to eigenvalue of 1w,Q=np.linalg.eig(R)i=np.where(abs(np.real(w)-1.0)<1e-8)[0]ifnotlen(i):raiseValueError("no unit eigenvector corresponding to eigenvalue 1")point=np.real(Q[:,i[-1]]).squeeze()point/=point[3]# rotation angle depending on directioncosa=(np.trace(R33)-1.0)/2.0ifabs(direction[2])>1e-8:sina=(R[1,0]+(cosa-1.0)*direction[0]*direction[1])/direction[2]elifabs(direction[1])>1e-8:sina=(R[0,2]+(cosa-1.0)*direction[0]*direction[2])/direction[1]else:sina=(R[2,1]+(cosa-1.0)*direction[1]*direction[2])/direction[0]angle=np.arctan2(sina,cosa)returnangle,direction,pointdefscale_matrix(factor,origin=None,direction=None):"""Return matrix to scale by factor around origin in direction. Use factor -1 for point symmetry. >>> v = (np.random.rand(4, 5) - 0.5) * 20 >>> v[3] = 1 >>> S = scale_matrix(-1.234) >>> np.allclose(np.dot(S, v)[:3], -1.234*v[:3]) True >>> factor = random.random() * 10 - 5 >>> origin = np.random.random(3) - 0.5 >>> direct = np.random.random(3) - 0.5 >>> S = scale_matrix(factor, origin) >>> S = scale_matrix(factor, origin, direct) """ifdirectionisNone:# uniform scalingM=np.diag([factor,factor,factor,1.0])iforiginisnotNone:M[:3,3]=origin[:3]M[:3,3]*=1.0-factorelse:# nonuniform scalingdirection=unit_vector(direction[:3])factor=1.0-factorM=np.identity(4)M[:3,:3]-=factor*np.outer(direction,direction)iforiginisnotNone:M[:3,3]=(factor*np.dot(origin[:3],direction))*directionreturnMdefscale_from_matrix(matrix):"""Return scaling factor, origin and direction from scaling matrix. >>> factor = random.random() * 10 - 5 >>> origin = np.random.random(3) - 0.5 >>> direct = np.random.random(3) - 0.5 >>> S0 = scale_matrix(factor, origin) >>> factor, origin, direction = scale_from_matrix(S0) >>> S1 = scale_matrix(factor, origin, direction) >>> is_same_transform(S0, S1) True >>> S0 = scale_matrix(factor, origin, direct) >>> factor, origin, direction = scale_from_matrix(S0) >>> S1 = scale_matrix(factor, origin, direction) >>> is_same_transform(S0, S1) True """M=np.asarray(matrix,dtype=np.float64)M33=M[:3,:3]factor=np.trace(M33)-2.0try:# direction: unit eigenvector corresponding to eigenvalue factorw,V=np.linalg.eig(M33)i=np.where(abs(np.real(w)-factor)<1e-8)[0][0]direction=np.real(V[:,i]).squeeze()direction/=vector_norm(direction)exceptIndexError:# uniform scalingfactor=(factor+2.0)/3.0direction=None# origin: any eigenvector corresponding to eigenvalue 1w,V=np.linalg.eig(M)i=np.where(abs(np.real(w)-1.0)<1e-8)[0]ifnotlen(i):raiseValueError("no eigenvector corresponding to eigenvalue 1")origin=np.real(V[:,i[-1]]).squeeze()origin/=origin[3]returnfactor,origin,directiondefprojection_matrix(point,normal,direction=None,perspective=None,pseudo=False):"""Return matrix to project onto plane defined by point and normal. Using either perspective point, projection direction, or none of both. If pseudo is True, perspective projections will preserve relative depth such that Perspective = dot(Orthogonal, PseudoPerspective). >>> P = projection_matrix([0, 0, 0], [1, 0, 0]) >>> np.allclose(P[1:, 1:], np.identity(4)[1:, 1:]) True >>> point = np.random.random(3) - 0.5 >>> normal = np.random.random(3) - 0.5 >>> direct = np.random.random(3) - 0.5 >>> persp = np.random.random(3) - 0.5 >>> P0 = projection_matrix(point, normal) >>> P1 = projection_matrix(point, normal, direction=direct) >>> P2 = projection_matrix(point, normal, perspective=persp) >>> P3 = projection_matrix(point, normal, perspective=persp, pseudo=True) >>> is_same_transform(P2, np.dot(P0, P3)) True >>> P = projection_matrix([3, 0, 0], [1, 1, 0], [1, 0, 0]) >>> v0 = (np.random.rand(4, 5) - 0.5) * 20 >>> v0[3] = 1 >>> v1 = np.dot(P, v0) >>> np.allclose(v1[1], v0[1]) True >>> np.allclose(v1[0], 3-v1[1]) True """M=np.identity(4)point=np.asarray(point[:3],dtype=np.float64)normal=unit_vector(normal[:3])ifperspectiveisnotNone:# perspective projectionperspective=np.asarray(perspective[:3],dtype=np.float64)M[0,0]=M[1,1]=M[2,2]=np.dot(perspective-point,normal)M[:3,:3]-=np.outer(perspective,normal)ifpseudo:# preserve relative depthM[:3,:3]-=np.outer(normal,normal)M[:3,3]=np.dot(point,normal)*(perspective+normal)else:M[:3,3]=np.dot(point,normal)*perspectiveM[3,:3]=-normalM[3,3]=np.dot(perspective,normal)elifdirectionisnotNone:# parallel projectiondirection=np.asarray(direction[:3],dtype=np.float64)scale=np.dot(direction,normal)M[:3,:3]-=np.outer(direction,normal)/scaleM[:3,3]=direction*(np.dot(point,normal)/scale)else:# orthogonal projectionM[:3,:3]-=np.outer(normal,normal)M[:3,3]=np.dot(point,normal)*normalreturnMdefprojection_from_matrix(matrix,pseudo=False):"""Return projection plane and perspective point from projection matrix. Return values are same as arguments for projection_matrix function: point, normal, direction, perspective, and pseudo. >>> point = np.random.random(3) - 0.5 >>> normal = np.random.random(3) - 0.5 >>> direct = np.random.random(3) - 0.5 >>> persp = np.random.random(3) - 0.5 >>> P0 = projection_matrix(point, normal) >>> result = projection_from_matrix(P0) >>> P1 = projection_matrix(*result) >>> is_same_transform(P0, P1) True >>> P0 = projection_matrix(point, normal, direct) >>> result = projection_from_matrix(P0) >>> P1 = projection_matrix(*result) >>> is_same_transform(P0, P1) True >>> P0 = projection_matrix(point, normal, perspective=persp, pseudo=False) >>> result = projection_from_matrix(P0, pseudo=False) >>> P1 = projection_matrix(*result) >>> is_same_transform(P0, P1) True >>> P0 = projection_matrix(point, normal, perspective=persp, pseudo=True) >>> result = projection_from_matrix(P0, pseudo=True) >>> P1 = projection_matrix(*result) >>> is_same_transform(P0, P1) True """M=np.asarray(matrix,dtype=np.float64)M33=M[:3,:3]w,V=np.linalg.eig(M)i=np.where(abs(np.real(w)-1.0)<1e-8)[0]ifnotpseudoandlen(i):# point: any eigenvector corresponding to eigenvalue 1point=np.real(V[:,i[-1]]).squeeze()point/=point[3]# direction: unit eigenvector corresponding to eigenvalue 0w,V=np.linalg.eig(M33)i=np.where(abs(np.real(w))<1e-8)[0]ifnotlen(i):raiseValueError("no eigenvector corresponding to eigenvalue 0")direction=np.real(V[:,i[0]]).squeeze()direction/=vector_norm(direction)# normal: unit eigenvector of M33.T corresponding to eigenvalue 0w,V=np.linalg.eig(M33.T)i=np.where(abs(np.real(w))<1e-8)[0]iflen(i):# parallel projectionnormal=np.real(V[:,i[0]]).squeeze()normal/=vector_norm(normal)returnpoint,normal,direction,None,Falseelse:# orthogonal projection, where normal equals direction vectorreturnpoint,direction,None,None,Falseelse:# perspective projectioni=np.where(abs(np.real(w))>1e-8)[0]ifnotlen(i):raiseValueError("no eigenvector not corresponding to eigenvalue 0")point=np.real(V[:,i[-1]]).squeeze()point/=point[3]normal=-M[3,:3]perspective=M[:3,3]/np.dot(point[:3],normal)ifpseudo:perspective-=normalreturnpoint,normal,None,perspective,pseudodefclip_matrix(left,right,bottom,top,near,far,perspective=False):"""Return matrix to obtain normalized device coordinates from frustum. The frustum bounds are axis-aligned along x (left, right), y (bottom, top) and z (near, far). Normalized device coordinates are in range [-1, 1] if coordinates are inside the frustum. If perspective is True the frustum is a truncated pyramid with the perspective point at origin and direction along z axis, otherwise an orthographic canonical view volume (a box). Homogeneous coordinates transformed by the perspective clip matrix need to be dehomogenized (divided by w coordinate). >>> frustum = np.random.rand(6) >>> frustum[1] += frustum[0] >>> frustum[3] += frustum[2] >>> frustum[5] += frustum[4] >>> M = clip_matrix(perspective=False, *frustum) >>> a = np.dot(M, [frustum[0], frustum[2], frustum[4], 1]) >>> np.allclose(a, [-1., -1., -1., 1.]) True >>> b = np.dot(M, [frustum[1], frustum[3], frustum[5], 1]) >>> np.allclose(b, [ 1., 1., 1., 1.]) True >>> M = clip_matrix(perspective=True, *frustum) >>> v = np.dot(M, [frustum[0], frustum[2], frustum[4], 1]) >>> c = v / v[3] >>> np.allclose(c, [-1., -1., -1., 1.]) True >>> v = np.dot(M, [frustum[1], frustum[3], frustum[4], 1]) >>> d = v / v[3] >>> np.allclose(d, [ 1., 1., -1., 1.]) True """ifleft>=rightorbottom>=topornear>=far:raiseValueError("invalid frustum")ifperspective:ifnear<=_EPS:raiseValueError("invalid frustum: near <= 0")t=2.0*nearM=[[t/(left-right),0.0,(right+left)/(right-left),0.0],[0.0,t/(bottom-top),(top+bottom)/(top-bottom),0.0],[0.0,0.0,(far+near)/(near-far),t*far/(far-near)],[0.0,0.0,-1.0,0.0],]else:M=[[2.0/(right-left),0.0,0.0,(right+left)/(left-right)],[0.0,2.0/(top-bottom),0.0,(top+bottom)/(bottom-top)],[0.0,0.0,2.0/(far-near),(far+near)/(near-far)],[0.0,0.0,0.0,1.0],]returnnp.array(M)defshear_matrix(angle,direction,point,normal):"""Return matrix to shear by angle along direction vector on shear plane. The shear plane is defined by a point and normal vector. The direction vector must be orthogonal to the plane's normal vector. A point P is transformed by the shear matrix into P" such that the vector P-P" is parallel to the direction vector and its extent is given by the angle of P-P'-P", where P' is the orthogonal projection of P onto the shear plane. >>> angle = (random.random() - 0.5) * 4*np.pi >>> direct = np.random.random(3) - 0.5 >>> point = np.random.random(3) - 0.5 >>> normal = np.cross(direct, np.random.random(3)) >>> S = shear_matrix(angle, direct, point, normal) >>> np.allclose(1, np.linalg.det(S)) True """normal=unit_vector(normal[:3])direction=unit_vector(direction[:3])ifabs(np.dot(normal,direction))>1e-6:raiseValueError("direction and normal vectors are not orthogonal")angle=np.tan(angle)M=np.identity(4)M[:3,:3]+=angle*np.outer(direction,normal)M[:3,3]=-angle*np.dot(point[:3],normal)*directionreturnMdefshear_from_matrix(matrix):"""Return shear angle, direction and plane from shear matrix. >>> angle = np.pi / 2.0 >>> direct = [0.0, 1.0, 0.0] >>> point = [0.0, 0.0, 0.0] >>> normal = np.cross(direct, np.roll(direct,1)) >>> S0 = shear_matrix(angle, direct, point, normal) >>> angle, direct, point, normal = shear_from_matrix(S0) >>> S1 = shear_matrix(angle, direct, point, normal) >>> is_same_transform(S0, S1) True """M=np.asarray(matrix,dtype=np.float64)M33=M[:3,:3]# normal: cross independent eigenvectors corresponding to the eigenvalue 1w,V=np.linalg.eig(M33)i=np.where(abs(np.real(w)-1.0)<1e-4)[0]iflen(i)<2:raiseValueError(f"no two linear independent eigenvectors found {w}")V=np.real(V[:,i]).squeeze().Tlenorm=-1.0fori0,i1in((0,1),(0,2),(1,2)):n=np.cross(V[i0],V[i1])w=vector_norm(n)ifw>lenorm:lenorm=wnormal=nnormal/=lenorm# direction and angledirection=np.dot(M33-np.identity(3),normal)angle=vector_norm(direction)direction/=angleangle=np.arctan(angle)# point: eigenvector corresponding to eigenvalue 1w,V=np.linalg.eig(M)i=np.where(abs(np.real(w)-1.0)<1e-8)[0]ifnotlen(i):raiseValueError("no eigenvector corresponding to eigenvalue 1")point=np.real(V[:,i[-1]]).squeeze()point/=point[3]returnangle,direction,point,normaldefdecompose_matrix(matrix):"""Return sequence of transformations from transformation matrix. matrix : array_like Non-degenerative homogeneous transformation matrix Return tuple of: scale : vector of 3 scaling factors shear : list of shear factors for x-y, x-z, y-z axes angles : list of Euler angles about static x, y, z axes translate : translation vector along x, y, z axes perspective : perspective partition of matrix Raise ValueError if matrix is of wrong type or degenerative. >>> T0 = translation_matrix([1, 2, 3]) >>> scale, shear, angles, trans, persp = decompose_matrix(T0) >>> T1 = translation_matrix(trans) >>> np.allclose(T0, T1) True >>> S = scale_matrix(0.123) >>> scale, shear, angles, trans, persp = decompose_matrix(S) >>> bool(np.isclose(scale[0], 0.123)) True >>> R0 = euler_matrix(1, 2, 3) >>> scale, shear, angles, trans, persp = decompose_matrix(R0) >>> R1 = euler_matrix(*angles) >>> np.allclose(R0, R1) True """M=np.array(matrix,dtype=np.float64).Tifabs(M[3,3])<_EPS:raiseValueError("M[3, 3] is zero")M/=M[3,3]P=M.copy()P[:,3]=0.0,0.0,0.0,1.0ifnotnp.linalg.det(P):raiseValueError("matrix is singular")scale=np.zeros((3,))shear=[0.0,0.0,0.0]angles=[0.0,0.0,0.0]ifany(abs(M[:3,3])>_EPS):perspective=np.dot(M[:,3],np.linalg.inv(P.T))M[:,3]=0.0,0.0,0.0,1.0else:perspective=np.array([0.0,0.0,0.0,1.0])translate=M[3,:3].copy()M[3,:3]=0.0row=M[:3,:3].copy()scale[0]=vector_norm(row[0])row[0]/=scale[0]shear[0]=np.dot(row[0],row[1])row[1]-=row[0]*shear[0]scale[1]=vector_norm(row[1])row[1]/=scale[1]shear[0]/=scale[1]shear[1]=np.dot(row[0],row[2])row[2]-=row[0]*shear[1]shear[2]=np.dot(row[1],row[2])row[2]-=row[1]*shear[2]scale[2]=vector_norm(row[2])row[2]/=scale[2]shear[1:]/=scale[2]ifnp.dot(row[0],np.cross(row[1],row[2]))<0:np.negative(scale,scale)np.negative(row,row)angles[1]=np.arcsin(-row[0,2])ifnp.cos(angles[1]):angles[0]=np.arctan2(row[1,2],row[2,2])angles[2]=np.arctan2(row[0,1],row[0,0])else:angles[0]=np.arctan2(-row[2,1],row[1,1])angles[2]=0.0returnscale,shear,angles,translate,perspectivedefcompose_matrix(scale=None,shear=None,angles=None,translate=None,perspective=None):"""Return transformation matrix from sequence of transformations. This is the inverse of the decompose_matrix function. Sequence of transformations: scale : vector of 3 scaling factors shear : list of shear factors for x-y, x-z, y-z axes angles : list of Euler angles about static x, y, z axes translate : translation vector along x, y, z axes perspective : perspective partition of matrix >>> scale = np.random.random(3) - 0.5 >>> shear = np.random.random(3) - 0.5 >>> angles = (np.random.random(3) - 0.5) * (2*np.pi) >>> trans = np.random.random(3) - 0.5 >>> persp = np.random.random(4) - 0.5 >>> M0 = compose_matrix(scale, shear, angles, trans, persp) >>> result = decompose_matrix(M0) >>> M1 = compose_matrix(*result) >>> is_same_transform(M0, M1) True """M=np.identity(4)ifperspectiveisnotNone:P=np.identity(4)P[3,:]=perspective[:4]M=np.dot(M,P)iftranslateisnotNone:T=np.identity(4)T[:3,3]=translate[:3]M=np.dot(M,T)ifanglesisnotNone:R=euler_matrix(angles[0],angles[1],angles[2],"sxyz")M=np.dot(M,R)ifshearisnotNone:Z=np.identity(4)Z[1,2]=shear[2]Z[0,2]=shear[1]Z[0,1]=shear[0]M=np.dot(M,Z)ifscaleisnotNone:S=np.identity(4)S[0,0]=scale[0]S[1,1]=scale[1]S[2,2]=scale[2]M=np.dot(M,S)M/=M[3,3]returnMdeforthogonalization_matrix(lengths,angles):"""Return orthogonalization matrix for crystallographic cell coordinates. Angles are expected in degrees. The de-orthogonalization matrix is the inverse. >>> O = orthogonalization_matrix([10, 10, 10], [90, 90, 90]) >>> np.allclose(O[:3, :3], np.identity(3, float) * 10) True >>> O = orthogonalization_matrix([9.8, 12.0, 15.5], [87.2, 80.7, 69.7]) >>> np.allclose(np.sum(O), 43.063229) True """a,b,c=lengthsangles=np.radians(angles)sina,sinb,_=np.sin(angles)cosa,cosb,cosg=np.cos(angles)co=(cosa*cosb-cosg)/(sina*sinb)returnnp.array([[a*sinb*np.sqrt(1.0-co*co),0.0,0.0,0.0],[-a*sinb*co,b*sina,0.0,0.0],[a*cosb,b*cosa,c,0.0],[0.0,0.0,0.0,1.0],])defaffine_matrix_from_points(v0,v1,shear=True,scale=True,usesvd=True):"""Return affine transform matrix to register two point sets. v0 and v1 are shape (ndims, *) arrays of at least ndims non-homogeneous coordinates, where ndims is the dimensionality of the coordinate space. If shear is False, a similarity transformation matrix is returned. If also scale is False, a rigid/Euclidean transformation matrix is returned. By default the algorithm by Hartley and Zissermann [15] is used. If usesvd is True, similarity and Euclidean transformation matrices are calculated by minimizing the weighted sum of squared deviations (RMSD) according to the algorithm by Kabsch [8]. Otherwise, and if ndims is 3, the quaternion based algorithm by Horn [9] is used, which is slower when using this Python implementation. The returned matrix performs rotation, translation and uniform scaling (if specified). >>> v0 = [[0, 1031, 1031, 0], [0, 0, 1600, 1600]] >>> v1 = [[675, 826, 826, 677], [55, 52, 281, 277]] >>> mat = affine_matrix_from_points(v0, v1) >>> T = translation_matrix(np.random.random(3)-0.5) >>> R = random_rotation_matrix(np.random.random(3)) >>> S = scale_matrix(random.random()) >>> M = concatenate_matrices(T, R, S) >>> v0 = (np.random.rand(4, 100) - 0.5) * 20 >>> v0[3] = 1 >>> v1 = np.dot(M, v0) >>> v0[:3] += np.random.normal(0, 1e-8, 300).reshape(3, -1) >>> M = affine_matrix_from_points(v0[:3], v1[:3]) >>> check = np.allclose(v1, np.dot(M, v0)) More examples in superimposition_matrix() """v0=np.array(v0,dtype=np.float64)v1=np.array(v1,dtype=np.float64)ndims=v0.shape[0]ifndims<2orv0.shape[1]<ndimsorv0.shape!=v1.shape:raiseValueError("input arrays are of wrong shape or type")# move centroids to origint0=-np.mean(v0,axis=1)M0=np.identity(ndims+1)M0[:ndims,ndims]=t0v0+=t0.reshape(ndims,1)t1=-np.mean(v1,axis=1)M1=np.identity(ndims+1)M1[:ndims,ndims]=t1v1+=t1.reshape(ndims,1)ifshear:# Affine transformationA=np.concatenate((v0,v1),axis=0)u,s,vh=np.linalg.svd(A.T)vh=vh[:ndims].TB=vh[:ndims]C=vh[ndims:2*ndims]t=np.dot(C,np.linalg.pinv(B))t=np.concatenate((t,np.zeros((ndims,1))),axis=1)M=np.vstack((t,((0.0,)*ndims)+(1.0,)))elifusesvdorndims!=3:# Rigid transformation via SVD of covariance matrixu,s,vh=np.linalg.svd(np.dot(v1,v0.T))# rotation matrix from SVD orthonormal basesR=np.dot(u,vh)ifnp.linalg.det(R)<0.0:# R does not constitute right handed systemR-=np.outer(u[:,ndims-1],vh[ndims-1,:]*2.0)s[-1]*=-1.0# homogeneous transformation matrixM=np.identity(ndims+1)M[:ndims,:ndims]=Relse:# Rigid transformation matrix via quaternion# compute symmetric matrix Nxx,yy,zz=np.sum(v0*v1,axis=1)xy,yz,zx=np.sum(v0*np.roll(v1,-1,axis=0),axis=1)xz,yx,zy=np.sum(v0*np.roll(v1,-2,axis=0),axis=1)N=[[xx+yy+zz,0.0,0.0,0.0],[yz-zy,xx-yy-zz,0.0,0.0],[zx-xz,xy+yx,yy-xx-zz,0.0],[xy-yx,zx+xz,yz+zy,zz-xx-yy],]# quaternion: eigenvector corresponding to most positive eigenvaluew,V=np.linalg.eigh(N)q=V[:,np.argmax(w)]q/=vector_norm(q)# unit quaternion# homogeneous transformation matrixM=quaternion_matrix(q)ifscaleandnotshear:# Affine transformation; scale is ratio of RMS deviations from centroidv0*=v0v1*=v1M[:ndims,:ndims]*=np.sqrt(np.sum(v1)/np.sum(v0))# move centroids backM=np.dot(np.linalg.inv(M1),np.dot(M,M0))M/=M[ndims,ndims]returnMdefsuperimposition_matrix(v0,v1,scale=False,usesvd=True):"""Return matrix to transform given 3D point set into second point set. v0 and v1 are shape (3, *) or (4, *) arrays of at least 3 points. The parameters scale and usesvd are explained in the more general affine_matrix_from_points function. The returned matrix is a similarity or Euclidean transformation matrix. This function has a fast C implementation in transformations.c. >>> v0 = np.random.rand(3, 10) >>> M = superimposition_matrix(v0, v0) >>> np.allclose(M, np.identity(4)) True >>> R = random_rotation_matrix(np.random.random(3)) >>> v0 = [[1,0,0], [0,1,0], [0,0,1], [1,1,1]] >>> v1 = np.dot(R, v0) >>> M = superimposition_matrix(v0, v1) >>> np.allclose(v1, np.dot(M, v0)) True >>> v0 = (np.random.rand(4, 100) - 0.5) * 20 >>> v0[3] = 1 >>> v1 = np.dot(R, v0) >>> M = superimposition_matrix(v0, v1) >>> np.allclose(v1, np.dot(M, v0)) True >>> S = scale_matrix(random.random()) >>> T = translation_matrix(np.random.random(3)-0.5) >>> M = concatenate_matrices(T, R, S) >>> v1 = np.dot(M, v0) >>> v0[:3] += np.random.normal(0, 1e-9, 300).reshape(3, -1) >>> M = superimposition_matrix(v0, v1, scale=True) >>> np.allclose(v1, np.dot(M, v0)) True >>> M = superimposition_matrix(v0, v1, scale=True, usesvd=False) >>> np.allclose(v1, np.dot(M, v0)) True >>> v = np.zeros((4, 100, 3)) >>> v[:, :, 0] = v0 >>> M = superimposition_matrix(v0, v1, scale=True, usesvd=False) >>> np.allclose(v1, np.dot(M, v[:, :, 0])) True """v0=np.asarray(v0,dtype=np.float64)[:3]v1=np.asarray(v1,dtype=np.float64)[:3]returnaffine_matrix_from_points(v0,v1,shear=False,scale=scale,usesvd=usesvd)defeuler_matrix(ai,aj,ak,axes="sxyz"):"""Return homogeneous rotation matrix from Euler angles and axis sequence. ai, aj, ak : Euler's roll, pitch and yaw angles axes : One of 24 axis sequences as string or encoded tuple >>> R = euler_matrix(1, 2, 3, 'syxz') >>> np.allclose(np.sum(R[0]), -1.34786452) True >>> R = euler_matrix(1, 2, 3, (0, 1, 0, 1)) >>> np.allclose(np.sum(R[0]), -0.383436184) True >>> ai, aj, ak = (4*np.pi) * (np.random.random(3) - 0.5) >>> for axes in _AXES2TUPLE.keys(): ... R = euler_matrix(ai, aj, ak, axes) >>> for axes in _TUPLE2AXES.keys(): ... R = euler_matrix(ai, aj, ak, axes) """try:firstaxis,parity,repetition,frame=_AXES2TUPLE[axes]except(AttributeError,KeyError):_TUPLE2AXES[axes]# validationfirstaxis,parity,repetition,frame=axesi=firstaxisj=_NEXT_AXIS[i+parity]k=_NEXT_AXIS[i-parity+1]ifframe:ai,ak=ak,aiifparity:ai,aj,ak=-ai,-aj,-akif"sympy"instr(type(ai)):# if we have been passed input values as sympy.Symbol# use symbolic cosine and identity matrixfromsympyimportcos,eye,sinM=eye(4)else:sin,cos=np.sin,np.cosM=np.eye(4)si,sj,sk=sin(ai),sin(aj),sin(ak)ci,cj,ck=cos(ai),cos(aj),cos(ak)cc,cs=ci*ck,ci*sksc,ss=si*ck,si*skifrepetition:M[i,i]=cjM[i,j]=sj*siM[i,k]=sj*ciM[j,i]=sj*skM[j,j]=-cj*ss+ccM[j,k]=-cj*cs-scM[k,i]=-sj*ckM[k,j]=cj*sc+csM[k,k]=cj*cc-sselse:M[i,i]=cj*ckM[i,j]=sj*sc-csM[i,k]=sj*cc+ssM[j,i]=cj*skM[j,j]=sj*ss+ccM[j,k]=sj*cs-scM[k,i]=-sjM[k,j]=cj*siM[k,k]=cj*cireturnMdefeuler_from_matrix(matrix,axes="sxyz"):"""Return Euler angles from rotation matrix for specified axis sequence. axes : One of 24 axis sequences as string or encoded tuple Note that many Euler angle triplets can describe one matrix. >>> R0 = euler_matrix(1, 2, 3, 'syxz') >>> al, be, ga = euler_from_matrix(R0, 'syxz') >>> R1 = euler_matrix(al, be, ga, 'syxz') >>> np.allclose(R0, R1) True >>> angles = (4*np.pi) * (np.random.random(3) - 0.5) >>> for axes in _AXES2TUPLE.keys(): ... R0 = euler_matrix(axes=axes, *angles) ... R1 = euler_matrix(axes=axes, *euler_from_matrix(R0, axes)) ... if not np.allclose(R0, R1): print(axes, "failed") """try:firstaxis,parity,repetition,frame=_AXES2TUPLE[axes.lower()]except(AttributeError,KeyError):_TUPLE2AXES[axes]# validationfirstaxis,parity,repetition,frame=axesi=firstaxisj=_NEXT_AXIS[i+parity]k=_NEXT_AXIS[i-parity+1]M=np.asarray(matrix,dtype=np.float64)[:3,:3]ifrepetition:sy=np.sqrt(M[i,j]*M[i,j]+M[i,k]*M[i,k])ifsy>_EPS:ax=np.arctan2(M[i,j],M[i,k])ay=np.arctan2(sy,M[i,i])az=np.arctan2(M[j,i],-M[k,i])else:ax=np.arctan2(-M[j,k],M[j,j])ay=np.arctan2(sy,M[i,i])az=0.0else:cy=np.sqrt(M[i,i]*M[i,i]+M[j,i]*M[j,i])ifcy>_EPS:ax=np.arctan2(M[k,j],M[k,k])ay=np.arctan2(-M[k,i],cy)az=np.arctan2(M[j,i],M[i,i])else:ax=np.arctan2(-M[j,k],M[j,j])ay=np.arctan2(-M[k,i],cy)az=0.0ifparity:ax,ay,az=-ax,-ay,-azifframe:ax,az=az,axreturnax,ay,azdefeuler_from_quaternion(quaternion,axes="sxyz"):"""Return Euler angles from quaternion for specified axis sequence. >>> angles = euler_from_quaternion([0.99810947, 0.06146124, 0, 0]) >>> np.allclose(angles, [0.123, 0, 0]) True """returneuler_from_matrix(quaternion_matrix(quaternion),axes)defquaternion_from_euler(ai,aj,ak,axes="sxyz"):"""Return quaternion from Euler angles and axis sequence. ai, aj, ak : Euler's roll, pitch and yaw angles axes : One of 24 axis sequences as string or encoded tuple >>> q = quaternion_from_euler(1, 2, 3, 'ryxz') >>> np.allclose(q, [0.435953, 0.310622, -0.718287, 0.444435]) True """try:firstaxis,parity,repetition,frame=_AXES2TUPLE[axes.lower()]except(AttributeError,KeyError):_TUPLE2AXES[axes]# validationfirstaxis,parity,repetition,frame=axesi=firstaxis+1j=_NEXT_AXIS[i+parity-1]+1k=_NEXT_AXIS[i-parity]+1ifframe:ai,ak=ak,aiifparity:aj=-ajai/=2.0aj/=2.0ak/=2.0ci=np.cos(ai)si=np.sin(ai)cj=np.cos(aj)sj=np.sin(aj)ck=np.cos(ak)sk=np.sin(ak)cc=ci*ckcs=ci*sksc=si*ckss=si*skq=np.zeros((4,))ifrepetition:q[0]=cj*(cc-ss)q[i]=cj*(cs+sc)q[j]=sj*(cc+ss)q[k]=sj*(cs-sc)else:q[0]=cj*cc+sj*ssq[i]=cj*sc-sj*csq[j]=cj*ss+sj*ccq[k]=cj*cs-sj*scifparity:q[j]*=-1.0returnqdefquaternion_about_axis(angle,axis):"""Return quaternion for rotation about axis. >>> q = quaternion_about_axis(0.123, [1, 0, 0]) >>> np.allclose(q, [0.99810947, 0.06146124, 0, 0]) True """q=np.array([0.0,axis[0],axis[1],axis[2]])qlen=vector_norm(q)ifqlen>_EPS:q*=np.sin(angle/2.0)/qlenq[0]=np.cos(angle/2.0)returnqdefquaternion_matrix(quaternion):""" Return a homogeneous rotation matrix from quaternion. >>> M = quaternion_matrix([0.99810947, 0.06146124, 0, 0]) >>> np.allclose(M, rotation_matrix(0.123, [1, 0, 0])) True >>> M = quaternion_matrix([1, 0, 0, 0]) >>> np.allclose(M, np.identity(4)) True >>> M = quaternion_matrix([0, 1, 0, 0]) >>> np.allclose(M, np.diag([1, -1, -1, 1])) True >>> M = quaternion_matrix([[1, 0, 0, 0],[0, 1, 0, 0]]) >>> np.allclose(M, np.array([np.identity(4), np.diag([1, -1, -1, 1])])) True """q=np.array(quaternion,dtype=np.float64).reshape((-1,4))n=np.einsum("ij,ij->i",q,q)# how many entries do we havenum_qs=len(n)identities=n<_EPSq[~identities,:]*=np.sqrt(2.0/n[~identities,None])q=np.einsum("ij,ik->ikj",q,q)# store the resultret=np.zeros((num_qs,4,4))# pack the values into the resultret[:,0,0]=1.0-q[:,2,2]-q[:,3,3]ret[:,0,1]=q[:,1,2]-q[:,3,0]ret[:,0,2]=q[:,1,3]+q[:,2,0]ret[:,1,0]=q[:,1,2]+q[:,3,0]ret[:,1,1]=1.0-q[:,1,1]-q[:,3,3]ret[:,1,2]=q[:,2,3]-q[:,1,0]ret[:,2,0]=q[:,1,3]-q[:,2,0]ret[:,2,1]=q[:,2,3]+q[:,1,0]ret[:,2,2]=1.0-q[:,1,1]-q[:,2,2]ret[:,3,3]=1.0# set any identitiesret[identities]=np.eye(4)[None,...]returnret.squeeze()defquaternion_from_matrix(matrix,isprecise=False):"""Return quaternion from rotation matrix. If isprecise is True, the input matrix is assumed to be a precise rotation matrix and a faster algorithm is used. >>> q = quaternion_from_matrix(np.identity(4), True) >>> np.allclose(q, [1, 0, 0, 0]) True >>> q = quaternion_from_matrix(np.diag([1, -1, -1, 1])) >>> np.allclose(q, [0, 1, 0, 0]) or np.allclose(q, [0, -1, 0, 0]) True >>> R = rotation_matrix(0.123, (1, 2, 3)) >>> q = quaternion_from_matrix(R, True) >>> np.allclose(q, [0.9981095, 0.0164262, 0.0328524, 0.0492786]) True >>> R = [[-0.545, 0.797, 0.260, 0], [0.733, 0.603, -0.313, 0], ... [-0.407, 0.021, -0.913, 0], [0, 0, 0, 1]] >>> q = quaternion_from_matrix(R) >>> np.allclose(q, [0.19069, 0.43736, 0.87485, -0.083611]) True >>> R = [[0.395, 0.362, 0.843, 0], [-0.626, 0.796, -0.056, 0], ... [-0.677, -0.498, 0.529, 0], [0, 0, 0, 1]] >>> q = quaternion_from_matrix(R) >>> np.allclose(q, [0.82336615, -0.13610694, 0.46344705, -0.29792603]) True >>> R = random_rotation_matrix() >>> q = quaternion_from_matrix(R) >>> is_same_transform(R, quaternion_matrix(q)) True >>> is_same_quaternion(quaternion_from_matrix(R, isprecise=False), ... quaternion_from_matrix(R, isprecise=True)) True >>> R = euler_matrix(0.0, 0.0, np.pi/2.0) >>> is_same_quaternion(quaternion_from_matrix(R, isprecise=False), ... quaternion_from_matrix(R, isprecise=True)) True """M=np.asarray(matrix,dtype=np.float64)[:4,:4]ifisprecise:q=np.zeros((4,))t=np.trace(M)ift>M[3,3]:q[0]=tq[3]=M[1,0]-M[0,1]q[2]=M[0,2]-M[2,0]q[1]=M[2,1]-M[1,2]else:i,j,k=0,1,2ifM[1,1]>M[0,0]:i,j,k=1,2,0ifM[2,2]>M[i,i]:i,j,k=2,0,1t=M[i,i]-(M[j,j]+M[k,k])+M[3,3]q[i]=tq[j]=M[i,j]+M[j,i]q[k]=M[k,i]+M[i,k]q[3]=M[k,j]-M[j,k]q=q[[3,0,1,2]]q*=0.5/np.sqrt(t*M[3,3])else:m00=M[0,0]m01=M[0,1]m02=M[0,2]m10=M[1,0]m11=M[1,1]m12=M[1,2]m20=M[2,0]m21=M[2,1]m22=M[2,2]# symmetric matrix KK=np.array([[m00-m11-m22,0.0,0.0,0.0],[m01+m10,m11-m00-m22,0.0,0.0],[m02+m20,m12+m21,m22-m00-m11,0.0],[m21-m12,m02-m20,m10-m01,m00+m11+m22],])K/=3.0# quaternion is eigenvector of K that corresponds to largest eigenvaluew,V=np.linalg.eigh(K)q=V[[3,0,1,2],np.argmax(w)]ifq[0]<0.0:np.negative(q,q)returnqdefquaternion_multiply(quaternion1,quaternion0):"""Return multiplication of two quaternions. >>> q = quaternion_multiply([4, 1, -2, 3], [8, -5, 6, 7]) >>> np.allclose(q, [28, -44, -14, 48]) True """w0,x0,y0,z0=quaternion0w1,x1,y1,z1=quaternion1returnnp.array([-x1*x0-y1*y0-z1*z0+w1*w0,x1*w0+y1*z0-z1*y0+w1*x0,-x1*z0+y1*w0+z1*x0+w1*y0,x1*y0-y1*x0+z1*w0+w1*z0,],dtype=np.float64,)defquaternion_conjugate(quaternion):"""Return conjugate of quaternion. >>> q0 = random_quaternion() >>> q1 = quaternion_conjugate(q0) >>> q1[0] == q0[0] and all(q1[1:] == -q0[1:]) True """q=np.array(quaternion,dtype=np.float64)np.negative(q[1:],q[1:])returnqdefquaternion_inverse(quaternion):"""Return inverse of quaternion. >>> q0 = random_quaternion() >>> q1 = quaternion_inverse(q0) >>> np.allclose(quaternion_multiply(q0, q1), [1, 0, 0, 0]) True """q=np.array(quaternion,dtype=np.float64)np.negative(q[1:],q[1:])returnq/np.dot(q,q)defquaternion_real(quaternion):"""Return real part of quaternion. >>> quaternion_real([3, 0, 1, 2]) 3.0 """returnfloat(quaternion[0])defquaternion_imag(quaternion):"""Return imaginary part of quaternion. >>> quaternion_imag([3, 0, 1, 2]) array([0., 1., 2.]) """returnnp.array(quaternion[1:4],dtype=np.float64)defquaternion_slerp(quat0,quat1,fraction,spin=0,shortestpath=True):"""Return spherical linear interpolation between two quaternions. >>> q0 = random_quaternion() >>> q1 = random_quaternion() >>> q = quaternion_slerp(q0, q1, 0) >>> np.allclose(q, q0) True >>> q = quaternion_slerp(q0, q1, 1, 1) >>> np.allclose(q, q1) True >>> q = quaternion_slerp(q0, q1, 0.5) >>> angle = np.arccos(np.dot(q0, q)) >>> np.allclose(2, np.arccos(np.dot(q0, q1)) / angle) or \ np.allclose(2, np.arccos(-np.dot(q0, q1)) / angle) True """q0=unit_vector(quat0[:4])q1=unit_vector(quat1[:4])iffraction==0.0:returnq0eliffraction==1.0:returnq1d=np.dot(q0,q1)ifabs(abs(d)-1.0)<_EPS:returnq0ifshortestpathandd<0.0:# invert rotationd=-dnp.negative(q1,q1)angle=np.arccos(d)+spin*np.piifabs(angle)<_EPS:returnq0isin=1.0/np.sin(angle)q0*=np.sin((1.0-fraction)*angle)*isinq1*=np.sin(fraction*angle)*isinq0+=q1returnq0defrandom_quaternion(rand=None,num=1):"""Return uniform random unit quaternion. rand: array like or None Three independent random variables that are uniformly distributed between 0 and 1. >>> q = random_quaternion() >>> np.allclose(1, vector_norm(q)) True >>> q = random_quaternion(num=10) >>> np.allclose(1, vector_norm(q, axis=1)) True >>> q = random_quaternion(np.random.random(3)) >>> len(q.shape), q.shape[0]==4 (1, True) """ifrandisNone:rand=np.random.rand(3*num).reshape((3,-1))else:assertrand.shape[0]==3r1=np.sqrt(1.0-rand[0])r2=np.sqrt(rand[0])pi2=np.pi*2.0t1=pi2*rand[1]t2=pi2*rand[2]returnnp.array([np.cos(t2)*r2,np.sin(t1)*r1,np.cos(t1)*r1,np.sin(t2)*r2]).T.squeeze()defrandom_rotation_matrix(rand=None,num=1,translate=False):"""Return uniform random rotation matrix. rand: array like Three independent random variables that are uniformly distributed between 0 and 1 for each returned quaternion. >>> R = random_rotation_matrix() >>> np.allclose(np.dot(R.T, R), np.identity(4)) True >>> R = random_rotation_matrix(num=10) >>> np.allclose(np.einsum('...ji,...jk->...ik', R, R), np.identity(4)) True """matrix=quaternion_matrix(random_quaternion(rand=rand,num=num))iftranslate:scale=float(translate)matrix[:3,3]=(np.random.random(3)-0.5)*scalereturnmatrixclassArcball:"""Virtual Trackball Control. >>> ball = Arcball() >>> ball = Arcball(initial=np.identity(4)) >>> ball.place([320, 320], 320) >>> ball.down([500, 250]) >>> ball.drag([475, 275]) >>> R = ball.matrix() >>> np.allclose(np.sum(R), 3.90583455) True >>> ball = Arcball(initial=[1, 0, 0, 0]) >>> ball.place([320, 320], 320) >>> ball.setaxes([1, 1, 0], [-1, 1, 0]) >>> ball.constrain = True >>> ball.down([400, 200]) >>> ball.drag([200, 400]) >>> R = ball.matrix() >>> np.allclose(np.sum(R), 0.2055924) True >>> ball.next() """def__init__(self,initial=None):"""Initialize virtual trackball control. initial : quaternion or rotation matrix """self._axis=Noneself._axes=Noneself._radius=1.0self._center=[0.0,0.0]self._vdown=np.array([0.0,0.0,1.0])self._constrain=FalseifinitialisNone:self._qdown=np.array([1.0,0.0,0.0,0.0])else:initial=np.array(initial,dtype=np.float64)ifinitial.shape==(4,4):self._qdown=quaternion_from_matrix(initial)elifinitial.shape==(4,):initial/=vector_norm(initial)self._qdown=initialelse:raiseValueError("initial not a quaternion or matrix")self._qnow=self._qpre=self._qdowndefplace(self,center,radius):"""Place Arcball, e.g. when window size changes. center : sequence[2] Window coordinates of trackball center. radius : float Radius of trackball in window coordinates. """self._radius=float(radius)self._center[0]=center[0]self._center[1]=center[1]defsetaxes(self,*axes):"""Set axes to constrain rotations."""ifaxesisNone:self._axes=Noneelse:self._axes=[unit_vector(axis)foraxisinaxes]@propertydefconstrain(self):"""Return state of constrain to axis mode."""returnself._constrain@constrain.setterdefconstrain(self,value):"""Set state of constrain to axis mode."""self._constrain=bool(value)defdown(self,point):"""Set initial cursor window coordinates and pick constrain-axis."""self._vdown=arcball_map_to_sphere(point,self._center,self._radius)self._qdown=self._qpre=self._qnowifself._constrainandself._axesisnotNone:self._axis=arcball_nearest_axis(self._vdown,self._axes)self._vdown=arcball_constrain_to_axis(self._vdown,self._axis)else:self._axis=Nonedefdrag(self,point):"""Update current cursor window coordinates."""vnow=arcball_map_to_sphere(point,self._center,self._radius)ifself._axisisnotNone:vnow=arcball_constrain_to_axis(vnow,self._axis)self._qpre=self._qnowt=np.cross(self._vdown,vnow)ifnp.dot(t,t)<_EPS:self._qnow=self._qdownelse:q=[np.dot(self._vdown,vnow),t[0],t[1],t[2]]self._qnow=quaternion_multiply(q,self._qdown)defnext(self,acceleration=0.0):"""Continue rotation in direction of last drag."""q=quaternion_slerp(self._qpre,self._qnow,2.0+acceleration,False)self._qpre,self._qnow=self._qnow,qdefmatrix(self):"""Return homogeneous rotation matrix."""returnquaternion_matrix(self._qnow)defarcball_map_to_sphere(point,center,radius):"""Return unit sphere coordinates from window coordinates."""v0=(point[0]-center[0])/radiusv1=(center[1]-point[1])/radiusn=v0*v0+v1*v1ifn>1.0:# position outside of spheren=np.sqrt(n)returnnp.array([v0/n,v1/n,0.0])else:returnnp.array([v0,v1,np.sqrt(1.0-n)])defarcball_constrain_to_axis(point,axis):"""Return sphere point perpendicular to axis."""v=np.array(point,dtype=np.float64)a=np.array(axis,dtype=np.float64)v-=a*np.dot(a,v)# on planen=vector_norm(v)ifn>_EPS:ifv[2]<0.0:np.negative(v,v)v/=nreturnvifa[2]==1.0:returnnp.array([1.0,0.0,0.0])returnunit_vector([-a[1],a[0],0.0])defarcball_nearest_axis(point,axes):"""Return axis, which arc is nearest to point."""point=np.asarray(point,dtype=np.float64)nearest=Nonemx=-1.0foraxisinaxes:t=np.dot(arcball_constrain_to_axis(point,axis),point)ift>mx:nearest=axismx=treturnnearest# epsilon for testing whether a number is close to zero_EPS=np.finfo(float).eps*4.0# axis sequences for Euler angles_NEXT_AXIS=[1,2,0,1]# map axes strings to/from tuples of inner axis, parity, repetition, frame_AXES2TUPLE={"sxyz":(0,0,0,0),"sxyx":(0,0,1,0),"sxzy":(0,1,0,0),"sxzx":(0,1,1,0),"syzx":(1,0,0,0),"syzy":(1,0,1,0),"syxz":(1,1,0,0),"syxy":(1,1,1,0),"szxy":(2,0,0,0),"szxz":(2,0,1,0),"szyx":(2,1,0,0),"szyz":(2,1,1,0),"rzyx":(0,0,0,1),"rxyx":(0,0,1,1),"ryzx":(0,1,0,1),"rxzx":(0,1,1,1),"rxzy":(1,0,0,1),"ryzy":(1,0,1,1),"rzxy":(1,1,0,1),"ryxy":(1,1,1,1),"ryxz":(2,0,0,1),"rzxz":(2,0,1,1),"rxyz":(2,1,0,1),"rzyz":(2,1,1,1),}_TUPLE2AXES={v:kfork,vin_AXES2TUPLE.items()}defvector_norm(data,axis=None,out=None):"""Return length, i.e. Euclidean norm, of ndarray along axis. >>> v = np.random.random(3) >>> n = vector_norm(v) >>> np.allclose(n, np.linalg.norm(v)) True >>> v = np.random.rand(6, 5, 3) >>> n = vector_norm(v, axis=-1) >>> np.allclose(n, np.sqrt(np.sum(v*v, axis=2))) True >>> n = vector_norm(v, axis=1) >>> np.allclose(n, np.sqrt(np.sum(v*v, axis=1))) True >>> v = np.random.rand(5, 4, 3) >>> n = np.zeros((5, 3)) >>> vector_norm(v, axis=1, out=n) >>> np.allclose(n, np.sqrt(np.sum(v*v, axis=1))) True >>> float(vector_norm([])) 0.0 >>> float(vector_norm([1])) 1.0 """data=np.array(data,dtype=np.float64)ifoutisNone:ifdata.ndim==1:returnnp.sqrt(np.dot(data,data))data*=dataout=np.atleast_1d(np.sum(data,axis=axis))np.sqrt(out,out)returnoutelse:data*=datanp.sum(data,axis=axis,out=out)np.sqrt(out,out)defunit_vector(data,axis=None,out=None):"""Return ndarray normalized by length, i.e. Euclidean norm, along axis. >>> v0 = np.random.random(3) >>> v1 = unit_vector(v0) >>> np.allclose(v1, v0 / np.linalg.norm(v0)) True >>> v0 = np.random.rand(5, 4, 3) >>> v1 = unit_vector(v0, axis=-1) >>> v2 = v0 / np.expand_dims(np.sqrt(np.sum(v0*v0, axis=2)), 2) >>> np.allclose(v1, v2) True >>> v1 = unit_vector(v0, axis=1) >>> v2 = v0 / np.expand_dims(np.sqrt(np.sum(v0*v0, axis=1)), 1) >>> np.allclose(v1, v2) True >>> v1 = np.zeros((5, 4, 3)) >>> unit_vector(v0, axis=1, out=v1) >>> np.allclose(v1, v2) True >>> list(unit_vector([])) [] >>> [float(i) for i in unit_vector([1])] [1.0] """ifoutisNone:data=np.array(data,dtype=np.float64)ifdata.ndim==1:data/=np.sqrt(np.dot(data,data))returndataelse:ifoutisnotdata:out[:]=np.asarray(data)data=outlength=np.atleast_1d(np.sum(data*data,axis))np.sqrt(length,length)ifaxisisnotNone:length=np.expand_dims(length,axis)data/=lengthifoutisNone:returndatadefrandom_vector(size):"""Return array of random doubles in the half-open interval [0.0, 1.0). >>> v = random_vector(10000) >>> bool(np.all(v >= 0) and np.all(v < 1)) True >>> v0 = random_vector(10) >>> v1 = random_vector(10) >>> bool(np.any(v0 == v1)) False """returnnp.random.random(size)defvector_product(v0,v1,axis=0):"""Return vector perpendicular to vectors. >>> v = vector_product([2, 0, 0], [0, 3, 0]) >>> np.allclose(v, [0, 0, 6]) True >>> v0 = [[2, 0, 0, 2], [0, 2, 0, 2], [0, 0, 2, 2]] >>> v1 = [[3], [0], [0]] >>> v = vector_product(v0, v1) >>> np.allclose(v, [[0, 0, 0, 0], [0, 0, 6, 6], [0, -6, 0, -6]]) True >>> v0 = [[2, 0, 0], [2, 0, 0], [0, 2, 0], [2, 0, 0]] >>> v1 = [[0, 3, 0], [0, 0, 3], [0, 0, 3], [3, 3, 3]] >>> v = vector_product(v0, v1, axis=1) >>> np.allclose(v, [[0, 0, 6], [0, -6, 0], [6, 0, 0], [0, -6, 6]]) True """returnnp.cross(v0,v1,axis=axis)defangle_between_vectors(v0,v1,directed=True,axis=0):"""Return angle between vectors. If directed is False, the input vectors are interpreted as undirected axes, i.e. the maximum angle is pi/2. >>> a = angle_between_vectors([1, -2, 3], [-1, 2, -3]) >>> np.allclose(a, np.pi) True >>> a = angle_between_vectors([1, -2, 3], [-1, 2, -3], directed=False) >>> np.allclose(a, 0) True >>> v0 = [[2, 0, 0, 2], [0, 2, 0, 2], [0, 0, 2, 2]] >>> v1 = [[3], [0], [0]] >>> a = angle_between_vectors(v0, v1) >>> np.allclose(a, [0, 1.5708, 1.5708, 0.95532]) True >>> v0 = [[2, 0, 0], [2, 0, 0], [0, 2, 0], [2, 0, 0]] >>> v1 = [[0, 3, 0], [0, 0, 3], [0, 0, 3], [3, 3, 3]] >>> a = angle_between_vectors(v0, v1, axis=1) >>> np.allclose(a, [1.5708, 1.5708, 1.5708, 0.95532]) True """v0=np.asarray(v0,dtype=np.float64)v1=np.asarray(v1,dtype=np.float64)dot=np.sum(v0*v1,axis=axis)dot/=vector_norm(v0,axis=axis)*vector_norm(v1,axis=axis)# clip off floating point error to avoid `nan` in the arccos`dot=np.clip(dot,-1.0,1.0)returnnp.arccos(dotifdirectedelsenp.fabs(dot))definverse_matrix(matrix):"""Return inverse of square transformation matrix. >>> M0 = random_rotation_matrix() >>> M1 = inverse_matrix(M0.T) >>> np.allclose(M1, np.linalg.inv(M0.T)) True >>> for size in range(1, 7): ... M0 = np.random.rand(size, size) ... M1 = inverse_matrix(M0) ... if not np.allclose(M1, np.linalg.inv(M0)): print(size) """returnnp.linalg.inv(matrix)defconcatenate_matrices(*matrices):"""Return concatenation of series of transformation matrices. >>> M = np.random.rand(16).reshape((4, 4)) - 0.5 >>> np.allclose(M, concatenate_matrices(M)) True >>> np.allclose(np.dot(M, M.T), concatenate_matrices(M, M.T)) True """M=np.identity(4)foriinmatrices:M=np.dot(M,i)returnMdefis_same_transform(matrix0,matrix1):"""Return True if two matrices perform same transformation. >>> is_same_transform(np.identity(4), np.identity(4)) True >>> is_same_transform(np.identity(4), random_rotation_matrix()) False """matrix0=np.array(matrix0,dtype=np.float64)matrix0/=matrix0[3,3]matrix1=np.array(matrix1,dtype=np.float64)matrix1/=matrix1[3,3]returnnp.allclose(matrix0,matrix1)defis_same_quaternion(q0,q1):"""Return True if two quaternions are equal."""q0=np.array(q0)q1=np.array(q1)returnnp.allclose(q0,q1)ornp.allclose(q0,-q1)deftransform_around(matrix,point):""" Given a transformation matrix, apply its rotation around a point in space. Parameters ---------- matrix: (4,4) or (3, 3) float, transformation matrix point: (3,) or (2,) float, point in space Returns --------- result: (4,4) transformation matrix """point=np.asanyarray(point)matrix=np.asanyarray(matrix)dim=len(point)ifmatrix.shape!=(dim+1,dim+1):raiseValueError("matrix must be (d+1, d+1)")translate=np.eye(dim+1)translate[:dim,dim]=-pointresult=np.dot(matrix,translate)translate[:dim,dim]=pointresult=np.dot(translate,result)returnresultdefplanar_matrix(offset=None,theta=None,point=None,scale=None):""" 2D homogeonous transformation matrix. Parameters ---------- offset : (2,) float XY offset theta : float Rotation around Z in radians point : (2, ) float Point to rotate around scale : (2,) float or None Scale to apply Returns ---------- matrix : (3, 3) flat Homogeneous 2D transformation matrix """ifoffsetisNone:offset=[0.0,0.0]ifthetaisNone:theta=0.0offset=np.asanyarray(offset,dtype=np.float64)theta=float(theta)ifnotnp.isfinite(theta):raiseValueError("theta must be finite angle!")ifoffset.shape!=(2,):raiseValueError("offset must be length 2!")T=np.eye(3,dtype=np.float64)s=np.sin(theta)c=np.cos(theta)T[0,:2]=[c,s]T[1,:2]=[-s,c]T[:2,2]=offsetifpointisnotNone:T=transform_around(matrix=T,point=point)ifscaleisnotNone:S=np.eye(3)S[:2,:2]*=scaleT=np.dot(S,T)returnTdefplanar_matrix_to_3D(matrix_2D):""" Given a 2D homogeneous rotation matrix convert it to a 3D rotation matrix that is rotating around the Z axis Parameters ---------- matrix_2D: (3,3) float, homogeneous 2D rotation matrix Returns ---------- matrix_3D: (4,4) float, homogeneous 3D rotation matrix """matrix_2D=np.asanyarray(matrix_2D,dtype=np.float64)ifmatrix_2D.shape!=(3,3):raiseValueError("Homogeneous 2D transformation matrix required!")matrix_3D=np.eye(4)# translationmatrix_3D[:2,3]=matrix_2D[:2,2]# rotation from 2D to around Zmatrix_3D[:2,:2]=matrix_2D[:2,:2]returnmatrix_3Ddefspherical_matrix(theta,phi,axes="sxyz"):""" Give a spherical coordinate vector, find the rotation that will transform a [0,0,1] vector to those coordinates Parameters ----------- theta: float, rotation angle in radians phi: float, rotation angle in radians Returns ---------- matrix: (4,4) rotation matrix where the following will be a cartesian vector in the direction of the input spherical coordinates: np.dot(matrix, [0,0,1,0]) """result=euler_matrix(0.0,phi,theta,axes=axes)returnresult
[docs]deftransform_points(points:ArrayLike,matrix:ArrayLike,translate:bool=True)->NDArray[np.float64]:""" Returns points rotated by a homogeneous transformation matrix. If points are (n, 2) matrix must be (3, 3) If points are (n, 3) matrix must be (4, 4) Parameters ---------- points : (n, dim) float Points where `dim` is 2 or 3. matrix : (3, 3) or (4, 4) float Homogeneous rotation matrix. translate : bool Apply translation from matrix or not. Returns ---------- transformed : (n, dim) float Transformed points. """points=np.asanyarray(points,dtype=np.float64)iflen(points)==0ormatrixisNone:returnpoints.copy()# check the matrix against the pointsmatrix=np.asanyarray(matrix,dtype=np.float64)# shorthand the shapecount,dim=points.shape# quickly check to see if we've been passed an identity matrixifnp.abs(matrix-_IDENTITY[:dim+1,:dim+1]).max()<1e-8:returnnp.ascontiguousarray(points.copy())iftranslate:# apply translation and rotationstack=np.column_stack((points,np.ones(count)))returnnp.dot(matrix,stack.T).T[:,:dim]# only apply the rotationreturnnp.dot(matrix[:dim,:dim],points.T).T
deffix_rigid(matrix,max_deviance=1e-5):""" If a homogeneous transformation matrix is *almost* a rigid transform but many matrix-multiplies have accumulated some floating point error try to restore the matrix using SVD. Parameters ----------- matrix : (4, 4) or (3, 3) float Homogeneous transformation matrix. max_deviance : float Do not alter the matrix if it is not rigid by more than this amount. Returns ---------- repaired : (4, 4) or (3, 3) float Repaired homogeneous transformation matrix """dim=matrix.shape[0]-1check=np.abs(np.dot(matrix[:dim,:dim],matrix[:dim,:dim].T)-_IDENTITY[:dim,:dim]).max()# if the matrix differs by more than float-zero and less# than the threshold try to repair the matrix with SVDifcheck>1e-13andcheck<max_deviance:# reconstruct the rotation from the SVDU,_,V=np.linalg.svd(matrix[:dim,:dim])repaired=np.eye(dim+1)repaired[:dim,:dim]=np.dot(U,V)# copy in the translationrepaired[:dim,dim]=matrix[:dim,dim]# should be within tolerance of the original matrixassertnp.allclose(repaired,matrix,atol=max_deviance)returnrepairedreturnmatrixdefis_rigid(matrix,epsilon=1e-8):""" Check to make sure a homogeonous transformation matrix is a rigid transform. Parameters ----------- matrix : (4, 4) float A transformation matrix Returns ----------- check : bool True if matrix is a a transform with only translation, scale, and rotation """matrix=np.asanyarray(matrix,dtype=np.float64)ifmatrix.shape!=(4,4):returnFalse# make sure last row has no scalingifnp.ptp(matrix[-1]-[0,0,0,1])>epsilon:returnFalse# check dot product of rotation against transposecheck=np.dot(matrix[:3,:3],matrix[:3,:3].T)-_IDENTITY[:3,:3]returnnp.ptp(check)<epsilondefscale_and_translate(scale=None,translate=None):""" Optimized version of `compose_matrix` for just scaling then translating. Scalar args are broadcast to arrays of shape (3,) Parameters -------------- scale : float or (3,) float Scale factor translate : float or (3,) float Translation """M=np.eye(4)ifnp.any(scale!=1):M[:3,:3]*=scaleiftranslateisnotNone:M[:3,3]=translatereturnMdefflips_winding(matrix):""" Check to see if a matrix will invert triangles. Parameters ------------- matrix : (4, 4) float Homogeneous transformation matrix Returns -------------- flip : bool True if matrix will flip winding of triangles. """# get input as numpy arraymatrix=np.asanyarray(matrix,dtype=np.float64)# how many random triangles do we really wantcount=3# test rotation against some random trianglestri=np.random.random((count*3,3))rot=np.dot(matrix[:3,:3],tri.T).T# stack them into one triangle souptriangles=np.vstack((tri,rot)).reshape((-1,3,3))# find the normals of every trianglevectors=np.diff(triangles,axis=1)cross=np.cross(vectors[:,0],vectors[:,1])# rotate the original normals to matchcross[:count]=np.dot(matrix[:3,:3],cross[:count].T).T# unitize normalsnorm=np.sqrt(np.dot(cross*cross,[1,1,1])).reshape((-1,1))cross=cross/norm# find the projection of the two normalsprojection=np.dot(cross[:count]*cross[count:],[1.0]*3)# if the winding was flipped but not the normal# the projection will be negative, and since we're# checking a few triangles check against the meanflip=projection.mean()<0.0returnflip