Прямоугольник кадрирования, возвращаемый minAreaRect OpenCV [Python]

minAreaRect в OpenCV возвращает повернутый прямоугольник. Как обрезать эту часть изображения, которая находится внутри прямоугольника?

boxPoints возвращает координаты угловых точек повернутого прямоугольника, чтобы можно было получить доступ к пикселям, просматривая точки внутри поля, но есть ли более быстрый способ обрезки в Python?

РЕДАКТИРОВАТЬ

Увидеть code в моем ответе ниже.

5 ответов

Решение

Вы не предоставили пример кода, поэтому я отвечаю и без кода. Вы могли бы действовать следующим образом:

  1. Из углов прямоугольника определите угол альфа вращения относительно горизонтальной оси.
  2. Поверните изображение по альфа так, чтобы обрезанный прямоугольник был параллелен границам изображения. Убедитесь, что временное изображение больше по размеру, чтобы информация не терялась (см. Поворот изображения без обрезки OpenCV)
  3. Обрезать изображение с помощью обрезки (см. Как обрезать изображение в OpenCV с использованием Python)
  4. Повернуть изображение обратно на альфа.

Вот функция, которая выполняет эту задачу:

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

вот пример использования

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

это выход

оригинальное и обрезанное изображение

@AbdulFatir был на пути к хорошему решению, но, как отмечалось в комментариях (@Randika @epinal), оно тоже не совсем работало для меня, поэтому я немного изменил его, и, похоже, оно работает для моего случая. вот изображение, которое я использую. 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()

Это должно создать серию таких изображений: изолированный контур 1 изолированный контур 2 изолированный контур 3

И это также даст результат изображения, как это: Результаты

Вот код для выполнения вышеуказанной задачи. Чтобы ускорить процесс, вместо того, чтобы сначала поворачивать все изображение и обрезать его, часть изображения, которая имеет повернутый прямоугольник, сначала обрезается, затем поворачивается и снова обрезается для получения окончательного результата.

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

К сожалению, ответ Оливера Уилкена не привел к показанным изображениям. Может из-за другой версии openCV? Вот моя принятая версия, которая добавляет несколько функций:

  • масштабирование и заполнение прямоугольника, т.е. чтобы получить также части за пределами исходного прямоугольника
  • угол результирующего изображения может быть настроен относительно прямоугольника, т. е. угол 0 или 90 [градусов] вернет прямоугольник по горизонтали или вертикали.
  • возврат матрицы перевода для поворота других объектов, например точек, линий и т. д.
  • вспомогательные функции для индексации массивов numpy и openCV и манипуляций с прямоугольниками

Код

      import cv2
import numpy as np


def img_rectangle_cut(img, rect=None, angle=None):
    """Translate an image, defined by a rectangle. The image is cropped to the size of the rectangle
    and the cropped image can be rotated.
    The rect must be of the from (tuple(center_xy), tuple(width_xy), angle).
    The angle are in degrees.
    PARAMETER
    ---------
    img: ndarray
    rect: tuple, optional
        define the region of interest. If None, it takes the whole picture
    angle: float, optional
        angle of the output image in respect to the rectangle.
        I.e. angle=0 will return an image where the rectangle is parallel to the image array axes
        If None, no rotation is applied.
    RETURNS
    -------
    img_return: ndarray
    rect_return: tuple
        the rectangle in the returned image
    t_matrix: ndarray
        the translation matrix
    """
    if rect is None:
        if angle is None:
            angle = 0
        rect = (tuple(np.array(img.shape) * .5), img.shape, 0)
    box = cv2.boxPoints(rect)

    rect_target = rect_rotate(rect, angle=angle)
    pts_target = cv2.boxPoints(rect_target)

    # get max dimensions
    size_target = np.int0(np.ceil(np.max(pts_target, axis=0) - np.min(pts_target, axis=0)))

    # translation matrix
    t_matrix = cv2.getAffineTransform(box[:3].astype(np.float32),
                                      pts_target[:3].astype(np.float32))

    # cv2 needs the image transposed
    img_target = cv2.warpAffine(cv2.transpose(img), t_matrix, tuple(size_target))

    # undo transpose
    img_target = cv2.transpose(img_target)
    return img_target, rect_target, t_matrix


def reshape_cv(x, axis=-1):
    """openCV and numpy have a different array indexing (row, cols) vs (cols, rows), compensate it here."""
    if axis < 0:
        axis = len(x.shape) + axis
    return np.array(x).astype(np.float32)[(*[slice(None)] * axis, slice(None, None, -1))]

def connect(x):
    """Connect data for a polar or closed loop plot, i.e. np.append(x, [x[0]], axis=0)."""
    if isinstance(x, np.ma.MaskedArray):
        return np.ma.append(x, [x[0]], axis=0)
    else:
        return np.append(x, [x[0]], axis=0)


def transform_np(x, t_matrix):
    """Apply a transform on a openCV indexed array and return a numpy indexed array."""
    return transform_cv2np(reshape_cv(x), t_matrix)


def transform_cv2np(x, t_matrix):
    """Apply a transform on a numpy indexed array and return a numpy indexed array."""
    return reshape_cv(cv2.transform(np.array([x]).astype(np.float32), t_matrix)[0])


def rect_scale_pad(rect, scale=1., pad=40.):
    """Scale and/or pad a rectangle."""
    return (rect[0],
            tuple((np.array(rect[1]) + pad) * scale),
            rect[2])


def rect_rotate(rect, angle=None):
    """Rotate a rectangle by an angle in respect to it's center.
    The rect must be of the from (tuple(center_xy), tuple(width_xy), angle).
    The angle is in degrees.
    """
    if angle is None:
        angle = rect[2]
    rad = np.deg2rad(np.abs(angle))
    rot_matrix_2d = np.array([[np.cos(rad), np.sin(rad)],
                              [np.sin(rad), np.cos(rad)]])

    # cal. center of rectangle
    center = np.sum(np.array(rect[1]).reshape(1, -1) * rot_matrix_2d, axis=-1) * .5
    center = np.abs(center)

    return tuple(center), rect[1], angle

Пример:

      # Generate Image
img = np.zeros((1200, 660), dtype=np.uint8)

# Draw some lines and gen. points
x_0 = np.array([150,600])
x_1 = np.int0(x_0 + np.array((100, 100)))
x_2 = np.int0(x_0 + np.array((100, -100))*2.5)
img = cv2.line(img,tuple(x_0),tuple(x_1),1,120)
img = cv2.line(img,tuple(x_0),tuple(x_2),1,120)
points = np.array([x_0, x_1, x_2])

# Get Box
rect = cv2.minAreaRect(np.argwhere(img))

# Apply transformation
rect_scale = rect_scale_pad(rect, scale = 1., pad = 40.)
img_return, rect_target, t_matrix = img_rectangle_cut(
    img, 
    rect_scale, 
    angle=0,
    angle_normalize=True  # True <-> angel=0 vertical; angel=90 horizontal
   )

# PLOT
fig, ax = plt.subplots(ncols=2, figsize=(10,5))
ax = ax.flatten()
ax[0].imshow(img)

box_i = reshape_cv(cv2.boxPoints(rect))
ax[0].plot(*connect(box_i).T, 'o-', color='gray', alpha=.75, label='Original Box')
box_i = reshape_cv(cv2.boxPoints(rect_scale))
ax[0].plot(*connect(box_i).T, 'o-', color='green', alpha=.75, label='Scaled Box')
ax[0].plot(*points.T, 'o', label='Points')


ax[1].imshow(img_return)
box_i = transform_cv2np(cv2.boxPoints(rect), t_matrix)
ax[1].plot(*connect(box_i).T, 'o-', color='gray', alpha=.75, label='Original Box')

point_t = transform_np(points, t_matrix)
ax[1].plot(*point_t.T, 'o', label='Points')

ax[0].set_title('Original')
ax[1].set_title('Translated')

for axi in ax:
    axi.legend(loc=1)
    
plt.tight_layout()

Другие вопросы по тегам