\section{Implementierung}
\label{implementierung}

\subsection{Vergleichsdaten}

Als Ausgangsdaten für die Durchführung wurde eine zufällige Auswahl an Dokumentationsscreenshots getroffen. Zusätzlich wurden auch Bilder in die Stichprobe mit aufgenommen, die beispielsweise aufgrund ihrer Auflösung oder Kontrastverhältnisse schwer lesbar sind.

\begin{figure}[ht]
  \centering
  \fbox{\includegraphics[width=.7\textwidth]{include/screenshots/driver_SEL_Options_002.png}}
  \caption{Ein gut für die Texterkennung geeigneter Screenshot. Die wesentlichen Inhalte weisen einen guten Kontrast zum Hintergrund auf und befinden sich in Bereichen mit gleichmäßiger Helligkeit.}
  \label{}
\end{figure}

\begin{figure}[ht]
  \centering
  \fbox{\includegraphics[width=.7\textwidth]{include/screenshots/editor_startpage_project-exist_001.png}}
  \caption{Ein schlecht lesbarer Screenshot. Aufgrund der vielen Symbole und der bunten Flächen stellt dieses Bild eine Herausforderung für das Texterkennungssystem dar.}
  \label{}
\end{figure}

Die textuellen Inhalte aller ausgewählten Bilder wurden anschließend manuell extrahiert und für den programmatischen Vergleich in einer Datei im selben Verzeichnis wie die Quellbilddatei festgehalten.

\begin{figure}[ht]
  \begin{minipage}{0.5\textwidth}
    \includegraphics[width=\textwidth]{include/screenshots/zrs_ZAMS_filter-alarmgroup_001.png}
  \end{minipage}
  \hspace{0.1\textwidth}
  \begin{minipage}{0.4\textwidth}
    \lstinputlisting[numbers=none]{include/screenshots/zrs_ZAMS_filter-alarmgroup_001.json}
  \end{minipage}
  \caption{Ein Screenshot und die daraus manuell extrahierten Schlagworte.}
  \label{fig:screenshot_verschlagwortet}
\end{figure}

\subsection{Verwendete Bibliotheken}
\label{components}

\subsubsection{Fremdbibliotheken}
\label{components_external}

In der prototypischen Implementierung, geschrieben in der Programmiersprache \csharp, wurden in Referenz an die in \autoref{technologien} vorgestellten Technologien und Werkzeuge folgende NuGet-Bibliotheken als Basis für die Implementierung verwendet:

\begin{itemize}
  \item \textbf{Magick.NET}\\Version: 13.1.0\\Lizenz: Apache-2.0\\\url{https://www.nuget.org/packages/Magick.NET.Core}
  \item \textbf{Tesseract}\\Version: 5.2.0\\Lizenz: Apache-2.0\\\url{https://www.nuget.org/packages/Tesseract}
\end{itemize}

\subsubsection{Verarbeitungsketten}
\label{components_processorchain}

Beim Entwurf des Verarbeitungssystems für die unterschiedlichen Bild- und Textverarbeitungsschritte wurde bewusst auf Flexibilität geachtet. Mithilfe von Interfaces und Builder-Methoden ist es möglich, Verarbeitungsschritte als Prozessoren (\engl{Processors}) zu definieren. So stellt beispielsweise das Normalisieren eines durch Tesseract erkannten Wortes einen Verarbeitungsschritt dar.

\begin{CsCode}[numbers=none]
  public interface IProcessor
    {
      IEnumerable Process(IEnumerable inputs);
    }

  public interface IProcessor<in TInput, out TOutput> : IProcessor
    {
      IEnumerable<TOutput> Process(IEnumerable<TInput> inputs);

      new IEnumerable Process(IEnumerable inputs)
      {
          return Process((IEnumerable<TInput>)inputs);
        }
    }
\end{CsCode}
\label{program:processor_interface}
\captionof{program}{Auszug aus Datei "IProcessor.cs": Schnittstelle eines Prozessors.}

\begin{CsCode}[numbers=none]
  public class ToLowerProcessor
  : Processor<ScanResult, ScanResult>
    {
      public override IEnumerable<ScanResult> Process(
      IEnumerable<ScanResult> inputs
      )
      {
          foreach (var kv in inputs)
          {
              kv.Word.Text = kv.Word.Text.ToLower();
              yield return kv;
            }
        }
    }
\end{CsCode}
\label{program:processor_implementation}
\captionof{program}{Auszug aus Datei "ToLowerProcessor.cs": Normalisieren als einzelner Verarbeitungsschritt}

Sollen mehrere Schritte verbunden werden, ist es möglich, bietet das Processing-Framework die Möglichkeit, eine sogenannte Verarbeitungskette aufzubauen. Hier können Delegates oder komplette Prozessoren dynamisch als einzelne Schritte aneinandergereiht werden, wobei die Typensicherheit durch das generische Typensystem von \csharp stets gewahrt wird:

\begin{CsCode}[numbers=none]
  public interface IProcessorChainConfiguration<TInput, TOutput> : IProcessorChain<TInput, TOutput>
    {
      IProcessorChainConfiguration<T, TOutput, TInput, TOutput> Use<T>(
      IProcessor<TInput, T> processor);

      IProcessorChainConfiguration<T, TOutput, TInput, TOutput> Use<T>(
      Func<IEnumerable<TInput>, IEnumerable<T>> processorFunc);

      IProcessorChainConfiguration<TInput, TOutput> Complete(
      IProcessor<TInput, TOutput> processor);

      IProcessorChainConfiguration<TInput, TOutput> Complete(
      Func<IEnumerable<TInput>, IEnumerable<TOutput>> processorFunc);
    }

  public interface IProcessorChainConfiguration<TInput, TOutput, TInChain, TOutChain>
    {
      IProcessorChainConfiguration<T, TOutput, TInChain, TOutChain> Use<T>(
      IProcessor<TInput, T> processor);

      IProcessorChainConfiguration<T, TOutput, TInChain, TOutChain> Use<T>(
      Func<IEnumerable<TInput>, IEnumerable<T>> processorFunc);

      IProcessorChain<TInChain, TOutChain> Complete(
      IProcessor<TInput, TOutput> processor);

      IProcessorChain<TInChain, TOutChain> Complete(
      Func<IEnumerable<TInput>, IEnumerable<TOutput>> processorFunc);
    }
\end{CsCode}
\label{program:processor_chain_interface}
\captionof{program}{Auszug aus Datei "IProcessorChainConfiguration.cs": Schnittstelle zur Konfiguration einer Verarbeitungskette}

Ist die Aufbauphase abgeschlossen, kann die Verarbeitungskette schlussendlich gestartet werden.

\begin{CsCode}[numbers=none]
  var postProcessor = new ProcessorChainConfiguration<ScanResult, ScanResult>()
  .Use(new ConfidenceFilter(50))
  .Use(new ToLowerProcessor())
  .Use(new DuplicateFilter())
  .Complete(new RegexFilter(WordRegex));

  // ...

  postProcessor.Process(data);
\end{CsCode}
\label{program:processor_chain_implementation}
\captionof{program}{Auszug aus Datei "ImageViewModel.cs": Konfiguration und Starten einer Verarbeitungskette}

Abhängig von den verwendeten Prozessoren können also Eingangsdaten jeglichen Typs, in diesem Fall Bildobjekte der Magick.NET Bibliothek oder Ergebnisdaten des Texterkennungsvorgangs dynamisch verarbeitet werden.

\subsubsubsection{Bildverarbeitungskette}
\label{processor_chain_image}

Für den grundlegenden Ablauf der Bildverarbeitung und der anschließenden Ergebnisfilterung werden die Erkenntnisse aus \autoref{konzept} mithilfe des in \autoref{components_processorchain} beschriebenen Processing-Frameworks angewandt.

Angefangen mit einem Ausgangsbild, welches über die Softwarebibliothek Magick.NET geladen wurde, beginnt die Bildverarbeitung zunächst mit dem Resampling. Falls der geladene Screenshot die Mindestauflösung von 300dpi unterschreitet, wird es mittels Lanczos2-Verfahren, eine von Magick.NET mitgelieferte Implementierung des Lanczos2-Algorithmus mit leichter Schärfung \mcite{imagemagick}, auf die Mindestauflösung vergrößert. Anschließend wird das Bild normalisiert, in Graustufen umgewandelt und jegliche Transparenz durch einen weißen Hintergrund ersetzt. Danach wird es mittels Schwellwertverfahren binarisiert und rund um das Bild wird ein Rahmen mit einer Dicke von 10px eingefügt. Um Texterkennungsfehler durch falsche Vorder- \bzw Hintergrundfarben auszuschließen, wird das Bild schlussendlich gemeinsam mit einer farblich invertierten Version an das Texterkennungssystem weitergegeben.

\begin{CsCode}[numbers=none]
  var preprocessing = new ProcessorChainConfiguration<MagickImage, MagickImage>()
  .Use(new CloneImageProcessor())
  .Use(new ResizeProcessor(FilterType.Lanczos2Sharp, PixelInterpolateMethod.Mesh))
  .Use(new NormalizeProcessor())
  .Use(_thresholdProcessor)
  .Use(new AddBorderProcessor(10))
  .Use(new BinarizeProcessor())
  .Use(new NegateCloneProcessor())
  .Complete(OnPreprocessed);
\end{CsCode}
\label{program:program_preprocessor_definition}
\captionof{program}{Auszug aus Datei "EvaluationProcessor.cs": Definition der Preprocessing-Kette}

Wurde der übergebene Screenshot vom Texterkennungssystem verarbeitet, müssen nun die Ergebnisse gefiltert werden. Dazu werden zunächst die Metadaten der einzelnen Wörter betrachtet und alle Elemente mit einer Confidence unter einem Schwellenwert von 50\% verworfen. Danach werden die erkannten Texte mittels der \csharp-Funktion ToLower() normalisiert und anschließend auf Duplikate untersucht. Sind alle Duplikate verworfen, werden die Wörter schlussendlich mittels sprachabhängigen Regular Expressions -- in diesem Fall gibt es gemäß den Annahmen in \autoref{annahmen_mehrsprachigkeit} einen Sprachfilter für Englisch und Deutsch -- gefiltert.

\begin{CsCode}[numbers=none]
  var postprocessing = new ProcessorChainConfiguration<ScanResult, ScanResult>()
  .Use(new ConfidenceFilter(50))
  .Use(new ToLowerProcessor())
  .Use(new DuplicateFilter())
  .Complete(new RegexFilter(wordRegex));
\end{CsCode}
\label{program:program_postprocessor_definition}
\captionof{program}{Auszug aus Datei "EvaluationProcessor.cs": Definition der Postprocessing-Kette}

\subsubsection{Lookup}

Die "Lookup" Bibliothek abstrahiert das Speichern von Schlüssel-Wert-Paaren. Dabei kann flexibel zwischen verschiedenen Speicherimplementierungen gewechselt werden. So ist es beispielsweise möglich, die Werte in einer Listenstruktur im Arbeitsspeicher, in einer Datei oder -- mittels der EntityFramework-Bibliothek, welche von der .NET Foundation entwickelt wird -- in einer Datenbank persistent abzulegen.

Unabhängig von der Ablagestruktur im Hintergrund können Lookups mittels einer gemeinsamen Schnittstelle manipuliert werden:

\begin{CsCode}[numbers=none]
  public interface ILookup<TKey, TValue>
  : ILookup,
  IDictionary<TKey, ICollection<TValue>>,
  IDisposable
    {
      ICollection<TValue> Add(TKey key);

      public void Add(TKey key, TValue value);

      public void AddRange(TKey key, IEnumerable<TValue> values);

      public bool Remove(TKey key, TValue value);

      public ICollection<TValue> GetOrAdd(TKey key);
    }
\end{CsCode}
\label{program:program_lookup_interface}
\captionof{program}{Auszug aus Datei "ILookup.cs": Definition der gemeinsamen Schnittstelle für Lookups}

\subsubsection{OCR}

Die "OCR" Bibliothek enthält elementare Komponenten für die Texterkennung mittels der oben genannten Komponenten. Sie enthält Funktionen zur Bearbeitung von Bildern mittels Magick.NET inklusive anschließender Verarbeitung mittels Tesseract. Kernkomponenten wie das Texterkennungssystem werden automatisch auf Basis der Eingabeparameter konfiguriert und die Verwendung der Ergebnisdaten in externen Programmteilen wird durch die Zurverfügungstellung von Datenmodellen für die Ergebnisdaten vereinfacht. Außerdem enthält die Bibliothek eine Reihe von vordefinierten Verarbeitungsketten \bzw Prozessoren für die Bild- und Textverarbeitung.

\subsubsection{Automatische Berichterstellung}
\label{components_reportgenerator}

Mithilfe des ReportGenerator-Frameworks wird die automatische Berichterstellung für unterschiedlichste Ausgabeformate abstrahiert. Durch die mitgelieferten Schnittstellendefinitionen ist es möglich, eigene Ausgabeformate zu definieren und den Funktionsumfang des ReportGenerators, wie beispielsweise das Erstellen von Tabellen oder das Anlegen und Überschriften, an die jeweilige Syntax und Dokumentstruktur anzupassen.

\begin{CsCode}[numbers=none]
  public interface IDocumentGenerator : IStreamWriter
    {
      IDocumentGenerator Append(string? text = default);

      IDocumentGenerator AppendLine(string? text = default);

      IDocumentGenerator AppendParagraph(string? text = default);

      IDocumentGenerator AppendHeading(int level, string text);

      IDocumentGenerator AppendTable(int columns, Action<ITableGenerator> table);

      string FormatImage(string path, IBounds? bounds = default);
    }
\end{CsCode}
\label{program:program_reportgenerator_interface}
\captionof{program}{Auszug aus Datei "IDocumentGenerator.cs": Hauptschnittstelle für den ReportGenerator}

\subsection{Programmablauf}

Die prototypische Implementierung besteht neben den oben genannten Komponenten aus einem ausführbaren Kommandozeilenprogramm zur Texterkennung und einem Programm zum Vergleich der Ergebnisse mit den manuell verschlagworteten Soll-Daten. Alle relevanten Daten werden in entsprechenden Ausgabeverzeichnissen festgehalten und dadurch für den jeweiligen nächsten Schritt verfügbar gemacht.

\subsubsection{Texterkennung}

Zu Beginn der Ausführung des Kommandozeilenprogramms wird für jedes zu verarbeitende Bild abhängig von den definierten Schwellenwertverfahren eine Reihe von Prozessoren angelegt. Dazu wurde der statische Teil der Bildverarbeitungskette gemäß \autoref{processor_chain_image} innerhalb der "EvaluationProcessor" Klasse definiert. Lediglich die zu evaluierenden Prozessoren für die jeweiligen Schwellwertverfahren können außerhalb der klasse dynamisch definiert werden. Der EvaluationProcessor legt die erzeugten Ergebnisdaten, bestehend aus den gefundenen Wörtern und zugehörigen Metadaten wie die Confidence, auf Dateiebene ab. Um überprüfen zu können, welches Bild schlussendlich an das Texterkennungssystem übergeben wurde, werden auch die verarbeiteten Bilder nach der Binarisierung gespeichert.

\begin{CsCode}[numbers=none]
  private static IEnumerable<EvaluationProcessor> MakeThresholdVariations()
  {
      for (int i = 4; i <= 24; i += 4)
      {
          yield return new(new ThresholdAdaptiveProcessor(i));
        }

      for (int i = 20; i <= 80; i += 10)
      {
          yield return new(new ThresholdProcessor(i));
        }

      yield return new(new AutoThresholdProcessor(AutoThresholdMethod.Kapur));
      yield return new(new AutoThresholdProcessor(AutoThresholdMethod.OTSU));
      yield return new(new AutoThresholdProcessor(AutoThresholdMethod.Triangle));
    }
\end{CsCode}
\label{program:program_processor_definition_dynamic}
\captionof{program}{Auszug aus Datei "Program.cs": Definition der Thresholding Prozessoren}

Ist die Erstellung der Bildbearbeitungsprozessoren abgeschlossen, wird jeder einzelne Prozessor über die Systembibliothek "System.Threading.Tasks" als eigener Auftrag (\engl{Task}) gestartet. In der Kommandozeile wird anschließend der aktuelle Status jedes Tasks angezeigt. Wurden alle Tasks abgeschlossen, wird das Programm beendet.

\subsubsection{Vergleich mit Soll-Daten}

Wurden die in den jeweiligen Screenshots erkannten Textdaten abgelegt, werden diese Daten im zweiten Kommandozeilenprogramm "ReportGenerator" nun mit den manuell verschlagworteten Daten verglichen und die Ergebnisse in einen Bericht (\engl{Report}) gespeichert.

Als zentrale Komponente für den Vergleich spielt die Berechnung der in \autoref{metriken} erklärten Metriken eine wesentliche Rolle. Wie in Programm \ref{program:distance_levenshtein} ersichtlich, wird die Distanz zwischen zwei \csharp-Enumerables, seien es zwei Strings oder zwei Listen, über das Verfahren nach Levenshtein berechnet.

\begin{CsCode}[numbers=none]
  public static double GetDistance<T>(T reference, T? hypothesis)
  where T : IEnumerable
    {
      var refArr = reference.Cast<object>().ToArray();
      var hypArr = hypothesis?.Cast<object>().ToArray() ?? Array.Empty<object>();

      var distance = new int[refArr.Length + 1, hypArr.Length + 1];

      for (var x = 0; x <= refArr.Length; x++)
      {
          distance[x, 0] = x;
        }

      for (var y = 0; y <= hypArr.Length; y++)
      {
          distance[0, y] = y;
        }

      for (var x = 0; x < refArr.Length; x++)
      {
          for (var y = 0; y < hypArr.Length; y++)
          {
              var cost = Equals(refArr[x], hypArr[y]) ? 0 : 1;

              var c1 = distance[x, y] + cost; // Bottom left

              var c2 = distance[x, y + 1] + 1; // Top left
              var c3 = distance[x + 1, y] + 1; // Bottom right

              distance[x + 1, y + 1] = Min(c1, c2, c3); // Top right
            }
        }

      return distance[refArr.Length, hypArr.Length];
    }
\end{CsCode}
\label{program:distance_levenshtein}
\captionof{program}{Auszug aus Datei "Calculator.cs": Berechnung der Levenshtein-Distanz}

Nach der Ermittlung der jeweiligen Distanzen auf Wort- \bzw Bildbasis werden sie mit den jeweiligen Ursprungsbildern, Prozessoren und den verwendeten Algorithmen in Bezug gesetzt. Die so aufbereiteten Ergebnisse werden anschließend an den ReportGenerator übergeben und in einen Bericht zusammengefasst.