Marcos Duarte, Renato Naville Watanabe
Laboratory of Biomechanics and Motor Control
Federal University of ABC, Brazil
Translation and rotation are two examples of affine transformations. Affine transformations preserve straight lines, but not necessarily the distance between points. Other examples of affine transformations are scaling, shear, and reflection. The figure below illustrates different affine transformations in a plane. Note that a 3x3 matrix is shown on top of each transformation; these matrices are known as the transformation matrices and are the mathematical representation of the physical transformations. Next, we will study how to use this approach to describe the translation and rotation of a rigid-body.
The kinematics of a rigid body is completely described by its pose, i.e., its position and orientation in space (and the corresponding changes are translation and rotation). The translation and rotation of a rigid body are also known as rigid-body transformations (or simply, rigid transformations).
Remember that in physics, a rigid body is a model (an idealization) for a body in which deformation is neglected, i.e., the distance between every pair of points in the body is considered constant. Consequently, the position and orientation of a rigid body can be completely described by a corresponding coordinate system attached to it. For instance, two (or more) coordinate systems can be used to represent the same rigid body at two (or more) instants or two (or more) rigid bodies in space.
Rigid-body transformations are used in motion analysis (e.g., of the human body) to describe the position and orientation of each segment (using a local (anatomical) coordinate system defined for each segment) in relation to a global coordinate system fixed at the laboratory. Furthermore, one can define an additional coordinate system called technical coordinate system also fixed at the rigid body but not based on anatomical landmarks. In this case, the position of the technical markers is first described in the laboratory coordinate system, and then the technical coordinate system is calculated to recreate the anatomical landmarks position in order to finally calculate the original anatomical coordinate system (and obtain its unknown position and orientation through time).
In what follows, we will study rigid-body transformations by looking at the transformations between two coordinate systems. For simplicity, let's first analyze planar (two-dimensional) rigid-body transformations and later we will extend these concepts to three dimensions (where the study of rotations are more complicated).
import numpy as np
np.set_printoptions(precision=4, suppress=True)
In a two-dimensional space, two coordinates and one angle are sufficient to describe the pose of the rigid body, totalizing three degrees of freedom for a rigid body. Let's see first the transformation for translation, then for rotation, and combine them at last.
A pure two-dimensional translation of a coordinate system in relation to other coordinate system and the representation of a point in these two coordinate systems are illustrated in the figure below (remember that this is equivalent to describing a translation between two rigid bodies).
The position of point $\mathbf{P}$ originally described in the local coordinate system but now described in the Global coordinate system in vector form is:
Or for each component:
And in matrix form is:
\begin{equation} \begin{bmatrix} \mathbf{P_X} \\ \mathbf{P_Y} \end{bmatrix} = \begin{bmatrix} \mathbf{L_X} \\ \mathbf{L_Y} \end{bmatrix} + \begin{bmatrix} \mathbf{P}_x \\ \mathbf{P}_y \end{bmatrix} \end{equation}Because position and translation can be treated as vectors, the inverse operation, to describe the position at the local coordinate system in terms of the Global coordinate system, is simply:
\begin{equation} \mathbf{P_l} = \mathbf{P_G} -\mathbf{L_G} \end{equation}From classical mechanics, this transformation is an example of Galilean transformation.
For example, if the local coordinate system is translated by $\mathbf{L_G}=[2, 3]$ in relation to the Global coordinate system, a point with coordinates $\mathbf{P_l}=[4, 5]$ at the local coordinate system will have the position $\mathbf{P_G}=[6, 8]$ at the Global coordinate system:
LG = np.array([2, 3]) # (Numpy 1D array with 2 elements)
Pl = np.array([4, 5])
PG = LG + Pl
PG
array([6, 8])
This operation also works if we have more than one data point (NumPy knows how to handle vectors with different dimensions):
Pl = np.array([[4, 5], [6, 7], [8, 9]]) # 2D array with 3 rows and two columns
PG = LG + Pl
PG
array([[ 6, 8], [ 8, 10], [10, 12]])
A pure two-dimensional rotation of a coordinate system in relation to other coordinate system and the representation of a point in these two coordinate systems are illustrated in the figure below (remember that this is equivalent to describing a rotation between two rigid bodies). The rotation is around an axis orthogonal to this page, not shown in the figure (for a three-dimensional coordinate system the rotation would be around the $\mathbf{Z}$ axis).
Consider we want to express the position of point $\mathbf{P}$ in the Global coordinate system in terms of the local coordinate system knowing only the coordinates at the local coordinate system and the angle of rotation between the two coordinate systems.
There are different ways of deducing that, we will see three of these methods next.
From figure below, the coordinates of point $\mathbf{P}$ in the Global coordinate system can be determined finding the sides of the triangles marked in red.
Then:
The equations above can be expressed in matrix form:
Or simply:
\begin{equation} \mathbf{P_G} = \mathbf{R_{Gl}}\mathbf{P_l} \end{equation}Where $\mathbf{R_{Gl}}$ is the rotation matrix that rotates the coordinates from the local to the Global coordinate system:
So, given any position at the local coordinate system, with the rotation matrix above we are able to determine the position at the Global coordinate system. Let's check that before looking at other methods to obtain this matrix.
For instance, consider a local coordinate system rotated by $45^o$ in relation to the Global coordinate system, a point in the local coordinate system with position $\mathbf{P_l}=[1, 1]$ will have the following position at the Global coordinate system:
α = np.pi / 4
RGl = np.array([[np.cos(α), -np.sin(α)],
[np.sin(α), np.cos(α)]])
Pl = np.array([[1, 1]]).T # transpose the array for correct matrix multiplication
PG = RGl @ Pl # the operator @ is used for matrix multiplication of arrays
PG
array([[0. ], [1.4142]])
And if we have the points [1,1], [0,1], [1,0] at the local coordinate system, their positions at the Global coordinate system are:
Pl = np.array([[1, 1], [0, 1], [1, 0]]).T # transpose array for matrix multiplication
PG = RGl@Pl
PG
array([[ 0. , -0.7071, 0.7071], [ 1.4142, 0.7071, 0.7071]])
Another way to determine the rotation matrix is to use the concept of direction cosine.
Direction cosines are the cosines of the angles between any two vectors.
For the present case with two coordinate systems, they are the cosines of the angles between each axis of one coordinate system and each axis of the other coordinate system. The figure below illustrates the directions angles between the two coordinate systems, expressing the local coordinate system in terms of the Global coordinate system.
The same rotation matrix as obtained before.
Note that the order of the direction cosines is because in our convention, the first row is for the $\mathbf{X}$ coordinate and the second row for the $\mathbf{Y}$ coordinate (the outputs). For the inputs, we followed the same order, first column for the $\mathbf{x}$ coordinate, second column for the $\mathbf{y}$ coordinate.
Yet another way to deduce the rotation matrix is to view the axes of the rotated coordinate system as unit vectors, versors, of a basis as illustrated in the figure below.
A basis is a set of linearly independent vectors that can represent every vector in a given vector space, i.e., a basis defines a coordinate system.
The coordinates of these two versors at the local coordinate system in terms of the Global coordinate system are:
Note that as unit vectors, each of the versors above should have norm (length) equals to one, which indeed is the case.
If we express each versor above as different columns of a matrix, we obtain the rotation matrix again:
This means that the rotation matrix can be viewed as the basis of the rotated coordinate system defined by its versors.
This third way to derive the rotation matrix is in fact the method most commonly used in motion analysis because the coordinates of markers (in the Global/laboratory coordinate system) are what we measure with cameras.
Probably you are wondering how to perform the inverse operation, given a point in the Global coordinate system how to calculate its position in the local coordinate system. Let's see this now.
If we want the inverse operation, to express the position of point $\mathbf{P}$ in the local coordinate system in terms of the Global coordinate system, the figure below illustrates that using trigonometry.
Then:
And in matrix form:
Where $\mathbf{R_{lG}}$ is the rotation matrix that rotates the coordinates from the Global to the local coordinate system (note the inverse order of the subscripts):
If we use the direction cosines to calculate the rotation matrix, because the axes didn't change, the cosines are the same, only the order changes, now $\mathbf{x, y}$ are the rows (outputs) and $\mathbf{X, Y}$ are the columns (inputs):
And defining the versors of the axes in the Global coordinate system for a basis in terms of the local coordinate system would also produce this latter rotation matrix.
The two sets of equations and matrices for the rotations from Global-to-local and local-to-Global coordinate systems are very similar, this is no coincidence. Each of the rotation matrices we deduced, $\mathbf{R_{Gl}}$ and $\mathbf{R_{lG}}$, perform the inverse operation in relation to the other. Each matrix is the inverse of the other.
In other words, the relation between the two rotation matrices means it is equivalent to instead of rotating the local coordinate system by $\alpha$ in relation to the Global coordinate system, to rotate the Global coordinate system by $-\alpha$ in relation to the local coordinate system; remember that $\cos(-\alpha)=\cos(\alpha)$ and $\sin(-\alpha)=-\sin(\alpha)$.
See here for a review about matrix and its main properties.
A nice property of the rotation matrix is that its inverse is the transpose of the matrix (because the columns/rows are mutually orthogonal and have norm equal to one).
This property can be shown with the rotation matrices we deduced:
This means that if we have a rotation matrix, we know its inverse.
The transpose and inverse operators in NumPy are methods of the array:
α = np.pi/4
RGl = np.array([[np.cos(α), -np.sin(α)],
[np.sin(α), np.cos(α)]])
print('Orthogonal matrix (RGl):\n', RGl)
print('Transpose (RGl.T):\n', RGl.T)
print('Inverse (RGl.I):\n', np.linalg.inv(RGl))
Orthogonal matrix (RGl): [[ 0.7071 -0.7071] [ 0.7071 0.7071]] Transpose (RGl.T): [[ 0.7071 0.7071] [-0.7071 0.7071]] Inverse (RGl.I): [[ 0.7071 0.7071] [-0.7071 0.7071]]
Using the inverse and the transpose mathematical operations, the coordinates at the local coordinate system given the coordinates at the Global coordinate system and the rotation matrix can be obtained by:
Where we referred the inverse of $\mathbf{R_{Gl}}\;(\:\mathbf{R_{Gl}^{-1}})$ as $\mathbf{R_{lG}}$ (note the different order of the subscripts).
Let's show this calculation in NumPy:
α = np.pi/4
RGl = np.array([[np.cos(α), -np.sin(α)],
[np.sin(α), np.cos(α)]])
print('Rotation matrix (RGl):\n', RGl)
Pl = np.array([[1, 1]]).T # transpose the array for correct matrix multiplication
print('Position at the local coordinate system (Pl):\n', Pl)
PG = RGl@Pl
print('Position at the Global coordinate system (PG=RGl*Pl):\n', PG)
Pl = RGl.T@PG
print('Position at the local coordinate system using the inverse of RGl (Pl=RlG*PG):\n', Pl)
Rotation matrix (RGl): [[ 0.7071 -0.7071] [ 0.7071 0.7071]] Position at the local coordinate system (Pl): [[1] [1]] Position at the Global coordinate system (PG=RGl*Pl): [[0. ] [1.4142]] Position at the local coordinate system using the inverse of RGl (Pl=RlG*PG): [[1.] [1.]]
Another use of the rotation matrix is 'to rotate a vector by a given angle around an axis of the coordinate system as shown in the figure below.
We will not prove that we use the same rotation matrix, but think that in this case the vector position rotates by the same angle instead of the coordinate system. The new coordinates of the vector position $\mathbf{P'}$ rotated by an angle $\alpha$ is simply the rotation matrix (for the angle $\alpha$) multiplied by the coordinates of the vector position $\mathbf{P}$:
\begin{equation} \mathbf{P'} = \mathbf{R}_\alpha\mathbf{P} \end{equation}Consider for example that $\mathbf{P}=[2,1]$ and $\alpha=30^o$; the coordinates of $\mathbf{P'}$ are:
α = np.pi/6
R = np.array([[np.cos(α), -np.sin(α)],
[np.sin(α), np.cos(α)]])
P = np.array([[2, 1]]).T
Pl = R@P
print("P':\n", Pl)
P': [[1.2321] [1.866 ]]
In summary, some of the properties of the rotation matrix are:
On the different meanings of the rotation matrix:
Which matrix to use, from local to Global or Global to local?
A typical scenario in motion analysis is to calculate the rotation matrix using the position of markers placed on the moving rigid body. With the markers' positions, we create a local basis, which by definition is the rotation matrix for the rigid body with respect to the Global (laboratory) coordinate system. To define a coordinate system using a basís, we also will need to define an origin.
Let's see how to calculate a basis given the markers' positions.
Consider the markers at $m1=[1,1]$, $m2=[1,2]$ and $m3=[-1,1]$ measured in the Global coordinate system as illustrated in the figure below:
A possible local coordinate system with origin at the position of m1 is also illustrated in the figure above. Intentionally, the three markers were chosen to form orthogonal vectors.
The translation vector between the two coordinate system is:
The vectors expressing the axes of the local coordinate system are:
$$ \hat{\mathbf{x}} = m_2 - m_1 = [1,2] - [1,1] = [0,1] $$ $$ \hat{\mathbf{y}} = m_3 - m_1 = [-1,1] - [1,1] = [-2,0] $$Note that these two vectors do not form a basis yet because they are not unit vectors (in fact, only y is not a unit vector). Let's normalize these vectors:
$$ \begin{array}{} \hat{\mathbf{e_x}} = \frac{x}{||x||} = \frac{[0,1]}{\sqrt{0^2+1^2}} = [0,1] \\ \\ \hat{\mathbf{e_y}} = \frac{y}{||y||} = \frac{[-2,0]}{\sqrt{2^2+0^2}} = [-1,0] \end{array} $$Beware that the versors above are not exactly the same as the ones shown in the right plot of the last figure, the versors above if plotted will start at the origin of the coordinate system, not at [1,1] as shown in the figure.
Since the markers $m1$, $m2$ and $m3$ were carefully chosen, the versors $\hat{\mathbf{e_x}}$ and $\hat{\mathbf{e_y}}$ are orthogonal.
We could have done this calculation in NumPy (we will need to do that when dealing with real data later):
m1 = np.array([1.,1.]) # marker 1
m2 = np.array([1.,2.]) # marker 2
m3 = np.array([-1.,1.]) # marker 3
x = m2 - m1 # vector x
y = m3 - m1 # vector y
vx = x/np.linalg.norm(x) # versor x
vy = y/np.linalg.norm(y) # verson y
print("x =", x, ", y =", y, "\nex=", vx, ", ey=", vy)
x = [0. 1.] , y = [-2. 0.] ex= [0. 1.] , ey= [-1. 0.]
Now, both $\hat{\mathbf{e}_x}$ and $\hat{\mathbf{e}_y}$ are unit vectors (versors) and they are orthogonal, a basis can be formed with these two versors, and we can represent the rotation matrix using this basis (just place the versors of this basis as columns of the rotation matrix):
This rotation matrix makes sense because from the figure above we see that the local coordinate system we defined is rotated by 90$^o$ in relation to the Global coordinate system and if we use the general form for the rotation matrix:
So, the position of any point in the local coordinate system can be represented in the Global coordinate system by:
For example, the point $\mathbf{P_l}=[1,1]$ has the following position at the Global coordinate system:
LGl = np.array([[1, 1]]).T
print('Translation vector:\n', LGl)
RGl = np.array([[0, -1],
[1, 0]])
print('Rotation matrix:\n', RGl)
Pl = np.array([[1, 1]]).T
print('Position at the local coordinate system:\n', Pl)
PG = LGl + RGl@Pl
print('Position at the Global coordinate system, PG = LGl + RGl*Pl:\n', PG)
Translation vector: [[1] [1]] Rotation matrix: [[ 0 -1] [ 1 0]] Position at the local coordinate system: [[1] [1]] Position at the Global coordinate system, PG = LGl + RGl*Pl: [[0] [2]]
If we didn't know the angle of rotation between the two coordinate systems, which is the typical situation in motion analysis, we simply would equate one of the terms of the two-dimensional rotation matrix in its algebraic form to its correspondent value in the numerical rotation matrix we calculated.
So the angle $\alpha$ can be found by:
For instance, taking the first term of the rotation matrices above: $\cos\alpha = 0$ implies that $\alpha$ is 90$^o$ or 270$^o$, but combining with another matrix term, $\sin\alpha = 1$, implies that $\alpha=90^o$. We can solve this problem in one step using the tangent $(\sin\alpha/\cos\alpha)$ function with two terms of the rotation matrix and calculating the angle with the arctan2(y, x)
function:
RGl = np.array([[0, -1],
[1, 0]])
ang = np.arctan2(RGl[1, 0], RGl[0, 0])*180/np.pi
print('The angle is:', ang)
The angle is: 90.0
And this procedure would be repeated for each segment and for each instant of the analyzed movement to find the rotation of each segment.
In the notebook about two-dimensional angular kinematics, we calculated segment and joint angles using simple trigonometric relations. We can also calculate these two-dimensional angles using what we learned here about the rotation matrix.
The segment angle will be given by the matrix representing the rotation from the laboratory coordinate system (G) to a coordinate system attached to the segment and the joint angle will be given by the matrix representing the rotation from one segment coordinate system (l1) to the other segment coordinate system (l2).
So, we have to compute two basis now, one for each segment and the joint angle will be given by the product between the two rotation matrices obtained from both basis.
The description of a point P in the global basis given the description of this point in the l1 basis is:
The description of the same point P in the global basis given the description of this point in the l2 basis is:
So, to find the description of this point P in the l1 basis given the description of this point in the l2 basis is:
The rotation matrix from $l_2$ to $l_1$ is:
The rotation matrices of both basis are:
So, the rotation matrix from $l_2$ to $l_1$ is:
The angle $\theta_{l_1l_2} = \theta_2-\theta_1$ is the angle between the two reference frames. So to find the $\theta_{l_1l_2}$ is:
Below is an example: To define a two-dimensional basis, we need to calculate vectors perpendicular to each of these lines. Here is a way of doing that. First, let's find three non-collinear points for each basis:
x1, y1, x2, y2 = 0, 0, 1, 1 # points at segment 1
x3, y3, x4, y4 = 1.1, 1, 2.1, 0 # points at segment 2
#The slope of the perpendicular line is minus the inverse of the slope of the line
xl1 = x1 - (y2-y1); yl1 = y1 + (x2-x1) # point at the perpendicular line 1
xl2 = x4 - (y3-y4); yl2 = y4 + (x3-x4) # point at the perpendicular line 2
With these three points, we can create a basis and the corresponding rotation matrix:
b1x = np.array([x2-x1, y2-y1])
b1x = b1x/np.linalg.norm(b1x) # versor x of basis 1
b1y = np.array([xl1-x1, yl1-y1])
b1y = b1y/np.linalg.norm(b1y) # versor y of basis 1
b2x = np.array([x3-x4, y3-y4])
b2x = b2x/np.linalg.norm(b2x) # versor x of basis 2
b2y = np.array([xl2-x4, yl2-y4])
b2y = b2y/np.linalg.norm(b2y) # versor y of basis 2
RGl1 = np.array([b1x, b1y]).T # rotation matrix from segment 1 to the laboratory
RGl2 = np.array([b2x, b2y]).T # rotation matrix from segment 2 to the laboratory
print('rotation matrix from segment 1 to global:\n', RGl1)
print('rotation matrix from segment 2 to global:\n', RGl2)
rotation matrix from segment 1 to global: [[ 0.7071 -0.7071] [ 0.7071 0.7071]] rotation matrix from segment 2 to global: [[-0.7071 -0.7071] [ 0.7071 -0.7071]]
Now, the segment and joint angles are simply matrix operations:
print('Rotation matrix for segment 1:\n', RGl1)
print('\nRotation angle of segment 1:', np.arctan2(RGl1[1,0], RGl1[0,0])*180/np.pi)
print('\nRotation matrix for segment 2:\n', RGl2)
print('\nRotation angle of segment 2:', np.arctan2(RGl1[1,0], RGl2[0,0])*180/np.pi)
Rl1l2 = RGl1.T@RGl2 # Rl1l2 = Rl1G*RGl2
print('\nJoint rotation matrix (Rl1l2 = Rl1G*RGl2):\n', Rl1l2)
print('\nJoint angle:', np.arctan2(Rl1l2[1,0], Rl1l2[0,0])*180/np.pi)
Rotation matrix for segment 1: [[ 0.7071 -0.7071] [ 0.7071 0.7071]] Rotation angle of segment 1: 45.0 Rotation matrix for segment 2: [[-0.7071 -0.7071] [ 0.7071 -0.7071]] Rotation angle of segment 2: 135.0 Joint rotation matrix (Rl1l2 = Rl1G*RGl2): [[ 0. -1.] [ 1. -0.]] Joint angle: 90.0
Same result as obtained in Angular kinematics in a plane (2D).
The fact that we simply multiplied the rotation matrices to calculate the rotation matrix of one segment in relation to the other is powerful and can be generalized for any number of segments: given a serial kinematic chain with links 1, 2, ..., n and 0 is the base/laboratory, the rotation matrix between the base and last link is: $\mathbf{R_{n,n-1}R_{n-1,n-2} \dots R_{2,1}R_{1,0}}$, where each matrix in this product (calculated from right to left) is the rotation of one link with respect to the next one.
For instance, consider a kinematic chain with two links, the link 1 is rotated by $\alpha_1$ with respect to the base (0) and the link 2 is rotated by $\alpha_2$ with respect to the link 1.
Using Sympy, the rotation matrices for link 2 w.r.t. link 1 $(R_{12})$ and for link 1 w.r.t. base 0 $(R_{01})$ are:
from IPython.display import display, Math
from sympy import sin, cos, Matrix, simplify, latex, symbols
from sympy.interactive import printing
printing.init_printing()
a1, a2 = symbols('alpha1 alpha2')
R12 = Matrix([[cos(a2), -sin(a2)], [sin(a2), cos(a2)]])
display(Math(r'\mathbf{R_{12}}=' + latex(R12)))
R01 = Matrix([[cos(a1), -sin(a1)], [sin(a1), cos(a1)]])
display(Math(r'\mathbf{R_{01}}=' + latex(R01)))
The rotation matrix of link 2 w.r.t. the base $(R_{02})$ is given simply by $R_{01}*R_{12}$:
R02 = R01*R12
display(Math(r'\mathbf{R_{02}}=' + latex(R02)))
Which simplifies to:
display(Math(r'\mathbf{R_{02}}=' + latex(simplify(R02))))
As expected.
The typical use of all these concepts is in the three-dimensional motion analysis where we will have to deal with angles in different planes, which needs a special manipulation as we will see next.
Consider now the case where the local coordinate system is translated and rotated in relation to the Global coordinate system and a point is described in both coordinate systems as illustrated in the figure below (once again, remember that this is equivalent to describing a translation and a rotation between two rigid bodies).
The position of point $\mathbf{P}$ originally described in the local coordinate system, but now described in the Global coordinate system in vector form is:
And in matrix form:
This means that we first disrotate the local coordinate system and then correct for the translation between the two coordinate systems. Note that we can't invert this order: the point position is expressed in the local coordinate system and we can't add this vector to another vector expressed in the Global coordinate system, first we have to convert the vectors to the same coordinate system.
If now we want to find the position of a point at the local coordinate system given its position in the Global coordinate system, the rotation matrix and the translation vector, we have to invert the expression above:
The expression above indicates that to perform the inverse operation, to go from the Global to the local coordinate system, we first translate and then rotate the coordinate system.
It is possible to combine the translation and rotation operations in only one matrix, called the transformation matrix (also referred as homogeneous transformation matrix):
Or simply:
The inverse operation, to express the position at the local coordinate system in terms of the Global coordinate system, is:
However, because $\mathbf{T_{Gl}}$ is not orthonormal, which means its inverse is not its transpose. Its inverse in matrix form is given by:
A. Determine the matrices for rotating one coordinate system to another (two-dimensional).
B. What are the coordinates of the point [1, 1] (local coordinate system) at the global coordinate system?
C. And if this point is at the Global coordinate system and we want the coordinates at the local coordinate system?
D. Consider that the local coordinate system, besides the rotation is also translated by [2, 2]. What are the matrices for rotation, translation, and transformation from one coordinate system to another (two-dimensional)?
E. Repeat B and C considering this translation.
A. Determine the rotation matrices of all possible transformations between the coordinate systems.
B. For the point [1, 1] in the coordinate system U, what are its coordinates in coordinate system V and in the Global coordinate system?
Using the rotation matrix, deduce the new coordinates of a square figure with coordinates [0, 0], [1, 0], [1, 1], and [0, 1] when rotated by 0$^o$, 45$^o$, 90$^o$, 135$^o$, and 180$^o$ (always clockwise).
Solve the problem 2 of Angular kinematics in a plane (2D) but now using the concept of two-dimensional transformations.
Write a Python code to solve the problem in https://leetcode.com/problems/rotate-image/.
Rotate a triangle placed at A(0,0), B(1,1) and C(5,2) by an angle $45^o$ with respect to origin. From https://studyresearch.in/2019/12/14/numerical-example-of-rotation-in-2d-transformation/.
Rotate a triangle placed at A(0,0), B(1,1) and C(5,2) by an angle $45^o$ with respect to point P(-1,-1). From https://studyresearch.in/2019/12/14/numerical-example-of-rotation-in-2d-transformation/.