Прямоугольник кадрирования, возвращаемый minAreaRect OpenCV [Python]
minAreaRect
в OpenCV возвращает повернутый прямоугольник. Как обрезать эту часть изображения, которая находится внутри прямоугольника?
boxPoints
возвращает координаты угловых точек повернутого прямоугольника, чтобы можно было получить доступ к пикселям, просматривая точки внутри поля, но есть ли более быстрый способ обрезки в Python?
РЕДАКТИРОВАТЬ
Увидеть code
в моем ответе ниже.
5 ответов
Вы не предоставили пример кода, поэтому я отвечаю и без кода. Вы могли бы действовать следующим образом:
- Из углов прямоугольника определите угол альфа вращения относительно горизонтальной оси.
- Поверните изображение по альфа так, чтобы обрезанный прямоугольник был параллелен границам изображения. Убедитесь, что временное изображение больше по размеру, чтобы информация не терялась (см. Поворот изображения без обрезки OpenCV)
- Обрезать изображение с помощью обрезки (см. Как обрезать изображение в OpenCV с использованием Python)
- Повернуть изображение обратно на альфа.
Вот функция, которая выполняет эту задачу:
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), оно тоже не совсем работало для меня, поэтому я немного изменил его, и, похоже, оно работает для моего случая. вот изображение, которое я использую.
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()
Вот код для выполнения вышеуказанной задачи. Чтобы ускорить процесс, вместо того, чтобы сначала поворачивать все изображение и обрезать его, часть изображения, которая имеет повернутый прямоугольник, сначала обрезается, затем поворачивается и снова обрезается для получения окончательного результата.
# 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()