Anmelden Registrieren

Badges

Follow Peter Bouda on Google Plus

Feeds

Neueste BlogeinträgeBlog

Augmented Reality mit OpenCV, PyQt und OpenGL

Bearbeitet am Mittwoch, 17. August 2011, 12:46 Uhr von pbouda

Das Thema “Augemented Reality” spielt eine immer größere, gerade auf mobilen Geräten. Dabei werden Informationen zu einem aktuellen Kamerabild hinzugefügt, etwa die Namen und Informationen zu aktuell sichtbaren Objekten im Bild oder Navigationsanweisungen in einem Live-Bild. Der schwierigste Teil dabei ist die Erkennung und Verfolgung der Objekte, schließlich kann auf dem Bild einfach alles zu erkennen sein, der PC oder das Mobiltelefon kann das zunächst wenig eingrenzen. Mit OpenCV (CV = “Computer Vision”) existiert eine maßgeblich von Intel entwickelte Bibliothek, die gerade für Aufgaben der Objekterkennung und -verfolgung geschaffen wurde. So lassen sich schon einmal bewegliche Dinge innerhalb eines Bildes von den unbeweglichen trennen. Zusätzlich enthält die Bibliothek eine Reihe von allgemeinen Klassifizieren, die sich mit Bilddaten bekannter Objekte trainieren lassen. So lassen sich dann beispielsweise auf Live-Bildern Gesichter erkennen. Hat man das gesuchte Objekt erst einmal erkannt, lassen sich auf das Live-Bild schließlich weitere Bilddaten projezieren. Beispielsweise könnte man einem erkannten Geischt einen schönen Bart hinzufügen.

Dieses Tutorial beschäftigt sich mit der Benutzung von OpenCV zur Gesichterkennung mit Python. Wir werden sehen, dass für diese Aufgabe erstaunlich wenig Code nötig ist. Die Trainingsdaten zur Gesichtserkennung sind frei im Internet verfügbar. Das erkannte Gesicht soll mit einem roten Rechteck gekennzeichnet werden. Anschließend zeigen wir das Ganze in einem PyQt-OpenGL-Widget und malen noch ein sich drehendes Dreieck darüber. Wir haben somit eine schöne Basis für eine Augmented-Reality-Anwendung, schließlich können mit OpenGL und PyQt noch weitere zweidimensionale und dreidimensionale Objekte in das Bild gerendert werden. Hier schon einmal ein Screenshot der unter Ubuntu laufenden Anwendung:

Voraussetzungen

Für dieses Tutorial müssen insgesamt drei Python-Module installiert werden (und natürlich Python):

  • OpenCV für Python: ist samt Python-Beispielen enthalten im OpenCV-Paket (Ubuntu-Paketname: python-opencv)
  • PyQt: zur Darstellung des Live-Bildes in einem OpenGL-Widget (Ubuntu-Paketname: python-qt4 und python-qt4-gl)
  • OpenGL für Python: Zur Darstellung von dreidimensionalen Overlay-Objekte (Ubuntu-Paketname: python-opengl)

Diese Module übernehmen die Hauptarbeit für uns. Im Prinzip müssen wir nur die einzelnen Funktionen verknüpfen.

Quellcode des Projekts

Das gesamte Paket könnt ihr euch auch hier herunterladen. Entpackt einfach das Archiv und startet das Skript “ar_pyqt_gl.py”:

python ar_pyqt_gl.py

Das Skript erwartet die Datei “haarcascade_frontalface_alt.xml” im selben Verzeichnis, eine Version ist im Download-Paket enthalten. Diese Datei enthält die Daten aus dem Training für Gesichtserkennung. Das gesamte Programm besteht also nur aus einer Datei. Den Inhalt dieser Datei wollen wir uns jetzt der Reihe nach anschauen.

Zur Erstellung des Tutorials hatte ich hauptsächlich zwei Vorlagen, die ihr euch auch einmal anschauen solltet:

Der gesamte Gesichtserkennungscode stammt aus dem ersten Skript, Ideen für das OpenGL-Widget stammen aus dem Blogeintrag bei Intel.

Das Hauptprogramm

Das Skript startet zunächst mit einer Reihe von Imports und einigen globalen Variablen, die die Gesichtserkennung von OpenCV steuern. Zu diesen Variablen werde wir später noch kommen. Anschließend erzeugt die Funktion main() das Hauptfenster und startet die Hauptschleife der Qt-Awendung:

# -*- coding: utf-8 -*-

import sys
from PyQt4 import QtCore, QtGui, QtOpenGL
from OpenGL.GL import *
import cv

camera_index = 0
min_size = (20, 20)
image_scale = 2
haar_scale = 1.2
min_neighbors = 2
haar_flags = 0

def main(argv):
    app = QtGui.QApplication(argv)
    mainwindow = MainWindow()
    mainwindow.show()
    sys.exit(app.exec_())
    
# ... Code des Gesichtserkenners und des Hauptfensters

if __name__ == "__main__":
    main(sys.argv)

Wichtig ist vor allem zunächst die Variable camera_index, die den Index der anzusprechenden Webcam enthält. “0” ist normalerweise die erste Webcam, aber probiert ruhig auch andere Indizes, falls die Webcam nicht gestartet werden kann.

Das Hauptfenster wird in main() aus der Klasse MainWindow erzeugt, die vor der if-Anweisung am Ende des Skripts implementiert ist. Zunächst folgt aber nach main() der Code für den Gesichtserkenner.

Der Gesichtserkenner

Gesichter erkennt das Skript, indem es die Trainingsdaten in einen kaskadierten Klassifizierer lädt. Die Hintergründe beschreibt diese Seite des OpenCV-Wikis recht gut, wenn auch nur oberflächlich. Die Mathematik dahinter ist wohl etwas kompliziert, aber wir müssen sie für unsere Zwecke auch gar nicht verstehen. Der Klassifizierer wird im Konstruktor der Klasse FaceDetect erzeugt (self.cascade):

class FaceDetect:
    
    def __init__(self):
        self.cascade = cv.Load("haarcascade_frontalface_alt.xml")
        self.captureStarted = False
        self.frame_copy = None

Das Attribut self.frame_copy enthält jeweils das aktuelle Frame, in self.captureStarted speichern wird, ob die Webcam schon gestartet wurde. Nach dem Konstruktor folgen die beiden Methoden zum Starten und Stoppen des Capture-Vorgangs:

    def startCapture(self):
        self.capture = cv.CreateCameraCapture(camera_index)
        self.captureStarted = True

    def stopCapture(self):
        self.captureStarted = False

Die Webcam wird dabei vom OpenCV-Modul gestartet. Danach geht’s auch schon ans Eingemachte: die Methode frame() liest ein Bild von der Webcam, dreht das Bild gegebenenfalls und ruft die Methdoe zum Erkennen der Gesichter auf. Anschließend liefert es das Bild samt erkannten Gesicht zurück. Die Methode frame() wird später von der Hauptfenster-Klasse aufgerufen, um das aktuelle Bild zeichnen zu können:

    def frame(self):
        if self.captureStarted:
            frame = cv.QueryFrame(self.capture)
                
            if not self.frame_copy:
                self.frame_copy = cv.CreateImage((frame.width,frame.height),
                                                 cv.IPL_DEPTH_8U, frame.nChannels)

            if frame.origin == cv.IPL_ORIGIN_TL:
                cv.Copy(frame, self.frame_copy)
            else:
                cv.Flip(frame, self.frame_copy, 0)
                
            self.detectFaces()
            

        return self.frame_copy

Die Methode zur Erkennung der Gesichter heißt detectFaces() und greift auf eine Reihen von OpenCV-Funktionen zu:

    def detectFaces(self):
        # allocate temporary images
        gray = cv.CreateImage((self.frame_copy.width, self.frame_copy.height), 8, 1)
        small_img = cv.CreateImage((cv.Round(self.frame_copy.width / image_scale),
                                    cv.Round(self.frame_copy.height / image_scale)), 8, 1)
    
        # convert color input image to grayscale
        cv.CvtColor(self.frame_copy, gray, cv.CV_BGR2GRAY)
    
        # scale input image for faster processing
        cv.Resize(gray, small_img, cv.CV_INTER_LINEAR)
    
        cv.EqualizeHist(small_img, small_img)
        

        if(self.cascade):
            t = cv.GetTickCount()
            faces = cv.HaarDetectObjects(small_img, self.cascade, cv.CreateMemStorage(0),
                                         haar_scale, min_neighbors, haar_flags, min_size)
            t = cv.GetTickCount() - t
            print "detection time = %gms" % (t/(cv.GetTickFrequency()*1000.))
            if faces:
                for ((x, y, w, h), n) in faces:
                    # the input to cv.HaarDetectObjects was resized, so scale the 
                    # bounding box of each face and convert it to two CvPoints
                    pt1 = (int(x * image_scale), int(y * image_scale))
                    pt2 = (int((x + w) * image_scale), int((y + h) * image_scale))
                    cv.Rectangle(self.frame_copy, pt1, pt2, cv.RGB(255, 0, 0), 3, 8, 0)

Das Bild wird zunächst in Graustufen umgerechnet und verkleinert, um die Performance erträglich zu machen. Das kleine Bild wird schließlich an den Klassifizierer geschickt, das erledigt der Aufruf von HaarDetectObjects(). Danach wird ein rotes Rechteck um das erkannte Gesicht gezeichnet. Wir könnten natürlich das Rechteck auch später erst in unserem Hauptfenster in das OpenGL-Widget zeichen, dann müssten wir nur die Koordinaten der Gesichter speichern und später an das Hauptfenster liefern. Da aber auch OpenCV selbst Möglichkeiten zum Zeichen von Objekten in das Live-Bild bereitstellt, wollte ich diese heir gleich einmal vorstellen. Ihr könnt selbst einmal versuchen, das Rechteck später dann im Hauptfenster zeichnen zu lassen.

Das war’s auch schon mit der Gesichtserkenner-Klasse, mehr Code ist dafür nicht nötig. Was noch fehlt ist nun das Hauptfenster, in dem das Live-Bild erscheinen soll.

Das OpenGL-Widget

Als Hauptfenster verwenden wir einfach eine von QtOpenGL.QGLWidget abgeleitet Klasse aus PyQt. Jedes Qt-Widget kann ja im Prinzip als Hauptfenster dienen, und dieses Klass erlaubt es uns, OpenGL-Befehle in ihr zum Zeichen auszuführen. Der Kontruktor der Klasse erzeugt ein Objekt unserer FaceDetect-Klasse, startet die Webcam und deinen einen Timer, um regelmäßig Bilder auszulesen:

class MainWindow(QtOpenGL.QGLWidget):

    def __init__(self, *args):
        QtOpenGL.QGLWidget.__init__(self, *args)
        
        self.detector = FaceDetect()        
        self.detector.startCapture()
        
        self.frame = None
        self.angle = 0.0

        self.timer = QtCore.QTimer(self)
        self.timer.timeout.connect(self.updateWorld)
        self.timer.start(100)

Die Attribute self.frame bzw. self.angle werden hier initialisiert und enthalten später jeweils das aktuelle Webcam-Bild sowie die aktuelle Rotations unseres OpenGL-Overlay-Objektes. Der Rest der Klasse kümmert sich dann um das Auslesen des Bildes, die Initialisierung des GL-Kontextes sowie um das Zeichnen des aktuellen Bildes samt OpenGL-Dreieck:

    def updateWorld(self):
        self.frame = self.detector.frame()
        self.angle = self.angle + 5.0
        self.updateGL()
        
    def initializeGL(self):
        glClearColor(1.0, 1.0, 1.0, 1.0)        
        glClearDepth(1.0)
        glMatrixMode(GL_PROJECTION)

    def paintGL(self):
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glLoadIdentity()
        
        if self.frame != None:
            glDrawPixels(self.width(), self.height(), GL_RGB, GL_UNSIGNED_BYTE, self.frame.tostring()[::-1])

        glRotatef(self.angle, 0.0, 1.0, 0.0)

        glColor(0.1, 0.5, 0.8)
        glBegin(OpenGL.GL.GL_TRIANGLES)
        glVertex3f( 0.0, 0.5, 0.0) 
        glVertex3f(-0.5,-0.5, 0.0)
        glVertex3f( 0.5,-0.5, 0.0)
        glEnd()

    def resizeGL(self, width, height):
        glViewport(0, 0, width, height);
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()

Interessant ist hier noch die Funktion glDrawPixels() in paintGL(). Mit dieser wird das von FaceDetect gelieferte Bild in den GL-Kontext gemalt. Unter C(++) erhält wir das Bild von OpenCV als Pointer auf @char@s durch das Auslesen von imageData, diese Daten stehen aber unter Python nicht zur Verfügung. Stattdessen wandeln wir das Bild einfach in einen String und drehen diesen einmal komplett um (das erledigt die eckige Klammer [::-1]). Dadurch sind sowohl die Farben als auch das Bild in der richtigen Reihenfolge, so das wir die Daten als GL_RGB an glDrawPixels() übergeben können.

Das wars auch schon: ein Webcam-Bild, ein Gesicht und ein OpenGL-Dreieck, als Basis für Augmented-Reality-Awendungen mit Python! Natürlich kann man sich allerlei Ausbaustufen für das Skript vorstellen. Genannt wurde ja schon, dass FaceDetect eigentlich nur die Koordinaten der Gesichter zurückliefern müsste, und man alles Weitere im Hauptfenster machen kann. Versucht doch einmal, eurem Gesicht einen schönen OpenGL-Schnurrbart zu verpassen…