48

The following picture will tell you what I want.

I have the information of the rectangles in the image (width, height, center point and rotation degree). Now, I want to write a script to cut them out and save them as an image, but straighten them as well. As in, I want to go from the rectangle shown inside the image to the rectangle that is shown outside.

I am using OpenCV Python. Please tell me a way to accomplish this.

Kindly show some code as examples of OpenCV Python are hard to find.

Example Image

Joel G
  • 25
  • 2
  • 7
hakunami
  • 2,351
  • 4
  • 31
  • 50
  • possible duplicate of [Executing cv::warpPerspective for a fake deskewing on a set of cv::Point](http://stackoverflow.com/questions/7838487/executing-cvwarpperspective-for-a-fake-deskewing-on-a-set-of-cvpoint) – Sam Jul 24 '12 at 08:59
  • If converting form C++ to Python is not of a problem, the above link should be exactly what you're looking for – Sam Jul 24 '12 at 09:01
  • @vasile Actually, I don't need perspective transformation. I just need the pixels in the rotated rectangle map to the straight rectangle one by one. – hakunami Jul 24 '12 at 09:06
  • 1
    If you want just the corner positions, use perspectiveTransform(). If you want all the pixels, this is warpAffine() or warpPerspective() for – Sam Jul 24 '12 at 09:09
  • @vasile warpAffine is what I need. Thank you. By the way, do you have any knowledge of the resource for opencv python, you know, not only the offical document, but some thing like tutorial, open project etc. – hakunami Jul 25 '12 at 05:49
  • Unfortunately, no. I am a c++ guy, but I think python docs and the many OpenCv c++ tutorials will be enough to get a grasp on computer vision – Sam Jul 25 '12 at 05:55

6 Answers6

65

You can use the warpAffine function to rotate the image around a defined center point. The suitable rotation matrix can be generated using getRotationMatrix2D (where theta is in degrees).

Start Image After finding the desired rectangle

You then can use Numpy slicing to cut the image.

Rotated Image Result

import cv2
import numpy as np

def subimage(image, center, theta, width, height):

   ''' 
   Rotates OpenCV image around center with angle theta (in deg)
   then crops the image according to width and height.
   '''

   # Uncomment for theta in radians
   #theta *= 180/np.pi

   shape = ( image.shape[1], image.shape[0] ) # cv2.warpAffine expects shape in (length, height)

   matrix = cv2.getRotationMatrix2D( center=center, angle=theta, scale=1 )
   image = cv2.warpAffine( src=image, M=matrix, dsize=shape )

   x = int( center[0] - width/2  )
   y = int( center[1] - height/2 )

   image = image[ y:y+height, x:x+width ]

   return image

Keep in mind that dsize is the shape of the output image. If the patch/angle is sufficiently large, edges get cut off (compare image above) if using the original shape as--for means of simplicity--done above. In this case, you could introduce a scaling factor to shape (to enlarge the output image) and the reference point for slicing (here center).

The above function can be used as follows:

image = cv2.imread('owl.jpg')
image = subimage(image, center=(110, 125), theta=30, width=100, height=200)
cv2.imwrite('patch.jpg', image)
Community
  • 1
  • 1
rroowwllaanndd
  • 3,858
  • 1
  • 22
  • 20
20

I had problems with wrong offsets while using the solutions here and in similar questions.

So I did the math and came up with the following solution that works:

def subimage(self,image, center, theta, width, height):
    theta *= 3.14159 / 180 # convert to rad

    v_x = (cos(theta), sin(theta))
    v_y = (-sin(theta), cos(theta))
    s_x = center[0] - v_x[0] * ((width-1) / 2) - v_y[0] * ((height-1) / 2)
    s_y = center[1] - v_x[1] * ((width-1) / 2) - v_y[1] * ((height-1) / 2)

    mapping = np.array([[v_x[0],v_y[0], s_x],
                        [v_x[1],v_y[1], s_y]])

    return cv2.warpAffine(image,mapping,(width, height),flags=cv2.WARP_INVERSE_MAP,borderMode=cv2.BORDER_REPLICATE)

For reference here is an image that explains the math behind it:

Note that

w_dst = width-1
h_dst = height-1

This is because the last coordinate has the value width-1 and not width, or height.

Poul Bak
  • 10,450
  • 5
  • 32
  • 57
xaedes
  • 1,446
  • 2
  • 16
  • 19
  • You can get the correct transformation matrix with a sequence of linear transformations (written in reverse order of application): `TranslateBy((w-1)/2,(h-1)/2)*RotationMatrix(theta)*TranslateBy(-center_x, -center_y)` It must be `w-1` and `h-1`: Just think about a 1x1 image that you rotate around (0,0). The pixel should not be translated by 0.5, but by 0 in the last step. – xaedes Oct 18 '17 at 17:44
17

Similar recipe for openCV version 3.4.0.

from cv2 import cv
import numpy as np

def getSubImage(rect, src):
    # Get center, size, and angle from rect
    center, size, theta = rect
    # Convert to int 
    center, size = tuple(map(int, center)), tuple(map(int, size))
    # Get rotation matrix for rectangle
    M = cv2.getRotationMatrix2D( center, theta, 1)
    # Perform rotation on src image
    dst = cv2.warpAffine(src, M, src.shape[:2])
    out = cv2.getRectSubPix(dst, size, center)
    return out

img = cv2.imread('img.jpg')
# Find some contours
thresh2, contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# Get rotated bounding box
rect = cv2.minAreaRect(contours[0])
# Extract subregion
out = getSubImage(rect, img)
# Save image
cv2.imwrite('out.jpg', out)
abreschi
  • 581
  • 6
  • 10
  • 2
    One change to the code. At cv2.warpAffine(), instead of src.shape[:2] give (src.shape[0],src.shape[1]). Because src.shape[:2] gives (rows,cols). But opencv expects (cols,rows) – vamsidhar muthireddy Feb 23 '19 at 20:03
  • 5
    @vamsidharmuthireddy Thanks for the remark. I believe, you made a typo, however: it should be (src.shape[1] ,src.shape[0]) :) – Yohaï-Eliel Berreby May 13 '21 at 18:26
17

The other methods will work only if the content of the rectangle is in the rotated image after rotation and will fail badly in other situations. What if some of the part are lost? See an example below:

enter image description here

If you are to crop the rotated rectangle text area using the above method,

import cv2
import numpy as np


def main():
    img = cv2.imread("big_vertical_text.jpg")
    cnt = np.array([
            [[64, 49]],
            [[122, 11]],
            [[391, 326]],
            [[308, 373]]
        ])
    print("shape of cnt: {}".format(cnt.shape))
    rect = cv2.minAreaRect(cnt)
    print("rect: {}".format(rect))

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

    print("bounding box: {}".format(box))
    cv2.drawContours(img, [box], 0, (0, 0, 255), 2)

    img_crop, img_rot = crop_rect(img, rect)

    print("size of original img: {}".format(img.shape))
    print("size of rotated img: {}".format(img_rot.shape))
    print("size of cropped img: {}".format(img_crop.shape))

    new_size = (int(img_rot.shape[1]/2), int(img_rot.shape[0]/2))
    img_rot_resized = cv2.resize(img_rot, new_size)
    new_size = (int(img.shape[1]/2)), int(img.shape[0]/2)
    img_resized = cv2.resize(img, new_size)

    cv2.imshow("original contour", img_resized)
    cv2.imshow("rotated image", img_rot_resized)
    cv2.imshow("cropped_box", img_crop)

    # cv2.imwrite("crop_img1.jpg", img_crop)
    cv2.waitKey(0)


def crop_rect(img, rect):
    # get the parameter of the small rectangle
    center = rect[0]
    size = rect[1]
    angle = rect[2]
    center, size = tuple(map(int, center)), tuple(map(int, size))

    # get row and col num in img
    height, width = img.shape[0], img.shape[1]
    print("width: {}, height: {}".format(width, height))

    M = cv2.getRotationMatrix2D(center, angle, 1)
    img_rot = cv2.warpAffine(img, M, (width, height))

    img_crop = cv2.getRectSubPix(img_rot, size, center)

    return img_crop, img_rot


if __name__ == "__main__":
    main()

This is what you will get:

enter image description here

Apparently, some of the parts are cut out! Why do not directly warp the rotated rectangle since we can get its four corner points with cv.boxPoints() method?

import cv2
import numpy as np


def main():
    img = cv2.imread("big_vertical_text.jpg")
    cnt = np.array([
            [[64, 49]],
            [[122, 11]],
            [[391, 326]],
            [[308, 373]]
        ])
    print("shape of cnt: {}".format(cnt.shape))
    rect = cv2.minAreaRect(cnt)
    print("rect: {}".format(rect))

    box = cv2.boxPoints(rect)
    box = np.int0(box)
    width = int(rect[1][0])
    height = int(rect[1][1])

    src_pts = box.astype("float32")
    dst_pts = np.array([[0, height-1],
                        [0, 0],
                        [width-1, 0],
                        [width-1, height-1]], dtype="float32")
    M = cv2.getPerspectiveTransform(src_pts, dst_pts)
    warped = cv2.warpPerspective(img, M, (width, height))

Now the cropped image becomes

enter image description here

Much better, isn't it? If you check carefully, you will notice that there are some black area in the cropped image. That is because a small part of the detected rectangle is out of the bound of the image. To remedy this, you may pad the image a little bit and do the crop after that. There is an example illustrated in this answer.

Now, we compare the two methods to crop the rotated rectangle from the image. This method do not require rotating the image and can deal with this problem more elegantly with less code.

jdhao
  • 24,001
  • 18
  • 134
  • 273
3

This is my C++ version that performs the same task. I have noticed it is a bit slow. If anyone sees anything that would improve the performance of this function, then please let me know. :)

bool extractPatchFromOpenCVImage( cv::Mat& src, cv::Mat& dest, int x, int y, double angle, int width, int height) {

  // obtain the bounding box of the desired patch
  cv::RotatedRect patchROI(cv::Point2f(x,y), cv::Size2i(width,height), angle);
  cv::Rect boundingRect = patchROI.boundingRect();

  // check if the bounding box fits inside the image
  if ( boundingRect.x >= 0 && boundingRect.y >= 0 &&
       (boundingRect.x+boundingRect.width) < src.cols &&  
       (boundingRect.y+boundingRect.height) < src.rows ) { 

    // crop out the bounding rectangle from the source image
    cv::Mat preCropImg = src(boundingRect);

    // the rotational center relative tot he pre-cropped image
    int cropMidX, cropMidY;
    cropMidX = boundingRect.width/2;
    cropMidY = boundingRect.height/2;

    // obtain the affine transform that maps the patch ROI in the image to the
    // dest patch image. The dest image will be an upright version.
    cv::Mat map_mat = cv::getRotationMatrix2D(cv::Point2f(cropMidX, cropMidY), angle, 1.0f);
    map_mat.at<double>(0,2) += static_cast<double>(width/2 - cropMidX);
    map_mat.at<double>(1,2) += static_cast<double>(height/2 - cropMidY);

    // rotate the pre-cropped image. The destination image will be
    // allocated by warpAffine()
    cv::warpAffine(preCropImg, dest, map_mat, cv::Size2i(width,height)); 

    return true;
  } // if
  else {
    return false;
  } // else
} // extractPatch
mcvz
  • 494
  • 4
  • 13
  • In the examples I have seen you do not have to allocate memory for the destination matrix when you do `warpAffine` i.e. this line `dest.create(width, height, src.type());` is simply `cv::Mat dest;` Do you know if this is best practice to do this? Does warpAffine automatically allocated the matrix at the correct size if you miss this step out? – Robert Oct 18 '13 at 18:57
  • @Robert Apologies for a delayed response. Which examples are you referring to? As far as I know, the destination matrix has to be pre-allocated before calling the function cv::warpAffine(). I will check for you. – mcvz Dec 30 '13 at 09:50
  • @Robert The c++ warpAffine() takes a minimum of 4 arguments. The src and dst images, the affine transform matrix and the size of the destination matrix. When the dest matrix is unallocated and we pass dest.size() = (0,0) as the size argument of warpAffine(), the output dest matrix will have the same size as the input matrix. Otherwise, if the size argument of warpAffine() is anything else, the output matrix will be that size. I have no answer for you regarding the best practice in this case. Hope it helps. – mcvz Dec 30 '13 at 11:22
1

This was a very frustrating endeavor, but finally I solved it based on rroowwllaanndd's answer. I just had to add the angle correction when the width < height. Without this I got very strange results for images which fulfilled this condition.

def crop_image(rect, image):
    shape = (image.shape[1], image.shape[0])  # cv2.warpAffine expects shape in (length, height)
    center, size, theta = rect
    width, height = tuple(map(int, size))
    center = tuple(map(int, center))
    if width < height:
        theta -= 90
        width, height = height, width

    matrix = cv.getRotationMatrix2D(center=center, angle=theta, scale=1.0)
    image = cv.warpAffine(src=image, M=matrix, dsize=shape)

    x = int(center[0] - width // 2)
    y = int(center[1] - height // 2)

    image = image[y : y + height, x : x + width]

    return image
Roald
  • 2,459
  • 16
  • 43