353 lines
18 KiB
TeX
353 lines
18 KiB
TeX
\section{Implementierung}
|
|
\label{implementierung}
|
|
|
|
\subsection{Vergleichsdaten}
|
|
|
|
Als Ausgangsdaten für die Durchführung wurde eine zufällige Auswahl an Dokumentationsscreenshots, wie in \autoref{fig:screenshot_beispiel_gut} abgebildet, getroffen. Zusätzlich wurden auch Bilder wie in \autoref{fig:screenshot_beispiel_schlecht} 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{fig:screenshot_beispiel_gut}
|
|
\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{fig:screenshot_beispiel_schlecht}
|
|
\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. Ein Beispiel dafür ist der Screenshot "zrs\_ZAMS\_filter-alarmgroup\_001" in \autoref{fig:screenshot_verschlagwortet}, welcher insgesamt 15 verschiedene Wörter beinhaltet.
|
|
|
|
\begin{figure}[ht]
|
|
\centering
|
|
\begin{minipage}{0.5\textwidth}
|
|
\includegraphics[width=0.99\textwidth]{include/screenshots/zrs_ZAMS_filter-alarmgroup_001.png}
|
|
\end{minipage}
|
|
\hfill
|
|
\begin{minipage}{0.45\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}
|
|
|
|
Die für die prototypische Implementierung verwendeten Bibliotheken stellen Komponenten zur Verarbeitung von Screenshotdaten zur Verfügung. Um die Funktionalität flexibel auf unterschiedliche Anforderungen anpassen zu können, sind die Komponenten möglichst lose miteinander gekoppelt. Ein beispielhafter Ablauf eines Texterkennungsprogramms unter Nutzung der Bibliotheken ist in \autoref{fig:overview_diagram} abgebildet.
|
|
|
|
\begin{figure}[ht]
|
|
\centering
|
|
\includegraphics[width=.9\textwidth]{include/flowcharts/flow_overview.png}
|
|
\caption{Beispielhafter Ablauf eines Texterkennungsprogramms}
|
|
\label{fig:overview_diagram}
|
|
\end{figure}
|
|
|
|
\subsubsection{Fremdbibliotheken}
|
|
\label{components_external}
|
|
|
|
In der prototypischen Implementierung, entwickelt in \csharp .NET, 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} \mcite{nuget_magicknet}\\Version: 13.1.0\\Lizenz: Apache-2.0\\
|
|
\item\textbf{Tesseract} \mcite{nuget_tesseract}\\Version: 5.2.0\\Lizenz: Apache-2.0\\
|
|
\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 gemäß Programm \ref{prg:processor_interface} als Prozessoren (\engl{Processors}) zu definieren. So stellt beispielsweise das Normalisieren eines durch Tesseract erkannten Wortes einen Verarbeitungsschritt dar, wie in Programm \ref{prg:processor_implementation} gezeigt.
|
|
|
|
\begin{program}[!ht]
|
|
\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}
|
|
\caption{Auszug aus Datei "IProcessor.cs": Schnittstelle eines Prozessors.}
|
|
\label{prg:processor_interface}
|
|
\end{program}
|
|
|
|
\begin{program}[!ht]
|
|
\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}
|
|
\caption{Auszug aus Datei "ToLowerProcessor.cs": Normalisieren als einzelner Verarbeitungsschritt.}
|
|
\label{prg:processor_implementation}
|
|
\end{program}
|
|
|
|
Sollen mehrere Schritte verbunden werden, bietet das Processing-Framework die Möglichkeit, eine Verarbeitungskette aufzubauen. Gemäß der Schnittstellendefinition in Programm \ref{prg:processor_chain_interface} können Delegates oder komplette Prozessoren dynamisch als einzelne Schritte aneinandergereiht werden. Die Typensicherheit wird durch das generische Typensystem von \csharp stets gewahrt.
|
|
|
|
\begin{program}[!ht]
|
|
\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}
|
|
\caption{Auszug aus Datei "IProcessorChainConfiguration.cs": Schnittstelle zur Konfiguration einer Verarbeitungskette}
|
|
\label{prg:processor_chain_interface}
|
|
\end{program}
|
|
|
|
Ist die Aufbauphase abgeschlossen, kann die Verarbeitungskette, konfiguriert in Programm \ref{prg:processor_chain_implementation}, gestartet werden.
|
|
|
|
\begin{program}[!ht]
|
|
\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}
|
|
\caption{Auszug aus Datei "ImageViewModel.cs": Konfiguration und Starten einer Verarbeitungskette.}
|
|
\label{prg:processor_chain_implementation}
|
|
\end{program}
|
|
|
|
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 Ablauf der Bildverarbeitung und der anschließenden Ergebnisfilterung werden die Erkenntnisse aus \autoref{konzept} mithilfe des in \autoref{components_processorchain} beschriebenen Processing-Frameworks angewandt. Die resultierende Konfiguration folgt dem Ablauf aus \autoref{fig:preprocessor_diagram} und \autoref{fig:postprocessor_diagram} und ist in Programm \ref{prg:preprocessor_definition} \bzw \ref{prg:postprocessor_definition} definiert.
|
|
|
|
\begin{figure}[ht]
|
|
\centering
|
|
\includegraphics[width=.9\textwidth]{include/flowcharts/flow_preprocessing.png}
|
|
\caption{Beispielhafter Ablauf der Vorverarbeitungskette}
|
|
\label{fig:preprocessor_diagram}
|
|
\end{figure}
|
|
|
|
\begin{figure}[ht]
|
|
\centering
|
|
\includegraphics[width=.9\textwidth]{include/flowcharts/flow_postprocessing.png}
|
|
\caption{Beispielhafter Ablauf der Nachbearbeitungskette}
|
|
\label{fig:postprocessor_diagram}
|
|
\end{figure}
|
|
|
|
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 300 dpi 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. 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 gemeinsam mit einer farblich invertierten Version an das Texterkennungssystem weitergegeben.
|
|
|
|
\begin{program}[!ht]
|
|
\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}
|
|
\caption{Auszug aus Datei "EvaluationProcessor.cs": Definition der Preprocessing-Kette.}
|
|
\label{prg:preprocessor_definition}
|
|
\end{program}
|
|
|
|
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 \lstinline{ToLower()} normalisiert und anschließend auf Duplikate untersucht. Wurden alle Duplikate verworfen, werden die Wörter mittels sprachabhängigen Regular Expressions untersucht. Gemäß den Annahmen in \autoref{annahmen_mehrsprachigkeit} wurde ein simpler englischer (\lstinline|[\w'\-]{2,}|) sowie deutscher (\lstinline|[\w'\-äöüÄÖÜß]{2,}|) Sprachfilter festgelegt.
|
|
|
|
\begin{program}[!ht]
|
|
\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}
|
|
\caption{Auszug aus Datei "EvaluationProcessor.cs": Definition der Postprocessing-Kette.}
|
|
\label{prg:postprocessor_definition}
|
|
\end{program}
|
|
|
|
\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, abgebildert in Programm \ref{prg:lookup_interface}, manipuliert werden.
|
|
|
|
\begin{program}[!ht]
|
|
\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}
|
|
\caption{Auszug aus Datei "ILookup.cs": Definition der gemeinsamen Schnittstelle für Lookups}
|
|
\label{prg:lookup_interface}
|
|
\end{program}
|
|
|
|
\subsubsection{Optische Texterkennung}
|
|
|
|
Die OCR-Bibliothek beinhaltet elementare Funktionen für die Texterkennung. 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. Der gesamte Funktionsumfang des ReportGenerators, beispielsweise das Erstellen von Tabellen oder das Anlegen und Überschriften, kann durch die Implementierung von Interfaces wie Programm \ref{prg:reportgenerator_interface} an die jeweilige Syntax und Dokumentstruktur angepasst werden.
|
|
|
|
\begin{program}[!ht]
|
|
\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}
|
|
\caption{Auszug aus Datei "IDocumentGenerator.cs": Hauptschnittstelle für den ReportGenerator}
|
|
\label{prg:reportgenerator_interface}
|
|
\end{program}
|
|
|
|
\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, wie in Programm \ref{prg:processor_definition_dynamic} auszugsweise dargestellt. 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{program}[!ht]
|
|
\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}
|
|
\caption{Auszug aus Datei "Program.cs": Definition der Thresholding Prozessoren.}
|
|
\label{prg:processor_definition_dynamic}
|
|
\end{program}
|
|
|
|
Ist die Erstellung der Bildbearbeitungsprozessoren abgeschlossen, wird jeder einzelne Prozessor über die Systembibliothek "System.Threading.Tasks" als eigene Ausführungseinheit (\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{prg:distance_levenshtein} ersichtlich, wird die Distanz zwischen zwei \csharp-Enumerables, seien es zwei Strings oder zwei Listen, über das Verfahren nach Levenshtein berechnet.
|
|
|
|
\begin{program}[!ht]
|
|
\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}
|
|
\caption{Auszug aus Datei "Calculator.cs": Berechnung der Levenshtein-Distanz.}
|
|
\label{prg:distance_levenshtein}
|
|
\end{program}
|
|
|
|
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. |