Rotate Image and Crop Out Black Borders

Rotate image and crop out black borders

So, after investigating many claimed solutions, I have finally found a method that works; The answer by Andri and Magnus Hoff on Calculate largest rectangle in a rotated rectangle.

The below Python code contains the method of interest - largest_rotated_rect - and a short demo.

import math
import cv2
import numpy as np

def rotate_image(image, angle):
"""
Rotates an OpenCV 2 / NumPy image about it's centre by the given angle
(in degrees). The returned image will be large enough to hold the entire
new image, with a black background
"""

# Get the image size
# No that's not an error - NumPy stores image matricies backwards
image_size = (image.shape[1], image.shape[0])
image_center = tuple(np.array(image_size) / 2)

# Convert the OpenCV 3x2 rotation matrix to 3x3
rot_mat = np.vstack(
[cv2.getRotationMatrix2D(image_center, angle, 1.0), [0, 0, 1]]
)

rot_mat_notranslate = np.matrix(rot_mat[0:2, 0:2])

# Shorthand for below calcs
image_w2 = image_size[0] * 0.5
image_h2 = image_size[1] * 0.5

# Obtain the rotated coordinates of the image corners
rotated_coords = [
(np.array([-image_w2, image_h2]) * rot_mat_notranslate).A[0],
(np.array([ image_w2, image_h2]) * rot_mat_notranslate).A[0],
(np.array([-image_w2, -image_h2]) * rot_mat_notranslate).A[0],
(np.array([ image_w2, -image_h2]) * rot_mat_notranslate).A[0]
]

# Find the size of the new image
x_coords = [pt[0] for pt in rotated_coords]
x_pos = [x for x in x_coords if x > 0]
x_neg = [x for x in x_coords if x < 0]

y_coords = [pt[1] for pt in rotated_coords]
y_pos = [y for y in y_coords if y > 0]
y_neg = [y for y in y_coords if y < 0]

right_bound = max(x_pos)
left_bound = min(x_neg)
top_bound = max(y_pos)
bot_bound = min(y_neg)

new_w = int(abs(right_bound - left_bound))
new_h = int(abs(top_bound - bot_bound))

# We require a translation matrix to keep the image centred
trans_mat = np.matrix([
[1, 0, int(new_w * 0.5 - image_w2)],
[0, 1, int(new_h * 0.5 - image_h2)],
[0, 0, 1]
])

# Compute the tranform for the combined rotation and translation
affine_mat = (np.matrix(trans_mat) * np.matrix(rot_mat))[0:2, :]

# Apply the transform
result = cv2.warpAffine(
image,
affine_mat,
(new_w, new_h),
flags=cv2.INTER_LINEAR
)

return result

def largest_rotated_rect(w, h, angle):
"""
Given a rectangle of size wxh that has been rotated by 'angle' (in
radians), computes the width and height of the largest possible
axis-aligned rectangle within the rotated rectangle.

Original JS code by 'Andri' and Magnus Hoff from Stack Overflow

Converted to Python by Aaron Snoswell
"""

quadrant = int(math.floor(angle / (math.pi / 2))) & 3
sign_alpha = angle if ((quadrant & 1) == 0) else math.pi - angle
alpha = (sign_alpha % math.pi + math.pi) % math.pi

bb_w = w * math.cos(alpha) + h * math.sin(alpha)
bb_h = w * math.sin(alpha) + h * math.cos(alpha)

gamma = math.atan2(bb_w, bb_w) if (w < h) else math.atan2(bb_w, bb_w)

delta = math.pi - alpha - gamma

length = h if (w < h) else w

d = length * math.cos(alpha)
a = d * math.sin(alpha) / math.sin(delta)

y = a * math.cos(gamma)
x = y * math.tan(gamma)

return (
bb_w - 2 * x,
bb_h - 2 * y
)

def crop_around_center(image, width, height):
"""
Given a NumPy / OpenCV 2 image, crops it to the given width and height,
around it's centre point
"""

image_size = (image.shape[1], image.shape[0])
image_center = (int(image_size[0] * 0.5), int(image_size[1] * 0.5))

if(width > image_size[0]):
width = image_size[0]

if(height > image_size[1]):
height = image_size[1]

x1 = int(image_center[0] - width * 0.5)
x2 = int(image_center[0] + width * 0.5)
y1 = int(image_center[1] - height * 0.5)
y2 = int(image_center[1] + height * 0.5)

return image[y1:y2, x1:x2]

def demo():
"""
Demos the largest_rotated_rect function
"""

image = cv2.imread("lenna_rectangle.png")
image_height, image_width = image.shape[0:2]

cv2.imshow("Original Image", image)

print "Press [enter] to begin the demo"
print "Press [q] or Escape to quit"

key = cv2.waitKey(0)
if key == ord("q") or key == 27:
exit()

for i in np.arange(0, 360, 0.5):
image_orig = np.copy(image)
image_rotated = rotate_image(image, i)
image_rotated_cropped = crop_around_center(
image_rotated,
*largest_rotated_rect(
image_width,
image_height,
math.radians(i)
)
)

key = cv2.waitKey(2)
if(key == ord("q") or key == 27):
exit()

cv2.imshow("Original Image", image_orig)
cv2.imshow("Rotated Image", image_rotated)
cv2.imshow("Cropped Image", image_rotated_cropped)

print "Done"

if __name__ == "__main__":
demo()

Image Rotation Demo

Simply place this image (cropped to demonstrate that it works with non-square images) in the same directory as the above file, then run it.

Is it possible to remove black borders from rotated image?

yesno = Image.open('images\yesno.jpg').convert('RGBA')
#
asset = member.avatar_url_as(size = 128)
data = BytesIO(await asset.read())
pfp = Image.open(data).convert('RGBA')
pfp = pfp.resize((100,100))
pfp = pfp.rotate(-50,fillcolor = 0)
#
yesno.paste(pfp, (138,408),pfp)
#
yesno.save('proba.png')

I figured out how to do it, here's the code example which worked for me, if someone needs

Remove the black border and keep the image size same in python

you can try to crop the image as much as part of the image you want. Although the cropping in python is possible in the form of pixels. Hence, you can try to crop the image as much is required.
The following code might help you to crop

im=Image.open(r"specify the path of the image")`

width, height = im.size

left = "specify the value in pixels"
top = "specify the value in pixels"
right = "specify the value in pixels"
bottom = "specify the value in pixels"
# Cropped image of above dimension
# (It will not change original image)
im1 = im.crop((left, top, right, bottom))
#im1.show()
im1.save('specify destination path')

Rotate an image without black area

If you want to 'clone' or 'heal' the missing areas based on some part of the background, that's a complex problem, usually done with user intervention (in tools like Photoshop or GIMP).

Alternatives would be to fill the background with a calculated average colour - or just leave the original image. Neither will look 'natural' though.

The only approach that will work for all images will be to crop the rotated image to the largest rectangle within the rotated area. That will achieve your objective of having no black areas and looking natural, but at the cost of reducing the image size.

Rotate and crop

Replace these lines at around the end of your code:

$degrees = rand(1,360);
$square_image = imagerotate($square_image, $degrees, 0);
imagejpeg($square_image,NULL,100);

With this:

$degrees = rand(1,360);
$square_image = imagerotate($square_image, $degrees, 0);

$rotated_size = imagesx($square_image);
$enlargement_coeff = ($rotated_size - $square_size) * 1.807;
$enlarged_size = round($rotated_size + $enlargement_coeff);
$enlarged_image = imagecreatetruecolor($enlarged_size, $enlarged_size);
$final_image = imagecreatetruecolor($square_size, $square_size);

imagecopyresampled($enlarged_image, $square_image, 0, 0, 0, 0, $enlarged_size, $enlarged_size, $rotated_size, $rotated_size);
imagecopyresampled($final_image, $enlarged_image, 0, 0, round($enlarged_size / 2) - ($square_size / 2), round($enlarged_size / 2) - ($square_size / 2), $square_size, $square_size, $square_size, $square_size);

imagejpeg($final_image,NULL,100);

Here's the logic behind that:

1) After performing imagerotate() our new image has changed its dimensions, since every rotation generally results in a larger image. Since the source is a square image we take either the width or the height in order to determine the dimensions of the rotated image.

2) When the original image is rotated, even a little bit, the dimensions of the largest square of usable pixel data from the original image will always be smaller than the original unrotated square image. Therefore, in order to generate a new square image of the same size as the initial square image, but without the "black border" artifact, as you call it, we need to enlarge the rotated image, so that the largest square of usable pixel data from the original image in the rotated image can become as big as the initial square image.

The key value here is 1.807. This value basically shows how many pixels you need to enlarge a rotated image for each pixel of difference between its dimensions and the dimensions of the original unrotated image. There's probably a better Math formula to retrieve this value, unfortunately I suck at Math, so here's the hard way of coming up with that value.

  • A rotation of 45 / 135 / 225 / 315 degrees will always produce the largest image with the smallest usable pixel data square.
  • Knowing this, you compare the dimensions of the original image and its 45-degrees-rotated version. In our case the original image is 200x200 and a 45-degrees-rotated version is about 283x283
  • In a program like Photoshop, you determine how many times you need to enlarge the 45-degrees-rotated version of the image in order to be able to extract a 200x200 square from it, without a "black border" - in our case the 283x283 image needed to be enlarged to a 433x433 image, so we could extract a 200x200 square
  • 433 - 283 = 150 -> meaning we need to enlarge the largest possible rotated image with 150 pixels in order to be able to extract a 200x200 square from it.
  • 283 - 200 = 83 -> 83 pixels is the difference between the largest possible rotated image and the original unrotated image.
  • The "smaller" the transformation - the "larger" the square area we can use and thus - the "smaller" the amount of enlargement we need to apply. And since a 45 degree rotation resulted in a difference of 83 pixels between the original image and the transformed image that required a 150 pixel enlargement, we can do:
  • 150 / 83 = 1.807 -> meaning a difference of 1 pixel between the original image and the rotated image requires that the rotated image is enlarged with 1.807 pixels, so that we can extract a square from it that has the same dimensions as the original image

3) Knowing that for each 1 pixel difference we need to enlarge with 1.807 pixels, we check what's the difference between our rotated image size and original image size and multiply it by that value, to see what dimensions should the enlarged image have:

$enlargement_coeff = ($rotated_size - $square_size) * 1.807;
$enlarged_size = round($rotated_size + $enlargement_coeff);

4) We go ahead and generate the enlarged rotated image.

imagecopyresampled($enlarged_image, $square_image, 0, 0, 0, 0, $enlarged_size, $enlarged_size, $rotated_size, $rotated_size);

5) Finally, we extract a 200x200 square from our enlarged rotated image, using its center coordinates as reference

imagecopyresampled($final_image, $enlarged_image, 0, 0, round($enlarged_size / 2) - ($square_size / 2), round($enlarged_size / 2) - ($square_size / 2), $square_size, $square_size, $square_size, $square_size);

To break that down ($square_size / 2) returns the X and Y coordinates of the center point in the enlarged rotated image. round($enlarged_size / 2) returns the amount of pixels that you need left from the center along the X axis, and above the center along the Y axis, in order to get a 200x200 square.


I hope you understand the logic, although I'm understanding my explanation may sound a bit ambiguous, so please feel free to ask more!

Remove Black Bars/Excess from Image rotated with PIL

The proper answer to this is to ensure your image is in the correct mode. Even if you load an image that supports transparency (such as a .png), you will still encounter this error unless you convert the image mode from its default to 'RGBA', like so:

image = Image.open('image.png').convert('RGBA')

Which then allows the rotation to be completely scuff-free!

Sample Image

I decided to post this question/answer combo because I searched for hours to find this answer, and I thought I'd make it more accessible to anybody else who experiences my issue!

Here's where I found the answer, on an old forum from long, long ago:

    rotate image with transparent background?

Is there another way to fill the area outside a rotated image with white color? 'fillcolor' does not work with older versions of Python

You can try Interpolating the Original Image, with the cropped one via Image.composite() to get rid of the black bars/borders.

from PIL import Image

img = Image.open(r"Image_Path").convert("RGBA")

angle = 30

img = img.rotate(angle)

new_img = Image.new('RGBA', img.size, 'white')

Alpha_Image = Image.composite(img, new_img, img)

Alpha_Image = Alpha_Image.convert(img.mode)

Alpha_Image.show()

The above code takes in an Image, converts it into mode RGBA (Alpha is required for this process), and then rotates the Image by 30 degrees. After that It creates a empty Image object of mode RGBA of the same dimensions as the original image, with each pixel having a default value of 255 each channel (i.e Pure white for RGB, and Full Opacity in the context of Alpha/Transparency). Then Interpolates the original image with this empty one using the mask of original Image (we are using the transparency mask of the first image). This results in the Desired images, where black bars/edges are replaced by white. In the end we convert the image color space to the original one.

ORIGINAL IMAGE:-

Sample Image

IMAGE AFTER ROTATING 30 DEGREES:-

Sample Image



Related Topics



Leave a reply



Submit