How to Do a Perspective Transform in OpenCV, Explained with Images and Coordinates

How to Do a Perspective Transform in OpenCV, Explained with Images and Coordinates

Written by

in

This article uses Python and OpenCV to run a perspective transform: move an image’s four corners onto four new ones. In outline, getPerspectiveTransform builds the matrix and warpPerspective warps the pixels. The example is a 600×600 grid with a person silhouette.

An OpenCV perspective-transform figure, judged by a grid and a person silhouette. It shows that four-corner pairing can make a trapezoid or a parallelogram. The four panels are front, shear, a wide-bottom trapezoid, and one receding right.
The same input, with only the four corner destinations changed

Key points

Look at thisPoint
What we are doingSet where the four corners go, and the plane stretches into a trapezoid
Code and resultRun the script below with the same folder layout as the figure
FoldersPut source_pics and save_pics next to the script
CoordinatesTop-left is [0, 0]. Order is top-left → bottom-left → bottom-right → top-right
The remaining linesRead, size, matrix, warp, save
Output sizePixels outside the third argument are cropped
Your own imageChange only the point order and the output size to the rectangle you want
A constrained cameraEven from an angle, matching the four corners of the real plane still yields a rectangle of data

A perspective transform sets where the four corners go

An affine transform (warpAffine) keeps parallel lines parallel. A perspective transform (getPerspectiveTransform and warpPerspective) can move the four corners independently, so you can make a trapezoid that is wide in front and narrow in the back. Flattening a business card to a rectangle, or looking down on a floor, belong to these functions.

Pass the input corners and the output corners in the same order. This article does not walk through the algebra inside the matrix. If you know where the four corners go, you can use the functions.

The script and before/after

A working script first. The next sections follow the coordinates and the remaining lines.

test_1.py
import numpy as np
import cv2

image = cv2.imread("source_pics/test_1.png", cv2.IMREAD_COLOR)

height, width, channels = image.shape[:3]

source_points = np.array([[0, 0], [0, height], [width, height], [width, 0]], dtype=np.float32)
target_points = np.array([[200, 0], [0, 600], [600, 600], [400, 0]], dtype=np.float32)

mat = cv2.getPerspectiveTransform(source_points, target_points)

perspective_image = cv2.warpPerspective(image, mat, (width, height))
cv2.imwrite("save_pics/perspective_image.png", perspective_image)
A before-and-after of a perspective transform, compared by input file and output file. It shows that pulling the top corners in makes a trapezoid narrow at the top. Left is square test_1.png; right is perspective_image.png.
test_1.png before, and perspective_image.png after

This example pulls the top edge inward, so the result is a trapezoid that is narrow at the top and wide at the bottom. The black margin is the part of the output canvas that sits outside the warped quad.

Put the folders next to the script

A run layout for the script, shown by where the script and the input and output folders sit. It maps the paths in the code to where files are read and written. test_1.py sits beside source_pics and save_pics.
source_pics and save_pics sit next to test_1.py

Paths in the code are relative to the test_1.py you run. The image you read is source_pics/test_1.png; the file you write is save_pics/perspective_image.png. For your own files, match those names or change the path strings.

💡 Tip

If imread fails, image is None and the next line, image.shape, stops. Before you run, check the folder spelling and the current working directory.

Coordinates start at [0, 0] in the top-left

Python
source_points = np.array([[0, 0], [0, height], [width, height], [width, 0]], dtype=np.float32)
target_points = np.array([[200, 0], [0, 600], [600, 600], [400, 0]], dtype=np.float32)

The arrays are in this order. The origin is not at the bottom, as on graph paper.

Ordersource_pointstarget_points
Top-left[0, 0][200, 0]
Bottom-left[0, height][0, 600]
Bottom-right[width, height][600, 600]
Top-right[width, 0][400, 0]

This example is 600×600, so height and width are both 600. Filled in, the arrays look like this.

Python
source_points = np.array([[0, 0], [0, 600], [600, 600], [600, 0]], dtype=np.float32)
target_points = np.array([[200, 0], [0, 600], [600, 600], [400, 0]], dtype=np.float32)
A four-corner map for a perspective transform, referenced to source_points and target_points. It shows that with origin at the top-left, pulling the top edge in shortens the top. [0, 0] and [600, 0] move to [200, 0] and [400, 0].
Pull the top edge inward and the top becomes a narrow trapezoid

The two top points move inward, so the top edge gets shorter. The two bottom points stay put. Change the numbers and the same four-corner pairing makes a different trapezoid.

⚠️ Common Pitfall

If the input order and the output order do not match, the mapping twists. Write both as top-left → bottom-left → bottom-right → top-right. If you pick points on a mathematical y-axis (up is positive), the image looks upside down.

What the remaining lines do

Imports

Python
import numpy as np
import cv2

NumPy holds the point arrays. OpenCV’s cv2 reads, writes, and warps images.

Reading the image

Python
image = cv2.imread("source_pics/test_1.png", cv2.IMREAD_COLOR)

cv2.imread loads test_1.png from source_pics, next to the script, in colour, into image.

Image size

Python
height, width, channels = image.shape[:3]

shape gives height, width, and channel count. This example does not use the channels, but the number of values you unpack has to match, so all three are taken.

💡 Tip

Match the number of variables to the number of values you unpack.

Points for the matrix

Python
source_points = np.array([[0, 0], [0, height], [width, height], [width, 0]], dtype=np.float32)
target_points = np.array([[200, 0], [0, 600], [600, 600], [400, 0]], dtype=np.float32)

Four XY points each. On this source image [height, width] is [600, 600], so the arrays contain:

Python
source_points =  [[  0.   0.]
 [  0. 600.]
 [600. 600.]
 [600.   0.]]
target_points =  [[200.   0.]
 [  0. 600.]
 [600. 600.]
 [400.   0.]]

Those are the coordinates from the figure. The warp uses that pairing.

The transform matrix

Python
mat = cv2.getPerspectiveTransform(source_points, target_points)

cv2.getPerspectiveTransform computes the matrix from the four input corners and the four output corners. In this example the values are:

Python
mat =  [[ 3.33333333e-01 -3.33333333e-01  2.00000000e+02]
 [ 0.00000000e+00  3.33333333e-01  0.00000000e+00]
 [ 0.00000000e+00 -1.11111111e-03  1.00000000e+00]]

You do not need the meaning of each entry here. If the corners go where you intended, pass the return value to the next line.

The warp

Python
perspective_image = cv2.warpPerspective(image, mat, (width, height))

cv2.warpPerspective takes the source image, the matrix, and the output width and height. This example only shortens an edge, so the output size stays (width, height), the same as the source. Pick the real size by looking at the transformed coordinates.

Saving

Python
cv2.imwrite("save_pics/perspective_image.png", perspective_image)

cv2.imwrite writes perspective_image.png into save_pics, next to the script. Create the folder first; the write fails if it is missing.

If the output is too small, the overflow is cropped

Keep the output at 600×600 and take points like these, and the stretched top leaves the canvas.

Python
source_points = np.array([[200, 0], [0, 600], [600, 600], [400, 0]], dtype=np.float32)
target_points = np.array([[0, 0], [0, 600], [600, 600], [600, 0]], dtype=np.float32)
A crop when the output canvas is too small, shown by points that leave the frame. It explains that warpPerspective's third argument is the written image size. The original [0, 0] and [600, 0] sit outside 600×600, so the top is cut.
Leave the output at 600×600 and the stretched top is cut off

The third argument is the size of the image you write, not the size of the warped world. Pixels whose points sit outside that rectangle are gone. To keep them, enlarge the output first, or pick target points that all fit.

⚠️ Common Pitfall

If a target x or y is past the output width or height, and you leave the third argument at the source size, you get a crop. When it looks cut, compare the points with the output size first.

The same steps work on your own photos

Keep the four-corner order and the same two functions work on photos that are not a grid. Change only the target numbers and the output size to the rectangle you want.

Even if the camera sits at an angle, matching four corners still gives you a rectangle

What you did is a short chain: pair the input corners with the output corners, solve for the matrix, and write onto the output canvas. The functions are the same on the grid and on a photo from the floor. On site, only how you pick the points and the output size change.

When the camera position is fixed by the machine

On equipment and inspection rigs, you often cannot put the camera directly above the part. Covers, pipes, guards, and robot reach are decided first; the lens is squeezed in afterwards. Measure or read that image as-is and you still have a trapezoid that is wide in front and narrow in the back, so lengths and positions are skewed.

Pair the four corners and you can recover the rectangle of the plane you want, without rebuilding the frame. Data collection still works when the camera pose is constrained by the structure. The real trick is to put the points on the four corners of the physical plane, not the four corners of the screen. A workpiece corner, a fixture hole, a floor joint — any point you can point to again later — and this article’s steps apply as written.

You do not have to write the maths yourself

OpenCV is one implementation for that job. Laying the points out with NumPy and handing them to OpenCV’s getPerspectiveTransform and warpPerspective is the choice to use a maintained implementation instead of writing the algebra yourself. That choice is not limited to OpenCV. Around image processing, numerics, comms, and control, the same work already has implementations, and people keep them and bump the versions. Reading and using that code gets you back to the machine’s constraints sooner than deriving the projective maths from scratch.

Using it is enough. If you hit a hole or something unclear, you can report it, fix the docs, or send a patch. How much you give back varies. Even zero still puts you on the side that keeps that work going, by waiting for the next release. Fit the tool to your machine, and give a little back if you need to. That loop spends engineering time better than owning a full in-house rewrite.

Even with a camera at an angle, four corners still make a rectangle of data. After that, the real job is to measure, read, and keep what sits on that rectangle.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *