Info
Observe how the order of the transformations affects the outputs.
This tutorial demonstrates the use of linear transformations using Python. We assume you have read the reading material for this week prior to starting the tutorial.
The tutorial covers the following topics:
Recall that linear transformations ($Ax=y$) leave the origin fixed and preserve parallelism. Scaling, shearing, rotation and reflection are examples of linear transformations. Affine transformations ($Ax + b = y$) are like linear transformations, but also allow for translation. Affine transformations can also be formulated as a matrix multiplication using homogeneous coordinates.
Define a point $x=\begin{bmatrix} 1 \\ 2 \end{bmatrix} \in \mathbb{R}^2$ and a data matrix $A =\begin{bmatrix} 0 & 1 & 0 & -1\\ 1 & -1 & -1 & 0 \end{bmatrix} \in \mathbb{R}^{2\times 4}$, where each column is considered a separate point:
import numpy as np
# single 2d point
x = np.array([1, 2])
# matrix with column vectors of points
A = np.array([[0, 1],
[1, 0],
[0, -1],
[-1, 0]]).T
print("x=", x)
print("A=\n", A)
x= [1 2] A= [[ 0 1 0 -1] [ 1 0 -1 0]]
The following steps involve only 2D to 2D transformations. Let $x, x'$ $\in \mathbb{R}^2$ be vectors and $T \in \mathbb{R}^{2x2}$ the transformation matrix. Linearly transforming $x$ to $x'$ is achieved through matrix multiplication $Tx=x'$. In the following steps we will investigate specific choices of $T$, where subscript $T_R$, $T_{SC}$ and $T_{S}$ represent which type of transformation it is eg. rotation, scaling and shearing.
The function below visualizes points before and after a transformation:
#import libraries
import matplotlib.pyplot as plt
import numpy as np
import math
np.set_printoptions(precision=4)
np.set_printoptions(suppress=True)
def plot_points(points, t_points, origin=(0, 0)):
"""
Displays the points and t_points in separate subplots and where the points with the same index have the same color
Args:
points: matrix of 2D points.
t_points: matrix of transformed 2D points.
origin: it is simply show the origin and is (0,0) by default.
"""
color_lut = 'rgbcmy'
fig, (ax1, ax2) = plt.subplots(1, 2)
xs_t = []
ys_t = []
xs = []
ys = []
i = 0
text_offset = 0.15
ax1.scatter(origin[0], origin[1], color='k')
ax1.text(origin[0] + text_offset, origin[1], "o")
ax2.scatter(origin[0], origin[1], color='k')
ax2.text(origin[0] + text_offset, origin[1], "o")
for row, t_row in zip(points.T, t_points.T):
x_s, y_s = row
x_s_t, y_s_t = t_row
xs.append(x_s)
ys.append(y_s)
xs_t.append(x_s_t)
ys_t.append(y_s_t)
c_s = color_lut[i] # these are the same but, its good to be explicit
ax1.scatter(x_s, y_s, color=c_s)
ax1.text(x_s + text_offset, y_s, str(i))
ax2.scatter(x_s_t, y_s_t, color=c_s)
ax2.text(x_s_t + text_offset, y_s_t, str(i))
i += 1
xs.append(xs[0])
ys.append(ys[0])
xs_t.append(xs_t[0])
ys_t.append(ys_t[0])
ax1.plot(xs, ys, color="gray", linestyle='dotted')
ax1.set_xticks(np.arange(-2.5, 3, 1))
ax1.set_yticks(np.arange(-2.5, 4, 1))
ax2.plot(xs_t, ys_t, color="gray", linestyle='dotted')
ax2.set_xticks(np.arange(-2.5, 3, 1))
ax2.set_yticks(np.arange(-2.5, 4, 1))
ax1.grid()
ax2.grid()
plt.show()
Let $T_{SC} = \begin{bmatrix} s_x & 0\\ 0 & s_y \end{bmatrix}$ be a SCaling matrix such that $x'=T_{SC} x$ scales $x_1$ with $s_x$ and $x_2$ with $s_y$. The following example scales (anisotropically) the first coordinate with $2$ and the second coordinate with $3$.
# scaling transformation
T_SC = np.array([[2, 0], [0, 3]])
x_prime = T_SC @ x
print("original point: \n", x)
print("scaled point: \n", x_prime)
original point: [1 2] scaled point: [2 6]
Apply scaling transformation $T_{SC}$ to the matrix $A$ ( $A_s=T_{SC}A$ ). Transforming the points in the columns of $A$ is achieved by multipying $A$ by $T_{SC}$ (notice the order of the multiplication) $$ A_s=T_{SC}A $$
A_s = T_SC @ A
plot_points(A, A_s)
Define shearing matrices $T_{Sx}$ and $T_{Sy}$ as
$$ T_{Sx} = \begin{bmatrix} 1 & sh_x\\ 0 & 1 \end{bmatrix} \,\quad T_{Sy} = \begin{bmatrix} 1 & 0\\ sh_y & 1 \end{bmatrix}, $$where $T_{Sx}$ is a horizontal shear and $T_{Sy}$ is a vertical shear (with shearing factors $sh_x$ and $sh_y$ respectively). The cell below demonstrates the transformations:
# Horizontal shear
T_Sx = np.array([[1, 1], [0, 1]])
print('Horizontal shear \n', T_Sx)
x_sx = T_Sx.dot(A)
plot_points(A, x_sx)
# Vertical shear
T_Sy = np.array([[1, 0], [1, 1]])
x_sy = T_Sy.dot(A)
print('Vertical shear \n', T_Sy)
plot_points(A, x_sy)
Horizontal shear [[1 1] [0 1]]
Vertical shear [[1 0] [1 1]]
The rotation matrix $T_R$ around the origin is given by:
$$ T_R = \begin{bmatrix} \cos(\theta) & -\sin(\theta)\\ \sin(\theta) & \cos(\theta) \end{bmatrix}, $$where $\theta$ is the (anticlockwise) rotation angle around the origin $\begin{bmatrix} 0 \\ 0 \end{bmatrix}$ as illustrated in the code cell below:
# Rotation with angle theta
angle = 30
theta = np.radians(angle)
T_R = np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]])
#Rotation of a single point using matrix multiplication
x_prime = T_R @ x
print('Rotation matrix \n', T_R)
print(x)
print("Rotated P", x_prime)
#Rotation of multiple points using matrix multiplication
A_prime = T_R @ A #rotate the points in A around the origin through matrix multiplication with T_r
print("Points before rotation \n", A) #Printing such that each point is on one row.. Hence transposing the matrix
print("Points after rotation \n", A_prime.T)
Rotation matrix [[ 0.866 -0.5 ] [ 0.5 0.866]] [1 2] Rotated P [-0.134 2.2321] Points before rotation [[ 0 1 0 -1] [ 1 0 -1 0]] Points after rotation [[-0.5 0.866] [ 0.866 0.5 ] [ 0.5 -0.866] [-0.866 -0.5 ]]
The following cell visualizes the transformations $T_{R}A=A'$, where $A$ is the dataset from above.
plot_points(A,A_prime)
The following steps are needed to transform a point $x$ (or matrix of points $A$) around an arbitrary point $p$
The following example shows a 30-degree rotation of matrix $A$ around point $p=\begin{bmatrix}1\\1\end{bmatrix}$:
# centered point
p = np.array([1, 1])
# Rotation around a point
# STEP 1: translation to the point
# np.newaxis is used to add an additional axis to an array and reshapes p into a column vector.
A_1 = A - p[:, np.newaxis]
# STEP 2: rotate
A_2 = T_R.dot(A_1)
# STEP 3: reverse translation
A_3 = A_2 + p[:, np.newaxis]
#plot
plot_points(A, A_3, p)
Similar to 2D transformations, define a data matrix $A =\begin{bmatrix} 0 & 1 & 0 & -31\\ 2 & 0 & -1 & 0 \\ 0 & 0 & 0 & 0\end{bmatrix}$ of columns of three-dimensional points:
# 3d points
a, b, c, d = [0, 2, 0], [1, 0, 0], [0, -1, 0], [-3, 0, 0]
# matrix with row vectors of points
A = np.array([a, b, c, d]).T
print('A=', A)
A= [[ 0 1 0 -3] [ 2 0 -1 0] [ 0 0 0 0]]
The following three rotation matrices, rotate points by an angle θ about the x-axis, y-axis, and z-axis in three dimensions:
$$ Rx = \begin{bmatrix} 1 & 0 & 0 \\ 0 & \cos(\theta) & -\sin(\theta) \\ 0 & \sin(\theta) & \cos(\theta) \\ \end{bmatrix} ,\quad Ry = \begin{bmatrix} \cos(\theta) & 0 & \sin(\theta) \\ 0 & 1 & 0 \\ -\sin(\theta) & 0 & \cos(\theta) \\ \end{bmatrix} ,\quad Rz = \begin{bmatrix} \cos(\theta) & -\sin(\theta) & 0 \\ \sin(\theta) & \cos(\theta) & 0\\ 0 & 0 & 1 \\ \end{bmatrix} $$Use the function get_3d_rotation_matrix
below to construct a 3D rotation matrix.
def get_3d_rotation_matrix(theta, axis=0):
"""
This function return a rotation matrix given an input theta angle in
radians.
"""
if axis == 0:
return np.array(
[
[1, 0, 0],
[0, math.cos(theta), -math.sin(theta)],
[0, math.sin(theta), math.cos(theta)],
],
dtype=np.float64,
)
elif axis == 1:
return np.array(
[
[math.cos(theta), 0, math.sin(theta)],
[0, 1, 0],
[-math.sin(theta), 0, math.cos(theta)],
],
dtype=np.float64,
)
return np.array(
[
[math.cos(theta), -math.sin(theta), 0],
[math.sin(theta), math.cos(theta), 0],
[0, 0, 1],
],
dtype=np.float64,
)
The following example applies 3D rotation matrices $R_x$, $R_y$, and $R_z$ (representing rotations around the x, y, and z axes, respectively) to transform the points (columns) in $A$, eg. $A'=R_xA$.
#rotation angle
angle = 60 #degrees
theta = np.radians(angle) #radians
#3d rotation around X-axis
R_x = get_3d_rotation_matrix(theta, axis=0)
print('3d rotation over X-axis', angle, 'degrees \n', R_x)
P_rx = np.dot(R_x, A)
#3d rotation around Y-axis
R_y = get_3d_rotation_matrix(theta, axis=1)
print('3d rotation over Y-axis', angle, 'degrees \n', R_y)
P_ry = np.dot(R_y, A)
#3d rotation around Z-axis
R_z = get_3d_rotation_matrix(theta, axis=2)
print('3d rotation over Z-axis', angle, 'degrees \n', R_z)
P_rz = np.dot(R_z, A)
3d rotation over X-axis 60 degrees [[ 1. 0. 0. ] [ 0. 0.5 -0.866] [ 0. 0.866 0.5 ]] 3d rotation over Y-axis 60 degrees [[ 0.5 0. 0.866] [ 0. 1. 0. ] [-0.866 0. 0.5 ]] 3d rotation over Z-axis 60 degrees [[ 0.5 -0.866 0. ] [ 0.866 0.5 0. ] [ 0. 0. 1. ]]
The following function is used to plot points in 3D. The details of this method are not essential for the core concepts of the tutorial.
def plot3d_points(points, t_points, view_point=45):
"""This function get a matrix of 3D points and transformed points and them in separate subplots.
The details are not important for understanding the course material.
Args:
points: matrix of 3D points
t_points: matrix of transformed 3D points
view_point: view point if 3D plot in degree
"""
color_lut = 'rgbcmy'
fig, (ax, ax2) = plt.subplots(1, 2, figsize=(10, 10), subplot_kw=dict(projection='3d'))
ax.view_init(elev=45., azim=view_point)
ax2.view_init(elev=45., azim=view_point)
xs = []
ys = []
zs = []
xs_t = []
ys_t = []
zs_t = []
i = 0
ax.scatter(0, 0, 0, color='k')
ax2.scatter(0, 0, 0, color='k')
for row, t_row in zip(points.T, t_points.T):
x, y, z = row
xs.append(x)
ys.append(y)
zs.append(z)
c = color_lut[i] # these are the same but, its good to be explicit
ax.scatter(x, y, z, color=c)
x_t, y_t, z_t = t_row
xs_t.append(x_t)
ys_t.append(y_t)
zs_t.append(z_t)
c_t = color_lut[i] # these are the same but, its good to be explicit
ax2.scatter(x_t, y_t, z_t, color=c_t)
i += 1
xs.append(xs[0])
ys.append(ys[0])
zs.append(zs[0])
ax.plot(xs, ys, zs, color="gray", linestyle='dotted')
ax.set_xlabel('X Label')
ax.set_ylabel('Y Label')
ax.set_zlabel('Z Label')
ax.set_xticks(np.arange(-2.5, 3, 0.5))
ax.set_yticks(np.arange(-2.5, 3, 0.5))
ax.set_zticks(np.arange(-2.5, 3, 0.5))
plt.grid()
xs_t.append(xs_t[0])
ys_t.append(ys_t[0])
zs_t.append(zs_t[0])
ax2.plot(xs_t, ys_t, zs_t, color="gray", linestyle='dotted')
ax2.set_xlabel('X Label')
ax2.set_ylabel('Y Label')
ax2.set_zlabel('Z Label')
ax2.set_xticks(np.arange(-2.5, 3, 0.5))
ax2.set_yticks(np.arange(-2.5, 3, 0.5))
ax2.set_zticks(np.arange(-2.5, 3, 0.5))
plt.grid()
plt.show()
plot3d_points
is used to plot the points $A$ and rotated points $P_{rx}$, $P_{ry}$ and $P_{rz}$.
#3d rotation over X-axis
print('3D rotation over X-axis', angle, 'degrees \n')
plot3d_points(A, P_rx)
#3d rotation over Y-axis
print('3D rotation over Y-axis', angle, 'degrees \n')
plot3d_points(A, P_ry)
#3d rotation over Z-axis
print('3D rotation over Z-axis', angle, 'degrees \n')
plot3d_points(A, P_rz)
3D rotation over X-axis 60 degrees
3D rotation over Y-axis 60 degrees
3D rotation over Z-axis 60 degrees
The scaling matrix ($S$) in three dimensions is given by:
$$ S = \begin{bmatrix} s_x & 0 & 0\\ 0 & s_y & 0 \\ 0 & 0 & s_z \end{bmatrix} , $$Three 3D-points are transformed with the scaling matrix and plotted with the original points for reference.
# 3D points
a, b, c = (1, 0,0), (0, 1,0), (0, 0,1)
# Matrix with row vectors of points
A = np.array([a, b, c]).T
# 3D-Scaling
T_3d_s=np.array([[2,0,0],[0,1,0],[0,0,3]])
print('3d Scaling \n',T_3d_s)
P_3d_s=np.dot(T_3d_s,A)
plot3d_points(A,P_3d_s)
3d Scaling [[2 0 0] [0 1 0] [0 0 3]]
The composition of 3D rotations is generally not commutative (the order of the matrices in the composition matter). The example below shows how compositions of different rotation matrices will give different results:
#Commutative property
T_1 = R_z.dot(R_y).dot(R_x)
T_2 = R_x.dot(R_y).dot(R_z)
T_3 = R_x.dot(R_z).dot(R_y)
print("T_1=R_z*R_y*R_x")
plot3d_points(A, np.dot(T_1, A))
print("T_2=R_x*R_y*R_z")
plot3d_points(A, np.dot(T_2, A))
print("T_3=R_x*R_z*R_y")
plot3d_points(A, np.dot(T_3, A))
T_1=R_z*R_y*R_x
T_2=R_x*R_y*R_z
T_3=R_x*R_z*R_y
Compositions of 3D rotations around the same axis are commutative, as shown in the cell below.
#Commutative property in rotations over the same axis
#3d rotation over X-axis
angle_1 = np.radians(30)
angle_2 = np.radians(50)
angle_3 = np.radians(70)
R_x1 = get_3d_rotation_matrix(angle_1, axis=0)
R_x2 = get_3d_rotation_matrix(angle_2, axis=0)
R_x3 = get_3d_rotation_matrix(angle_3, axis=0)
#building different ordering of rotations
T_1 = R_x1.dot(R_x2).dot(R_x3)
T_2 = R_x2.dot(R_x1).dot(R_x3)
T_3 = R_x3.dot(R_x2).dot(R_x1)
print("T_1=R_x1*R_x2*R_x3")
plot3d_points(A, np.dot(T_1, A))
print("T_2==R_x2*R_x1*R_x3")
plot3d_points(A, np.dot(T_2, A))
print("T_3==R_x3*R_x2*R_x1")
plot3d_points(A, np.dot(T_3, A))
T_1=R_x1*R_x2*R_x3
T_2==R_x2*R_x1*R_x3
T_3==R_x3*R_x2*R_x1
The cell below defines data matrix $A =\begin{bmatrix} 0 & 1 & 0 & -1\\ 1 & -1 & -1 & 0 \end{bmatrix}$, where each column contains a point:
# points a, b, c and d
a, b, c, d = (0, 1), (1, 0), (0, -1), (-1, 0)
# matrix with row vectors of points
A = np.array([a, b, c, d]).T
The following cell applies a rotation of $90^\circ$ then a translates the points $a,b,c$ and $d$ by the vector $p$ $=$ $\begin{bmatrix}1\\2\end{bmatrix}$:
# rotation and translation
p = np.array([[1], [2]])
T_r = np.array([[0, 1], [-1, 0]]).T
p_r = T_r @ A
p_rt = p_r + p
plot_points(A, p_r)
plot_points(p_r, p_rt)
In the following cell, the transformation is a translation followed by a rotation:
#2- translation and rotation
p_t = A + p
p_tr = T_r @ p_t
plot_points(p_t,p_tr)
Affine transformations ($Ax+b$) can conveniently be expressed as a single matrix multiplication using homogeneous coordinates.
Generally applying the transformation $T$ to a homogeneous 2D point:
$$ \begin{bmatrix}tx'\\ ty'\\ t\end{bmatrix} = T\begin{bmatrix}x\\ y\\1\end{bmatrix}. $$The cell below provides the functions to_homogeneous
and to_euclidean
to convert points between Euclidean and homogeneous coordinates. Recall that to convert homogeneous coordinates to Euclidean coordinates division by the scaling factor $t$ is necessary:
def to_homogeneous(points):
"""Transform points to homogeneous coordinates."""
return np.vstack((points, np.ones((1, points.shape[1]))))
def to_euclidean(points):
"""Transform homogeneous points to euclidean points."""
return points[:2] / points[2]
The cell below transforms the points in the data matrix $A$ to homogeneous coordinates:
#Affine transformation (rotation nad translation)
print('points in euclidean coordinates \n', A)
A_h = to_homogeneous(A)
print('points in homogeneous coordinates \n', A_h)
points in euclidean coordinates [[ 0 1 0 -1] [ 1 0 -1 0]] points in homogeneous coordinates [[ 0. 1. 0. -1.] [ 1. 0. -1. 0.] [ 1. 1. 1. 1.]]
The following example illustrates a transformation consisting of a rotation and a translation using homogeneous coordinates:
$$ T=\begin{bmatrix}\cos\theta&-\sin\theta & x_t\\\sin\theta& \cos\theta & y_t\\ 0&0&1\end{bmatrix} $$The cell below defines an affine transformation $T_A$ and applies it to the homogeneous coordinates of the matrix $A_h$.
$$ T_A=\begin{bmatrix}0 & -1 & 1 \\ 1 & 0 & 2 \\ 0 & 0 & 1\end{bmatrix} $$#Affine transformation (rotation then translation)
T_A = np.array([[0, 1, 0], [-1, 0, 0], [1, 2, 1]]).T
print('Affine transformation \n', T_A)
A_h_prime = np.dot(T_A, A_h)
print('transformed points in homogeneous coordinate \n', A_h_prime)
Affine transformation [[ 0 -1 1] [ 1 0 2] [ 0 0 1]] transformed points in homogeneous coordinate [[0. 1. 2. 1.] [2. 3. 2. 1.] [1. 1. 1. 1.]]
Finally, the points are converted from homogeneous coordinates $A'_h$ to Euclidean coordinates by using to_euclidean
. The points are plotted below:
A_prime = to_euclidean(A_h_prime)
print('transformed points in euclidean coordinates \n', A_prime)
plot_points(A, A_prime)
transformed points in euclidean coordinates [[0. 1. 2. 1.] [2. 3. 2. 1.]]