Raspberry Video Camera – Teil 20: Exkurs – Farbdarstellung per 2D-Histogramm

Farbhistogramme helfen bei der Beurteilung, welche Farben wie häufig in einem Bild vertreten sind. Vereinfacht ausgedrückt zumindest. Eindimensionale Histogramme haben wir bei der Farbkalibrierung des Python-Kamera-Triggerprogramms bereits verwendet. Für die drei HSV-Farbkanäle Hue, Saturation und Value, hatten wir zu diesem Zweck drei Histogramme generiert und daraus die Grenzen der Eichhörnchenfarben abgelesen. Nun stelle ich das 2D-Histogramm vor, mit dem wir zwei Kanäle gleichzeitig berechnen und visualisieren können – typischerweise Hue und Saturation.


Diesmal gibt es kein Oachkatzl-Video sondern eine Zeitrafferaufnahme des Bildhintergrunds mit dem zugehörigen Hue-Saturation-Histogramm. Alle Eichhörnchen-Videos sind natürlich weiterhin in meinem YouTube-Kanal.

Zur Wahrung deiner Privatsphäre wird erst eine Verbindung zu YouTube hergestelt, wenn du den Abspielbutton betätigst.

Ein- und zweidimensionale Histogramme

Um eine automatische Farberkennung eine Videokamera triggern lassen zu können, müssen wir die Farbe(n) unseres Zielobjekts kennen. Hier im Beispiel links ist das Zielobjekt ein rotbraunes Eichhörnchen. Das wurde auf dem Bild grob freigestellt, also vom Bildhintergrund befreit. Der Hintergrund ist nun transparent, deshalb scheint die weiße Blogseite durch. Zahlenmäßig hat The Gimp, mit dem ich die Freistellung vorgenommen habe, den Hintergrund als schwarz kodiert. Per Farbhistogramm können wir nun die Farbverteilung aus dem Bild herauslesen. Das eindimensionale Histogramm hat auf der X-Achse den jeweiligen Farbkanal aufgetragen und auf der Y-Achse die zugehörige Anzahl der im Bild vorhandenen Pixel. Für die drei Farbkanäle Hue, Saturation und Value sind das also drei Histogramme.

Wenn wir nun beschließen würden, den Value-Wert, der für die Helligkeit des jeweiligen Bildpunktes steht, zu vernachlässigen, dann hätten wir es nur noch mit zwei Farbkanälen, nämlich Hue und Saturation zu tun. Das können wir auch ruhig machen, denn die Helligkeit unserer Objekte (Eichhörnchen) nimmt auf der Skala einen relativ breiten Bereich ein, so dass der Value-Kanal am allerwenigsten zu einer treffsicheren Erkennung beiträgt.

Zwei Farbkanäle, also Hue (Farbton) und Saturation (Sättigung), können wir dann leicht zu einem zweidimensionalen Histogramm vereinigen. Hue tragen wir auf der X-Achse auf, Saturation auf der Y-Achse und die Pixelanzahl für das jeweilige Hue-Saturation-Paar wird durch eine Falschfarbe repräsentiert. Falschfarbe deshalb, weil die Farbe nichts mit der realen Bildfarbe zu tun hat, sondern nur für einen Zahlenwert steht. Die Zuordnung kann man der nebenstehenden Farbskala entnehmen. Blau steht hier für Null und Braun für den Maximalwert 100.

Vergleichen wir das 2D-Histogramm des rotbraunen Eichhörnchens mit den beiden Einzel-Histogrammen für Hue und Saturation (nur jeweils die roten Kurven), dann eröffnet sich der Zusammenhang. Die rote Kurve schlägt für den Farbwert (Hue) nur ganz links bei niedrigen Zahlenwerten aus. Entsprechend befindet sich der Farbbalken auch im 2D-Histogramm ziemlich links. Die Sättigungskurve der roten Linie ist relativ breit und hat ihr Maximum in etwa in der Mitte der Skala. Und genauso ist es, wenn wir das auf die Y-Achse des 2D-Histogramms übertragen: Das Maximum liegt mit dem braunen Kästchen in der Mitte – nach oben und unten fallen die Werte gegen blau (null) ab.

Die Darstellung als 2D-Histogramm ist erst einmal ungewohnt und sie wirkt auch weniger genau, als die beiden einzelnen 1D-Histogramme. Das hat nicht nur mit der Falschfarbendarstellung zu tun, sondern auch mit der Tatsache, dass ich die Auflösung radikal verringert habe. Bei den 1D-Histogrammen haben wir auf der X-Achse eine Auflösung von 180 Skalenteilen (Bins) für Hue und 256 für Saturation. Beim 2D-Histogramm sind es jeweils nur 30 Bins. Daher kommt die grobe Pixeligkeit auf der blauen Fläche, die aber durchaus gewollt ist. 30 Skalenteile (Bins) in beide Richtungen reichen durchaus aus, um einen qualitativen Eindruck von der Farbverteilung zu erhalten. 180 x 256 Bins wären zwar zahlenmäßig exakter, der optische Eindruck aber eher schlierenhaft und verwaschen. 30 Bins – das ist erst mal eine völlig willkürliche Wahl meinerseits, wenn wir später mit 2D-Histogrammen rechnen wollen, dann gilt es einen Kompromiss zu finden zwischen Auflösung und Datenumfang. Denn klar ist, dass sich mit einer Matrix aus 30 x 30 Werten einfacher und schneller rechnen lässt als mit einer in der Größe von 180 x 256.

Das 2D-Histogramm hat in meinen Augen gegenüber den 2 (oder 3) Einzelhistogrammen folgende Vorteile:

  • Es fasst zwei Grafiken zu einer einzigen zusammen
  • Es ermöglicht einen schnellen qualitativen Überblick
  • Auch rechnerisch repräsentiert ein einziges Histogramm (ein Python-Objekt) zwei Farbkanäle

2D-Histogramm im Video

Und damit ist nun klar, was in dem Video oben passiert. Der Tag von Sonnenaufgang bis Sonnenuntergang wird in ca. eineinhalb Minuten durchlaufen und die verschiedenen Beleuchtungssituationen zeigen ihre Auswirkungen direkt im Histogramm. Der Regen gegen 20 Uhr zum Beispiel verbreitert das Farbspektrum und fährt gleichzeitig die Sättigung im Bild zurück. Und auch in dunkler Nacht scheint es noch Farben zu geben – hier Hue 8 bei Saturation 5 – nur Helligkeit ist natürlich keine mehr vorhanden.

2D Histogramm berechnen und anzeigen

import cv2
import numpy as np
import matplotlib
matplotlib.use('Agg') 
from matplotlib import pyplot as plt

fileIn  = "samples/r33.png"                                     # image file name
fileOut = "hist2d.png"                                          # histogram file name

img = cv2.imread(fileIn,1)                                      # load image
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)                      # convert to HSV
hist = cv2.calcHist([hsv],[0,1],None,[30,30],[0,180,0,256])     # calc hist satu + hue
hist[0,0]=0                                                     # delete bachground
cv2.normalize(hist,hist,0,100,cv2.NORM_MINMAX)                  # normalize to 100

plt.imshow(hist.T, interpolation = "nearest", origin = "lower") # plot histogram
plt.title('Hue - Saturation - Histogram')
plt.xlabel('Hue')
plt.ylabel('Saturation')
plt.colorbar(ticks=(0,20,40,60,80,100))
plt.savefig(fileOut)                                            # save histogram
plt.close()
print(fileIn, 'processed')

Das kleine Python-Programm bietet nicht viel Neues. Eine Image-Datei wird in OpenCV geladen und in den HSV-Farbraum konvertiert, so dass wir die Farbkanäle Hue, Saturation und Value haben. Dann wird das Histogramm erzeugt mit:

hist = cv2.calcHist([hsv],[0,1],None,[30,30],[0,180,0,256])

Diesen Befehl hatten wir auch schon bei der Farbkalibrierung verwendet, allerdings drei mal und jeweils nur für einen Farbkanal. Hier kommt nun der zweite Farbkanal hinzu und damit die zweite Dimension. Bei den Kanälen wird 0 und 1 angegeben, jeweils 30 Bins für beide und als Ranges 0-180 für Kanal 0 (Hue) und 0-256 für Kanal 1 (Saturation).

Da es sich bei diesem Beispielbild um ein freigestelltes Eichhörnchen handelt, müssen wir den Hintergrund zahlenmäßig eliminieren, sonst hätte dessen Farbe im Histogramm den absoluten Spitzenwert. Im Beispielbild ist der Hintergrund transparent und zahlenmäßig schwarz, deshalb wird die Histogrammposition [0,0] auf Null gesetzt.

Und danach erfolgt noch eine Normalisierung auf den Wert 100. Das ist zum Beispiel sehr wichtig im Video (ganz oben), damit die Farbskala rechts für alle Einzelbilder konstant bleibt.

Die Ausgabe eines 2D-Histogramms mit Pyplot erfolgt allerdings auf eine andere Weise als gewohnt. Das 1D-Histogramm hatten wir mit plt.plot erzeugt, nun verwenden wir:

plt.imshow(hist.T, interpolation = "nearest", origin = "lower")
  • plt.imshow plottet das Histogramm-Array als Image mit X- und Y-Achse.
  • hist.T verweist auf das Histogramm-Array. Das .T verdreht dabei das Array, so dass Hue auf der X-Achse zu liegen kommt und Saturation auf der Y-Achse. imshow würde das nämlich sonst genau andersherum machen.
  • interpolation = "nearest" sorgt dafür, dass klare Rechtecke dargestellt und keine Farbverläufe interpoliert werden.
  • Und origin = "lower" verlegt den Ursprung des Diagramms nach unten links. Der würde per Standard oben links liegen.

Neben Titel und Achsenbeschriftungen fügen wir vor dem Speichern noch rechts den Farbbalken hinzu mit plt.colorbar. Den Parameter ticks kann man auch weglassen, die Vorgabe sorgt aber dafür, dass mehrere Histogramme (wie im Video) eine einheitliche Beschriftung erhalten.

Warum dann nicht gleich ein 3D-Histogramm?

Wenn ein 2D-Histogramm besser ist, als 1D, dann liegt die Frage nahe, warum man nicht gleich ein 3D-Histogramm erzeugen sollte, um dann den Value-Kanal noch mit an Bord zu haben. Abgesehen davon, dass der Value-Kanal entbehrlich ist, wie ich oben schon erwähnt habe, gibt es ein paar Gründe und Hindernisse, die gegen das 3D-Histogram sprechen:

  • Zur Darstellung bräuchten wir die dritte Dimension und damit ein räumliches Bild. Und das sollte dreh- und schwenkbar sein um es von allen Seiten betrachten zu können. Mit einem einfachen Image geht das nicht.
  • Pyplot hat auch keine 3D-Darstellungsfunktion.
  • Zum Rechnen wäre ein 3D-Histogramm reizvoll, aber es würde ein wesentlich höheres Datenvolumen beanspruchen und zur Verarbeitung mehr Rechnerzeit.
  • Angesehen davon funktioniert cv2.calcBackProject (siehe nächster Artikel) mit drei Dimensionen nicht. Zumindest ist es mir nicht gelungen und was man im Internet so findet, anderen auch nicht.

Wofür braucht man das?

Meine Artikelserie zur Farberkennung als Kameraauslöser ist noch nicht zu Ende. In den folgenden Beiträgen werde ich bevorzugt das 2D-Histogramm verwenden um Farbverteilungen zu veranschaulichen.Und das 2D-Histogramm wird zusammen mit der Histogramm-Rück-Projektion das nächste Mittel zur Farberkennung werden.

Diejenigen, die selbst programmieren wollen und meine Programme als Anregung verwenden, die haben mit den 2D-Histogrammen ein Werkzeug zur Hand, um schnell mal das zu visualisieren, was das Programm errechnet. Ich empfehle, die Pyplot-Ausgabe aus obigem Programmcode während der Entwicklungs- und Testphase in eigene Programme einzubauen. In der Produktivphase kann man sie dann auskommentieren um Verarbeitungszeit zu sparen.


Weitere Artikel in dieser Kategorie:

1 Kommentar

  1. Detlef Brauckhoff

    Geniale Idee, nun sogar 2D-Histogramme zu verwenden.

    Antworten

Schreiben Sie einen Kommentar

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