Во-первых, предварительный код, который мы будем использовать ниже:
import numpy as np
import cv2
from matplotlib import pyplot as plt
from skimage.morphology import extrema
from skimage.morphology import watershed as skwater
def ShowImage(title,img,ctype):
if ctype=='bgr':
b,g,r = cv2.split(img) # get b,g,r
rgb_img = cv2.merge([r,g,b]) # switch it to rgb
plt.imshow(rgb_img)
elif ctype=='hsv':
rgb = cv2.cvtColor(img,cv2.COLOR_HSV2RGB)
plt.imshow(rgb)
elif ctype=='gray':
plt.imshow(img,cmap='gray')
elif ctype=='rgb':
plt.imshow(img)
else:
raise Exception("Unknown colour type")
plt.title(title)
plt.show()
Для справки, вот ваше оригинальное изображение:
#Read in image
img = cv2.imread('cells.jpg')
ShowImage('Original',img,'bgr')
Метод Оцу - это один из способов сегментировать цвета.Метод предполагает, что интенсивность пикселей изображения может быть нанесена на бимодальную гистограмму, и находит оптимальный разделитель для этой гистограммы.Я применяю метод ниже.
#Convert to a single, grayscale channel
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#Threshold the image to binary using Otsu's method
ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
ShowImage('Grayscale',gray,'gray')
ShowImage('Applying Otsu',thresh,'gray')
Все эти маленькие пятнышки раздражают, мы можем избавитьсяиз них путем расширения:
#Adjust iterations until desired result is achieved
kernel = np.ones((3,3),np.uint8)
dilated = cv2.dilate(thresh, kernel, iterations=5)
ShowImage('Dilated',dilated,'gray')
Теперь нам нужно идентифицировать пики водораздела и дать им отдельные метки.Цель этого состоит в том, чтобы сгенерировать набор пикселей так, чтобы каждая из ячеек имела пиксель внутри, и ни у двух ячеек не было соприкасающихся пикселей идентификатора.
Для этого мы выполняем преобразование расстояния и затем фильтруемрасстояния, которые находятся слишком далеко от центра ячейки.
#Calculate distance transformation
dist = cv2.distanceTransform(dilated,cv2.DIST_L2,5)
ShowImage('Distance',dist,'gray')
#Adjust this parameter until desired separation occurs
fraction_foreground = 0.6
ret, sure_fg = cv2.threshold(dist,fraction_foreground*dist.max(),255,0)
ShowImage('Surely Foreground',sure_fg,'gray')
Каждая область белого на приведенном выше изображении, с точки зрения алгоритма, представляет собой отдельную ячейку.
Теперь мы идентифицируем неизвестные области, области, которые будут помечены алгоритмом водораздела,вычитая максимумы:
# Finding unknown region
unknown = cv2.subtract(dilated,sure_fg.astype(np.uint8))
ShowImage('Unknown',unknown,'gray')
Неизвестные области должны образовывать полные пончики вокруг каждой клетки.
Далее мыдайте каждой из отдельных областей, полученных в результате уникальных меток преобразования расстояния, а затем отметьте неизвестные области перед окончательным выполнением преобразования водораздела:
# Marker labelling
ret, markers = cv2.connectedComponents(sure_fg.astype(np.uint8))
ShowImage('Connected Components',markers,'rgb')
# Add one to all labels so that sure background is not 0, but 1
markers = markers+1
# Now, mark the region of unknown with zero
markers[unknown==np.max(unknown)] = 0
ShowImage('markers',markers,'rgb')
dist = cv2.distanceTransform(dilated,cv2.DIST_L2,5)
markers = skwater(-dist,markers,watershed_line=True)
ShowImage('Watershed',markers,'rgb')
Теперь общее количество ячеек равно количеству уникальных маркеров минус 1 (чтобы игнорировать фон):
len(set(markers.flatten()))-1
В этом случае мы получим 23.
Вы можете сделать это более или менее точным, отрегулировав порог расстояния, степень расширения, возможно, используя h-максимумы (локально-пороговые максимумы).Но остерегайтесь переоснащения;то есть, не думайте, что настройка для одного изображения даст вам лучшие результаты везде.
Оценка неопределенности
Вы также можете алгоритмически слегка изменить параметры, чтобы получить представление о неопределенностив подсчете.Это может выглядеть так:
import numpy as np
import cv2
import itertools
from matplotlib import pyplot as plt
from skimage.morphology import extrema
from skimage.morphology import watershed as skwater
def CountCells(dilation=5, fg_frac=0.6):
#Read in image
img = cv2.imread('cells.jpg')
#Convert to a single, grayscale channel
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#Threshold the image to binary using Otsu's method
ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
#Adjust iterations until desired result is achieved
kernel = np.ones((3,3),np.uint8)
dilated = cv2.dilate(thresh, kernel, iterations=dilation)
#Calculate distance transformation
dist = cv2.distanceTransform(dilated,cv2.DIST_L2,5)
#Adjust this parameter until desired separation occurs
fraction_foreground = fg_frac
ret, sure_fg = cv2.threshold(dist,fraction_foreground*dist.max(),255,0)
# Finding unknown region
unknown = cv2.subtract(dilated,sure_fg.astype(np.uint8))
# Marker labelling
ret, markers = cv2.connectedComponents(sure_fg.astype(np.uint8))
# Add one to all labels so that sure background is not 0, but 1
markers = markers+1
# Now, mark the region of unknown with zero
markers[unknown==np.max(unknown)] = 0
markers = skwater(-dist,markers,watershed_line=True)
return len(set(markers.flatten()))-1
#Smaller numbers are noisier, which leads to many small blobs that get
#thresholded out (undercounting); larger numbers result in possibly fewer blobs,
#which can also cause undercounting.
dilations = [4,5,6]
#Small numbers equal less separation, so undercounting; larger numbers equal
#more separation or drop-outs. This can lead to over-counting initially, but
#rapidly to under-counting.
fracs = [0.5, 0.6, 0.7, 0.8]
for params in itertools.product(dilations,fracs):
print("Dilation={0}, FG frac={1}, Count={2}".format(*params,CountCells(*params)))
Получение результата:
Dilation=4, FG frac=0.5, Count=22
Dilation=4, FG frac=0.6, Count=23
Dilation=4, FG frac=0.7, Count=17
Dilation=4, FG frac=0.8, Count=12
Dilation=5, FG frac=0.5, Count=21
Dilation=5, FG frac=0.6, Count=23
Dilation=5, FG frac=0.7, Count=20
Dilation=5, FG frac=0.8, Count=13
Dilation=6, FG frac=0.5, Count=20
Dilation=6, FG frac=0.6, Count=23
Dilation=6, FG frac=0.7, Count=24
Dilation=6, FG frac=0.8, Count=14
Получение медианы значений счетчика - один из способов включения этой неопределенности в одно число.
Помните, что для лицензирования StackOverflow необходимо указать соответствующую атрибуцию .В академической работе это может быть сделано путем цитирования.