Вы получили почти все права, если вас не беспокоит один ошибочно классифицированный блоб, частичные капли вообще не обнаруживаются и (очевидно) неточный размер для некоторых блобов.
Последняя проблема, которую нужно решить, - это получить разумный порог между светлыми и темными каплями.Один из способов сделать это - использовать адаптивный порог, например, метод Отсу или др.
Здесь вы найдете другие пороговые методы от scikit-learn..
РЕДАКТИРОВАТЬ: Обновлено, чтобы лучше соответствовать тому, что вы просили.
Вкратце, по сравнению с вашим кодом, я сделал следующие изменения:
- Поместите весь код в функции (это помогает мне лучше рассуждать)
- Я определил функцию улучшения контраста, но она не используется в коде (потому что у меня получались худшие результаты.)
- определяет функцию, которая генерирует маски, связанные с кругами (обратите внимание, что эта функция будет доступна, с немного другими параметрами, в PyMRT - Отказ от ответственности : Я являюсьосновной автор этого.)
- пороговые круги с использованием масок сверху и метод Otsu для определения оптимального порогового значения
(незначительное примечание: я сохранил входное изображение как blobs.jpg
).
Это то, как я бы это сделал, но я уверен, что есть место для улучшения его устойчивости путем настройки параметров.
import numpy as np
import cv2
import matplotlib.pyplot as plt
from skimage.filters import threshold_otsu
# based on: https://stackoverflow.com/questions/46626267/how-to-generate-a-sphere-in-3d-numpy-array/46626448#46626448
def circle(shape, radius, position):
semisizes = (radius,) * 2
grid = [slice(-x0, dim - x0) for x0, dim in zip(position, shape)]
position = np.ogrid[grid]
arr = np.zeros(shape, dtype=float)
for x_i, semisize in zip(position, semisizes):
arr += (np.abs(x_i / semisize) ** 2)
return arr <= 1.0
def enhance_contrast(
in_img,
save_filepath=None):
"""Enhance contrast."""
lab_img = cv2.cvtColor(in_img, cv2.COLOR_BGR2LAB)
l_ch, a_ch, b_ch = cv2.split(lab_img)
# Applying CLAHE to L-channel
clahe_filter = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
l_ch = clahe_filter.apply(l_ch)
out_img = cv2.merge((l_ch, a_ch, b_ch))
out_img = cv2.cvtColor(out_img, cv2.COLOR_LAB2BGR)
if save_filepath:
cv2.imwrite(save_filepath, out_img)
return out_img
def find_circles(
in_filepath,
out_filepath='circles_{in_filepath}',
enh_filepath='enh_{in_filepath}',
hough_circles_kws=(
('dp', 1), ('minDist', 15), ('param1', 30), ('param2', 30),
('minRadius', 5), ('maxRadius', 25)),
verbose=True):
"""Find circles in image."""
out_filepath = out_filepath.format(**locals())
enh_filepath = enh_filepath.format(**locals())
hough_circles_kws = dict(hough_circles_kws) if hough_circles_kws else {}
in_img = cv2.imread(in_filepath)
lab_img = cv2.cvtColor(in_img, cv2.COLOR_BGR2LAB)
l_ch, a_ch, b_ch = cv2.split(lab_img)
blur_l_ch = cv2.medianBlur(l_ch, 1)
circles = cv2.HoughCircles(blur_l_ch, cv2.HOUGH_GRADIENT, **hough_circles_kws)
if circles is not None:
values_img = l_ch
# compute means
if verbose:
print('Image size: ', values_img.shape)
circles = np.squeeze(circles)
values = []
for x0, y0, r in circles:
mask = circle(values_img.shape, r, (y0, x0))
values.append(np.percentile(values_img[mask], 90))
circles = np.concatenate((circles, np.array(values).reshape(-1, 1)), -1)
threshold = threshold_otsu(np.array(values))
if verbose:
print('Threshold: ', threshold)
# plot circles
for x0, y0, r, mean in circles:
if mean > threshold:
# good circles in green
cv2.circle(in_img, (int(x0), int(y0)), int(r), (0, 255, 0), 2)
else:
# bad circles in red
cv2.circle(in_img, (int(x0), int(y0)), int(r), (0, 0, 255), 2)
if verbose:
print('Circles:')
print(circles)
print('Num Circles: ', circles.shape[0])
print('Good Circles: ', np.sum(values > threshold))
if out_filepath:
cv2.imwrite(out_filepath.format(**locals()), in_img)
return out_filepath, circles, threshold
out_filepath, circles, threshold = find_circles('blobs.jpg')
Это приведет к следующему выводу:
Image size: (230, 294)
Threshold: 96.1328125
Circles:
[[ 36.5 108.5 21.10000038 155.5 ]
[170.5 124.5 24.39999962 170. ]
[ 43.5 156.5 21.10000038 156.5 ]
[ 33.5 57.5 22.20000076 190. ]
[101.5 40.5 19.89999962 90. ]
[ 75.5 78.5 18.79999924 88. ]
[254.5 171.5 16.60000038 82. ]
[138.5 52.5 15.39999962 90. ]
[123.5 148.5 14.39999962 90. ]
[ 42.5 199.5 15.39999962 174. ]
[138.5 15.5 14.10000038 88. ]
[ 86.5 176.5 15.39999962 90. ]
[256.5 23.5 15.5 146. ]
[211.5 140.5 14.39999962 87. ]
[132.5 193.5 13.19999981 90.1 ]
[174.5 35.5 9.60000038 93. ]
[ 81.5 129.5 11. 93. ]
[223.5 54.5 9.60000038 87. ]
[177.5 75.5 13.19999981 146. ]
[214.5 195.5 11. 90. ]
[259.5 126.5 9.60000038 90. ]
[ 62.5 22.5 11. 96. ]
[220.5 98.5 9.60000038 89. ]
[263.5 77.5 12.10000038 84.1 ]
[116.5 101.5 9.60000038 92. ]
[170.5 177.5 11. 91. ]
[251.5 215.5 11. 91. ]
[167.5 215.5 11. 87. ]
[214.5 14.5 9.60000038 92. ]]
Num Circles: 29
Good Circles: 7
и соответствующее изображение:
(и, конечно, вы можете адаптировать приведенный выше код, чтобы лучше соответствовать вашим потребностям).
РЕДАКТИРОВАТЬ: включены некоторые код и цифры.
Также можно построить график хороших / плохих результатов:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
values = circles[:, -1]
data = [np.sum(values <= threshold), np.sum(values > threshold)]
labels = ['Bad', 'Good']
colors = ['red', 'green']
ax.bar(labels, data, color=colors)
plt.show()
Или для построения полной гистограммы, например:
fig, ax = plt.subplots()
hist, edges = np.histogram(values, bins=40)
widths = (edges[1:] - edges[:-1])
ax.bar(edges[:-1] + widths / 2, hist, widths) # plots the histogram
ax.axvline(x=threshold, color='black') # plots the threshold (optional)
plt.show()
РЕДАКТИРОВАТЬ: включены дополнительныегистограмма и гистограмма