Klassifiziere Verkehrsschilder

In diesem Jupyter Notebook geht es um die Erkennung von Verkehrszeichen. Der dafür verwendete Datensatz ist der German Traffic Sign Recognition Benchmark, kurz GTSRB. Sämtliche Informationen rund um den Datensatz sind online einsehbar.

[ ]:
import numpy as np
import os.path
import imageio.v2 as imageio
import skimage.transform
import skimage.feature
import skimage.color
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import ConfusionMatrixDisplay

import gtsrb_db_loader  # hierbei handelt es sich um ein selbstgeschriebenes Modul in gleichen Ordner

Mit diesem Code-Schnippsel wird die ZIP-Datei automatisch heruntergeladen.

[ ]:
import functools
import shutil
import requests
from tqdm.auto import tqdm

file_name = "GTSRB_Final_Training_Images.zip"
if file_name not in os.listdir("."):
    url = "https://sid.erda.dk/public/archives/daaeac0d7ce1152aea9b61d9f1e19370/GTSRB_Final_Training_Images.zip"
    r = requests.get(url, stream=True, allow_redirects=True)
    if r.status_code != 200:
        r.raise_for_status()
        raise RuntimeError(f"Request to {url} returned status code {r.status_code}")
    file_size = int(r.headers.get('Content-Length', 0))
    desc = "(Unknown total file size)" if file_size == 0 else ""
    r.raw.read = functools.partial(r.raw.read, decode_content=True)
    with tqdm.wrapattr(r.raw, "read", total=file_size, desc=desc) as r_raw:
        with open(file_name, "wb") as f:
            shutil.copyfileobj(r_raw, f)
else:
    print(f"Datei '{file_name}' ist bereits heruntergeladen")

print("Inhalt des Ordners:")
for file_entry in os.listdir("."):
    print("- ", file_entry)

Nun wird die ZIP-Datei in den Unterordner GTSRB_Final_Training_Images entpackt:

[ ]:
import zipfile

folder_name = "GTSRB_Final_Training_Images"

if folder_name not in os.listdir("."):
    with zipfile.ZipFile(file_name, 'r') as zip_ref:
        zip_ref.extractall("GTSRB_Final_Training_Images")
else:
    print("Ziel-Ordner fürs Entpacken existiert bereits")

Sie können statt der vorherigen zwei Code-Zellen auch einfach das ZIP-Archiv von hier herunterladen und im gleichen Ordner wie das Jupyter Notebook entpacken.

Legen Sie das Verzeichnis GTSRB in das gleiche Verzeichnis wie das Notebook. Falls das Kopieren zu viel Zeit benötigt, passen Sie path_to_directory so an, dass der Pfad auf den entpackten Ordner mit den Verkehrsschildern zeigt.

[ ]:
path_to_directory = "./GTSRB_Final_Training_Images"

df = gtsrb_db_loader.load_traffic_sign_database(path_to_directory)

df.head()
[ ]:
images = []
for row in gtsrb_db_loader.log_progress(df.itertuples(), size=len(df)):
    color_image = imageio.imread(row.path_to_image).astype(int, copy=False)
    gray_image = skimage.color.rgb2gray(color_image)
    cropped_image = gray_image[row.Roi_Y1:row.Roi_Y2, row.Roi_X1:row.Roi_X2]
    resized_image = skimage.transform.resize(cropped_image, [40, 40], mode="constant")
    images.append(resized_image)
df = df.assign(image=images)

Ein Verkehrsschild sieht nun so aus:

[ ]:
sample_image = df.iloc[0].image
plt.imshow(sample_image, cmap='gray')
plt.show()

Mithilfe von Histograms of Gradients (HOGs) können Gegenstände gut automatisiert klassifiziert werden. Diese werden z. B. im Artikel „Histograms of Oriented Gradients for Human Detection“ von N. Dalal und B. Triggs gut beschrieben. Die HOGs des oben gezeigte Beispielbild werden im Folgenden angezeigt.

Eine einfache Möglichkeit, Bilder in HOGs umzuwandeln, wird von scikit-image bereitgestellt und im Folgenden verwendet.

Zunächst wird für das obige Verkehrszeichen gezeigt, wie die HOG-Repräsentation aussieht.

[ ]:
hog_img = skimage.feature.hog(
    sample_image,
    transform_sqrt=True,
    orientations=8,
    pixels_per_cell=(6, 6),
    cells_per_block=(3, 3),
    feature_vector=True,
    block_norm="L2-Hys",
    visualize=True
)[1]
plt.imshow(hog_img)
plt.show()
[ ]:
hog_features = []
for row in gtsrb_db_loader.log_progress(df.itertuples(), size=len(df)):
    hog_feature = skimage.feature.hog(
        row.image,
        transform_sqrt=True,
        orientations=8,   # es gibt 8 verschiedene Kanten-Gruppen
        pixels_per_cell=(6, 6),
        cells_per_block=(3, 3),
        feature_vector=True,
        block_norm="L2-Hys",
        visualize=False
    )
    hog_features.append(hog_feature)
df = df.assign(hog_feature=hog_features)

Nun sind die Daten soweit vorbereitet, dass der Zusammenhang zwischen dem Bild und dem Verkehrszeichen durch eine geeignete Methode des ML hergestellt werden kann.

[ ]:
features = df["hog_feature"].values
targets = df["ClassId"].values

X_train, X_test, y_train, y_test = train_test_split(features, targets, test_size=0.33)

Noch eine Kleinigkeit: Die HOGs liegen als Arrays von Arrays vor, der Lernalgorithmus benötigt aber ein einzelnes langes Array. Dafür müssen die Arrays aneinander gehängt werden. Dies kann einfach durch`np.stack <https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.stack.html>`__ vorgenommen werden.

[ ]:
X_train = np.stack(X_train)
X_test = np.stack(X_test)

Als Klassifizierungs-Algorithmus wird ein Random Forest (RF) eingesetzt. Der Random Forest ist ein Ensemble von mehreren Entscheidungsbäumen. Im Folgenden wird eine Implementierung von scikit-learn eingesetzt.

Für die Bewertung der Ergebnisse können nun verschiedene Aspekte betrachtet werden. Die Accuracy ist ein einfaches Maß, um den Anteil der richtig bestimmten Klassen zu quantifizieren. Die Confusion Matrix ermöglicht es, zu sehen, welche Klassen miteinander verwechselt werden.

[ ]:
random_forest = RandomForestClassifier(
    n_estimators=10,
    max_depth=7,
    n_jobs=-1,
    max_features="sqrt",
    class_weight=None
)
random_forest.fit(X_train, y_train)
score = random_forest.score(X_test, y_test)
print(f"Genauigkeit (Accurarcy): {score:.2%}")

Eine schöne Möglichkeit, eine Confusion Matrix zu visualisieren, ist, diese als Bild zu plotten. Der folgende Code ist der Dokumentation von scikit-learn entnommen und angepasst worden. Die Verkehrsschilder (als Liste in der Variablen classes) werden durch die Zahlen 0 bis 42 repräsentiert. Dies entspricht den Klassennamen aus der Vorlesung.

[ ]:
fig, ax = plt.subplots(figsize=(20, 20))
matrix = ConfusionMatrixDisplay.from_estimator(random_forest, X_test, y_test, include_values=True, cmap="BuPu", ax=ax)
ax.images[-1].colorbar.remove()
plt.colorbar(matrix.im_, shrink=0.6)
plt.show()

Hier kann man nun sehen, welche Verkehrszeichen besonders häufig miteinander verwechselt werden. Der Schlüssel, welche Klassen-ID welchem Schild entspricht, ist in den Vorlesungsunterlagen enthalten. Der Einfachkeit halber ist hier aber auch ein Suchtool hinterlegt. Klicken Sie auf den Button Suche nach Schild, um nach dem Schild mit der ausgewählten class_id zu suchen.

[ ]:
import ipywidgets as widgets
from IPython.display import Markdown


def plot_sample_for_class_id(class_id):
    display(Markdown(f"### Beispielbilder für ID {class_id}"))
    ncols = 3
    nrows = 7
    _, axs = plt.subplots(ncols, nrows, figsize=(12, 5))
    for j in range(ncols):
        ax = axs[j]
        for i in range(nrows):
            sample_image_for_class_id = df[df.ClassId == class_id].sample(n=1).iloc[0]
            ax[i].imshow(sample_image_for_class_id.image, cmap='gray')
    plt.tight_layout()
    plt.show()
    display(Markdown(f"### Erster Eintrag für ID {class_id}"))
    display(df[df.ClassId == class_id].iloc[0])


widgets.interactive(
    plot_sample_for_class_id,
    {
        'manual': True,
        'manual_name': "Suche nach Schild"
    },
    class_id=(0, 42)
)

Dieser Datensatz ist bereits häufiger in wissenschaftlichen Veröffentlichungen referenziert worden. Besonders zu empfehlen ist der Artikel „Man vs. computer: Benchmarking machine learning algorithms for traffic sign recognition“ von J. Stallkamp, M. Schlipsing, J. Salmena und C. Igel.

Creative Commons Lizenzvertrag     Dieses Werk von Marvin Kastner ist lizenziert unter einer Creative Commons Namensnennung 4.0 International Lizenz.