Rotation of images
About 909 wordsAbout 3 min
2026-07-30
Performing arbitrary angle rotations on images is a common task in image processing. But if you try it yourself, you may find that it is not as straightforward as it seems. The main challenge is that the rotated image may not align perfectly with the original pixel grid, leading to some pixels being left out or interpolated. This also results in the rotated image having a different size than the original image.
First, to ensure the rotated image is fully contained within the output image, we need to calculate the size of the output image based on the rotation angle. The following figure illustrates this concept:

So the size of the output image can be calculated as follows:
output_width=⌈∣width⋅cos(θ)∣+∣height⋅sin(θ)∣⌉
output_height=⌈∣height⋅cos(θ)∣+∣width⋅sin(θ)∣⌉
Second, we move the origin of the coordinate system to the center of the image, so that we can rotate the image around its center. The following figure illustrates this concept:

The center of the original image is at (xsc,ysc)=(2width−1,2height−1), and the center of the output image is at (xoc,yoc)=(2output_width−1,2output_height−1).
Then, we do an inverse rotation to find the corresponding pixel in the original image for each pixel in the output image. The inverse rotation can be expressed as follows:
[xsys]=[cos(θ)sin(θ)−sin(θ)cos(θ)][xo−xocyo−yoc]+[xscysc]
Note this inverse rotation is a clockwise rotation, which is the opposite of the counterclockwise rotation we want to achieve. This is because we are mapping from the output image back to the original image. Also, the rotation matrix is different from the standard rotation matrix because we are using a coordinate system where the y-axis points downwards, which is common in image processing.
Finally, we can use bilinear interpolation to calculate the pixel value at (xs,ys) in the original image. The following figure illustrates this concept:

A horizontal linear interpolation is performed first, followed by a vertical linear interpolation. The final pixel value is a weighted average of the four neighboring pixels, with weights based on the distances to the neighboring pixels.
[xsys]=[cos(θ)sin(θ)−sin(θ)cos(θ)][xo−xocyo−yoc]+[xscysc]
Note this inverse rotation is a clockwise rotation, which is the opposite of the counterclockwise rotation we want to achieve. This is because we are mapping from the output image back to the original image. Also, the rotation matrix is different from the standard rotation matrix because we are using a coordinate system where the y-axis points downwards, which is common in image processing.
Finally, we can use bilinear interpolation to calculate the pixel value at (xs,ys) in the original image. The following figure illustrates this concept:

A horizontal linear interpolation is performed first, followed by a vertical linear interpolation. The final pixel value is a weighted average of the four neighboring pixels, with weights based on the distances to the neighboring pixels.
pixel_value=(1−wx)(1−wy)⋅I(x0,y0)+wx(1−wy)⋅I(x1,y0)++(1−wx)wy⋅I(x0,y1)+wxwy⋅I(x1,y1)
The full python code for rotating an image using bilinear interpolation is as follows:
def manual_rotate_bilinear(mat: np.ndarray, angle_degrees: float, *, expand: bool = True, fill_value = 0):
original_dtype = mat.dtype
source = mat.astype(np.float64)
height, width = source.shape[:2]
theta = np.deg2rad(angle_degrees)
cos_theta = np.cos(theta)
sin_theta = np.sin(theta)
# calculate the size of the rotated picture
theta = np.deg2rad(angle_degrees)
cos_theta = np.cos(theta)
sin_theta = np.sin(theta)
# calculate the size of the rotated picture
if expand:
output_width = int(np.ceil(
abs(width * cos_theta) + abs(height * sin_theta)
))
output_height = int(np.ceil(
abs(height * cos_theta) + abs(width * sin_theta)
))
else:
output_height = height
output_width = width
output_shape = (output_height, output_width) + source.shape[2:]
output = np.full(output_shape, fill_value, dtype=np.float64)
# the center of the source and the output
source_cx = (width - 1) / 2
source_cy = (height - 1) / 2
output_cx = (output_width - 1) / 2
output_cy = (output_height - 1) / 2
for output_y in range(output_height):
for output_x in range(output_width):
# coordinate relative to the center
x = output_x - output_cx
y = output_y - output_cy
# inverse rotation: rotate the source pixels clockwise to the output
source_x = cos_theta * x - sin_theta * y + source_cx
source_y = sin_theta * x + cos_theta * y + source_cy
x0 = int(np.floor(source_x))
y0 = int(np.floor(source_y))
x1 = x0 + 1
y1 = y0 + 1
if x0 < 0 or y0 < 0 or x1 >= width or y1 >= height:
continue
# Fractional position between neighboring pixels.
wx = source_x - x0
wy = source_y - y0
# bilinear interpolation
top = (
(1 - wx) * source[y0, x0]
+ wx * source[y0, x1]
)
bottom = (
(1 - wx) * source[y1, x0]
+ wx * source[y1, x1]
)
output[output_y, output_x] = (
(1 - wy) * top
+ wy * bottom
)
if np.issubdtype(original_dtype, np.integer):
limits = np.iinfo(original_dtype)
output = np.clip(output, limits.min, limits.max)
return output.astype(original_dtype)