OpenCV 3 mit C++ – Teil 2 – Grundlegende Datentypen

Im letzten Beitrag sind wir das erste Mal der Klasse Mat als begegnet. Sie ist einer der grundlegenden Klassen zur Datenspeicherung in OpenCV. Die Grundlagen derselben wollen wir uns in diesem Artikel anschauen.

Den Code gibt es, wie immer, im Github-Repository zu diesem Projekt.

Grundlagen zur Speicherung von Bildern

Die meisten von euch wissen sicherlich, dass Bilder üblicherweise als Pixelraster gespeichert werden, also als eine Art Tabelle einzelner Bildpunkte. Das gilt zumindest für die sog. Rastergrafiken, der Standard in OpenCV. Vektorgrafiken müssen erst durch externe Bibliotheken konvertiert werden, um anschließend verarbeitet werden zu können.

Vielen bekannt ist sicherlich auch das RGB-Format. Dabei werden für jeden Pixel drei Werte gespeichert: Einer für die Farbe Rot, einer für Blau, einer für Grün. Diese Kanäle werden dann additiv gemischt. Normalerweise hat jeder Kanal eine Größe von einem Byte (8 Bit) und kann somit Werte zwischen 0 (kein Anteil an der Pixelfarbe) und 255 (maximaler Anteil an der Pixelfarbe) annehmen. Häufig kommt auch das RGBA-Format vor, bei dem pro Pixel auch die Information über Alpha, die Deckkraft oder Transparenz gespeichert wird. In OpenCV wird mit dem BGR-Format gearbeitet, hier ist also einfach nur die Reihenfolge der Farbkanäle vertauscht.

HSV-Bilder (für hue, saturationvalue) speichern ebenfalls drei Kanäle: Farbton, Sättigung und Helligkeitswert. Den HSV-Farbraum kann man sich gut vorstellen als Kreiskegel. Auf dessen runder Grundfläche sich ganz außen die voll gesättigten und hellen Farben, je weiter man nach innen wandert, desto geringer ist die Sättigung. In Richtung Scheitel des Kegels nimmt der Helligkeitswert immer weiter ab.

Matrix m(2, 3Der HSV-Farbraum.Hue (H, Farbwert) ist der Winkel, wobei 0° Rot entspricht und die Winkel Richtung Gelb größer werden.

Anders als viele andere Programme und Bibliotheken speichert OpenCV die Werte in anderen Wertebereichen:

  • Hue (H, Farbwert): normalerweise 0 bis 360, in OpenCV 0 bis 180 (ihr verliert also die Hälfte der Farbauflösung)
  • Saturation (S, Sättigung): normalerweise zwischen 0 und 1, in OpenCV 0 bis 255
  • Value (V, Helligkeit): normalerweise zwischen 0 und 1, in OpenCV 0 bis 255

Es ist wichtig, das zu beachten, da die meisten Programme mit diesen Wertebereichen nicht richtig umgehen können bzw. OpenCV nicht mit denen der meiten Programme.

YCbCr-Bilder speichern die Information ebenfalls in drei Kanälen. Im Kanal Y wird die Grundhelligkeit (Luminanz) gespeichert, im Kanal Cb (Blue-Yellow Chrominance) und Cr (Red-Green Chrominance). OpenCV speichert alle drei Kanäle im Wertebereich von 0 bis 255.

YCbCr-CbCr Scaled Y50
Die CbCr-Achse.

Dann gibt noch Graustufenbilder, bei denen keine Farbe gespeichert wird, sondern nur der Graustufenwert. Dafür wird pro Pixel üblicherweise ein Kanal gespeichert, der wieder pro Pixel ein Byte hat, also wieder Werte zwischen 0 und 255 speichern kann.

HDR-Bilder (High Dynamic Range) gewinnen momentan immer weiter an Bedeutung. Sie sind in der Lage, hohe Kontraste (also große Helligkeitsunterschiede) zu speichern und werden immer mehr in der Computergrafik eingesetzt. Die Speicherung ist etwas komplizierter  – üblicherweise werden die Helligkeitswerte logarithmisch gespeichert, um der nichtlinearen Wahrnehmung des menschlichen Auges gerecht zu werden. In OpenCV werden sie z. B. durch das Format OpenEXR, welches im Kompilierprozess eingebunden werden kann, unterstützt. Ich werde sie jedoch vorerst nicht behandeln.

Die grundlegenden Datentypen

Nun schauen wir uns die Datentypen an, die in OpenCV Bilder und mehr repräsentieren. Diese Datentypen kommen alle aus dem Bereich der linearen Algebra. So repräsentiert eine Matrix aus dreidimensionalen Vektoren beispielsweise ein RGB-Bild, eine Matrix aus Skalaren ein Graufstufenbild usw.

1. Matx

Von der Matx-Klasse leiten einige grundlegende Datentypen von OpenCV ab. Zudem ist sie der dynamischen Mat-Klasse sehr ähnlich, es lohnt sich also, sie genauer zu untersuchen.

Matx wird benutzt, wenn Matrizen mit schon zur Kompilationszeit bekannter, also nicht dynamischer Größe benötigt werden – für letzteres wird die sehr ähnliche Klasse Mat verwendet, siehe hierfür weiter unten. Wie die meisten Klassen ist Matx ein Template. Wir müssen dem Template erst den Typ der Elemente übergeben, dann die Anzahl der Zeilen, dann die Anzahl der Spalten.

Damit sieht beispielsweise die Deklaration einer 2×3-Matrix mit float-Einträgen wie folgt aus:

OpenCV unterstützt auch eine Standardausgabe von diesen komplexeren Datentypen mittels cout:

Ausgabe einer 2x3-Matrix.
Ausgabe einer 2×3-Matrix.

Beachtet, dass wir hier die C++-Standardfunktion getchar() verwenden, um das Fenster offenzuhalten. waitkey() gilt nämlich nur für Elemente aus der Highgui-API von OpenCV selbst, nicht also für das Konsolenfenster, und kann somit nicht wie im letzten Beitrag verwendet werden.

Wenn ihr weniger Argumente angebt, als benötigt, um alle Einträge zu füllen, wird der Rest mit Nullen aufgefüllt. Hier ein Beispiel mit Integer-Einträgen:

Ausgabe einer 3x3-Matrix mit aufgefüllten Einträgen
Ausgabe einer 3×3-Matrix mit aufgefüllten Einträgen.

Um die Schreibweise zu verkürzen, gibt es einige typedefs:

Mit

greift man auf das Element in der (i+1)-ten Zeile und der (j+1)-ten Spalte zu (Index nullbasiert), z. B.

Ergebnis: 1.3

Es gibt drei statische Funktionen, um eine Matrix auf einfache Weise zu erstellen: Matx::zeros() erstellt die Nullmatrix, Matx::ones() die Einsmatrix, Matx::eye() die Einheitsmatrix.

Die Einsmatrix, Nullmatrix und Einheitsmatrix.
Die Einsmatrix, Nullmatrix und Einheitsmatrix.

Mathematische Operationen

(Dieser Abschnitt ist teilweise etwas mathelastig. Er kann übersprungen und später bei Bedarf nachgeholt werden.)

Natürlich sind auch mathematische Operationen mit Matrizen sehr wichtig. OpenCV überschreibt einige Operatoren für die intuitive Verwendung der Operationen. So kann eine beliebige Matrix wie folgt mit einem Skalar multipliziert werden:

Multiplikation einer Matrix mit einem Skalar.
Multiplikation einer Matrix mit einem Skalar.

Natürlich lassen sich Matrizen derselben Größe auch addieren.

Addition von zwei Matrizen.
Addition von zwei Matrizen.

An diesem Beispiel sieht man auch, dass sich die Operationen wie bei den primitiven Datentypen verknüpfen lassen.

Natürlich lassen sich Matrizen auch miteinander multiplizieren. Das geht elementweise oder wie die klassische Matrizenmultiplikation. Die elementweise Multiplikation zweier gleich großer Matrizen geschieht mit Hilfe der mul()-Funktion der Klasse Matx:

Elementweise Multiplikation zweier Matrizen.
Elementweise Multiplikation zweier Matrizen.

Die normale Matrizenmultiplikation einer (lxm)– mit einer (mxn)-Matrix geschieht über die Überladung des Multiplikationsoperators:

Matrizenmultiplikation.
Matrizenmultiplikation.

Auch die Vergleichsoperatoren == und != sind definiert. == liefert 1, wenn die Komponenten beider Matrizen vollständig identisch sind, != das invertierte Ergebnis.

Die Determinante einer (quadratischen) Matrix bekommt ihr über die Funktion cv::determinant():

Ergebnis: 4

Die Inverse bildet ihr über die Funktion inv() der Matx-Klasse:

Inverse
Inverse

Ihr könnt ein Argument übergeben, der bestimmt, mit welchem Verfahren die Inverse bestimmt wird. Zur Auswahl stehen DECOMP_SVD, DECOMP_LU, DECOMP_CHOLESKY, die Verfahren werden etwas weiter unten kurz beschrieben.

Lineare Gleichungssysteme lösen:

Natürlich kann man mit OpenCV Gleichungssysteme der Form Ax = b lösen. Dafür erstellen wir eine Matrix A, einen Vektor b und einen leeren Vektor x und lösen das Gleichungssystem mit der solve()-Methode der Klasse Matx.

Lösung des LGS.
Lösung des LGS.

Die Methode solve() bekommt den Vektor b und eine Zahl übergeben, die angibt, mit welchem Algorithmus das Gleichungssystem gelöst werden soll. Welcher Algorithmus benutzt werden kann, hängt davon ab, welche Bedingungen die Matrix A erfüllt. Je mehr man über die Matrix weiß, desto besser, da dann ein umso effizienterer Algorithmus gewählt werden kann:

  • DECOMP_LU: Der Gauß-Algorithmus, realisiert durch LU-Zerlegung (siehe z. B. hier)
  • DECOMP_CHOLESKY: Lösung durch Aufteilung der Matrix in zwei Diagonalmatrizen sowie Vorwärts- und anschließendes Rückwärtseinstzen (siehe hier).
  • DECOMP_QR: Lösung durch die QR-Faktorisierung.
  • DECOMP_EIG: Die Matrix wird mit Hilfe von Eigenwerten diagonalisiert, gelöst, und zurücktransformiert. Dafür muss sie natürlich diagonalisierbar sein.
  • DECOMP_SVD: Lösung mit Hilfe der Singulärwertzerlegung.

Es gibt noch viele weitere nützliche Funktionen zum Diagonalisieren von Matrizen, Berechnen der Nullstellen von Polynomen, etc. Diese können hier und hier näher begutachtet werden.

2. Mat

Die Mat-Klasse ist wohl die wichtigste Klasse in OpenCV, da in einer solchen meist die Bilder gespeichert werden, die untersucht werden sollen. Da wir uns bereits die Matx-Klasse ausführlich angeschaut haben, können wir hier darauf größtenteils verzichten. Der Code der Mat-Klasse kann hier abgerufen werden.

Da jedoch die Größe der Matrix sowie der gespeicherte Typ bei der Mat-Klasse dynamisch sind (also nicht zur Kompilierzeit festgelegt), müssen wir der Matrix bei der Erstellung diese Parameter mitteilen. Einer der zahlreichen Konstruktoren der Klasse Mat sieht dann z. B. so aus (für die anderen Konstruktoren einfach schnell in diese Datei schauen):

Man übergibt also im Konstruktor die Anzahl der Zeilen und Spalten sowie den Typ jeweils als int.  Doch wie sieht der letzt Parameter konkret aus? Um die Zahl zu bestimmen, gibt es das Makro CV_MAKETYPE(depth, cn) in dieser Datei:

Diesem Makro übergibt man die Bittiefe des Datentyps sowie der Anzahl der Kanäle, die pro Matrixeintrag gespeichert werden sollen. Die Bittiefen verschiedener Datentypen sind in derselben Datei definiert:

Natürlich gibt es (in derselben Datei) schon eine Vielzhal an vordefinerten Werten:

So sieht dann die Erstellung einer neuen Mat-Instanz mit 3 Kanälen aus unsigned chars (Bittiefe von 8, perfekt, um ein normales RGB-Bild zu speichern) so aus:

Häufig unterscheiden sich die Methoden der Mat-Klasse von denen der Matx-Klasse nur durch Parameter, deren Notwendigkeit sich durch die mögliche dynamische Änderung der Matrix zur Laufzeit ergibt (wie eben im Konstruktor betrachtet.

Datenstruktur von Mat und Speichermanagement

Wie hoffentlich jeder weiß, muss man beim Speichermanagement in C++ aufpassen, um keine Sicherheitslücken aufzureißen oder memory leaks zu produzieren. Deshalb wollen wir uns kurz die interne Datenstruktur von Mat anschauen.

Mat besteht im Wesentlichen aus zwei Teilen: Einem Header, der Eigenschaften wie die Größe der Matrix, den Datentyp, die Kanalanzahl, etc. angibt, und einem Pointer auf einen Speicherbereich, in dem die eigentlichen Daten liegen. Die Mat-Klasse funktioniert sehr ähnlich wie der neue Shared Pointer in C++ 11 (std::shared_ptr): Die Klasse zählt mit, wie häufig auf diesen Speicherbereich verwiesen wird. Erlischt eine Referenz (z. B. weil eine Funktion verlassen wird), wird der der Referenzzähler heruntergezählt, übergebt ihr einer Funktion ein Mat-Objekt, wird er heraufgezählt. Erreicht der Referenzzähler den Wert 0, wird der Speicherbereich freigegeben. Somit ist also keine manuelle Speicherfreigabe erforderlich. Jedoch gibt es auch die Möglichkeit, eigene Daten an ein Mat-Objekt zu übergeben, welches diese dann nur noch referenziert. In diesem Fall ist man wieder selber verantwortlich für die Freigabe der Daten nach deren Nutzung.

Das ist deshalb sehr sinnvoll, weil man Mat-Objekte sehr häufig an Funktionen übergibt oder auf Teilbereiche (ROIs) verweisen will.

3. Vec

Die Klasse Vec leitet direkt von Matx ab und setzt im Wesentlichen nur die zweite Dimension konstant auf 1:

Auch hier kann also wieder ein Datentyp angegeben werden. Der Code zur Klasse findet sich hier. Auch hier gibt es wieder typedefs, die uns das Leben leichter machen:

Ansonsten gelten die üblichen Rechenregeln:

Die Funktion cv::norm(v) gibt die euklidische Norm des Vektors (Betrag wie aus der Schule) zurück. Auf einzelne Komponenten des Vektors kann mit [] zugegriffen werden.

4. Scalar

Scalar ist eine von der Vec-Klasse abgeleitete Datenstruktur, die im Wesentlichen ein Vektor mit 4 Komponenten darstellt und wie folgt definiert ist:

Auch hier kann also wieder ein Datentyp angegeben werden. Den Code findet ihr hier. Und es gibt wieder ein typedef:

Schreibt man also nur Scalar, so ist ein vierkomponentiger Vector mit double-Einträgen gemeint.

5. Point[3]

Die Point-Klassen leiten nicht von Matx ab. Sie speichern 2 bzw. 3 Koordinaten (x, y, z). Die Klassen können hier gefunden werden. Die Definitionen:

bzw.

Und auch hier gibt es wieder ein paar typedefs:

bzw.

Die Rechenoperationen sind wie gewohnt definiert.

6. Size

Size leitet ebenfalls nicht von Matx ab. Die Klasse speichert einfach die Höhe und die Breite (height und width) und wird in OpenCV sehr häufig verwendet, um die Größe eines Bildes anzugeben. Die Definition (hier zu finden):

Und wieder ein paar typedefs:

Zudem bietet die Klasse ein area()-Funktion, welche die eingeschlossene Fläche zurückgibt.

7. Rect

Rect speichert vier Werte: x, y, width, height und somit alle Informationen, die notwendig sind, um eine Rechteck inklusive Position zu beschreiben. Diese Klasse wird sehr häufig verwendet, um ROIs (Region of Interest) zu beschreiben. Die Definition sieht so aus (hier zu finden):

Wieder einige typedefs:

Die area()-Funktion liefert den Flächeninhalt, size() die Größe vom Typ Size. tl() (top left) und br() (bottom right) geben die obere linke und die untere rechte Ecke des Rechtecks als Points zurück.

8. RotatedRect

Die Klasse RotatedRect repräsentiert ein um einen Mittelpunkt rotiertes Rechteck. Es werden der Mittelpunkt und die Größe als Point2f bzw. Size2f sowie der Rotationswinkel als float gespeichert.

Ihr habt drei Konstruktoren zur Auswahl:

Die ersten beiden sind ohne Erklärung verständlich. Beim dritten werden drei Punkte entweder gegen oder mit dem Uhrzeigersinn angegeben (nicht aber ohne jegliche Reihenfolge), die im Rechteck liegen sollen.

Die Funktionen boundingRect2f() und boundingRect() geben dasjenige nichtrotierte Rechteck (als Rect2i bzw. Rect2f) zurück, welches das rotierte umschließt:

Das Bounding Rect, welches das rotierte Rechteck umschließt.
Das Bounding Rect, welches das rotierte Rechteck umschließt.

9. InputArray und OutputArray

Sehr häufig begegnet man in OpenCV dem Typ InputArray bzw. OutputArray. Diese werden in OpenCV standardmäßig verwendet, wenn ein Bild an eine Funktion übergeben wird. Sie ermöglichen es, dafür nicht nur die Klasse Mat zu verwenden, sondern zum Beispiel auch Matx, einen (Standard-)Vektor von einem (Standard-)Vektor, also vector<vector<int>>, einen OpenGL-Buffer, eine CUDA-GPU-Matrix, etc. Welche Typen unterstütz werden sieht man in der Quelldatei der Mat-Klasse:

Die beiden Klassen vereinheitlichen den Zugriff auf die Daten durch eine Reihe von Methoden, die man ebenfalls in der Datei findet. In der Praxis hat man aber so gut wie nie direkt mit diesen Klassen zu tun, trotzdem ist es wichtig, deren Bedeutung zu kennen. So werden wir beispielsweise später bei den Konturen einen Vektor von Vektoren von Punkten übergeben.