Nach einem Tutorial zur Gestenerkennung will ich mich weiter der Künstlichen Intelligenz widmen und diesmal über Gesichtserkennung schreiben.

Grundsätzlich muss man bei der Gesichtserkennung zwischen verschiedenen Teilproblemen unterscheiden. Eines ist die Face Detection, das andere die Face Recognition.

Bei der Face Detection will man auf einem großen Bild die Stelle finden, an der sich das Gesicht befindet. Ins Deutsche könnte man das als Gesichtsentdeckung übersetzen. Dieses Problem kann mit OpenCV gelöst werden. Die eigentliche Gesichtserkennung (Face Recognition) befasst sich dann damit, das Gesicht einer bereits bekannten Person zuzuordnen. Es wird hier also ein Speicher benötigt, der die bereits bekannten Gesichter repräsentiert.

Grundsätzlich gibt es für beide Verfahren ganz verschiedene Algorithmen. Als sehr effektiv in der Detektion haben sich allerdings Haar-Features als sehr effektiv erwiesen. OpenCV liefert bereits ein Paket an solchen vortrainierten Haar-Features, sodass man nicht mehr selbst trainieren muss, sondern direkt Gesichter erkennen kann. Bei der Gesichtserkennung ist ein sehr altes Verfahren die Verwendung von Eigenfaces. Diese verwenden zur Erkennung einen Vergleich von Frontalgesichtern, wobei jeweils Durchschnittsgesichter berechnet werden. Der große Nachteil der Eigenface-Methode ist, dass sie lediglich mit Frontalaufnahmen umgehen kann und sehr, sehr anfällig gegenüber verschiedenen Größen von Gesichtern ist. Gerade das letztere Problem kann man allerdings mit der Gesichtsdetektion von OpenCV sehr gut lösen.

Gesichtsdetektion

Ein Gesicht zu finden ist mit OpenCV nicht besonders schwer. Man muss lediglich das Bild laden, zur besseren Erkennung in Graustufen umwandeln und anschließend noch das Histogramm ausgleichen. Letzteres macht man, um den Kontrast in den Bereichen zu erhöhen, die besonders wichtig sind. D.h. wenn im Bild sehr viele Graustufen vorhanden sind, werden diese so getrennt, dass sie besser unterscheidbar sind.

Dazu wird zunächst eine Funktion zum Extrahieren der Gesichter benötigt. Umgesetzt wird die Extraktion dann mit cv2.CascadeClassifier::detectMultiScale, welches innerhalb eines Fotos Gesichter verschiedener Größen erkennen kann. Dazu muss man jedoch einen Faktor angeben, um den das Bild nach jeder Iteration verändert werden soll, um Gesichter in anderen Größen zu finden. Außerdem lohnt es sich aus Geschwindigkeitsgründen evtl. eine minimale und eine maximale Größe anzugeben.

def detect_faces(img, cascade_fn='/usr/share/opencv/haarcascades/haarcascade_frontalface_alt.xml',
    scaleFactor=1.1, minNeighbors=4, minSize=(100, 100), maxSize=(2000, 2000    ),
    flags=cv.CV_HAAR_SCALE_IMAGE):

    cascade = cv2.CascadeClassifier(cascade_fn)
    rects = cascade.detectMultiScale(img, scaleFactor=scaleFactor,
            minNeighbors=minNeighbors, minSize=minSize, maxSize=maxSize,
            flags=flags)

    if len(rects) == 0:
        return []
    rects[:, 2:] += rects[:, :2]
    return rects

Die Funktion detect_faces erkennt Gesichter in einem Bild und gibt die Koordinaten der Eckpunkte aus. Anschließend werden diese Eckpunkte verwendet, um das Bild aus dem Gesamtbild zu extrahieren und an einem neuen Pfad abzuspeichern. Dazu wird eine Funktion crop angelegt. Diese erledigt auch die Umwandlung in Grauwerte und den Histogrammausgleich. Der Einfachheit halber kann diese Funktion bisher nur mit einem einzelnen Gesicht pro Bild umgehen.

def crop(in_fn, out_fn):
    img_color = cv2.imread(in_fn)
    img_gray = cv2.cvtColor(img_color, cv.CV_RGB2GRAY)
    img_gray = cv2.equalizeHist(img_gray)

    for x1, y1, x2, y2 in detect_faces(img_gray):
        # TODO: Will override all previous occurrences
        img_out = img_color[y1:y2, x1:x2]
        cv2.imwrite(out_fn, img_out)

In der Hauptroutine wird diese Funktion dann für jedes Bild einmal ausgeführt.

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("Usage: %s source_dir dest_dir" % (sys.argv[0]))
        sys.exit(1)

    for f in glob.glob('%s/*.jpg' % (sys.argv[1],)):
        filename = os.path.basename(f)
        crop(f, "/".join((sys.argv[2], filename)))

Gesichtserkennung

Die Gesichtserkennung kann man anschließend mit Eigenfaces umsetzen. Das ist ein relativ alter und nicht mehr ganz aktueller Ansatz, aber er ist nicht so schwer umzusetzen. Grundsätzlich ist die Idee von Eigenfaces, eine Menge von Grundbildern zu erzeugen und dann diese so aufeinander aufzuaddieren, dass möglichst exakt wieder das Originalbild rekonstruiert wird. Die Koeffizienten dieser Zerlegung wählt man dann als charakterisierende Eigenschaft jedes Bildes. Ähnliche Gesichter sollten nun auch ähnliche Koeffizienten erhalten, sodass man erkennen kann, welche Bilder die gleichen Gesichter darstellen.

Da für Eigenfaces bereits die Bilder vom reinen Gesicht (d.h. keine weiteren Körperteile) benötigt werden und vor allem auch alle Bilder in derselben Auflösung sein müssen, ist ein wenig Vorarbeit nötig.

Dafür kann man sich ein Shell-Skript (Linux) schreiben, welches diese Vorarbeit routiniert durchführt. Zunächst einmal muss das bereits oben erstellte Skript zur Gesichtsdetektion für Trainungs- und Testdaten ausgeführt werden. Anschließend müssen noch alle Bilder auf das gleiche Format gebracht werden. Da die Gesichtsdetektion bereits quadratische Bereiche erkennt, muss hierauf nicht mehr geachtet werden. Man muss sich lediglich noch einen guten Kompromiss für die Auflösung überlegen. Ich habe beim ersten Versuch 250x250 Pixel gewählt.

#!/bin/bash

# find faces on training and test images
python2 extract_faces.py raw faces
python2 extract_faces.py todetectraw todetectfaces

# resize all faces to the same size (required by PyFaces)
for file in faces/*; do
    convert -resize 250x250! $file $file
done

for file in todetectfaces/*; do
    convert -resize 250x250! $file $file
done

Die Erkennung mit scikit-learn ist dann ziemlich leicht. Man muss lediglich noch die Bilder in Vektoren umwandeln (d.h. alle Pixel eindimensional anordnen) und anschließend eine Hauptkomponentenzerlegung durchführen. Für letzteres gibt es glücklicherweise bereits Algorithmen, da dies tiefergehende Mathematik erfordern würde. Diese Hauptkomponentenzerlegung berechnet die Eigenfaces (d.h. die Referenzgesichter, aus denen später das Originalgesicht rekonstruiert werden soll).

Hat man die Hauptkomponentenzerlegung berechnet, kann man alle Bilder auf diese Zerlegung transformieren und erhält die Gewichte jedes einzelnen Eigenface. Ähnliche Gesichter sollten hier ähnliche Gewichte haben (da sie denselben Referenzgesichern ähnlich sind), sodass die euklidische Distanz zwischen den Bildern als Ähnlichkeitsmaß ausreicht.

Zusammengefasst sind also folgende Schritte nötig:

  1. Pixel der Bilder eindimensional anordnen
  2. Hauptkomponentenzerlegung berechnen
  3. Hauptkomponentenzerlegung auf jedes Bild anwenden
  4. Prüfen, welches Bild aus der bekannten Datenbank dem unbekannten Bild am ähnlichsten ist

Korrigierte Version

from sklearn.decomposition import RandomizedPCA
import numpy as np
import glob
import cv2
import math
import os.path
 
 
def actor_from_filename(filename):
    filename = os.path.basename(filename)
    return filename.rpartition('-')[0]
 
def prepare_image(filename):
    img_color = cv2.imread(filename)
    img_gray = cv2.cvtColor(img_color, cv2.cv.CV_RGB2GRAY)
    img_gray = cv2.equalizeHist(img_gray)
    return img_gray.flat
 
 
IMG_RES = 250 * 250
NUM_EIGENFACES = 15
 
train_faces = glob.glob('faces/*')
 
# Create an array with flattened images X
# and an array with names of the people on each image y
X_train = np.zeros([len(train_faces), IMG_RES], dtype='int8')
y_train = []
 
for i, face in enumerate(train_faces):
    X_train[i,:] = prepare_image(face)
    y_train.append(actor_from_filename(face))
 
# perform principal component analysis on the images
pca = RandomizedPCA(n_components=NUM_EIGENFACES, whiten=True).fit(X_train)
X_pca = pca.transform(X_train)
 
# Search suggestions for all test faces,
# usually you will only need this for one
test_faces = glob.glob('todetectfaces/*')
X_test = np.zeros([len(test_faces), IMG_RES], dtype='int8')
y_test = []
 
for i, face in enumerate(test_faces):
    X_test[i,:] = prepare_image(face)
    y_test.append(actor_from_filename(face))
 
for j, ref_pca in enumerate(pca.transform(X_test)):
    # Calculate euclidian distance to each of the known images
    # and select smallest
    distances = []
    for i, known_pca in enumerate(X_pca):
        dist = math.sqrt(sum([diff**2 for diff in (ref_pca - known_pca)]))
 
        distances.append((dist, y_train[i]))
 
 
    test_name = y_test[j]
    found_name = min(distances)[1]
 
    print("Testing: %s" % (test_name,))
    print("Suggestion: %s" % (found_name,))

Ursprünglich veröffentlichte Version

from sklearn.decomposition import RandomizedPCA
import numpy as np
import glob
import cv2
import math
import os.path
 

def actor_from_filename(filename):
    filename = os.path.basename(filename)
    return filename.rpartition('-')[0]

def prepare_image(filename):
    img_color = cv2.imread(filename)
    img_gray = cv2.cvtColor(img_color, cv2.cv.CV_RGB2GRAY)
    img_gray = cv2.equalizeHist(img_gray)
    return img_gray.flat


IMG_RES = 250 * 250
NUM_EIGENFACES = 15

train_faces = glob.glob('faces/*')

# Create an array with flattened images X
# and an array with names of the people on each image y
X = np.zeros([len(train_faces), IMG_RES], dtype='int8')
y = []

for i, face in enumerate(train_faces):
    X[i,:] = prepare_image(face)
    y.append(actor_from_filename(face))

# perform principal component analysis on the images
pca = RandomizedPCA(n_components=NUM_EIGENFACES, whiten=True).fit(X)
X_pca = pca.transform(X)

# Search suggestions for all test faces,
# usually you will only need this for one
test_faces = glob.glob('todetectfaces/*')
X = np.zeros([len(test_faces), IMG_RES], dtype='int8')
y = []

for i, face in enumerate(test_faces):
    X[i,:] = prepare_image(face)
    y.append(actor_from_filename(face))

for j, ref_pca in enumerate(pca.transform(X)):
    # Calculate euclidian distance to each of the known images
    # and select smallest
    distances = []
    for i, test_pca in enumerate(X_pca):
        dist = math.sqrt(sum([diff**2 for diff in (ref_pca - test_pca)]))

        distances.append((dist, y[i]))
 

    test_name = y[j]
    found_name = min(distances)[1]

    print("Testing: %s" % (test_name,))
    print("Suggestion: %s" % (found_name,))

Noch verbessert werden kann der Erkenner, indem man statt der euklidischen Distanz Support-Vector-Machines verwendet, wie im verlinkten Artikel bei scikit-learn. Das wäre allerdings noch Stoff für einen weiteren Artikel.

I do not maintain a comments section. If you have any questions or comments regarding my posts, please do not hesitate to send me an e-mail to blog@stefan-koch.name.