Einfacher Fotoeditor
In dieser Anwendung wollen wir die Belichtung eines Fotos verändern. Bevor wir beginnen, müssen wir verstehen, woraus ein digitales Foto besteht, d. h. wie der Computer die fotografische Information “versteht” und wie sie dargestellt wird.
Hinter einem Bild stehen 3 Kanäle: RGB – RED (Rot), GREEN (Grün), BLUE (Blau). Die Farbe eines Pixels ergibt sich aus der Summe der Intensitäten der Subpixel in jedem Kanal. Für einen Standard-8-Bit-Kanal kann die Intensität jeder Farbe Werte zwischen [0, 255] annehmen, wobei 0 für Schwarz/Aus und 255 für maximale Intensität steht.
Um also die Belichtung des Fotos zu manipulieren, müssen wir lediglich die Intensitätswerte aller Kanäle gleichzeitig verändern: Erhöhen der Werte zur Aufhellung (Überbelichtung) bzw. Verringern zur Unterbelichtung.
Voraussetzungen:
- Entwicklungsumgebung, z. B. VS Code
- Git for Windows, um die Bibliotheken herunterzuladen, die uns bei der Erstellung der Anwendung helfen.
- Installation von vcpkg, das die Kompilierung der Bibliotheken für uns übernimmt.
- Befehle, die in der PowerShell ausgeführt werden müssen:
- cd C:</li>
- git clone https://github.com/microsoft/vcpkg.git
- cd vcpkg
- .\bootstrap-vcpkg.bat
- .\vcpkg integrate install
- .\vcpkg install glfw3 imgui[opengl3-binding,glfw-binding] stb
Warum das alles?
- GLFW3 – zur Fenstererstellung und Maussteuerung
- OpenGL – zur Kommunikation mit der Grafikkarte
- Dear ImGui – für den Slider, mit dem wir die Belichtung regeln
- stb_image – hilft uns, eine .jpg-Datei in Pixelvektoren umzuwandeln, die wir beliebig manipulieren können.
Wir besprechen nun, wie ich die Anwendung angegangen bin. Zuerst muss ich das Bild lesen können, das ich modifizieren werde. Ich benötige daraus die Pixelintensitäten jedes Kanals, die ich in einen Vektor speichere. Danach kann ich sie durch Wertänderung manipulieren. Schließlich müssen sie zum erneuten Lesen zurückgesendet werden.
Wofür steht jede Bibliothek/jeder Header?
#include “stb_image.h” – der Bildleser. Damit verwandeln wir das Foto in eine riesige Zahlenfolge im RAM, also die Pixel.
#include <GLFW/glfw3.h> – der oben erklärte Teil von GLFW
#include “imgui.h” – für Buttons und Slider
#include “imgui_impl_glfw.h” – Verbindung zwischen ImGUI und dem von GLFW erstellten Fenster (Mausposition auf dem Bildschirm)
#include “imgui_impl_opengl3.h” – Zeichnen über die Grafikkarte
Wir müssen verstehen, dass das Bild, das wir verarbeiten, eigentlich ein Pointer auf einen Bereich im RAM ist. Daher nutzen wir den abstrakten Datentyp “struct raw_pic”, um Platz für das Originalbild (das wir nicht verändern) und für das modifizierte Bild (eine geänderte Kopie des Originals) zu reservieren.

Kommen wir zur Belichtungslogik. Wir müssen den gesamten Vektor der Werte durchlaufen. Da unser Bild eine Breite und eine Höhe hat, entspricht die Anzahl der zu verarbeitenden Werte dem Produkt aus beiden, multipliziert mit 3. Warum habe ich trotzdem mit 4 multipliziert? Es gibt noch einen Alpha-Kanal, über den wir nicht gesprochen haben, der angibt, wie deckend oder transparent ein Pixel ist. Da er für die Belichtung nicht relevant ist, ignorieren wir ihn einfach.

Wir nutzen eine Funktion für diese Berechnung. Sie ist vom Typ void, da sie nichts zurückgeben, sondern Daten verarbeiten soll. Da ich die geänderten Werte des Bildes beibehalten möchte, verwende ich “Pass-by-Reference” (raw_pic& img). Das ist auch effizienter, da beim Funktionsaufruf keine Kopie erstellt wird. Das zweite Argument ist natürlich die Belichtung.
Die Funktion ist leicht verständlich. Wie erwähnt: Wenn ich den Kanal Nr. 4 (Alpha) erreiche, ignoriere ich ihn. Es ist sehr wichtig sicherzustellen, dass wir die Grenzen der Pixelintensität nicht verlassen, d. h. der Pixelwert muss zwischen [0, 255] bleiben.
Die Funktion glBindTexture setzt einen internen Pointer. Ab diesem Moment wird jeder Befehl an GL_TEXTURE_2D an den Video-Speicherort geleitet, der durch img.textureID indiziert ist. Mit anderen Worten: Die Funktion zeigt an, wo wir die Operationen durchführen.
Die Funktion glTexSubImage2D dient der Aktualisierung des Inhalts. Wir übernehmen die Argumente so, wie sie sind; für die Komplexität unserer Anwendung ist das ausreichend.

In dieser Funktion dekomprimieren wir unser Bild vom jpg-Format in Wertfolgen für jeden Kanal. Die Funktion stbi_load erledigt das. Wenn alles klappt, müssen wir Speicher dynamisch im Heap allokieren. Dieser Buffer dient als Ziel für die mathematischen Berechnungen, bevor die Daten an den Grafikprozessor zurückgesendet werden.
Die Funktion glGenTextures: Fragt den Grafiktreiber ab, um eine eindeutige Kennung im VRAM zu reservieren. Es handelt sich im Wesentlichen um eine “Handle”-Allokation.
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Diese Anweisungen definieren das Verhalten der Texture Processing Unit (TMU) in der GPU:
Lineare Interpolation: Falls das Bild in einer anderen Auflösung als der nativen angezeigt wird, berechnet die GPU einen gewichteten Durchschnitt zwischen benachbarten Pixeln (Bilinear Filtering). Visuell verhindert dies “Aliasing”-Artefakte (Treppchenbildung). (Zitiert von der KI, ich hatte keine Ahnung, was das macht).
Die Funktion glTexImage2D kümmert sich um die physische Platzallokation im Speicher der Grafikkarte, den Datentransfer vom Originalbild zum Grafikprozessor und die Festlegung des Formats (eine Matrix von W x H mit 4 Komponenten/Pixel).
Es folgt die Main-Funktion, in der wir initial ein Fenster öffnen bzw. “zeichnen”, in das wir das Foto laden, sowie einen Slider zur Steuerung der Belichtung.

Die Funktion ImGui::CreateContext(); zur Initialisierung von ImGui?
ImGui_ImplGlfw_InitForOpenGL(window, true); zur Integration mit dem Betriebssystem. So fängt ImGui Mausbewegungen oder Tastaturanschläge ab.

Wir laden das Bild und definieren die benötigten Parameter.


Wir erstellen lediglich das Fenster für die Anwendung sowie den Slider für die Belichtung. Die Funktionen innerhalb von if (myImage.textureID != 0) {…} dienen dem korrekten Laden des Bildes.
Die Funktion ImGui::Render() zeichnet nichts auf den Bildschirm. Sie bereitet lediglich ein Datenpaket vor, das die GPU verstehen kann.
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()) : führt das Zeichnen der ImGui-Oberfläche aus. Dieser Aufruf muss nach dem Zeichnen des Bildes erfolgen, sonst erscheint das Interface hinter dem Foto.
glfwSwapBuffers: Ohne dies würden wir einen statischen Bildschirm sehen, da keine Änderungen an den Monitor übertragen würden.

Der Rest dient der Bereinigung (Cleanup).
Vielen Dank für die Aufmerksamkeit!
Der vollständige Code ist unter https://github.com/renea-bogdan/Simple-photo-editor zu finden.
Anmerkung: Die KI war mir behilflich beim Verständnis einiger Konzepte, die in diesem Tutorial ausführlich erklärt wurden, sowie bei der Erstellung der Anwendung.
