Medianfilter

Worum geht es in dem Projekt und was ist sein Zweck?

Der folgende Artikel wird eine Anwendung behandeln, die PGM-P2-Bilder entrauscht (wir werden unten sehen, was diese sind und was sie darstellen). Die Anwendung wurde mir an der Universität als fakultative Aufgabe vorgeschlagen. Da ich sie interessant fand, habe ich mich entschlossen, eine ausführliche Beschreibung der Konzeption und Funktionsweise der Anwendung zu verfassen. Ich werde im Folgenden die Denkweise sequenziell darlegen.

I. Theoretischer Abriss

     

  1. Was ist ein Medianfilter?

In der Theorie der Signalverarbeitung ist ein Filter ein Gerät oder ein Prozess, der einen unerwünschten Teil aus einem Signal entfernt. Es gibt unzählige Arten von Filtern, aber der, den wir in dieser Arbeit untersuchen werden, ist der Medianfilter. Diese Art von Filter wird häufig verwendet, um Rauschen (oder Flecken) aus Bildern zu eliminieren.

Das Ergebnis der Anwendung eines Medianfilters auf ein Bild kann unten beobachtet werden:

 

     

  1. Der zu verarbeitende Dateityp, nämlich PGM-Typ P2

Im Speicher eines Computers wird ein Schwarz-Weiß-Bild gespeichert, indem der Grauwert jedes Pixels gespeichert wird, ein Wert, der immer im Intervall [0; 255] liegt, wobei 0 Schwarz und 255 Weiß bedeutet.

PGM ist ein Akronym für Portable GrayMap. Es gibt zwei Dateiformate für PGM – P5, eine Binärdatei, und P2, eine Textdatei. Eine Datei im P2-Format hat die folgende Struktur:

     

  • In der ersten Zeile steht der Code P2, der den Dateityp signalisiert.
  •  

  • Jede Zeile, die mit # beginnt, wird als Kommentar betrachtet und ignoriert.
  •  

  • Die nächste gültige Zeile enthält zwei Werte (width und height), die die Breite und Höhe des Bildes darstellen.
  •  

  • Die nächste gültige Zeile enthält einen Wert, der den maximalen Wert eines Pixels darstellt, der im Bild vorkommen wird (also der Wert, der dem Weißwert entspricht).
  •  

  • Die folgenden Zeilen enthalten width x height Werte, getrennt durch Whitespaces, die die Grauwerte für jeden Pixel darstellen, von links nach rechts und dann von oben nach unten.

Beispiel:

     

  1. Anwendung eines Medianfilters

Ein Medianfilter wird immer auf einem n x n Fenster um jeden Pixel im Originalbild angewendet, wobei n eine positive ungerade ganze Zahl ist (3, 5, 7, usw.). Für jeden Pixel im Originalbild werden alle seine Nachbarn innerhalb eines n x n Fensters um ihn herum extrahiert. Diese Pixel werden in einem Vektor platziert, der sortiert wird. Der Wert des Pixels im gefilterten Bild ist der Wert in der Mitte des sortierten Vektors. Für ein 3 x 3 Fenster:

Was mache ich an den Rändern?

An der Grenze, wo das n x n Fenster den Bildrand überschreitet, wird dieser durch Wiederholung der Randpixel aufgefüllt (Bordering). Betrachten Sie die obere linke Ecke des Originalbildes:

Wenn ein 5×5 Filter auf den oberen linken Pixel (mit dem Wert 10) angewendet wird, wird ein 5×5 quadratisches Fenster um ihn herum benötigt:

Fettgedruckt ist das Element in der Ecke, und um das 5×5 Fenster zu erhalten, wurden die Elemente am Rand repliziert.

Anforderung

Gegeben sei ein Sortieralgorithmus (BubbleSort oder MergeSort), eine Fenstergröße und zwei Dateinamen im PGM-P2-Format, eine Eingabe- und eine Ausgabedatei. Wenden Sie einen Medianfilter der angegebenen Größe auf jeden Pixel des Eingabebildes an, verwenden Sie den angegebenen Algorithmus zum Sortieren des Fensters und schreiben Sie das gefilterte Bild in die Ausgabedatei. Beide Dateien sind im PGM-P2-Format.

Eingabedaten

Über die Tastatur wird in einer einzigen Zeile der Algorithmustyp (**bubble** oder **merge**), die Fenstergröße und die Namen der beiden Dateien (Eingabe und Ausgabe) eingegeben, alle getrennt durch ein oder mehrere **Whitespace**-Zeichen.

Beispiel:

merge 3 test_in.pgm test_out.pgm

II. Beschreibung des Programms

Wir werden den Code sequenziell besprechen, wie bereits erwähnt. Zuerst müssen wir, da wir eine Datei als Eingabe erhalten, diese öffnen und die für uns relevanten Daten daraus lesen. Dazu verwenden wir eine boolesche Funktion namens readPGM.

Der gewählte Funktionstyp. Warum bool?

Der boolesche Typ speichert nur 2 Werte, 0 oder 1, in der logischen Arithmetik Wahr oder Falsch. Vorerst möchten wir überprüfen, ob unsere Datei erfolgreich geöffnet wird und ob wir die für uns relevanten Werte daraus lesen können, nämlich die Werte selbst, Breite, Höhe und maxVal.

Argumente der Funktion? Warum werden sie als Referenz deklariert? Was ist vector<vector <uint8_t>>& image?

Die Übergabe per Referenz bietet uns zwei entscheidende Vorteile für unsere Art von Anwendung. Erstens möchten wir die gelesenen Werte verwenden, wenn wir die Ausgabe erstellen, die offensichtlich dieselben Dimensionen haben wird. Daher benötige ich beim Aufruf der Funktion innerhalb der `main()`-Funktion deren Werte, was nur durch die Übergabe per Referenz möglich ist. Wenn ich sie beim Aufruf nicht als Referenz deklariere, würde die Funktion nur mit lokalen Kopien der Werte arbeiten (nur innerhalb der `readPGM`-Funktion), und beim Verlassen hätte ich nur beliebige Werte.

Ein weiterer Vorteil der Übergabe per Referenz ist die Effizienz. Dies wird besonders relevant, wenn wir die Grauwerte des Bildes mit Hilfe von `vector<vector<uint8_t>>& image` lesen und verarbeiten. Dies ist im Grunde eine Matrix vom Typ `uint8_t`.

Die while-Schleifen lesen und überprüfen, ob das Gelesene gültig ist.

Hier findet eine sequentielle Durchquerung des Vektors statt, in dem die aus der Datei gelesenen Werte eingefügt werden. **Warum habe ich hier `static_cast` verwendet und nicht direkt `image[i][j]=pixel`?**

Ich habe `pixel` als `int` deklariert, weil ich erwarte, auch beschädigte oder falsch geschriebene Dateien zu erhalten, die einen Wert größer als 255 oder einen negativen Wert enthalten könnten. Es ist nur eine Sicherheitsmaßnahme. Daher muss ich den Cast zurück zu `uint8_t` durchführen.

Wenn alles wie gewünscht verlaufen ist, gebe ich `true` zurück, was signalisiert, dass das Lesen erfolgreich war.

Die Schreibfunktion

Nun werden die Argumente Breite, Höhe und maxVal nicht mehr als Referenz übergeben, da das Schreiben der Datei der letzte Schritt ist und ich deren Werte nicht mehr für andere Operationen benötige. Im Übrigen dienen die beiden verschachtelten for-Schleifen dazu, die Datei wie eine Matrix zu behandeln, sie zu durchlaufen und die Werte hineinzuschreiben.

**Die Funktion** **`uint8_t getPixel(vector>& image, int x, int y, int latime, int inaltime)` und warum sie wichtig ist.**

Ihre Rolle ist es, die Bildränder korrekt zu verwalten, damit wir beim Anwenden des Filters nicht außerhalb des Bildes geraten.

Ergebnis: Wenn der Filter den Pixel an den Koordinaten **(-1, 5)** anfordert, gibt die Funktion den Wert des Pixels bei **(0, 5)** zurück. Wenn er **(50, 1000)** in einem Bild der Höhe 500 anfordert, gibt er den Wert des Pixels bei **(50, 499)** zurück.

Darauf folgen die Sortierfunktionen. Wir werden sie nicht im Detail durchgehen, da der Unterschied nur in der Effizienz liegt. Jeder Sortieralgorithmus kann verwendet werden. Alles, was ich getan habe, war, die Anforderung zu erfüllen.

** Warum haben wir zwei `mergeSort`-Funktionen?
**

Die erste (die mit 3 Argumenten) ist die **rekursive Basisfunktion** des Merge-Sort-Algorithmus. Ihre Rolle ist es, den Vektor in immer kleinere Unterabschnitte zu **zerlegen** und sie anschließend wieder in sortierter Reihenfolge zu **kombinieren**. Die zweite hat die Rolle, sie zu “verpacken”, d.h. die Aufrufmethode zu vereinfachen, indem sie auf ein einziges Argument reduziert wird. Sie dient der Vereinfachung des Codes.

Anwendung des Filters

Schließlich kommen wir zur eigentlichen Filteranwendung.

Wofür verwenden wir die Variable k?

Die Variable `k` berechnet die **Verschiebung** von der Fenstermitte (dem aktuellen Pixel (x, y)) zum Rand. Wenn `marimeFereastra` (Fenstergröße) 3 ist, ist `k` 1. Das Fenster erstreckt sich von -1 bis +1 relativ zur Mitte.

Die ersten 2 Schleifen beginnen die Durchquerung der Datei.

Welche Rolle hat `window`?

Es wird ein temporärer Vektor erstellt, der alle Pixelwerte aus dem Filterfenster speichert.

Es folgt die **Fensterschleife**. Innerhalb der innersten Schleife werden die effektiven Koordinaten im Eingabebild berechnet:

`int pixelX = x + wx;`

`int pixelY = y + wy;`

Anschließend wird der erhaltene Wert dem Vektor **`window`** hinzugefügt.

Das Problem ist damit abgeschlossen. Die Funktion **`main()`** stellt lediglich die Verbindung zum Benutzer her. Zum Ausführen geben wir beispielsweise **`merge 3 test_in.pgm test_out.pgm`** ein. Nach der Ausführung wird eine Datei **`test_out.pgm`** im Stammverzeichnis, in dem das Programm gespeichert ist, erstellt. Achtung, auch `test_in.pgm` muss sich dort befinden!

III. Abschließende Anmerkungen:

Das Problem wurde mir im Rahmen des SDA-Kurses (Datenstrukturen und Algorithmen), der Fakultät für *Elektronik, Telekommunikation und Informationstechnologie* der *Nationalen Universität für Wissenschaft und Technologie Politehnica Bukarest*, vorgeschlagen.

Dozent *Radu Hobincu*.

*November 2025.*

Quellen: https://cppreference.com/

*Ich habe AI zur Lösung des Problems sowie zum Verständnis und zur Behebung von auftretenden Bugs verwendet.