web-dev-qa-db-fra.com

Améliorer la précision du traitement des images pour compter les spores de champignons

J'essaie de compter la quantité de spores d'une maladie provenant d'un échantillon microscopique avec Pythony, mais jusqu'à présent sans grand succès.

Parce que la couleur de la spore est semblable à celle de l'arrière-plan et que beaucoup sont proches.

suivant la microscopie photographique de l'échantillon.

 Microscopic photograph of spores

Code de traitement d'image:

import numpy as np
import argparse
import imutils
import cv2

ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required=True,
                help="path to the input image")
ap.add_argument("-o", "--output", required=True,
                help="path to the output image")
args = vars(ap.parse_args())

counter = {}

image_orig = cv2.imread(args["image"])
height_orig, width_orig = image_orig.shape[:2]

image_contours = image_orig.copy()

colors = ['Yellow']
for color in colors:

    image_to_process = image_orig.copy()

    counter[color] = 0

    if color == 'Yellow':
        lower = np.array([70, 150, 140])  #rgb(151, 143, 80)
        upper = np.array([110, 240, 210])  #rgb(212, 216, 106)

    image_mask = cv2.inRange(image_to_process, lower, upper)

    image_res = cv2.bitwise_and(
        image_to_process, image_to_process, mask=image_mask)

    image_gray = cv2.cvtColor(image_res, cv2.COLOR_BGR2GRAY)
    image_gray = cv2.GaussianBlur(image_gray, (5, 5), 50)

    image_edged = cv2.Canny(image_gray, 100, 200)
    image_edged = cv2.dilate(image_edged, None, iterations=1)
    image_edged = cv2.erode(image_edged, None, iterations=1)

    cnts = cv2.findContours(
        image_edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    cnts = cnts[0] if imutils.is_cv2() else cnts[1]

    for c in cnts:

        if cv2.contourArea(c) < 1100:
            continue

        hull = cv2.convexHull(c)
        if color == 'Yellow':

            cv2.drawContours(image_contours, [hull], 0, (0, 0, 255), 1)

        counter[color] += 1      

print("{} esporos {}".format(counter[color], color))

cv2.imwrite(args["output"], image_contours)

L'algorithme compté 11 spores

Mais dans l'image contient 27 spores

Le résultat du traitement de l’image montre que les spores sont regroupées  spores are grouped

Comment puis-je rendre cela plus précis?

12
Georg Augusto

Tout d'abord, un code préliminaire que nous utiliserons ci-dessous:

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()

Pour référence, voici votre image d'origine:

#Read in image
img         = cv2.imread('cells.jpg')
ShowImage('Original',img,'bgr')

 Original image

La méthode d'Otsu est un moyen de segmenter les couleurs. La méthode suppose que l’intensité des pixels de l’image puisse être tracée dans un histogramme bimodal et trouve un séparateur optimal pour cet histogramme. J'applique la méthode ci-dessous.

#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')

 Grayscale cells  Tresholded cells

Toutes ces petites taches sont gênantes, on peut s'en débarrasser en les dilatant:

#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')

 With noise eliminated

Nous devons maintenant identifier les sommets du bassin versant et leur attribuer des étiquettes distinctes. L'objectif est de générer un ensemble de pixels tel que chacune des cellules contienne un pixel et qu'aucune cellule ne possède son identificateur de pixels en contact.

Pour ce faire, nous effectuons une transformation de distance puis filtrons les distances trop éloignées du centre de la cellule.

#Calculate distance transformation
dist         = cv2.distanceTransform(dilated,cv2.DIST_L2,5)
ShowImage('Distance',dist,'gray')

 Distance Transformation

#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')

 Foreground isolation

Chaque zone de blanc dans l'image ci-dessus est, en ce qui concerne l'algorithme, une cellule distincte.

Nous identifions maintenant les régions inconnues, celles qui seront étiquetées par l’algorithme de gestion des bassins versants, en soustrayant les maxima:

# Finding unknown region
unknown = cv2.subtract(dilated,sure_fg.astype(np.uint8))
ShowImage('Unknown',unknown,'gray')

 Unknown regions

Les régions inconnues doivent former des beignets complets autour de chaque cellule.

Ensuite, nous donnons à chacune des régions distinctes résultant de la transformation de distance des étiquettes uniques, puis marquons les régions inconnues avant de procéder à la transformation des bassins versants:

# 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')

 Connected components  Uncertain area  Separate cells

Maintenant, le nombre total de cellules est le nombre de marqueurs uniques moins 1 (pour ignorer le fond):

len(set(markers.flatten()))-1

Dans ce cas, nous obtenons 23.

Vous pouvez rendre cela plus ou moins précis en ajustant le seuil de distance, le degré de dilatation, en utilisant éventuellement h-maxima (maxima à seuil local). Mais méfiez-vous de l'overfitting; En d'autres termes, ne supposez pas que le réglage d'une seule image vous donnera les meilleurs résultats partout.

Estimation de l'incertitude

Vous pouvez également modifier légèrement les paramètres de manière algorithmique pour avoir une idée de l’incertitude du décompte. Cela pourrait ressembler à ceci

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)))

Donner le résultat:

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

Prendre la médiane des valeurs de comptage est un moyen d'incorporer cette incertitude dans un nombre unique.

N'oubliez pas que la licence de StackOverflow nécessite que vous donniez appropriéattribution . Dans le travail académique, cela peut être fait via la citation.

12
Richard