Freistellung eines Teilbereichs des Kamerabildes in EyesWeb

Aus toolbox_interaktion
Wechseln zu: Navigation, Suche

Für ein interaktives System, das eine Aktionsfläche bereitstellt, indem es auf einer Kombination von Projektor und Kamera basiert, ist es essentiell, dass das zu verarbeitende Kamerabild die exakte Ausdehnung der Projektion erfasst. Es dürfen keine Bereiche der Projektion abgeschnitten sein oder Überstand im zu verarbeitenden Bild vorhanden sein, da dies in der späteren Anwendung zu einem Positionsversatz führen würde.

Softwarevoraussetzungen

Prinzip

  • Die Kamera erfasst einen möglichst kleinen Rand um die Projektion.
  • Es wird eine weiße Fläche auf dem Projektor ausgegeben, um einen Hell-Dunkel-Kontrast zu erreichen.
  • Mit Hilfe der Binarisierung wird ein umschließendes Rechteck um die Projektion gesetzt.
  • Der Bereich des Rechtecks wird quasi ausgeschnitten und auf die konstante Ausgangsgröße des ursprünglichen Kamerabildes vergrößert.
  • Die Koordinaten des Rechtecks können festgehalten werden, sodass die Projektion gewechselt werden kann.

EyesWeb-Patch

EyesWeb-Patch
Der beispielhafte EyesWeb-Patch activearea.eyw besteht aus einer einfachen Binarisierung. Durch optimales Setzen des Binarisierungsschwellwertes wird die im Vergleich zum Hintergrund helle Projektion exakt aus dem Kamerabild herausgestellt. Um die Projektion im Binärbild wird sodann ein umschließendes Rechteck gesetzt. Aus dessen zur Verfügung stehenden Daten werden die einzelnen Koordinaten des linken, oberen Eckpunkts und des rechten, unteren Eckpunkts extrahiert und an den selbst programmierten, auf OpenCV beruhenden, EyesWeb-Block (ActiveArea) weitergegeben (die Software zu diesem Block ist im internen Bereich als Datei activearea.zip abgelegt). Durch einen Button können die aktuellen Koordinaten des umschließenden Rechtecks im ActiveArea-Block gesichert werden. Die freizustellende Fläche des Kamerabildes verbleibt daraufhin im weiteren Verlauf eines komplexeren Patches mit der festgelegten, konstanten Ausdehnung an ebenso festgelegter, gleichbleibender Position im Kamerabild.

Block "ActiveArea"

Das Grundgerüst des Blocks wird mit dem EyesWeb Wizard / EyesWeb Block Creator in Microsoft Visual C++ 6.0 erstellt.

EyesWeb Block Creator

Hierbei wird unter "Block Description" festgelegt, dass es sich bei dem zu erstellenden Block um einen passiven Block handelt. Anschließend werden die Parameter des Blocks definiert ("Add Param" > [Add...]):

  • Button zum Sichern der Koordinaten
Typ: "Command"; Name: "Lock"
Typ: "Integer"; Name: "X1"; Has Min; Min Value: 0; Has Max; Max Value: 718; Default Value: "0"; Is exported by default: "1"
Typ: "Integer"; Name: "Y1"; Has Min; Min Value: 0; Has Max; Max Value: 574; Default Value: "0"; Is exported by default: "1"
Typ: "Integer"; Name: "X2"; Has Min; Min Value: 1; Has Max; Max Value: 719; Default Value: "1"; Is exported by default: "1"
Typ: "Integer"; Name: "Y2"; Has Min; Min Value: 1; Has Max; Max Value: 575; Default Value: "1"; Is exported by default: "1"

Anschließend wird ein eingehendes Kamerabild als Eingangssignal definiert ("Add Input" > [Add...]):

Name: "imageIn"; Typ: "Image"; Interface: "IDTImage"

Schließlich werden noch das ausgehende, freigestellte Bild und ein Skalarwert, der anzeigt, ob die Koordinatensperre geöffnet oder geschlossen ist, als Ausgangssignale festgelegt ("Add Output"):

Name: "imageOut"; Typ: "Image"; Interface: "IDTImage"; Any Index: "-1"; In-place Index: "0"
Name: "lockedOut"; Typ: "Scalar"; Interface: "IDTScalar" ; Any Index: "-1"; In-place Index: "-1"

Durch das Setzen des In-place Indexes vom auszugebenden Bild auf 0 wird das eingehende Bild (imageIn) an das auszugebende Bild (imageOut) schlicht weitergeleitet. Während dieser Weiterleitung werden später die notwendigen Operationen durchgeführt.

Nach Abschluss des Block Creators liegt das Grundgerüst des neuen EyesWeb-Blocks vor. Nun müssen im Menu von MS Visual C++ unter [Projekt] > [Einstellungen] als erstes noch dem Linker die OpenCV-Bibliothek "cv.lib" bekannt gemacht werden indem sie mit einem Leerzeichen getrennt in der Textzeile "Objekt-/Bibliothekmodule" angehängt wird. Danach kann man sich der Programmierarbeit widmen.

Header-Datei

Zu Beginn der Header-Datei wird zunächst die OpenCV-Datei "cv.h" eingebunden.

#include "cv.h"

Im private-Teil der Header-Datei wird anschließend dort, wo bereits Klassenvariablen für die Koordinaten definiert werden, noch eine boolsche Variable (locked) benötigt, die speichert, ob die aktuellen Koordinaten beibehalten werden sollen. Außerdem wird noch ein "Zwischenbild" (m_croppedImg) benötigt, das während der späteren Freistellungsprozedur den freizustellende Bereich aufnimmt (dazu später mehr):

// Koordinaten des freizustellenden Rechtecks (bereits durch den Block Creator erstellt)
int m_X1; // Punkt links oben X
int m_Y1; // Punkt links oben Y
int m_X2; // Punkt rechts unten X
int m_Y2; // Punkt rechts unten Y
 
IplImage* m_croppedImg; // Zwischenbild, das den freizustellenden Bereich aufnimmt
bool locked; // Sperren der Kooridnaten

Soviel zur Editierung der Header-Datei.

Konstruktor

In der Body-Datei wird zuerst der Konstruktor betrachtet. Die beiden eben definierten Klassenvariablen müssen nun noch initialisiert werden. Ein erstes leeres Bild wird mit cvCreateImage() erzeugt und dem Zwischenbild m_croppedImg zugewiesen. Außerdem wird die Koordinatensperre geschlossen beziehungsweise auf false gesetzt.

m_croppedImg = cvCreateImage(cvSize(10,10), 8, 3); // Initalisierung des temporären Bildes, da cvReleaseImgage sonst zu Absturz führt
locked = false;

SetParam()

Als nächstes wird die Methode SetParam() editiert. Der Koordinatensperre, dem Parameter "Lock", muss noch Funktionalität zugewiesen werden. Der Hinweis "// Add command hanling code here" verrät die Stelle, an der die Negation der Koordinatensperre (je nachdem Schließen oder Öffnen) gehandhabt werden muss. Das Kommando ist simpel:

locked = !locked;

Nun wird beim Setzen der Koordinatenparameter noch die Sperrfunktionalität mit eingebunden. Dies geschieht mit Hilfe einer if-Anweisung. Um zusätzliche Sicherheit vor etwaigen NULL-Werten zu haben, falls etwa in EyesWeb kein umschließendes Rechteck im binarisierten Bild gebildet werden kann, wird in dieser if-Anweisung noch eine entsprechende Abfrage miteingefügt. Die if-Anweisung

if (!locked && pValue->value.i != NULL)

wird sodann bei jedem Zuweisungsvorgang der Koordinatenparameter eingebunden:

case EYWPAR_X1:
  if (!locked && pValue->value.i != NULL) m_X1 = pValue->value.i;
  break;
 
case EYWPAR_Y1:
  if (!locked && pValue->value.i != NULL) m_Y1 = pValue->value.i;
  break;
 
case EYWPAR_X2:
  if (!locked && pValue->value.i != NULL) m_X2 = pValue->value.i;
  break;
 
case EYWPAR_Y2:
  if (!locked && pValue->value.i != NULL) m_Y2 = pValue->value.i;
  break;

Execute()

Nun fehlt als letztes noch die eigentliche Kernfunktionalität des Blocks. Diese wird in der noch jungfräulichen Methode Execute() (einfach zu finden über den Kommentar "//TODO: Add your implementation code here") festgelegt. Als erstes werden die Ein- und Ausgangskanäle gesperrt, damit deren Daten bearbeitet werden können.

IDTImage* imageIn = (IDTImage*) LockInput(EYWIN_IMAGEIN);
IDTImage* imageOut = (IDTImage*) LockOutput(EYWOUT_IMAGEOUT);
IDTScalar* lockedOut = (IDTScalar*) LockOutput(EYWOUT_LOCKED);

Vom Signalgang des eingehenden Bildes (imageIn) wird ein Zeiger auf ein IplImage initialisiert. Falls dies misslingt, soll die Methode einen Fehler zurückmelden:

IplImage* img = NULL;
imageIn->GetIplImage((void**) &img);
if (img == NULL || m_X2-m_X1 <= 0 || m_Y2-m_Y1 <= 0) {
  return E_FAIL;
} else {

Da es nie falsch ist, mehrer Sicherheiten beim Hantieren mit EyesWeb einzuflechten, wird in der if-Anweisung zudem abgefragt, ob die Ausdehnung in X- und Y-Richtung jeweils kleiner 1 ist. Davon abgesehen ist dies auch wichtig für die Erstellung eines EyesWeb-Patches, bei dem die Koordinaten des freizustellenden Bereichs manuell festgelegt werden sollen.

Sind die Daten in Ordnung werden die aktuell gesetzten Koordinaten des umschließenden Rechtecks in zwei Variablen des OpenCV-Datentyps CvRect umgesetzt. Mit Hilfe dieses Datentyps wird Position und Ausdehnung eines Rechtecks in einer Variable erfasst. cropRect wird genau auf das umschließende Rechteck in EyesWeb gesetzt; es bestimmt den zu kopierenden Bereich. Das Rechteck tmpCropRect erhält ebenfalls die Ausdehnung des umschließenden Rechtecks, wird aber in der linken, oberen Ecke des Zwischenbildes platziert. Dieses wird im Folgenden ebenso mit der genauen Ausdehnung des freizustellenden Bereichs initialisiert.

  CvRect cropRect, tmpCropRect; // Rechteck im Ausgangsbild und im neuen Bild (== Abmessung des Zwischenbildes)
 
  cropRect.x = m_X1;
  cropRect.y = m_Y1;
  cropRect.width = m_X2-m_X1;
  cropRect.height = m_Y2-m_Y1;
 
  tmpCropRect.x = 0;
  tmpCropRect.y = 0;
  tmpCropRect.width = cropRect.width;
  tmpCropRect.height = cropRect.height;

Nun muss das genannte Zwischenbild m_croppedImg noch gemäß den Rechteckdaten erzeugt werden. Da die Methode Execute() jedesmal aufgerufen wird, wenn der Block ein Eingangssignal (also hier ein Kamerabild) empfängt, wird mit der Funktion cvReleaseImage() erst einmal der Speicherbereich des veralteten Zwischenbildes und des dazugehörigen Headers freigegeben. Mit cvCreateImage() wird das Zwischenbild wie bereits erwähnt mit der Ausdehnung des freizustellenden Bereichs neu erzeugt und anschließend wird mit cvSetZero() der Speicher des Bildarrays erst einmal bereinigt.

  // Zwischenbild erstellen
  cvReleaseImage(&m_croppedImg);
  m_croppedImg = cvCreateImage(cvSize(cropRect.width, cropRect.height), img->depth, img->nChannels);
  cvSetZero(m_croppedImg);

Zur Einleitung des tatsächlichen Kopiervorgangs werden nun mit cvSetImageROI() Regions Of Interest (ROIs) gesetzt, die anhand der Daten der vorhin gesetzten Rechtecke definiert werden. Eine Region Of Interest definiert einen Bereich innerhalb eines Bildes, auf den folgende Funktionen (hier die Kopierfunktion) angewendet werden sollen. Mit cvCopy() wird sodann der Inhalt der ROI des eingehenden Kamerabildes (also quasi das umschließende Rechteck aus EyesWeb) in die ROI des Zwischenbildes kopiert. Die ROI im Zwischenbild muss erzeugt werden, da Daten aus einer ROI nicht einfach in ein Bild kopiert werden können, sondern nur in eine andere ROI kopiert werden können. Nach dem Kopiervorgang werden die ROIs zur Sicherheit wieder entfernt.

  cvSetImageROI(img, cropRect); // zu kopierender Bereich
  cvSetImageROI(m_croppedImg, tmpCropRect); // Zielbereich (entspricht ganzer Fläche des Bildes)
 
  cvCopy(img, m_croppedImg);
 
  cvResetImageROI(img);
  cvResetImageROI(m_croppedImg);

Nun hat man im Zwischenbild m_croppedImg bereits den freigestellten Bereich enthalten. Würde man dieses Bild ausgeben, hätte man allerdings bei jeder Änderung des umschließenden Rechtecks in EyesWeb ein Ausgangsbild mit anderer Ausdehnung. Um also einen konstanten Wertebereich für eine dem Block folgende Anwendung zu erlangen, muss das Zwischenbild auf eine konstante Ausgabegröße vergrößert werden. Da festgelegt wurde, dass das Eingangsbild in diesem Block zum auszugebenden Bild durchgeschleift wird, muss das auszugebende Bild im übrigen dieselben Abmessung wie das Eingangsbild besitzen. Zu diesem Zweck wird noch einmal ein Zeiger auf ein IplImage erzeugt (tempLoad) und mit einem Bild initialisiert, dass die Abmessungen des Eingangsbildes aufweist. Mit cvResize() wird sodann das Bildarray des Zwischenbildes m_croppedImg in dieses zweite temporäre Bild quasi vergrößert einkopiert. Hierbei wird standardmäßige bilineare Interpolation verwendet. Der Speicherbereich des Zwischenbildes m_croppedImg wird nun wiederum freigegeben, um danach tempLoad m_croppedImg zuzuweisen. Diese zumindest auf den zweiten Blick umständlich erscheinende Prozedur ist notwendig, da ansonsten während der Laufzeit des Blocks in EyesWeb andauernd weiterer Speicher allokiert wird.
Im Anschluss wird das Bilddatenarray von m_croppedImg in das Array des Eingangsbildes kopiert, das später nach Freigabe des Outputs an selbigen durchgeschleift wird.

  // beschnittenen Bereich wieder auf Größe des Inputs bringen
  IplImage* tempLoad = cvCreateImage(cvSize(img->width,img->height), m_croppedImg->depth, m_croppedImg->nChannels);
  cvResize(m_croppedImg, tempLoad);
  cvReleaseImage(&m_croppedImg);
  m_croppedImg = tempLoad;
 
  cvCopy(m_croppedImg, img); // dem überbrückten Input wieder zuweisen
 
}

Zu guter Letzt wird für die Ausgabe noch der aktuelle Stand der Koordinatensperre bestimmt und je nachdem der Ausgabeschnittstelle lockedOut der Skalarwert 1 oder 0 ausgegeben.

if (locked) {
  lockedOut->SetIntValue(1);
} else {
  lockedOut->SetIntValue(0);
}

Damit sind alle Bearbeitungsschritte für das aktuelle Kamerabild abgeschlossen und die Input- und Outpuschnittstellen des Blocks können freigegeben werden.

UnlockInput(EYWIN_IMAGEIN);
UnlockOutput(EYWOUT_IMAGEOUT);
UnlockOutput(EYWOUT_LOCKED);
 
return S_OK;

Die Programmierung des Blocks "ActiveArea" wäre hiermit abgeschlossen. Nun kann die dll-Datei des Blocks erstellt und diese als Steuerelement registriert werden. Anschließend steht der Block in EyesWeb 3.x zur Verfügung.

Die Software zu diesem Block ist im internen Bereich als Datei activearea.zip abgelegt.


Eventuelle Probleme

  • Fehlermeldung "Sizes of input arguments do not match [...]"
Der Fehler tritt auf, wenn das umschließende Rechteck in EyesWeb über die Grenzen des eingehenden Videobildes hinausgehen. Entweder das Format des eingehenden Videosignals ist zu klein oder es sind noch veraltete Werte als Ausgangswerte im Freistellungsblock eingetragen. Diese können manuell durch Doppelklick auf den Block zurückgesetzt werden.