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$) adds translation but 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 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)
Matrix multiplication $Tx=x'$ linearly transforms $x$ to $x'$ where $T$ is a transformation matrix and $x$ is an arbitrary point.
The following cell visualizes the transformations using Matplotlib:
#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, 0.5))
ax1.set_yticks(np.arange(-2.5, 3, 0.5))
ax2.plot(xs_t, ys_t, color="gray", linestyle='dotted')
ax2.set_xticks(np.arange(-2.5, 3, 0.5))
ax2.set_yticks(np.arange(-2.5, 3, 0.5))
ax1.grid()
ax2.grid()
plt.show()
plot_points(A, A_prime)
Let $S = \begin{bmatrix} s_x & 0\\ 0 & s_y \end{bmatrix}$ be scaling matrix such that $x'=S p$ scales the first coordinate of $x$ with $s_x$ and the second coordinate with $s_y$. The following example scales (anisotropically) the first coordinate with $2$ and the second coordinate with $3$.
# scaling transformation
S = np.array([[2, 0], [0, 3]])
x_prime = S @ x
print("original point: \n", x)
print("scaled point: \n", x_prime)
Let us apply scaling transformation S to the matrix $A$ ( $A_s=SA$ ):Transforming the points in the columns of $A$ is done by multipying $A$ by $S$ $$ A_s=SA $$
A_s = S @ A
plot_points(A, A_s)
Define shearing matrices $S_{x}$ and $S_{y}$ as
$$ S_x = \begin{bmatrix} 1 & sh_x\\ 0 & 1 \end{bmatrix} \,\quad S_y = \begin{bmatrix} 1 & 0\\ sh_y & 1 \end{bmatrix}, $$where $S_x$ is a horizontal shear and $S_y$ is a vertical shear (with shearing factors $sh_x$ and $sh_y$ respectively). The cell below demonstrates the transformations:
# Horizontal shear
S_x = np.array([[1, 1], [0, 1]])
print('Horizontal shear \n', S_x)
x_sx = S_x.dot(A)
plot_points(A, x_sx)
# Vertical shear
S_y = np.array([[1, 0], [1, 1]])
x_sy = S_y.dot(A)
print('Vertical shear \n', S_y)
plot_points(A, x_sy)
The rotation matrix $R$ around the origin is given by:
$$ 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)
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 = R @ x
print('Rotation matrix \n', R)
print(x)
print("Rotated P", x_prime)
#Rotation of multiple points using matrix multiplication
A_prime = 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)
Three 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
# HAVE comment for repeat
A_1 = A - np.repeat(x, 2)
# STEP 2: rotate
A_2 = R.dot(A_1)
# STEP 3: reverse tranlation
A_3 = A_2 + np.repeat(x, 2)
#plot
plot_points(A, A_3, p)
Similar to 2D transformations, define a 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)
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 build a 3D rotation matrix. Feel free to study the code.
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 code transforms matrix $A$ by right multiplying 3D rotation matrices $R_x$, $R_y$ and $R_z$ (rotation around x, y, or z-axis respectively).
#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)
The following code snippet is used to plot points in 3D. The details of these methods 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)
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 Sacaling \n',T_3d_s)
P_3d_s=np.dot(T_3d_s,A)
plot3d_points(A,P_3d_s)
The composition of 3D rotations is generally not commutative (the order of the matrices in the composition matter). The code below shows how different compositions of rotations 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))
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))
Given the 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 code cell first applies a rotation of $90^\circ$ and then a translation of $\begin{bmatrix}1\\2\end{bmatrix}$:
# rotation and translation
T_t = np.array([[1], [2]])
T_r = np.array([[0, 1], [-1, 0]]).T
P_r = T_r @ A
P_rt = P_r + T_t
plot_points(P_r, P_rt)
In the following cell, the translation is performed first, followed by the rotation:
#2- translation and rotation
P_t = A + T_t
P_tr = T_r @ P_t
plot_points(P_t,P_tr)
Affine transformations can conveniently be expressed as a matrix multiplication using homogeneous coordinates.
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} $$Applying the transformation yields:
$$ \begin{bmatrix}x'\\ y'\\1\end{bmatrix} = T\begin{bmatrix}x\\ y\\1\end{bmatrix}. $$The cell below provides the functions to_homogeneous
and to_euclidean
for easy conversion of points:
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 $A$ into 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)
The following code defines the affine transformation $T_A$ and apply it to the converted points in 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)
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)