Schädelentfernung: Bildsegmentierung in 3D-MRT-Bildern

Inhalt

Dieser Artikel wurde im Rahmen der Data Science Blogathon

Die Entfernung des Schädels ist einer der ersten Schritte auf dem Weg zur Erkennung von Anomalien im Gehirn.. Es ist der Prozess der Isolierung von Hirngewebe von Nicht-Gehirngewebe aus einem MRT-Bild eines Gehirns. Diese Segmentierung des Gehirns vom Schädel ist selbst für erfahrene Radiologen eine mühsame Aufgabe und die Genauigkeit der Ergebnisse variiert stark von Person zu Person.. Hier versuchen wir, den Prozess zu automatisieren, indem wir eine End-to-End-Pipeline erstellen, bei der wir nur das MRT-Rohbild eingeben müssen und die Pipeline nach der erforderlichen Vorverarbeitung ein segmentiertes Bild des Gehirns generieren soll..

Dann, Was ist ein MRT-Bild??

1fi2vww-5delfs1b4ccryya-1785122
https://www.diagnosticimaging.com/view/mri-shows-brain-abnormalities-some-covid-19-patients

Um ein MRT-Bild eines Patienten zu erhalten, werden in einen Tunnel mit einem Magnetfeld im Inneren eingeführt. Dadurch werden alle Protonen im Körper “ausrichten”, also ist sein Quantenspin derselbe. Dann, ein oszillierender Magnetfeldimpuls wird verwendet, um diese Ausrichtung zu stören. Wenn die Protonen ins Gleichgewicht zurückkehren, eine elektromagnetische Welle aussenden. Je nach Fettgehalt, die chemische Zusammensetzung und, Was ist wichtiger, die Art der Stimulation (nämlich, die sequenzen) verwendet, um Protonen zu zerstören, verschiedene Bilder werden erhalten. Vier häufige Sequenzen, die erhalten werden, sind T1, T1 mit Kontrast (T1C), T2, Ja INSTINKT.

Häufige Herausforderungen bei der Arbeit mit der Bildgebung des Gehirns

  • Herausforderungen realer Daten

    Ein Modell zu bauen und eine gute Genauigkeit auf einem Jupyter-Laptop zu erreichen, ist gut. Aber die meiste Zeit, ein Modell mit sehr guter Leistung schneidet bei realen Daten sehr schlecht ab. Dies geschieht aufgrund von Datendrift, wenn das Modell völlig andere Daten sieht, als es trainiert wurde.. In unserem Fall, es kann aufgrund von Unterschieden in einigen Parametern oder Magnetresonanztomographie-Methoden vorkommen. Hier ist ein Blog Beschreibung einiger realer KI-Fehler.

Problem Formulierung

Die Aufgabe, die wir hier haben, besteht darin, ein 3D-MRT-Bild zu erstellen, mit dem wir das Gehirn identifizieren und das Gehirngewebe aus dem Vollbild eines Schädels segmentieren müssen.. Für diese Aufgabe, wir werden ein grundlegendes Wahrheits-Tag haben und, Daher, wird eine überwachte Bildsegmentierungsaufgabe sein. Wir werden verwenden Verlust von Würfeln als unsere Verlustfunktion.

Daten

Werfen wir einen Blick auf den Datensatz, den wir für diese Aufgabe verwenden werden. Der Datensatz kann heruntergeladen werden unter hier.

Das Repository enthält Daten von 125 Teilnehmer, von 21 ein 45 Jahre, mit einer Vielzahl von klinischen und subklinischen psychiatrischen Symptomen. Für jeden Teilnehmer, das Repository enthält:

  • Strukturelle T1-gewichtete Magnetresonanztomographie (gesichtslos): Dies ist ein T1-gewichtetes MRT-Rohbild mit einem einzigen Kanal.
  • Gehirnmaske: ist die Bildmaske des Gehirns oder kann man sie die fundamentale Wahrheit nennen. Erhalten mit der Beast-Methode (Gehirnextraktion basierend auf nicht-lokaler Segmentierung) und Anwenden manueller Bearbeitungen durch Domänenexperten, um Nicht-Hirn-Gewebe zu entfernen.
  • Abgestreiftes Schädelbild: dies kann man sich als Teil des Gehirns vorstellen, dem das vorherige T1-gewichtete Bild entzogen wurde. Dies ist vergleichbar mit dem Überlagern von Masken auf realen Bildern.

Die Auflösung der Bilder ist 1 mm3 und jede Datei ist im NiFTI-Format (.nii.gz). Ein einzelner Datenpunkt sieht so aus …

1-kcvvzh7aogzyxs6m7zygw-8622226
Abgestreiftes Schädelbild

Vorverarbeitung unserer Raw-Bilder

img=nib.load('/content/NFBS_Dataset/A00028185/sub-A00028185_ses-NFB3_T1w.nii.gz')
drucken('Bildform=",img.shape)
1tt3snsvdt2pc7vhvsqn96a-2696271

Stellen Sie sich die 3D-Bilder oben vor, als hätten wir es getan 192 Bilder in 2D-Größe 256 * 256 übereinander gestapelt.

Lassen Sie uns einen Datenrahmen erstellen, der die Position der Bilder und der entsprechenden Masken und der Bilder ohne Schädel enthält.

#Speichern der Adresse von 3 Dateitypen
Importieren von OS
brain_mask=[]
Gehirn=[]
roh=[]
für Unterverzeichnis, dirs, Dateien in os.walk("/Inhalt/NFBS_Dataset'):
    für Datei in Dateien:
        #os.path.join drucken(Unterverzeichnis, Datei)ja
        Dateipfad = Unterverzeichnis + os.sep + Datei

        if filepath.endswith(".gz"):
          if '_Gehirnmaske.' im Dateipfad:
            brain_mask.append(Dateipfad)
          elif '_hirn.' im Dateipfad:
            Gehirn.anhängen(Dateipfad)
          anders:
            raw.anhängen(Dateipfad)
1tlgnedzbophwntu0wf5-mq-1635190

Das Polarisationsfeldsignal ist ein sehr weiches, niederfrequentes Signal, das MRT-Bilder verfälscht., insbesondere solche, die von alten MRTs erzeugt werden (Magnetresonanztomographie) Maschinen. Bildverarbeitungsalgorithmen wie Segmentierung, Texturanalyse oder Klassifizierung anhand von Bildpixel-Graustufenwerten führt nicht zu zufriedenstellenden Ergebnissen. Ein Vorverarbeitungsschritt ist erforderlich, um das Polarisationsfeldsignal zu korrigieren, bevor beschädigte MRT-Bilder an solche Algorithmen gesendet werden, oder die Algorithmen müssen modifiziert werden.

  • Zuschneiden und Größe ändern

    Aufgrund von rechnerischen Einschränkungen bei der Anpassung des vollständigen Bildes an das Modell hier, Wir haben uns entschieden, die Größe des MRT-Bildes von (256 * 256 * 192) ein (96 * 128 * 160). Die Größe des Ziels ist so gewählt, dass der größte Teil des Schädels erfasst wird und, nach dem Zuschneiden und Ändern der Größe, wirkt zentrierend auf Bilder.

  • Intensitätsnormalisierung

    Die Normalisierung ändert und skaliert ein Bild, sodass die Pixel im Bild null Mittelwert und Einheitsvarianz aufweisen. Dies hilft dem Modell, schneller zu konvergieren, indem Skalenabweichungen eliminiert werden. Unten ist der Code dafür.

    Klassenvorverarbeitung():
      def __init__(selbst,df):
        self.data=df
        self.raw_index=[]
        self.mask_index=[]
      def Bias_Korrektur(selbst):
        !mkdir bias_korrektur
        n4 = N4BiasFieldCorrection()
        n4.Eingänge.Maß = 3
        n4.inputs.shrink_factor = 3
        n4.inputs.n_iterations = [20, 10, 10, 5]
        index_corr=[]
        für ich in tqdm(Bereich(len(self.data))):
          n4.inputs.input_image = self.data.raw.iloc[ich]
          n4.inputs.output_image="Voreingenommenheit_Korrektur/"+str(ich)+'.nii.gz'
          index_korr.anhängen('bias_correction/'+str(ich)+'.nii.gz')
          res = n4.run()
        index_corr=['bias_correction/'+str(ich)+'.nii.gz' für i in Reichweite(125)]
        Daten['bias_korr']=index_korr
        drucken('Bias-korrigierte Bilder gespeichert unter : Voreingenommenheit_Korrektur/')
      def resize_crop(selbst):
        #Reduzieren der Bildgröße aufgrund von Speicherbeschränkungen
        !mkdir in der Größe geändert
        target_shape = np.array((96,128,160))                   #Verkleinerung des Bildes von 256*256*192 zu 96*128*160
        neue_auflösung = [2,]*3
        new_affine = np.null((4,4))
        neu_affin[:3,:3] = np.diag(neue_auflösung)
        # Punkt setzen 0,0,0 mitten im neuen Band - das könnte in Zukunft verfeinert werden
        neu_affin[:3,3] = Zielform*neue_Auflösung/2.*-1
        neu_affin[3,3] = 1.
        raw_index=[]
        mask_index=[]
        #Größe von Bild und Maske ändern und im Ordner speichern
        für mich in Reichweite(len(Daten)):
          downsampled_and_cropped_nii = resample_img(self.data.bias_corr.iloc[ich], target_affine=new_affine, target_shape=target_shape, interpolation='nächste')
          downsampled_and_cropped_nii.to_filename('Größe geändert/roh'+str(ich)+'.nii.gz')
          self.raw_index.append('Größe geändert/roh'+str(ich)+'.nii.gz')
          downsampled_and_cropped_nii = resample_img(self.data.brain_mask.iloc[ich], target_affine=new_affine, target_shape=target_shape, interpolation='nächste')
          downsampled_and_cropped_nii.to_filename('Größe/Maske'+Str(ich)+'.nii.gz')
          self.mask_index.append('Größe/Maske'+Str(ich)+'.nii.gz')
        Rückgabe self.raw_index,self.mask_index
      def Intensität_Normalisierung(selbst):
        für i in self.raw_index:
          image = sitk.ReadImage(ich)
          resacleFilter = sitk.RescaleIntensityImageFilter()
          resacleFilter.SetOutputMaximum(255)
          resacleFilter.SetOutputMinimum(0)
          image = resacleFilter.Execute(Bild)
          sitk.WriteImage(Bild,ich)
        drucken('Normalisierung abgeschlossen. Bilder gespeichert unter: verkleinert/')

Modellieren

Nun, da unsere Vorverarbeitung abgeschlossen ist, wir können anfangen zu modellieren. Zuerst, Wir machen einen Split-Train-Test. Später, Wir werden einen benutzerdefinierten Datengenerator verwenden, um die Eingabebilder in das Modell einzuspeisen.

Werfen wir einen Blick auf die Architektur des Modells.

  def data_gen(selbst,img_list, mask_list, batch_size):
    '''Benutzerdefinierter Datengenerator zum Einspeisen von Bildern in das Modell'''
    c = 0
    n = [i für i in Reichweite(len(img_list))]  #Liste der Trainingsbilder
    random.shuffle(n)
    
    während (Wahr):
      img = np.null((batch_size, 96, 128, 160,1)).astyp('schweben')   #Hinzufügen zusätzlicher Dimensionen, da conv3d die Dateigröße nimmt 5
      Maske = np.null((batch_size, 96, 128, 160,1)).astyp('schweben')

      für mich in Reichweite(C, c+batch_größe): 
        train_img = nib.load(img_list[n[ich]]).Daten bekommen()
        
        train_img=np.expand_dims(train_img,-1)
        train_mask = nib.load(mask_list[n[ich]]).Daten bekommen()

        train_mask=np.expand_dims(train_mask,-1)

        img[NS]=train_img
        Maske[NS] = train_mask
      c+=batch_size
      wenn(c+batch_größe>= len(img_list)):
        c=0
        random.shuffle(n)

      Ertrag img,Maske
1_pvqilgjalr6pi2nnirl-w-2075584

Wir verwenden ein U-Net 3D als unsere Architektur. Wenn Sie mit dem U-Net 2D bereits vertraut sind, das wird ganz einfach. Zuerst, Wir haben einen Schrumpfpfad durch einen Encoder, der die Bildgröße allmählich reduziert und die Anzahl der Filter erhöht, um Engpasseigenschaften zu erzeugen. Dieser wird dann einem Decoderblock zugeführt, der die Größe nach und nach erweitert, um schließlich eine Maske als beabsichtigte Ausgabe zu erzeugen.

  def Faltungsblock(Eingang, Filter=3, kernel_size=3, Batchnorm = True):
    '''conv-Layer gefolgt von Batchnormalisierung'''
    x = Conv3D(Filter = Filter, Kernelgröße = (Kernelgröße, Kernelgröße,Kernelgröße),
               kernel_initializer="er_normal", Polsterung = 'gleich')(Eingang)
    wenn Batchnorm:
        x = BatchNormalisierung()(x)
    x = Aktivierung('relu')(x)
    x = Conv3D(Filter = Filter, Kernelgröße = (Kernelgröße, Kernelgröße,Kernelgröße),
               kernel_initializer="er_normal", Polsterung = 'gleich')(Eingang)
    wenn Batchnorm:
        x = BatchNormalisierung()(x)
    x = Aktivierung('relu')(x) 
    Rückgabe x
def resunet_opt(input_img, Filter = 64, Ausfall = 0.2, Batchnorm = True):
    """Rest 3D Unet"""
    conv1 = Faltungsblock(input_img, Filter * 1, Kernelgröße = 3, Batchnorm = Batchnorm)
    pool1 = MaxPooling3D((2, 2, 2))(conv1)
    drop1 = Ausfall(aussteigen)(Pool1)

    conv2 = Faltungsblock(drop1, Filter * 2, Kernelgröße = 3, Batchnorm = Batchnorm)
    pool2 = MaxPooling3D((2, 2, 2))(conv2)
    drop2 = Ausfall(aussteigen)(Pool2)

    conv3 = Faltungsblock(drop2, Filter * 4, Kernelgröße = 3, Batchnorm = Batchnorm)
    pool3 = MaxPooling3D((2, 2, 2))(conv3)
    drop3 = Ausfall(aussteigen)(Pool3)

    conv4 = Faltungsblock(drop3, Filter * 8, Kernelgröße = 3, Batchnorm = Batchnorm)
    pool4 = MaxPooling3D((2, 2, 2))(conv4)
    drop4 = Ausfall(aussteigen)(Pool4)

    conv5 = Faltungsblock(drop4, Filter = Filter * 16, Kernelgröße = 3, Batchnorm = Batchnorm)
    conv5 = Faltungsblock(conv5, Filter = Filter * 16, Kernelgröße = 3, Batchnorm = Batchnorm)
    
    ups6 = Conv3DTranspose(Filter * 8, (3, 3, 3), Schritte = (2, 2, 2), Polsterung = 'gleich',Aktivierung = 'neu lesen',kernel_initializer="er_normal")(conv5)
    ups6 = verketten([ups6, conv4])
    ups6 = Aussteiger(aussteigen)(ups6)
    conv6 = Faltungsblock(ups6, Filter * 8, Kernelgröße = 3, Batchnorm = Batchnorm)

    ups7 = Conv3DTranspose(Filter * 4, (3, 3, 3), Schritte = (2, 2, 2), Polsterung = 'gleich',Aktivierung = 'neu lesen',kernel_initializer="er_normal")(conv6)
    ups7 = verketten([ups7, conv3])
    ups7 = Aussteiger(aussteigen)(ups7)
    conv7 = Faltungsblock(ups7, Filter * 4, Kernelgröße = 3, Batchnorm = Batchnorm)

    ups8 = Conv3DTranspose(Filter * 2, (3, 3, 3), Schritte = (2, 2, 2), Polsterung = 'gleich',Aktivierung = 'neu lesen',kernel_initializer="er_normal")(conv7)
    ups8 = verketten([ups8, conv2])
    ups8 = Aussteiger(aussteigen)(ups8)
    conv8 = Faltungsblock(ups8, Filter * 2, Kernelgröße = 3, Batchnorm = Batchnorm)
    
    ups9 = Conv3DTranspose(Filter * 1, (3, 3, 3), Schritte = (2, 2, 2), Polsterung = 'gleich',Aktivierung = 'neu lesen',kernel_initializer="er_normal")(conv8)
    ups9 = verketten([ups9, conv1])
    ups9 = Aussteiger(aussteigen)(ups9)
    conv9 = Faltungsblock(ups9, Filter * 1, Kernelgröße = 3, Batchnorm = Batchnorm)
    
    Ausgänge = Conv3D(1, (1, 1, 2), Aktivierung='Sigmoid',padding='gleich')(conv9)
    Modell = Modell(Eingänge=[input_img], Ausgänge=[Ausgänge])
    Rückgabemodell

Dann trainieren wir das Modell mit Adams Optimierer und Würfelverlust als Verlustfunktion …

  Def-Training(selbst,Epochen):
    im_höhe=96
    im_width=128
    img_depth=160
    Epochen=60
    train_gen = data_gen(self.X_train,self.y_train, batch_size = 4)
    val_gen = data_gen(self.X_test,self.y_test, batch_size = 4)
    Kanäle=1
    input_img = Eingabe((im_höhe, im_width,img_depth,Kanäle), name="img")
    self.model = resunet_opt(input_img, Filter=16, Ausfall = 0,05, Batchnorm=Wahr)
    Selbstmodell.Zusammenfassung()
    self.model.compile(Optimierer=Adam(lr=1e-1),Verlust=Focal_Loss,Metriken=[iou_score,'Richtigkeit'])
    #passend zum Modell
    Rückrufe = Rückrufe = [
        ModellCheckpoint('best_model.h5', ausführlich=1, save_best_only=Wahr, save_weights_only=Falsch)]
    result=self.model.fit(train_gen,steps_per_epoch=16,epochs=epochs,validation_data=val_gen,validierungsschritte=16,initial_epoch=0,callbacks=callbacks)

Nach dem Training für 60 Epochen, Wir haben eine Bestätigung iou_score von 0.86.

1wza0wfs_sur9k-zqjprpwg-3656223

Schauen wir uns an, wie unser Modell funktionierte. Unser Modell wird einfach die Maske vorhersagen. Um das Bild ohne Schädel zu erhalten, wir müssen es über das Raw-Bild legen, um das Bild ohne Schädel zu erhalten …

1iegnuyrbtpve1jhtnzkk5g-2476386
1kxqkl-xygtvmzadvak0xqw-8587356
15t8c9ypb-dipiburmqvl_q-9163219

Wenn wir uns die Vorhersagen ansehen, können wir sagen, dass es zwar in der Lage ist, das Gehirn zu identifizieren und zu segmentieren, nicht annähernd perfekt. In diesem Punkt, Wir können uns mit einem Domänenexperten zusammensetzen, um herauszufinden, welche zusätzlichen Vorverarbeitungsschritte durchgeführt werden können, um die Genauigkeit zu verbessern. Aber was diesen Beitrag angeht, Ich schließe es hier ab. Bitte, Folgen link1 Ja / Ö link2 Wenn du mehr wissen willst …

Fazit:

Ich freue mich, dass du es bis zum Ende geschafft hast. Ich hoffe, dies hilft Ihnen beim Einstieg in die Bildsegmentierung in 3D-Bilder. Sie finden den Google Colab-Link, der den Code enthält. hier. Fühlen Sie sich frei, Vorschläge oder Fragen im Kommentarbereich hinzuzufügen. Einen schönen Tag noch!

Die in diesem Artikel zur Schädelentfernung gezeigten Medien sind nicht Eigentum von DataPeaker und werden nach Ermessen des Autors verwendet.

Abonniere unseren Newsletter

Wir senden Ihnen keine SPAM-Mail. Wir hassen es genauso wie du.