OpenCV perspective warping: keystone effect and rotation around center

June 8, 2025  opencv  computervision  numpy  python 

In this blog post, I am going to show two examples of using OpenCV’s functionality for prespective warping (cv2.getPerspectiveTransform and cv2.warpPerspective). We will apply perspective transformation to an image to (1) simulate a keystone effect, i.e. a camera looking at an object from an angle, and (2) to simulate rotation of an object around its center (which will requre some additional rigid transformations).

Let’s start with the initial imports:

import cv2
import numpy as np
import skimage
import itertools
from matplotlib import pyplot as plt

np.set_printoptions(formatter={"float_kind": "{: .3f}".format})

We are going to work with a synthetic image of a chessboard surrounded by a green background and a white rim:

def create_base_image(w, h, rim, backgroud_color, top=50, left=100):

    im = np.ones((h, w, 3), dtype=np.uint8) * 255
    im[rim:h-rim, rim:w-rim] = backgroud_color
    cb = skimage.data.checkerboard()
    cb_w, cb_h = cb.shape

    for channel in range(3):
        im[top:top+cb_w, left:left+cb_h, channel] = cb

    return im

The image is 400 x 300 pixels, with the rim of 10 pixels on each side:

w = 400
h = 300
rim = 10

im = create_base_image(w, h, rim, backgroud_color=(173, 255, 47))

plt.imshow(im, interpolation='none')
plt.show()

png

Perspective transformation with OpenCV require a 3 by 3 matrix, which can be obtained from four src and four dst points (describing the same locations appearing in the original and the transformed image).

For a keystone effect, we will select the cornes of the green rectangle as our src points, and a copy of those with the two top points moved closer together as dst.

last_x_before_rim = w - rim - 1
last_y_before_rim = h - rim - 1

src = np.array([
    [rim, rim],
    [last_x_before_rim, rim],
    [last_x_before_rim, last_y_before_rim],
    [rim, last_y_before_rim]
], dtype=np.float32)

dst = np.array([
    [rim+30, rim],
    [last_x_before_rim-30, rim],
    [last_x_before_rim, last_y_before_rim],
    [rim, last_y_before_rim]
], dtype=np.float32)
print(f'src =\n{src}')
print()
print(f'dst =\n{dst}')
src =
[[ 10.000  10.000]
 [ 389.000  10.000]
 [ 389.000  289.000]
 [ 10.000  289.000]]

dst =
[[ 40.000  10.000]
 [ 359.000  10.000]
 [ 389.000  289.000]
 [ 10.000  289.000]]

In the visualization below, the src points are shown as larger pink dots, while the transformed ones (dst), sort of as seen from a different perspective, are the smaller cyan dots:

plt.imshow(im, interpolation='none')
plt.scatter(src[:, 0], src[:, 1], color='deeppink', s=100)
plt.scatter(dst[:, 0], dst[:, 1], color='cyan', s=20)
plt.show()

png

We then use cv2.getPerspectiveTransform to obtain the transformation matrix:

M = cv2.getPerspectiveTransform(src, dst)
print(f'M =\n{M}')
M =
[[ 0.837 -0.113  32.531]
 [-0.000  0.831  1.631]
 [ 0.000 -0.001  1.000]]

Having the matrix at hand, we pass it over to cv2.warpPerspective, together with the original image, the desired new image size, and the interpolation method to get the transformed version:

im_warped = cv2.warpPerspective(im, M, (w, h), flags=cv2.INTER_LINEAR)

_, (ax_original, ax_warped) = plt.subplots(1, 2)
ax_original.imshow(im, interpolation='none')
ax_warped.imshow(im_warped, interpolation='none')
plt.show()

png

For the second example, we would like to get an image rotated around the center of our green object. We start with some points around the origin (top left corner):

src_at_origin = np.array([[-2, -1], [2, -1], [2, 1], [-2, 1]], dtype=np.float32)

However, the goal is not to rotate around the origin, but rather around the center of the object. We will use some homogeneous transformations for that, and will start with defining the helper functions:

def rotation_matrix(theta):

    c = np.cos(theta)
    s = np.sin(theta)

    return np.array([[c, -s], [s, c]])


def create_transform(t_x, t_y, theta):

    translation = np.array([t_x, t_y])
    rotation = rotation_matrix(theta)

    transform = np.eye(3, dtype=float)
    transform[:2, :2] = rotation
    transform[:2, 2] = translation

    return transform


def transform_points(transform, points):

    n_points, dim = points.shape

    x = np.ones((dim + 1, n_points))
    x[:dim, :] = points.T

    transformed = transform @ x
    normalized = transformed[:dim, :] / transformed[-1, :]

    return np.array(normalized.T, dtype=np.float32)

Let’s define two tranformations:

T_obj = create_transform(w / 2, h / 2, 0)
T_rot = create_transform(0, 0, 0.2)

print(f'T_obj =\n{T_obj}')
print()
print(f'T_rot =\n{T_rot}')
T_obj =
[[ 1.000 -0.000  200.000]
 [ 0.000  1.000  150.000]
 [ 0.000  0.000  1.000]]

T_rot =
[[ 0.980 -0.199  0.000]
 [ 0.199  0.980  0.000]
 [ 0.000  0.000  1.000]]

As the source points, we will move the points around the origin to be around the center (T_obj). For the destination points, we will apply T_obj first, followed by rotation T_rot, which is equivalent to the matrix multiplication T_obj @ T_rot:

src_at_center = transform_points(T_obj, src_at_origin)
dst_at_center = transform_points(T_obj @ T_rot, src_at_origin)

M_rot = cv2.getPerspectiveTransform(src_at_center, dst_at_center)
im_rotated = cv2.warpPerspective(im, M_rot, (w, h), flags=cv2.INTER_LINEAR)

_, (ax_original, ax_warped) = plt.subplots(1, 2)
ax_original.imshow(im, interpolation="none")
ax_warped.imshow(im_rotated, interpolation="none")
plt.show()

png

Below is a visualization of the pairs of source and destiation points used in the rotation example:

_, ax = plt.subplots()
ax.axis("equal")
ax.invert_yaxis()
ax.scatter(src_at_center[:, 0], src_at_center[:, 1], color='blue')
ax.scatter(dst_at_center[:, 0], dst_at_center[:, 1], color='orange')
ax.axhline(h / 2, color='gray', linestyle='--')
ax.axvline(w / 2, color='gray', linestyle='--')

for i, j in ((0, 1), (1, 2), (2, 3), (3, 0)):
    ax.plot([src_at_center[i, 0], src_at_center[j, 0]], 
            [src_at_center[i, 1], src_at_center[j, 1]], 
            color='blue', linewidth=1)
    
    ax.plot([dst_at_center[i, 0], dst_at_center[j, 0]],
            [dst_at_center[i, 1], dst_at_center[j, 1]], 
            color='orange', linewidth=1)

plt.show()

png

Useful resources: