Den kenn ich doch (Linux-Magazin, Juni 2018)

Facebook-Nutzer nehmen es schon als gegeben hin, dass der soziale Netzwerkgorilla auf hochgeladenen Bildern Personen aus dem Freundeskreis an ihren Gesichtern erkennt, aber wie funktioniert diese wundersame Technik eigentlich? Einige freie Libraries, die jeder Linux-Nutzer von Github herunterladen kann, extrahieren ebenfalls Gesichter aus Fotos und vergleichen sie mit vorher erkannten, erlauben es also dem Heimnutzer, auch ganz ohne Cloud, Datenschnüffelei und Werbung zum Beispiel auf der privaten Urlaubsfotosammlung abgebildete Personen zu erkennen und die Bilder entsprechend zu markieren.

Dabei läuft bei maschineller Gesichtserkennung einiges hinter den Kulissen ab, viel mehr, als der uneingeweihte Beobachter vielleicht vermuten würde ([3]). Zunächst muss ein Algorithmus aus den Millionen von Pixeln eines Fotos ein gesichtsähnliches Objekt herausfieseln. Zwei runde dunklere Bereiche als Augen, ein hervorstehender Zinken in der Mitte als Nase, darunter eine waagrechte Linie als Mund, darunter ein Kinn -- das könnte ein Gesicht sein. Ein gutes Face-Recognition-Programm erkennt aber nicht nur formatfüllende Gesichter auf Portraitfotos, sondern auch die der Teilnehmer auf Fotos einer Tafelrunde, deren Gesicher nur ein paar Hundert Pixel messen, weil sie weiter weg sind, oder verzerrt, weil Personen seitlich in die Kamera schauen.

Zielsicher nach Training

Hier gilt es, aus dem großen Rauschen eines Bildes alles, was ein Gesicht sein könnte, zu extrahieren, und dazu eignen sich neuronale Netzwerke ganz hervorragend. Sie springen nicht auf fixe Pixelwerte an, denn das wäre auch keine gute Lösung, denn selbst zwei Fotos von ein und derselben Person unterscheiden sich auf Pixelebene gewaltig. Vielmehr wird das Netzwerk während der Lernphase mit Millionen unterschiedlicher Gesichter trainiert und erkennt anschließend alles zielsicher, was auch nur so ähnlich aussieht.

Abbildung 1: Der Autor mit Kappe in der Wüste Arizonas

Abbildung 2: Das Skript face-box.py hat das Gesicht erkannt und eingerahmt.

Abbildung 3: Auch wenn ein Gesicht weit weg ist, findet es der Algorithmus.

Installationsorgie abgewendet

Das Github-Projekt face_recognition von Adam Geitgey [2] nutzt die weit verbreitete Library dlib, um Gesichter auf Fotos und Videos zu erkennen. Mittels

   $ git clone https://github.com/ageitgey/face_recognition.git

legt git im lokalen Verzeichnis einen Klon des Github-Repositories an und das dort im Top-Verzeichnis liegende Dockerfile nimmt dem User die Installationsorgie dutzender abhängiger Projekte ab. Der Aufruf

    $ docker build -t face .

im Projektverzeichnis lädt alles Erforderliche vom Netz, kompiliert dlib und lädt auch noch ein 100MB schweres neuronales Modell, das bereits auf Gesichter trainiert wurde, in den Docker-Container. Eine längere Kaffeepause sollte man einkalkulieren.

Um das Skript in Listing 1 zur Gesichtserkennung im Container aufzurufen, kopiert der User es in das examples-Verzeichnis des Github-Klons und ruft

    $ docker run -v `pwd`:/build -it face bash -c "cd /build; python3 face-box.py pic.jpg"

auf. Dies bindet das aktuelle Verzeichnis (in dem das Python-Skript liegt) an das Directory /build innerhalb des Containers, und das Bash-Kommando wechselt dorthin und ruft face-box.py im Container auf, der das ganze Pipapo der Gesichtserkennung installiert hat. Als Parameter an face-box.py übergibt der Bash-Befehl das Bild pic.jpg, das wie in Abbildung 1 zu sehen, mich mit Baseballkappe in der höllisch heißen Wüste von Arizona zeigt. Aus dem Skript heraus kommt die Datei pic-box.jpg, in der wie in Abbildung 2 gezeigt der Algorithmus mein Gesicht unter der Baseball-Kappe gefunden und eingerahmt hat. Da das aktuelle Verzeichnis an den Container gebunden ist, liegt die Box-Datei nach Abschluss des Docker-Kommandos dort auch außerhalb des Containers.

Listing 1: face-box.py

    01 #!/usr/bin/python3
    02 import face_recognition as fr
    03 import sys
    04 from PIL import Image, ImageDraw
    05 
    06 try:
    07   _, img_name = sys.argv
    08 except:
    09   raise SystemExit(
    10      "usage: " + sys.argv[0] + " image")
    11 
    12 img   = fr.load_image_file(img_name)
    13 faces = fr.face_locations(img)
    14 
    15 pil = Image.fromarray(img)
    16 pil = pil.convert("RGBA")
    17 
    18 tmp  = Image.new(
    19         'RGBA', pil.size, (0,0,0,0))
    20 draw = ImageDraw.Draw(tmp)
    21 
    22 for (y0, x1, y1, x0) in faces:
    23   draw.rectangle(((x0, y0), (x1, y1)),
    24           fill=(30, 0, 0, 200))
    25 del draw
    26 pil = Image.alpha_composite(pil, tmp)
    27 pil = pil.convert("RGB")
    28 
    29 img_name = img_name.replace(".", "-box.")
    30 pil.save(img_name)

Dabei kommt Listing 1 mit nur 30 Zeilen recht schlank daher, nicht umsonst heißt der Slogan des Projekts "The world's simplest facial recognition". Die eigentliche Gesichtserkennung läuft mit face_locations() (Zeile 13) auf einem mit load_image_file() geladenen JPEG-Bild ab. Zurück kommt eine Liste mit Rechteckskoordinaten von erkannten Gesichtern, denn der Algorithmus fieselt auf Bildern mit mehreren Personen nicht nur ein Gesicht, sondern alle auf einen Rutsch heraus. Die for-Schleife in Zeile 22 iteriert über alle Viererblocks dieser Koordinaten und malt mittels der Klasse ImageDraw aus dem Fundus der PIL-Library ein halbtransparentes graues Rechteck an den Gesichtskoordinaten.

Abbildung 4: Die Eckdaten des Gesichts des Autors in Abbildung 1.

Nicht blickdicht

Für die Transparenz braucht das Bild erstmal eine temporäre Leinwand in tmp mit einem Alpha-Channel (Bildmodus RGBA), der den Durchsichtigkeitswert angibt (200 von 255 in Zeile 24). Der Fill-Parameter für die zu verwendende Farbe steht auf leichtem Rot (30). Allerdings kann die PIL-Library aus einem Bild mit Alpha-Channel kein JPEG mehr machen, sodass Zeile 27 daraus ein RGB-Bild ohne Alpha-Channel macht, bevor pil.save() das JPEG unter einem Namen mit -box-Suffix ablegt. Das Verfahren funktioniert relativ zuverlässig, von einigen krassen Ausreißern abgesehen, wie das in Abbildung 5 gezeigte Bild einer Tropfsteinhöhle zeigt, in deren Stalaktiten das neurale Netzwerk meint, meine Gesichtszüge zu erkennen.

Nicht perfekt, passt schon

Gesichter im Bild zu sehen, ist freilich ein alter Hut, doch face_recognition kann noch mehr, nämlich erkennen zu welcher Person das Gesicht gehört. Um zwei in verschiedenen Bildern lokalisierte Gesichter miteinander zu vergleichen, kann der Algorithmus ebenfalls nicht einfach rohe Bilder pixelmäßig abgleichen. Vielmehr muss er die Gesichtsausschnitte normieren, entzerren, und dann eine Reihe von Merkmalen extrahieren. Ein Mensch würde vielleicht auf die Größe der Nase oder die Augenfarbe achten, wie hoch die Stirn ist oder die Dicke der Augenbrauen. Der Algorithmus zur Gesichtserkennung lernt hingegen anhand von Millionen von Testbildern in der Lernphase anhand von passenden und nicht passenden Bildern, welche Merkmale die meisten Treffer und die wenigsten falschen Positive erzeugen. Hinterher stehen dort aber nur nichtssagende Zahlenkolonnen, und, wie allgemein beim Machine Learning üblich, weiß hinterher kein Mensch, aufgrund welcher Argumente der Algorithmus nun seine Entscheidung trifft.

Abbildung 4 zeigt die Eckdaten des aus Abbildung 1 extrahierten Referenzgesichts, die die Funktion face_encodings() geliefert hat. Der Algorithmus zum Gesichtsvergleich nimmt nun von jedem erkannten Gesicht diese Eckdaten und vergleicht sie. Stimmen zwei Datensätze ungefähr überein, handelt es sich höchstwahrscheinlich um diesselbe Person.

Abbildung 5: Hier meint die künstliche Intelligenz, den Autor zu erkennen ...

Abbildung 6: ... irrt sich aber gewaltig.

Mit diesem Rüstzeug kann nun ein Skript ein Gesicht aus einem Referenzbild extrahieren und das Ergebnis mit anderen Gesichtern auf anderen Bildern vergleichen. Als praktische Anwendung habe ich das Skript in Listing 3 auserkoren, das meine Fotosammlung (immerhin 36.525 Aufnahmen seit 2000) nach Aufnahmen durchsucht, auf denen ich selbst abgebildet bin. Die Dateihierarchie ist nach dem Schuhschachtelprinzip angelegt, die Fotos wurden nach Jahr, Monat und Tag sortiert. Und da ich lieber Land und Leute als mich selbst fotografiere, bin ich nur auf einem kleinen Bruchteil der Fotos zu sehen, die Sammlung von Hand zu durchforsten wäre also extrem arbeitsintensiv. Aber ich könnte natürlich einem KI-System das Foto aus Abbildung 1 zeigen und durch die Bildsammlung rattern lassen, um zu sehen, ob das Gesicht in Abbildung 1 auch auf anderen Fotos erkannt wird.

Listing 2: photos.py

    01 #!/usr/bin/python3
    02 import os
    03 import re
    04 
    05 def photos(dir):
    06   for root, dirs, files in os.walk(dir):
    07     if re.search(r'\.cache', root):
    08         continue
    09     for file in files:
    10       if re.search(r'jpg$', file, 
    11                    re.IGNORECASE):
    12         yield(os.path.join(root, file))
    13 
    14   # testing
    15 if __name__ == "__main__":
    16     for photo in photos("/photos"):
    17         print(photo)

Ganze Sammlung auspacken

Hierzu definiert Listing 2 zunächst einen Iterator über alle JPEG-Fotos auf der Festplatte unter dem Verzeichnis /photos. Dabei überspringt es andere Formate (teilweise lungern dort rohe .NEF-Dateien herum, mit denen Algorithmus nichts anfangen kann) und alle Einträge in .cache-Verzeichnissen, wo mein Bildverarbeitungsprogramm die Thumbnails ablegt, die bei der Gesichtsanalyse außen vor bleiben sollen. Der Iterator photos() ab Zeile 5 nimmt das Startverzeichnis entgegen und orgelt dann durch alle gefundenen Dateien, die der yield()-Operator in Zeile 12 Stück für Stück herausgibt, wenn das Hauptprogramm nach mehr verlangt.

Listing 3: face-search.py

    01 #!/usr/bin/python3
    02 import face_recognition as fr
    03 import dbm
    04 import re
    05 from photos import photos
    06 import sys
    07 
    08 try:
    09   _, ref_img_name, search_path = sys.argv
    10 except ValueError as e:
    11   raise SystemExit("usage: " +
    12     sys.argv[0] + " ref_img search_path")
    13 
    14 cache = dbm.open('cache', 'c')
    15 
    16 ref_img  = fr.load_image_file(ref_img_name)
    17 ref_face = fr.face_encodings(ref_img)[0]
    18 
    19 for photo in photos(search_path):
    20   if photo in cache:
    21     print(photo + " already seen")
    22     continue
    23   cache[photo] = "1"
    24 
    25   try:
    26     img = fr.load_image_file(photo)
    27   except:
    28     continue
    29 
    30   for face in fr.face_encodings(img):
    31     hits = \
    32       fr.compare_faces([ref_face], face)
    33     if any(hit for hit in hits):
    34       print(photo)

Die Zeilen 8-12 in Listing 3 prüfen, ob der User auf der Kommandozeile sowohl ein Referenzbild als auch den Top-Suchpfad für die Fotos angegeben hat. Das erste Element von sys.argv enthält den Skriptnamen, das in der Unterstrich-Variable (_) landet und verworfen wird. Dann lädt Zeile 16 das Referenzbild von der Platte und die nächste Zeile extrahiert das einzige darauf abgebildete Gesicht unter dem Index 0 der zurückkommenden Koordinaten-Liste.

Später orgelt Zeile 19 durch alle von photos.py gefundenen JPEG-Dateien und für jede ruft das Skript die Funktion compare_faces() aus dem Fundus von face_recognition mit den Gesichtswerten aus dem Referenzbild auf. Das Konstrukt any(hit for hit in hits) prüft, ob nur eines der auf dem aktuellen Bild erkannten Gesichter mit dem auf dem Referenzbild übereinstimmt. In diesem Fall führt eines der Elemente in der Liste hits den Wert True und Zeile 34 druckt den Pfad zur Bilddatei auf der Standardausgabe aus, wo es der erstaunte User gleich aufschnappt und es sich mit einem Photo-Viewer zu Gemüte führt. Listing 4 zeigt den Aufruf des Skripts im Docker-Container und dessen Ausgabe. Ich war überwältigt von der Vielzahl von Fotos aus der Perl-Gründerzeit, die der Algorithmus mit einem jugendlich aussehenden Michael Schilli fand. Ach, ja, der Zahn der Zeit.

Listing 4: run.sh

    1 $ docker run -v /photos:/photos -v `pwd`:/build -it face bash -c "cd /build; python3 face-search.py me.jpg /photos"
    2 /photos/2001/12/29/13:55:38.jpg
    3 /photos/2001/07/22/11:47:27.jpg
    4 /photos/2001/07/22/10:35:33.jpg
    5 /photos/2001/07/22/15:43:23.jpg
    6 ...

So viele Nerds

Allerdings ist das Verfahren noch nicht 100% perfekt, es macht zum Teil auch eklatante Fehler. Besonders bei Bildern, die ich auf Open-Source-Konferenzen geschossen habe, und die dutzende weiße junge Nerds zeigen, scheint der Algorithmus anzunehmen, es handle sich um mich, obwohl ich selbst auf den Auslöser gedrückt habe, Unverschämtheit!

Nachdem die künstliche Intelligenz sehr verschwenderisch mit Rechenzeit umgeht und auf jedem Foto gut und gern ein paar Sekunden herumwerkelt, dauert das Durchforsten des gesamten Bilderbaums extrem lange. Bricht das Skript aus irgendeinem Grund ab, wäre es bedauerlich, jedesmal wieder von vorne anfangen zu müssen, deswegen merkt sich Listing 3 alle bisher bearbeiteten Bilder in der persistenten dbm-Datei cache. Praktischerweise schließt Python diese automatisch bei Programmabbruch, sodass das Skript sie lediglich eingangs mit dem Flag "c" öffnen muss (um sie gegebenenfalls erstmals anzulegen) und dann auf das Dictionary cache zugreifen kann, um zu sehen, ob es den Namen der gerade gefundenen Datei bereits enthält.

Brave New World

Da das Verfahren nicht nur ein sondern gleich alle Gesichter eines Bildes herausfieseln und einordnen kann, eröffnen sich ganz neue Möglichkeiten der Informationsbeschaffung. Anhand einer großen Anzahl von Bildern, aufgenommen zum Beispiel von in der Öffentlichkeit positionierten Überwachungskameras, ergebens sich überraschend potente Methoden der Massenüberwachung. Anhand eines Fotos einer Verdachtsperson könnte ein KI-System zum Beispiel herausfinden, mit welchen anderen Personen sich jemand am häufigsten in der Öffentlichkeit herumtreibt. Big Brother is watching!

Infos

[1]

Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2018/06/snapshot/

[2]

"The world's simplest facial recognition api for Python and the command line", https://github.com/ageitgey/face_recognition#face-recognition

[3]

"Machine Learning is Fun! Part 4: Modern Face Recognition with Deep Learning", https://medium.com/@ageitgey/machine-learning-is-fun-part-4-modern-face-recognition-with-deep-learning-c3cffc121d78

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 5:

Unknown directive: =desc