Die Umwandlung von Bilddaten in verschiedenen Formaten ist eine wichtige Aufgabe in der Bildverarbeitung, da sie die Kompatibilität und Interoperabilität zwischen verschiedenen Anwendungen und Bibliotheken ermöglicht. In diesem Text werden zwei häufig verwendete Formate für Bilddaten verglichen: das Format, das von Halcon und OpenCV genutzt wird, und das Format, das bmp nutzt. Dabei wird nur auf die Rohdaten eingegangen, ohne den Header zu berücksichtigen. Außerdem wird der Begriff des LineStride erklärt, der eine Rolle bei der Speicherung und Verarbeitung von Bilddaten spielt.
Halcon und OpenCV sind zwei populäre Bibliotheken für die Bildverarbeitung, die in verschiedenen Bereichen wie Industrie, Medizin, Robotik und Computer Vision eingesetzt werden. Beide Bibliotheken nutzen ein ähnliches Format für die Speicherung und Verarbeitung von Bilddaten, das auf einem Array von Pixelwerten basiert. Ein Pixel ist die kleinste Einheit eines digitalen Bildes, die eine bestimmte Farbe oder Intensität repräsentiert. Die Anzahl der Pixel in einem Bild wird als Bildauflösung bezeichnet und hängt von der Breite und Höhe des Bildes ab. Die Anzahl der möglichen Farben oder Intensitäten pro Pixel wird als Farbtiefe oder Bit-Tiefe bezeichnet und hängt vom verwendeten Farbmodell ab.
Das Farbmodell
Ein Farbmodell ist eine Methode, um Farben in einem digitalen Bild zu beschreiben und zu repräsentieren. Es gibt verschiedene Farbmodelle, die für verschiedene Zwecke geeignet sind. Die häufigsten Farbmodelle sind RGB, HSV, CMYK und Graustufen. RGB steht für Rot, Grün und Blau, die drei Grundfarben, aus denen alle anderen Farben zusammengesetzt werden können. HSV steht für Hue, Saturation und Value, die drei Attribute, die eine Farbe charakterisieren. CMYK steht für Cyan, Magenta, Gelb und Schwarz, die vier Farben, die in Druckern verwendet werden. Graustufen sind ein Farbmodell, das nur eine Dimension hat, die die Helligkeit eines Pixels angibt.
Halcon und OpenCV unterstützen verschiedene Farbmodelle, aber das am häufigsten verwendete ist RGB. Ein RGB-Bild besteht aus drei Kanälen, die jeweils die Rot-, Grün- und Blau-Komponente eines Pixels enthalten. Jeder Kanal hat eine bestimmte Bit-Tiefe, die die Anzahl der möglichen Werte pro Pixel angibt. Die gängigsten Bit-Tiefen sind 8 Bit, 16 Bit und 32 Bit, die jeweils 256, 65536 und 4294967296 mögliche Werte pro Pixel erlauben. Die Gesamtzahl der Bits, die ein Pixel benötigt, ist die Summe der Bit-Tiefen der einzelnen Kanäle. Zum Beispiel benötigt ein RGB-Bild mit 8 Bit pro Kanal 24 Bit pro Pixel.
Interleaved und planar: Die zwei Layouts für Bildkanäle
Die Speicherung und Verarbeitung von Bilddaten in Halcon und OpenCV erfolgt in Form eines eindimensionalen Arrays von Pixelwerten, das als Bildpuffer bezeichnet wird. Der Bildpuffer enthält die Pixelwerte in einer bestimmten Reihenfolge, die von der Breite, Höhe, Farbtiefe und dem Layout des Bildes abhängt. Das Layout des Bildes gibt an, wie die Kanäle eines Pixels angeordnet sind. Es gibt zwei gängige Layouts: interleaved und planar. Bei einem interleaved Layout werden die Kanäle eines Pixels nacheinander gespeichert, zum Beispiel R-G-B-R-G-B-… Bei einem planar Layout werden die Kanäle eines Bildes getrennt gespeichert, zum Beispiel R-R-R-… G-G-G-… B-B-B-…
Um die Position eines Pixels in einem Bildpuffer zu bestimmen, muss man die Anzahl der Bytes kennen, die ein Pixel benötigt, und die Anzahl der Bytes, die eine Zeile des Bildes benötigt. Die Anzahl der Bytes, die ein Pixel benötigt, ist die Farbtiefe geteilt durch 8, da ein Byte 8 Bit hat. Zum Beispiel benötigt ein RGB-Bild mit 8 Bit pro Kanal 3 Bytes pro Pixel. Die Anzahl der Bytes, die eine Zeile des Bildes benötigt, ist die Breite des Bildes multipliziert mit der Anzahl der Bytes pro Pixel. Zum Beispiel benötigt ein RGB-Bild mit 8 Bit pro Kanal und einer Breite von 640 Pixeln 1920 Bytes pro Zeile.
Der LineStride
Die Anzahl der Bytes, die eine Zeile des Bildes benötigt, wird auch als LineStride bezeichnet. Der LineStride ist ein wichtiger Parameter, der die Speicherung und Verarbeitung von Bilddaten beeinflusst. Der LineStride gibt an, wie viele Bytes zwischen dem Anfang einer Zeile und dem Anfang der nächsten Zeile liegen. Der LineStride kann größer oder gleich der Anzahl der Bytes pro Zeile sein, aber nicht kleiner. Wenn der LineStride größer als die Anzahl der Bytes pro Zeile ist, bedeutet das, dass es einen zusätzlichen Abstand zwischen den Zeilen gibt, der als Padding bezeichnet wird. Das Padding wird verwendet, um die Zeilen auf eine bestimmte Grenze auszurichten, die die Leistung der Bildverarbeitung verbessern kann. Zum Beispiel kann ein RGB-Bild mit 8 Bit pro Kanal und einer Breite von 640 Pixeln einen LineStride von 1924 Bytes haben, was einem Padding von 4 Bytes entspricht.
Um die Position eines Pixels in einem Bildpuffer zu berechnen, muss man die folgende Formel verwenden:
Position = (Zeilennummer * LineStride) + (Spaltennummer * Bytes pro Pixel)
Zum Beispiel hat das Pixel an der Position (100, 200) in einem RGB-Bild mit 8 Bit pro Kanal, einer Breite von 640 Pixeln und einem LineStride von 1924 Bytes die Position:
Position = (100 * 1924) + (200 * 3) = 193200 + 600 = 193800
Das bedeutet, dass das Pixel an der Position 193800 im Bildpuffer gespeichert ist.
Die Konvertierung
Die Umwandlung von Bilddaten in dem Format, das Halcon und OpenCV nutzen, in das Format, das bmp nutzt, erfordert einige Schritte, die im Folgenden beschrieben werden. Bmp ist ein einfaches Format für die Speicherung und Übertragung von Bilddaten, das auf einem unkomprimierten Array von Pixelwerten basiert. Bmp unterstützt verschiedene Farbmodelle, aber das am häufigsten verwendete ist ebenfalls RGB. Ein bmp-Bild besteht aus einem Header und einem Datenblock, wobei der Header Informationen über die Breite, Höhe, Farbtiefe, Layout und das Padding des Bildes enthält, und der Datenblock die unkomprimierten Pixelwerte enthält.
Der erste Schritt bei der Umwandlung von Bilddaten in dem Format, das Halcon und OpenCV nutzen, in das Format, das bmp nutzt, ist die Anpassung des Layouts. Wie bereits erwähnt, gibt es zwei gängige Layouts: interleaved und planar. Bmp unterstützt nur das interleaved Layout, daher muss das Bild in diesem Layout vorliegen. Wenn das Bild in dem planar Layout vorliegt, muss es in das interleaved Layout umgewandelt werden. Dies kann durch eine einfache Umordnung der Pixelwerte im Bildpuffer erfolgen, indem man die Kanäle eines Pixels nacheinander speichert, statt sie getrennt zu speichern.
Der zweite Schritt bei der Umwandlung von Bilddaten in dem Format, das Halcon und OpenCV nutzen, in das Format, das bmp nutzt, ist die Anpassung des LineStride. Wie bereits erwähnt, ist der LineStride die Anzahl der Bytes, die zwischen dem Anfang einer Zeile und dem Anfang der nächsten Zeile liegen. Bmp erwartet, dass der LineStride ein Vielfaches von 4 ist, daher muss das Padding hinzugefügt oder entfernt werden, um diese Bedingung zu erfüllen. Dies kann durch eine einfache Erhöhung oder Verringerung der Größe des Bildpuffers erfolgen, indem man die entsprechende Anzahl von Bytes am Ende jeder Zeile hinzufügt oder ignoriert.
Der dritte Schritt bei der Umwandlung von Bilddaten in dem Format, das Halcon und OpenCV nutzen, in das Format, das bmp nutzt, ist die Umkehrung der Reihenfolge der Zeilen. Bmp speichert die Pixelwerte in einem Datenblock, der von unten nach oben und von links nach rechts gelesen wird. Das bedeutet, dass die erste Zeile im Datenblock die unterste Zeile im Bild ist, und die letzte Zeile im Datenblock die oberste Zeile im Bild ist. Dies ist das Gegenteil von dem Format, das Halcon und OpenCV nutzen, das die Pixelwerte von oben nach unten und von links nach rechts speichert. Um die Reihenfolge der Zeilen umzukehren, muss man die Zeilen im Bildpuffer vertauschen, indem man die erste Zeile mit der letzten Zeile, die zweite Zeile mit der vorletzten Zeile, und so weiter tauscht.
Der resultierende Datenblock ist das Ergebnis der Umwandlung von Bilddaten in dem Format, das Halcon und OpenCV nutzen, in das Format, das bmp nutzt. Der Datenblock ist gleich groß wie der ursprüngliche Bildpuffer, da keine Kompression angewendet wurde. Der Datenblock kann zusammen mit dem Header, der die Breite, Höhe, Farbtiefe, Layout und das Padding des Bildes enthält, als eine bmp-Datei gespeichert oder übertragen werden.
Ein einfacher C# Algorithmus
Im folgenden erkläre ich Schritt für Schritt die Umwandlung. Im Normalfall wird kein Bitmap zum Speichern in einer Datei benötigt, da Halcon das bereits beherrscht. Vielmehr benötigt man eine performante Umwandlung, damit die Bilddaten in einer WPF Anwendung in Echtzeit ohne große CPU Belastung dargestellt werden können.
Zu Demonstrationszwecken soll dieser Algorithmus reichen. Mit Bit Shifting und dem Lesen und Schreiben mehrerer Werte als int oder long (und dann natürlich mit pointern), kann dieser Algorithmus noch erheblich optimiert werden.
// Eine Methode, die einen Bildpuffer aus dem Halcon Format in einen Bildpuffer aus dem Bitmap Format umwandelt. // Parameter: halconBuffer - der Bildpuffer aus dem Halcon Format // width - die Breite des Bildes in Pixeln // height - die Höhe des Bildes in Pixeln // lineStride - die Anzahl der Bytes pro Zeile im Halcon Format // Rückgabewert: bitmapBuffer - der Bildpuffer aus dem Bitmap Format static byte[] ConvertHalconToBitmap(byte[] halconBuffer, int width, int height, int lineStride) { // Die Anzahl der Bytes pro Pixel im Halcon Format ist 3, da die Bit-Tiefe 24 ist. const int bytesPerPixel = 3; // Die Anzahl der Bytes pro Zeile im Bitmap Format ist die Breite mal die Bytes pro Pixel. // Außerdem muss sie ein Vielfaches von 4 sein, daher wird sie gegebenenfalls aufgerundet. int bitmapLineStride = (width * bytesPerPixel + 3) / 4 * 4; // Die Größe des Bildpuffers im Bitmap Format ist die Höhe mal die Bytes pro Zeile im Bitmap Format. int bitmapBufferSize = height * bitmapLineStride; // Erstelle einen neuen Bildpuffer für das Bitmap Format mit der entsprechenden Größe. byte[] bitmapBuffer = new byte[bitmapBufferSize]; // Überprüfe, ob das Bild in dem planar Layout vorliegt. bool isPlanar = lineStride == width; // Gehe durch jede Zeile des Bildes parallel mit mehreren Threads. Parallel.For(0, height, y => { // Berechne die Position der aktuellen Zeile im Halcon Format. // Die Zeilen sind von oben nach unten gespeichert. int halconRowPosition = y * lineStride; // Berechne die Position der aktuellen Zeile im Bitmap Format. // Die Zeilen sind von unten nach oben gespeichert, daher muss man die Reihenfolge umkehren. int bitmapRowPosition = (height - 1 - y) * bitmapLineStride; // Gehe durch jede Spalte der aktuellen Zeile. for (int x = 0; x < width; x++) { // Berechne die Position des aktuellen Pixels in beiden Formaten. int halconPixelPosition = halconRowPosition + x * bytesPerPixel; int bitmapPixelPosition = bitmapRowPosition + x * bytesPerPixel; // Kopiere die Pixelwerte aus dem Halcon Format in das Bitmap Format. // Wenn das Bild in dem planar Layout vorliegt, muss man die Kanäle umordnen. if (isPlanar) { bitmapBuffer[bitmapPixelPosition] = halconBuffer[halconPixelPosition + width * height * 2]; // Blau bitmapBuffer[bitmapPixelPosition + 1] = halconBuffer[halconPixelPosition + width * height]; // Grün bitmapBuffer[bitmapPixelPosition + 2] = halconBuffer[halconPixelPosition]; // Rot } else { bitmapBuffer[bitmapPixelPosition] = halconBuffer[halconPixelPosition]; // Blau bitmapBuffer[bitmapPixelPosition + 1] = halconBuffer[halconPixelPosition + 1]; // Grün bitmapBuffer[bitmapPixelPosition + 2] = halconBuffer[halconPixelPosition + 2]; // Rot } } }); // Gib den Bildpuffer aus dem Bitmap Format zurück. return bitmapBuffer; }