webentwicklung-frage-antwort-db.com.de

Verbessern Sie die Genauigkeit der Bildverarbeitung, um Pilzsporen zu zählen

Ich versuche, die Anzahl der Sporen einer Krankheit aus einer mikroskopischen Probe mit Pythony zu zählen, aber bisher ohne großen Erfolg.

Weil die Farbe der Spore dem Hintergrund ähnlich ist und viele davon nahe beieinander liegen.

nach der fotografischen Mikroskopie der Probe.

 Microscopic photograph of spores

Bildverarbeitungscode:

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)

Der Algorithmus gezählt 11 Sporen

Aber im Bild sind 27 Sporen enthalten

Ergebnis der Bildverarbeitung zeigt, dass Sporen gruppiert sind  spores are grouped

Wie mache ich das genauer?

12
Georg Augusto

Zunächst ein paar vorläufige Codes, die wir unten verwenden werden:

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

Hier sehen Sie Ihr Originalbild:

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

Original image

Otsus Methode ist eine Möglichkeit, Farben zu segmentieren. Das Verfahren geht davon aus, dass die Intensität der Pixel des Bildes in ein bimodales Histogramm eingetragen werden kann, und er findet ein optimales Trennzeichen für dieses Histogramm. Ich wende die Methode unten an.

#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

Alle diese kleinen Flecken sind ärgerlich, wir können sie durch Dehnen loswerden:

#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

Wir müssen nun die Gipfel der Wasserscheide identifizieren und ihnen separate Etiketten geben. Das Ziel hiervon ist es, einen Satz von Pixeln zu erzeugen, so dass jede der Zellen ein Pixel in sich hat und dass sich nicht zwei Zellen mit ihren identifizierenden Pixeln berühren.

Um dies zu erreichen, führen wir eine Entfernungstransformation durch und filtern dann Entfernungen heraus, die zu weit vom Zentrum der Zelle entfernt sind.

#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

Jeder weiße Bereich im obigen Bild ist für den Algorithmus eine separate Zelle.

Nun identifizieren wir unbekannte Regionen, dh die Regionen, die durch den Algorithmus der Wasserscheide markiert werden, indem wir die Maxima abziehen:

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

Unknown regions

Die unbekannten Regionen sollten vollständige Donuts um jede Zelle bilden.

Als Nächstes geben wir den einzelnen Regionen, die sich aus der Distanztransformation ergeben, eindeutige Bezeichnungen und markieren dann die unbekannten Regionen, bevor sie die Wasserscheidetransformation durchführen:

# 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

Jetzt ist die Gesamtanzahl der Zellen die Anzahl der eindeutigen Marker minus 1 (um den Hintergrund zu ignorieren):

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

In diesem Fall bekommen wir 23.

Sie können dies mehr oder weniger genau tun, indem Sie den Entfernungsschwellenwert und den Erweiterungsgrad anpassen und möglicherweise h-Maxima (lokal Schwellenwert-Maxima) verwenden. Aber hüte dich vor Überanpassung; Nehmen Sie also nicht an, dass Sie durch die Optimierung eines einzelnen Bildes überall die besten Ergebnisse erzielen.

Unsicherheit schätzen

Sie können die Parameter auch algorithmisch geringfügig ändern, um ein Gefühl für die Unsicherheit in der Zählung zu erhalten. Das könnte so aussehen

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

Das Ergebnis geben:

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

Der Mittelwert der Zählwerte ist eine Möglichkeit, diese Unsicherheit in eine einzige Zahl zu integrieren.

Beachten Sie, dass für die Lizenzierung von StackOverflow geeignetattribution erforderlich ist. In der akademischen Arbeit kann dies durch Zitieren erfolgen.

12
Richard

Diese Pilzsporen haben ungefähr die gleiche Größe. Wenn Sie sich nicht für die exakte Genauigkeit interessieren, anstatt durch das Kaninchenloch zu springen und Grenzen und Wasserscheide zu erweitern, können Sie könnte ein sehr gutes Ergebnis erzielen einfache Änderung Ihrer aktuellen Algorithmen und eine Tonne mehr Genauigkeit.

Die Sporen in dieser Szene scheinen eine ähnliche Größe und ungefähr homogene Form zu haben. Unter der Voraussetzung, dass Sie möglicherweise in der Lage sind, den Bereich Ihrer Konturen zu verwenden, um die ungefähre Anzahl von Sporen zu ermitteln, die diesen Bereich einnehmen würden, wobei die durchschnittliche Sporenfläche verwendet wird. Sporen können diese willkürlichen Formen nicht vollständig ausfüllen, daher müssen Sie dies berücksichtigen. Sie erreichen dies, indem Sie die Hintergrundfarbe suchen und den Bereich entfernen, den die Hintergrundfarbe aus dem Konturbereich einnimmt. In solchen Szenen solltest du ganz nah dran an der realen Antwort für den Zellenbereich sein.

so um es zusammenzufassen:

Find average area of spore,

Find background color

Find contour area,

subtract background color pixels/area from contour

approximate_spore_count = ceil(contour_area / (average_area_of_spore))

Sie verwenden hier ceil, um die Tatsache zu berücksichtigen, dass Sie möglicherweise Sporen haben, die kleiner als der Durchschnitt sind, die einzeln gefunden werden. Sie können jedoch auch eine bestimmte Bedingung festlegen, um dies zu handhaben. Dann müssen Sie jedoch eine Entscheidung treffen, wenn Sie möchten Zählen Sie den Bruchteil einer Spore oder Runde zu einer ganzen Zahl mit Konturfläche> durchschnittliche Sporenfläche.

Sie können jedoch feststellen, dass, wenn Sie die Hintergrundfarbe herausfinden können und Ihre Sporen ungefähr gleich geformt und von homogener Farbe sind, Sie eine bessere Leistung erzielen, wenn Sie einfach den Bereich der Hintergrundfarbe vom gesamten Bild subtrahieren und dividiere die durchschnittliche Sporengröße aus dem verbleibenden Bereich. Dies wäre viel schneller als die Verwendung von Dilatation.

Eine andere Sache, die Sie in Betracht ziehen sollten, obwohl ich nicht glaube, dass sie Ihr Problem mit der Klumpenbildung unbedingt beheben wird, ist die Verwendung von OpenCV eingebaute Blob-Erkennung , was möglicherweise hilfreich sein kann, wenn Sie sich für den Gebietsansatz entscheiden Sie mit den Randfällen, die der Farbverlauf in Ihrem Hintergrund darstellen könnte. Mit der Blob-Erkennung können Sie nur Blobs erkennen und die gesamte Blob-Fläche durch die durchschnittliche Sporenfläche teilen. Sie können dieses Tutorial folgen, um zu verstehen, wie man es in Python verwendet. Möglicherweise stellen Sie auch den Erfolg eines einfachen Konturansatzes fest, indem Sie die Konturen von opencv verwenden, die für Ihren Anwendungsfall hilfreich sind.

TLDR: Ihre Sporen haben ungefähr die gleiche Größe und Farbe, Ihr Hintergrund ist ungefähr homogen, verwenden Sie die durchschnittliche Sporenfläche und teilen Sie die Fläche durch die Sporenfarben, um eine genauere Zählung zu erhalten

Adendum:

Wenn Sie Schwierigkeiten haben, die durchschnittliche Sporenfläche zu finden, können Sie, wenn Sie eine Vorstellung von der durchschnittlichen "Einsamkeit" einer Spore haben (deutlich getrennt), die Konturen/Kleckse nach Fläche sortieren und dann den Boden abnehmen n% der Sporen nach der "Einsamkeitswahrscheinlichkeit" (n) und deren Durchschnitt. Solange "Einsamkeit" nicht stark von der Sporengröße abhängt, sollte dies eine ziemlich genaue Messung der durchschnittlichen Sporengröße sein. Dies funktioniert, denn wenn Sie davon ausgehen, dass die Sporen gleichmäßig "einsam" sind, können Sie sich das als Zufallsstichprobe vorstellen. Wenn Sie den durchschnittlichen Prozentsatz an Einsamkeit kennen, werden Sie wahrscheinlich einen sehr hohen Prozentsatz an Einsamkeit bekommen Sporen, wenn Sie diese% n der sortierten Sporen nach Größe sortieren (oder n leicht verkleinern, um die Wahrscheinlichkeit zu verringern, versehentlich große Sporen zu ergreifen). Sie müssten dies theoretisch nur einmal tun, wenn Sie den Zoomfaktor kennen.

0
opa