Ein Projekt in meinem Studium war für das Fach Bildverarbeitung ein Programm zu erstellen mit dem automatisch Objekte in Bildern erkannt werden können. Dieses Projekt stelle ich nun, bis auf die Präsentationsfolien, online.
Die technische Umsetzung stand mir frei und somit habe ich mich für OpenCV, C++ und Code::Blocks entschieden. Zunächst gab es Probleme mit der aktuellen Version 2.4, mit einem Downgrade auf die zwei Jahre alte Version 2.2 ließ es sich aber in mein Projekt integrieren - OpenCV sollte übrigens in den Windows-Suchpfad aufgenommen werden.
Außerdem möchte ich darauf hinweisen dass nach der Installation von OpenCV der Rechner zwingend neu gestartet werden muss obwohl nicht explizit darauf hingewiesen wird.
Integration von OpenCV in das Projekt
Das Projekt hat sich auch gut dafür geeignet meine C++-Kenntnisse wieder aufzufrischen. Die Integration in die Entwicklungsumgebung Code::Blocks sah folgendermaßen aus:
1. Unter Project/Build Options sowohl für die Debug- als auch die Release-Version unter Search directories/Compiler das Verzeichnis include von OpenCV einbinden und im selben Menü unter Linker Settings alle *.lib-Dateien einfügen. Als Compiler sollte der GNU GCC Compiler voreingestellt bleiben.
2. In das eigene Projekt müssen die Includes
#include <opencv/cv.h>
#include <opencv/highgui.h>
eingefügt werden.
3. Bei den ganzen Header-Dateien von OpenCV müssen anschließend in den Includes die Pfade angepasst werden, denn im OpenCV-Verzeichnis include gibt es zwei Unterverzeichnisse, opencv und opencv2. In der Datei cv.h muss beispielsweise die Zeile
#include "core/core_c.h"
durch
#include "opencv2/core/core_c.h"
ersetzt werden. Einfach immer weiter versuchen zu kompilieren bis keine Fehler mehr gemeldet werden. Für so etwas benutze ich immer gerne die Everything search engine, die ich auf einen Tastatur-Shortcut bei mir gelegt habe.
Leider lässt Code::Blocks doch so einige Komfortsfunktionen von Eclipse vermissen, die Refactoring-Methoden zur Umbenennung von Variablen habe ich beispielsweise schmerzlich vermisst. Mit Eclipse lässt sich auch in C++ programmieren; vermutlich werde ich es später nicht nur für Java, sondern auch für C++ verwenden.
Kantenerkennung mittels OpenCV
Danach geht es ans Eingemachte, für die ersten Schritte empfehle ich das Tutorial Bildverarbeitung mit OpenCV von Johannes Wienke.
Ein möglicher Ansatz für die Objekterkennung ist die Nutzung des Canny-Algorithmus von OpenCV, der die Kanten in einem Bild erkennt. Der Algorithmus benötigt aber als Eingabe ein Bild, das lediglich einen Farbkanal enthält. Das Ausgangsbild muss somit erst geladen und gesplittet werden:
IplImage* img;
img = cvLoadImage (file);
IplImage* binaryImage = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
cvSplit (img, binaryImage, NULL , NULL , NULL );
Danach sollte man unterscheiden ob man 1. mit realen Bildern oder 2. mit abstrakten Bildern arbeitet. Für den ersten Fall ist es zu empfehlen erst Filter wie Weichzeichner anzuwenden, um die Kantenerkennungsleistung zu verbessern – für abstrakte Bilder ist das aber kontraproduktiv und sollte unterlassen werden.
Für reale Bilder (Fotos) sollte zunächst eine Normalisierung erfolgen (Anpassung der Kontrast- und Helligkeitswerte) und danach ein Weichzeichner wie Gauss angewandt werden:
IplImage* normalized = cvCreateImage (cvGetSize (img), img ->depth , 1);
cvEqualizeHist ( binaryImage , normalized );
IplImage* gauss = cvCreateImage ( cvGetSize (img), img->depth , 1);
cvSmooth ( normalized , gauss , CV_GAUSSIAN , 13, 13);
Danach kann man schlussendlich den Canny-Algorithmus auf dem Singlechannel-Bild anwenden:
IplImage* cannyimg = cvCreateImage ( cvGetSize (img/gauss), img/gauss ->depth , 1);
cvCanny(img/gauss, cannyimg, 40, 130);
Um die Templates (Vergleichsbilder) zu erstellen habe ich das Ergebnis des Canny-Effekts in Paint kopiert, das zu erkennende Objekt separiert (ausgeschnitten), in ein neues Bild kopiert und dort alles entfernt, was nicht zum Objekt gehört.
Funktionsweise meiner Objekterkennungsroutinen
Für die Objekterkennung nutze ich die S/W-Bilder die von Canny erzeugt wurden und vergleiche sie mit den Templates. Meine Algorithmen nutzen dazu Offsets, damit kleinere Bilder (die Templates) in Größeren gefunden werden können. Das Template wird dazu quasi über das Ausgangsbild gelegt und solange verschoben bis es gefunden oder das Ende erreicht wurde.
Ein wichtiger Schritt der Algorithmen ist es nur weiße Pixel vom Template im Originalbild zu suchen – das heißt alle weißen Pixel vom Template müssen im Originalbild sein, aber es dürfen zusätzliche weiße Pixel im Ausgangsbild sein die umgekehrt nicht im Template vorhanden sein müssen. Es ist also keine hundertprozentige Übereinstimmung für die Objekterkennung notwendig.
Für mein Projekt habe ich als Ausgangsbasis Bilder von Verkehrsschildern genommen, die ich über die Google Bildersuche gefunden habe. Wie es sich herausstellte war es gar nicht so einfach geeignete Bilder zu finden - die meisten Bilder waren zu klein, die Verkehrsschilder zu weit entfernt oder sie wurden von irgendetwas überlagert so dass sie nicht vollständig zu erkennen waren.
Aus rechtlichen Gründen verzichte ich an dieser Stelle darauf Bilder von anderen auf meiner Seite zu posten, möchte aber darauf hinweisen dass sich z. B. das Schilderwald-Foto von Martin Vogel aus Dortmund als Vorlage eignet.
Wie es sich herausstellte war meine erste Funktion zur Objekterkennung in Bildern (matchingRecognition1) wegen der Offsetverschiebung sehr, sehr langsam. Ich habe daraufhin nach Auswegen gesucht um sie zu beschleunigen.
Meine erste Idee zur Beschleunigung war nicht immer alle schwarzen und weißen Pixel vom Template mit dem Ausgangsbild zu vergleichen, sondern lediglich die weißen Pixel auf die es ankommt. Da ich das Speicherformat nicht verändern wollte bestand die Lösung darin in einem Präprozessing zunächst alle weißen Pixel zu zählen, danach ein dynamisches Array entsprechend zu dimensionieren und die weißen Pixel in einem neuen Durchlauf in das Array einzulesen.
An der Stelle lässt C++ leider dynamische Strukturen wie ArrayListen aus Java/C# vermissen, so dass dieser zweite Durchlauf nötig ist weil man vorher nicht weiß wieviele weiße Pixel im Bild sind um das Array zu dimensionieren. Aber dieser zweite Durchlauf kostet kaum Zeit.
Dieses Verfahren habe ich in der zweiten Variante meines Algorithmus verwendet und sie zusätzlich implementiert – matchingRecognition2. Das war mir aber immer noch zu langsam und ich kam auf die Idee auch das Ausgangsbild vorzuverarbeiten.
Dabei geht es im Kern darum auf die immer wiederkehrende Nutzung der Funktion cvGetReal2D zu verzichten, dazu lese ich in einem weiteren Präprozessing alle Farbwerte des Originalbildes in ein dynamisches zweidimensionales Array ein. In matchingRecognition3 kann ich dann direkt auf das Array zugreifen.
cvGet2D war übrigens zum Auslesen nicht zu empfehlen und stürzte immer wieder ab, erst cvGetReal2D funktionierte stabil.
Konfiguration der Objekterkennung
Ich habe diverse Konfigurationsoptionen über defines eingebaut, um das Programm flexibel zu halten aber auch um es weiter zu beschleunigen. Diese defines sind u. a.:
#define MODUS CV_RETR_LIST
Erkennungsmodus für die Canny-Funktion. Mit CV_RETR_EXTERNAL werden nur äußere Konturen erkannt, mit CV_RETR_LIST werden auch innere eingelesen. Sollte darauf voreingestellt bleiben womit die Templates erstellt wurden.
#define WAITTIME 50000
Wartezeit nach jedem Schritt in Millisekunden. Mein Programm zeigt zunächst gleichzeitig das Ausgangsbild und das Ergebnis der Canny-Funktion an. Nach einer gewissen Wartezeit oder nachdem die Fenster manuell geschlossen wurden geht es weiter.
#define SKIPFOLLOWINGBYMISMATCHNR 50
Die Funktion matchingRecognition3 kann soweit konfiguriert werden dass sie bei Mismatches (Nicht-Übereinstimmungen der Pixel) sofort oder nach einiger Zeit abbricht oder immer komplett durchläuft. Die Anzahl Pixel-Matches wird angezeigt. 0 bedeutet Fehler zu ignorieren.
Dann die Schwellwerte ab wann ein Objekt als erkannt gilt:
#define PIXELTHRESHOLD 100
#define DIFFERENCINGROWS 10
#define DIFFERENCINGCOLS 10
PIXELTHRESHOLD gibt an ab wievielen Pixeln Übereinstimmung ein Objekt als erkannt gilt. Bei 0 werden die Pixel-Matches für jedes Template einzeln angezeigt.
Das Programm bestimmt auch Start- und End-Koordinaten für jedes erkannte Objekt. DIFFERENCINGROWS und DIFFERENCINGCOLS geben an wieviel Differenz zwischen End- und Start-Koordinaten bestehen muss um eine Übereinstimmung zu melden. Wenn die Pixel-Matches in lediglich einer Pixel-Zeile stehen wäre das nicht sonderlich aussagekräftig.
Mit matchingRecognition3 und SKIPFOLLOWINGBYMISMATCHNR=50 ist das Programm bereits sehr schnell, auf meinem Testrechner brauche ich damit für die Suche eines kleinen Templates (39x72 Pixel) in einem 1800x1922 Pixel großen Ausgangsbild höchstens 1,9 Sekunden.
Wenn man SKIPFOLLOWINGBYMISMATCHNR auf 1 setzen würde geht es noch schneller, aber er sollte nicht beim ersten Mismatch abbrechen.
Weitere Objekterkennungsfunktionen von OpenCV
OpenCV bietet direkte Funktionen zur Erkennung von Linien oder Kreisen an, für Kreise ist dies cvHoughCircles. Diese Funktion habe ich anhand von abstrakten Bildern in der Funktion findPrimitiveObjects getestet.
Unter abstrakten Bildern verstehe ich einfache selbstgezeichnete primitive Objekte wie Ovalen, Kreise oder Rechtecke auf schwarzem oder weißem Hintergrund – demnach ideale Ausgangsbedingungen für cvHoughCircles, denn in Fotos würde die Erkennung natürlich schlechter funktionieren. Entsprechend habe ich auch keine Weichzeichner wie Gauss vorgeschaltet.
Mit meinen beiden schwarzen und weißen Ausgangsbildern findet er den jeweiligen Kreis auch korrekt und zeichnet ihn in ein neues Bild korrekt ein, aber der erkannte Radius ist nicht ganz korrekt. Ich habe mit den Parametern herumexperimentiert aber ein besseres Ergebnis ließ sich nicht erzielen.
Ich habe hierbei noch den Trick benutzt als Parameter von cvHoughCircles bei der Option minDist die gesamte Bildbreite anzugeben. Dieser Parameter gibt an wie groß die Distanz zwischen den Mittelpunkten von Kreisen mindestens sein muss um weitere Kreise zu finden. Mit Angabe der gesamten Bildbreite findet er natürlich nur einen einzigen Kreis im Bild, weil das Bild mehr breit als hoch ist.
Ohne diesen Trick funktioniert die Erkennung von Kreisen aber mehr schlecht als recht. Bei Bildbreite/3 findet er im schwarzen Bild bereits zwei Kreise, aber es ist nur eines vorhanden. Bei Bildbreite/10 (demnach 80 Pixeln) sind es bereits drei erkannte Kreise im schwarzen Bild, im weißen Bild dagegen weiterhin nur ein erkannter Kreis.
Das schwarze Bild enthält bei mir einen Kreis, ein Oval, ein Quadrat, ein Rechteck mit eckigen Kanten und ein weiteres mit runden Kanten. Das weiße Bild enthält einen Kreis, ein Quadrat, ein Rechteck mit eckigen Kanten und ein weiteres mit runden Kanten. Demnach wie gesagt optimale Voraussetzungen, nur die Erkennung von OpenCV funktioniert einfach nicht hundertprozentig.
Dieses Blog bestätigt mir meine Erkenntnisse, denn er hat es auch nicht besser hinbekommen. Er schreibt “cvHoughCircles() is a very inefficient method!”.
Beim ersten Lesen hatte ich inefficient zunächst mit langsam übersetzt, aber er meint dass der Algorithmus von OpenCV einfach schlechte Ergebnisse liefert. Er hatte versucht in einem Bild mit einem Auge eine Pupille zu erkennen, und selbst mit vorgeschalteten Filtern erkannte er Kreise wo überhaupt keine sind – die Pupille hat OpenCV AUCH erkannt (mit Einsatz von Filtern), aber eben nicht nur.
Wenn man OpenCV aber dazu bringen kann zumindest AUCH das Zielobjekt zu erkennen kann sich ein Einsatz dennoch lohnen. Erfahrungsgemäß sind eigene Implementierungen immer langsamer als vom System bereitgestellte Routinen.
Als Beispiel könnte ich hier meine Binärbaum-Programme nennen; ich wollte einmalige Zufallszahlen generieren und habe das zunächst mit einer einfachen Schleife erreicht – diese war jedoch sehr langsam. Als ich auf die HashSets von Java zugriff welche die Eigenschaft haben keine doppelten Einträge aufnehmen zu können wurde die Routine direkt hundertmal schneller. Aber das nur am Rande.
Leider musste ich feststellen dass auch die cvFindContours-Funktion von OpenCV eher schlechte Ergebnisse erzielt. Mein Programm gibt auch die Koordinaten erkannter Konturen aus, leider sind diese Ergebnisse nur teilweise nachvollziehbar und unvollständig. Änderung der Parameter brachte keine nennenswerte Verbesserung, ich werte diese Ergebnisse aber eh nicht weiter aus im Programm.
Ausbau der Kantenerkennung
OpenCV bietet auch Approximations-Methoden an, um beispielsweise fehlende Konturen zu berechnen und somit bessere Templates zu erstellen. Dazu möchte ich auf die ApproxPoly-Funktion verweisen.
Im Tutorial von Johannes Wienke wird in Kapitel 3.1.3 unter Morphologische Operationen ein Verfahren namens Closing genannt um Lücken zu schließen, dazu verwendet er die Funktion cvMorphologyEx.
Das wären erste Möglichkeiten um die Erkennung noch weiter auszubauen.
Zusatzbibliotheken für OpenCV
Es gibt diverse Zusatzbibliotheken für OpenCV, die anstatt der cvFindContours-Funktion benutzt werden können und weiter spezialisiert sind. Als erstes möchte ich hier cvBlobsLib nennen und als zweites cvBlob. Ich hatte zunächst versucht diese Zusatzbibliotheken zu verwenden, daher hier meine ersten Erkenntnisse dazu.
Bei der erstgenannten Bibliothek muss man Microsoft Visual C++6 aufwärts verwenden und benötigt zusätzlich die MFC-Headerdateien von Microsoft. Nach Einbindung der MFC-Dateien meldete der gcc-Compiler von Code::Blocks Fehler bei den Typedefs; gcc ist nicht kompatibel zu dieser mit MSVC++6 erstellten Bibliothek.
Bei der zweiten Bibliothek gibt es nur eine Integrationsanleitung für Linux, nicht für Windows. Es wird darauf verwiesen unter Linux cmake zu verwenden.
Beide Bibliotheken liefern im Gegensatz zu OpenCV selbst nicht die benötigten *.lib oder *.a-Dateien für den Linker mit, diese muss man selbst generieren. Mein Versuch mittels Code::Blocks bei cvBlob die nötigen Dateien für den Linker (Typ statische Bibliothek) zu erstellen scheiterte leider.
Weitere Referenzen / Links
Ein guter erster Einstieg ist wie gesagt Bildverarbeitung mit OpenCV von Johannes Wienke. Weiterhin möchte ich diese Anleitung Introduction to programming with OpenCV verlinken.
Zur Benutzung der cvFindContours-Funktion möchte ich diese beiden Referenzen gegenüberstellen: 1 2.
Update 30.10.2012: Ich habe die älteren matchingRecognition-Funktionen entfernt, die wurden ja nicht mehr genutzt und waren nur noch historisch interessant.
OpenCV hat auch eine integrierte matchingTemplate-Funktion, die will ich beizeiten auch testen und gegenüberstellen.
MatchingTemplate-Funktionen sind mehr für 2D-Objekte geeignet, für 3D wird man sich mehr mit Mathematik und speziell Vektorrechnung beschäftigen müssen.
/*******************************************************
Bildverarbeitungsprojekt v1.0 von Thomas Kramer
"Lage, Form und Orientierung von mehreren Objekten
im Blickfeld erkennen können und die Daten
darstellen"
********************************************************/
/* Includes */
#include <opencv/cv.h>
#include <opencv/highgui.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <math.h>
#include <dirent.h>
/* Namensräume */
using namespace std;
using namespace cv;
/* defines */
#define TERMINATEPROGRAM 0
#define ANALYSEREALPICTURE 1
#define ANALYSEABSTRACTPICTURE 2
#define FINDOBJECTS 3
#define FINDPRIMITIVEOBJECTS 4
#define OPTIONSCOUNT 5
/* Kantenerkennung konfigurieren - CV_RETR_EXTERNAL bedeutet nur
äußere Konturen zu erkennen, CV_RETR_LIST dagegen auch innere.
Testbar an den ersten beiden Programmoptionen.
ACHTUNG: die Templates liegen mit inneren Konturen vor */
#define MODUS CV_RETR_LIST
/* nach jedem Schritt x Millisekunden warten */
#define WAITTIME 50000
/* Pfadeinstellungen */
#define PICTUREPATH "./pictures/"
#define TEMPLATEPATH "./templates/"
/* bei erstem Mismatch abbrechen, ja oder nein
(nachfolgende Zeilen auf 0 (=false) oder 1 (=true) setzen) */
#define SKIPFOLLOWINGLINESBYFIRSTMISMATCH 0
#define SKIPFOLLOWINGCOLSBYFIRSTMISMATCH 0
/* bei der dritten Matching-Variante kann man sogar genau einstellen,
ab wievielen Pixeln Nicht-Übereinstimmung das nächste Offset
vorgenommen werden soll (0 = kein Skipping) */
#define SKIPFOLLOWINGBYMISMATCHNR 0
/* Schwellwerte für Match-Erkennung */
#define PIXELTHRESHOLD 100
#define DIFFERENCINGROWS 10
#define DIFFERENCINGCOLS 10
/* Prototypen */
void buildMenu();
IplImage* analyseRealPicture(char* file);
IplImage* analyseAbstractPicture(char* file);
IplImage* loadImage(char* file);
void findObjects(void);
void listDirContent(char* dir);
int matchingRecognition(IplImage* picture1, IplImage* picture2);
void findPrimitiveObjects(void);
int testGetPixels1(IplImage* picture);
int testGetPixels2(IplImage* picture);
/* Variablen */
int selection;
CvSeq* contoursOriginalPicture = 0;
CvSeq* contoursDestinationPicture = 0;
bool** pic1 = 0;
int* pic2X = 0;
int* pic2Y = 0;
int picWhitePoints = 0;
int main (int argc , char* argv[])
{
do
{
buildMenu();
switch (selection)
{
case TERMINATEPROGRAM:
cout << endl << "Adios" << endl;
break;
case ANALYSEREALPICTURE:
analyseRealPicture(NULL);
break;
case ANALYSEABSTRACTPICTURE:
analyseAbstractPicture(NULL);
break;
case FINDOBJECTS:
findObjects();
break;
case FINDPRIMITIVEOBJECTS:
findPrimitiveObjects();
default:
break;
}
}
while (selection != TERMINATEPROGRAM);
return 0;
}
void buildMenu()
{
selection = 0;
do
{
/* Fehleingabe, Bildschirm löschen und von vorn beginnen */
if (selection >= OPTIONSCOUNT)
{
cout << endl << "Fehleingabe! Eine beliebige Taste druecken um Menue neu aufzubauen.";
/* auf beliebigen Tastendruck warten */
while(!kbhit());
}
/* Bildschirm löschen */
system("cls");
cout << "-------------------------------------------------------" <<endl;
cout << "--- Bildverarbeitungsprojekt v1.0 von Thomas Kramer ---" <<endl;
cout << "-------------------------------------------------------" <<endl;
cout << endl;
cout << "[0] Beendigung" <<endl;
cout << "[1] Kantenerkennung in realen Bildern" <<endl;
cout << "[2] Kantenerkennung in abstrakten Bildern" <<endl;
cout << "[3] Objekte in realen Bildern finden" <<endl;
cout << "[4] Primitive Objekte in abstrakten Bildern finden" <<endl;
cout << endl;
cout << "Bitte eine Eingabe vornehmen: ";
cin >> selection;
}
while (selection >= OPTIONSCOUNT);
}
void listDirContent(char* dir)
{
DIR *dirHandle;
struct dirent *dirEntry;
cout << endl;
//Einen Ordner öffnen
dirHandle = opendir(dir);
//Konnte der Ordner geöffnet werden?
if ( dirHandle != NULL )
{
//Alle Ordner/Dateien auslesen
while ( 0 != ( dirEntry = readdir( dirHandle ) ) )
{
string temp = dirEntry->d_name;
if (temp != "." && temp != "..")
{
puts( dirEntry->d_name );
}
}
//Den Ordner schliessen
closedir( dirHandle );
}
}
IplImage* loadImage(char* file)
{
char filename[260];
char praefix[260] = PICTUREPATH;
IplImage* img;
// cout << "datei: " << file << endl;
if (file == NULL)
{
cout << endl << "Folgende Bilder sind verfuegbar: " << endl;
listDirContent(praefix);
cout << endl;
cout << "Welche Bilddatei davon wollen Sie benutzen? " <<endl;
cin >> filename;
strcat(praefix, filename);
img = cvLoadImage (praefix);
}
else
{
img = cvLoadImage (file);
}
if (! img )
{
cerr << " Could not load image file : " << praefix <<endl ;
exit ( EXIT_FAILURE );
}
/*cvNamedWindow("Originalbild");
cvShowImage("Originalbild", img);
cvWaitKey(WAITTIME);
cvDestroyWindow("Originalbild");*/
return img;
}
IplImage* analyseRealPicture(char* file)
{
IplImage* img;
if (file == NULL)
{
img = loadImage(NULL);
}
else
{
img = loadImage(file);
}
IplImage* binaryImage = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
IplImage* normalized = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
IplImage* gauss = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
IplImage* cannyimg = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
IplImage* newimg = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
binaryImage = cvCreateImage ( cvGetSize(img),IPL_DEPTH_8U, 1);
cvSplit (img, binaryImage, NULL , NULL , NULL );
/* neues Bild erzeugen, 1. Parameter Bildgröße, 2. Parameter Farbtiefe, 3. Parameter Anzahl Farbkanäle */
normalized = cvCreateImage ( cvGetSize (img), img ->depth , 1);
/* normalisiert Helligkeit und erhöht Kontrast */
/* 1. Parameter Input, 2. Parameter Output (selbe Größe) */
cvEqualizeHist ( binaryImage , normalized );
/* Weichzeichner anwenden um Konturen besser herauszustellen */
gauss = cvCreateImage ( cvGetSize (img), img->depth , 1);
cvSmooth ( normalized , gauss , CV_GAUSSIAN , 13, 13);
/* canny = Kantenerkennung durch canny-Algorithmus */
cannyimg = cvCreateImage ( cvGetSize (img), img ->depth , 1);
cvCanny(gauss, cannyimg, 40, 130);
/* Speicher allozieren für die Konturen */
CvMemStorage* canny_storage = cvCreateMemStorage(0);
/* enthält nach Anwendung der FindContours-Funktion den Zeiger auf die erste Kontur */
contoursOriginalPicture = 0;
/* Konturen finden */
int conts = cvFindContours(cannyimg, canny_storage, &contoursOriginalPicture, sizeof(CvContour), MODUS, CV_CHAIN_APPROX_NONE);
/* Konturen zeichnen in ein neues Bild */
cvDrawContours( newimg, contoursOriginalPicture, cvScalar(255,255,255), cvScalarAll(255), 100);
/* wenn Datei gezielt angegegeben wurde wollen wir keine Fenster erstellen */
if (file == NULL)
{
/* Fenster erstellen */
cvNamedWindow("Loaded Image", CV_WINDOW_AUTOSIZE);
//cvNamedWindow("Canny Image", CV_WINDOW_AUTOSIZE);
cvNamedWindow("Canny Contours", CV_WINDOW_AUTOSIZE);
/* Bilder in den Fenstern anzeigen */
cvShowImage("Loaded Image", img);
//cvShowImage("Canny Image", cannyimg);
cvShowImage("Canny Contours", newimg);
cout << endl << "Konturen gefunden: " << conts << ", Punkte gefunden: " << contoursOriginalPicture->total;
cout << endl;
/* Koordinatenausgabe der gefundenen Konturen */
for (int i = 0; i < contoursOriginalPicture->total; i++)
{
CvPoint *line = (CvPoint *)cvGetSeqElem(contoursOriginalPicture, i);
cout << i+1 << ". x: " << line->x << ", y: " << line->y << endl;
}
cvWaitKey(WAITTIME);
/* Daten wieder freigeben */
cvDestroyWindow("Loaded Image");
//cvDestroyWindow("Canny Image");
cvDestroyWindow("Canny Contours");
}
cvReleaseMemStorage( &canny_storage);
cvReleaseImage(&img);
cvReleaseImage(&binaryImage);
cvReleaseImage(&normalized);
cvReleaseImage(&gauss);
cvReleaseImage(&cannyimg);
return newimg;
}
IplImage* analyseAbstractPicture(char* file)
{
IplImage* img;
if (file == NULL)
{
img = loadImage(NULL);
}
else
{
img = loadImage(file);
}
IplImage* cannyimg = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
IplImage* newimg = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
IplImage* greyimg = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
cvCvtColor(img, greyimg, CV_BGR2GRAY);
/* canny = Kantenerkennung durch canny-Algorithmus */
cvCanny(greyimg, cannyimg, 50, 150, 3);
cvConvertImage(cannyimg, newimg);
/* Speicher allozieren für die Konturen */
CvMemStorage* canny_storage = cvCreateMemStorage(0);
/* enthält nach Anwendung der FindContours-Funktion den Zeiger auf die erste Kontur */
contoursOriginalPicture = 0;
contoursDestinationPicture = 0;
CvSeq* canny_contours = 0;
/* Konturen finden */
int conts = 0;
if (file == NULL)
{
conts = cvFindContours(newimg, canny_storage, &contoursOriginalPicture, sizeof(CvContour), MODUS, CV_CHAIN_APPROX_NONE);
canny_contours = contoursOriginalPicture;
}
else
{
conts = cvFindContours(newimg, canny_storage, &contoursDestinationPicture, sizeof(CvContour), MODUS, CV_CHAIN_APPROX_NONE);
canny_contours = contoursDestinationPicture;
}
/* Konturen zeichnen in ein neues Bild */
cvDrawContours( newimg, canny_contours, cvScalar(255,255,255), cvScalarAll(255), 100);
/* wenn Datei gezielt angegegeben wurde wollen wir keine Fenster erstellen */
if (file == NULL)
{
/* Fenster erstellen */
cvNamedWindow("Loaded Image", CV_WINDOW_AUTOSIZE);
//cvNamedWindow("Canny Image", CV_WINDOW_AUTOSIZE);
cvNamedWindow("Canny Contours", CV_WINDOW_AUTOSIZE);
/* Bilder in den Fenstern anzeigen */
cvShowImage("Loaded Image", img);
//cvShowImage("Canny Image", cannyimg);
cvShowImage("Canny Contours", newimg);
cout << endl << "Konturen gefunden: " << conts << ", Punkte gefunden: " << canny_contours->total;
cout << endl;
/* Koordinatenausgabe der gefundenen Konturen */
for (int i = 0; i < canny_contours->total; i++)
{
CvPoint *line = (CvPoint *)cvGetSeqElem(canny_contours, i);
cout << i+1 << ". x: " << line->x << ", y: " << line->y << endl;
}
cvWaitKey(WAITTIME);
/* Daten wieder freigeben */
cvDestroyWindow("Loaded Image");
//cvDestroyWindow("Canny Image");
cvDestroyWindow("Canny Contours");
}
cvReleaseMemStorage( &canny_storage);
cvReleaseImage(&img);
cvReleaseImage(&cannyimg);
cvReleaseImage(&greyimg);
return newimg;
}
void findObjects(void)
{
double *matches;
int i=0;
int counter = 0;
DIR *dirHandle;
struct dirent *dirEntry;
char praefix[260] = TEMPLATEPATH;
/* Quelldatei einlesen */
IplImage* sourceFile = analyseRealPicture(NULL);
testGetPixels1(sourceFile);
/* Ordner öffnen, zuerst separat Dateien zählen */
dirHandle = opendir(praefix);
//Konnte der Ordner geöffnet werden?
if ( dirHandle != NULL )
{
//Alle Ordner/Dateien auslesen
while ( 0 != ( dirEntry = readdir( dirHandle ) ) )
{
string temp = dirEntry->d_name;
if (temp != "." && temp != "..")
{
counter++;
}
}
//Den Ordner schliessen
closedir( dirHandle );
}
/* arraygröße entsprechend anzahl dateien setzen */
matches = new double [counter];
/* erneut öffnen, diesmal zur Sache kommen */
dirHandle = opendir(praefix);
//Konnte der Ordner geöffnet werden?
if ( dirHandle != NULL )
{
//Alle Ordner/Dateien auslesen
while ( 0 != ( dirEntry = readdir( dirHandle ) ) )
{
string temp = dirEntry->d_name;
if (temp != "." && temp != "..")
{
char praefix2[260] = TEMPLATEPATH;
strncat(praefix2, dirEntry->d_name, dirEntry->d_namlen);
cout << endl << "Vergleich mit Template: " << temp << endl;
IplImage* destFile = loadImage(praefix2);
IplImage* binaryImage = cvCreateImage ( cvGetSize(destFile),IPL_DEPTH_8U, 1);
cvSplit (destFile, binaryImage, NULL , NULL , NULL );
testGetPixels2(binaryImage);
double time1 = 0.0, tstart;
tstart = clock();
matches[i] = matchingRecognition(sourceFile, binaryImage);
time1 += clock() - tstart;
time1 /= CLOCKS_PER_SEC;
cout << "benoetigte Zeit fuer Vergleich in Sekunden: " << time1 << endl;
cvReleaseImage(&destFile);
cvReleaseImage(&binaryImage);
i++;
}
}
//Den Ordner schliessen
closedir( dirHandle );
}
/* Speicher wieder freigeben */
for (int j = 0; j < sourceFile->width ; j++)
delete [] pic1[j];
delete [] pic1;
delete [] pic2X;
delete [] pic2Y;
cvReleaseImage(&sourceFile);
cout << endl << "Vergleiche abgeschlossen!" << endl;
/* warten */
int temp;
cin >> temp;
}
/* komplettes Ausgangsbild in Array einlesen um spätere, immer
wiederkehrende Aufrufe von cvGetReal2D zu vermeiden */
int testGetPixels1(IplImage* picture)
{
if (picture == NULL)
return 0;
/* 1. Dimension des zweidimensionalen Arrays festlegen */
pic1 = new bool*[picture->width];
/* 2. Dimension des zweidimensionalen Arrays festlegen */
for (int i = 0; i < picture->width; i++)
pic1[i] = new bool[picture->height];
for (int x=0; x<(picture->width); x++)
{
for (int y=0; y<(picture->height); y++)
{
pic1[x][y] = (cvGetReal2D(picture, y, x) == 255);
}
}
//cout << "Originalbild eingelesen." <<endl;
}
/* vom Zielbild lesen wir gezielt nur die weissen Bildpunkte ein */
int testGetPixels2(IplImage* picture)
{
if (picture == NULL)
return 0;
int blackPixels = 0;
int whitePixels = 0;
int temp = 0;
cout << "Abmessungen des Bildes: x=" << picture->width << ", y=" << picture->height << endl;
/* beim ersten Durchlauf wissen wir die Anzahl der weissen Punkte noch nicht, die wir
aber benötigen um das Array zu dimensionieren */
for (int x=0; x<(picture->width); x++)
{
for (int y=0; y<(picture->height); y++)
{
temp = cvGetReal2D(picture, y, x);
if (temp == 0)
{
blackPixels++;
}
else if (temp == 255)
{
whitePixels++;
}
}
}
cout << "Schwarze Pixel: " << blackPixels << ", weisse Pixel: " << whitePixels << endl;
/* für schnellere matchingRecognition2-Funktion nur die weissen Punkte zwischenspeichern */
pic2X = (int*) malloc(whitePixels*sizeof(int));
pic2Y = (int*) malloc(whitePixels*sizeof(int));
picWhitePoints = whitePixels;
/* nur weisse Pixel einlesen */
int i = 0;
for (int x=0; x<(picture->width); x++)
{
for (int y=0; y<(picture->height); y++)
{
temp = cvGetReal2D(picture, y, x);
if (temp == 255)
{
pic2X[i] = x;
pic2Y[i] = y;
i++;
}
}
}
}
int matchingRecognition(IplImage* picture1, IplImage* picture2)
{
/* Match-Variablen */
int matches = 0;
int maxMatches = 0;
int maxMatchesBeginPositionX = 0;
int maxMatchesBeginPositionY = 0;
int maxMatchesEndPositionX = 0;
int maxMatchesEndPositionY = 0;
boolean mismatch = false;
int mismatchNr = 0;
/* Offset-Variablen */
int offsetX = 0;
int maxOffsetX = picture1->width - picture2->width;
int offsetY = 0;
int maxOffsetY = picture1->height - picture2->height;
/* Initiale Werte für Minimumsuche */
int tempBeginX = picture1->width + 1;
int tempBeginY = picture1->height + 1;
/* Initiale Werte für Maximumsuche */
int tempEndX = 0;
int tempEndY = 0;
/* temporäre Variablen */
int tempX = 0;
int tempY = 0;
int tempPic1Width = picture1->width;
int tempPic1Height = picture1->height;
if (picture1 == NULL || picture2 == NULL)
{
return 0;
}
/* picture1 sollte schon größer als picture2 sein */
if (maxOffsetX < 0 || maxOffsetY < 0)
{
cout << "Template groesser als Originalbild und damit ungueltig!" << endl;
return 0;
}
int x = 0;
int y = 0;
do
{
do
{
/* Beginn Offsetverschiebungen */
/* Werte zurücksetzen */
mismatch = false;
mismatchNr = 0;
matches = 0;
/* Initiale Werte für Minimumsuche */
tempBeginX = tempPic1Width;
tempBeginY = tempPic1Height;
/* Initiale Werte für Maximumsuche */
tempEndX = 0;
tempEndY = 0;
for (int t = 0; t < picWhitePoints; t++)
{
y = pic2Y[t];
x = pic2X[t];
/* wir wollen diese Rechnung nicht immer wieder neu vornehmen */
tempY = y + offsetY;
tempX = x + offsetX;
/* Übereinstimmung */
if (pic1[tempX][tempY])
{
mismatch = false;
matches++;
/* Minimumsuche- und Maximumsuche */
tempBeginY = min(tempY, tempBeginY);
tempBeginX = min(tempX, tempBeginX);
tempEndY = max(tempY, tempEndY);
/* x wird nie zurückgesetzt bei den Offsets,
den Vergleich können wir hier einsparen */
tempEndX = tempX;
}
else
{
/* keine Übereinstimmung */
mismatch = true;
mismatchNr++;
if (matches > maxMatches)
{
maxMatches = matches;
maxMatchesBeginPositionX = tempBeginX;
maxMatchesBeginPositionY = tempBeginY;
maxMatchesEndPositionX = tempEndX;
maxMatchesEndPositionY = tempEndY;
}
matches = 0;
if (mismatchNr == SKIPFOLLOWINGBYMISMATCHNR)
{
break;
}
}
}
/* vor einer neuen Offsetverschiebung müssen Minima/
Maxima gegebenenfalls zwischengespeichert werden */
if (matches > maxMatches)
{
maxMatches = matches;
maxMatchesBeginPositionX = tempBeginX;
maxMatchesBeginPositionY = tempBeginY;
maxMatchesEndPositionX = tempEndX;
maxMatchesEndPositionY = tempEndY;
}
offsetY++;
}
while (offsetY <= maxOffsetY);
offsetY = 0;
offsetX++;
}
while (offsetX <= maxOffsetX);
//cout << "offset x: " << offsetX << endl;
/* bei einem Match ausgeben */
if (maxMatches >= PIXELTHRESHOLD)
{
if ((abs(maxMatchesEndPositionX - maxMatchesBeginPositionX) >= DIFFERENCINGCOLS)
&& (abs(maxMatchesEndPositionY - maxMatchesBeginPositionY) >= DIFFERENCINGROWS))
{
cout << "Pixel-Matches im Vergleich zum Originalbild: " << maxMatches << endl;
cout << ", beginnend bei den Koordinaten (x = " << maxMatchesBeginPositionX << ", y = " << maxMatchesBeginPositionY << ")" << endl;
cout << ", endend bei den Koordinaten (x = " << maxMatchesEndPositionX << ", y = " << maxMatchesEndPositionY << ")" << endl;
}
else
{
cout << "Kein Match!" << endl;
}
}
else
{
cout << "Kein Match!" << endl;
}
return matches;
}
void findPrimitiveObjects(void)
{
IplImage* img = loadImage(NULL);
IplImage* greyimg = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
IplImage* normalized = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
IplImage* gauss = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
IplImage* newimg = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);
cvCvtColor(img, greyimg, CV_BGR2GRAY);
/* Bildverbesserer hier vorerst deaktiviert weil es hier mehr um abstrakte Objekte wie Kreisen geht,
für diese nimmt man zum Testen besser abstrakte Bilder.
Weichzeichner wie Gauss erschweren sonst die Erkennung!
Für die Kantenerkennung weiter oben ist dagegen Weichzeichner wichtig! */
////////////////////////////////////////////////////////////////////////////////////////////////////////////
/* neues Bild erzeugen, 1. Parameter Bildgröße, 2. Parameter Farbtiefe, 3. Parameter Anzahl Farbkanäle */
normalized = cvCreateImage ( cvGetSize (img), img ->depth , 1);
/* normalisiert Helligkeit und erhöht Kontrast */
/* 1. Parameter Input, 2. Parameter Output (selbe Größe) */
cvEqualizeHist ( greyimg , normalized );
/* Weichzeichner anwenden um Konturen besser herauszustellen */
gauss = cvCreateImage ( cvGetSize (img), img->depth , 1);
cvSmooth ( normalized , gauss , CV_MEDIAN , 13, 13);
////////////////////////////////////////////////////////////////////////////////////////////////////////////
CvMemStorage* storage_var = cvCreateMemStorage(0);
CvSeq* results = cvHoughCircles(gauss, storage_var , CV_HOUGH_GRADIENT , 2 , img->width/3);
cout << results->total << " Kreise gefunden!" << endl;
for( int i = 0; i < results->total; i++ )
{
//cout << i << endl;
float* p = (float*) cvGetSeqElem( results, i );
cout << p << endl;
CvPoint pt = cvPoint( cvRound( p[0] ), cvRound( p[1] ));
cout << i+1 << ". x: " << pt.x << ", y: " << pt.y << endl;
cvCircle(gauss,pt,cvRound( cvRound( p[2] ) ),cvScalar(255,255,255), 1.8);
}
/* Fenster erstellen */
cvNamedWindow("Circles", CV_WINDOW_AUTOSIZE);
/* Bilder in den Fenstern anzeigen */
cvShowImage("Circles", gauss);
cvWaitKey(WAITTIME);
cvDestroyWindow("Circles");
cvReleaseMemStorage(&storage_var);
cvReleaseImage(&img);
cvReleaseImage(&greyimg);
cvReleaseImage(&normalized);
cvReleaseImage(&gauss);
cvReleaseImage(&newimg);
}