18

minAreaRect in OpenCV returns a rotated rectangle. How do I crop this part of the image which is inside the rectangle?

boxPoints returns the co-ordinates of the corner points of the rotated rectangle so one can access the pixels by looping through the points inside the box, but is there a faster way to crop in Python?

EDIT

See code in my answer below.

Abdul Fatir
  • 5,903
  • 5
  • 27
  • 56
  • You can: 1) create the mask for the rotated rect (easy enough with `fillConvexPoly` or `drawContours(... CV_FILLED)`). 2) Black initialize a matrix the same size as the original. 3) Copy only the content of the mask in the new image (`new_image.setTo(old_image, mask)`), 4) Crop the new image on the bounding box of the rotated rectangle – Miki May 12 '16 at 08:59
  • Possible duplicate of [How to straighten a rotated rectangle area of an image using opencv in python?](https://stackoverflow.com/questions/11627362/how-to-straighten-a-rotated-rectangle-area-of-an-image-using-opencv-in-python) – jdhao Dec 21 '18 at 08:12

4 Answers4

34

here a function that does this task:

import cv2
import numpy as np

def crop_minAreaRect(img, rect):

    # rotate img
    angle = rect[2]
    rows,cols = img.shape[0], img.shape[1]
    M = cv2.getRotationMatrix2D((cols/2,rows/2),angle,1)
    img_rot = cv2.warpAffine(img,M,(cols,rows))

    # rotate bounding box
    rect0 = (rect[0], rect[1], 0.0) 
    box = cv2.boxPoints(rect0)
    pts = np.int0(cv2.transform(np.array([box]), M))[0]    
    pts[pts < 0] = 0

    # crop
    img_crop = img_rot[pts[1][1]:pts[0][1], 
                       pts[1][0]:pts[2][0]]

    return img_crop

here an example usage

# generate image
img = np.zeros((1000, 1000), dtype=np.uint8)
img = cv2.line(img,(400,400),(511,511),1,120)
img = cv2.line(img,(300,300),(700,500),1,120)

# find contours / rectangle
_,contours,_ = cv2.findContours(img, 1, 1)
rect = cv2.minAreaRect(contours[0])

# crop
img_croped = crop_minAreaRect(img, rect)

# show
import matplotlib.pylab as plt
plt.figure()
plt.subplot(1,2,1)
plt.imshow(img)
plt.subplot(1,2,2)
plt.imshow(img_croped)
plt.show()

this is the output

original and croped image

Rich
  • 11,049
  • 9
  • 58
  • 91
Oliver Wilken
  • 2,454
  • 1
  • 22
  • 31
  • 1
    That's exactly what I want and the function is totally clear! However I haven't been able to get it to work with this image. https://i.imgur.com/4E8ILuI.jpg It ends up rotated slightly wrong and the edges cut off. Would you be willing to take a look at it? – Hatshepsut Jul 24 '17 at 18:33
  • 6
    The image should be rotated around the center of the rotated rectangle, not the image center. There is a correct answer [here](https://stackoverflow.com/a/48553593/6064933). – jdhao Dec 20 '18 at 01:49
  • Thanks to @oliver-wilken for decision, but in case if **angle = 90** then you get division by zero. My decision is change `rect0 = (rect[0], rect[1], 0.0)` on `rect0 = (rect[0], rect[1], angle)`. – Volkov Maxim Nov 16 '21 at 18:54
14

Here's the code to perform the above task. To speed up the process, instead of first rotating the entire image and cropping, part of the image which has the rotated rectangle is first cropped, then rotated, and cropped again to give the final result.

# Let cnt be the contour and img be the input

rect = cv2.minAreaRect(cnt)  
box = cv2.boxPoints(rect) 
box = np.int0(box)

W = rect[1][0]
H = rect[1][1]

Xs = [i[0] for i in box]
Ys = [i[1] for i in box]
x1 = min(Xs)
x2 = max(Xs)
y1 = min(Ys)
y2 = max(Ys)

angle = rect[2]
if angle < -45:
    angle += 90

# Center of rectangle in source image
center = ((x1+x2)/2,(y1+y2)/2)
# Size of the upright rectangle bounding the rotated rectangle
size = (x2-x1, y2-y1)
M = cv2.getRotationMatrix2D((size[0]/2, size[1]/2), angle, 1.0)
# Cropped upright rectangle
cropped = cv2.getRectSubPix(img, size, center)
cropped = cv2.warpAffine(cropped, M, size)
croppedW = H if H > W else W
croppedH = H if H < W else W
# Final cropped & rotated rectangle
croppedRotated = cv2.getRectSubPix(cropped, (int(croppedW),int(croppedH)), (size[0]/2, size[1]/2))
Abdul Fatir
  • 5,903
  • 5
  • 27
  • 56
14

@AbdulFatir was on to a good solution but as the comments stated(@Randika @epinal) it wasn't quite working for me either so I modified it slightly and it seems to be working for my case. here is the image I am using.mask_of_image

im, contours, hierarchy = cv2.findContours(open_mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
print("num of contours: {}".format(len(contours)))


mult = 1.2   # I wanted to show an area slightly larger than my min rectangle set this to one if you don't
img_box = cv2.cvtColor(img.copy(), cv2.COLOR_GRAY2BGR)
for cnt in contours:
    rect = cv2.minAreaRect(cnt)
    box = cv2.boxPoints(rect)
    box = np.int0(box)
    cv2.drawContours(img_box, [box], 0, (0,255,0), 2) # this was mostly for debugging you may omit

    W = rect[1][0]
    H = rect[1][1]

    Xs = [i[0] for i in box]
    Ys = [i[1] for i in box]
    x1 = min(Xs)
    x2 = max(Xs)
    y1 = min(Ys)
    y2 = max(Ys)

    rotated = False
    angle = rect[2]

    if angle < -45:
        angle+=90
        rotated = True

    center = (int((x1+x2)/2), int((y1+y2)/2))
    size = (int(mult*(x2-x1)),int(mult*(y2-y1)))
    cv2.circle(img_box, center, 10, (0,255,0), -1) #again this was mostly for debugging purposes

    M = cv2.getRotationMatrix2D((size[0]/2, size[1]/2), angle, 1.0)

    cropped = cv2.getRectSubPix(img_box, size, center)    
    cropped = cv2.warpAffine(cropped, M, size)

    croppedW = W if not rotated else H 
    croppedH = H if not rotated else W

    croppedRotated = cv2.getRectSubPix(cropped, (int(croppedW*mult), int(croppedH*mult)), (size[0]/2, size[1]/2))

    plt.imshow(croppedRotated)
    plt.show()

plt.imshow(img_box)
plt.show()

This should produce a series of images like these: isolated contour 1isolated contour 2isolated contour 3

And it will also give a result image like this: results

mkrinblk
  • 836
  • 9
  • 20
2

You have not given sample code, so I am answering without code also. You could proceed as follows:

  1. From corners of rectangle, determine angle alpha of rotation against horizontal axis.
  2. Rotate image by alpha so that cropped rectangle is parallel to image borders. Make sure that the temporary image is larger in size so that no information gets lost (cf: Rotate image without cropping OpenCV)
  3. Crop image using numpy slicing (cf: How to crop an image in OpenCV using Python)
  4. Rotate image back by -alpha.
Community
  • 1
  • 1
tfv
  • 5,704
  • 3
  • 31
  • 62
  • 1
    For a large image, wouldn't this be expensive? – Abdul Fatir May 12 '16 at 06:23
  • My guess is that the built-in functions will always be quicker than doing a nested loop over pixels. But the only way to find this out is to measure it, it's just a fey lines of code as described above. – tfv May 12 '16 at 06:26
  • If I have lot of rects then it'll be probably show. I'll code and get back to you after trying. – Abdul Fatir May 12 '16 at 06:28
  • There may be other ways depening on what you want to do with the cropped image: do you want to use it in its original orientation, or rotated such that the borders of the cropped region are parallel to the image borders? – tfv May 12 '16 at 06:30
  • No, I don't want to use it in its original orientation. Just want to extract some info from cropped section. – Abdul Fatir May 12 '16 at 06:31