Обнаружение тривиальных кругов с открытым CV - как получить наименьшие квадраты вместо контура? - PullRequest
6 голосов
/ 18 февраля 2020

Моя цель - точно измерить диаметр отверстия под микроскопом. Рабочий процесс: взять изображение, процесс для подгонки, подгонка, преобразовать радиус в пикселях в мм, записать в CSV

skewed hough circle

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

В качестве альтернативы я усреднил много совпадений примерно так:

many fits to avg

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

Вот часть моего сценария, подходящего для кругов, если бы вы могли взглянуть и рассказать мне, как сделать больше подхода наименьших квадратов порядка 5 кругов. Я не хочу использовать определение минимального круга, потому что жидкость течет через это отверстие, поэтому я бы хотел, чтобы он был больше похож на гидравлический диаметр c - спасибо!

(thresh, blackAndWhiteImage0) = cv2.threshold(img0, 100, 255, cv2.THRESH_BINARY) #make black + white 
median0 = cv2.medianBlur(blackAndWhiteImage0, 151) #get rid of noise 
circles0 = cv2.HoughCircles(median0,cv2.HOUGH_GRADIENT,1,minDist=5,param1= 25, param2=10, minRadius=min_radius_small,maxRadius=max_radius_small) #fit circles to image

Ответы [ 4 ]

3 голосов
/ 19 февраля 2020

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

Ввод:

enter image description here

import cv2
import numpy as np
from skimage import measure

# load image and set the bounds
img = cv2.imread("dark_circle.png")

# convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# blur
blur = cv2.GaussianBlur(gray, (3,3), 0)

# threshold
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

# apply morphology open with a circular shaped kernel
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))
binary = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)

# find contour and draw on input (for comparison with circle)
cnts = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
c = cnts[0]
result = img.copy()
cv2.drawContours(result, [c], -1, (0, 255, 0), 1)

# find radius and center of equivalent circle from binary image and draw circle
# see https://scikit-image.org/docs/dev/api/skimage.measure.html#skimage.measure.regionprops
# Note: this should be the same as getting the centroid and area=cv2.CC_STAT_AREA from cv2.connectedComponentsWithStats and computing radius = 0.5*sqrt(4*area/pi) or approximately from the area of the contour and computed centroid via image moments.
regions = measure.regionprops(binary)
circle = regions[0]
yc, xc = circle.centroid
radius = circle.equivalent_diameter / 2.0
print("radius =",radius, "  center =",xc,",",yc)
xx = int(round(xc))
yy = int(round(yc))
rr = int(round(radius))
cv2.circle(result, (xx,yy), rr, (0, 0, 255), 1)

# write result to disk
cv2.imwrite("dark_circle_fit.png", result)

# display it
cv2.imshow("image", img)
cv2.imshow("thresh", thresh)
cv2.imshow("binary", binary)
cv2.imshow("result", result)
cv2.waitKey(0)


Результат, показывающий контур (зеленый) по сравнению с подгонкой по кругу (красный):

enter image description here

Радиус и центр окружности:

radius = 117.6142467296168   center = 220.2169911178609 , 150.26823599797507



Метод наименьших квадратов (между точками контура и окружностью) может получить с помощью Scipy. Например, см .:

https://gist.github.com/lorenzoriano/6799568

https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html

2 голосов
/ 19 февраля 2020

Я бы предложил вычислить маску, как в ответе Натанси , но затем просто подсчитать количество пикселей в маске opening, которую он вычислил (что является объективной оценкой площади отверстия) и затем переводим область в радиус, используя radius = sqrt(area/pi). Это даст вам радиус круга с той же площадью, что и отверстие, и соответствует одному методу для получения круга наилучшего соответствия.

Другой способ получения круга наилучшего соответствия - это взять контур дыра (как возвращено в cnts на cv.findContours в ответе Нэтэнси), нахождение ее центроида , а затем вычисление среднего расстояния каждой вершины до центроида. Это приблизительно соответствует * подгонке круга наименьших квадратов к периметру отверстия.

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


Вот пример кода с использованием DIPlib (раскрытие: я автор) (примечание: для утверждения import PyDIP, приведенного ниже, требуется вы устанавливаете DIPlib, и вы не можете установить его с pip, на странице GitHub есть бинарный выпуск для Windows, или вам нужно собрать его из исходников).

import PyDIP as dip
import imageio
import math

img = imageio.imread('https://i.stack.imgur.com/szvc2.jpg')
img = dip.Image(img[:,2600:-1])
img.SetPixelSize(0.01, 'mm')      # Use your actual values!
bin = ~dip.OtsuThreshold(dip.Gauss(img, [3]))
bin = dip.Opening(bin, 25)
#dip.Overlay(img, bin - dip.BinaryErosion(bin, 1, 3)).Show()

msr = dip.MeasurementTool.Measure(dip.Label(bin), features=['Size', 'Radius'])
#print(msr)

print('Method 1:', math.sqrt(msr[1]['Size'][0] / 3.14), 'mm')
print('Method 2:', msr[1]['Radius'][1], 'mm')

MeasurementTool.Measure функция вычисляет 'Size', которая является областью; и 'Radius', который возвращает максимальное, среднее, минимальное и стандартное отклонение расстояний между каждым граничным пикселем и центроидом. Из 'Radius' мы берем 2-е значение, средний радиус.

Это выводит:

Method 1: 7.227900647539411 mm
Method 2: 7.225178113501325 mm

Но обратите внимание, что я назначил случайный размер пикселя (0,01 мм на пиксель), вам нужно будет указать правильное значение преобразования пикселов в мм.

Обратите внимание, что эти две оценки очень близки. Оба метода хорошие, объективные оценки. Первый метод в вычислительном отношении дешевле.

1 голос
/ 19 февраля 2020

Подход заключается в размытии по Гауссу , затем пороге Оцу изображения для получения двоичного изображения. Отсюда мы выполняем морфологическое открытие с эллиптическим ядром . Этот шаг будет эффективно удалять крошечные частицы шума. Чтобы получить хорошую оценку круга, мы находим контуры и используем cv2.minEnclosingCircle(), который также дает нам центральную точку и радиус. Вот визуализация:

Входное изображение (снимок экрана)

image

Двоичное изображение

image

Morph open

image

Результат -> Результат с радиусом

image image

Radius: 122.11396026611328

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

код

import cv2
import numpy as np

# Load image, convert to grayscale, Gaussian blur, then Otsu's threshold
image = cv2.imread('1.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (3,3), 0)
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

# Morph open with a elliptical shaped kernel
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)

# Find contours and draw minimum enclosing circle
cnts = cv2.findContours(opening, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
    ((x, y), r) = cv2.minEnclosingCircle(c)
    cv2.circle(image, (int(x), int(y)), int(r), (36, 255, 12), 2)
    print('Radius: {}'.format(r))

cv2.imshow('thresh', thresh)
cv2.imshow('opening', opening)
cv2.imshow('image', image)
cv2.waitKey()
1 голос
/ 18 февраля 2020

Одно предложение, которое я имею, смотрит на cv2.fitEllipse ()

OpenCV fitEllipse example on ballons

OpenCV fitEllipse result

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

...