Created by Ryo Kurashina
Email: rk2014@ic.ac.uk
HTML Version (This will be a link)


2D Transformations

Learning Objectives:

  • Understand basic types of matrix transformations.
  • Be able to implement these transformations on Python to create animations on Plotly.

Table of Contents

  1. Introduction
  2. Rotation Matrices
  3. Scaling Matrices
  4. Custom Matrices
  5. Skew Matrices
  6. Determinants

1. Introduction

A general matrix transformation in 2D can be written as: $$A:I\!R^2 \mapsto I\!R^2$$
$$A \begin{pmatrix}x\\y\end{pmatrix}=\begin{pmatrix}a&b\\c&d\end{pmatrix}\begin{pmatrix}x\\y\end{pmatrix}= \begin{pmatrix}ax+by\\cx+dy\end{pmatrix}$$
On this IPython Notebook we will be looking at particular cases of these matrix transformations and how they transform vectors from a geometric point of view.

2. Rotation Matrices

If we consider any point in the $x$-$y$ plane to be written in terms of its $\mathbf{\hat{i}},\,\mathbf{\hat{j}}$ unit vectors:

$$ \begin{pmatrix}x \\ y \end{pmatrix} = x\begin{pmatrix} 1 \\ 0 \end{pmatrix} + y\begin{pmatrix} 0 \\ 1 \end{pmatrix} \qquad (1)$$
Then rotation of both of these unit vectors by an amount $\theta$ would lead to the unit vectors being mapped to:

$$ R_{\theta} : \begin{pmatrix} 1 \\ 0 \end{pmatrix} \mapsto \begin{pmatrix} \cos\theta \\ \sin\theta\end{pmatrix}, \; R_{\theta} : \begin{pmatrix} 0 \\ 1 \end{pmatrix} \mapsto \begin{pmatrix} -\sin\theta \\ \cos\theta\end{pmatrix} \qquad (2)$$
Now, if we want to rotate an arbitrary vector by an amount $\theta$ then we can combine $(1)$ and $(2)$ to get:

$$ R_{\theta} : \begin{pmatrix} x \\ y \end{pmatrix} \mapsto x\begin{pmatrix} \cos\theta \\ \sin\theta\end{pmatrix} +y\begin{pmatrix} -\sin\theta \\ \cos\theta\end{pmatrix} $$
Which is equivalent to the matrix or linear transformation:

$$ R_{\theta} \begin{pmatrix} x \\ y \end{pmatrix} = \begin{pmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta\end{pmatrix}\begin{pmatrix} x \\ y \end{pmatrix} $$


In [ ]:
# Import libraries/packages to be used (HIT SHIFT + ENTER TO RUN CELL)
import numpy as np
import math as ma 
import plotly.figure_factory as ff
from plotly.offline import download_plotlyjs,init_notebook_mode,plot,iplot
import plotly.graph_objs as go
init_notebook_mode(connected=True)

Now, let's apply the theory of rotation matrices to write some code which will rotate a vector by amount $\theta$. The function rotmat(th) returns the rotation matrix.


In [ ]:
def rotmat(th):
    rotator = np.array([[ma.cos(th), -ma.sin(th)],[ma.sin(th), ma.cos(th)]])
    return rotator

This function rotation(th, vec) takes in a rotation angle and vector input and returns a tuple of numpy arrays which can be animated to create a "smooth transition" of the rotation using Plotly Animate.


In [ ]:
def rotation(th, vec):
    # Parameters 
    t = np.linspace(0,1,50)
    tt = th*t
    # Rotation matrix
    BigR = np.identity(2)
    for i in range(len(tt)-1):
        BigR = np.vstack((BigR,rotmat(tt[i+1])))
    newvec = np.matmul(BigR,vec)
    x = newvec[::2]
    y = newvec[1::2]
    return (x,y)

In the cell below, enter a rotation angle and vector inside the rotation() function which has some inputs inside already and hit shift enter to generate an animation of the rotation! (N.B. Don't worry too much if you're not familiar with the plotly syntax, it's more important you understand what the matrices are doing, the cell will run itself after you choose the input arguments and hit `Shift` + `Enter`)


In [ ]:
# Enter a 2D vector here...
vec = [1,0]
# Enter rotation angle here...
th = 4
(x0,y0) = rotation(th, vec)
x0 = list(x0)
y0 = list(y0)

# Syntax for plotly, see documentation for more info
data = [{"x": [x0[i],0], "y": [y0[i],0], "frame": i} for i in range(len(x0))]

figure = {'data': [{'x': data[0]['x'], 'y': data[0]['y']}],
          'layout': {'xaxis': {'range': [-2, 2], 'autorange': False},
                     'yaxis': {'range': [-2, 2], 'autorange': False},
                     'height': 600,
                     'width': 600,
                     'title': 'Rotation Animation',
                     'updatemenus': [{'type': 'buttons',
                                      'buttons': [{'label': 'Play',
                                                   'method': 'animate',
                                                   'args': [None, dict(frame=dict(duration=50, redraw=False), 
                                                               transition=dict(duration=50),
                                                               fromcurrent=True,
                                                               mode='immediate')]}]}]
                    },
          'frames': [{'data': [{'x': data[i]['x'], 'y': data[i]['y']}]} for i in range(len(x0))]
         }
# Plot
iplot(figure)

3. Scaling Matrices

Now we are familiar with rotation matrices, we will move onto another type of matrix transformation known as a "scaling" matrix. Scaling matrices have the form:

$$ \text{Scale} = \begin{pmatrix} s1 & 0 \\ 0 & s2 \end{pmatrix} $$
Now let's look at what this matrix does to an arbitrary vector $(x, y)$:

$$ \begin{pmatrix} s1 & 0 \\ 0 & s2 \end{pmatrix}\begin{pmatrix} x \\ y\end{pmatrix} = s1\begin{pmatrix}x\\0\end{pmatrix}+s2\begin{pmatrix}0\\y\end{pmatrix}$$
As we can see, this "scale" matrix scales the vector in the $x$-direction by a factor $s1$ and scales the vector in the $y$-direction by a factor s2. Now we write a function scale(vec, *args) which takes in a vector input as well as an additional 1 OR 2 arguments. If one is given, then a matrix which scales both $x$ and $y$ directions equally is returned while if 2 are given then a matrix which scales by the arguments given is returned.


In [ ]:
# Input vector, scale 1, scale 2 as arguments
def scale(vec, *args):
    assert len(vec)==2, "Please provide a 2D vector for the first argument"
    assert len(args)==1 or len(args)==2, "Please provide 1 or 2 scale arguments"
    t = np.linspace(1,args[0],50)
    # If only one scale argument given then scale in both directions by same amount
    if len(args) == 1:
        x = vec[0]*t
        y = vec[1]*t
        return(x,y)
    # If two scale arguments given then scale individual directions
    else:
        s = np.linspace(1,args[1],50)
        x = vec[0]*t
        y = vec[1]*s
        return(x,y)

Now try it for yourself by running the function with your own inputs, by default 2 scale arguments have been inputted but you can try 1 if you like as well.


In [ ]:
# Again input vector here
vec = [1,1]
# Arguments here
s1 = 2
s2 = 3
(x1,y1) = scale(vec, s1, s2)
x1 = list(x1)
y1 = list(y1)

# Plotly syntax again
data = [{"x": [x1[i],0], "y": [y1[i],0], "frame": i} for i in range(len(x1))]

figure = {'data': [{'x': data[0]['x'], 'y': data[0]['y']}],
          'layout': {'xaxis': {'range': [-2, 2], 'autorange': False},
                     'yaxis': {'range': [-2, 2], 'autorange': False},
                     'height': 600,
                     'width': 600,
                     'title': 'Scale Animation',
                     'updatemenus': [{'type': 'buttons',
                                      'buttons': [{'label': 'Play',
                                                   'method': 'animate',
                                                   'args': [None, dict(frame=dict(duration=50, redraw=False), 
                                                               transition=dict(duration=50),
                                                               fromcurrent=True,
                                                               mode='immediate')]}]}]
                    },
          'frames': [{'data': [{'x': data[i]['x'], 'y': data[i]['y']}]} for i in range(len(x1))]
         }

iplot(figure)

4. Custom Matrix

Now we have explained some basic matrix transformations, feel free to use the following code to create your own 2x2 matrix transformations.


In [ ]:
# Custom 2D transformation
def custom(vec):
    print("Enter values for 2x2 matrix [[a,b],[c,d]] ")
    a = input("Enter a value for a: ")
    b = input("Enter a value for b: ")
    c = input("Enter a value for c: ")
    d = input("Enter a value for d: ")
    try:
        a = float(a)
    except ValueError:
        print("Enter a float or integer for a")
    try:
        b = float(b)
    except ValueError:
        print("Enter a float or integer for b")
    try:
        c = float(c)
    except ValueError:
        print("Enter a float or integer for c")
    try:
        d = float(d)
    except ValueError:
        print("Enter a float or integer for d")
    
    A = [[a,b],[c,d]]
    t = np.linspace(0,1,50)
    w = np.matmul(A,vec)-vec
    x = [vec[0]+tt*w[0] for tt in t]
    y = [vec[1]+tt*w[1] for tt in t]
    
    return(x,y)

In [ ]:
(x2,y2) = custom([1,1])
x2 = list(x2)
y2 = list(y2)

data = [{"x": [x2[i],0], "y": [y2[i],0], "frame": i} for i in range(len(x2))]

figure = {'data': [{'x': data[0]['x'], 'y': data[0]['y']}],
          'layout': {'xaxis': {'range': [-2, 2], 'autorange': False},
                     'yaxis': {'range': [-2, 2], 'autorange': False},
                     'height': 600,
                     'width': 600,
                     'title': 'Custom Animation',
                     'updatemenus': [{'type': 'buttons',
                                      'buttons': [{'label': 'Play',
                                                   'method': 'animate',
                                                   'args': [None, dict(frame=dict(duration=50, redraw=False), 
                                                               transition=dict(duration=50),
                                                               fromcurrent=True,
                                                               mode='immediate')]}]}]
                    },
          'frames': [{'data': [{'x': data[i]['x'], 'y': data[i]['y']}]} for i in range(len(x2))]
         }

iplot(figure)

5. Skew Matrices

For the next matrix we will use a slightly different approach to visualize what this transformation does. Instead of taking one vector and following what the matrix does to it, we will take 3 vectors ((1, 0), (1, 1) and (0, 1)) and look at what the matrix does to the entire area captured between these 3 points and the origin (i.e. the unit box). Why is this?
Well, matrix transformations are linear transformations and any point inside the box is a linear combination of $\mathbf{\hat{i}},\,\mathbf{\hat{j}}$ unit vectors. Consider a matrix $A$ acting upon a vector (x,y).

$$ A \begin{pmatrix}x\\y\end{pmatrix} = \begin{pmatrix}a&b\\c&d\end{pmatrix}\begin{pmatrix}x\\y\end{pmatrix} = x\begin{pmatrix}a\\c\end{pmatrix}+y\begin{pmatrix}b\\d\end{pmatrix} $$
As we can see, the $\mathbf{\hat{i}},\,\mathbf{\hat{j}}$ unit vectors are mapped to vectors $(a,\,c)$ and $(b,\,d)$ , respectively, so any points inside the unit square are mapped inside the parallelogram formed by the 2 vectors $(a,\,c)$ and $(b,\,d)$, (see the Parallelipiped visualization for more info). To visualize this, let's write a function which returns a skew matrix and see how it deforms the unit square. It's okay if you're not sure what a skew matrix is or what it does as you'll see what happens when we make the animation.


In [ ]:
def skew(axis, vec):
    t = np.linspace(0,1,50)
    # Skew in x-direction
    if axis == 0:
        A = [[1,1],[0,1]]
        w = np.matmul(A,vec)-vec
        x = [vec[0]+tt*w[0] for tt in t]
        y = [vec[1]+tt*w[1] for tt in t]
        return(x, y)
    # Skew in y-direction
    elif axis == 1:
        A = [[1,0],[1,1]]
        w = np.matmul(A,vec)-vec
        x = [vec[0]+tt*w[0] for tt in t]
        y = [vec[1]+tt*w[1] for tt in t]
        return(x, y)
    else: 
        return ValueError('Axis must be 0 or 1')

Now we write a function which will take 6 arrays in total (2 for (1, 0), 2 for (0, 1) and 2 for (1, 1)) and shows an animation of how the 3 vectors are transformed. Remember that we can forget about the origin as it is always mapped to itself (this is a standard property of linear transformations).


In [ ]:
# Function that returns data in a format to be used by plotly and then plots it 
def sqtransformation(x0,x1,x2,y0,y1,y2):
    data = [{"x": [0,x0[i],x1[i],x2[i],0], "y": [0,y0[i],y1[i],y2[i],0], "frame": i} for i in range(len(x0))]

    figure = {'data': [{'x': data[0]['x'], 'y': data[0]['y'], 'fill':'tonexty'}],
              'layout': {'xaxis': {'range': [-2, 2], 'autorange': False},
                         'yaxis': {'range': [-2, 2], 'autorange': False},
                         'height': 600,
                         'width': 600,
                         'title': 'Square Animation',
                         'updatemenus': [{'type': 'buttons',
                                          'buttons': [{'label': 'Play',
                                                       'method': 'animate',
                                                       'args': [None, dict(frame=dict(duration=50, redraw=False), 
                                                                   transition=dict(duration=50),
                                                                   fromcurrent=True,
                                                                   mode='immediate')]}]}]
                        },
              'frames': [{'data': [{'x': data[i]['x'], 'y': data[i]['y']}]} for i in range(len(x0))]
             }

    iplot(figure)

In [ ]:
# Transform the 3 vectors that form the unit box. 
(x0,y0) = skew(1,[1,0])
(x1,y1) = skew(1,[1,1])
(x2,y2) = skew(1,[0,1])

sqtransformation(x0,x1,x2,y0,y1,y2)

So a skew transformation in 2D can be seen as a "shear" where the box is pushed into a parallelogram.

A good exercise might be to combine the above script as well as the functions we have already written into making one wrapper function which will transform a square using any of the transformations we have discussed above (see html version of this pynb).

6. Determinants

The determinant of a 2 x 2 matrix is defined to be:

$$ |A| = \begin{vmatrix}a_1&a_2\\b_1&b_2\end{vmatrix} = a_1b_2-a_2b_1$$


Now if we take the magnitude of the curl of two 3D vectors $\vec{a}=(a_1,\,a_2,\,0)$ and $\vec{b}=(b_1,\,b_2,\,0)$ with a zero $z$-component, recall that this is the area of a parallelogram formed by $\vec{a}$ and $\vec{b}$ (see Parallelipiped visualisation), then we get:

$$ \mid\vec{a}\times\vec{b}\mid = \begin{vmatrix}\mathbf{\hat{i}}&\mathbf{\hat{j}}&\mathbf{\hat{k}}\\a_1&a_2&0\\b_1&b_2&0\end{vmatrix} = a_1b_2-a_2b_1 $$


So for two vectors which lie on the $x-y$ plane we get the absolute value of the cross product of 2 vectors is equal to the area of the parallelogram formed. However, any two vectors in 3D are always coplanar so this result is always true for two general 3D vectors, since we can always rotate coordinate systems such that the two vectors lie on the $x-y$ plane (Google isometries for more info), without changing the area of the parallelogram.


In [ ]: