My application: I am trying to rotate an image (using OpenCV and Python)
At the moment I have developed the below code which rotates an input image, padding it with black borders, giving me A. What I want is B - the largest possible area crop window within the rotated image. I call this the axis-aligned boundED box.
This is essentially the same as Rotate and crop, however I cannot get the answer on that question to work. Additionally, that answer is apparently only valid for square images. My images are rectangular.
Code to give A:
import cv2
import numpy as np
def getTranslationMatrix2d(dx, dy):
"""
Returns a numpy affine transformation matrix for a 2D translation of
(dx, dy)
"""
return np.matrix([[1, 0, dx], [0, 1, dy], [0, 0, 1]])
def rotateImage(image, angle):
"""
Rotates the given image about it's centre
"""
image_size = (image.shape[1], image.shape[0])
image_center = tuple(np.array(image_size) / 2)
rot_mat = np.vstack([cv2.getRotationMatrix2D(image_center, angle, 1.0), [0, 0, 1]])
trans_mat = np.identity(3)
w2 = image_size[0] * 0.5
h2 = image_size[1] * 0.5
rot_mat_notranslate = np.matrix(rot_mat[0:2, 0:2])
tl = (np.array([-w2, h2]) * rot_mat_notranslate).A[0]
tr = (np.array([w2, h2]) * rot_mat_notranslate).A[0]
bl = (np.array([-w2, -h2]) * rot_mat_notranslate).A[0]
br = (np.array([w2, -h2]) * rot_mat_notranslate).A[0]
x_coords = [pt[0] for pt in [tl, tr, bl, br]]
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 [tl, tr, bl, br]]
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))
new_image_size = (new_w, new_h)
new_midx = new_w * 0.5
new_midy = new_h * 0.5
dx = int(new_midx - w2)
dy = int(new_midy - h2)
trans_mat = getTranslationMatrix2d(dx, dy)
affine_mat = (np.matrix(trans_mat) * np.matrix(rot_mat))[0:2, :]
result = cv2.warpAffine(image, affine_mat, new_image_size, flags=cv2.INTER_LINEAR)
return result
Inspired by Coprox's amazing work I wrote a function that forms together with Coprox's code a complete solution (so it can be used by copying & pasting with no-brainer). The rotate_max_area function below simply returns a rotated image without black boundary.
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.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.
The math behind this solution/implementation is equivalent to this solution of an analagous question, but the formulas are simplified and avoid singularities. This is python code with the same interface as
largest_rotated_rect
from the other solution, but giving a bigger area in almost all cases (always the proven optimum):Here is a comparison of the function with the other solution:
With angle
a
in[0,pi/2[
the bounding box of the rotated image (widthw
, heighth
) has these dimensions:w_bb = w*cos(a) + h*sin(a)
h_bb = w*sin(a) + h*cos(a)
If
w_r
,h_r
are the computed optimal width and height of the cropped image, then the insets from the bounding box are:(w_bb-w_r)/2
(h_bb-h_r)/2
Proof:
Looking for the axis aligned rectangle between two parallel lines that has maximal area is an optimization problem with one parameter, e.g.
x
as in this figure:Let
s
denote the distance between the two parallel lines (it will turn out to be the shorter side of the rotated rectangle). Then the sidesa
,b
of the sought-after rectangle have a constant ratio withx
,s-x
, resp., namely x = a sin α and (s-x) = b cos α:So maximizing the area
a*b
means maximizingx*(s-x)
. Because of "theorem of height" for right-angled triangles we knowx*(s-x) = p*q = h*h
. Hence the maximal area is reached atx = s-x = s/2
, i.e. the two corners E, G between the parallel lines are on the mid-line:This solution is only valid if this maximal rectangle fits into the rotated rectangle. Therefore the diagonal
EG
must not be longer than the other sidel
of the rotated rectangle. SinceEG = AF + DH = s/2*(cot α + tan α) = s/(2*sin αcos α) = s/sin 2α
we have the condition s ≤ lsin 2α, where s and l are the shorter and longer side of the rotated rectangle.
In case of s > lsin 2α the parameter
x
must be smaller (than s/2) and s.t. all corners of the sought-after rectangle are each on a side of the rotated rectangle. This leads to the equationx*cot α + (s-x)*tan α = l
giving x = sin α(lcos α - ssin α)/cos 2α. From a = x/sin α and b = (s-x)/cos α we get the above used formulas.
A small update for brevity that makes use of the excellent imutils library.
Congratulations for the great work! I wanted to use your code in OpenCV with the C++ library, so I did the conversion that follows. Maybe this approach could be helpful to other people.
There is an easy way to take care of this issue which uses another module called PIL (helpful only if you okay with not using opencv)
The code below does exactly the same and roates any image in such a way that you won't get the black pixels