OpenCV Python实现旋转矩形的裁剪

环境

  • Python 3.8
  • OpenCV 4.4
  • 最小外接矩形

    矩形操作是我们在 OpenCV 里最常用的操作,其中最为常见的就是包围框( Bounding Box )和旋转矩形( Rotated Box )。 其中包围框是最为常见的,对应 OpenCV 中的 boundingRect() ,使用正矩形框处物体,一般多用在目标检测中。使用包围框框柱目标物体,这种操作比较简单,但是通常框中也会有一些其他的区域。其次就是使用旋转矩形,也叫最小外接矩形,对应 OpenCV 中的 minAreaRect() ,用来使用旋转矩形最大限度的框出目标物体,减小背景干扰,在 OCR 任务中较为常用。

    def drow_box(img, cnt):
        rect_box = cv2.boundingRect(cnt)
        rotated_box = cv2.minAreaRect(cnt)
        cv2.rectangle(img, (rect_box[0], rect_box[1]), (rect_box[0] + rect_box[2], rect_box[1] + rect_box[3]), (0, 255, 0), 2)
        box = cv2.boxPoints(rotated_box)
        box = np.int0(box)
        cv2.drawContours(img, [box], 0, (0, 0, 255), 2)
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        # plt.imshow(img)
        # plt.show()
        return img, rotated_box, box
    

    minAreaRect()返回了所需区域的最小斜矩形的参数,与包围框直接返回四个顶点的坐标不同,最小外接矩形返回的是矩形的((x, y), (w, h), angle),对应了矩形的中心,宽度,高度和旋转角度。

    旋转角度angle是水平轴(x轴)逆时针旋转,与碰到的矩形的第一条边的夹角。并且这个边的边长是width,另一条边边长是height。也就是说,在这里widthheight不是按照长短来定义的。

    OpenCV中,坐标系原点在左上角,相对于x轴,逆时针旋转角度为负,顺时针旋转角度为正,所以函数minAreaRect()返回的角度范围时[-90~0)。想象一个平放的长矩形,调用minAreaRect() 返回的角度为-90度。如果我们旋转图像,直到矩形树立起来,这是调用minAreaRect()得到的角度依然是-90度。

  • 计算旋转矩形。
  • 基于旋转矩形的中心和角度计算得到一个变换矩阵。这里我想要得到的车牌是一个横着的正矩形,因此需要判断一下angle, widthheight,保证旋转后的矩形是横着的矩形。
  • 以旋转矩形的中心为基准点,对整张图进行旋转,这里旋转的实现是基于仿射变换实现的。
  • 由于旋转之后矩形的中点坐标是不变的,以中心为基础,通过widthheight抠出正矩形。
  • def crop1(img, cnt):
        horizon = True
        img, rotated_box, _ = drow_box(img, cnt)
        center, size, angle = rotated_box[0], rotated_box[1], rotated_box[2]
        center, size = tuple(map(int, center)), tuple(map(int, size))
        print(angle)
        if horizon:
            if size[0] < size[1]:
                angle -= 270
                w = size[1]
                h = size[0]
            else:
                w = size[0]
                h = size[1]
            size = (w, h)
        height, width = img.shape[0], img.shape[1]
        M = cv2.getRotationMatrix2D(center, angle, 1)
        img_rot = cv2.warpAffine(img, M, (width, height))
        img_crop = cv2.getRectSubPix(img_rot, size, center)
        show([img, img_rot, img_crop])
    

    如果不做边长和角度的判断,则只会沿着x轴的顺时针方向做相同大小角度的旋转,不能保证旋转后的视角是正确的视角:

  • 计算旋转矩形。
  • 基于矩形的四个顶点和想要抠出的正矩形的四个顶点得到一个变换矩阵。这里我想要得到的车牌是一个横着的正矩形,因此需要判断一下widthheight,将映射的平面定义为一个横着的正矩形。
  • 通过透视变换,将四个点组成的平面转换成另四个点组成的一个平面,以此抠出正矩形。
  • def crop2(img, cnt):
        img, rotated_box, box = drow_box(img, cnt)
        width = int(rotated_box[1][0])
        height = int(rotated_box[1][1])
        print(width, height)
        if width > height:
            w = width
            h = height
        else:
            w = height
            h = width
        src_pts = box.astype("float32")
        dst_pts = np.array([[w - 1, h - 1],
                            [0, h - 1],
                            [0, 0],
                            [w - 1, 0]], dtype="float32")
        M = cv2.getPerspectiveTransform(src_pts, dst_pts)
        warped = cv2.warpPerspective(img, M, (w, h))
        show([img, warped])