[PYTHON] Segmentation d'image avec CaDIS: un ensemble de données sur la cataracte

1.Tout d'abord

Pour les débutants, cet article vise à utiliser TensorFlow 2.0 pour la segmentation sémantique avec Deep Learning pour le moment. L'ensemble de données d'image utilise l'ensemble de données de segmentation de la chirurgie de la cataracte [^ 1] publié par Digital Surgery Ltd. De plus, le réseau sera SegNet [^ 2] avec le CNN à 10 couches utilisé précédent comme encodeur.

Tout le code

2. Environnement

  1. CaDIS: Cataract Dataset for Image Segmentation 4738 (25 vidéos) Ensemble de données sur la segmentation de la chirurgie de la cataracte publié par Digital Surgery Ltd. Vous pouvez télécharger des images chirurgicales et des images de segmentation à partir des liens ci-dessous. CaDIS Dataset https://cataracts.grand-challenge.org/CaDIS/

Les étiquettes de segmentation sont les suivantes. Dans le même temps, il montre également le pourcentage de chaque classe en pixels. Dans le tableau, vous pouvez voir que certaines classes n'existent pas dans chaque groupe. ~~ C'est ennuyeux! ~~ Le nombre d'images pour l'apprentissage, la vérification et le test est respectivement de 3584 (19 vidéos), 540 (3 vidéos) et 614 (3 vidéos).

Index Class Rapport de pixels(Apprentissage)[%] Rapport de pixels(Vérification)[%] Rapport de pixels(tester)[%]
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

De plus, un exemple d'image est présenté ci-dessous. L'image de segmentation brute est une image en échelle de gris avec l'index dans le tableau ci-dessus comme valeur de pixel.

<détails>

Contient des images grotesques </ summary>
Image chirurgicale et image de segmentation [^ 1] Image de segmentation brute

4. Répartition des données

Cet ensemble de données détermine les images (vidéos) à utiliser pour la formation, la validation et les tests. Les détails peuvent être trouvés dans un fichier appelé splits.txt dans l'ensemble de données. Par conséquent, le groupe divisé adopte le contenu de splits.txt et décrit le chemin du fichier de l'image chirurgicale et de l'image de segmentation de chaque groupe et leur correspondance dans le fichier csv avec le code suivant.

Code qui décrit le chemin du fichier image dans le fichier csv
import os
from collections import defaultdict
import pandas as pd


#Créez un fichier csv qui décrit la correspondance entre les images et les étiquettes
def make_csv(fpath, dirlist):
    #Examiner le chemin du fichier de l'image d'entraînement
    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))

    #Enregistrer en tant que fichier csv
    dataset = pd.DataFrame(dataset)
    dataset.to_csv(fpath, index=False)



#Dossier vidéo des données d'entraînement
train_dir = ['Video01', 'Video03', 'Video04', 'Video06', 'Video08', 'Video09',
             'Video10', 'Video11', 'Video13', 'Video14', 'Video15', 'Video17',
             'Video18', 'Video20', 'Video21', 'Video22', 'Video23', 'Video24',
             'Video25']

#Dossier vidéo des données de vérification
val_dir = ['Video05', 'Video07', 'Video16']

#Dossier vidéo des données de test
test_dir = ['Video02', 'Video12', 'Video19']


#Créez un fichier csv qui décrit la correspondance entre l'image des données d'entraînement et l'étiquette
make_csv('train.csv', train_dir)

#Créez un fichier csv qui décrit la correspondance entre l'image des données de vérification et l'étiquette
make_csv('val.csv', val_dir)

#Créez un fichier csv qui décrit la correspondance entre l'image des données d'entraînement et l'étiquette
make_csv('test.csv', test_dir)

Le fichier csv contenant les chemins d'accès aux fichiers pour les données d'entraînement, de vérification et de test est dans ce 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

5. Construction et apprentissage de modèles

Tout d'abord, importez la bibliothèque que vous souhaitez utiliser.

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

Ensuite, décrivez les paramètres et ainsi de suite.

directory = 'CaDIS' #Dossier dans lequel les images sont stockées
df_train = pd.read_csv('train.csv') #DataFrame avec informations sur les données d'entraînement
df_validation = pd.read_csv('val.csv') #DataFrame avec informations de données de validation
image_size = (224, 224) #Taille de l'image d'entrée
classes = 36 #Nombre de classes de classification
batch_size = 32 #Taille du lot
epochs = 300 #Nombre d'époques
loss = cce_dice_loss #Fonction de perte
optimizer = Adam(lr=0.001, amsgrad=True) #Fonction d'optimisation
metrics = dice_coeff #Méthode d'évaluation
#ImageDataGenerator Paramètres d'amplification d'image
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}

Ce qui suit est appliqué comme processus de rappel pendant l'apprentissage.

# val_Enregistrer le modèle uniquement lorsque la perte est minimisée
mc_cb = ModelCheckpoint('model_weights.h5',
                        monitor='val_loss', verbose=1,
                        save_best_only=True, mode='min')
#Lorsque l'apprentissage stagne, le taux d'apprentissage est de 0.Double
rl_cb = ReduceLROnPlateau(monitor='loss', factor=0.2, patience=3,
                          verbose=1, mode='auto',
                          min_delta=0.0001, cooldown=0, min_lr=0)
#Si l'apprentissage ne progresse pas, l'apprentissage sera interrompu de force
es_cb = EarlyStopping(monitor='loss', min_delta=0,
                      patience=5, verbose=1, mode='auto')

Génère un générateur de données de formation et de validation. Utilisez ʻImageDataGeneratorpour l'expansion des données. De plus, cette fois, nous utiliseronsSequence` pour créer des données de mini-lots.

La fonction __getitem__ est la partie qui crée spécifiquement un mini-lot. L'image d'entrée est traitée selon la procédure suivante.

  1. Chargez l'image
  2. Redimensionner à la taille d'image d'entrée spécifiée
  3. Convertir en type flottant
  4. Effectuer le traitement d'expansion des données
  5. Divisez la valeur par 255 et normalisez-la à 0-1

Le traitement de l'image de segmentation est effectué selon la procédure suivante.

  1. Chargez l'image
  2. Redimensionner à la taille d'image d'entrée spécifiée
  3. Convertir en type flottant
  4. Effectuer le traitement d'expansion des données
  5. Créez une image dans laquelle les pixels de la classe 0 sont 1 et 0 sinon, une image dans laquelle les pixels de la classe 1 sont 1 et 0 sinon ... (pour le nombre de classes) et connectez-les dans le sens du canal. Créer un tableau de tailles (verticales, horizontales, nombre de classes)
#Générateur de données
@dataclasses.dataclass
class TrainSequence(Sequence):
    directory: str #Dossier dans lequel les images sont stockées
    df: pd.DataFrame #DataFrame avec informations sur les données
    image_size: tuple #Taille de l'image d'entrée
    classes: int #Nombre de classes de classification
    batch_size: int #Taille du lot
    aug_params: dict #ImageDataGenerator Paramètres d'amplification d'image

    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))
            #Image d'entrée
            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)

            #Image de segmentation
            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

#Générer un générateur
##Générateur de données d'entraînement
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)
##Générateur de données de validation
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)

Dernière fois Construit SegNet avec la structure du CNN simple à 10 couches créé excluant toutes les connexions en tant qu'encodeur et la structure des encodeurs dans l'ordre inverse en tant que décodeur. Faire. Veuillez vous référer à ici pour l'explication de SegNet.

# SegNet(8 couches d'encodeur, 8 couches de décodeur)Construire
def cnn(input_shape, classes):
    #La taille de l'image d'entrée doit être un multiple de 32
    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.'

    #encodeur
    ##Couche d'entrée
    inputs = Input(shape=(input_shape[0], input_shape[1], 3))

    ##1ère couche
    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ème couche
    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)

    ##3e couche
    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ème couche
    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ème et 6ème couches
    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ème et 8ème couches
    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)

    #Décodeur
    ##1ère couche
    x = Conv2D(1024, (3, 3), strides=(1, 1), padding='same', kernel_initializer='he_normal')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)

    ##2ème et 3ème couches
    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ème couche
    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ème couche
    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ème couche
    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ème et 8ème couches
    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)

#Construction de réseau
model = cnn(image_size, classes)
model.summary()
model.compile(loss=loss, optimizer=optimizer, metrics=[metrics])

Le reste est identique à Dernière fois. Entraînez-vous et enregistrez la courbe d'apprentissage.

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

#Dessinez et enregistrez un graphique de la courbe d'apprentissage
def plot_history(history):
    fig, (axL, axR) = plt.subplots(ncols=2, figsize=(10, 4))

    # [la gauche]Graphique sur les métriques
    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')

    # [Côté droit]Graphique sur la perte
    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')

    #Enregistrer le graphique en tant qu'image
    fig.savefig('history.jpg')
    plt.close()

#Sauvegarder la courbe d'apprentissage
plot_history(history)

Les résultats d'apprentissage sont les suivants.

history.jpg

6. Évaluation

L'évaluation est effectuée par IoU moyen pour chaque classe et IoU moyen qui est la moyenne d'entre eux. Le calcul a été fait avec le code suivant.

Importation supplémentaire.

from collections import defaultdict

L'inférence et l'évaluation sont effectuées selon la procédure suivante.

  1. Chargez l'image
  2. Redimensionner à la taille d'image d'entrée spécifiée
  3. Convertissez en type flottant et normalisez la valeur de 0 à 1
  4. Créez un tableau de taille de lot 1
  5. Inférer et obtenir l'image de segmentation
  6. Restaurez la taille de l'image de segmentation à sa taille d'origine
  7. Calculer l'IoU pour chaque image et chaque classe
  8. Calculer l'IoU moyen pour chaque classe

    directory = 'CaDIS' #Dossier dans lequel les images sont stockées
    df_test = pd.read_csv('test.csv') #DataFrame avec informations sur les données de test
    image_size = (224, 224) #Taille de l'image d'entrée
    classes = 36 #Nombre de classes de classification


    #Construction de réseau
    model = cnn(image_size, classes)
    model.summary()
    model.load_weights('model_weights.h5')


    #inférence
    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)

        ##Calcul IoU
        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)

Voici les résultats de l'évaluation. De plus, Io U moyen était de 15,0%. D'après l'article [^ 1], VGG est de 20,61%, donc je pense que c'est le cas.

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

7. Résumé

Dans cet article, nous avons effectué une segmentation sémantique de l'ensemble de données de segmentation de la chirurgie de la cataracte [^ 1] publié par Digital Surgery Ltd. en utilisant SegNet avec 8 couches chacune pour l'encodeur et le décodeur. Selon l'article [^ 1], il semble que 52,66% seront obtenus avec PSPNet, donc à l'avenir, sur la base de ce résultat, je viserai des performances identiques ou meilleures tout en incorporant les dernières méthodes telles que la structure du réseau et la méthode d'expansion des données.

Recommended Posts

Segmentation d'image avec CaDIS: un ensemble de données sur la cataracte
Segmentation d'image avec scikit-image et scikit-learn
Classification d'images avec un jeu de données d'images de fond d'oeil grand angle
Créez une image factice avec Python + PIL.
Comparaison des performances en incorporant une structure de saut dans SegNet (CaDIS: a Cataract Dataset)
Traitement d'image avec MyHDL
Reconnaissance d'image avec keras
Format A4 avec python-pptx
Segmentation d'image à l'aide de U-net
Traitement d'image avec Python
Créer un chargeur de jeu de données
Décorer avec un décorateur
Traitement d'image avec PIL