14 Commits

Author SHA1 Message Date
Simon b21afe6121 Formatting fixes 2024-01-15 00:05:04 +01:00
Simon 22800639d0 added generated figures 2024-01-14 20:30:33 +01:00
Simon 745e41f474 Fixed figure generation 2024-01-14 20:30:22 +01:00
Simon 2736feb0e7 moved generators to folders 2024-01-14 19:40:49 +01:00
Simon fb9213e6c5 Added latex include table generator 2024-01-14 19:40:42 +01:00
Simon 1fdedb89ad switched json encoder 2024-01-11 00:16:41 +01:00
Simon 41aa4bf151 Improved postprocessing analysis 2024-01-11 00:14:20 +01:00
Simon 6eda25fce5 Added oneshot apps 2024-01-10 23:33:58 +01:00
Simon 946eea2347 Fixed live scanner adjustments 2024-01-10 18:35:04 +01:00
Simon Gruber 74619876c2 Updated results and report 2024-01-08 16:45:35 +01:00
Simon Gruber 6c554e444f Renamed Examples to Implementation 2024-01-08 16:23:24 +01:00
Simon Gruber b17044f959 Cleanup 2024-01-08 16:21:10 +01:00
Simon f3768348e9 Merge pull request 'feature/reports' (#1) from feature/reports into main
Reviewed-on: bsc/ocr#1
2024-01-07 21:37:04 +01:00
Simon 71c7a27b36 Adjustments for BA 2024-01-07 21:33:01 +01:00
1376 changed files with 2307 additions and 1061 deletions
-26
View File
@@ -1,26 +0,0 @@
using GUI.ViewModels;
using GUI.Views;
using Serilog;
using System.Windows;
namespace GUI;
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
/// <inheritdoc />
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var loggingCollection = new LoggingCollection(100);
Log.Logger = new LoggerConfiguration()
.WriteTo.Sink(loggingCollection)
.CreateLogger();
new LogView(loggingCollection).Show();
new ImageView().Show();
}
}
-292
View File
@@ -1,292 +0,0 @@
using Common;
using GUI.Model;
using ImageMagick;
using Microsoft.Win32;
using Ocr.Tesseract;
using Ocr.Tesseract.Configuration;
using Ocr.Tesseract.Models;
using Ocr.Tesseract.Screenshots;
using Ocr.Tesseract.Screenshots.Configuration;
using Ocr.Tesseract.Screenshots.Threshold;
using Process.Abstract.Configuration;
using Process.Interface;
using Serilog;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace GUI.ViewModels;
internal class ImageViewModel : INotifyPropertyChanged
{
/// <summary>
/// The internally used <see cref="ScreenshotScanner"/>
/// </summary>
public ScreenshotScanner Scanner { get; private set; }
/// <summary>
/// Tesseract engine configuration
/// </summary>
public static readonly ITesseractConfiguration TesseractConfig =
new TesseractScreenshotConfiguration
{
DataPath = "tessdata",
Languages = new[] { "eng", "deu" }
};
public ScreenshotProcessorConfiguration ProcessorConfig { get; } = new();
/// <summary>
/// <see cref="Regex"/> expression for extracting whole words from scan results
/// </summary>
public static readonly Regex WordRegex = new(
@"[\w'\-]{2,}",
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase
);
public ImageViewModel()
{
Scanner = new ScreenshotScanner(MakeProcessor());
ProcessorConfig.PropertyChanged += (sender, args) => Task.Run(UpdateImage);
OpenFileCommand = new Command(OpenFile);
SaveEditedImageCommand = new Command(SaveEditedImage);
}
public ImageViewModel(MagickImage image) : this()
{
Image = image;
}
private IProcessorChain<MagickImage, ScanResult> MakeProcessor()
{
var threshold =
new ThresholdAdaptiveProcessor(
ProcessorConfig.ThresholdWidth,
ProcessorConfig.ThresholdHeight
);
var preprocessing = new ProcessorChainConfiguration<MagickImage, MagickImage>()
.Use(new CloneImageProcessor())
.Use(new ResizeProcessor(FilterType.Lanczos2Sharp, PixelInterpolateMethod.Mesh))
.Use(new NormalizeProcessor())
.Use(threshold)
.Use(new AddBorderProcessor(ProcessorConfig.Border))
.Use(new BinarizeProcessor())
.Complete(new NegateCloneProcessor());
var postprocessing = new ProcessorChainConfiguration<ScanResult, ScanResult>()
.Use(new ConfidenceFilter(50))
.Use(new ToLowerProcessor())
.Use(new DuplicateFilter())
.Complete(new RegexFilter(WordRegex));
var scan = new TesseractProcessor(TesseractConfig);
return new ProcessorChainConfiguration<MagickImage, ScanResult>()
.Use(preprocessing)
.Use(new ProcessingEvent<MagickImage>(OnProcessing))
.Use(scan)
.Use(new ProcessingEvent<ScanResult>(OnProcessed))
.Complete(postprocessing);
}
#region Overrides of Scanner
/// <inheritdoc />
protected void OnProcessing(IProcessor sender, ICollection<MagickImage> inputs)
{
Application.Current.Dispatcher.Invoke(() =>
{
foreach (var image in inputs)
{
Edited.Add(image);
}
});
}
/// <inheritdoc />
protected void OnProcessed(IProcessor sender, ICollection<ScanResult> inputs)
{
ScannedText = $"[{inputs.Count} words] " + string.Join(' ', inputs);
}
/// <inheritdoc />
public void Clear()
{
Scanner.Clear();
Application.Current.Dispatcher.Invoke(() =>
{
ScannedText = string.Empty;
Words.Clear();
Edited.Clear();
}
);
}
#endregion
#region File Handling
private void OpenFile()
{
var dialog = new OpenFileDialog()
{
InitialDirectory = Directory.GetCurrentDirectory()
};
if (dialog.ShowDialog() == true)
{
Image = new MagickImage(dialog.FileName);
UpdateImage();
}
}
private void SaveEditedImage()
{
var basePath = AppDomain.CurrentDomain.BaseDirectory;
for (var i = 0; i < Edited.Count; i++)
{
Edited[i].Write(Path.Combine(basePath, $"edited_{i}.png"));
}
Log.Information($"Saved image to '{basePath}'");
System.Diagnostics.Process.Start(
"explorer.exe",
Path.GetDirectoryName(basePath) ?? string.Empty
);
}
#endregion
#region Updating data
private void UpdateConfidence()
{
Confidence = Scanner.Lookup.Keys.Any()
? Scanner.Lookup.Keys.Sum(key => key.Confidence) / Scanner.Lookup.Keys.Count
: 0;
}
private void UpdateImage()
{
Task.Run(() =>
{
IsIdle = false;
Clear();
if (Image != null)
{
Scanner.Process(new[] { Image });
}
UpdateWords();
UpdateConfidence();
IsIdle = true;
});
}
private void UpdateWords()
{
Application.Current.Dispatcher.Invoke(() =>
{
foreach (var word in Scanner.Lookup.Keys)
{
Words.Add(word);
}
});
}
#endregion
#region Properties
private float _confidence;
private MagickImage? _image;
private bool _isIdle;
private string _scannedText = string.Empty;
public string ScannedText
{
get => _scannedText;
set
{
if (value == _scannedText)
{
return;
}
_scannedText = value;
OnPropertyChanged();
}
}
public bool IsIdle
{
get => _isIdle;
set
{
if (value == _isIdle)
{
return;
}
_isIdle = value;
OnPropertyChanged();
}
}
public float Confidence
{
get => _confidence;
set
{
_confidence = value;
OnPropertyChanged();
}
}
public ObservableCollection<MagickImage> Edited { get; } = new();
public MagickImage? Image
{
get => _image;
set
{
_image = value;
OnPropertyChanged();
}
}
public ObservableCollection<Word> Words { get; } = new();
#endregion Properties
#region Commands
public ICommand OpenFileCommand { get; private set; }
public ICommand SaveEditedImageCommand { get; private set; }
#endregion Commands
#region INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion INotifyPropertyChanged
}
-10
View File
@@ -1,10 +0,0 @@
using System;
namespace GUI.ViewModels;
public class LogMessage
{
public DateTime Timestamp { get; set; }
public string Message { get; set; }
}
-11
View File
@@ -1,11 +0,0 @@
namespace GUI.ViewModels;
public class LogViewModel
{
public LoggingCollection LoggingCollection { get; }
public LogViewModel(LoggingCollection loggingCollection)
{
LoggingCollection = loggingCollection;
}
}
@@ -1,43 +0,0 @@
using Serilog.Core;
using Serilog.Events;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace GUI.ViewModels;
public class LoggingCollection : ILogEventSink
{
public int Capacity { get; }
public ObservableCollection<LogMessage> Items { get; }
public LoggingCollection(int capacity)
{
Capacity = capacity;
Items = new ObservableCollection<LogMessage>(new List<LogMessage>(capacity));
}
public void Trim(int offset = 0)
{
for (int i = Items.Count - Capacity - offset; i >= 0; i--)
{
Items.RemoveAt(0);
}
}
#region Implementation of ILogEventSink
/// <inheritdoc />
public void Emit(LogEvent logEvent)
{
Trim(1);
Items.Add(new LogMessage
{
Timestamp = logEvent.Timestamp.DateTime,
Message = logEvent.RenderMessage()
});
}
#endregion
}
-41
View File
@@ -1,41 +0,0 @@
<Window
x:Class="GUI.Views.LogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:GUI.ViewModels"
Title="LogView"
Width="800"
Height="450"
d:DataContext="{d:DesignInstance viewModels:LogViewModel}"
mc:Ignorable="d">
<Grid>
<DataGrid
AutoGenerateColumns="False"
IsReadOnly="True"
ItemsSource="{Binding LoggingCollection.Items}"
VirtualizingPanel.ScrollUnit="Pixel">
<DataGrid.Columns>
<DataGridTextColumn
Width="Auto"
MinWidth="120"
Binding="{Binding Timestamp}"
Header="Timestamp" />
<DataGridTextColumn
Width="*"
MinWidth="120"
MaxWidth="300"
Binding="{Binding Message}"
Header="Message">
<DataGridTextColumn.ElementStyle>
<Style>
<Setter Property="TextBlock.TextAlignment" Value="Left" />
<Setter Property="TextBlock.TextWrapping" Value="Wrap" />
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
-16
View File
@@ -1,16 +0,0 @@
using GUI.ViewModels;
using System.Windows;
namespace GUI.Views;
/// <summary>
/// Interaction logic for LogView.xaml
/// </summary>
public partial class LogView : Window
{
public LogView(LoggingCollection loggingCollection)
{
InitializeComponent();
DataContext = new LogViewModel(loggingCollection);
}
}
@@ -1,12 +0,0 @@
namespace ReportGenerator.Generator.Interface;
public interface IBounds
{
public string Unit { get; }
public int? MinWidth { get; }
public int? MinHeight { get; }
public int? MaxWidth { get; }
public int? MaxHeight { get; }
public int? Width { get; }
public int? Height { get; }
}
@@ -1,31 +0,0 @@
using System.Text.Json;
namespace ReportGenerator.Models;
internal struct TagFileInfo
{
public string Path { get; private init; }
public string ImageName { get; set; }
public ICollection<string> GetWords()
{
using var file = File.OpenRead(Path);
return JsonDocument
.Parse(file)
.RootElement
.GetProperty("words")
.EnumerateArray()
.Select(w => w.GetString() ?? throw new Exception("Cannot parse null words"))
.ToArray();
}
public static TagFileInfo FromPath(string path) => new()
{
Path = path,
ImageName = System.IO.Path.GetFileNameWithoutExtension(path),
};
/// <inheritdoc />
public override string ToString() => ImageName;
}
-17
View File
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.001}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0011}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0012}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0014}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0014}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0011}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0014}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0013}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0014}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0011}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0014}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0009}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0008}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0009}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0009}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0014}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0009}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0014}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0008}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0011}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0009}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0009}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0009}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0009}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0009}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0009}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0008}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0009}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0009}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0009}
@@ -1 +0,0 @@
{"Words":[],"Elapsed":0.0009}
@@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/contentModel.xml
/projectSettingsUpdater.xml
/modules.xml
/.idea.Implementation.iml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="commitMessageIssueKeyValidationOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
<option name="commitMessageValidationEnabledOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="ASK" />
<option name="description" value="" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
@@ -1,6 +1,6 @@
using System.Text; using System.Text;
namespace CLI.Monitor; namespace Ocr.Cli.Monitor;
public class CliTaskMonitor : TaskMonitor public class CliTaskMonitor : TaskMonitor
{ {
@@ -1,6 +1,6 @@
using System.Text; using System.Text;
namespace CLI.Monitor; namespace Ocr.Cli.Monitor;
public class CompactCliTaskMonitor : TaskMonitor public class CompactCliTaskMonitor : TaskMonitor
{ {
@@ -1,4 +1,4 @@
namespace CLI.Monitor; namespace Ocr.Cli.Monitor;
public interface ITaskMonitor public interface ITaskMonitor
{ {
@@ -1,4 +1,4 @@
namespace CLI.Monitor; namespace Ocr.Cli.Monitor;
public abstract class TaskMonitor : ITaskMonitor public abstract class TaskMonitor : ITaskMonitor
{ {
@@ -10,7 +10,7 @@ using Process.Interface;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace CLI.Processor; namespace Ocr.Cli.Processor;
internal class EvaluationProcessor internal class EvaluationProcessor
{ {
@@ -20,7 +20,7 @@ internal class EvaluationProcessor
/// <see cref="Regex"/> expression for extracting whole words from scan results /// <see cref="Regex"/> expression for extracting whole words from scan results
/// </summary> /// </summary>
private static readonly Regex wordRegex = new( private static readonly Regex wordRegex = new(
@"[\w'\-]{2,}", @"[\w'\-äöüÄÖÜß]{2,}",
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase
); );
@@ -42,6 +42,24 @@ internal class EvaluationProcessor
.Use(new DuplicateFilter()) .Use(new DuplicateFilter())
.Complete(new RegexFilter(wordRegex)); .Complete(new RegexFilter(wordRegex));
private IProcessorChain<MagickImage, ScanResult> MakeProcessor()
{
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);
return new ProcessorChainConfiguration<MagickImage, ScanResult>()
.Use(preprocessing)
.Use(tesseractProcessor)
.Complete(postProcessor);
}
private static readonly TesseractProcessor tesseractProcessor = new(tesseractConfig); private static readonly TesseractProcessor tesseractProcessor = new(tesseractConfig);
private readonly StopwatchProcessor<MagickImage, MagickImage> _thresholdProcessor; private readonly StopwatchProcessor<MagickImage, MagickImage> _thresholdProcessor;
@@ -75,24 +93,6 @@ internal class EvaluationProcessor
await JsonSerializer.SerializeAsync(file, result); await JsonSerializer.SerializeAsync(file, result);
}); });
private IProcessorChain<MagickImage, ScanResult> MakeProcessor()
{
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);
return new ProcessorChainConfiguration<MagickImage, ScanResult>()
.Use(preprocessing)
.Use(tesseractProcessor)
.Complete(postProcessor);
}
private IEnumerable<MagickImage> OnPreprocessed(IEnumerable<MagickImage> images) private IEnumerable<MagickImage> OnPreprocessed(IEnumerable<MagickImage> images)
{ {
var tImages = images.ToArray(); var tImages = images.ToArray();
@@ -2,7 +2,7 @@
using Process.Interface; using Process.Interface;
using System.Diagnostics; using System.Diagnostics;
namespace CLI.Processor; namespace Ocr.Cli.Processor;
public class StopwatchProcessor<TInput, TOutput> : Processor<TInput, TOutput> public class StopwatchProcessor<TInput, TOutput> : Processor<TInput, TOutput>
{ {
@@ -1,11 +1,11 @@
using CLI.Monitor; using Common.Extensions;
using CLI.Processor;
using Common.Extensions;
using ImageMagick; using ImageMagick;
using Ocr.Cli.Monitor;
using Ocr.Cli.Processor;
using Ocr.Tesseract.Screenshots.Threshold; using Ocr.Tesseract.Screenshots.Threshold;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
namespace CLI; namespace Ocr.Cli;
public class Program public class Program
{ {
@@ -20,6 +20,7 @@ public class Program
select (Key: path, Task: processor.Process(new MagickImage(path))) select (Key: path, Task: processor.Process(new MagickImage(path)))
).ToArray(); ).ToArray();
// return new CliTaskMonitor(scans) { Interval = TimeSpan.FromMilliseconds(500) }.Run();
return new CompactCliTaskMonitor(scans) { Interval = TimeSpan.FromMilliseconds(500) }.Run(); return new CompactCliTaskMonitor(scans) { Interval = TimeSpan.FromMilliseconds(500) }.Run();
} }
@@ -7,7 +7,7 @@
"Test all img": { "Test all img": {
"commandName": "Project", "commandName": "Project",
"commandLineArgs": "\"img/*.png\"", "commandLineArgs": "\"img/*.png\"",
"workingDirectory": "D:\\git\\BA\\Examples\\testdata" "workingDirectory": "D:\\git\\ba_ocr\\Implementation\\testdata"
}, },
"Test single img": { "Test single img": {
"commandName": "Project", "commandName": "Project",
@@ -5,7 +5,7 @@ namespace Common.Distance;
public static class Calculator public static class Calculator
{ {
/// <summary> /// <summary>
/// Calculates the levenshtein distance between /// Calculates the levenshtein distance between two enumerables
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
/// <param name="reference"></param> /// <param name="reference"></param>
@@ -37,7 +37,9 @@ public readonly struct DistanceComparer<T> : IDistanceComparer<T>
return str ?? string.Empty; return str ?? string.Empty;
} }
return // Enable for HTML/MD only
$"<strong style='color: orange;' title='REf: {Reference}, CER: {Distance}'>{str ?? "-"}</strong>"; // return $"<strong style='color: orange;' title='REf: {Reference}, CER: {Distance}'>{str ?? "-"}</strong>";
return str ?? "-";
} }
} }
@@ -42,9 +42,4 @@ public class ScreenshotScanner
Lookup.Add(kv.Word, kv.Image); Lookup.Add(kv.Word, kv.Image);
} }
} }
public virtual void Clear()
{
Lookup.Clear();
}
} }
@@ -1,4 +1,4 @@
<Application x:Class="GUI.App" <Application x:Class="Ocr.Gui.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources/> <Application.Resources/>
+17
View File
@@ -0,0 +1,17 @@
using Ocr.Gui.Views;
using System.Windows;
namespace Ocr.Gui;
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
/// <inheritdoc />
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
new ImageView().Show();
}
}
@@ -1,11 +1,11 @@
<UserControl <UserControl
x:Class="GUI.Controls.ImageControl" x:Class="Ocr.Gui.Controls.ImageControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:converters="clr-namespace:GUI.Converters" xmlns:converters="clr-namespace:Ocr.Gui.Converters"
xmlns:controls="clr-namespace:GUI.Controls" xmlns:controls="clr-namespace:Ocr.Gui.Controls"
d:DataContext="{d:DesignInstance controls:ImageControl}" d:DataContext="{d:DesignInstance controls:ImageControl}"
d:DesignHeight="450" d:DesignHeight="450"
d:DesignWidth="800" d:DesignWidth="800"
@@ -2,7 +2,7 @@
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
namespace GUI.Controls; namespace Ocr.Gui.Controls;
/// <summary> /// <summary>
/// Interaction logic for ImageControl.xaml /// Interaction logic for ImageControl.xaml
@@ -6,12 +6,10 @@ using System.IO;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
namespace GUI.Converters; namespace Ocr.Gui.Converters;
internal class ImageConverter : IValueConverter internal class ImageConverter : IValueConverter
{ {
#region Implementation of IValueConverter
/// <inheritdoc /> /// <inheritdoc />
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {
@@ -48,6 +46,4 @@ internal class ImageConverter : IValueConverter
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
#endregion
} }
@@ -1,7 +1,7 @@
using System; using System;
using System.Windows.Input; using System.Windows.Input;
namespace GUI.Model; namespace Ocr.Gui.Model;
public class Command : ICommand public class Command : ICommand
{ {
@@ -8,6 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="13.2.0" />
<PackageReference Include="Serilog" Version="3.0.1" /> <PackageReference Include="Serilog" Version="3.0.1" />
</ItemGroup> </ItemGroup>
@@ -1,16 +1,16 @@
<Window <Window
x:Class="GUI.Views.ImageView" x:Class="Ocr.Gui.Views.ImageView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:componentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase" xmlns:componentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:GUI.Controls" xmlns:views="clr-namespace:Ocr.Gui.Views"
xmlns:viewModels="clr-namespace:GUI.ViewModels" xmlns:controls="clr-namespace:Ocr.Gui.Controls"
Title="OcrView" Title="OcrView"
Width="800" Width="800"
Height="450" Height="450"
d:DataContext="{d:DesignInstance viewModels:ImageViewModel}" d:DataContext="{d:DesignInstance views:ImageViewModel}"
mc:Ignorable="d"> mc:Ignorable="d">
<Window.Resources> <Window.Resources>
<CollectionViewSource <CollectionViewSource
@@ -161,52 +161,6 @@
x:Name="EnableThreshold" x:Name="EnableThreshold"
Content="Apply Threshold" Content="Apply Threshold"
IsChecked="{Binding ProcessorConfig.EnableThresholding}" /> IsChecked="{Binding ProcessorConfig.EnableThresholding}" />
<Grid
Margin="4"
IsEnabled="{Binding ElementName=EnableThreshold,
Path=IsChecked}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0">Block Width:</TextBlock>
<Slider
x:Name="SldThreshold1"
Grid.Column="1"
Margin="4,0"
Maximum="100"
Minimum="0"
Thumb.DragCompleted="SldThreshold1_OnDragCompleted"
Value="15" />
<TextBlock Grid.Column="2">
<Run Text="{Binding Value, ElementName=SldThreshold1, FallbackValue=0, StringFormat=0.00}" />
</TextBlock>
</Grid>
<Grid
Margin="4"
IsEnabled="{Binding ElementName=EnableThreshold,
Path=IsChecked}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0">Block Height:</TextBlock>
<Slider
x:Name="SldThreshold2"
Grid.Column="1"
Margin="4,0"
Maximum="100"
Minimum="0"
Thumb.DragCompleted="SldThreshold2_OnDragCompleted"
Value="17" />
<TextBlock Grid.Column="2">
<Run Text="{Binding Value, ElementName=SldThreshold2, FallbackValue=0, StringFormat=0.00}" />
</TextBlock>
</Grid>
<CheckBox <CheckBox
Content="Resize" Content="Resize"
IsChecked="{Binding ProcessorConfig.EnableResizing}" /> IsChecked="{Binding ProcessorConfig.EnableResizing}" />
@@ -229,13 +183,6 @@
<Run Text="{Binding Value, ElementName=SldBorder, FallbackValue=0, StringFormat=0.00}" /> <Run Text="{Binding Value, ElementName=SldBorder, FallbackValue=0, StringFormat=0.00}" />
</TextBlock> </TextBlock>
</Grid> </Grid>
<Rectangle />
<CheckBox
Content="Filter connected components"
IsChecked="{Binding ProcessorConfig.FilterConnectedComponents}" />
</UniformGrid> </UniformGrid>
</Grid> </Grid>
</Window> </Window>
@@ -1,11 +1,10 @@
using GUI.ViewModels; using ImageMagick;
using ImageMagick;
using System; using System;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Controls.Primitives; using System.Windows.Controls.Primitives;
namespace GUI.Views; namespace Ocr.Gui.Views;
/// <summary> /// <summary>
/// Interaction logic for MainWindow.xaml /// Interaction logic for MainWindow.xaml
@@ -26,19 +25,6 @@ public partial class ImageView : Window
InitializeComponent(); InitializeComponent();
} }
private void SldThreshold1_OnDragCompleted(object sender, DragCompletedEventArgs args)
{
var vm = ViewModel;
vm.ProcessorConfig.ThresholdWidth = (int)Math.Round(((Slider)sender).Value);
}
private void SldThreshold2_OnDragCompleted(object sender, DragCompletedEventArgs args)
{
var vm = ViewModel;
vm.ProcessorConfig.ThresholdHeight = (int)Math.Round(((Slider)sender).Value);
}
private void SldBorder_OnDragCompleted(object sender, DragCompletedEventArgs e) private void SldBorder_OnDragCompleted(object sender, DragCompletedEventArgs e)
{ {
var vm = ViewModel; var vm = ViewModel;
+298
View File
@@ -0,0 +1,298 @@
using Common;
using ImageMagick;
using Microsoft.Win32;
using Ocr.Gui.Model;
using Ocr.Tesseract;
using Ocr.Tesseract.Configuration;
using Ocr.Tesseract.Models;
using Ocr.Tesseract.Screenshots;
using Ocr.Tesseract.Screenshots.Configuration;
using Ocr.Tesseract.Screenshots.Threshold;
using Process.Abstract.Configuration;
using Process.Interface;
using Serilog;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Ocr.Tesseract.Extensions;
namespace Ocr.Gui.Views;
internal class ImageViewModel : INotifyPropertyChanged
{
/// <summary>
/// Tesseract engine configuration
/// </summary>
public static readonly ITesseractConfiguration TesseractConfig =
new TesseractScreenshotConfiguration
{
DataPath = "tessdata",
Languages = new[] { "eng", "deu" }
};
public ScreenshotProcessorConfiguration ProcessorConfig { get; } = new();
/// <summary>
/// <see cref="Regex"/> expression for extracting whole words from scan results
/// </summary>
public static readonly Regex WordRegex = new(
@"[\w'\-]{2,}",
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase
);
public ImageViewModel()
{
ProcessorConfig.PropertyChanged += (sender, args) => Task.Run(UpdateImage);
OpenFileCommand = new Command(OpenFile);
SaveEditedImageCommand = new Command(SaveEditedImage);
}
public ImageViewModel(MagickImage image) : this()
{
Image = image;
}
private IProcessorChain<MagickImage, ScanResult> MakeProcessor()
{
var threshold =
new ThresholdAdaptiveProcessor(
ProcessorConfig.ThresholdWidth,
ProcessorConfig.ThresholdHeight
);
var chainConfig = new ProcessorChainConfiguration<MagickImage, MagickImage>()
.Use(new CloneImageProcessor());
if (ProcessorConfig.EnableResizing)
{
chainConfig
.Use(new ResizeProcessor(FilterType.Lanczos2Sharp, PixelInterpolateMethod.Mesh))
.Use(new ProcessingEvent<MagickImage>(OnProcessing));
}
chainConfig = chainConfig
.Use(new NormalizeProcessor())
.Use(new ProcessingEvent<MagickImage>(OnProcessing));
if (ProcessorConfig.EnableThresholding)
{
chainConfig = chainConfig
.Use(threshold)
.Use(new ProcessingEvent<MagickImage>(OnProcessing));
}
var preprocessing = chainConfig
.Use(new AddBorderProcessor(ProcessorConfig.Border))
.Use(new BinarizeProcessor())
.Use(new ProcessingEvent<MagickImage>(OnProcessing))
.Complete(new NegateCloneProcessor());
var postprocessing = new ProcessorChainConfiguration<ScanResult, ScanResult>()
.Use(new ConfidenceFilter(50))
.Use(new ToLowerProcessor())
.Use(new DuplicateFilter())
.Complete(new RegexFilter(WordRegex));
var scan = new TesseractProcessor(TesseractConfig);
return new ProcessorChainConfiguration<MagickImage, ScanResult>()
.Use(preprocessing)
.Use(scan)
.Use(new ProcessingEvent<ScanResult>(OnProcessed))
.Complete(postprocessing);
}
#region Overrides of Scanner
/// <inheritdoc />
protected void OnProcessing(IProcessor sender, ICollection<MagickImage> inputs)
{
Application.Current.Dispatcher.Invoke(() =>
{
foreach (var image in inputs)
{
Edited.Add(image.CloneImage());
}
});
}
/// <inheritdoc />
protected void OnProcessed(IProcessor sender, ICollection<ScanResult> inputs)
{
var wordStr = string.Join("\", \"", inputs);
ScannedText = $"{inputs.Count} words:{Environment.NewLine}[ \"{wordStr}\" ]";
}
/// <inheritdoc />
public void Clear()
{
Application.Current.Dispatcher.Invoke(() =>
{
ScannedText = string.Empty;
Words.Clear();
Edited.Clear();
}
);
}
#endregion
#region File Handling
private void OpenFile()
{
var dialog = new OpenFileDialog()
{
InitialDirectory = Directory.GetCurrentDirectory()
};
if (dialog.ShowDialog() == true)
{
Image = new MagickImage(dialog.FileName);
UpdateImage();
}
}
private void SaveEditedImage()
{
var basePath = AppDomain.CurrentDomain.BaseDirectory;
for (var i = 0; i < Edited.Count; i++)
{
Edited[i].Write(Path.Combine(basePath, $"edited_{i}.png"));
}
Log.Information($"Saved image to '{basePath}'");
System.Diagnostics.Process.Start(
"explorer.exe",
Path.GetDirectoryName(basePath) ?? string.Empty
);
}
#endregion
#region Updating data
private void UpdateImage()
{
Task.Run(() =>
{
IsIdle = false;
var scanner = new ScreenshotScanner(MakeProcessor());
Clear();
if (Image != null)
{
scanner.Process(new[] { Image });
}
Application.Current.Dispatcher.Invoke(() =>
{
var confidence = 0f;
foreach (var word in scanner.Lookup.Keys)
{
confidence += word.Confidence;
Words.Add(word);
}
Confidence = confidence / scanner.Lookup.Keys.Count;
}
);
IsIdle = true;
});
}
#endregion
#region Properties
private float _confidence;
private MagickImage? _image;
private bool _isIdle;
private string _scannedText = string.Empty;
public string ScannedText
{
get => _scannedText;
set
{
if (value == _scannedText)
{
return;
}
_scannedText = value;
OnPropertyChanged();
}
}
public bool IsIdle
{
get => _isIdle;
set
{
if (value == _isIdle)
{
return;
}
_isIdle = value;
OnPropertyChanged();
}
}
public float Confidence
{
get => _confidence;
set
{
_confidence = value;
OnPropertyChanged();
}
}
public ObservableCollection<MagickImage> Edited { get; } = new();
public MagickImage? Image
{
get => _image;
set
{
_image = value;
OnPropertyChanged();
}
}
public ObservableCollection<Word> Words { get; } = new();
#endregion Properties
#region Commands
public ICommand OpenFileCommand { get; private set; }
public ICommand SaveEditedImageCommand { get; private set; }
#endregion Commands
#region INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion INotifyPropertyChanged
}
@@ -23,9 +23,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lookup.Database", "..\Looku
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lookup.Abstract", "..\Lookup\Lookup.Abstract\Lookup.Abstract.csproj", "{D14DA0B8-5EAE-4C77-992E-3527DC84CE6D}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lookup.Abstract", "..\Lookup\Lookup.Abstract\Lookup.Abstract.csproj", "{D14DA0B8-5EAE-4C77-992E-3527DC84CE6D}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CLI", "CLI\CLI.csproj", "{2856493F-EF1C-42A1-8EE5-6C0387D08F95}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocr.Cli", "CLI\Ocr.Cli.csproj", "{2856493F-EF1C-42A1-8EE5-6C0387D08F95}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GUI", "GUI\GUI.csproj", "{DA447F14-1B1D-4733-99F3-6EF8225DCBAB}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocr.Gui", "GUI\Ocr.Gui.csproj", "{DA447F14-1B1D-4733-99F3-6EF8225DCBAB}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "Common\Common.csproj", "{A6C738AC-DCD7-4024-A92D-3FC3CDCD7229}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "Common\Common.csproj", "{A6C738AC-DCD7-4024-A92D-3FC3CDCD7229}"
EndProject EndProject
@@ -35,7 +35,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocr.Tesseract", "..\Ocr\Ocr
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocr.Tesseract.Screenshots", "..\Ocr\Ocr.Tesseract.Screenshots\Ocr.Tesseract.Screenshots.csproj", "{251F9AC9-3765-498C-83FD-DB3539A19CB3}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocr.Tesseract.Screenshots", "..\Ocr\Ocr.Tesseract.Screenshots\Ocr.Tesseract.Screenshots.csproj", "{251F9AC9-3765-498C-83FD-DB3539A19CB3}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReportGenerator", "ReportGenerator\ReportGenerator.csproj", "{729CB7AA-AB0D-4C39-AA17-7435E61FA0A6}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocr.Report", "ReportGenerator\Ocr.Report.csproj", "{729CB7AA-AB0D-4C39-AA17-7435E61FA0A6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ReportGeneration", "ReportGeneration", "{8E08DA62-584B-4E26-AEB7-2B35742EF7A5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReportGeneration.Interface", "..\ReportGeneration\ReportGeneration.Interface\ReportGeneration.Interface.csproj", "{A0760AFE-5CB7-4603-8861-285F62BE510F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReportGeneration.Generators", "..\ReportGeneration\ReportGeneration.Generators\ReportGeneration.Generators.csproj", "{F291F2D4-5BC5-4576-A210-BE8D447276FC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReportGeneration.Abstract", "..\ReportGeneration\ReportGeneration.Abstract\ReportGeneration.Abstract.csproj", "{80BF15A5-78EB-4B87-8C1B-7F90D6D8BC74}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocr.OneShot.Resampling", "Ocr.OneShot.Resampling\Ocr.OneShot.Resampling.csproj", "{4149E8A8-BD3C-4581-BDDD-FA70A58FBE37}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocr.OneShot.Postprocessing", "Ocr.OneShot.Postprocessing\Ocr.OneShot.Postprocessing.csproj", "{40EA7F8E-BF46-410F-BA2B-CB05B80DCDBB}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -95,6 +107,26 @@ Global
{729CB7AA-AB0D-4C39-AA17-7435E61FA0A6}.Debug|Any CPU.Build.0 = Debug|Any CPU {729CB7AA-AB0D-4C39-AA17-7435E61FA0A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{729CB7AA-AB0D-4C39-AA17-7435E61FA0A6}.Release|Any CPU.ActiveCfg = Release|Any CPU {729CB7AA-AB0D-4C39-AA17-7435E61FA0A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{729CB7AA-AB0D-4C39-AA17-7435E61FA0A6}.Release|Any CPU.Build.0 = Release|Any CPU {729CB7AA-AB0D-4C39-AA17-7435E61FA0A6}.Release|Any CPU.Build.0 = Release|Any CPU
{A0760AFE-5CB7-4603-8861-285F62BE510F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A0760AFE-5CB7-4603-8861-285F62BE510F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0760AFE-5CB7-4603-8861-285F62BE510F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0760AFE-5CB7-4603-8861-285F62BE510F}.Release|Any CPU.Build.0 = Release|Any CPU
{F291F2D4-5BC5-4576-A210-BE8D447276FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F291F2D4-5BC5-4576-A210-BE8D447276FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F291F2D4-5BC5-4576-A210-BE8D447276FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F291F2D4-5BC5-4576-A210-BE8D447276FC}.Release|Any CPU.Build.0 = Release|Any CPU
{80BF15A5-78EB-4B87-8C1B-7F90D6D8BC74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{80BF15A5-78EB-4B87-8C1B-7F90D6D8BC74}.Debug|Any CPU.Build.0 = Debug|Any CPU
{80BF15A5-78EB-4B87-8C1B-7F90D6D8BC74}.Release|Any CPU.ActiveCfg = Release|Any CPU
{80BF15A5-78EB-4B87-8C1B-7F90D6D8BC74}.Release|Any CPU.Build.0 = Release|Any CPU
{4149E8A8-BD3C-4581-BDDD-FA70A58FBE37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4149E8A8-BD3C-4581-BDDD-FA70A58FBE37}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4149E8A8-BD3C-4581-BDDD-FA70A58FBE37}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4149E8A8-BD3C-4581-BDDD-FA70A58FBE37}.Release|Any CPU.Build.0 = Release|Any CPU
{40EA7F8E-BF46-410F-BA2B-CB05B80DCDBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{40EA7F8E-BF46-410F-BA2B-CB05B80DCDBB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{40EA7F8E-BF46-410F-BA2B-CB05B80DCDBB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{40EA7F8E-BF46-410F-BA2B-CB05B80DCDBB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -112,6 +144,10 @@ Global
{E55F78E4-09F1-4D79-A9A2-460562C96DAB} = {CF65AA6A-2F25-4FEE-BDC1-AD96E1FFFA49} {E55F78E4-09F1-4D79-A9A2-460562C96DAB} = {CF65AA6A-2F25-4FEE-BDC1-AD96E1FFFA49}
{D9B70035-0159-4D75-8ED6-2461F060F683} = {E55F78E4-09F1-4D79-A9A2-460562C96DAB} {D9B70035-0159-4D75-8ED6-2461F060F683} = {E55F78E4-09F1-4D79-A9A2-460562C96DAB}
{251F9AC9-3765-498C-83FD-DB3539A19CB3} = {E55F78E4-09F1-4D79-A9A2-460562C96DAB} {251F9AC9-3765-498C-83FD-DB3539A19CB3} = {E55F78E4-09F1-4D79-A9A2-460562C96DAB}
{8E08DA62-584B-4E26-AEB7-2B35742EF7A5} = {CF65AA6A-2F25-4FEE-BDC1-AD96E1FFFA49}
{A0760AFE-5CB7-4603-8861-285F62BE510F} = {8E08DA62-584B-4E26-AEB7-2B35742EF7A5}
{F291F2D4-5BC5-4576-A210-BE8D447276FC} = {8E08DA62-584B-4E26-AEB7-2B35742EF7A5}
{80BF15A5-78EB-4B87-8C1B-7F90D6D8BC74} = {8E08DA62-584B-4E26-AEB7-2B35742EF7A5}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DFA659EE-FE78-4BD9-888B-78984354093E} SolutionGuid = {DFA659EE-FE78-4BD9-888B-78984354093E}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Ocr\Ocr.Tesseract.Screenshots\Ocr.Tesseract.Screenshots.csproj" />
<ProjectReference Include="..\..\Ocr\Ocr.Tesseract\Ocr.Tesseract.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,85 @@
// See https://aka.ms/new-console-template for more information
using System.Text.Json;
using System.Text.RegularExpressions;
using ImageMagick;
using Ocr.Tesseract;
using Ocr.Tesseract.Models;
using Ocr.Tesseract.Screenshots;
using Ocr.Tesseract.Screenshots.Configuration;
using Ocr.Tesseract.Screenshots.Threshold;
using Process.Abstract.Configuration;
using Process.Interface;
var wordRegex = new Regex(
@"[\w'\-]{2,}",
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase
);
var tesseractConfig = new TesseractScreenshotConfiguration
{
DataPath = "tessdata",
Languages = new[] { "eng", "deu" }
};
var jsonOptions = new JsonSerializerOptions()
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
var processor = MakeProcessor();
processor.Process(new[] { new MagickImage(args.Single()) });
return;
IProcessorChain<MagickImage, ScanResult> MakeProcessor()
{
var preprocessing = new ProcessorChainConfiguration<MagickImage, MagickImage>()
.Use(new CloneImageProcessor())
.Use(new ResizeProcessor(FilterType.Lanczos2Sharp, PixelInterpolateMethod.Mesh))
.Use(new NormalizeProcessor())
.Use(new ThresholdAdaptiveProcessor(15, 15))
.Use(new AddBorderProcessor(10))
.Use(new BinarizeProcessor())
.Complete(new NegateCloneProcessor());
var postprocessing = new ProcessorChainConfiguration<ScanResult, ScanResult>()
.Use(new ProcessingEvent<ScanResult>((_, data) => WriteToFile(data, "source")))
.Use(new ConfidenceFilter(50))
.Use(new ProcessingEvent<ScanResult>((_, data) => WriteToFile(data, "confidence")))
.Use(new ToLowerProcessor())
.Use(new ProcessingEvent<ScanResult>((_, data) => WriteToFile(data, "normalize")))
.Use(new DuplicateFilter())
.Use(new ProcessingEvent<ScanResult>((_, data) => WriteToFile(data, "duplicates")))
.Use(new RegexFilter(wordRegex))
.Complete(new ProcessingEvent<ScanResult>((_, data) => WriteToFile(data, "regex")));
var scan = new TesseractProcessor(tesseractConfig);
return new ProcessorChainConfiguration<MagickImage, ScanResult>()
.Use(preprocessing)
.Use(scan)
.Complete(postprocessing);
}
void WriteToFile(ICollection<ScanResult> data, string name)
{
using var file1 = File.Open($"{name}.detailed.json", FileMode.Create);
JsonSerializer.Serialize(file1, data.Select(WordInfo.Create), jsonOptions);
using var file2 = File.Open($"{name}.json", FileMode.Create);
JsonSerializer.Serialize(file2, data.Select(d => d.Word.Text), jsonOptions);
}
struct WordInfo
{
public string Text { get; set; }
public double Confidence { get; set; }
public static WordInfo Create(ScanResult result) => new()
{
Text = result.Word.Text,
Confidence = result.Word.Confidence
};
}
@@ -0,0 +1,9 @@
{
"profiles": {
"Refresh thesis results": {
"commandName": "Project",
"commandLineArgs": "source.png",
"workingDirectory": "C:\\Users\\Simon\\Documents\\Userdata\\FH\\SEM5\\BA\\bsc\\include\\postprocessing"
}
}
}
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="13.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Ocr\Ocr.Tesseract\Ocr.Tesseract.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,29 @@
using ImageMagick;
using Ocr.Tesseract.Extensions;
const float factor = 4f;
const string downscaled = "downscaled.png";
using (var image = new MagickImage(args.Single()))
{
image.ResizeImage(0.8f, FilterType.Catrom, PixelInterpolateMethod.Undefined);
image.Write(downscaled);
}
using (var image = new MagickImage(downscaled))
{
image.ResizeImage(factor, FilterType.Lanczos, PixelInterpolateMethod.Catrom);
image.Write("Lanczos.png");
}
using (var image = new MagickImage(downscaled))
{
image.ResizeImage(factor, FilterType.Point, PixelInterpolateMethod.Integer);
image.Write("Nearest.png");
}
using (var image = new MagickImage(downscaled))
{
image.ResizeImage(factor, FilterType.Hermite, PixelInterpolateMethod.Mesh);
image.Write("Hermite.png");
}
@@ -0,0 +1,14 @@
{
"profiles": {
"Test single img": {
"commandName": "Project",
"commandLineArgs": "source.png",
"workingDirectory": "D:\\git\\ba_ocr\\ocr\\Implementation\\testdata"
},
"Refresh thesis results": {
"commandName": "Project",
"commandLineArgs": "source.png",
"workingDirectory": "C:\\Users\\Simon\\Documents\\Userdata\\FH\\SEM5\\BA\\bsc\\include\\resampling"
}
}
}
@@ -1,4 +1,4 @@
namespace ReportGenerator.Models; namespace Ocr.Report.Models;
public readonly struct ImageStats public readonly struct ImageStats
{ {
@@ -1,6 +1,6 @@
using Common.Distance; using Common.Distance;
namespace ReportGenerator.Models; namespace Ocr.Report.Models;
public readonly struct ProcessorStat : IDistanceComparer<IEnumerable<string>> public readonly struct ProcessorStat : IDistanceComparer<IEnumerable<string>>
{ {
@@ -47,7 +47,6 @@ public readonly struct ProcessorStat : IDistanceComparer<IEnumerable<string>>
) / reference.Count; ) / reference.Count;
Words = GetDistanceInfos(reference, hypothesis).ToArray(); Words = GetDistanceInfos(reference, hypothesis).ToArray();
// Words = reference.Select(r => GetDistanceInfo(r, hypothesis)).ToArray();
} }
@@ -66,8 +65,7 @@ public readonly struct ProcessorStat : IDistanceComparer<IEnumerable<string>>
foreach (var reference in referenceCollection) foreach (var reference in referenceCollection)
{ {
var tResults = hypothesisCollection var tResults = hypothesisCollection
.Select(hypothesis => new DistanceComparer<string>(reference, hypothesis)) .Select(hypothesis => (IDistanceComparer<string>)new DistanceComparer<string>(reference, hypothesis))
.Cast<IDistanceComparer<string>>()
.ToList(); .ToList();
results.AddRange(tResults); results.AddRange(tResults);
@@ -1,7 +1,7 @@
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace ReportGenerator.Models; namespace Ocr.Report.Models;
public struct ScanFileInfo public struct ScanFileInfo
{ {
@@ -0,0 +1,31 @@
using System.Text.Json;
namespace Ocr.Report.Models;
internal struct TagFileInfo
{
public string Path { get; private init; }
public string ImageName { get; set; }
public ICollection<string> GetWords()
{
using var file = File.OpenRead(Path);
return JsonDocument
.Parse(file)
.RootElement
.GetProperty("words")
.EnumerateArray()
.Select(w => w.GetString() ?? throw new Exception("Cannot parse null words"))
.ToArray();
}
public static TagFileInfo FromPath(string path) => new()
{
Path = path,
ImageName = System.IO.Path.GetFileNameWithoutExtension(path),
};
/// <inheritdoc />
public override string ToString() => ImageName;
}
@@ -11,15 +11,13 @@
<None Remove="Properties\htmldocument-style.css" /> <None Remove="Properties\htmldocument-style.css" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Generator\Generator\Resources\Style.css" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\ReportGeneration\ReportGeneration.Generators\ReportGeneration.Generators.csproj" />
<ProjectReference Include="..\..\ReportGeneration\ReportGeneration.Interface\ReportGeneration.Interface.csproj" />
<ProjectReference Include="..\Common\Common.csproj" /> <ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup> </ItemGroup>
@@ -1,8 +1,9 @@
using Common.Extensions; using Common.Extensions;
using ReportGenerator.Generator.Generator; using Ocr.Report.Models;
using ReportGenerator.Models; using ReportGeneration.Generators;
using ReportGeneration.Generators.LatexInclude;
namespace ReportGenerator; namespace Ocr.Report;
internal static class Program internal static class Program
{ {
@@ -19,22 +20,24 @@ internal static class Program
Console.WriteLine("Generating report"); Console.WriteLine("Generating report");
var scans = Scan(tagFileInfos, scanFileInfos); var scans = Scan(tagFileInfos, scanFileInfos);
var path = Path.GetFullPath("report.html"); var path = Path.GetFullPath("report");
using var document = new HtmlDocumentGenerator(path); using var document = new LatexIncludeDocumentGenerator(path, true);
using var report = new ReportGenerator("OCR Report", document, scans) using var report = new ReportGenerator("OCR Report", document, scans)
{
MaxDisplayRows = 8
}
.AddComparison("Processing summary (Average)", v => .AddComparison("Processing summary (Average)", v =>
{ {
var result = v.Average(out var deviation); var result = v.Average(out var deviation);
return (result, deviation); return (result, deviation);
}) })
// .AddComparison("Processing summary (Cumulative)", v => v.Sum())
.AddComparison("Processing summary (Median)", v => .AddComparison("Processing summary (Median)", v =>
{ {
var result = v.Median(out var deviation); var result = v.Median(out var deviation);
return (result, deviation); return (result, deviation);
}) })
// .AddProcessorStats("Processor Stats") .AddProcessorStats("Processor Stats")
.AddImageStatsFull("Scan Results"); .AddImageStatsFull("Scan Results");
Console.WriteLine($"Saved report to '{path}'"); Console.WriteLine($"Saved report to '{path}'");
@@ -3,7 +3,7 @@
"ReportGenerator": { "ReportGenerator": {
"commandName": "Project", "commandName": "Project",
"commandLineArgs": "\"img\" \"results\"", "commandLineArgs": "\"img\" \"results\"",
"workingDirectory": "D:\\git\\BA\\Examples\\testdata" "workingDirectory": "D:\\git\\ba_ocr\\Implementation\\testdata"
} }
} }
} }
@@ -1,11 +1,15 @@
using ReportGenerator.Generator.Interface; using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
using ReportGenerator.Generator.Model; using Ocr.Report.Models;
using ReportGenerator.Models; using ReportGeneration.Abstract.Model;
using ReportGeneration.Interface;
using System;
namespace ReportGenerator; namespace Ocr.Report;
public class ReportGenerator : IDisposable public class ReportGenerator : IDisposable
{ {
public int? MaxDisplayRows { get; init; }
private IDocumentGenerator Document { get; } private IDocumentGenerator Document { get; }
private ICollection<ImageStats> Images { get; } private ICollection<ImageStats> Images { get; }
@@ -57,7 +61,7 @@ public class ReportGenerator : IDisposable
foreach (var (processor, images) in processors) foreach (var (processor, images) in processors)
{ {
var ordered = images IEnumerable<(ImageStats Stats, double Distance)> ordered = images
.Select(i => (Stats: i, Distance: i .Select(i => (Stats: i, Distance: i
.Processors .Processors
.Where(p => p.Name.Equals(processor)) .Where(p => p.Name.Equals(processor))
@@ -66,10 +70,17 @@ public class ReportGenerator : IDisposable
)) ))
.OrderBy(i => i.Distance) .OrderBy(i => i.Distance)
.ToArray(); .ToArray();
if (MaxDisplayRows.HasValue)
{
ordered = ordered
.Take(MaxDisplayRows.Value / 2)
.Concat(ordered.TakeLast(MaxDisplayRows.Value / 2));
}
Document Document
.AppendHeading(3, processor) .AppendHeading(3, processor)
.AppendTable(2, table => .AppendTable(3, table =>
{ {
table.AppendHeader(new[] { "Image", "Preview", "Distance" }); table.AppendHeader(new[] { "Image", "Preview", "Distance" });
@@ -79,7 +90,7 @@ public class ReportGenerator : IDisposable
table.AppendRow(new[] table.AppendRow(new[]
{ {
$"<a href=\"#{stats.ImageName}\">{stats.ImageName}</a>", Ellipsis(stats.ImageName.Replace("\\", "\\\\") + ".png", 25),
Document.FormatImage(imgPath, new Bounds(0, 150)), Document.FormatImage(imgPath, new Bounds(0, 150)),
distance.ToString("F2") distance.ToString("F2")
}); });
@@ -103,21 +114,30 @@ public class ReportGenerator : IDisposable
Document.FormatImage(Path.Combine("img", stat.ImageName), new Bounds(0, 350)) Document.FormatImage(Path.Combine("img", stat.ImageName), new Bounds(0, 350))
) )
.AppendTable( .AppendTable(
stat.Reference.Count + 5, stat.Reference.Count + 6,
table => table =>
{ {
table.AppendHeader(stat table.AppendHeader(stat
.Reference .Reference
.Prepend("Image") .Prepend("Image")
.Prepend("Perfect matches")
.Prepend("CER (avg)") .Prepend("CER (avg)")
.Prepend("WER") .Prepend("WER")
.Prepend("Elapsed") .Prepend("Elapsed")
.Prepend("Processor") .Prepend("Processor")
); );
var processors = stat.Processors IEnumerable<ProcessorStat> processors = stat.Processors
.OrderBy(s => s.Distance) .OrderBy(s => s.Distance)
.ThenBy(s => s.ProcessingTime); .ThenBy(s => s.ProcessingTime)
.ToArray();
if (MaxDisplayRows.HasValue)
{
processors = processors
.Take(MaxDisplayRows.Value / 2)
.Concat(processors.TakeLast(MaxDisplayRows.Value / 2));
}
foreach (var processor in processors) foreach (var processor in processors)
{ {
@@ -126,10 +146,11 @@ public class ReportGenerator : IDisposable
table.AppendRow(processor.Words table.AppendRow(processor.Words
.Select(s => s.ToString() ?? string.Empty) .Select(s => s.ToString() ?? string.Empty)
.Prepend(Document.FormatImage(imgPath, new Bounds(0, 150))) .Prepend(Document.FormatImage(imgPath, new Bounds(0, 150)))
.Prepend($"{processor.Words.Count(w => w.Distance == 0)} / {processor.Words.Count}")
.Prepend(processor.Words.Average(s => s.Distance).ToString("F2")) .Prepend(processor.Words.Average(s => s.Distance).ToString("F2"))
.Prepend($"{processor.Distance * 100:F1}%") .Prepend($"{processor.Distance * 100:F1}%")
.Prepend($"{processor.ProcessingTime * 1000:F1}ms") .Prepend($"{processor.ProcessingTime * 1000:F1}ms")
.Prepend(processor.Name) .Prepend(Ellipsis(processor.Name, 25))
); );
} }
}) })
@@ -198,7 +219,6 @@ public class ReportGenerator : IDisposable
return this; return this;
} }
private void AppendComparison( private void AppendComparison(
(string name, string unit)? valueInfo, (string name, string unit)? valueInfo,
IEnumerable<(string, double, double)> values IEnumerable<(string, double, double)> values
@@ -242,6 +262,20 @@ public class ReportGenerator : IDisposable
#endregion #endregion
private static string Ellipsis(string str, int maxLength, string ellipsis = "...")
{
if (str.Length <= maxLength)
{
return str;
}
int subStrLen = (maxLength - ellipsis.Length) / 2;
return string.Concat(
str.Substring(0, subStrLen),
ellipsis,
str.Substring(str.Length - subStrLen, subStrLen));
}
#region IDisposable #region IDisposable
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
@@ -260,4 +294,4 @@ public class ReportGenerator : IDisposable
} }
#endregion #endregion
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Some files were not shown because too many files have changed in this diff Show More