Извлечение размеров прямоугольника

Итак, я работаю над проектом обработки изображений с изображениями сонара. Чтобы быть более конкретным, я пытаюсь извлечь размеры изображения пула, сделанного сонарным сканером. Мне удалось извлечь прямоугольную область бассейна, но я не могу понять, как получить размеры каждого края в пикселях. Я работаю с OpenCV в Python.

Буду признателен, если кто-нибудь подскажет, как это можно сделать.

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

Код пока.

import cv2 
import numpy as np 
from scipy import ndimage as ndi
from scipy.ndimage.measurements import label

def largest_component(indices):
    #this function takes a list of indices denoting
    #the white regions of the image and returns the largest 
    #white object of connected indices 

    return_arr = np.zeros((512,512), dtype=np.uint8)
    for index in indeces:
        return_arr[index[0]][index[1]] = 255

    return return_arr

image = cv2.imread('sonar_dataset/useful/sonarXY_5.bmp', 0)
image_gaussian = ndi.gaussian_filter(image, 4)
image_gaussian_inv = cv2.bitwise_not(image_gaussian)
kernel = np.ones((3,3),np.uint8)

# double thresholding extracting the sides of the rectangle
ret1, img1 = cv2.threshold(image_gaussian_inv, 120, 255, cv2.THRESH_BINARY)
ret2, img2 = cv2.threshold(image_gaussian_inv, 150, 255, cv2.THRESH_BINARY)

double_threshold = img1 - img2
closing = cv2.morphologyEx(double_threshold, cv2.MORPH_CLOSE, kernel1)

labeled, ncomponents = label(closing, kernel)
indices = np.indices(closing.shape).T[:,:,[1, 0]]
twos = indices[labeled == 2]
area =[np.sum(labeled==val) for val in range(ncomponents+1)]

rectangle = largest_component(twos)

cv2.imshow('rectangle', rectangle)
cv2.waitKey(0)

Исходное изображение и извлеченный объект приведены ниже.

Извлечение размеров прямоугольника

Извлечение размеров прямоугольника

Можете ли вы предоставить код, который вы написали для создания изображения выше? Это даст нам точку прыжка.

rayryeng 27.05.2019 21:52

Ну конечно; естественно! Извините, что не предоставил это раньше. Сначала я просто хотел услышать некоторые мнения, но, конечно, сам код мог бы помочь больше! Пожалуйста, найдите его в отредактированном вопросе.

rockmyboat 27.05.2019 22:09

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

rayryeng 27.05.2019 22:16

Пожалуйста, верните все операторы import, чтобы другие люди могли запускать ваш код, не догадываясь, что может быть ndi.gaussian_filter. Спасибо.

Mark Setchell 27.05.2019 22:23

если вы можете заполнить внешний контур, вы можете попытаться обрезать дефекты выпуклости: stackoverflow.com/questions/35226993/…, за которым следует minAreaRect, чтобы получить прямоугольник.

Micka 27.05.2019 22:23

@MarkSetchell Только что добавил их! Извините, что доставил вам проблемы с поиском конкретных пакетов.

rockmyboat 27.05.2019 22:34

@Micka Я попробую это прямо сейчас, сначала мне нужно будет размыть изображение с помощью вертикальной SE, чтобы соединить правый край и посмотреть, как оно пойдет. Спасибо за совет! Выглядит многообещающе.

rockmyboat 27.05.2019 22:36
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
9
7
4 767
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Это не идеально, но этот простой подход должен стать для вас хорошей отправной точкой:

import cv2, math
import numpy as np

img = cv2.imread(R'D:\dev\projects\stackoverflow\dimensions_of_rectangle\img1.png')
print(img.shape)
img_moments=cv2.moments(img[:,:,0]) #use only one channel here (cv2.moments operates only on single channels images)
print(img_moments)
# print(dir(img_moments))

# calculate centroid (center of mass of image)
x = img_moments['m10'] / img_moments['m00']
y = img_moments['m01'] / img_moments['m00']

# calculate orientation of image intensity (it corresponds to the image intensity axis)
u00 = img_moments['m00']
u20 = img_moments['m20'] - x*img_moments['m10']
u02 = img_moments['m02'] - y*img_moments['m01']
u11 = img_moments['m11'] - x*img_moments['m01']

u20_prim = u20/u00
u02_prim = u02/u00
u11_prim = u11/u00

angle = 0.5 * math.atan(2*u11_prim / (u20_prim - u02_prim))
print('The image should be rotated by: ', math.degrees(angle) / 2.0, ' degrees')

cols,rows = img.shape[:2]
# rotate the image by half of this angle
rotation_matrix = cv2.getRotationMatrix2D((cols/2,rows/2), math.degrees(angle / 2.0), 1)
img_rotated = cv2.warpAffine(img, rotation_matrix ,(cols,rows))
# print(img_rotated.shape, img_rotated.dtype)

cv2.imwrite(R'D:\dev\projects\stackoverflow\dimensions_of_rectangle\img1_rotated.png', img_rotated)

img_rotated_clone = np.copy(img_rotated)
img_rotated_clone2 = np.copy(img_rotated)

# first method - just calculate bounding rect
bounding_rect = cv2.boundingRect(img_rotated[:, :, 0])
cv2.rectangle(img_rotated_clone, (bounding_rect[0], bounding_rect[1]), 
    (bounding_rect[0] + bounding_rect[2], bounding_rect[1] + bounding_rect[3]), (255,0,0), 2)

# second method - find columns and rows with biggest sums
def nlargest_cols(a, n):
    col_sums = [(np.sum(col), idx) for idx, col in enumerate(a.T)]
    return sorted(col_sums, key=lambda a: a[0])[-n:]

def nlargest_rows(a, n):
    col_sums = [(np.sum(col), idx) for idx, col in enumerate(a[:,])]
    return sorted(col_sums, key=lambda a: a[0])[-n:]

top15_cols_indices = nlargest_cols(img_rotated[:,:,0], 15)
top15_rows_indices = nlargest_rows(img_rotated[:,:,0], 15)

for a in top15_cols_indices:
    cv2.line(img_rotated_clone, (a[1], 0), (a[1], rows), (0, 255, 0), 1)

for a in top15_rows_indices:
    cv2.line(img_rotated_clone, (0, a[1]), (cols, a[1]), (0, 0, 255), 1)

cv2.imwrite(R'D:\dev\projects\stackoverflow\dimensions_of_rectangle\img2.png', img_rotated_clone)

Конечно, вам нужно настроить пути. img1.png — второе изображение из вашего вопроса, img1_rotated — результат поворота изображения:

и img2 - это окончательный результат:

Синий прямоугольник — это метод1 (просто ограничивающий прямоугольник), а зеленые и красные линии (15 красных и 15 зеленых — все шириной в 1 пиксель) — второй метод.

Алгоритм довольно прост:

  1. Рассчитайте моменты изображения, чтобы определить главную ось интенсивности изображения (я не знаю, как это правильно описать — проверьте вики-страницу https://en.wikipedia.org/wiki/Image_moment#Examples_2). По сути, это угол, на который вы должны повернуть изображение, чтобы белые пиксели распределились по горизонтали или вертикали.
  2. Как только вы узнаете угол - поверните изображение (и сохраните результат).
  3. Метод 1 - вычислить и нарисовать повернутый прямоугольник всех пикселей.
  4. Метод 2 - найдите 15 строк и 15 столбцов с наибольшими суммами (== наибольшее количество белых пикселей) и нарисуйте горизонтальные/вертикальные линии в этих строках/столбцах. Обратите внимание, что число 15 было выбрано методом проб и ошибок, но должно быть легко выбрать 2 столбца (и строки) с большой суммой, которые не близки друг к другу. Эти столбцы/строки являются хорошими кандидатами на роль границ прямоугольников.

Надеюсь, это то, что вы искали, дайте мне знать, если у вас возникнут вопросы.

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

Cris Luengo 28.05.2019 08:42

Это хорошее решение, но в нем отсутствует последний шаг, где вам нужно измерить длину каждой стороны. Второй способ более точный. Поместите это во второй метод, и вы получите мой голос.

rayryeng 28.05.2019 09:11
Ответ принят как подходящий

Итак, вот что я придумал - это немного трудоемко, но в конечном итоге дает нам правильный ответ. Я буду напрямую использовать вывод подключенных компонентов, который вы показали на последнем изображении.

  1. Используйте скелетирование морфологического изображения, чтобы получить скелет большого двоичного объекта. Таким образом, это даст нам самое минимальное представление контура, так что мы получим границу шириной в один пиксель, которая проходит через середину каждого толстого края. Вы можете добиться этого с помощью метода skeletonize Scikit-image.

  2. Используйте Преобразование Хафа, который является методом обнаружения линий на скелетонизированном изображении. Таким образом, он параметризует линии в полярной области, и на выходе будет набор rho и theta, которые сообщают нам, какие линии обнаружены в скелетонизированном изображении. Для этого мы можем использовать cv2.HoughLines OpenCV. Очень важно, чтобы вы сделали это на скелетонизированном изображении, иначе у нас будет много линий-кандидатов, параллельных тому месту, где находится истинная граница ограничивающей рамки, и вы не сможете их различить.

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

  4. Из-за шума в контурах мы можем получить более четырех точек пересечения. Мы можем использовать выпуклая оболочка, чтобы наконец получить 4 точки пересечения для прямоугольника. Таким образом, алгоритм выпуклой оболочки работает со списком точек, где он определяет подмножество точек, которое может минимально охватывать список точек. Мы можем использовать cv2.convexHull.

  5. Наконец, из-за квантования преобразования Хафа в окрестностях каждого угла может быть несколько точек. Поэтому примените Кластеризация K-средних, чтобы найти 4 кластера точек и, таким образом, найти их центроиды. Для этого мы можем использовать cv2.kmeans.

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

Давайте пройдемся по каждому пункту один за другим:

Шаг №1 - Морфологическое скелетирование изображения

Используя skeletonize Scikit-image, мы можем скелетировать изображение подключенных компонентов, которое вы показали выше. Обратите внимание, что вам нужно преобразовать изображение в двоичный формат, прежде чем продолжить. После того, как вы вызовете метод, нам нужно будет преобразовать обратно в 8-битное целое число без знака для остальной части процесса. Я загрузил изображение выше и сохранил его локально. Мы можем запустить метод skeletonize после:

from skimage.morphology import skeletonize

im = cv2.imread('K7ELI.png', 0)
out = skeletonize(im > 0)

# Convert to uint8
out = 255*(out.astype(np.uint8))

Получаем вот такое изображение:

fig1

Шаг #2 - Используйте преобразование Хафа

Используя преобразование Хафа, мы можем обнаружить наиболее заметные линии на этом изображении:

lines = cv2.HoughLines(out,1,np.pi/180,60)

Здесь мы указываем пространство поиска, чтобы мы искали строки, в которых размер ячейки имеет длину 1, а углы имеют ячейку 1 градус или pi / 180 радиан. Таким образом, преобразование Хафа просматривает каждую точку края и перебирает диапазон углов theta, которые простираются от начала координат до каждой точки края, и вычисляет соответствующее значение rho с учетом размера ячейки. Эта пара регистрируется в 2D-гистограмме, и мы регистрируем голосование. Мы порогуем эту 2D-гистограмму, чтобы любые интервалы за пределами определенного значения были кандидатами на линии. В приведенной выше строке кода установите пороговое значение для количества бинов равным 60.

Этот код необязателен, но я хотел показать вам, как выглядят визуализированные линии:

img_colour = np.dstack([im, im, im])
lines = cv2.HoughLines(edges,1,np.pi/180,60)
for rho,theta in lines[:,0]:
    a = np.cos(theta)
    b = np.sin(theta)
    x0 = a*rho
    y0 = b*rho
    x1 = int(x0 + 1000*(-b))
    y1 = int(y0 + 1000*(a))
    x2 = int(x0 - 1000*(-b))
    y2 = int(y0 - 1000*(a))
    cv2.line(img_colour,(x1,y1),(x2,y2),(0,0,255),2)

Этот код я вытащил из следующего урока. Он рисует линии, обнаруженные преобразованием Хафа, на изображении красным цветом. Я получаю следующее изображение:

fig2

Как мы видим, на изображении есть четыре точки пересечения. Наша задача – найти эти точки пересечения.

Шаг №3 - Найдите точки пересечения

В преобразовании Хафа мы можем связать длину линии от начала координат до точки (x, y) на изображении, расположенной под углом theta, следующим образом:

rho = x*cos(theta) + y*sin(theta)

Мы также можем составить уравнение прямой y = m*x + c в декартовой форме. Мы можем преобразовать их, разделив обе части уравнения rho на sin(theta), а затем переместив соответствующие члены в каждую сторону:

Следовательно, мы должны перебрать все уникальные пары линий и, используя приведенное выше уравнение, мы можем найти их точку пересечения, установив их декартовы формы равными друг другу. Я не буду выводить это для вас в интересах экономии места, а просто установим две линии в декартовой форме равными друг другу и найду x координату пересечения. Как только это будет сделано, подставьте эту точку в любую из двух строк, чтобы найти координату y. Мы, очевидно, должны пропускать точки пересечения, которые выходят за пределы изображения, в случае двух почти параллельных линий или если мы выбираем две пары линий, которые идут в одном направлении и не пересекаются.

pts = []
for i in range(lines.shape[0]):
    (rho1, theta1) = lines[i,0]
    m1 = -1/np.tan(theta1)
    c1 = rho1 / np.sin(theta1)
    for j in range(i+1,lines.shape[0]):
        (rho2, theta2) = lines[j,0]
        m2 = -1 / np.tan(theta2)
        c2 = rho2 / np.sin(theta2)
        if np.abs(m1 - m2) <= 1e-8:
            continue
        x = (c2 - c1) / (m1 - m2)
        y = m1*x + c1
        if 0 <= x < img.shape[1] and 0 <= y < img.shape[0]:
            pts.append((int(x), int(y)))

pts — это список кортежей, в который мы добавляем все точки пересечения, которые находятся внутри изображения и не выходят за его пределы.

Шаг #4 - Используйте выпуклую оболочку

Мы можем использовать этот список кортежей и использовать выпуклую оболочку, чтобы найти список точек, определяющих внешний периметр прямоугольника. Обратите внимание, что порядок точек, определяющих прямоугольник, - против часовой стрелки. Это не имеет значения для этого шага, но будет иметь значение позже:

pts = np.array(pts)
pts = pts[:,None] # We need to convert to a 3D numpy array with a singleton 2nd dimension
hull = cv2.convexHull(pts)

hull содержит 3D-массив NumPy, который представляет собой подмножество исходных точек пересечения, создающих внешнюю границу изображения. Мы можем использовать эти точки, чтобы нарисовать, где они расположены на изображении для иллюстрации.

out2 = np.dstack([im, im, im])
for pt in hull[:,0]:
    cv2.circle(out2, tuple(pt), 2, (0, 255, 0), 2)

Я взял исходное изображение и нарисовал угловые точки зеленым цветом. Получаем вот такое изображение:

fig3

Шаг № 5 — Примените кластеризацию K-средних

Как вы можете видеть на изображении выше, есть несколько точек, которые сопоставляются с каждым углом. Было бы хорошо, если бы мы могли объединить несколько точек в каждом углу в одну точку. Один из способов — усреднить все точки в каждом углу, и самый простой способ сделать это сразу — использовать кластеризацию K-средних. Нам нужны центроиды, чтобы получить конечные угловые точки прямоугольника. Нам нужно убедиться, что мы указываем 4 кластера для поиска.

Из Учебник по кластеризации K-Means из документов OpenCV мы можем использовать этот код:

# Define criteria = ( type, max_iter = 10 , epsilon = 1.0 )
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)

# Set flags (Just to avoid line break in the code)
flags = cv2.KMEANS_RANDOM_CENTERS

# Apply KMeans
# The convex hull points need to be float32
z = hull.copy().astype(np.float32)
compactness,labels,centers = cv2.kmeans(z,4,None,criteria,10,flags)

Первый параметр — это выпуклая оболочка точек, которые должны находиться в float32, как того требует алгоритм. Второй параметр указывает количество кластеров, которые мы хотим найти, в нашем случае 4. Третий параметр можно пропустить. Это заполнитель для лучшего идентификатора кластера, которому назначена каждая точка, но нам не нужно его использовать. criteria — это параметры K-Means, используемые для механики алгоритма, а пятый параметр говорит нам, сколько попыток мы должны выполнить, чтобы найти лучшие кластеры. Мы выбираем 10, что означает, что мы запускаем K-Means 10 раз и выбираем конфигурацию кластеризации с наименьшим количеством ошибок. Ошибка сохраняется в переменной compactness, которая выводится из алгоритма. Наконец, последняя переменная — это необязательные флаги, и мы устанавливаем их так, чтобы начальные центроиды алгоритма просто выбирались случайным образом из точек.

labels указывает, какой идентификатор кластера назначен каждой точке, а centers — это ключевая переменная, которая нам нужна, которая, таким образом, возвращает:

array([[338.5   , 152.5   ],
       [302.6667, 368.6667],
       [139.    , 340.    ],
       [178.5   , 127.    ]], dtype=float32)

Это четыре угловые точки прямоугольника. Мы можем увидеть, где они выстраиваются в линию, нарисовав их непосредственно на исходном изображении, и мы также получим это изображение:

out3 = np.dstack([im, im, im])
for pt in centers:
    cv2.circle(out3, tuple(pt), 2, (0, 255, 0), 2)

fig5

Шаг № 6 - Измерьте длину сейчас

Наконец, мы можем просмотреть каждую пару строк и найти соответствующие измерения. Обратите внимание, что, поскольку K-Means имеет центроиды в случайном порядке из-за случайного характера алгоритма, мы можем запустить выпуклую оболочку на этих центроидах, чтобы гарантировать, что порядок является круговым.

centers = cv2.convexHull(centers)[:,0]
for (i, j) in zip(range(4), [1, 2, 3, 0]):
    length = np.sqrt(np.sum((centers[i] - centers[j])**2.0))
    print('Length of side {}: {}'.format(i+1, length))

Таким образом, мы получаем:

Length of side 1: 219.11654663085938
Length of side 2: 166.1582489013672
Length of side 3: 216.63160705566406
Length of side 4: 162.019287109375

Если вы хотите, чтобы перспектива увидела, как выстраивается ограничительная рамка, давайте на самом деле нарисуем эти линии на изображении, которые определены в этих центрах:

out4 = np.dstack([im, im, im])
for (i, j) in zip(range(4), [1, 2, 3, 0]):
    cv2.line(out4, tuple(centers[i]), tuple(centers[j]), (0, 0, 255), 2)

Мы получили:

fig5

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

out5 = cv2.imread('no8BP.png') # Note - grayscale image read in as colour
for (i, j) in zip(range(4), [1, 2, 3, 0]):
    cv2.line(out5, tuple(centers[i]), tuple(centers[j]), (0, 0, 255), 2)

fig6


Для полноты картины вот весь код от начала до конца без всех выводов отладки — мы переходим от чтения изображения к рисованию линий на исходном изображении с выводом длин каждой стороны в обнаруженном прямоугольнике.

from skimage.morphology import skeletonize
import cv2
import numpy as np

# Step #1 - Skeletonize
im = cv2.imread('K7ELI.png', 0)
out = skeletonize(im > 0)

# Convert to uint8
out = 255*(out.astype(np.uint8))

# Step #2 - Hough Transform
lines = cv2.HoughLines(out,1,np.pi/180,60)

# Step #3 - Find points of intersection
pts = []
for i in range(lines.shape[0]):
    (rho1, theta1) = lines[i,0]
    m1 = -1/np.tan(theta1)
    c1 = rho1 / np.sin(theta1)
    for j in range(i+1,lines.shape[0]):
        (rho2, theta2) = lines[j,0]
        m2 = -1 / np.tan(theta2)
        c2 = rho2 / np.sin(theta2)
        if np.abs(m1 - m2) <= 1e-8:
            continue
        x = (c2 - c1) / (m1 - m2)
        y = m1*x + c1
        if 0 <= x < img.shape[1] and 0 <= y < img.shape[0]:
            pts.append((int(x), int(y)))

# Step #4 - Find convex hull
pts = np.array(pts)
pts = pts[:,None] # We need to convert to a 3D numpy array with a singleton 2nd dimension
hull = cv2.convexHull(pts)

# Step #5 - K-Means clustering
# Define criteria = ( type, max_iter = 10 , epsilon = 1.0 )
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)

# Set flags (Just to avoid line break in the code)
flags = cv2.KMEANS_RANDOM_CENTERS

# Apply KMeans
# The convex hull points need to be float32
z = hull.copy().astype(np.float32)
compactness,labels,centers = cv2.kmeans(z,4,None,criteria,10,flags)

# Step #6 - Find the lengths of each side
centers = cv2.convexHull(centers)[:,0]
for (i, j) in zip(range(4), [1, 2, 3, 0]):
    length = np.sqrt(np.sum((centers[i] - centers[j])**2.0))
    print('Length of side {}: {}'.format(i+1, length))

# Draw the sides of each rectangle in the original image
out5 = cv2.imread('no8BP.png') # Note - grayscale image read in as colour
for (i, j) in zip(range(4), [1, 2, 3, 0]):
    cv2.line(out5, tuple(centers[i]), tuple(centers[j]), (0, 0, 255), 2)

# Show the image
cv2.imshow('Output', out5); cv2.waitKey(0); cv2.destroyAllWindows()

Это потрясающе! Спасибо, что потратили свое время на этот очень подробный ответ! Я многому научился только благодаря твоему подходу. Будет весело использовать его в других моих проектах. Еще раз большое спасибо!

rockmyboat 28.05.2019 02:53

@rockmyboat всегда пожалуйста! Извините, что это заняло у меня так много времени. Чтобы найти ответ, пришлось немного поэкспериментировать. Это было на самом деле весело решать!

rayryeng 28.05.2019 02:59

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

Cris Luengo 28.05.2019 08:45

@КрисЛуенго, ха-ха! С такими вопросами и, следовательно, ответами, я всегда нахожу улучшения в языке или что-то в алгоритме, которое я не объяснил правильно. Также существует корреляция между моими лучшими ответами и количеством правок. Та, что про взаимную информацию, которую я написал в MATLAB, имеет около 20 правок! Кстати, хорошее быстрое решение с вашей стороны. Когда я проверил длины сторон и то, насколько они похожи на мои, это придало мне большей уверенности в том, что это более или менее правильно.

rayryeng 28.05.2019 08:48

Я думаю, что ответы, которые вы продолжаете улучшать, — это те, в которых вы развлекались и, возможно, чему-то научились сами. Я уверен, что это делает пост более интересным, за который люди с большей вероятностью проголосуют. К тому же, вы натыкаетесь на пост... :)

Cris Luengo 28.05.2019 08:54

Уже есть два хороших решения, я хотел предложить более простое, основанное на другом способе обнаружения прямоугольника.

(Здесь я использую MATLAB с DIPизображение, потому что это быстрее для меня, чем Python, чтобы собрать доказательство концепции вместе, но точно такая же функциональность доступна в Python, см. В конце поста. Отказ от ответственности: я автор DIPизображения.)

Поскольку прямоугольник представляет собой яркую фигуру на более темном фоне и (я полагаю) гарантированно окружает центр изображения, мы можем создать затравку в центре изображения и затравку на периферии и использовать водораздел для найти прямоугольник. В этом случае водораздел гарантированно создает один замкнутый контур толщиной 1 пиксель.

img = readim('https://i.stack.imgur.com/no8BP.png');
seeds = clone(img);
seeds(rr(seeds)<50) = 1;
seeds(rr(seeds)>250) = 2;
rect = waterseed(seeds,gaussf(img));
overlay(img,rect) % for display only

OP's image with a red rectangle drawn over the rectangle to be detected

Обратите внимание, что я немного сгладил входное изображение. Но прямоугольник все еще довольно шумный, что повлияет на измерение размера, которое мы проведем позже. Мы можем сгладить его, используя морфологическое отверстие с большим круговым структурирующим элементом. Эта операция также срежет углы, но закругленные углы не повлияют на результат измерения.

rect = opening(fillholes(rect),35);
overlay(img,rect-berosion(rect)) % for display only

Same image, but the rectangle is smoother and with rounded corners

Теперь у нас есть красивая форма, подходящая для измерения. Диаметры Фере - это длины выступов формы. Измеряем длину кратчайшего выступа (равную ширине прямоугольника) и длину выступа, перпендикулярного кратчайшему (равную длине прямоугольника). См. этот пост в моем блоге для подробного описания алгоритма, который вычисляет эти длины.

msr = measure(rect,[],'feret');
sz = msr(1).feret(2:3)

Это возвращает sz = [162.7506, 215.0775].


Вот Python-эквивалент приведенного выше кода (запускаются точно такие же реализации алгоритма). PyDIP, привязки Python к библиотеке DIPlib, не так совершенен, как набор инструментов DIPimage, который я использую выше, и некоторые синтаксис немного более многословны (хотя в основном намеренно). Коллега работает над упаковкой бинарного дистрибутива PyDIP, а до этого вам нужно будет собрать его из источников, что, надеюсь, довольно просто, если вы будете следовать инструкции.

import PyDIP as dip

img = dip.ImageRead('no8BP.png')

seeds = img.Similar()
seeds.Fill(0)
rr = dip.CreateRadiusCoordinate(seeds.Sizes())
seeds[rr<50] = 1
seeds[rr>250] = 2

rect = dip.SeededWatershed(dip.Gauss(img), seeds)
dip.viewer.Show(dip.Overlay(img,rect))
dip.viewer.Spin()

rect = dip.Opening(dip.FillHoles(rect),35)
dip.viewer.Show(dip.Overlay(img,rect-dip.BinaryErosion(rect,1,1)))
dip.viewer.Spin()

msr = dip.MeasurementTool.Measure(dip.Label(rect),features=['Feret'])
sz = (msr[1]['Feret'][1],msr[1]['Feret'][2])
print(sz)

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

Тоже очень красивое решение!

Bart Kiers 28.05.2019 08:44

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