Итак, вот что я придумала - это немного трудоемко, но в конечном итоге оно дает нам правильный ответ. Я буду напрямую использовать вывод подключенных компонентов, который вы показали с последним изображением.
Используйте морфологическое скелетирование изображения , чтобы мы получили скелет капли. Таким образом, это даст нам самое минимальное представление контура, так что мы получим границу шириной в один пиксель, которая проходит через середину каждого толстого края. Этого можно добиться с помощью метода skeletonize
Scikit-image.
Используйте Преобразование Хафа , которое является методом обнаружения линий на скелетонизированном изображении. Таким образом, он параметризует линии в полярной области, и на выходе будет набор rho
и theta
, которые сообщают нам, какие линии обнаружены на скелетонизированном изображении. Для этого мы можем использовать OpenCV cv2.HoughLines
. Очень важно, чтобы вы делали это на скелетонизированном изображении, иначе у нас будет много линий-кандидатов, параллельных истинному контуру ограничивающего прямоугольника, и вы не сможете различить их.
Возьмите каждую пару линий и найдите их точку пересечения. Мы ожидаем, что со всеми парами линий будет 4 преобладающих группы пересечений, которые дают нам угол каждого прямоугольника.
Из-за шума в контурах мы можем получить более четырех точек пересечения. Мы можем использовать выпуклую оболочку , чтобы наконец получить 4 точки пересечения для прямоугольника. Таким образом, алгоритм выпуклой оболочки работает со списком точек, где он определяет подмножество точек, которые могут минимально охватывать список точек. Мы можем использовать cv2.convexHull
.
Наконец, из-за квантования преобразования Хафа, может быть несколько точек, которые находятся в непосредственной близости от каждого угла. Поэтому примените K-Means кластеризацию , чтобы найти 4 группы точек и, таким образом, найти их центроиды. Для этого мы можем использовать cv2.kmeans
.
Как только мы найдем центроиды, мы можем просто циклически перебирать каждую пару точек, чтобы, наконец, найти расстояния до каждого угла и, таким образом, найти расстояния, которые вас интересуют.
Давайте пройдемся по каждой точке по очереди:
Шаг № 1 - скелетизация морфологического изображения
Используя Scikit-image skeletonize
, мы можем скелетировать изображение подключенных компонентов, которое вы показали выше. Обратите внимание, что вам нужно преобразовать изображение в двоичный файл, прежде чем продолжить. После того, как вы вызовете метод, нам нужно будет преобразовать обратно в 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](https://i.stack.imgur.com/4OhVN.png)
Шаг № 2 - Используйте преобразование Хафа
Используя преобразование Хафа, мы можем обнаружить наиболее заметные линии на этом изображении:
lines = cv2.HoughLines(out,1,np.pi/180,60)
Здесь мы указываем пространство поиска, так что мы ищем строки, где размер ячейки имеет длину 1, а углы имеют ячейку 1 градус, или pi / 180
радиан. Таким образом, преобразование Хафа просматривает каждую точку края и перебирает диапазон углов theta
, которые проходят от начала координат до каждой точки края, и вычисляет соответствующее значение rho
с учетом размера ячейки. Эта пара регистрируется в 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](https://i.stack.imgur.com/6E4V7.png)
Как мы видим, на изображении четыре точки пересечения.Наша задача - найти эти точки пересечения.
Шаг № 3 - Найти точки пересечения
В преобразовании Хафа мы можем связать длину линии от начала координат до точки(x, y)
в изображении, представленном под углом theta
с помощью:
rho = x*cos(theta) + y*sin(theta)
Мы также можем сформировать уравнение прямой y = m*x + c
в декартовой форме.Мы можем преобразовать между ними, разделив обе стороны уравнения rho
на sin(theta)
, а затем переместив соответствующие термины в каждую сторону:
![](https://upload.wikimedia.org/math/d/d/0/dd0879191cd59006c4a9badb7d435999.png)
Следовательно, мы должны выполнить циклвсе уникальные пары линий и, используя приведенное выше уравнение, мы можем найти их точку пересечения, установив их декартовы формы равными друг другу.Это я не буду выводить для вас в целях экономии места, а просто установите две линии в декартовой форме равными друг другу и решите для 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](https://i.stack.imgur.com/ZHypn.png)
Шаг № 5 - Применение кластеризации K-Means
Как вы можете видеть на изображении выше,Есть несколько точек, которые отображаются в каждом углу.Было бы хорошо, если бы мы могли объединить несколько точек на каждом углу в одну точку.Одним из способов является усреднение всех точек в каждом углу, и самый простой способ сделать это из коробки - использовать кластеризацию K-Means.Нам нужны центроиды, чтобы, таким образом, дать нам последние угловые точки прямоугольника.Нам нужно убедиться, что мы указываем 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-средних, используемые для механики алгоритма, а пятый параметр говорит нам, сколько попыток нам нужно предпринять, чтобы найти лучшие кластеры.Мы выбираем 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](https://i.stack.imgur.com/9LXUt.png)
Шаг № 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](https://i.stack.imgur.com/qpCxD.png)
Чтобы увидеть, где это совпадает с исходным изображением, давайте просто повторим приведенный выше код, но рисуем линии на исходном изображении. Я скачал копию исходного изображения, чтобы сделать это:
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](https://i.stack.imgur.com/5SfNE.png)
Для полноты, вот весь код от начала до конца без всех выходных данных отладки - мы переходим от чтения изображения к рисованию линий в исходном изображении с печатью длин каждой стороны в обнаруженном прямоугольнике.
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()