Umwandlung von Bilddaten

In der industriellen Bildverarbeitung ist die Umwandlung von Bilddaten zwischen verschiedenen Formaten Standard – vor allem, wenn Halcon, OpenCV oder EmguCV ins Spiel kommen. Wer .NET nutzt, steht oft vor der Herausforderung, Rohdaten in ein Format wie BMP zu bringen, sei es für Debugging, Anzeige in WPF oder den Export. Hier geht’s konkret um die Unterschiede zwischen den Formaten und wie man performant konvertiert – inklusive Erklärung des LineStride und Layouts (interleaved vs. planar).

Halcon (siehe Wikipedia) und OpenCV – bzw. EmguCV als .NET-Wrapper – nutzen in der Regel rohe Bildpuffer ohne Header. Die Bilddaten liegen je nach Bibliothek unterschiedlich strukturiert vor, was beim Speichern oder Darstellen Probleme verursachen kann. Gerade wenn man Bilder z. B. in WPF anzeigen will, sind Formatdetails entscheidend.

Das Farbmodell

Die Klassiker: RGB, HSV, CMYK und Graustufen. RGB ist der Standard in Halcon und OpenCV. Ein RGB-Bild besteht aus drei Kanälen – Rot, Grün, Blau. Jeder Kanal hat eine bestimmte Bit-Tiefe: 8 Bit (Standard), 16 Bit oder 32 Bit. Ein RGB-Bild mit 8 Bit pro Kanal kommt also auf 24 Bit – das sind 3 Byte pro Pixel.

Farbtiefe und Auflösung bestimmen Speicherbedarf und Performance. Für Entwickler bedeutet das: Wer Echtzeit will, muss wissen, was im Puffer passiert.

Interleaved und planar: Die zwei Layouts für Bildkanäle

Im interleaved-Layout sind die Farbkanäle eines Pixels direkt hintereinander gespeichert: R-G-B-R-G-B… Beim planar-Layout hingegen wird erst das ganze R, dann das ganze G, dann das ganze B gespeichert. Wichtig, wenn man auf einzelne Kanäle zugreifen oder zwischen Layouts konvertieren will.

Für BMP ist nur interleaved erlaubt – planar muss also vor dem Export umgewandelt werden. Das geht mit einer simplen Schleife, ist aber nichts für Copy-Paste-Cowboys: korrektes Mapping der Bytes ist Pflicht.

Der LineStride

LineStride = Anzahl der Bytes pro Zeile. Manchmal entspricht er Breite × Bytes pro Pixel. Manchmal nicht. Warum? Weil Speicherzugriffe auf 4-Byte-Grenzen ausgerichtet werden (Stichwort: Padding). Beispiel: Ein RGB-Bild mit 640 Pixeln Breite und 3 Byte pro Pixel ergibt 1920 Bytes. Aber der LineStride ist z. B. 1924, weil auf 4 aufgerundet wird.

Padding = zusätzliche Bytes am Ende jeder Zeile. Wichtig bei der Konvertierung zu BMP, weil BMP immer 4-Byte-aligned ist.

Positionsberechnung im Puffer:

Position = (Zeilennummer * LineStride) + (Spaltennummer * Bytes pro Pixel)

Beispiel: Pixel (100, 200), RGB mit 8 Bit pro Kanal, 640 Pixel breit, LineStride = 1924:

Position = (100 * 1924) + (200 * 3) = 193800

Die Konvertierung

BMP ist simpel: Header + Datenblock, keine Kompression. Wichtig: BMP braucht interleaved-Layout, 4-Byte-aligned LineStride und speichert Zeilen von unten nach oben. Daraus ergeben sich drei Aufgaben:

1. Layout anpassen: Planar → interleaved.

2. Padding ergänzen oder entfernen: LineStride auf 4-Byte-Grenze bringen.

3. Zeilen invertieren: BMP speichert die unterste Zeile zuerst.

Fertig ist der BMP-kompatible Datenblock – direkt verwendbar in Streams, UI oder Dateisystemen.

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;
}

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert