如何使用OpenCV在Python中将图像中的旋转矩形区域拉直?

48
以下图片会告诉你我的需求。
我有这张图片中矩形的信息(宽度、高度、中心点和旋转角度)。现在,我想编写一个脚本将它们剪切并保存为图像,但也要将它们调直。也就是说,我想从图片内显示的矩形到外部显示的矩形。
我正在使用OpenCV Python。请告诉我一种实现这个目标的方法。
请提供一些代码示例,因为很难找到OpenCV Python的例子。 Example Image

如果将C++转换为Python不是问题,那么上面的链接应该正是您要寻找的。 - Sam
@vasile 实际上,我不需要透视变换。我只需要将旋转矩形中的像素逐个映射到直角矩形中。 - hakunami
1
如果您只想要角落位置,请使用perspectiveTransform()。如果您想要所有像素,则可以使用warpAffine()或warpPerspective()。 - Sam
@vasile warpAffine 是我需要的。谢谢。顺便问一下,你有没有关于opencv python的资源知识,不仅仅是官方文档,还有像教程、开源项目等等的东西? - hakunami
很遗憾,我是一个C++程序员,但我认为Python文档和许多OpenCV C++教程足以让你掌握计算机视觉。 - Sam
6个回答

66
你可以使用warpAffine函数围绕定义的中心点旋转图像。适当的旋转矩阵可以使用getRotationMatrix2D生成(其中theta度数表示)。

Start Image After finding the desired rectangle

你可以使用Numpy切片来裁剪图片。

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

请注意,dsize输出图像的形状。如果补丁/角度足够大,则边缘会被剪切掉(与上面的图像进行比较),如果使用原始形状作为简化的手段,就像上面所做的那样。在这种情况下,您可以引入一个缩放因子到shape(以扩大输出图像)和用于切片的参考点(这里是center)。
可以按以下方式使用上述函数:
image = cv2.imread('owl.jpg')
image = subimage(image, center=(110, 125), theta=30, width=100, height=200)
cv2.imwrite('patch.jpg', image)

我在代码中进行了一项小更改,因为对于非正方形(矩形)图像,它对我来说无法正确工作:image = cv2.warpAffine(src=image, M=matrix, dsize=shape[::-1])。 - Julian
2
你如何计算 theta - Sabito stands with Ukraine
@Sabito錆兎支持乌克兰,请查看OpenCV中的minAreaRect()函数。 - Tengerye

20

我在使用这里和类似问题的解决方案时遇到了错误偏移量的问题。

因此,我进行了计算,并得出了以下可行的解决方案:

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)

参考此处的图片解释了其中的数学原理:

请注意:

w_dst = width-1
h_dst = height-1
这是因为最后一个坐标的值为width-1而不是widthheight


您可以通过一系列线性变换(以相反的应用顺序编写)获得正确的转换矩阵: TranslateBy((w-1)/2,(h-1)/2)*RotationMatrix(theta)*TranslateBy(-center_x, -center_y) 必须是 w-1h-1:只需考虑一个围绕(0,0)旋转的1x1图像。像素在最后一步不应该被平移0.5,而应该是0。 - xaedes

17

针对OpenCV版本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)

2
代码只需要做一个小改动。在cv2.warpAffine()函数中,将src.shape[:2]替换为(src.shape[0], src.shape[1])。因为src.shape[:2]返回的是(rows, cols),但是opencv需要的是(cols, rows)。 - vamsidhar muthireddy
5
@vamsidharmuthireddy 感谢您的评论。我相信您打错了一个字,应该是(src.shape[1],src.shape[0]):) - Yohaï-Eliel Berreby

17

其他方法只有在旋转后矩形的内容仍在旋转后的图像中时才有效,在其他情况下会遭遇严重失败。如果部分内容丢失了怎么办?请参考下面的示例:

enter image description here

如果您要使用上述方法裁剪旋转矩形文本区域,
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()

这是你将会得到的:

enter image description here

显然,一些部分被剪掉了!既然我们可以使用cv.boxPoints()方法获取其四个角点,为什么不直接扭曲旋转的矩形呢?

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))

现在裁剪后的图片变成了{{cropped image}}。

enter image description here

很好,不是吗?如果你仔细检查,你会注意到裁剪的图像中有一些黑色区域。这是因为检测到的矩形的一小部分超出了图像的边界。为了解决这个问题,你可以稍微填充图像,然后再进行裁剪。 这个答案中有一个示例。
现在,我们比较两种从图像中裁剪旋转矩形的方法。这种方法不需要旋转图像,并且可以用更少的代码更优雅地处理这个问题。

3

这是我用C++编写的版本,执行相同的任务。我注意到它有点慢。如果有人发现可以提高此函数性能的任何内容,请告诉我。:)

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

在我看过的例子中,当你使用warpAffine时,你不必为目标矩阵分配内存,也就是说这一行代码dest.create(width, height, src.type());可以简写成cv::Mat dest;。你知道这样做是否是最佳实践吗?如果省略了这一步,warpAffine会自动分配正确大小的矩阵吗? - Robert
@Robert 很抱歉回复晚了。你指的是哪些例子?据我所知,在调用函数cv::warpAffine()之前,目标矩阵必须预先分配好。我会为你查一下。 - mcvz
@Robert,c++的warpAffine()函数至少需要4个参数。源图像和目标图像、仿射变换矩阵以及目标矩阵的大小。当目标矩阵未分配内存且我们将dest.size() = (0,0)作为warpAffine()函数的大小参数传递时,输出的目标矩阵将与输入矩阵具有相同的大小。否则,如果warpAffine()函数的大小参数是其他任何值,则输出矩阵将是该大小。关于这种情况下的最佳实践,我没有答案。希望能对你有所帮助。 - mcvz

1

这是一个非常令人沮丧的尝试,但最终我根据rroowwllaanndd的答案解决了它。 width < height时,我只需要添加角度修正即可。如果没有这个修正,对于满足此条件的图像,我得到的结果非常奇怪。

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

1
非常干净和方便的函数! - nealmcb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接