## How to rotate an image with OpenCV

June 22, 2019

Somewhat surprisingly, rotating images in OpenCV is not a simple function call but requires a bit of math to do properly. The tutorial in the official documentation does not go into much detail on this, so this is my attempt of explaining how to do this after spending the effort of wrapping my head around the details myself.

To make sense out of image rotations in OpenCV, we first need to understand the coordinate system used: An image is stored as an array, where the first index runs down and the second one to the right. This order of indexing is exactly how matrix elements are indexed in mathematics, except zero-based. In case of color images, there is also a third index, which indicates the image channel — e.g. the familiar red, green, and blue color channel if the image is represented in the RGB color space.

However, for geometric image transformations, the coordinate system
is a Cartesian one, so the horizontal coordinate comes *first*
and the vertical second. Furthermore, the *y* axis points *down*. This is
a common convention in computer graphics, but not in mathematics. So if
we want to know what the pixel value at the integer coordinates (*x*,*y*) in the image is, we
have to look at the values at `[y, x]`

(in a grayscale image)
or `[y, x, :]`

(color image) in the array.

The aim here is to rotate an image *θ* degrees in a way that none of it
gets clipped out. The rotation alone is a linear transformation and
straightforward to derive if you're fluent in trigonometry: The new
coordinates of the original point (*x*,*y*) are, using the matrix
notation, $$
\begin{pmatrix} x \\ y \end{pmatrix}
\to
\begin{pmatrix} x' \\ y' \end{pmatrix} =
\begin{pmatrix} \cos\theta && \sin\theta \\ -\sin\theta
&& \cos\theta \end{pmatrix}
\begin{pmatrix} x \\ y \end{pmatrix}.
$$ Note that the minus sign is chosen here in the way that
positive rotations are counterclockwise and takes into account that the
*y* axis points down.

After applying the transformation to the image, OpenCV clips it to
the rectangle with the top-left corner at the origin and the
bottom-right corner at the destination image size. That's why we also
need to translate the image so that the topmost and leftmost points of
the rotated image land on the *x* and *y* axis, respectively, and calculate
the size that the rotated image takes (to use as the destination image
size). A rotation combined with translation is an *affine
transformation*. We can express the rotation by the angle *θ* followed by a translation of *Δ**x* horizontally and *Δ**y* vertically in the matrix
notation as $$
\begin{pmatrix} x \\ y \end{pmatrix}
\to
\begin{pmatrix} x' \\ y' \end{pmatrix} =
\begin{pmatrix} \cos\theta && \sin\theta && \Delta x \\
-\sin\theta && \cos\theta && \Delta y \end{pmatrix}
\begin{pmatrix} x \\ y \\ 1 \end{pmatrix}.
$$ The OpenCV function for applying an affine transformation to
an image is called `warpAffine`

, and it takes the original
image, the above 2 × 3 matrix, and the
destination image size as its parameters.

OpenCV provides the function `getRotationMatrix2D`

that
computes a rotation matrix about an arbitrary point. To get both the
needed translation and the destination image size, we can take the
corner points of the original image, rotate them by applying the above
rotation matrix, and compute the bounding box of those rotated points
using the OpenCV function `boundingRect`

, which returns the
coordinates of the top-left corner and the width and the height of the
bounding box.

Putting this all together, we get the following example code:

```
import cv2
import numpy as np
= cv2.imread("cat.jpg")
image = 30 # counterclockwise
rotation_angle_degrees
# Compute the rotation part of the affine transformation
= cv2.getRotationMatrix2D((0, 0), rotation_angle_degrees, 1)
transformation_matrix
# Compute the translation and the new image size
= image.shape # note the index order
nrows, ncols, _ = np.array([[0, 0], [ncols, 0], [ncols, nrows], [0, nrows]]).T
original_corners = np.int32(np.dot(transformation_matrix[:, :2], original_corners))
new_corners = cv2.boundingRect(new_corners.T.reshape(1, 4, 2))
x, y, new_width, new_height 2] = [-x, -y]
transformation_matrix[:,
# Note the order of image width and height; they are in image coordinates
= cv2.warpAffine(image, transformation_matrix, (new_width, new_height))
image
"rotated_cat.jpg", image) cv2.imwrite(
```