5.3. Pixelformate

Ein Algorithmus, der Kanten erkennt, erwartet, dass jeder Pixel einen Helligkeitswert enthält. Ein Algorithmus, der ein farbiges Objekt verfolgt, erwartet, dass jeder Pixel Farbe trägt. Ein Algorithmus, der ein morphologisches Schließen ausführt, erwartet, dass jeder Pixel entweder ein- oder ausgeschaltet ist. Das Pixelformat, das ein Image trägt – eines der im Katalog der Vision Sensors aufgeführten – macht diese Erwartungen von vornherein überprüfbar: Das Format gibt im Voraus an, in welcher Form die Pixel vorliegen und welche Algorithmen daher ohne einen Konvertierungsschritt darauf laufen können.

Auf dieser Seite geht es darum, wie sich diese Einschränkung in der Praxis auswirkt. Welches Format die richtige Wahl ist, hängt davon ab, was die Pipeline tun soll, und die Konvertierungsmethoden zwischen den Formaten sind das Mittel, mit dem eine Pipeline, die mehr als eines davon benötigt, die Stufen miteinander verknüpft.

Ein vertikaler Stapel von fünf beschrifteten Byte-Layout-Streifen. BINARY zeigt ein Byte, aufgeteilt in acht Einzel-Bit-Zellen, markiert mit "8 pixels per byte". GRAYSCALE zeigt drei beschriftete Einzel-Byte-Zellen, jeweils markiert mit "1 pixel". RGB565 zeigt zwei benachbarte Bytes mit den Bitfeldern RRRRR GGGGGG BBBBB, beschriftet mit "1 pixel". YUV422 zeigt vier beschriftete Byte- Zellen Y0, U, Y1, V, markiert mit "2 pixels". BAYER zeigt zwei Reihen mit je vier beschrifteten Byte- Zellen: R G R G in der oberen Reihe, G B G B in der unteren Reihe.

Die fünf unkomprimierten Pixelformate und wie ihre Bytes gepackt werden. JPEG und PNG sind hier nicht dargestellt, weil sie komprimierte Datenströme variabler Länge und keine Pixelraster fester Größe sind.

5.3.1. Das Arbeitspferd Graustufen

Der größte Teil des klassischen maschinellen Sehens läuft darauf hinaus, mit Helligkeitswerten zu arbeiten. Kantenerkennung, Template Matching, AprilTag-Dekodierung, Schätzung des optischen Flusses, die morphologischen Operatoren, Blob-Analyse – sie alle betrachten auf der Ebene, auf der die Algorithmen arbeiten, wie hell jeder Pixel ist und wie sich die Helligkeit im Vergleich zur Helligkeit benachbarter Pixel verhält. Die Farbe der Szene ist für die aufrufende Anwendung oft nützlich, aber die Algorithmen selbst benötigen sie nicht.

Das Graustufenformat liefert den Algorithmen genau das, ohne Overhead. Ein Byte pro Pixel enthält einen Helligkeitswert von 0 (schwarz) bis 255 (weiß). Das Format ist halb so groß wie RGB565 und YUV422 und ein Drittel so groß wie RGB888, sodass jede Operation weniger Daten verarbeitet – sowohl schneller als auch mit geringerem Druck auf den Puffer. Auf den kleineren Kameras, wo der Framebuffer mit dem Rest des Skripts um RAM konkurriert, kann dieser Unterschied im Speicherbedarf darüber entscheiden, ob eine Pipeline überhaupt hineinpasst. Wenn Farbe nicht das Merkmal ist, das der Algorithmus benötigt, sind Graustufen die richtige Antwort.

5.3.2. Farbe über RGB565

Wenn Farbe das Merkmal ist – das Verfolgen einer farbigen Markierung, das Unterscheiden roter Äpfel von grünen, das Herausgreifen eines UI-Elements anhand seines Farbtons – erkaufen zwei Bytes pro Pixel genügend Farbe für die Art von Klassifizierung, die die Algorithmen durchführen. RGB565 ist das Standard-Farbformat auf der Kamera und das, was die farbbewussten Methoden auf der Oberfläche erwarten.

Auch das Rendern eines annotierten Einzelbildes – das Zeichnen von Erkennungsrahmen, das Schreiben von Diagnosetext, das Ausgeben des Einzelbildes auf einen Bildschirm oder an einen entfernten Betrachter – ruft ganz natürlich nach RGB565. Die IDE-Vorschau, die On-Board-Display-Controller und die meisten Netzwerkziele verarbeiten das Format entweder direkt oder konvertieren günstig daraus.

5.3.3. Bayer als Speicherformat

Ein Bayer-Bild ist die rohe Sensorausgabe, bevor der ISP es in eine fertige Farbdarstellung debayert hat. Jeder Pixel ist ein Byte, das einen einzelnen Farbkanal enthält – denjenigen, den der Farbfilter an dieser Position im Mosaik durchgelassen hat. Dadurch ist ein Bayer-Bild genauso groß wie ein Graustufenbild und ein Drittel so groß wie RGB888, was zu dem passt, wofür Bayer tatsächlich nützlich ist: das gleichzeitige Speichern vieler Einzelbilder, wenn RAM die begrenzende Bedingung ist.

Der Haken ist, dass die Algorithmen im image-Modul nicht direkt auf Bayer-Bildern arbeiten. Ohne Debayering trägt kein Pixel genügend Informationen, um für sich allein ein Farburteil zu fällen, und die Muster, nach denen die Algorithmen suchen – Kanten, Ecken, Blobs – würden durch das Mosaik verzerrt. Die einzigen Möglichkeiten, ein Bayer-Bild zu lesen oder zu verändern, sind get_pixel() und set_pixel(); alles andere erwartet eine fertige Darstellung.

Das sich daraus ergebende Muster besteht darin, Einzelbilder so lange als Bayer zu speichern, wie sie in einer Warteschlange verbleiben müssen, und jedes in dem Moment, in dem seine Verarbeitung tatsächlich beginnt, entweder in Graustufen oder in RGB565 zu konvertieren. Die Konvertierung kostet CPU-Zyklen, spart aber den RAM, der sonst durch das Vorhalten fertiger Einzelbilder über die Lebensdauer der Anwendung gebunden wäre.

Bemerkung

Die einzigen Operationen des image-Moduls direkt auf Bayer-Pixeln sind get_pixel(), set_pixel() und der JPEG-Kodierungspfad, der die IDE-Vorschau oder einen entfernten Betrachter speist. Zeichnen, Analyse und Filterung erfordern alle zunächst eine Konvertierung nach Graustufen, RGB565 oder binär.

5.3.4. YUV422 für Pipelines, die beides wollen

YUV422 trennt die Information jedes Pixels in einen Luminanzkanal (Y) und zwei Chrominanzkanäle (U und V) und unterabtastet die Chrominanz, sodass benachbarte Pixelpaare sich ein einzelnes U und ein einzelnes V teilen. Die Bytes pro Pixel mitteln sich auf zwei – genauso wie bei RGB565 –, aber sie sind so angeordnet, dass der Y-Kanal bereits ein zusammenhängendes 8-Bit-Graustufenbild ist, das an bekannten Offsets im Puffer liegt.

Genau dieses Layout will eine Pipeline, wenn einige ihrer Stufen Graustufenarbeit sind und einige Farbe benötigen. Das direkte Lesen der Y-Werte für die Graustufenstufen erspart die Kosten einer expliziten Konvertierung; die U- und V-Kanäle sind vorhanden, wenn eine spätere Stufe tatsächlich Farbe benötigt. Außerhalb dieses speziellen Musters ist RGB565 für Farbe in der Regel die einfachere Wahl und Graustufen die einfachere Wahl für reine Helligkeitsarbeit – der Wert von YUV422 ergibt sich daraus, dass es beides gleichzeitig gut beherrscht.

Bemerkung

Das image-Modul arbeitet mit YUV422 auf eingeschränktere Weise als mit Graustufen, RGB565 oder binär – direkte Y-Kanal-Lesevorgänge für Graustufenarbeit und der JPEG-Kodierungspfad, der die IDE-Vorschau oder einen entfernten Betrachter speist. Farbbewusste Methoden erwarten RGB565; YUV422-Einzelbilder benötigen eine explizite Konvertierung, bevor eine Farbanalyse oder ein Zeichnen erfolgt.

5.3.5. Binär, Masken und geschwellte Ausgabe

Ein Binärbild ist ein Bit pro Pixel: Jeder Pixel ist entweder 0 oder 1. Das Format taucht selten als Sensoraufnahme auf; stattdessen erscheint es als natürliche Ausgabe des Schwellenwertverfahrens (bei dem ein Farb- oder Helligkeitstest jeden Pixel in „ja, passt“ oder „nein, passt nicht“ klassifiziert) und als natürliche Eingabe für morphologische Operationen und für das mask-Argument, das viele Methoden akzeptieren.

Der praktische Vorteil des Formats ist seine Größe. Ein Binärbild hat ein Achtel des Speicherbedarfs eines Graustufenbildes, sodass das Mitführen einer großen Maske – einer pixelweisen Auswahl, welche Positionen eine nachgelagerte Operation berühren soll – günstig ist. Die Tatsache, dass viele Operationen ein Binärbild als mask=-Schlüsselwortargument akzeptieren, ist die Kehrseite desselben Punktes: Das Format ist klein, und das Verketten der binären Ausgabe einer Stufe in die Maskeneingabe einer anderen ist ein gängiges Pipeline-Muster.

5.3.6. JPEG und PNG an der Grenze

JPEG- und PNG-Image-Objekte unterscheiden sich von den anderen im Katalog. Sie sind keine Pixelraster; sie sind komprimierte Byteströme, die Pixeldaten in einer Form kodieren, die Operationen auf Pixelebene nicht lesen können. Der Aufruf von get_pixel() bei einem JPEG gibt nicht den Pixel an einer Position zurück; der Pixel liegt nirgendwo entpackt im Puffer, von wo die Methode ihn abrufen könnte.

JPEG und PNG tauchen an der Grenze der Bildverarbeitung auf, wo Pixeldaten die Kamera in komprimierter Form verlassen oder betreten. Das Speichern eines Einzelbildes als JPEG auf der Festplatte hält die Datei klein; das Senden eines Einzelbildes über ein Netzwerk als JPEG hält die Übertragung günstig; das Laden eines Referenzeinzelbildes aus einer JPEG-Datei lässt es in einer viel kleineren Form auf der Festplatte liegen, als es die rohen Pixel würden. Für jeden dieser Anwendungsfälle ist die komprimierte Darstellung die richtige Antwort. Um jedoch eine tatsächliche Verarbeitung an einem JPEG durchzuführen, konvertiert die Anwendung es zunächst in ein verwendbares Format – und bei dieser Konvertierung werden die komprimierten Bytes in Pixel expandiert und dort findet das Anschwellen des Puffers (ein 30 KB großes JPEG kann zu 600 KB RGB565 werden) tatsächlich statt.

5.3.7. Konvertierung zwischen Formaten

Der Konvertierungspfad ist das, was verschiedene Formate zu einer einzigen Pipeline zusammenfügt. Fünf Methoden der Klasse Image nehmen ein vorhandenes Bild und geben ein neues in einem anderen Format zurück:

  • to_grayscale() erzeugt ein Bild mit einem Byte pro Pixel, das Format, das die klassischen Algorithmen wollen.

  • to_rgb565() erzeugt das Zwei-Byte-pro-Pixel-Farbformat, das sowohl die farbbewussten Methoden als auch die IDE-Vorschau sprechen.

  • to_bitmap() erzeugt ein Ein-Bit-Binärbild, das Format, das Morphologie und mask-Argumente akzeptieren.

  • to_jpeg() erzeugt ein JPEG-komprimiertes Bild, das sich zum Speichern oder Übertragen eignet.

  • to_png() erzeugt ein PNG-komprimiertes Bild, wenn eine verlustfreie Kodierung den kleineren Dateien von JPEG vorgezogen wird.

Jede Konvertierung läuft standardmäßig direkt ab: Der Puffer des Quellbildes wird mit dem konvertierten Ergebnis überschrieben, und die ursprünglichen Pixel der Quelle sind nach der Rückkehr des Aufrufs verschwunden. Das ist sowohl für die CPU als auch für den Speicher die günstigste Option, und es ist die richtige Antwort, wenn das Quelleinzelbild für nichts anderes mehr benötigt wird.

Wenn die Quelle noch benötigt wird – wenn eine spätere Stufe der Pipeline das ursprüngliche Einzelbild sehen muss – überschreiben zwei Schlüsselwortargumente das Standardverhalten der direkten Konvertierung. copy=True reserviert einen separaten Puffer für das konvertierte Bild auf dem Python-Heap und lässt die Quelle unverändert. copy_to_fb=True führt dieselbe Reservierung durch, legt sie aber stattdessen im Framebuffer statt auf dem Heap ab – worauf eine Anwendung zurückgreift, wenn das konvertierte Bild in der IDE-Vorschau landen soll, da die IDE aus dem Framebuffer liest.

Zwei weitere Methoden erzeugen RGB565-Bilder, die über eine Palette statt durch eine direkte Konvertierung eingefärbt werden. to_rainbow() bildet jeden Eingabewert eines einzelnen Kanals auf eine Farbe entlang eines weichen Gradienten ab, der durch das sichtbare Spektrum verläuft. to_ironbow() bildet jeden Eingabewert auf die nichtlineare Wärmebildkamera-Palette ab, die von Schwarz über dunkle Rottöne und Orangetöne bis Weiß verläuft. Beide sind Visualisierungs-Werkzeuge und keine Messwerkzeuge; das Ziel ist es, ein Bild mit einem einzigen Kanal, dessen Rohwerte sonst für das Auge unsichtbar wären, auf einen Blick lesbar zu machen.

5.3.8. Puffergröße

Ein letztes Detail zu Formaten, das es wert ist, ausdrücklich erwähnt zu werden. size() meldet immer die Größe des Byte-Puffers, nicht die Pixelanzahl. Für unkomprimierte Formate ergibt sich das direkt aus den Abmessungen und den Bytes pro Pixel: width * height * bytes_per_pixel. Für JPEG und PNG ist es die Größe des komprimierten Stroms, die von Einzelbild zu Einzelbild variiert, je nachdem, was die Szene enthält. Code, der Puffer aus Byte-Budgets reserviert, verwendet size() für den ersten Fall; Code, der komprimierte Einzelbilder aus der Kamera streamt, liest sie nach jeder Komprimierung, um zu wissen, wie viele Bytes der Strom tatsächlich enthält.