OpenCV mit Qt nutzen (Webcam, Gesichtserkennung...)

Code-Schnippsel, oder Tipps und Tricks, die einem beim Programmieren mit Qt helfen können.
Antworten
theprogrammer12
Beiträge: 38
Registriert: 12. August 2009 20:02

OpenCV mit Qt nutzen (Webcam, Gesichtserkennung...)

Beitrag von theprogrammer12 »

Hier werde ich euch jetzt zeigen, wie ihr in eurem Qt Programm mittels OpenCV eine Webcam benutzen könnt.
Als Erstes ein Überblick über die wichtigsten Typen und Funktionen von OpenCV:

typedef struct CvCapture:
Eine Struktur zur Videoerfassung. Entweder duch eine Webcam oder eine AVI-Datei

typedef struct IplImage:
Eine Struktur in der ein Bild gespeichert wird. Qt kennt dieses Format nicht, deshalb müssen wir es später in QImage konvertieren.

CvCapture* cvCaptureFromCAM(int index)
Diese Funktion startet den Stream einer Kamera. Sie gibt einen pointer auf ein CvCapture zurück.
Der erste Parameter ist die Nummer der zu benutzenden Kamera.

void cvReleaseCapture(CvCapture** capture)
schließt den Stream wieder.

IplImage* cvQueryFrame(CvCapture* capture)
Damit kann man ein Frame aus einem Stream auslesen.

Das waren alle wichtigen Funktionen.
Dann lasst uns mal anfangen!

Zum kompilieren müsst ihr euch erstmal OpenCV herunterladen: http://opencv.willowgarage.com/wiki/

Als Erstes brauchen wir ein MainWindow (bei mir "MainWin") mit einem GraphicsView ("ViewBox").
Das ist dann die mainwin.h:

Code: Alles auswählen

#ifndef MAINWIN_H
#define MAINWIN_H

#include "ui_mainwin.h"
#include <cv.h>
#include <highgui.h>

class MainWin : public QMainWindow, private Ui::MainWin
{
    Q_OBJECT

private:
    CvCapture *source; // Der Stream

public:
    MainWin(QWidget *parent = 0); // Konstruktor
    ~MainWin(void); // Destruktor

protected:
    void timerEvent(QTimerEvent*); // Timer-Funktion zum Frames-auslesen und anzeigen
};

#endif
In der mainwin.cpp schreiben wir jetzt die Initialisierungsanweisungen in den Konstruktor:

Code: Alles auswählen

#include <QtGui>
#include <stdio.h>
#include "imgconvert.h"
#include "mainwin.h"

MainWin::MainWin(QWidget *parent)
{
    setupUi(this);

    source = cvCaptureFromCAM(0);

    startTimer(30);
}
Und im Destruktor wird der Stream wieder geschlossen:

Code: Alles auswählen

MainWin::~MainWin(void)
{
    cvReleaseCapture(&source);
}
Jetzt wirds intreressant: Im Timer werden die Frames ausgelesen und angezeigt:

Code: Alles auswählen

void MainWin::timerEvent(QTimerEvent*)
{
    IplImage *image; // Eingelesenes Bild im OpenCV-Format
    QGraphicsScene *scene = new QGraphicsScene(); // Grafikszene

    scene->setBackgroundBrush(QColor(0, 0, 0)); // Schwarzer Hintergrund
    image = cvQueryFrame(source); // Frame auslesen
    scene->addPixmap(QPixmap::fromImage(MirrorImage(ConvertImage(image), H_MIRROR)));  // Bild in QImage konvertieren, auf den Kopf stellen und anzeigen
    ViewBox->setScene(scene); // Szene anzeigen
}
Vielleicht sind euch schon die Funktionen ConvertImage und MirrorImage aufgefallen.
Diese Funktionen müssen wir erstmal schreiben:

imgconvert.h:

Code: Alles auswählen

#ifndef IMGCONVERT_H
#define IMGCONVERT_H

#include <cv.h>
#include <QPixmap>
#include <QImage>

#define V_MIRROR 1
#define H_MIRROR 2

QImage MirrorImage(const QImage source, int side);
QImage ConvertImage(IplImage *source);

#end
imgconvert.cpp:

Code: Alles auswählen

#include "imgconvert.h"

QImage MirrorImage(QImage source, int side)
{
    int x, y;
    QImage ret;
    
    ret = QImage(source); //source.width(), source.height(), source.format());
    for(x=0; x<source.width(); x++)
    {
        for(y=0; y<source.height(); y++)
        {
            ret.setPixel(((side & V_MIRROR) ? source.width() - (x + 1) : x), ((side & H_MIRROR) ? source.height() - (y + 1) : y), source.pixel(x, y));
        }
    }

    return ret;
}

QImage ConvertImage(IplImage *source)
{
    int cvIndex, cvLineStart;
    QImage ret;

    if(source->depth != IPL_DEPTH_8U || source->nChannels != 3)
        return ret;

    QImage temp(source->width, source->height, QImage::Format_RGB32);
    ret = temp;
    cvIndex = 0;
    cvLineStart = 0;
    for (int y = 0; y < source->height; y++)
    {
        unsigned char red,green,blue;
        cvIndex = cvLineStart;
        for (int x = 0; x < source->width; x++)
        {
            red = source->imageData[cvIndex+2];
            green = source->imageData[cvIndex+1];
            blue = source->imageData[cvIndex+0];

            ret.setPixel(x,y,qRgb(red, green, blue));
            cvIndex += 3;
        }
        cvLineStart += source->widthStep;
    }

    return ret;
}
Auf diese Funktionen werde ich jetzt nicht näher eingehen.

Jetzt müssen wir noch die *.pro Datei editieren, damit das mit dem CV hinhaut:

Code: Alles auswählen

INCLUDEPATH += "D:\Programme\OpenCV\cxcore\include" \
    "D:\Programme\OpenCV\cv\include" \
    "D:\Programme\OpenCV\cvaux\include" \
    "D:\Programme\OpenCV\otherlibs\highgui"
win32:LIBS += -L"D:\Programme\OpenCV\lib"
LIBS += -lcv \
    -lhighgui
FORMS += mainwin.ui
SOURCES += mainwin.cpp \
    main.cpp \
    imgconvert.cpp
HEADERS += mainwin.h \
    imgconvert.h

Und das sollte es eigentlich schon gewesen sein!
Bild

Bitte gebt mir Feedback!

Das mit dem OpenCV brauchte ich für einen Webcam-Stream Server (von American Pie inspiriert :wink: ):
http://www.metallic-entertainment.com/?page=webcam_app
Dateianhänge
qtcv.zip
(3.62 KiB) 1267-mal heruntergeladen
Zuletzt geändert von theprogrammer12 am 21. Juli 2010 15:56, insgesamt 2-mal geändert.
androphinx
Beiträge: 170
Registriert: 26. Januar 2009 09:19
Wohnort: 127.0.0.2

Beitrag von androphinx »

Hallo theprogrammer12,
so was habe ich schon lange gesucht. Hatte keine Ahnung, wie ich eine Webcam angezapft kriege. Ich habe allerdings noch ein paar Fragen:
1. Angenommen, ich habe mehr als eine Videoquelle am Rechner. Wie kann ich auswählen,. welche ich brauche?
2. Warum musst du das Bild auf den Kopf stellen???
theprogrammer12
Beiträge: 38
Registriert: 12. August 2009 20:02

Beitrag von theprogrammer12 »

androphinx hat geschrieben:1. Angenommen, ich habe mehr als eine Videoquelle am Rechner. Wie kann ich auswählen,. welche ich brauche?
Die Nummer der Cam wird bei cvCaptureFromCAM(0) als Parameter mitgegeben. Wie man herauskriegt, welche Kamera welche Nummer hat, hab ich noch nicht herausgefunden. Ich glaub das geht mit OpenCV auch gar nicht.
androphinx hat geschrieben:2. Warum musst du das Bild auf den Kopf stellen???
Ich weiss nicht warum, aber bei OpenCV war bei mir das Bild immer verkehrt herum. Liegt wahrscheinlich an dem Format IplImage.
gelignite
Beiträge: 37
Registriert: 6. Dezember 2007 21:23
Kontaktdaten:

Beitrag von gelignite »

Hallo theprogrammer12,

es funktioniert. :D

Ich musste das Bild übrigens auch nicht drehen. Steht deine Kamera vielleicht auf dem Kopf?

Gruß,
gelignite
{brigens ist ein Kezboard/Treiber v;llig [berfl[ssig!
Curtis Newton
Beiträge: 122
Registriert: 11. Juni 2008 18:39

Beitrag von Curtis Newton »

Nur eine kleine Anmerkung: Man sollte auf setPixel verzichten, wenn man über das Bild iterieren will. Die ganzen in setPixel durchgeführten Abfragen sind unnötig. Besser ist, mittels scanLine immer auf den Anfang der Bildzeile zuzugreifen und die nötigen Konvertierungen selber zu machen.

C.
solarix
Beiträge: 1133
Registriert: 7. Juni 2007 19:25

Beitrag von solarix »

Ich weiss nicht warum, aber bei OpenCV war bei mir das Bild immer verkehrt herum. Liegt wahrscheinlich an dem Format IplImage.
Ich musste das Bild übrigens auch nicht drehen. Steht deine Kamera vielleicht auf dem Kopf?
Irgendwo bin ich mal über einen Artikel dazu gestolpert (ich glaube da wurden Linux-Treiber für Webcams getestet). Die WebCam-Hersteller basteln hin und wieder ziemlich heftig. Da kann es durchaus vorkommen, dass das Kameramodul verkehrt herum eingebaut (entweder aus Platzgründen oder eines Fabrikationsfehlers) und danach mit dem Software-Treiber nachgebessert wird.

Gute WebCam-Applikationen haben deswegen meist ein "invert"-Flag (sowohl horizontal als auch vertikal), so dass derartige Basteleien je nach Kamera per Software geradegebogen werden können.

hth...
gelignite
Beiträge: 37
Registriert: 6. Dezember 2007 21:23
Kontaktdaten:

Beitrag von gelignite »

Hallo,
Curtis Newton hat geschrieben:Nur eine kleine Anmerkung: Man sollte auf setPixel verzichten, wenn man über das Bild iterieren will. Die ganzen in setPixel durchgeführten Abfragen sind unnötig. Besser ist, mittels scanLine immer auf den Anfang der Bildzeile zuzugreifen und die nötigen Konvertierungen selber zu machen.

C.
folgendes geht auch:

Code: Alles auswählen

QImage convertImage( IplImage *source )
{
    QImage dummy;

    if ( source->depth != IPL_DEPTH_8U || source->nChannels != 3 )
        return dummy;

    // Bilddaten übernehmen
    unsigned char * data = ( unsigned char * ) source->imageData;

    // QImage mit Originaldaten erstellen
    QImage ret( data, source->width, source->height, QImage::Format_RGB888 );

    // Kanäle (BGR -> RGB) und Format (RGB888 -> RGB32) ändern
    return ret.rgbSwapped().convertToFormat( QImage::Format_RGB32 );
}
Scheint mir eine Spur schneller und übersichtlicher zu sein.

Gruß,
gelignite
{brigens ist ein Kezboard/Treiber v;llig [berfl[ssig!
Curtis Newton
Beiträge: 122
Registriert: 11. Juni 2008 18:39

Beitrag von Curtis Newton »

Richtig, sieht noch besser aus! Ich würde aber widthStep von IpLImage benutzen und den entsprechenden QImage-Ctor nehmen:

QImage::QImage ( uchar * data, int width, int height, int bytesPerLine, Format format )

C.
gelignite
Beiträge: 37
Registriert: 6. Dezember 2007 21:23
Kontaktdaten:

Re: OpenCV mit Qt nutzen (Webcam, Gesichtserkennung...)

Beitrag von gelignite »

Hallo,

ich beschäftige mich seit wenigen Tagen intensiver mit dieser Geschichte, also das Auslesen der Webcam, Auswerten und Anzeigen der Bilder. Und dieses "Tutorial" war ein recht netter Einstieg, nachdem ich auch andere Wege probiert habe. Nun fallen mir hier so manche Dinge auf, die man ein klein wenig anders machen könnte/sollte. Auf die convertImage() Funktion bin ich ja schon kurz eingegangen.

@Curtis Newton: Ist das dann nur ein minimal anderer Aufruf oder erspart das noch eine der erwähnten Funktionen? Beim Stöbern durch die OpenCV Doku ist mir dann auch noch cvCvtColor() ins Auge gefallen, womit man rgbSwapped() einsparen kann. Allerdings braucht man dann wieder ein zweites IplImage. :?

--------------------

Jetzt geht es insbesondere um folgenden Code-Schnippsel:
theprogrammer12 hat geschrieben:[...]

Code: Alles auswählen

void MainWin::timerEvent(QTimerEvent*)
{
    IplImage *image; // Eingelesenes Bild im OpenCV-Format
    QGraphicsScene *scene = new QGraphicsScene(); // Grafikszene

    scene->setBackgroundBrush(QColor(0, 0, 0)); // Schwarzer Hintergrund
    image = cvQueryFrame(source); // Frame auslesen
    scene->addPixmap(QPixmap::fromImage(MirrorImage(ConvertImage(image), H_MIRROR)));  // Bild in QImage konvertieren, auf den Kopf stellen und anzeigen
    ViewBox->setScene(scene); // Szene anzeigen
}
[...]
Hier wird z. B. in Zeile 4 per "new QGraphicsScene()" mit jedem Aufruf von timerEvent() Speicher für eine neue Szene reserviert. Und das passiert - dem Aufruf "startTimer( 30 )" zufolge - rund alle 30 ms. Da kommt am Ende ganz schön was zusammen. Vor allem Speicherlecks, da der reservierte Speicher nirgends wieder freigegeben wird.

Auch die Aufrufe scene->setBackgroundBrush(), scene->addPixmap() und ViewBox->setScene() werden viel zu oft ausgeführt. Das sind Aufrufe, die in den Konstruktor gehören. (Von addPixmap() einmal abgesehen, aber das lässt sich auch anders anstellen.) Bei dem immer wiederkehrenden Aufruf von addPixmap passiert noch eine Kleinigkeit, die böse Folgen hat. Es wird hier nicht das angezeigte Bild ersetzt, sondern immer das jeweils neue hinzugefügt. (Naja, in diesem Fall fügt man sogar jeweils ein neues Bild einer neuen Grafikszene hinzu.) Am Ende hat man also einen Berg von Bildern der Szenerie hinzugefügt und irgendwann ist kein Arbeitsspeicher mehr da, um weitere Bilder aufzunehmen.


Um das etwas aufzuräumen, wäre mein Vorschlag dieser:
In die mainwin.h kommen zusätzlich diese Attribute:

Code: Alles auswählen

private:
    // Zeiger auf ein Platzhalterobjekt für das spätere QPixmap
    QGraphicsPixmapItem * _gpi;
    // Zeiger auf jenes QPixmap (Ließe sich prinzipiell auch einsparen, wenn in der .cpp der Rückgabewert von QPixmap::fromImage() direkt an _gpi->setPixmap() übergeben würde.)
    QPixmap * _pixmap;
    // Zeiger auf die Grafikszene
    QGraphicsScene * _scene;
In der mainwin.cpp wird nun im Konstruktor die Grafikszene initialisiert und das Platzhalterobjekt für das QPixmap hinzugefügt.

Code: Alles auswählen

MainWin::MainWin(QWidget *parent)
{
    setupUi(this);

    source = cvCaptureFromCAM(0);

    startTimer(30);


    // Initialisieren der Grafikszenerie und setzen der Eigenschaften (z. B. schwarzer Hintergrund)
    _scene = new QGraphicsScene( viewBox );
    _scene->setBackgroundBrush( QColor( 0, 0, 0 ) );

    // Initialisieren des Platzhalters und hinzufügen zur Grafikszene
    _gpi = new QGraphicsPixmapItem();
    _scene->addItem( _gpi );

} 
So hat das MainWin nur eine QGraphicsView (viewBox) mit genau einer QGraphicsScene (_scene), welches immer nur ein Bild (_gpi) anzeigt. Das Bild muss nun nur noch adäquat erneuert werden. Die veränderte Methode timerEvent() schaut dann wie folgt aus:

Code: Alles auswählen

void MainWin::timerEvent( QTimerEvent * )
{
    // Eingelesenes Bild im OpenCV-Format
    IplImage * image = cvQueryFrame( _source );

    // Konvertieren des Bildes vom IplImage zum QPixmap
    *_pixmap = QPixmap::fromImage( convertImage( image ) );

    // Pixmap setzen - Das Ändern löst intern eine Aktualisierung der Oberfläche aus wodurch das jeweils neue Bild angezeigt wird
    _gpi->setPixmap( *_pixmap );
}
Letzte Baustelle ist dann noch der Destruktor. _gpi, _scene und _pixmap belegen noch Speicher. Dieser wird in aller Regel über die parent-Widgets freigegeben. Es dürfte aber nicht Schaden, wenn man sichergeht und dies im Destruktor noch einmal überprüft.

Gruß,
gelignite
{brigens ist ein Kezboard/Treiber v;llig [berfl[ssig!
theprogrammer12
Beiträge: 38
Registriert: 12. August 2009 20:02

Beitrag von theprogrammer12 »

ok. Ich hab eine Logitech Quickcam 9000. Baut Logitech wirklich chips verkehrt herum ein?

@gelignite
Danke für die Tipps! Wenn ich mal Zeit hab werde ich das Tutorial ein bisschen abändern!
mustermann.klaus
Beiträge: 23
Registriert: 6. April 2009 12:21
Wohnort: Berlin

Beitrag von mustermann.klaus »

Meiner Meinung nach schreiben einige Klassen von oben rechts nach unten links und einige von unten links nach rechts oben. z.B. bei openGL versus D3D ist das zu beobachten.
Weiterhin habe ich es so gemacht, dass das Ipl-Image und das QImage einen "shared-memory" benutzen. Dann kann man sich das ganze Kopieren sparen. Dazu habe ich beim initialisieren des QImage den ->Data´- member als *(unsigned char) gecastet und dann auf den ->bits - member des Ipl-Image zeigen lassen habe. Dazu muss das QImage 24bit also ...888 sein.
Hab jetzt gerade das snippet nicht zur HAnd. Werde es nachreichen.
mustermann.klaus
Beiträge: 23
Registriert: 6. April 2009 12:21
Wohnort: Berlin

Beitrag von mustermann.klaus »

ohh, hat ja gelignite schon gezeigt, ...naja

Dann sei noch gesagt, dass ich den Speicher durch einen Mutex schütze während des Zugriffs.
Neo E
Beiträge: 22
Registriert: 23. Juli 2008 17:32

Beitrag von Neo E »

@androphinx
1. Angenommen, ich habe mehr als eine Videoquelle am Rechner. Wie kann ich auswählen,. welche ich brauche?
Du kannst dir den Namen eines Videogerätes, das an den Rechner angeschlossen ist von OpenCV geben lassen:

Code: Alles auswählen

int mActualCam;
string mCamName;

CameraDescription camDesc;
			
cvcamGetProperty(mActualCam, CVCAM_DESCRIPTION, &camDesc);
			
mCamName = camDesc.DeviceDescription;
Wenn du die Geräte dann z.B. in einer Liste organisierst, dann kannste die Kamerainitalisierung mit der Nummer des Eintrags in der Liste durchführen.
theprogrammer12
Beiträge: 38
Registriert: 12. August 2009 20:02

Beitrag von theprogrammer12 »

Ein kleines Beispiel wie ich das ganze in der Praxis eingesetzt habe:
http://www.metallic-entertainment.com/?page=webcam_app
Antworten