[PYTHON] Bildsegmentierung mit CaDIS: ein Katarakt-Datensatz

1. Zuallererst

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.

Alle Codes

2. Umwelt

  1. CaDIS: Cataract Dataset for Image Segmentation 4738 (25 Videos) Datensatz zur Segmentierung der Kataraktchirurgie, veröffentlicht von Digital Surgery Ltd. Sie können chirurgische Bilder und Segmentierungsbilder über die folgenden Links herunterladen. CaDIS Dataset https://cataracts.grand-challenge.org/CaDIS/

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.

Enthält groteske Bilder
Chirurgisches Bild und Segmentierungsbild [^ 1] Rohes Segmentierungsbild

4. Datenaufteilung

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.

Code, der den Bilddateipfad in der CSV-Datei
beschreibt
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

5. Modellbau & Lernen

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.

  1. Laden Sie das Bild
  2. Ändern Sie die Größe auf die angegebene Eingabebildgröße
  3. In Float-Typ konvertieren
  4. Führen Sie die Datenerweiterungsverarbeitung durch
  5. Teilen Sie den Wert durch 255 und normalisieren Sie ihn auf 0-1

Die Verarbeitung des Segmentierungsbildes wird gemäß dem folgenden Verfahren durchgeführt.

  1. Laden Sie das Bild
  2. Ändern Sie die Größe auf die angegebene Eingabebildgröße
  3. In Float-Typ konvertieren
  4. Führen Sie die Datenerweiterungsverarbeitung durch
  5. Erstellen Sie ein Bild, in dem die Pixel der Klasse 0 ansonsten 1 und 0 sind, ein Bild, in dem die Pixel der Klasse 1 andernfalls 1 und 0 sind ... (für die Anzahl der Klassen), und verbinden Sie sie in Kanalrichtung. Erstellen Sie ein Array von Größen (vertikal, horizontal, Anzahl der Klassen)
#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.

history.jpg

6. Bewertung

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.

  1. Laden Sie das Bild
  2. Ändern Sie die Größe auf die angegebene Eingabebildgröße
  3. Konvertieren Sie in den Float-Typ und normalisieren Sie den Wert auf 0 zu 1
  4. Erstellen Sie ein Array mit der Stapelgröße 1
  5. Schliessen Sie und erhalten Sie das Segmentierungsbild
  6. Stellen Sie die ursprüngliche Größe des Segmentierungsbilds wieder her
  7. Berechnen Sie die IoU für jedes Bild und jede Klasse
  8. Berechnen Sie die durchschnittliche IoU für jede Klasse

    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

7. Zusammenfassung

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

Bildsegmentierung mit CaDIS: ein Katarakt-Datensatz
Bildsegmentierung mit Scikit-Image und Scikit-Learn
Bildklassifizierung mit Weitwinkel-Fundusbilddatensatz
Erstellen Sie mit Python + PIL ein Dummy-Image.
Leistungsvergleich durch Einbindung einer Sprungstruktur in SegNet (CaDIS: ein Katarakt-Datensatz)
Bildverarbeitung mit MyHDL
Bilderkennung mit Keras
A4 Größe mit Python-Pptx
Bildsegmentierung mit U-Net
Bildverarbeitung mit Python
Erstellen eines Dataset Loader
Mit Dekorateur dekorieren
Bildverarbeitung mit PIL