This commit is contained in:
Simon Gruber
2023-11-20 07:43:12 +01:00
parent fd26cf45e0
commit d8008e4f05
25 changed files with 1063 additions and 131 deletions
+24 -1
View File
@@ -8,32 +8,55 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Ocr\Ocr.Processors\Ocr.Processors.csproj" />
<ProjectReference Include="..\Common\Common.csproj" /> <ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Update="img\command-processing_screentypes_controlgroup_005.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\command-processing_screentypes_controlgroup_005.png"> <None Update="img\command-processing_screentypes_controlgroup_005.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="img\editor_startpage_project-exist_001.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\editor_startpage_project-exist_001.png"> <None Update="img\editor_startpage_project-exist_001.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="img\editor_windows_position_006.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\editor_windows_position_006.png"> <None Update="img\editor_windows_position_006.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="img\historian_assistent_001.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\historian_assistent_001.png"> <None Update="img\historian_assistent_001.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="img\zrs_MetadataEditor_variables_001.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\zrs_MetadataEditor_variables_001.png"> <None Update="img\zrs_MetadataEditor_variables_001.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="img\zrs_REPORTS_EfficencyClass_009.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\zrs_REPORTS_EfficencyClass_009.png"> <None Update="img\zrs_REPORTS_EfficencyClass_009.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="img\zrs_ZAMS_3rd-connector_014.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\zrs_ZAMS_3rd-connector_014.png"> <None Update="img\zrs_ZAMS_3rd-connector_014.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="img\zrs_ZAMS_filter-alarmgroup_001.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\zrs_ZAMS_filter-alarmgroup_001.png"> <None Update="img\zrs_ZAMS_filter-alarmgroup_001.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
+101
View File
@@ -0,0 +1,101 @@
using ImageMagick;
using Ocr.Tesseract;
using Ocr.Tesseract.Configuration;
using Ocr.Tesseract.Extensions;
using Ocr.Tesseract.Models;
using Ocr.Tesseract.Screenshots;
using Ocr.Tesseract.Screenshots.Configuration;
using Process.Abstract.Configuration;
using Process.Interface;
using System.Text.Json;
using System.Text.RegularExpressions;
internal class EvaluationProcessor
{
/// <summary>
/// <see cref="Regex"/> expression for extracting whole words from scan results
/// </summary>
private static Regex WordRegex = new(
@"[\w'\-]{2,}",
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase
);
private static ITesseractConfiguration TesseractConfig = new TesseractScreenshotConfiguration()
{
DataPath = "tessdata",
Languages = new[] { "eng", "deu" }
};
private ScreenshotProcessorConfiguration ProcessorConfig = new ScreenshotProcessorConfiguration
{
Border = 0,
EnableResizing = false,
EnableThresholding = false,
FilterConnectedComponents = false,
ThresholdHeight = 0,
ThresholdWidth = 0
};
private static readonly TesseractProcessor tesseractProcessor = new(TesseractConfig);
private static readonly IProcessorChain<ScanResult, ScanResult> postProcessor =
new ProcessorChainConfiguration<ScanResult, ScanResult>()
.Use(new ConfidenceFilter(50))
.Use(new ToLowerProcessor())
.Use(new DuplicateFilter())
.Complete(new RegexFilter(WordRegex));
private readonly IProcessor<MagickImage, MagickImage> _thresholdProcessor;
public string OutputFolder { get; init; } = "results";
public EvaluationProcessor(IProcessor<MagickImage, MagickImage> thresholdProcessor)
{
_thresholdProcessor = thresholdProcessor;
}
/// <inheritdoc />
public Task Process(MagickImage image) => Task.Run(async () =>
{
Directory.CreateDirectory(OutputFolder);
var processor = MakeProcessor();
var results = processor.Process(new[] { image }).Select(r => r.Word);
var name = Path.GetFileNameWithoutExtension(image.FileName);
await using var file = File.OpenWrite(Path.Combine(OutputFolder, $"{name}.{_thresholdProcessor}.json"));
await JsonSerializer.SerializeAsync(file, results);
});
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)
{
var tImages = images.ToArray();
for (var i = 0; i < tImages.Length; i++)
{
var image = tImages[i].CloneImage();
var name = Path.GetFileName(image.FileName);
image.Write(Path.Combine(OutputFolder, $"{_thresholdProcessor}.{i:D2}.{name}"));
}
return tImages;
}
}
+93 -11
View File
@@ -1,16 +1,98 @@
using Common; using Common.Extensions;
using Common.Extensions;
using ImageMagick; using ImageMagick;
using Ocr.Tesseract.Screenshots.Threshold;
using System.Diagnostics.CodeAnalysis;
using System.Text;
var scanner = new ScreenshotScanner(); namespace CLI;
Console.WriteLine($"# Scanning: {string.Join(',', args)}..."); public class Program
scanner.Process(GetImages(args)); {
private readonly IDictionary<TaskStatus, string> _statusMap = new Dictionary<TaskStatus, string>()
{
{ TaskStatus.RanToCompletion, "DONE" },
{ TaskStatus.Faulted, "FAULT" },
{ TaskStatus.Canceled, "CANCL" },
{ TaskStatus.Created, "WAIT" },
{ TaskStatus.WaitingToRun, "WAIT" },
{ TaskStatus.WaitingForActivation, "WAIT" },
{ TaskStatus.Running, "RUN" },
{ TaskStatus.WaitingForChildrenToComplete, "RUN" },
};
Console.WriteLine($"# Results ({scanner.Lookup.Keys.Count}):"); public Task Run(string[] args)
Console.WriteLine(string.Join(' ', scanner.Lookup.Keys)); {
var scans = (
from processor in MakeThresholdVariations()
from path in ExpandPaths(args)
select (Key: path, Task: processor.Process(new MagickImage(path)))
).ToArray();
static IEnumerable<MagickImage> GetImages(IEnumerable<string> paths) => paths var waitTask = Task.WhenAll(scans.Select(i => i.Task));
.SelectMany(p => p.ExpandPath())
.Select(p => new MagickImage(p)) while (!waitTask.Wait(TimeSpan.FromSeconds(1)))
.ToArray(); {
var sb = new StringBuilder(scans.Length * 30);
foreach (var info in scans)
{
sb.AppendLine($"{_statusMap[info.Task.Status],-5}: {info.Key}");
if (info.Task.Exception is not null)
{
sb.AppendLine($"> EX: {info.Task.Exception?.Message}");
}
}
sb.AppendLine();
Console.Clear();
Console.Write(sb.ToString());
}
return waitTask;
}
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
private static IEnumerable<EvaluationProcessor> MakeThresholdVariations()
{
yield return new(new ThresholdAdaptiveProcessor(17));
yield return new(new ThresholdAdaptiveProcessor(5));
yield return new(new ThresholdAdaptiveProcessor(10));
yield return new(new ThresholdAdaptiveProcessor(15));
yield return new(new ThresholdProcessor(20));
yield return new(new ThresholdProcessor(40));
yield return new(new ThresholdProcessor(60));
yield return new(new ThresholdProcessor(80));
yield return new(new AutoThresholdProcessor(AutoThresholdMethod.Kapur));
yield return new(new AutoThresholdProcessor(AutoThresholdMethod.OTSU));
yield return new(new AutoThresholdProcessor(AutoThresholdMethod.Triangle));
}
private static IEnumerable<string> ExpandPaths(params string[] paths) =>
paths.SelectMany(p => p.ExpandPath());
#region Main
public static int Main(string[] args)
{
Console.WriteLine("Starting up");
try
{
new Program()
.Run(args)
.Wait();
Console.WriteLine("Completed");
return 0;
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return 1;
}
}
#endregion
}
@@ -0,0 +1,13 @@
{
"words": [
"interlockings",
"active",
"interlocking",
"text",
"typ",
"static",
"id",
"10034",
"no"
]
}
@@ -0,0 +1,80 @@
{
"words": [
"connection",
"create",
"driver",
"variable",
"visualization",
"frame",
"screen",
"execution",
"function",
"first",
"steps",
"welcome",
"to",
"the",
"engineering",
"studio",
"we",
"want",
"make",
"your",
"in",
"easy",
"this",
"end",
"are",
"introducing",
"you",
"basic",
"for",
"configuring",
"a",
"project",
"following",
"chapters",
"how",
"work",
"and",
"what",
"need",
"projects",
"can",
"incorporate",
"data",
"points",
"into",
"is",
"tool",
"control",
"of",
"processes",
"generally",
"do",
"not",
"anything",
"because",
"properties",
"dialogs",
"setting",
"parameters",
"them",
"if",
"rproject",
"has",
"been",
"configured",
"compile",
"it",
"start",
"service",
"engine",
"then",
"see",
"front",
"application",
"back",
"next"
]
}
@@ -0,0 +1,76 @@
{
"words": [
"zenon",
"energy",
"edition",
"file",
"edit",
"screens",
"options",
"window",
"help",
"100",
"activatestartupscreen",
"properties",
"cross",
"reference",
"list",
"report",
"start",
"variables",
"functions",
"language",
"historian",
"recipes",
"time",
"control",
"windows",
"CE",
"filter",
"text",
"total",
"filtered",
"select",
"project",
"tree",
"network",
"topology",
"welcome",
"to",
"the",
"property",
"shows",
"you",
"brief",
"information",
"each",
"propery",
"what",
"is",
"needed",
"for",
"its",
"standard",
"value",
"from",
"other",
"depended",
"on",
"etc",
"a",
"link",
"online",
"also",
"provided",
"and",
"name",
"VBA",
"VSTA",
"displayed",
"output",
"load",
"DOKU",
"ready",
"elemente"
]
}
@@ -0,0 +1,57 @@
{
"words": [
"start",
"page",
"create",
"archive",
"archives",
"serve",
"for",
"the",
"recording",
"of",
"process",
"values",
"over",
"a",
"longer",
"period",
"time",
"recorded",
"data",
"can",
"be",
"summarized",
"in",
"aggregated",
"sum",
"average",
"value",
"minimum",
"and",
"maximum",
"certain",
"used",
"extended",
"trend",
"this",
"assistant",
"should",
"help",
"you",
"with",
"basic",
"engineering",
"an",
"if",
"want",
"to",
"teditor",
"without",
"press",
"cancel",
"now",
"back",
"next"
]
}
@@ -0,0 +1,56 @@
{
"words": [
"metadata",
"editor",
"file",
"edit",
"help",
"equipment",
"modeling",
"event",
"classes",
"groups",
"users",
"project",
"variables",
"archives",
"efficiency",
"class",
"models",
"reference",
"curves",
"name",
"visual",
"description",
"apply",
"clear",
"glass",
"bottle",
"line",
"WS",
"Cur",
"State",
"depalletizer",
"unpacker",
"washer",
"filler",
"pasteurizer",
"labeler",
"conveyor",
"grate",
"inspector",
"operation",
"packer",
"connected",
"to",
"reporting",
"on",
"testenv",
"local",
"total",
"filtered",
"selected",
"changed",
"ready"
]
}
@@ -0,0 +1,68 @@
{
"words": [
"new",
"report",
"theme",
"efficiency",
"class",
"analysis",
"this",
"contains",
"templates",
"that",
"allow",
"based",
"on",
"models",
"the",
"data",
"can",
"be",
"processed",
"archives",
"equipment",
"groups",
"or",
"formulas",
"template",
"formula",
"reports",
"perform",
"for",
"period",
"1",
"historic",
"is",
"normalised",
"to",
"a",
"defined",
"in",
"model",
"cen",
"shown",
"an",
"diagram",
"and",
"tables",
"preview",
"from",
"costs",
"hour",
"name",
"lower",
"limit",
"higher",
"EUR",
"value",
"current",
"zenon",
"analyzer",
"www",
"copadata",
"com",
"ok",
"cancel",
"page"
]
}
@@ -0,0 +1,38 @@
{
"words": [
"metadata",
"datasource",
"variables",
"project",
"glass",
"bottle",
"line",
"identification",
"visualname",
"description",
"cycle",
"time",
"dd",
"hh",
"mm",
"ss",
"assigned",
"equipment",
"groups",
"plant",
"media",
"total",
"previous",
"next",
"ok",
"cancel",
"new",
"archive",
"for",
"the",
"3rd",
"party",
"database",
"connector"
]
}
@@ -0,0 +1,22 @@
{
"words": [
"alarm",
"group",
"prefilter",
"static",
"enable",
"alarm",
"group",
"prefiltering",
"machine",
"alarm",
"emergency",
"stop",
"external",
"failure",
"eqauipment",
"select",
"all",
"deselect"
]
}
+7 -70
View File
@@ -1,14 +1,7 @@
using ImageMagick; using ImageMagick;
using Lookup.Memory; using Lookup.Memory;
using Ocr.Tesseract;
using Ocr.Tesseract.Configuration;
using Ocr.Tesseract.Models; 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 Process.Interface;
using System.Text.RegularExpressions;
namespace Common namespace Common
{ {
@@ -18,15 +11,10 @@ namespace Common
/// </summary> /// </summary>
public class ScreenshotScanner public class ScreenshotScanner
{ {
private readonly IProcessor<MagickImage, ScanResult> _processor;
/// <summary> /// <summary>
/// <see cref="Regex"/> expression for extracting whole words from scan results /// The screenshot processor
/// </summary> /// </summary>
private static readonly Regex wordRegex = new( protected IProcessor<MagickImage, ScanResult> Processor { get; }
@"[\w'\-]{2,}",
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase
);
/// <summary> /// <summary>
/// Data storage /// Data storage
@@ -34,25 +22,12 @@ namespace Common
public Lookup.Interface.ILookup<Word, MagickImage> Lookup { get; } = public Lookup.Interface.ILookup<Word, MagickImage> Lookup { get; } =
new MemoryLookup<Word, MagickImage>(); new MemoryLookup<Word, MagickImage>();
/// <summary>
/// Configuration of the <see cref="ImageProcessor"/>
/// </summary>
public ScreenshotProcessorConfiguration ImageProcessorConfiguration { get; }
public ITesseractConfiguration TesseractConfiguration { get; }
/// <summary> /// <summary>
/// Constructor /// Constructor
/// </summary> /// </summary>
public ScreenshotScanner( public ScreenshotScanner(IProcessor<MagickImage, ScanResult> processor)
ScreenshotProcessorConfiguration imageProcessorConfig,
ITesseractConfiguration tesseractConfig
)
{ {
ImageProcessorConfiguration = imageProcessorConfig; Processor = processor;
TesseractConfiguration = tesseractConfig;
_processor = MakeProcessor();
} }
/// <summary> /// <summary>
@@ -62,53 +37,15 @@ namespace Common
/// <param name="images">The <see cref="MagickImage"/>s to process</param> /// <param name="images">The <see cref="MagickImage"/>s to process</param>
public void Process(IEnumerable<MagickImage> images) public void Process(IEnumerable<MagickImage> images)
{ {
foreach (var kv in _processor.Process(images)) foreach (var kv in Processor.Process(images))
{ {
Lookup.Add(kv.Word, kv.Image); Lookup.Add(kv.Word, kv.Image);
} }
} }
private IProcessor<MagickImage, ScanResult> MakeProcessor() public virtual void Clear()
{
var threshold =
new ThresholdAdaptiveProcessor(ImageProcessorConfiguration.ThresholdWidth,
ImageProcessorConfiguration.ThresholdHeight);
// var threshold = new AutoThresholdProcessor(AutoThresholdMethod.Kapur);
// var threshold = new AutoThresholdProcessor(AutoThresholdMethod.OTSU);
// var threshold = new AutoThresholdProcessor(AutoThresholdMethod.Triangle);
// var threshold = new ThresholdProcessor(60);
var preprocessing = new ProcessorChainConfiguration<MagickImage, MagickImage>()
.Use(new CloneImageProcessor())
.Use(new ResizeProcessor(FilterType.Lanczos2Sharp, PixelInterpolateMethod.Mesh))
.Use(new NormalizeProcessor())
.Use(threshold)
.Use(new AddBorderProcessor(10))
.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(TesseractConfiguration);
return new ProcessorChainConfiguration<MagickImage, ScanResult>()
.Use(preprocessing)
.Use(new ProcessingEvent<MagickImage>(OnProcessing))
.Use(scan)
.Use(new ProcessingEvent<ScanResult>(OnProcessed))
.Complete(postprocessing);
}
protected virtual void OnProcessing(IProcessor sender, ICollection<MagickImage> inputs)
{
}
protected virtual void OnProcessed(IProcessor sender, ICollection<ScanResult> inputs)
{ {
Lookup.Clear();
} }
} }
} }
+6
View File
@@ -35,6 +35,8 @@ 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}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -89,6 +91,10 @@ Global
{251F9AC9-3765-498C-83FD-DB3539A19CB3}.Debug|Any CPU.Build.0 = Debug|Any CPU {251F9AC9-3765-498C-83FD-DB3539A19CB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{251F9AC9-3765-498C-83FD-DB3539A19CB3}.Release|Any CPU.ActiveCfg = Release|Any CPU {251F9AC9-3765-498C-83FD-DB3539A19CB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{251F9AC9-3765-498C-83FD-DB3539A19CB3}.Release|Any CPU.Build.0 = Release|Any CPU {251F9AC9-3765-498C-83FD-DB3539A19CB3}.Release|Any CPU.Build.0 = Release|Any CPU
{729CB7AA-AB0D-4C39-AA17-7435E61FA0A6}.Debug|Any CPU.ActiveCfg = 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.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
+86 -20
View File
@@ -2,9 +2,13 @@
using GUI.Model; using GUI.Model;
using ImageMagick; using ImageMagick;
using Microsoft.Win32; using Microsoft.Win32;
using Ocr.Tesseract;
using Ocr.Tesseract.Configuration; using Ocr.Tesseract.Configuration;
using Ocr.Tesseract.Models; using Ocr.Tesseract.Models;
using Ocr.Tesseract.Screenshots;
using Ocr.Tesseract.Screenshots.Configuration; using Ocr.Tesseract.Screenshots.Configuration;
using Ocr.Tesseract.Screenshots.Threshold;
using Process.Abstract.Configuration;
using Process.Interface; using Process.Interface;
using Serilog; using Serilog;
using System; using System;
@@ -14,33 +18,91 @@ using System.ComponentModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
namespace GUI.ViewModels; namespace GUI.ViewModels;
internal class ImageViewModel : ScreenshotScanner, INotifyPropertyChanged internal class ImageViewModel : INotifyPropertyChanged
{ {
private static ITesseractConfiguration CreateTesseractConfiguration() => /// <summary>
new TesseractScreenshotConfiguration() /// 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", DataPath = "tessdata",
Languages = new[] { "eng", "deu" } Languages = new[] { "eng", "deu" }
}; };
public ImageViewModel() : base(new(), CreateTesseractConfiguration()) 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()
{ {
ImageProcessorConfiguration.PropertyChanged += (sender, args) => Task.Run(UpdateImage); Scanner = new ScreenshotScanner(MakeProcessor());
ProcessorConfig.PropertyChanged += (sender, args) => Task.Run(UpdateImage);
OpenFileCommand = new Command(OpenFile); OpenFileCommand = new Command(OpenFile);
SaveEditedImageCommand = new Command(SaveEditedImage); 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 #region Overrides of Scanner
/// <inheritdoc /> /// <inheritdoc />
protected override void OnProcessing(IProcessor sender, ICollection<MagickImage> inputs) protected void OnProcessing(IProcessor sender, ICollection<MagickImage> inputs)
{ {
Application.Current.Dispatcher.Invoke(() => Application.Current.Dispatcher.Invoke(() =>
{ {
@@ -52,30 +114,28 @@ internal class ImageViewModel : ScreenshotScanner, INotifyPropertyChanged
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void OnProcessed(IProcessor sender, ICollection<ScanResult> inputs) protected void OnProcessed(IProcessor sender, ICollection<ScanResult> inputs)
{ {
ScannedText = $"[{inputs.Count} words] " + string.Join(' ', inputs); ScannedText = $"[{inputs.Count} words] " + string.Join(' ', inputs);
} }
#endregion /// <inheritdoc />
public void Clear()
public ImageViewModel(MagickImage image) : this()
{
Image = image;
}
private void Clear()
{ {
Scanner.Clear();
Application.Current.Dispatcher.Invoke(() => Application.Current.Dispatcher.Invoke(() =>
{ {
ScannedText = string.Empty; ScannedText = string.Empty;
Words.Clear(); Words.Clear();
Lookup.Clear();
Edited.Clear(); Edited.Clear();
} }
); );
} }
#endregion
#region File Handling
private void OpenFile() private void OpenFile()
{ {
var dialog = new OpenFileDialog() var dialog = new OpenFileDialog()
@@ -105,10 +165,14 @@ internal class ImageViewModel : ScreenshotScanner, INotifyPropertyChanged
); );
} }
#endregion
#region Updating data
private void UpdateConfidence() private void UpdateConfidence()
{ {
Confidence = Lookup.Keys.Any() Confidence = Scanner.Lookup.Keys.Any()
? Lookup.Keys.Sum(key => key.Confidence) / Lookup.Keys.Count ? Scanner.Lookup.Keys.Sum(key => key.Confidence) / Scanner.Lookup.Keys.Count
: 0; : 0;
} }
@@ -121,7 +185,7 @@ internal class ImageViewModel : ScreenshotScanner, INotifyPropertyChanged
Clear(); Clear();
if (Image != null) if (Image != null)
{ {
Process(new[] { Image }); Scanner.Process(new[] { Image });
} }
UpdateWords(); UpdateWords();
@@ -135,13 +199,15 @@ internal class ImageViewModel : ScreenshotScanner, INotifyPropertyChanged
{ {
Application.Current.Dispatcher.Invoke(() => Application.Current.Dispatcher.Invoke(() =>
{ {
foreach (var word in Lookup.Keys) foreach (var word in Scanner.Lookup.Keys)
{ {
Words.Add(word); Words.Add(word);
} }
}); });
} }
#endregion
#region Properties #region Properties
private float _confidence; private float _confidence;
+3 -3
View File
@@ -160,7 +160,7 @@
<CheckBox <CheckBox
x:Name="EnableThreshold" x:Name="EnableThreshold"
Content="Apply Threshold" Content="Apply Threshold"
IsChecked="{Binding ImageProcessorConfiguration.EnableThresholding}" /> IsChecked="{Binding ProcessorConfig.EnableThresholding}" />
<Grid <Grid
Margin="4" Margin="4"
@@ -209,7 +209,7 @@
<CheckBox <CheckBox
Content="Resize" Content="Resize"
IsChecked="{Binding ImageProcessorConfiguration.EnableResizing}" /> IsChecked="{Binding ProcessorConfig.EnableResizing}" />
<Grid Margin="4"> <Grid Margin="4">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
@@ -235,7 +235,7 @@
<CheckBox <CheckBox
Content="Filter connected components" Content="Filter connected components"
IsChecked="{Binding ImageProcessorConfiguration.FilterConnectedComponents}" /> IsChecked="{Binding ProcessorConfig.FilterConnectedComponents}" />
</UniformGrid> </UniformGrid>
</Grid> </Grid>
</Window> </Window>
+3 -3
View File
@@ -29,20 +29,20 @@ namespace GUI.Views
private void SldThreshold1_OnDragCompleted(object sender, DragCompletedEventArgs args) private void SldThreshold1_OnDragCompleted(object sender, DragCompletedEventArgs args)
{ {
var vm = ViewModel; var vm = ViewModel;
vm.ImageProcessorConfiguration.ThresholdWidth = (int)Math.Round(((Slider)sender).Value); vm.ProcessorConfig.ThresholdWidth = (int)Math.Round(((Slider)sender).Value);
} }
private void SldThreshold2_OnDragCompleted(object sender, DragCompletedEventArgs args) private void SldThreshold2_OnDragCompleted(object sender, DragCompletedEventArgs args)
{ {
var vm = ViewModel; var vm = ViewModel;
vm.ImageProcessorConfiguration.ThresholdHeight = (int)Math.Round(((Slider)sender).Value); 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;
vm.ImageProcessorConfiguration.Border = (int)Math.Round(((Slider)sender).Value); vm.ProcessorConfig.Border = (int)Math.Round(((Slider)sender).Value);
} }
} }
} }
+185
View File
@@ -0,0 +1,185 @@
namespace ReportGenerator;
internal struct CharacterErrorInfo
{
public string TaggedWord { get; }
public string? ScannedWord { get; set; } = null;
public double CharacterError { get; set; } = double.PositiveInfinity;
public CharacterErrorInfo(string taggedWord) => TaggedWord = taggedWord;
}
internal struct WordErrorInfo
{
public double WordError { get; set; } = double.PositiveInfinity;
public ICollection<CharacterErrorInfo> Words { get; } = new List<CharacterErrorInfo>();
public double CharacterErrorAvg => Words.Average(i => i.CharacterError);
public WordErrorInfo()
{
}
}
internal struct ScanTable
{
public string ImageName { get; set; }
public ICollection<ScanTableRow> Scans { get; set; }
}
internal struct ScanTableRow
{
public string ScannerName { get; set; }
public ICollection<string> ScannedWords { get; set; }
}
internal static class Program
{
internal static void Main(string[] args)
{
var errorInfos = new List<(ScannedResultInfo scan, WordErrorInfo error)>();
var tagFileInfos = GetTagFileInfos(args[0]);
var scanFileInfos = GetScanFileInfos(args[1]).ToLookup(i => i.ImageName);
Directory.CreateDirectory("reports");
foreach (var tagFileInfo in tagFileInfos)
{
var taggedWords = tagFileInfo.GetWords();
foreach (var scanFileInfo in scanFileInfos[tagFileInfo.ImageName])
{
var scannedWords = scanFileInfo.GetWords();
if (!scannedWords.Any())
{
continue;
}
// Calculate WER by comparing all tagged with all scanned words
var wordErrorInfo = new WordErrorInfo
{
WordError = CalculateWer(taggedWords, scannedWords),
};
// Calculate CER for each tagged word
foreach (var taggedWord in taggedWords)
{
var characterErrorInfo = new CharacterErrorInfo(taggedWord);
foreach (var scannedWord in scannedWords)
{
// Calculates the levenshtein distance to every word and returns the most similar combination
var err = CalculateCer(taggedWord, scannedWord);
if (err < characterErrorInfo.CharacterError)
{
characterErrorInfo.ScannedWord = scannedWord;
characterErrorInfo.CharacterError = err;
if (err == 0)
{
break;
}
}
}
wordErrorInfo.Words.Add(characterErrorInfo);
}
errorInfos.Add((scanFileInfo, wordErrorInfo));
}
}
// Somewhat off based on the amount of expected words
// If a processor did scan nothing at all this value can be very low
var bestCharErrorProcessor = errorInfos
.GroupBy(e => e.scan.ProcessorName, e => e.error)
.Select(g => (g.Key, g.Average(i => i.CharacterErrorAvg)))
.OrderBy(g => g.Item2)
.ToArray();
// Same here but with less impact
var bestWordErrorProcessor = errorInfos
.GroupBy(e => e.scan.ProcessorName, e => e.error)
.Select(g => (g.Key, g.Average(i => i.WordError)))
.OrderBy(g => g.Item2)
.ToArray();
}
static double CalculateCer(string s1, string s2)
{
if (string.IsNullOrEmpty(s1) || string.IsNullOrEmpty(s2))
{
return 0;
}
var distance = new int[s1.Length + 1, s2.Length + 1];
for (var i = 0; i <= s1.Length; i++)
{
distance[i, 0] = i;
}
for (var j = 0; j <= s2.Length; j++)
{
distance[0, j] = j;
}
for (var i = 1; i <= s1.Length; i++)
{
for (var j = 1; j <= s2.Length; j++)
{
var cost = s2[j - 1] == s1[i - 1] ? 0 : 1;
var c1 = Math.Min(distance[i - 1, j] + 1, distance[i, j - 1] + 1);
var c2 = distance[i - 1, j - 1] + cost;
distance[i, j] = Math.Min(c1, c2);
}
}
return distance[s1.Length, s2.Length];
}
static double CalculateWer(ICollection<string> expected, ICollection<string> actual)
{
// Amount of words that need to be substituted to match the original
int substitutions = expected
.Zip(
actual,
(e, a) => string.Equals(e, a) ? 0 : 1
)
.Sum();
// Amount of words dropped from the original
int deletions = expected.Except(actual).Count();
// Amount of extra words added compared to the original
int insertions = actual.Except(expected).Count();
return (substitutions + deletions + insertions) / (double)expected.Count;
}
private static IEnumerable<TagFileInfo> GetTagFileInfos(string dir)
{
if (!Directory.Exists(dir))
{
throw new ArgumentException($"Invalid tagged data directory '{dir}'");
}
return Directory.EnumerateFiles(dir, "*.json").Select(TagFileInfo.FromPath);
}
private static IEnumerable<ScannedResultInfo> GetScanFileInfos(string dir)
{
if (!Directory.Exists(dir))
{
throw new ArgumentException($"Invalid scan results directory '{dir}'");
}
return Directory.EnumerateFiles(dir, "*.json").Select(ScannedResultInfo.FromPath);
}
}
@@ -0,0 +1,9 @@
{
"profiles": {
"ReportGenerator": {
"commandName": "Project",
"commandLineArgs": "\"img\" \"results\"",
"workingDirectory": "D:\\git\\BA\\Examples\\CLI\\bin\\Debug\\net6.0"
}
}
}
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
@@ -0,0 +1,45 @@
using System.Text.Json;
using System.Text.RegularExpressions;
namespace ReportGenerator;
internal struct ScannedResultInfo
{
public string Path { get; private init; }
private static readonly Regex parseRegex = new(
@"(?'image'.+)\.(?'processor'.+)\..+",
RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase
);
public string ProcessorName { get; set; }
public string ImageName { get; set; }
public ICollection<string> GetWords()
{
using var file = File.OpenRead(Path);
return JsonDocument
.Parse(file)
.RootElement
.EnumerateArray()
.Select(e =>
e.GetProperty("Text").GetString() ?? throw new Exception("Cannot parse null words"))
.ToArray();
}
public static ScannedResultInfo FromPath(string path)
{
var match = parseRegex.Match(System.IO.Path.GetFileName(path));
return new ScannedResultInfo
{
Path = path,
ProcessorName = match.Groups["processor"].Value,
ImageName = match.Groups["image"].Value
};
}
/// <inheritdoc />
public override string ToString() => ImageName;
}
+31
View File
@@ -0,0 +1,31 @@
using System.Text.Json;
namespace ReportGenerator;
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,4 +17,11 @@ public class AutoThresholdProcessor : SingleImageProcessor
image.AutoThreshold(_method); image.AutoThreshold(_method);
return image; return image;
} }
#region Overrides of Object
/// <inheritdoc />
public override string ToString() => $"{nameof(AutoThresholdProcessor)}({_method})";
#endregion
} }
@@ -5,16 +5,27 @@ namespace Ocr.Tesseract.Screenshots.Threshold;
public class ThresholdAdaptiveProcessor : SingleImageProcessor public class ThresholdAdaptiveProcessor : SingleImageProcessor
{ {
private readonly int _width; private readonly int _width;
private readonly int _height; private readonly int _height;
public ThresholdAdaptiveProcessor(int width, int height) public ThresholdAdaptiveProcessor(int size) : this(size, size)
{ {
_width = width; }
_height = height;
}
/// <inheritdoc /> public ThresholdAdaptiveProcessor(int width, int height)
protected override MagickImage Process(MagickImage image) => image {
.ThresholdAdaptive(_width, _height); _width = width;
_height = height;
}
/// <inheritdoc />
protected override MagickImage Process(MagickImage image) => image
.ThresholdAdaptive(_width, _height);
#region Overrides of Object
/// <inheritdoc />
public override string ToString() => $"{nameof(ThresholdAdaptiveProcessor)}({_width:D2}_{_height:D2})";
#endregion
} }
@@ -4,18 +4,25 @@ namespace Ocr.Tesseract.Screenshots.Threshold;
public class ThresholdProcessor : SingleImageProcessor public class ThresholdProcessor : SingleImageProcessor
{ {
private readonly Percentage _percentage; private readonly Percentage _percentage;
public ThresholdProcessor(int percentage) public ThresholdProcessor(int percentage)
{ {
_percentage = new Percentage(percentage); _percentage = new Percentage(percentage);
} }
/// <inheritdoc /> /// <inheritdoc />
protected override MagickImage Process(MagickImage image) protected override MagickImage Process(MagickImage image)
{ {
image.Threshold(_percentage); image.Threshold(_percentage);
return image; return image;
} }
#region Overrides of Object
/// <inheritdoc />
public override string ToString() => $"{nameof(ThresholdProcessor)}({_percentage})";
#endregion
} }
+2
View File
@@ -1,4 +1,5 @@
using ImageMagick; using ImageMagick;
using System.Text.Json.Serialization;
namespace Ocr.Tesseract.Models; namespace Ocr.Tesseract.Models;
@@ -26,6 +27,7 @@ public struct ScanResult
/// <summary> /// <summary>
/// Value referenced by <see cref="Word"/> /// Value referenced by <see cref="Word"/>
/// </summary> /// </summary>
[JsonIgnore]
public MagickImage Image { get; set; } public MagickImage Image { get; set; }