Seite 1 von 1

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

Verfasst: 15. August 2009 20:07
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

Verfasst: 17. August 2009 10:56
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???

Verfasst: 17. August 2009 13:50
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.

Verfasst: 7. September 2009 12:27
von gelignite
Hallo theprogrammer12,

es funktioniert. :D

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

Gruß,
gelignite

Verfasst: 7. September 2009 13:19
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.

Verfasst: 7. September 2009 13:31
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...

Verfasst: 10. September 2009 12:34
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

Verfasst: 10. September 2009 13:38
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.

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

Verfasst: 11. September 2009 02:54
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

Verfasst: 10. November 2009 19:49
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!

Verfasst: 22. Februar 2010 11:26
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.

Verfasst: 22. Februar 2010 12:12
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.

Verfasst: 6. April 2010 14:23
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.

Verfasst: 21. Juli 2010 15:57
von theprogrammer12
Ein kleines Beispiel wie ich das ganze in der Praxis eingesetzt habe:
http://www.metallic-entertainment.com/?page=webcam_app