Für Anfänger zielt dieser Artikel darauf ab, TensorFlow 2.0 vorerst für die semantische Segmentierung mit Deep Learning zu verwenden. Der Bilddatensatz verwendet den von Digital Surgery Ltd. veröffentlichten Kataraktchirurgiesegmentierungsdatensatz [^ 1]. Das Netzwerk wird auch SegNet [^ 2] sein, wobei das 10-Schicht-CNN vorher als Encoder verwendet wird.
Die Segmentierungsbezeichnungen lauten wie folgt. Gleichzeitig wird auch der Prozentsatz jeder Klasse in Pixel angezeigt. Aus der Tabelle können Sie ersehen, dass einige Klassen nicht in jeder Gruppe vorhanden sind. ~~ Es ist nervig! ~~ Die Anzahl der Bilder zum Lernen, Überprüfen und Testen beträgt 3584 (19 Videos), 540 (3 Videos) bzw. 614 (3 Videos).
Index | Class | Pixelverhältnis(Lernen)[%] | Pixelverhältnis(Überprüfung)[%] | Pixelverhältnis(Prüfung)[%] |
---|---|---|---|---|
0 | Pupil | 17.1 | 15.7 | 16.2 |
1 | Surgical Tape | 6.51 | 6.77 | 4.81 |
2 | Hand | 0.813 | 0.725 | 0.414 |
3 | Eye Retractors | 0.564 | 0.818 | 0.388 |
4 | Iris | 11.0 | 11.0 | 12.8 |
5 | Eyelid | 0 | 0 | 1.86 |
6 | Skin | 12.0 | 20.4 | 10.7 |
7 | Cornea | 49.6 | 42.2 | 50.6 |
8 | Hydro. Cannula | 0.138 | 0.0984 | 0.0852 |
9 | Visco. Cannula | 0.0942 | 0.0720 | 0.0917 |
10 | Cap. Cystotome | 0.0937 | 0.0821 | 0.0771 |
11 | Rycroft Cannula | 0.0618 | 0.0788 | 0.0585 |
12 | Bonn Forceps | 0.241 | 0.161 | 0.276 |
13 | Primary Knife | 0.123 | 0.258 | 0.249 |
14 | Phaco. Handpiece | 0.173 | 0.240 | 0.184 |
15 | Lens Injector | 0.343 | 0.546 | 0.280 |
16 | A/I Handpiece | 0.327 | 0.380 | 0.305 |
17 | Secondary Knife | 0.102 | 0.0933 | 0.148 |
18 | Micromanipulator | 0.188 | 0.229 | 0.215 |
19 | A/I Handpiece Handle | 0.0589 | 0.0271 | 0.0358 |
20 | Cap. Forceps | 0.0729 | 0.0144 | 0.0384 |
21 | Rycroft Cannula Handle | 0.0406 | 0.0361 | 0.0101 |
22 | Phaco. Handpiece Handle | 0.0566 | 0.00960 | 0.0202 |
23 | Cap. Cystotome Handle | 0.0170 | 0.0124 | 0.0287 |
24 | Secondary Knife Handle | 0.0609 | 0.0534 | 0.0124 |
25 | Lens Injector Handle | 0.0225 | 0.0599 | 0.0382 |
26 | Water Sprayer | 0.000448 | 0 | 0.00361 |
27 | Suture Needle | 0.000764 | 0 | 0 |
28 | Needle Holder | 0.0201 | 0 | 0 |
29 | Charleux Cannula | 0.00253 | 0 | 0.0164 |
30 | Vannas Scissors | 0.00107 | 0 | 0 |
31 | Primary Knife Handle | 0.000321 | 0 | 0.000385 |
32 | Viter. Handpiece | 0 | 0 | 0.0782 |
33 | Mendez Ring | 0.0960 | 0 | 0 |
34 | Biomarker | 0.00619 | 0 | 0 |
35 | Marker | 0.0661 | 0 | 0 |
Zusätzlich wird unten ein Bildbeispiel gezeigt. Das rohe Segmentierungsbild ist ein Graustufenbild mit dem Index in der obigen Tabelle als Pixelwert.
Dieser Datensatz bestimmt die Bilder (Videos), die für Schulungen, Validierungen und Tests verwendet werden sollen. Details finden Sie in einer Datei namens splits.txt im Dataset. Daher übernimmt die geteilte Gruppe den Inhalt von splits.txt und beschreibt den Dateipfad des Operationsbilds und des Segmentierungsbildes jeder Gruppe sowie deren Entsprechung in der CSV-Datei mit dem folgenden Code.
import os
from collections import defaultdict
import pandas as pd
#Erstellen Sie eine CSV-Datei, die die Entsprechung zwischen Bildern und Beschriftungen beschreibt
def make_csv(fpath, dirlist):
#Untersuchen Sie den Dateipfad des Trainingsbildes
dataset = defaultdict(list)
for dir in dirlist:
filelist = sorted(os.listdir(f'CaDIS/{dir}/Images'))
dataset['filename'] += list(map(lambda x: f'{dir}/Images/{x}', filelist))
filelist = sorted(os.listdir(f'CaDIS/{dir}/Labels'))
dataset['label'] += list(map(lambda x: f'{dir}/Labels/{x}', filelist))
#Als CSV-Datei speichern
dataset = pd.DataFrame(dataset)
dataset.to_csv(fpath, index=False)
#Videoordner mit Trainingsdaten
train_dir = ['Video01', 'Video03', 'Video04', 'Video06', 'Video08', 'Video09',
'Video10', 'Video11', 'Video13', 'Video14', 'Video15', 'Video17',
'Video18', 'Video20', 'Video21', 'Video22', 'Video23', 'Video24',
'Video25']
#Verifizierungsdaten-Videoordner
val_dir = ['Video05', 'Video07', 'Video16']
#Datenvideoordner testen
test_dir = ['Video02', 'Video12', 'Video19']
#Erstellen Sie eine CSV-Datei, die die Entsprechung zwischen dem Bild der Trainingsdaten und dem Etikett beschreibt
make_csv('train.csv', train_dir)
#Erstellen Sie eine CSV-Datei, die die Entsprechung zwischen dem Bild der Verifizierungsdaten und dem Etikett beschreibt
make_csv('val.csv', val_dir)
#Erstellen Sie eine CSV-Datei, die die Entsprechung zwischen dem Bild der Trainingsdaten und dem Etikett beschreibt
make_csv('test.csv', test_dir)
Die CSV-Datei mit den Dateipfaden für Trainings-, Überprüfungs- und Testdaten hat dieses Format.
filename | label |
---|---|
Video01/Images/Video1_frame000090.png | Video01/Labels/Video1_frame000090.png |
Video01/Images/Video1_frame000100.png | Video01/Labels/Video1_frame000100.png |
Video01/Images/Video1_frame000110.png | Video01/Labels/Video1_frame000110.png |
Importieren Sie zunächst die Bibliothek, die Sie verwenden möchten.
import dataclasses
import math
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow.keras.callbacks import ModelCheckpoint, ReduceLROnPlateau
from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, MaxPool2D, UpSampling2D
from tensorflow.keras.layers import Conv2D, BatchNormalization, Activation
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.losses import categorical_crossentropy
from tensorflow.keras.utils import Sequence
import cv2
Beschreiben Sie als Nächstes die Parameter usw.
directory = 'CaDIS' #Ordner, in dem Bilder gespeichert werden
df_train = pd.read_csv('train.csv') #DataFrame mit Trainingsdateninformationen
df_validation = pd.read_csv('val.csv') #DataFrame mit Informationen zu Validierungsdaten
image_size = (224, 224) #Bildgröße eingeben
classes = 36 #Anzahl der Klassifizierungsklassen
batch_size = 32 #Chargengröße
epochs = 300 #Anzahl der Epochen
loss = cce_dice_loss #Verlustfunktion
optimizer = Adam(lr=0.001, amsgrad=True) #Optimierungsfunktion
metrics = dice_coeff #Bewertungsmethoden
#ImageDataGenerator Bildverstärkungsparameter
aug_params = {'rotation_range': 5,
'width_shift_range': 0.05,
'height_shift_range': 0.05,
'shear_range': 0.1,
'zoom_range': 0.05,
'horizontal_flip': True,
'vertical_flip': True}
Das Folgende wird als Rückrufprozess während des Lernens angewendet.
# val_Modell nur speichern, wenn der Verlust minimiert ist
mc_cb = ModelCheckpoint('model_weights.h5',
monitor='val_loss', verbose=1,
save_best_only=True, mode='min')
#Wenn das Lernen stagniert, beträgt die Lernrate 0.Doppelt
rl_cb = ReduceLROnPlateau(monitor='loss', factor=0.2, patience=3,
verbose=1, mode='auto',
min_delta=0.0001, cooldown=0, min_lr=0)
#Wenn das Lernen nicht voranschreitet, wird das Lernen gewaltsam abgebrochen
es_cb = EarlyStopping(monitor='loss', min_delta=0,
patience=5, verbose=1, mode='auto')
Erzeugt einen Generator für Trainings- und Validierungsdaten. Verwenden Sie "ImageDataGenerator" zur Datenerweiterung. Dieses Mal werden wir auch "Sequenz" verwenden, um Mini-Batch-Daten zu erstellen.
Die Funktion __getitem__
ist der Teil, der speziell einen Mini-Batch erstellt. Das Eingabebild wird wie folgt verarbeitet.
Die Verarbeitung des Segmentierungsbildes wird gemäß dem folgenden Verfahren durchgeführt.
#Datengenerator
@dataclasses.dataclass
class TrainSequence(Sequence):
directory: str #Ordner, in dem Bilder gespeichert werden
df: pd.DataFrame #DataFrame mit Dateninformationen
image_size: tuple #Bildgröße eingeben
classes: int #Anzahl der Klassifizierungsklassen
batch_size: int #Chargengröße
aug_params: dict #ImageDataGenerator Bildverstärkungsparameter
def __post_init__(self):
self.df_index = list(self.df.index)
self.train_datagen = ImageDataGenerator(**self.aug_params)
def __len__(self):
return math.ceil(len(self.df_index) / self.batch_size)
def __getitem__(self, idx):
batch_x = self.df_index[idx * self.batch_size:(idx+1) * self.batch_size]
x = []
y = []
for i in batch_x:
rand = np.random.randint(0, int(1e9))
#Bild eingeben
img = cv2.imread(f'{self.directory}/{self.df.at[i, "filename"]}')
img = cv2.resize(img, self.image_size, interpolation=cv2.INTER_LANCZOS4)
img = np.array(img, dtype=np.float32)
img = self.train_datagen.random_transform(img, seed=rand)
img *= 1./255
x.append(img)
#Segmentierungsbild
img = cv2.imread(f'{self.directory}/{self.df.at[i, "label"]}', cv2.IMREAD_GRAYSCALE)
img = cv2.resize(img, self.image_size, interpolation=cv2.INTER_LANCZOS4)
img = np.array(img, dtype=np.float32)
img = np.reshape(img, (self.image_size[0], self.image_size[1], 1))
img = self.train_datagen.random_transform(img, seed=rand)
img = np.reshape(img, (self.image_size[0], self.image_size[1]))
seg = []
for label in range(self.classes):
seg.append(img == label)
seg = np.array(seg, np.float32)
seg = seg.transpose(1, 2, 0)
y.append(seg)
x = np.array(x)
y = np.array(y)
return x, y
#Generator generieren
##Trainingsdatengenerator
train_generator = TrainSequence(directory=directory, df=df_train,
image_size=image_size, classes=classes,
batch_size=batch_size, aug_params=aug_params)
step_size_train = len(train_generator)
##Validierungsdatengenerator
validation_generator = TrainSequence(directory=directory, df=df_validation,
image_size=image_size, classes=classes,
batch_size=batch_size, aug_params={})
step_size_validation = len(validation_generator)
Letztes Mal Konstruiertes SegNet mit der Struktur des erstellten einfachen 10-Schicht-CNN ohne alle Verbindungen als Encoder und der Struktur des Encoders in umgekehrter Reihenfolge als Decoder Machen. Weitere Informationen zu SegNet finden Sie unter hier.
# SegNet(8 Schichten Encoder, 8 Schichten Decoder)Bauen
def cnn(input_shape, classes):
#Die Eingabebildgröße muss ein Vielfaches von 32 sein
assert input_shape[0]%32 == 0, 'Input size must be a multiple of 32.'
assert input_shape[1]%32 == 0, 'Input size must be a multiple of 32.'
#Encoder
##Eingabeebene
inputs = Input(shape=(input_shape[0], input_shape[1], 3))
##1. Schicht
x = Conv2D(32, (3, 3), padding='same', kernel_initializer='he_normal')(inputs)
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = MaxPool2D(pool_size=(2, 2))(x)
##2. Schicht
x = Conv2D(64, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = MaxPool2D(pool_size=(2, 2))(x)
##3. Schicht
x = Conv2D(128, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = MaxPool2D(pool_size=(2, 2))(x)
##4. Schicht
x = Conv2D(256, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = MaxPool2D(pool_size=(2, 2))(x)
##5. und 6. Schicht
x = Conv2D(512, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = Conv2D(512, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = MaxPool2D(pool_size=(2, 2))(x)
##7. und 8. Schicht
x = Conv2D(1024, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = Conv2D(1024, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
x = BatchNormalization()(x)
x = Activation('relu')(x)
#Decoder
##1. Schicht
x = Conv2D(1024, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
x = BatchNormalization()(x)
x = Activation('relu')(x)
##2. und 3. Schicht
x = UpSampling2D(size=(2, 2))(x)
x = Conv2D(512, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = Conv2D(512, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
x = BatchNormalization()(x)
x = Activation('relu')(x)
##4. Schicht
x = UpSampling2D(size=(2, 2))(x)
x = Conv2D(256, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
x = BatchNormalization()(x)
x = Activation('relu')(x)
##5. Schicht
x = UpSampling2D(size=(2, 2))(x)
x = Conv2D(128, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
x = BatchNormalization()(x)
x = Activation('relu')(x)
##6. Schicht
x = UpSampling2D(size=(2, 2))(x)
x = Conv2D(64, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
x = BatchNormalization()(x)
x = Activation('relu')(x)
##7. und 8. Schicht
x = UpSampling2D(size=(2, 2))(x)
x = Conv2D(64, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
x = Conv2D(classes, (1, 1), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
outputs = Activation('softmax')(x)
return Model(inputs=inputs, outputs=outputs)
#Netzwerkaufbau
model = cnn(image_size, classes)
model.summary()
model.compile(loss=loss, optimizer=optimizer, metrics=[metrics])
Der Rest ist der gleiche wie Letztes Mal. Trainiere und speichere die Lernkurve.
#Lernen
history = model.fit_generator(
train_generator, steps_per_epoch=step_size_train,
epochs=epochs, verbose=1, callbacks=[mc_cb, rl_cb, es_cb],
validation_data=validation_generator,
validation_steps=step_size_validation,
workers=3)
#Zeichnen und speichern Sie ein Diagramm der Lernkurve
def plot_history(history):
fig, (axL, axR) = plt.subplots(ncols=2, figsize=(10, 4))
# [links]Grafik über Metriken
L_title = 'Dice_coeff_vs_Epoch'
axL.plot(history.history['dice_coeff'])
axL.plot(history.history['val_dice_coeff'])
axL.grid(True)
axL.set_title(L_title)
axL.set_ylabel('dice_coeff')
axL.set_xlabel('epoch')
axL.legend(['train', 'test'], loc='upper left')
# [Rechte Seite]Grafik über Verlust
R_title = "Loss_vs_Epoch"
axR.plot(history.history['loss'])
axR.plot(history.history['val_loss'])
axR.grid(True)
axR.set_title(R_title)
axR.set_ylabel('loss')
axR.set_xlabel('epoch')
axR.legend(['train', 'test'], loc='upper left')
#Grafik als Bild speichern
fig.savefig('history.jpg')
plt.close()
#Lernkurve speichern
plot_history(history)
Die Lernergebnisse sind wie folgt.
Die Bewertung erfolgt anhand der durchschnittlichen IoU für jede Klasse und der mittleren IoU, die der Durchschnitt von ihnen ist. Die Berechnung wurde mit dem folgenden Code durchgeführt.
Zusätzlicher Import.
from collections import defaultdict
Inferenz und Bewertung werden gemäß dem folgenden Verfahren durchgeführt.
directory = 'CaDIS' #Ordner, in dem Bilder gespeichert werden
df_test = pd.read_csv('test.csv') #DataFrame mit Testdateninformationen
image_size = (224, 224) #Bildgröße eingeben
classes = 36 #Anzahl der Klassifizierungsklassen
#Netzwerkaufbau
model = cnn(image_size, classes)
model.summary()
model.load_weights('model_weights.h5')
#Inferenz
dict_iou = defaultdict(list)
for i in tqdm(range(len(df_test)), desc='predict'):
img = cv2.imread(f'{directory}/{df_test.at[i, "filename"]}')
height, width = img.shape[:2]
img = cv2.resize(img, image_size, interpolation=cv2.INTER_LANCZOS4)
img = np.array(img, dtype=np.float32)
img *= 1./255
img = np.expand_dims(img, axis=0)
label = cv2.imread(f'{directory}/{df_test.at[i, "label"]}', cv2.IMREAD_GRAYSCALE)
pred = model.predict(img)[0]
pred = cv2.resize(pred, (width, height), interpolation=cv2.INTER_LANCZOS4)
##IoU-Berechnung
pred = np.argmax(pred, axis=2)
for j in range(classes):
y_pred = np.array(pred == j, dtype=np.int)
y_true = np.array(label == j, dtype=np.int)
tp = sum(sum(np.logical_and(y_pred, y_true)))
other = sum(sum(np.logical_or(y_pred, y_true)))
if other != 0:
dict_iou[j].append(tp/other)
# average IoU
for i in range(classes):
if i in dict_iou:
dict_iou[i] = sum(dict_iou[i]) / len(dict_iou[i])
else:
dict_iou[i] = -1
print('average IoU', dict_iou)
Nachfolgend sind die Bewertungsergebnisse aufgeführt. Zusätzlich betrug das mittlere Io U 15,0%. Laut dem Papier [^ 1] liegt die VGG bei 20,61%, daher denke ich, dass dies der Fall ist.
Index | Class | average IoU[%] |
---|---|---|
0 | Pupil | 85.3 |
1 | Surgical Tape | 53.3 |
2 | Hand | 6.57 |
3 | Eye Retractors | 21.9 |
4 | Iris | 74.4 |
5 | Eyelid | 0.0 |
6 | Skin | 49.7 |
7 | Cornea | 88.0 |
8 | Hydro. Cannula | 0 |
9 | Visco. Cannula | 0 |
10 | Cap. Cystotome | 0 |
11 | Rycroft Cannula | 0 |
12 | Bonn Forceps | 3.58 |
13 | Primary Knife | 5.35 |
14 | Phaco. Handpiece | 0.0781 |
15 | Lens Injector | 16.4 |
16 | A/I Handpiece | 16.4 |
17 | Secondary Knife | 6.08 |
18 | Micromanipulator | 0 |
19 | A/I Handpiece Handle | 6.49 |
20 | Cap. Forceps | 0 |
21 | Rycroft Cannula Handle | 0 |
22 | Phaco. Handpiece Handle | 0 |
23 | Cap. Cystotome Handle | 0 |
24 | Secondary Knife Handle | 2.49 |
25 | Lens Injector Handle | 0 |
26 | Water Sprayer | ─ |
27 | Suture Needle | 0 |
28 | Needle Holder | ─ |
29 | Charleux Cannula | 0 |
30 | Vannas Scissors | ─ |
31 | Primary Knife Handle | 0 |
32 | Viter. Handpiece | 0 |
33 | Mendez Ring | ─ |
34 | Biomarker | ─ |
35 | Marker | ─ |
In diesem Artikel haben wir eine semantische Segmentierung des von Digital Surgery Ltd. veröffentlichten Datensatzes zur Segmentierung von Kataraktoperationen [^ 1] unter Verwendung von SegNet mit jeweils 8 Schichten für Codierer und Decodierer durchgeführt. Dem Artikel [^ 1] zufolge werden mit PSPNet 52,66% erzielt. In Zukunft werde ich auf der Grundlage dieses Ergebnisses die gleiche oder eine bessere Leistung anstreben und dabei die neuesten Methoden wie Netzwerkstruktur und Datenerweiterungsmethode einbeziehen.
Recommended Posts