Gestenerkennung ist ein zugegebenermaßen schon etwas älteres Thema, gerade in der Form, wie ich es hier beschreiben will. Vor allem dank Kinect ist der aktuelle Entwicklungsstand hier schon wesentlich weiter. Nichtsdestotrotz eignet sich so ein einfaches Thema sehr gut um wichtige Algorithmen und Techniken kennenzulernen und selbst etwas Eindrucksvolles zu programmieren.

Zur Umsetzung wähle ich Python, weil es eine praktische Sprache ist und meinem Empfinden nach gerade in Problemen der Maschinellen Intelligenz und Sprachverarbeitung häufig zum Einsatz kommt (wenn es nicht um geschwindigkeitsrelevante Algorithmen geht).

Die ersten Überlegungen

Es soll also ein Gestenerkenner entstehen? Was braucht man dazu? Natürlich müssen Bilder von der Webcam irgendwie im Computer verarbeitet werden: also eine Bibliothek zur Bildverarbeitung. Wenn diese Bibliothek das Objekt im Bild gefunden hat, muss ermittelt werden, um welche Geste es sich denn dabei handelt. Dazu braucht man Algorithmen des Maschinellen Lernens. Und natürlich braucht man auch irgendwelche Gesten, die überhaupt erst erkannt werden können.

Alles, was jetzt kursiv markiert ist, sind die groben Bestandteile des Systems. Interessant sind dabei vor allem die Bibliothek zur Bildverarbeitung und die Algorithmen des Maschinellen Lernens.

Von der Kamera zum Objekt

Das Ziel dieses einfachen Gestenerkenners ist, dass man mit einem farbigen Objekt vor der Kamera herumfahren kann und der Computer je nach Geste verschiedene Aktionen ausführt. Dazu soll zunächst aus dem Bild das Objekt extrahiert werden, das beobachtet wird. Hierbei könnte es sich wie in meinem Fall z.B. um eine kleine grüne Tasche handeln. So ein Problem kann man mit der Bibliothek OpenCV erledigen (OpenComputerVision). Diese ist zwar in C geschrieben, hat aber auch Support für Python.

Finden von farbigen Objekten

Also soll zunächst mal ein farbiges Objekt vom Hintergrund extrahiert werden. Dazu wandelt man das Bild am besten vom RGB-Farbraum in den HSV-Farbraum um. Letzteres ist ein Farbraum, bei dem H für die Farbe, S für die Sättigung und V für die Helligkeit steht. Wenn man nun H entsprechend einschränkt, erkennt man nur eine einzige (spezielle) Farbe. Wichtig ist, dass OpenCV nicht RGB verwendet, sondern BGR (also die Farben in einer anderen Reihenfolge, das muss bei der Umwandlung angegeben werden).

import cv2

cam = cv2.VideoCapture(0)

winName = "Gestures Detection"
cv2.namedWindow(winName, cv2.CV_WINDOW_AUTOSIZE)

while True:
    img = cam.read()[1]
    img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    # Extract by color and make binary image (=black/white)
    img = cv2.inRange(img, (70, 50, 50), (130, 200, 200))

    cv2.imshow(winName, img)

    key = cv2.waitKey(50)
    if key == 27:
        cv2.destroyWindow(winName)
        break

Dieses kurze Code-Segment liest in einer Schleife mit cam.read() Bilder von der Webcam ein, überträgt sie vom BGR in den HSV-Farbraum und filtert dann in diesem Bild die Farben innerhalb des Intervalls zwischen den beiden Tupeln. Also H zwischen 70 und 130 und S und V jeweils zwischen 50 und 200. Dies entspricht dem grün meiner Handytasche.

Nun könnte es noch sein, dass im Bild Rauschen vorhanden ist. Dieses kann man durch Erodieren entfernen. Grob gesagt werden dabei Pixel am Rand einer großen Fläche entfernt; ist die Fläche zu klein, wird sie komplett entfernt. Danach kann man Dilatieren, um die noch vorhandenen Flächen wieder auf die alte Größe zu bringen (es werden rund um alle Flächen wieder Pixel angebracht). Hierfür gibt es in OpenCV bereits Funktionen, die direkt nach der Zeile mit cv2.inRange eingefügt werden.

ed_size = 50

img = cv2.erode(img, numpy.array([[1] * ed_size] * ed_size), iterations=2)
img = cv2.dilate(img, numpy.array([[1] * ed_size] * ed_size), iterations=2)

Hier wird eine Matrix mit 50 Spalten und 50 Zeilen erstellt, welche zum Erodieren und Dilatieren gebraucht wird. Bei größeren Matrizen werden mehr Pixel am Rand des Objekts entfernt/hinzugefügt, bei kleineren Matrizen weniger.

Damit ist schon der erste Schritt auf dem Weg zum Gestenerkenner gelegt. Weiter geht es damit, zu erkennen, wie sich ein Objekt bewegt.

Bewegungen in Bildern entdecken

Um eine Bewegung in der Aufnahme zu erkennen, kann man sogenannte Differenzenbilder verwenden. Diese würden sogar dann funktionieren, wenn man nicht vorher nach Farben sucht. Ich will hier jedoch einen anderen Weg präsentieren, da Differenzenbilder in diesem Fall störanfällig gegenüber Bewegungen des Kopfes sind.

Da das Zielobjekt im Bild schon gefunden ist, kann mit sogenannten Momenten gearbeitet werden. Diese geben Details über das Gesamtbild an. Für den Gestenerkenner ist wichtig, wo der Mittelpunkt des erkannten Objekts ist. Dies kann man mit folgenden Berechnungen ermitteln, die hier direkt in einer Funktion gekapselt wurde:

def pointer_pos(img):
    moments = cv2.moments(img)
    area = moments['m00']

    # Check if found object is large enough to be the target
    # otherwise probably some noise
    if area > 100000:
        x = moments['m10'] / area
        y = moments['m01'] / area

        return x, y

    return (None, None)

So lässt sich für jedes Bild ermitteln, wo sich das Objekt momentan befindet. Mit zwei aufeinanderfolgenden Bildern kann man also die Bewegung des Objekts ermitteln.

Dazu muss die Schleife zum Einlesen der Bilder angepasst werden. Am besten lagert man dazu das Einlesen der Bilder in eine eigene Funktion aus:

def current_image(cam):
    img = cam.read()[1]
    img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    # Extract by color and make binary image (=black/white)
    img = cv2.inRange(img, (70, 50, 50), (130, 200, 200))
    
    # erode and dilate to reduce noise
    img = cv2.erode(img, numpy.array([[1] * ed_size] * ed_size), iterations=2)
    img = cv2.dilate(img, numpy.array([[1] * ed_size] * ed_size), iterations=2)

    return img
import cv2

ed_size = 50

cam = cv2.VideoCapture(0)

winName = "Gestures Detection"
cv2.namedWindow(winName, cv2.CV_WINDOW_AUTOSIZE)

img = current_image(cam)
x1, y1 = pointer_pos(img)

while True:
    x0 = x1
    y0 = y1
    
    img = current_image(cam)
    x1, y1 = pointer_pos(img)

    # only if there is no None position, we may calculate movement
    # None means the object is not visible to the webcam
    if x1 is not None and x0 is not None and y1 is not None and y0 is not None:
        x_delta = x1 - x0
        y_delta = y1 - y0

    cv2.imshow(winName, img)

    key = cv2.waitKey(50)
    if key == 27:
        cv2.destroyWindow(winName)
        break

Es gibt nun ein paar neue Variablen für x- und y-Koordinaten. Dabei geben x0 und y0 jeweils die Positionen im alten Bild und x1 und y1 die Positionen im neuen Bild an. Die Bewegung über das einzelne Bild hinweg wird dann in x_delta und y_delta gespeichert.

Aus diesen Bewegungen kann nun die Bewegungsrichtung in jedem Schritt ermittelt werden. Man nennt diesen Schritt auch die Quantisierung, weil aus einem kontinuierlichen Wertebereich (aller möglicher Kombinationen von x- und y-Bewegungen) ein diskreter Wertebereich aus lediglich 4 oder 8 Richtungen entsteht (z.B. oben, unten, links, rechts).

Hierzu dient die Funktion atan2, welche in den meisten Programmiersprachen mitgeliefert wird. Sie errechnet aus einem Vektor im euklidischen Raum das Bogenmaß. Da dies eindimensional ist, lässt sich über Intervalle leicht errechnen, in welche Richtung die Bewegung ging.

Dazu legt man eine neue Funktion an. Hierbei werden nur Bewegungen betrachtet, die mindestens threshold Pixel umfassen. Die genauen Werte für die Richtungseinschränkungen kann man an einer Beispielgrafik für den atan2 ablesen, jedoch muss man beachten, dass die Richtungen in OpenCV nicht so sind, wie man es vielleicht vermutet (also nicht 1 = oben, 1 = rechts).

def movement_direction(x_delta, y_delta, threshold = 10):
    if abs(x_delta) > threshhold or abs(y_delta) > threshhold:
        degree = math.atan2(y_delta, x_delta)

        if -0.875 * math.pi <= degree < -0.625 * math.pi:
            direction = models.UP_RIGHT
        elif -0.625 * math.pi <= degree < -0.375 * math.pi:
            direction = models.UP
        elif -0.375 * math.pi <= degree < -0.125 * math.pi:
            direction = models.UP_LEFT
        elif -0.125 * math.pi <= degree < 0.125 * math.pi:
            direction = models.LEFT
        elif 0.125 * math.pi <= degree < 0.375 * math.pi:
            direction = models.DOWN_LEFT
        elif 0.375 * math.pi <= degree < 0.625 * math.pi:
            direction = models.DOWN
        elif 0.625 * math.pi <= degree < 0.875 * math.pi:
            direction = models.DOWN_RIGHT
        else:
            direction = models.RIGHT

        return direction
    else:
        return None

Die hier verwendeten Konstanten UP, DOWN etc. stammen aus einer anderen Datei, welche wir unter dem Namen models.py neu anlegen.

# models.py

UP = 0
DOWN = 1
LEFT = 2
RIGHT = 3
UP_RIGHT = 4
UP_LEFT = 5
DOWN_RIGHT = 6
DOWN_LEFT = 7

Der sonstige Code muss noch um die neue Funktion erweitert werden. Außerdem sollte ein Zähler eingefügt werden, der mitzählt, wie lange es schon keine Bewegung mehr gibt. Zudem wird eine Variable eingefügt, die die bisherigen Bewegungen speichert.

import cv2
import models

ed_size = 50

cam = cv2.VideoCapture(0)

winName = "Gestures Detection"
cv2.namedWindow(winName, cv2.CV_WINDOW_AUTOSIZE)

img = current_image(cam)
x1, y1 = pointer_pos(img)

not_changed = 0
path = []

while True:
    x0 = x1
    y0 = y1
    
    img = current_image(cam)
    x1, y1 = pointer_pos(img)

    # only if there is no None position, we may calculate movement
    # None means the object is not visible to the webcam
    if x1 is not None and x0 is not None and y1 is not None and y0 is not None:
        x_delta = x1 - x0
        y_delta = y1 - y0

        direction = movement_direction(x_delta, y_delta)
            if direction is not None:
            path.append(direction)
        else:
            not_changed += 1
    else:
        not_changed += 1

    cv2.imshow(winName, img)

    key = cv2.waitKey(50)
    if key == 27:
        cv2.destroyWindow(winName)
        break

Die richtigen Gesten erkennen

Hat man diese Quantisierung eingebaut, kann man eigentlich schon Gesten erkennen. Ein Problem ist nur noch die Ungenauigkeit in der Bewegung. Gerade in den Bewegungsübergängen und -ausläufen gibt es oft Zusatzbewegungen, die die Geste unerkennbar machen würden. Wenn ich z.B. eine Geste OBEN-RECHTS durchführen will, dann ziehe ich die Hand am Ende der Rechtsbewegung wieder leicht nach unten.

Für solche Probleme nutzt man in der Informatik heutzutage fast immer stochastische Modellierungen. Das heißt, man arbeitet mit Wahrscheinlichkeiten, sodass man dann berechnen kann zu wieviel Prozent Wahrscheinlichkeit diese Geste ausgeführt wurde oder zu wieviel Prozent eine andere.

An dieser Stelle kommen Hidden-Markov-Models ins Spiel. Ich erspare mir hier eine ausführliche Einführung, werde aber noch einen längeren Artikel dazu schreiben.

Für jedes Hidden-Markov-Model benötigt man

  • eine Zustandsübergangswahrscheinlichkeitsmatrix,
  • eine Emissionswahrscheinlichkeitsmatrix und
  • einen Vektor der Anfangswahrscheinlichkeiten

Diese kann aus einer einfachen Gestendarstellung automatisch berechnet werden. Eine Geste besteht aus einer Abfolge von Bewegungen. In Python könnte man dies als Liste implementieren. Damit sieht eine Geste bspw. so aus:

[UP, RIGHT, DOWN]

Für jede Geste wird ein eigenes HMM generiert, d.h. für jede Geste benötigt man obige beiden Matrizen und den Vektor.

Anfangswahrscheinlichkeiten

Am leichtesten zu erzeugen ist der Vektor der Anfangswahrscheinlichkeiten, weil die Wahrscheinlichkeit für den Startzustand 1 für denjenigen Zustand ist, mit dem die Geste beginnt. Das bedeutet nicht, dass die Bewegung nicht unsauber/verschmiert sein kann, aber darum kümmern sich beim HMM die möglichen Ausgaben und Ausgabewahrscheinlichkeiten. Der Startzustand muss der Zustand sein, mit dem die Geste beginnt. Im Beispiel also UP.

Dann kann er Vektor erzeugt werden, indem man ihn zunächst mit 0 initialisiert und an der korrekte Stelle anschließend 1 setzt. Alle Funktionen zur Erzeugung eines HMM gehören in die Datei models.py.

# models.py
# A geste (something like a shortform of gesture :P) is one possible
# movement direction represented by a hidden state.
# There are up, down, left, right
GESTES_COUNT = 4

def initial_vector(gesture):
    vec = [0 for i in range(GESTES_COUNT)]
    # Take the beginning of the gesture and set probability to 1
    vec[gesture[0]] = 1
    return vec

Emissionswahrscheinlichkeiten

Auch brauchbare Werte für die Emissionswahrscheinlichkeiten sind nicht schwer zu finden. Das HMM soll intern 4 mögliche Zustände besitzen für Bewegungen nach oben, unten, rechts und links. Die Beobachtungen hingegen sollen feiner sein, indem die Diagonalen auch betrachtet werden. Jetzt kann man sich überlegen, dass eine Bewegung, die eigentlich nach oben gehen sollte (interner Zustand), ziemlich wahrscheinlich auch nach oben, rechts-oben oder links-oben gehen wird (Beobachtungen). Etwas unwahrscheinlicher ist es, dass sie nach rechts oder links geht und ziemlich unwahrscheinlich geht sie in irgendeiner Weise nach unten.

So kann man sich halbwegs willkürlich eine Wahrscheinlichkeitsverteilung für die Emissionen des Zustands UP überlegen, welche folgendermaßen aussieht:

# order as in constants definition as defined before
# [UP, DOWN, LEFT, RIGHT, UP_RIGHT, UP_LEFT, DOWN_RIGHT, DOWN_LEFT]
B[UP] = [0.37, 0.04, 0.08, 0.08, 0.165, 0.165, 0.05, 0.05]

Um ganz sicher zu gehen, dass alle Werte gültig sind, wird hier zunächst eine Matrix mit Gleichverteilung initialisiert. Die äußere Dimension der Matrix (die Zeilen) stehen jeweils für einen Zustand, die innere Dimension für die Emissionen in einem bestimmten Zustand.

Nach der Initialisierung werden die Zeilen wieder Schritt für Schritt mit den zu den jeweiligen Gesten passenden Wahrscheinlichkeitsverteilungen überschrieben. Diese sind gleich aufgebaut wie obige, nur mit den Wahrscheinlichkeiten in anderer Reihenfolge (damit z.B. DOWN wieder bei den passenden Richtungen wahrscheinlicher ist).

OBSERVATIONS_COUNT = 8

def emission_matrix():
    B = [[float(1)/OBSERVATIONS_COUNT for i in range(OBSERVATIONS_COUNT)] for j in range(GESTES_COUNT)]
    
    B[UP] = [0.37, 0.04, 0.08, 0.08, 0.165, 0.165, 0.05, 0.05]
    B[DOWN] = [0.04, 0.37, 0.08, 0.08, 0.05, 0.05, 0.165, 0.165]
    B[LEFT] = [0.08, 0.08, 0.37, 0.04, 0.05, 0.165, 0.05, 0.165]
    B[RIGHT] = [0.08, 0.08, 0.04, 0.37, 0.165, 0.05, 0.165, 0.05]
    return B

Übergangswahrscheinlichkeiten

Der schwierigste Teil der automatischen Generierung sind die Übergangswahrscheinlichkeiten. Zunächst muss hierin auf jeden Fall modelliert werden, dass man eine längere Zeit imselben Zustand verweilen kann. Die Wahrscheinlichkeit hierfür ist sogar sehr hoch. Wenn eine Geste die Bewegung UP enthält, dann wird der Benutzer im Regelfall mehrere über mehrere Einzelbilder hinweg den Arm nach oben bewegen. D.h. bei der Objekterkennung werden mehrere UP-Bewegungen erkannt.

Außerdem soll immer dann eine hohe Wahrscheinlichkeit am Übergang vorhanden sein, wenn innerhalb der Geste von einer Bewegungsrichtung zur anderen gewechselt wird.

Für den Endzustand hingegen soll die Wahrscheinlichkeit, in diesem Zustand zu verweilen, gerade 1 sein. Denn genauso wie beim Startzustand muss das HMM für diese Geste in genau diesem Zustand enden.

Durch diese Kombination von Wahrscheinlichkeiten sind für jede Geste immer nur genau die Abfolgen von Zuständen möglich, durch die diese Geste definiert ist (inkl. Wiederholungen). Alle Unsicherheiten und Fehler bei der Erkennung werden von den Ausgabewahrscheinlichkeiten abgedeckt.

Um die Arbeit etwas zu erleichtern, kann man noch eine Funktion zur Normalisierung der Zeilen erstellen. Diese sorgt dafür, dass in jeder Zeile wirklich echte Wahrscheinlichkeiten stehen, die sich zu 1 aufsummieren. Dann muss man beim Zuweisen der “Wahrscheinlichkeiten” nicht darauf achten, was vorher passiert ist und nachher passiert.

# models.py
def normalize_rows(matrix):
    new_matrix = []

    for row in matrix:
        div = sum(row)
        new_matrix.append([])
        
        for elem in row:
            if div != 0:
                new_matrix[-1].append(float(elem)/div)
            else:
                new_matrix[-1].append(elem)
    
    return new_matrix

def transition_matrix(gesture):
    A = [[0 for i in range(GESTES_COUNT)] for j in range(GESTES_COUNT)]
    # self transitions are high
    for geste in gesture:
        A[geste][geste] = 0.7

    first = gesture[0]

    for second in gesture[1:]:
        # if we have a transition, prob must be high
        A[first][second] = 0.4
        first = second

    # ending element has no transitions anymore, so give it one
    A[gesture[-1]][gesture[-1]] = 1

    return normalize_rows(A)

Mit diesen Methoden kann nun für jede Geste ein eigenes Hidden-Markov-Model erzeugt werden, welches die jeweilige Geste repräsentiert. Außerdem können Gesten tatsächlich mit der auszuführenden Aktion definiert werden.

# models.py
gestures = [
    ([UP, RIGHT], ['/usr/bin/xdotool', 'getactivewindow', 'windowkill']),
    ([UP, LEFT], ['notify-send', '"received command"', '"Hooray!"']),
    ([DOWN], ['amixer', 'set', 'Master', '10%-']),
    ([UP], ['amixer', 'set', 'Master', '10%+']),
]

models = []

sigma = ghmm.IntegerRange(0, 8)
B = emission_matrix()
for gesture in gestures:
    A = transition_matrix(gesture[0])
    pi = initial_vector(gesture[0])
    
    m = ghmm.HMMFromMatrices(sigma, ghmm.DiscreteDistribution(sigma), A, B, pi)
    models.append((m, gesture[1]))

Vom Modell zur wahrscheinlichsten Geste

Mit dem Forward-Algorithmus kann nun errechnet werden, wie gut jedes der Modelle auf eine eingegebene Geste passt. Das Modell, das am besten passt, wird wohl die Geste sein, die ausgeführt werden soll.

Es ist sinnvoll, hier noch einen Schwellwert (threshold) einzubauen, um sicherzustellen, dass komplett falsche Eingaben überhaupt kein Ergebnis hervorrufen.

Dieser Teil wird wieder in die Hauptdatei implementiert. Dazu wird eine EmissionSequence benötigt (eine GHMM-Repräsentation von Beobachtungen).

Die Funktion, die anhand der gegebenen EmissionSequence die wahrscheinlichste Geste findet, sieht so aus:

import subprocess

def execute(emission_seq, models):
    considered = []
    for model, command in models:
        res = model.forward(emission_seq)
        considered.append((res[1][-1], command))
    
    max_val, command = max(considered)
    if max_val >= 0.3:
        subprocess.call(command)

Hier werden die Forward-Wahrscheinlichkeiten ausgerechnet und deren Maximum gesucht. Der Befehl zum HMM mit der größten Wahrscheinlichkeit wird ausgeführt, sofern er einen Schwellwert übersteigt.

Der Aufruf dieser Funktion findet in der main-Schleife direkt hinter dem else-Zweig der Prüfung, ob irgendeine Zeigerposition None ist, statt. Am besten schaut man einfach im Gesamtcode am Ende des Tutorials schnell nach.

import ghmm

if not_changed > 5:
    if len(path) >= 2:
        execute(ghmm.EmissionSequence(models.sigma, path), models.models)
    path = []
    not_changed = 0

Alles zusammen

Die gesamte Hauptdatei sieht dann so aus:

import cv2
import subprocess
import ghmm
import models


def current_image(cam):
    img = cam.read()[1]
    img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    # Extract by color and make binary image (=black/white)
    img = cv2.inRange(img, (70, 50, 50), (130, 200, 200))
    
    # erode and dilate to reduce noise
    img = cv2.erode(img, numpy.array([[1] * ed_size] * ed_size), iterations=2)
    img = cv2.dilate(img, numpy.array([[1] * ed_size] * ed_size), iterations=2)

    return img

def pointer_pos(img):
    moments = cv2.moments(img)
    area = moments['m00']

    # Check if found object is large enough to be the target
    # otherwise probably some noise
    if area > 100000:
        x = moments['m10'] / area
        y = moments['m01'] / area

        return x, y

    return (None, None)

def movement_direction(x_delta, y_delta, threshold = 10):
    if abs(x_delta) > threshhold or abs(y_delta) > threshhold:
        degree = math.atan2(y_delta, x_delta)

        if -0.875 * math.pi <= degree < -0.625 * math.pi:
            direction = models.UP_RIGHT
        elif -0.625 * math.pi <= degree < -0.375 * math.pi:
            direction = models.UP
        elif -0.375 * math.pi <= degree < -0.125 * math.pi:
            direction = models.UP_LEFT
        elif -0.125 * math.pi <= degree < 0.125 * math.pi:
            direction = models.LEFT
        elif 0.125 * math.pi <= degree < 0.375 * math.pi:
            direction = models.DOWN_LEFT
        elif 0.375 * math.pi <= degree < 0.625 * math.pi:
            direction = models.DOWN
        elif 0.625 * math.pi <= degree < 0.875 * math.pi:
            direction = models.DOWN_RIGHT
        else:
            direction = models.RIGHT

        return direction
    else:
        return None

def execute(emission_seq, models):
    considered = []
    for model, command in models:
        res = model.forward(emission_seq)
        considered.append((res[1][-1], command))
    
    max_val, command = max(considered)
    if max_val >= 0.3:
        subprocess.call(command)


ed_size = 50

cam = cv2.VideoCapture(0)

winName = "Gestures Detection"
cv2.namedWindow(winName, cv2.CV_WINDOW_AUTOSIZE)

img = current_image(cam)
x1, y1 = pointer_pos(img)

not_changed = 0
path = []

while True:
    x0 = x1
    y0 = y1
    
    img = current_image(cam)
    x1, y1 = pointer_pos(img)

    # only if there is no None position, we may calculate movement
    # None means the object is not visible to the webcam
    if x1 is not None and x0 is not None and y1 is not None and y0 is not None:
        x_delta = x1 - x0
        y_delta = y1 - y0

        direction = movement_direction(x_delta, y_delta)
            if direction is not None:
            path.append(direction)
        else:
            not_changed += 1
    else:
        not_changed += 1

    if not_changed > 5:
        if len(path) >= 2:
            execute(ghmm.EmissionSequence(models.sigma, path), models.models)
        path = []
        not_changed = 0

    cv2.imshow(winName, img)

    key = cv2.waitKey(50)
    if key == 27:
        cv2.destroyWindow(winName)
        break

Die models.py, in der die Gesten definiert und die HMMs erzeugt werden, sieht folgendermaßen aus:

UP = 0
DOWN = 1
LEFT = 2
RIGHT = 3
UP_RIGHT = 4
UP_LEFT = 5
DOWN_RIGHT = 6
DOWN_LEFT = 7
GESTES_COUNT = 4
OBSERVATIONS_COUNT = 8

gestures = [
    ([UP, RIGHT], ['/usr/bin/xdotool', 'getactivewindow', 'windowkill']),
    ([UP, LEFT], ['notify-send', '"received command"', '"Hooray!"']),
    ([DOWN], ['amixer', 'set', 'Master', '10%-']),
    ([UP], ['amixer', 'set', 'Master', '10%+']),
]


def initial_vector(gesture):
    vec = [0 for i in range(GESTES_COUNT)]
    # Take the beginning of the gesture and set probability to 1
    vec[gesture[0]] = 1
    return vec

def emission_matrix():
    B = [[float(1)/OBSERVATIONS_COUNT for i in range(OBSERVATIONS_COUNT)] for j in range(GESTES_COUNT)]
    
    B[UP] = [0.37, 0.04, 0.08, 0.08, 0.165, 0.165, 0.05, 0.05]
    B[DOWN] = [0.04, 0.37, 0.08, 0.08, 0.05, 0.05, 0.165, 0.165]
    B[LEFT] = [0.08, 0.08, 0.37, 0.04, 0.05, 0.165, 0.05, 0.165]
    B[RIGHT] = [0.08, 0.08, 0.04, 0.37, 0.165, 0.05, 0.165, 0.05]
    return B

def normalize_rows(matrix):
    new_matrix = []

    for row in matrix:
        div = sum(row)
        new_matrix.append([])
        
        for elem in row:
            if div != 0:
                new_matrix[-1].append(float(elem)/div)
            else:
                new_matrix[-1].append(elem)
    
    return new_matrix

def transition_matrix(gesture):
    A = [[0 for i in range(GESTES_COUNT)] for j in range(GESTES_COUNT)]
    # self transitions are high
    for geste in gesture:
        A[geste][geste] = 0.7

    first = gesture[0]

    for second in gesture[1:]:
        # if we have a transition, prob must be high
        A[first][second] = 0.4
        first = second

    # ending element has no transitions anymore, so give it one
    A[gesture[-1]][gesture[-1]] = 1

    return normalize_rows(A)


models = []

sigma = ghmm.IntegerRange(0, 8)
B = emission_matrix()
for gesture in gestures:
    A = transition_matrix(gesture[0])
    pi = initial_vector(gesture[0])
    
    m = ghmm.HMMFromMatrices(sigma, ghmm.DiscreteDistribution(sigma), A, B, pi)
    models.append((m, gesture[1]))