【问题标题】:How to straighten a rotated rectangle area of an image using OpenCV in Python?如何在 Python 中使用 OpenCV 拉直图像的旋转矩形区域?
【发布时间】:2012-07-22 13:32:34
【问题描述】:

下面的图片会告诉你我想要什么。

我有图像中矩形的信息(宽度、高度、中心点和旋转度)。现在,我想编写一个脚本将它们剪下来并将它们保存为图像,但也要将它们拉直。如,我想从图像内部显示的矩形转到外部显示的矩形。

我正在使用 OpenCV Python。 告诉我实现此目的的方法。

展示一些代码,因为 OpenCV Python 的示例很难找到。

【问题讨论】:

  • 如果将 C++ 格式转换为 Python 没有问题,那么上面的链接应该正是您要查找的内容
  • @vasile 实际上,我不需要透视变换。我只需要将旋转矩形中的像素一一映射到直矩形即可。
  • 如果您只想要角落位置,请使用 perspectiveTransform()。如果你想要所有的像素,这是 warpAffine() 或 warpPerspective() for
  • @vasile warpAffine 是我需要的。谢谢你。顺便问一下,你对opencv python的资源有什么了解吗,你知道的,不仅是官方文档,还有教程,打开项目等。

标签: python image-processing opencv


【解决方案1】:

您可以使用warpAffine 函数围绕定义的中心点旋转图像。可以使用getRotationMatrix2D(其中theta为单位)生成合适的旋转矩阵。

然后您可以使用Numpy slicing 剪切图像。

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])
  • 如何计算theta
【解决方案2】:

在使用此处和类似问题的解决方案时,我遇到了偏移错误的问题。

所以我做了数学计算并想出了以下可行的解决方案:

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:想想1x1图像你围绕(0,0)旋转。像素不应平移 0.5,而应在最后一步平移 0。
【解决方案3】:

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

如果要使用上述方法裁剪旋转的矩形文本区域,

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

这是你将得到的:

显然,有些部分被剪掉了!为什么不直接扭曲旋转的矩形,因为我们可以使用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))

现在裁剪后的图像变成了

好多了,不是吗?如果您仔细检查,您会注意到裁剪后的图像中有一些黑色区域。这是因为检测到的矩形的一小部分超出了图像的范围。为了解决这个问题,您可以pad the image 一点点,然后再进行裁剪。 this answer中有一个例子。

现在,我们比较从图像中裁剪旋转矩形的两种方法。 这种方法不需要旋转图像,可以用更少的代码更优雅地处理这个问题。

【讨论】:

    【解决方案4】:

    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)
    

    【讨论】:

    • 对代码进行一次更改。在 cv2.warpAffine() 中,代替 src.shape[:2] 给出 (src.shape[0],src.shape[1])。因为 src.shape[:2] 给出(行,列)。但是 opencv 期望 (cols,rows)
    • @vamsidharmuthireddy 感谢您的评论。我相信,你打错了,但是:它应该是 (src.shape[1] ,src.shape[0]) :)
    【解决方案5】:

    这是我执行相同任务的 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 为延迟回复道歉。你指的是哪些例子?据我所知,在调用函数 cv::warpAffine() 之前,必须预先分配目标矩阵。我会帮你查的。
    • @Robert C++ warpAffine() 至少需要 4 个参数。 src 和 dst 图像、仿射变换矩阵和目标矩阵的大小。当 dest 矩阵未分配并且我们将 dest.size() = (0,0) 作为 warpAffine() 的大小参数传递时,输出 dest 矩阵将具有与输入矩阵相同的大小。否则,如果 warpAffine() 的大小参数是其他任何值,则输出矩阵将是该大小。关于这种情况下的最佳实践,我没有答案。希望对您有所帮助。
    猜你喜欢
    • 1970-01-01
    • 2012-05-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-01
    • 1970-01-01
    • 2022-09-30
    • 1970-01-01
    相关资源
    最近更新 更多