Einführung in die Python-Bildverarbeitung in der computergestützten Fotografie
Veröffentlicht: 2022-03-11Bei der computergestützten Fotografie geht es darum, den fotografischen Prozess durch Berechnung zu verbessern. Während wir normalerweise denken, dass dies nur für die Nachbearbeitung des Endergebnisses gilt (ähnlich wie bei der Fotobearbeitung), sind die Möglichkeiten viel umfangreicher, da die Berechnung bei jedem Schritt des fotografischen Prozesses aktiviert werden kann – angefangen bei der Szenenbeleuchtung bis hin zu am Objektiv und schließlich sogar bei der Anzeige des aufgenommenen Bildes.
Dies ist wichtig, da es viel mehr und auf andere Weise ermöglicht, als mit einer normalen Kamera erreicht werden kann. Es ist auch deshalb wichtig, weil der heutzutage am weitesten verbreitete Kameratyp – die mobile Kamera – im Vergleich zu seinem größeren Bruder (der DSLR) nicht besonders leistungsfähig ist, aber es schafft, gute Arbeit zu leisten, indem er die verfügbare Rechenleistung des Geräts nutzt .
Wir sehen uns zwei Beispiele an, bei denen die Berechnung die Fotografie verbessern kann – genauer gesagt, wir werden sehen, wie einfach das Aufnehmen von mehr Aufnahmen und die Verwendung von ein wenig Python, um sie zu kombinieren, in zwei Situationen, in denen die mobile Kamerahardware dies nicht tut, schöne Ergebnisse erzielen kann wirklich glänzen – geringes Licht und hoher Dynamikbereich.
Low-Light-Fotografie
Angenommen, wir möchten eine Szene bei schwachem Licht fotografieren, aber die Kamera hat eine kleine Blende (Linse) und eine begrenzte Belichtungszeit. Dies ist eine typische Situation für Handykameras, die bei einer schwach beleuchteten Szene ein Bild wie dieses erzeugen könnten (aufgenommen mit einer iPhone 6-Kamera):
Wenn wir versuchen, den Kontrast zu verbessern, ist das Ergebnis das folgende, was ebenfalls ziemlich schlecht ist:
Was geschieht? Woher kommt dieser ganze Lärm?
Die Antwort ist, dass das Rauschen vom Sensor kommt – dem Gerät, das versucht zu bestimmen, wann das Licht darauf trifft und wie intensiv dieses Licht ist. Bei schwachem Licht muss es jedoch seine Empfindlichkeit stark erhöhen, um etwas zu registrieren, und diese hohe Empfindlichkeit bedeutet, dass es auch anfängt, falsch positive Ergebnisse zu erkennen – Photonen, die einfach nicht da sind. (Nebenbei bemerkt, dieses Problem betrifft nicht nur Geräte, sondern auch uns Menschen: Wenn Sie das nächste Mal in einem dunklen Raum sind, nehmen Sie sich einen Moment Zeit, um das Rauschen in Ihrem Gesichtsfeld wahrzunehmen.)
In einem Abbildungsgerät ist immer ein gewisses Maß an Rauschen vorhanden; Wenn das Signal (nützliche Informationen) jedoch eine hohe Intensität hat, ist das Rauschen vernachlässigbar (hohes Signal-Rausch-Verhältnis). Wenn das Signal schwach ist – z. B. bei schwachem Licht – hebt sich das Rauschen ab (niedriges Signal-Rausch-Verhältnis).
Trotzdem können wir das Rauschproblem trotz aller Kameraeinschränkungen überwinden, um bessere Aufnahmen als die oben genannte zu erhalten.
Dazu müssen wir berücksichtigen, was im Laufe der Zeit passiert: Das Signal bleibt gleich (dieselbe Szene und wir nehmen an, dass es statisch ist), während das Rauschen völlig zufällig ist. Das bedeutet, dass, wenn wir viele Aufnahmen der Szene machen, sie unterschiedliche Versionen des Rauschens haben, aber die gleichen nützlichen Informationen.
Wenn wir also viele im Laufe der Zeit aufgenommene Bilder mitteln, wird das Rauschen aufgehoben, während das Signal unbeeinflusst bleibt.
Die folgende Abbildung zeigt ein vereinfachtes Beispiel: Wir haben ein Signal (Dreieck), das von Rauschen betroffen ist, und wir versuchen, das Signal wiederherzustellen, indem wir mehrere Instanzen desselben Signals mitteln, die von unterschiedlichem Rauschen betroffen sind.
Wir sehen, dass, obwohl das Rauschen stark genug ist, um das Signal in jedem einzelnen Fall vollständig zu verzerren, die Mittelung das Rauschen schrittweise reduziert und wir das ursprüngliche Signal wiederherstellen.
Mal sehen, wie dieses Prinzip auf Bilder angewendet wird: Zuerst müssen wir mehrere Aufnahmen des Motivs mit der maximalen Belichtung machen, die die Kamera zulässt. Verwenden Sie für beste Ergebnisse eine App, die manuelle Aufnahmen ermöglicht. Es ist wichtig, dass die Aufnahmen vom selben Ort gemacht werden, daher hilft ein (improvisiertes) Stativ.
Mehr Aufnahmen bedeuten im Allgemeinen eine bessere Qualität, aber die genaue Anzahl hängt von der Situation ab: wie viel Licht vorhanden ist, wie empfindlich die Kamera ist usw. Ein guter Bereich könnte irgendwo zwischen 10 und 100 liegen.
Sobald wir diese Bilder haben (möglichst im Rohformat), können wir sie in Python lesen und verarbeiten.
Für diejenigen, die mit der Bildverarbeitung in Python nicht vertraut sind, sollten wir erwähnen, dass ein Bild als ein 2D-Array von Bytewerten (0-255) dargestellt wird – das heißt, für ein monochromes oder Graustufenbild. Ein Farbbild kann man sich als einen Satz von drei solchen Bildern vorstellen, eines für jeden Farbkanal (R, G, B), oder effektiv ein 3D-Array, das nach vertikaler Position, horizontaler Position und Farbkanal (0, 1, 2) indiziert ist. .
Wir werden zwei Bibliotheken verwenden: NumPy (http://www.numpy.org/) und OpenCV (https://opencv.org/). Die erste ermöglicht es uns, Berechnungen auf Arrays sehr effektiv durchzuführen (mit überraschend kurzem Code), während OpenCV in diesem Fall das Lesen/Schreiben der Bilddateien übernimmt, aber viel leistungsfähiger ist und viele erweiterte Grafikverfahren bereitstellt – von denen wir einige verwenden werden später im Artikel verwenden.
import os import numpy as np import cv2 folder = 'source_folder' # We get all the image files from the source folder files = list([os.path.join(folder, f) for f in os.listdir(folder)]) # We compute the average by adding up the images # Start from an explicitly set as floating point, in order to force the # conversion of the 8-bit values from the images, which would otherwise overflow average = cv2.imread(files[0]).astype(np.float) for file in files[1:]: image = cv2.imread(file) # NumPy adds two images element wise, so pixel by pixel / channel by channel average += image # Divide by count (again each pixel/channel is divided) average /= len(files) # Normalize the image, to spread the pixel intensities across 0..255 # This will brighten the image without losing information output = cv2.normalize(average, None, 0, 255, cv2.NORM_MINMAX) # Save the output cv2.imwrite('output.png', output)
Das Ergebnis (mit angewendetem Autokontrast) zeigt, dass das Rauschen verschwunden ist, eine sehr große Verbesserung gegenüber dem Originalbild.
Wir bemerken jedoch immer noch einige seltsame Artefakte, wie den grünlichen Rahmen und das gitterartige Muster. Diesmal ist es kein zufälliges Rauschen, sondern ein festes Musterrauschen. Was ist passiert?
Auch hier können wir dem Sensor die Schuld geben. In diesem Fall sehen wir, dass verschiedene Teile des Sensors unterschiedlich auf Licht reagieren, was zu einem sichtbaren Muster führt. Einige Elemente dieser Muster sind regelmäßig und hängen höchstwahrscheinlich mit dem Sensorsubstrat (Metall/Silizium) zusammen und wie es einfallende Photonen reflektiert/absorbiert. Andere Elemente, wie das weiße Pixel, sind einfach defekte Sensorpixel, die zu lichtempfindlich oder zu unempfindlich sein können.
Glücklicherweise gibt es auch eine Möglichkeit, diese Art von Rauschen zu beseitigen. Dies wird als Dunkelbildsubtraktion bezeichnet.
Dazu benötigen wir ein Bild des Musterrauschens selbst, und das erhalten wir, wenn wir Dunkelheit fotografieren. Ja, das ist richtig – decken Sie einfach das Kameraloch ab und machen Sie viele Bilder (sagen wir 100) mit maximaler Belichtungszeit und ISO-Wert und verarbeiten Sie sie wie oben beschrieben.
Bei der Mittelung über viele schwarze Frames (die aufgrund des zufälligen Rauschens tatsächlich nicht schwarz sind) erhalten wir das Rauschen mit festem Muster. Wir können davon ausgehen, dass dieses feste Rauschen konstant bleibt, sodass dieser Schritt nur einmal erforderlich ist: Das resultierende Bild kann für alle zukünftigen Low-Light-Aufnahmen wiederverwendet werden.
So sieht der obere rechte Teil des Musterrauschens (Kontrast angepasst) für ein iPhone 6 aus:
Auch hier bemerken wir die gitterartige Textur und sogar etwas, das wie ein festsitzendes weißes Pixel aussieht.

Sobald wir den Wert dieses Dunkelbildrauschens (in der Variable average_noise
) haben, können wir ihn einfach von unserer bisherigen Aufnahme abziehen, bevor wir ihn normalisieren:
average -= average_noise output = cv2.normalize(average, None, 0, 255, cv2.NORM_MINMAX) cv2.imwrite('output.png', output)
Hier ist unser letztes Foto:
Hoher Dynamikbereich
Eine weitere Einschränkung, die eine kleine (mobile) Kamera hat, ist ihr kleiner Dynamikbereich, was bedeutet, dass der Bereich der Lichtintensitäten, bei dem sie Details erfassen kann, eher klein ist.
Mit anderen Worten, die Kamera kann nur ein schmales Band der Lichtintensitäten einer Szene erfassen; Die Intensitäten unterhalb dieses Bandes erscheinen als reines Schwarz, während die Intensitäten darüber als reines Weiß erscheinen, und alle Details aus diesen Bereichen gehen verloren.
Es gibt jedoch einen Trick, den die Kamera (oder der Fotograf) anwenden kann – und das ist die Anpassung der Belichtungszeit (die Zeit, in der der Sensor dem Licht ausgesetzt ist), um die Gesamtlichtmenge, die auf den Sensor gelangt, effektiv zu steuern Verschieben des Bereichs nach oben oder unten, um den am besten geeigneten Bereich für eine bestimmte Szene zu erfassen.
Aber das ist ein Kompromiss. Viele Details schaffen es nicht in das endgültige Foto. In den beiden Bildern unten sehen wir dieselbe Szene, die mit unterschiedlichen Belichtungszeiten aufgenommen wurde: eine sehr kurze Belichtung (1/1000 Sek.), eine mittlere Belichtung (1/50 Sek.) und eine Langzeitbelichtung (1/4 Sek.).
Wie Sie sehen können, kann keines der drei Bilder alle verfügbaren Details erfassen: Der Glühfaden der Lampe ist nur in der ersten Aufnahme sichtbar, und einige der Blumendetails sind entweder in der Mitte oder in der letzten Aufnahme sichtbar, aber nicht beide.
Die gute Nachricht ist, dass wir etwas dagegen tun können, und auch hier geht es darum, mit ein wenig Python-Code auf mehreren Einstellungen aufzubauen.
Der von uns verfolgte Ansatz basiert auf der Arbeit von Paul Debevec et al., der die Methode in seinem Artikel hier beschreibt. Die Methode funktioniert so:
Erstens erfordert es mehrere Aufnahmen derselben Szene (stationär), aber mit unterschiedlichen Belichtungszeiten. Auch hier benötigen wir, wie im vorherigen Fall, ein Stativ oder eine Stütze, um sicherzustellen, dass sich die Kamera überhaupt nicht bewegt. Wir brauchen auch eine manuelle Aufnahme-App (wenn Sie ein Telefon verwenden), damit wir die Belichtungszeit steuern und automatische Kameraanpassungen verhindern können. Die Anzahl der erforderlichen Aufnahmen hängt vom Bereich der im Bild vorhandenen Intensitäten ab (ab drei aufwärts), und die Belichtungszeiten sollten über diesen Bereich verteilt sein, sodass die Details, die wir bewahren möchten, in mindestens einem Schuss deutlich sichtbar sind.
Als nächstes wird ein Algorithmus verwendet, um die Reaktionskurve der Kamera basierend auf der Farbe derselben Pixel über die verschiedenen Belichtungszeiten hinweg zu rekonstruieren. Auf diese Weise können wir im Wesentlichen eine Karte zwischen der realen Szenenhelligkeit eines Punktes, der Belichtungszeit und dem Wert erstellen, den das entsprechende Pixel im aufgenommenen Bild haben wird. Wir werden die Implementierung der Debevec-Methode aus der OpenCV-Bibliothek verwenden.
# Read all the files with OpenCV files = ['1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg'] images = list([cv2.imread(f) for f in files]) # Compute the exposure times in seconds exposures = np.float32([1. / t for t in [1000, 500, 100, 50, 10]]) # Compute the response curve calibration = cv2.createCalibrateDebevec() response = calibration.process(images, exposures)
Die Antwortkurve sieht etwa so aus:
Auf der vertikalen Achse haben wir den kumulativen Effekt der Szenenhelligkeit eines Punktes und der Belichtungszeit, während wir auf der horizontalen Achse den Wert (0 bis 255 pro Kanal) haben, den das entsprechende Pixel haben wird.
Diese Kurve ermöglicht es uns dann, die umgekehrte Operation durchzuführen (was der nächste Schritt im Prozess ist) – angesichts des Pixelwerts und der Belichtungszeit können wir die tatsächliche Helligkeit jedes Punkts in der Szene berechnen. Dieser Helligkeitswert wird als Bestrahlungsstärke bezeichnet und misst die Menge an Lichtenergie, die auf eine Einheit der Sensorfläche fällt. Im Gegensatz zu den Bilddaten werden sie mit Fließkommazahlen dargestellt, da sie einen viel größeren Wertebereich widerspiegeln (daher ein hoher Dynamikbereich). Sobald wir das Bestrahlungsbild (das HDR-Bild) haben, können wir es einfach speichern:
# Compute the HDR image merge = cv2.createMergeDebevec() hdr = merge.process(images, exposures, response) # Save it to disk cv2.imwrite('hdr_image.hdr', hdr)
Für diejenigen von uns, die das Glück haben, ein HDR-Display zu haben (was immer häufiger vorkommt), ist es möglicherweise möglich, dieses Bild direkt in seiner ganzen Pracht zu visualisieren. Leider stecken die HDR-Standards noch in den Kinderschuhen, daher kann der Vorgang für verschiedene Displays etwas unterschiedlich sein.
Für den Rest von uns ist die gute Nachricht, dass wir diese Daten immer noch nutzen können, obwohl eine normale Anzeige erfordert, dass das Bild Byte-Wert-Kanäle (0-255) hat. Während wir auf einen Teil des Reichtums der Strahlungskarte verzichten müssen, haben wir zumindest die Kontrolle darüber, wie es geht.
Dieser Prozess wird als Tone-Mapping bezeichnet und umfasst die Umwandlung der Gleitkomma-Bestrahlungsstärkekarte (mit einem großen Wertebereich) in ein Standard-Byte-Wert-Bild. Es gibt Techniken, um dies zu tun, damit viele der zusätzlichen Details erhalten bleiben. Um Ihnen nur ein Beispiel dafür zu geben, wie dies funktionieren kann, stellen Sie sich vor, dass wir die im HDR-Bild vorhandenen Kanten verbessern (schärfen), bevor wir den Gleitkommabereich in Bytewerte komprimieren. Das Verbessern dieser Kanten trägt dazu bei, sie (und implizit die von ihnen bereitgestellten Details) auch in dem Bild mit niedrigem Dynamikbereich zu erhalten.
OpenCV bietet eine Reihe dieser Tone-Mapping-Operatoren, wie Drago, Durand, Mantiuk oder Reinhardt. Hier ist ein Beispiel dafür, wie einer dieser Operatoren (Durand) verwendet werden kann und welches Ergebnis er erzeugt.
durand = cv2.createTonemapDurand(gamma=2.5) ldr = durand.process(hdr) # Tonemap operators create floating point images with values in the 0..1 range # This is why we multiply the image with 255 before saving cv2.imwrite('durand_image.png', ldr * 255)
Mit Python können Sie auch Ihre eigenen Operatoren erstellen, wenn Sie mehr Kontrolle über den Prozess benötigen. Dies ist beispielsweise das Ergebnis, das mit einem benutzerdefinierten Operator erzielt wird, der Intensitäten entfernt, die in sehr wenigen Pixeln dargestellt werden, bevor der Wertebereich auf 8 Bit verkleinert wird (gefolgt von einem automatischen Kontrastschritt):
Und hier ist der Code für den obigen Operator:
def countTonemap(hdr, min_fraction=0.0005): counts, ranges = np.histogram(hdr, 256) min_count = min_fraction * hdr.size delta_range = ranges[1] - ranges[0] image = hdr.copy() for i in range(len(counts)): if counts[i] < min_count: image[image >= ranges[i + 1]] -= delta_range ranges -= delta_range return cv2.normalize(image, None, 0, 1, cv2.NORM_MINMAX)
Fazit
Wir haben gesehen, wie wir mit ein bisschen Python und ein paar unterstützenden Bibliotheken die Grenzen der physischen Kamera erweitern können, um das Endergebnis zu verbessern. Beide Beispiele, die wir besprochen haben, verwenden mehrere Aufnahmen mit geringer Qualität, um etwas Besseres zu schaffen, aber es gibt viele andere Ansätze für unterschiedliche Probleme und Einschränkungen.
Während viele Kamerahandys über Store- oder integrierte Apps verfügen, die sich mit diesen speziellen Beispielen befassen, ist es eindeutig überhaupt nicht schwierig, diese von Hand zu programmieren und das höhere Maß an Kontrolle und Verständnis zu genießen, das erreicht werden kann.
Wenn Sie an Bildberechnungen auf einem mobilen Gerät interessiert sind, sehen Sie sich das OpenCV-Tutorial an: Real-time Object Detection Using MSER in iOS von Toptaler und Elite-OpenCV-Entwickler Altaibayar Tseveenbayar.