Search code examples
pythonmathimage-processinggeometrypython-imaging-library

Rotate an image in Python, and get the original position of a pixel which is in the rotated image


One part of the project I am currently working on, requires me to achieve the following:

  1. Rotate an image by any specified angle (theta let's say), without cropping out any portion of the image (padding is allowed)
  2. If I have a pixel at x,y in the original image, find the x_r,y_r in the rotated image, where the pixel lies.
  3. If I have a pixel at x_r, y_r in the rotated image, find the x,y where this pixel lies in the original image.

I tried using PIL's image.rotate function for rotation and kept expand=True.

I developed a pixel reversal function which works fine for 90 degree rotation , but on using angles between 0 and 90, it doesn't work properly.

Here's a couple of images showing the rotation and it's results, with the red dot indicating the two pixels that are supposed to be identical. The image on the left is the original, whereas the one on the right is the rotated one

θ=90 θ=45

Here's the code I wrote so far.

import math
from PIL import Image, ImageDraw
from icecream import ic

def draw_circle(img, x, y,title):
    # Create an ImageDraw object to draw on the image
    draw = ImageDraw.Draw(img)

    # Set the coordinates for the center of the point
    point_x, point_y = x, y
    # Set the size (width and height) of the point (making it bigger)
    point_size = 50  # You can adjust this value to change the size of the point

    # Set the color of the point (red in this example, represented as RGB)
    point_color = (255, 0, 0)

    # Calculate the bounding box for the ellipse
    point_bbox = (
        point_x - point_size // 2,
        point_y - point_size // 2,
        point_x + point_size // 2,
        point_y + point_size // 2,
    )

    # Draw the ellipse (representing the point) on the image
    draw.ellipse(point_bbox, fill=point_color, outline=point_color)

    img.show(title=title)

    # Close the ImageDraw object
    del draw

def rotate_pixel(x, y, theta,rotated_img):
    """
    Give this function, the X,Y positions from the original image, the angle by which the original image is rotated,
    and the rotated image itself.

    Returns: The X,Y coordinates of that exact pixel in the rotated image

    To reverse the rotation, give the resulting X,Y coordinates, along with -theta, and the original image
    
    
    """
    rotated_img_width, rotated_img_height = rotated_img.size

    # Convert theta to radians
    theta_rad = math.radians(theta)
    
    # Calculate the new x and y coordinates after rotation
    new_x = round(x * math.cos(theta_rad) - y * math.sin(theta_rad))
    new_y = round(x * math.sin(theta_rad) + y * math.cos(theta_rad))
    
    if new_y < 0:
        new_y = rotated_img_height + new_y
    elif new_x < 0:
        new_x = rotated_img_width + new_x

    return new_x, new_y



strip_image_path = '../../dumb_data/perfect_strip.png'
angle = 90

img = Image.open(strip_image_path)
rot_img = img.rotate(angle, expand=True)
width,height = img.size
rot_width,rot_height = rot_img.size
original_x, original_y = 500, 100

rotated_x, rotated_y = rotate_pixel(original_x,original_y,-angle,rotated_img=rot_img) # rotation_coordinates(img, +angle, original_x, original_y)
reverse_x, reverse_y = rotate_pixel(rotated_x,rotated_y,angle,rotated_img=img) #rotation_coordinates(img, -angle, rotated_x, rotated_y)

# Define the image dimensions
image_width, image_height = img.size

print()
ic(img.size)
ic(rot_img.size)
ic(original_x, original_y)
ic(rotated_x, rotated_y)
ic(reverse_x, reverse_y)

draw_circle(img, original_x, original_y,title="original")
draw_circle(rot_img, rotated_x, rotated_y,title="rotated")


Any solutions which achieve the general above mentioned goal are welcome


Solution

  • Here is a solution using PIL with type hints and docstrings.

    The image is uploaded and rotated, but without cropping, using the method provided by the PIL library itself. Then, it's just about applying a simple rotation on the plane.

    If it's not what you are looking for, I will try to make it better.

    from pathlib import Path
    from typing import Tuple
    
    import numpy as np
    from PIL import Image
    from matplotlib import pyplot as plt
    
    
    def main(image_path_: Path, angle: float, pixel_coordinates: Tuple[int, int]) -> None:
        """
        Entry point.
    
        Parameters
        ----------
        image_path_ : Path
        angle : float
        pixel_coordinates : Tuple[int, int]
    
        """
        image = Image.open(image_path_)
        rotated_image = rotate_image(image, angle_)
        transformed_pixel_coordinates = transform_pixel_coordinates(
            pixel_coordinates, angle, image, rotated_image
        )
        draw_images(image, rotated_image, pixel_coordinates_, transformed_pixel_coordinates)
    
    
    def rotate_image(image: Image, angle: float) -> Image:
        """
        Rotate image.
    
        Parameters
        ----------
        image : Image
        angle : float
    
        Returns
        -------
        Image
    
        """
        return image.rotate(angle, expand=True)
    
    
    def transform_pixel_coordinates(
        pixel_coordinates: Tuple[int, int],
        angle: float,
        image: Image,
        rotated_image: Image,
    ) -> Tuple[int, int]:
        """
        Transform pixel coordinates.
    
        Parameters
        ----------
        pixel_coordinates : Tuple[int, int]
        angle : float
        image : Image
        rotated_image : Image
    
        Returns
        -------
        Tuple[int, int]
    
        """
    
        x, y = pixel_coordinates
    
        center = (image.width / 2, image.height / 2)
        transformed_center = (rotated_image.width / 2, rotated_image.height / 2)
    
        angle_radians = -np.deg2rad(angle)
    
        x -= center[0]
        y -= center[1]
    
        x_transformed = x * np.cos(angle_radians) - y * np.sin(angle_radians)
        y_transformed = x * np.sin(angle_radians) + y * np.cos(angle_radians)
    
        x_transformed += transformed_center[0]
        y_transformed += transformed_center[1]
    
        return int(x_transformed), int(y_transformed)
    
    
    def draw_images(
        image, rotated_image, pixel_coordinates, transformed_pixel_coordinates
    ) -> None:
        """
        Draw images and pixel.
    
        Parameters
        ----------
        image : Image
        rotated_image : Image
        pixel_coordinates : Tuple[int, int]
        transformed_pixel_coordinates : Tuple[int, int]
    
        """
        fig, axes = plt.subplots(1, 2, figsize=(12, 6))
    
        axes[0].imshow(image)
        axes[0].scatter(*pixel_coordinates, color="y", s=50)
        axes[0].set_title("Image")
        axes[0].axis("off")
    
        axes[1].imshow(rotated_image)
        axes[1].scatter(*transformed_pixel_coordinates, color="y", s=50)
        axes[1].set_title("Rotated Image")
        axes[1].axis("off")
        plt.show()
    
    
    if __name__ == "__main__":
        image_path = Path("test.png")
        angle_ = 30
        pixel_coordinates_ = (100, 200)
        main(image_path, angle_, pixel_coordinates_)
    

    enter image description here