feature/reports #1

Merged
sim merged 15 commits from feature/reports into main 2024-01-07 21:37:05 +01:00
1278 changed files with 4168 additions and 656 deletions
+24 -1
View File
@@ -8,32 +8,55 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Ocr\Ocr.Processors\Ocr.Processors.csproj" />
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="img\command-processing_screentypes_controlgroup_005.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\command-processing_screentypes_controlgroup_005.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\editor_startpage_project-exist_001.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\editor_startpage_project-exist_001.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\editor_windows_position_006.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\editor_windows_position_006.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\historian_assistent_001.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\historian_assistent_001.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\zrs_MetadataEditor_variables_001.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\zrs_MetadataEditor_variables_001.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\zrs_REPORTS_EfficencyClass_009.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\zrs_REPORTS_EfficencyClass_009.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\zrs_ZAMS_3rd-connector_014.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\zrs_ZAMS_3rd-connector_014.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\zrs_ZAMS_filter-alarmgroup_001.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\zrs_ZAMS_filter-alarmgroup_001.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
+46
View File
@@ -0,0 +1,46 @@
using System.Text;
namespace CLI.Monitor;
public class CliTaskMonitor : TaskMonitor
{
/// <inheritdoc />
public CliTaskMonitor(IEnumerable<(string? Name, Task Task)> tasks) : base(tasks) { }
/// <inheritdoc />
public CliTaskMonitor(IEnumerable<Task> tasks) : base(tasks) { }
#region Overrides of TaskMonitor
/// <inheritdoc />
protected override void OnUpdate(ICollection<(string? Name, Task Task)> tasks)
{
var sb = new StringBuilder(tasks.Count * 30);
foreach (var (name, task) in tasks)
{
sb.Append($"{StatusMap[task.Status],-5}");
if (name is not null)
{
sb.Append(": ")
.Append(name);
}
sb.AppendLine();
if (task.Exception is not null)
{
sb.Append("> EX: ")
.AppendLine(task.Exception.Message);
}
}
sb.AppendLine();
Console.Clear();
Console.Write(sb.ToString());
}
#endregion
}
@@ -0,0 +1,42 @@
using System.Text;
namespace CLI.Monitor;
public class CompactCliTaskMonitor : TaskMonitor
{
/// <inheritdoc />
public CompactCliTaskMonitor(IEnumerable<(string? Name, Task Task)> tasks) : base(tasks) { }
/// <inheritdoc />
public CompactCliTaskMonitor(IEnumerable<Task> tasks) : base(tasks) { }
#region Overrides of TaskMonitor
/// <inheritdoc />
protected override void OnUpdate(ICollection<(string? Name, Task Task)> tasks)
{
int completed = 0;
int total = tasks.Count;
var sb = new StringBuilder(total * 30);
foreach (var (_, task) in tasks)
{
var status = task.Status;
if (status > TaskStatus.WaitingForChildrenToComplete)
{
completed++;
}
sb.Append(task.Exception is not null ? 'X' : StatusMap[status].First());
}
sb.AppendLine($" ({completed}/{total})");
Console.Clear();
Console.Write(sb.ToString());
}
#endregion
}
+8
View File
@@ -0,0 +1,8 @@
namespace CLI.Monitor;
public interface ITaskMonitor
{
TimeSpan Interval { get; init; }
Task Run();
}
+42
View File
@@ -0,0 +1,42 @@
namespace CLI.Monitor;
public abstract class TaskMonitor : ITaskMonitor
{
/// <inheritdoc />
public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(1);
private readonly ICollection<(string? Name, Task Task)> _tasks;
protected static IReadOnlyDictionary<TaskStatus, string> StatusMap { get; } =
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" },
};
protected TaskMonitor(IEnumerable<(string? Name, Task Task)> tasks) => _tasks = tasks.ToArray();
protected TaskMonitor(IEnumerable<Task> tasks) :
this(tasks.Select(t => (t.Id.ToString(), t))!)
{ }
public Task Run()
{
var waitTask = Task.WhenAll(_tasks.Select(i => i.Task));
while (!waitTask.Wait(Interval))
{
OnUpdate(_tasks);
}
return waitTask;
}
protected abstract void OnUpdate(ICollection<(string? Name, Task Task)> tasks);
}
@@ -0,0 +1,110 @@
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;
namespace CLI.Processor;
internal class EvaluationProcessor
{
#region Configuration
/// <summary>
/// <see cref="Regex"/> expression for extracting whole words from scan results
/// </summary>
private static readonly Regex wordRegex = new(
@"[\w'\-äöüÄÖÜß]{2,}",
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase
);
private static readonly ITesseractConfiguration tesseractConfig =
new TesseractScreenshotConfiguration
{
DataPath = "tessdata",
Languages = new[] { "eng", "deu" }
};
#endregion
#region Processors
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 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 readonly StopwatchProcessor<MagickImage, MagickImage> _thresholdProcessor;
#endregion
public string OutputFolder { get; init; } = "results";
public EvaluationProcessor(
IProcessor<MagickImage, MagickImage> thresholdProcessor
) => _thresholdProcessor = new StopwatchProcessor<MagickImage, MagickImage>(thresholdProcessor);
/// <inheritdoc />
public Task Process(MagickImage image) => Task.Run(async () =>
{
var words = MakeProcessor()
.Process(new[] { image })
.Select(r => r.Word)
.ToArray();
var result = new
{
Words = words.ToArray(),
Elapsed = _thresholdProcessor.Elapsed?.TotalMilliseconds,
};
var name = Path.GetFileNameWithoutExtension(image.FileName);
var path = Path.Combine(OutputFolder, $"{name}.{_thresholdProcessor}.json");
await using var file = File.OpenWrite(path);
await JsonSerializer.SerializeAsync(file, result);
});
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);
var path = Path.Combine(OutputFolder, $"{_thresholdProcessor}.{i:D2}.{name}");
image.Write(path);
}
return tImages;
}
}
@@ -0,0 +1,29 @@
using Process.Abstract;
using Process.Interface;
using System.Diagnostics;
namespace CLI.Processor;
public class StopwatchProcessor<TInput, TOutput> : Processor<TInput, TOutput>
{
private readonly IProcessor<TInput, TOutput> _processor;
/// <summary>
/// Execution time of the last processing action
/// </summary>
public TimeSpan? Elapsed { get; private set; }
public StopwatchProcessor(IProcessor<TInput, TOutput> processor) => _processor = processor;
public override IEnumerable<TOutput> Process(IEnumerable<TInput> inputs)
{
var stopWatch = Stopwatch.StartNew();
var results = _processor.Process(inputs);
stopWatch.Stop();
Elapsed = stopWatch.Elapsed;
return results;
}
/// <inheritdoc />
public override string? ToString() => _processor.ToString();
}
+65 -10
View File
@@ -1,16 +1,71 @@
using Common;
using CLI.Monitor;
using CLI.Processor;
using Common.Extensions;
using ImageMagick;
using Ocr.Tesseract.Screenshots.Threshold;
using System.Diagnostics.CodeAnalysis;
var scanner = new ScreenshotScanner();
namespace CLI;
Console.WriteLine($"# Scanning: {string.Join(',', args)}...");
scanner.Process(GetImages(args));
public class Program
{
public Task Run(string[] args)
{
Directory.Delete("results", true);
Directory.CreateDirectory("results");
Console.WriteLine($"# Results ({scanner.Lookup.Keys.Count}):");
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
.SelectMany(p => p.ExpandPath())
.Select(p => new MagickImage(p))
.ToArray();
// return new CliTaskMonitor(scans) { Interval = TimeSpan.FromMilliseconds(500) }.Run();
return new CompactCliTaskMonitor(scans) { Interval = TimeSpan.FromMilliseconds(500) }.Run();
}
[SuppressMessage("ReSharper", "ArrangeObjectCreationWhenTypeNotEvident")]
private static IEnumerable<EvaluationProcessor> MakeThresholdVariations()
{
for (int i = 4; i <= 24; i += 4)
{
yield return new(new ThresholdAdaptiveProcessor(i));
}
for (int i = 20; i <= 80; i += 10)
{
yield return new(new ThresholdProcessor(i));
}
yield return new(new AutoThresholdProcessor(AutoThresholdMethod.Kapur));
yield return new(new AutoThresholdProcessor(AutoThresholdMethod.OTSU));
yield return new(new AutoThresholdProcessor(AutoThresholdMethod.Triangle));
}
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
}
+2 -1
View File
@@ -6,7 +6,8 @@
},
"Test all img": {
"commandName": "Project",
"commandLineArgs": "\"img/*\""
"commandLineArgs": "\"img/*.png\"",
"workingDirectory": "D:\\git\\BA\\Examples\\testdata"
},
"Test single img": {
"commandName": "Project",
-13
View File
@@ -12,22 +12,9 @@
<ItemGroup>
<ProjectReference Include="..\..\Lookup\Lookup.Memory\Lookup.Memory.csproj" />
<ProjectReference Include="..\..\Ocr\Ocr.Processors\Ocr.Processors.csproj" />
<ProjectReference Include="..\..\Ocr\Ocr.Tesseract.Screenshots\Ocr.Tesseract.Screenshots.csproj" />
<ProjectReference Include="..\..\Ocr\Ocr.Tesseract\Ocr.Tesseract.csproj" />
<ProjectReference Include="..\..\Process\Process.Interface\Process.Interface.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="tessdata\deu.traineddata">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="tessdata\eng.traineddata">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="tessdata\osd.traineddata">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
+66
View File
@@ -0,0 +1,66 @@
using System.Collections;
namespace Common.Distance;
public static class Calculator
{
/// <summary>
/// Calculates the levenshtein distance between two enumerables
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="reference"></param>
/// <param name="hypothesis"></param>
/// <returns></returns>
public static double GetDistance<T>(T reference, T? hypothesis)
where T : IEnumerable
{
// Setup
var refArr = reference.Cast<object>().ToArray();
var hypArr = hypothesis?.Cast<object>().ToArray() ?? Array.Empty<object>();
var distance = new int[refArr.Length + 1, hypArr.Length + 1];
// Fill matrix
for (var x = 0; x <= refArr.Length; x++)
{
// Reference on X axis
distance[x, 0] = x;
}
for (var y = 0; y <= hypArr.Length; y++)
{
// Hypothesis on Y axis
distance[0, y] = y;
}
// Calculate distance
for (var x = 0; x < refArr.Length; x++)
{
for (var y = 0; y < hypArr.Length; y++)
{
// BL Cost depends on whether the two elements are equal
var cost = Equals(refArr[x], hypArr[y]) ? 0 : 1;
// Apply distance mask
var c1 = distance[x, y] + cost; // Bottom left
var c2 = distance[x, y + 1] + 1; // Top left
var c3 = distance[x + 1, y] + 1; // Bottom right
distance[x + 1, y + 1] = Min(c1, c2, c3); // Top right
}
}
return distance[refArr.Length, hypArr.Length];
}
private static T Min<T>(params T[] values)
{
if (!values.Any())
{
throw new ArgumentException("Array cannot be empty", nameof(values));
}
return values.Min()!;
}
}
@@ -0,0 +1,43 @@
using System.Collections;
namespace Common.Distance;
public readonly struct DistanceComparer<T> : IDistanceComparer<T>
where T : IEnumerable
{
/// <inheritdoc />
public T Reference { get; }
/// <inheritdoc />
public T? Hypothesis { get; }
/// <inheritdoc />
public double Distance { get; }
public DistanceComparer(T reference) : this(reference, default)
{
}
public DistanceComparer(T reference, T? hypothesis)
{
Reference = reference;
Hypothesis = hypothesis;
Distance = Calculator.GetDistance(Reference, Hypothesis);
}
/// <inheritdoc />
public override string ToString()
{
var str = Hypothesis?.ToString();
if (Hypothesis is var hyp && Equals(hyp, Reference))
{
return str ?? string.Empty;
}
return
$"<strong style='color: orange;' title='REf: {Reference}, CER: {Distance}'>{str ?? "-"}</strong>";
}
}
@@ -0,0 +1,27 @@
using System.Collections;
namespace Common.Distance;
public interface IDistanceComparer
{
/// <summary>
/// The calculated absolute distance between
/// <see cref="IDistanceComparer{T}.Reference"/> and
/// <see cref="IDistanceComparer{T}.Hypothesis"/>
/// </summary>
public double Distance { get; }
}
public interface IDistanceComparer<out T> : IDistanceComparer
where T : IEnumerable
{
/// <summary>
/// The comparison reference, meaning the "known to be correct" value
/// </summary>
public T Reference { get; }
/// <summary>
/// The value hypothesis, whose correctness is checked against <see cref="Reference"/>
/// </summary>
public T? Hypothesis { get; }
}
@@ -0,0 +1,47 @@
namespace Common.Extensions;
public static class EnumerableExtensions
{
public static double Median(this IEnumerable<double> values)
{
var tValues = values.OrderBy(v => v).ToArray();
if (!tValues.Any())
{
throw new ArgumentException(
"The list must contain at least one value for calculating the median");
}
if (tValues.Length % 2 != 0)
{
return tValues[tValues.Length / 2];
}
var center = tValues.Length / 2;
return (tValues[center - 1] + tValues[center]) / 2.0;
}
public static double Median(this IEnumerable<double> values, out double deviation)
{
var tValues = values.ToArray();
var median = tValues.Median();
deviation = tValues.Select(value => Math.Abs(value - median)).Median();
return median;
}
public static double Average(this IEnumerable<double> values, out double deviation)
{
var tValues = values?.ToArray();
if (tValues is null || tValues.Length < 2)
{
throw new ArgumentException(
"The list must contain at least two values for calculating standard deviation");
}
var average = tValues.Average();
var diffSquaredSum = tValues.Sum(value => Math.Pow(value - average, 2));
deviation = Math.Sqrt(diffSquaredSum / (tValues.Length - 1));
return average;
}
}
@@ -0,0 +1,15 @@
using System.Text;
namespace Common.Extensions;
public static class StringBuilderExtensions
{
public static StringBuilder AppendHeading(this StringBuilder sb, int level, string text) => sb
.Append(new string('#', level))
.Append(' ')
.AppendParagraph(text);
public static StringBuilder AppendParagraph(this StringBuilder sb, string text) => sb
.AppendLine(text)
.AppendLine();
}
+12 -14
View File
@@ -1,14 +1,10 @@
using System.Text.RegularExpressions;
namespace Common.Extensions;
namespace Common.Extensions;
/// <summary>
/// Extensions for the string object type
/// </summary>
public static class StringExtensions
{
private static readonly Regex patternRegex = new Regex(@"^\*$");
/// <summary>
/// Determines whether this string contains the specified string. Not case sensitive.
/// </summary>
@@ -24,18 +20,20 @@ public static class StringExtensions
/// </summary>
/// <param name="self"></param>
/// <returns></returns>
public static ICollection<string> ExpandPath(this string self)
public static IEnumerable<string> ExpandPath(this string self)
{
string pattern = Path.GetFileName(self);
if (patternRegex.IsMatch(pattern))
var parts = self.Split(Path.DirectorySeparatorChar);
var fileName = parts.Last();
if (fileName.Contains('*') || fileName.Contains('?'))
{
return Directory.GetFiles(
self.Substring(0, self.Length - pattern.Length),
pattern,
SearchOption.TopDirectoryOnly
);
// Path contains file pattern
var path = Path.Combine(parts.SkipLast(1).ToArray());
return Directory.EnumerateFiles(path, fileName);
}
// Path contains no pattern
return new[] { self };
}
}
}
+37 -78
View File
@@ -1,91 +1,50 @@
using ImageMagick;
using Lookup.Memory;
using Ocr.Tesseract;
using Ocr.Tesseract.Configuration;
using Ocr.Tesseract.Models;
using Ocr.Tesseract.Screenshots;
using Ocr.Tesseract.Screenshots.Configuration;
using Process.Abstract.Configuration;
using Process.Interface;
using System.Text.RegularExpressions;
namespace Common
namespace Common;
/// <summary>
/// Scanner class, scanning <see cref="MagickImage"/>s for <see cref="Word"/>s
/// via optical character recognition. Optimized for digital Screenshots.
/// </summary>
public class ScreenshotScanner
{
/// <summary>
/// Scanner class, scanning <see cref="MagickImage"/>s for <see cref="Word"/>s
/// via optical character recognition. Optimized for digital Screenshots.
/// The screenshot processor
/// </summary>
public class ScreenshotScanner
protected IProcessor<MagickImage, ScanResult> Processor { get; }
/// <summary>
/// Data storage
/// </summary>
public Lookup.Interface.ILookup<Word, MagickImage> Lookup { get; } =
new MemoryLookup<Word, MagickImage>();
/// <summary>
/// Constructor
/// </summary>
public ScreenshotScanner(IProcessor<MagickImage, ScanResult> processor)
{
private readonly IProcessor<MagickImage, ScanResult> _processor;
Processor = processor;
}
/// <summary>
/// <see cref="Regex"/> expression for extracting whole words from scan results
/// </summary>
private static readonly Regex wordRegex = new(
@"[\w'\-]{2,}",
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase
);
/// <summary>
/// Data storage
/// </summary>
public Lookup.Interface.ILookup<Word, MagickImage> Lookup { get; } =
new MemoryLookup<Word, MagickImage>();
/// <summary>
/// Configuration of the <see cref="ImageProcessor"/>
/// </summary>
public ScreenshotProcessorConfiguration ImageProcessorConfiguration { get; }
public ITesseractConfiguration TesseractConfiguration { get; }
/// <summary>
/// Constructor
/// </summary>
public ScreenshotScanner(
ScreenshotProcessorConfiguration imageProcessorConfig,
ITesseractConfiguration tesseractConfig
)
{
ImageProcessorConfiguration = imageProcessorConfig;
TesseractConfiguration = tesseractConfig;
_processor = MakeProcessor();
}
/// <summary>
/// Process the provided <paramref name="images"/> and add the results to
/// the <see cref="Lookup"/>
/// </summary>
/// <param name="images">The <see cref="MagickImage"/>s to process</param>
public void Process(IEnumerable<MagickImage> images)
{
foreach (var kv in _processor.Process(images))
{
Lookup.Add(kv.Word, kv.Image);
}
}
private IProcessor<MagickImage, ScanResult> MakeProcessor()
{
return new ProcessorChainConfiguration<MagickImage, ScanResult>()
.Use(new ScreenshotProcessor(ImageProcessorConfiguration)) // Preprocess input data
.Use(new ProcessingEvent<MagickImage>(OnProcessing)) // Scan
.Use(new TesseractProcessor(TesseractConfiguration)) // Scan
.Use(new ProcessingEvent<ScanResult>(OnProcessed)) // Scan
.Use(new ConfidenceFilter(50)) // Process output data
.Use(new DuplicateFilter())
.Use(new ToLowerProcessor())
.Complete(new RegexFilter(wordRegex));
}
protected virtual void OnProcessing(IProcessor sender, ICollection<MagickImage> inputs)
{
}
protected virtual void OnProcessed(IProcessor sender, ICollection<ScanResult> inputs)
/// <summary>
/// Process the provided <paramref name="images"/> and add the results to
/// the <see cref="Lookup"/>
/// </summary>
/// <param name="images">The <see cref="MagickImage"/>s to process</param>
public void Process(IEnumerable<MagickImage> images)
{
foreach (var kv in Processor.Process(images))
{
Lookup.Add(kv.Word, kv.Image);
}
}
}
public virtual void Clear()
{
Lookup.Clear();
}
}
+6
View File
@@ -35,6 +35,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocr.Tesseract", "..\Ocr\Ocr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocr.Tesseract.Screenshots", "..\Ocr\Ocr.Tesseract.Screenshots\Ocr.Tesseract.Screenshots.csproj", "{251F9AC9-3765-498C-83FD-DB3539A19CB3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReportGenerator", "ReportGenerator\ReportGenerator.csproj", "{729CB7AA-AB0D-4C39-AA17-7435E61FA0A6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
+2
View File
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=tesseract/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
+16 -17
View File
@@ -3,25 +3,24 @@ using GUI.Views;
using Serilog;
using System.Windows;
namespace GUI
namespace GUI;
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
/// <inheritdoc />
protected override void OnStartup(StartupEventArgs e)
{
/// <inheritdoc />
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
base.OnStartup(e);
var loggingCollection = new LoggingCollection(100);
Log.Logger = new LoggerConfiguration()
.WriteTo.Sink(loggingCollection)
.CreateLogger();
var loggingCollection = new LoggingCollection(100);
Log.Logger = new LoggerConfiguration()
.WriteTo.Sink(loggingCollection)
.CreateLogger();
new LogView(loggingCollection).Show();
new ImageView().Show();
}
new LogView(loggingCollection).Show();
new ImageView().Show();
}
}
}
+35 -36
View File
@@ -2,44 +2,43 @@
using System.Windows;
using System.Windows.Controls;
namespace GUI.Controls
namespace GUI.Controls;
/// <summary>
/// Interaction logic for ImageControl.xaml
/// </summary>
public partial class ImageControl : UserControl
{
/// <inheritdoc cref="Image"/>
public static readonly DependencyProperty ImageProperty = DependencyProperty.Register(
nameof(Image), typeof(MagickImage), typeof(ImageControl), new PropertyMetadata(default(MagickImage)));
/// <summary>
/// Interaction logic for ImageControl.xaml
/// The <see cref="MagickImage"/> displayed in this <see cref="ImageControl"/>
/// </summary>
public partial class ImageControl : UserControl
public MagickImage Image
{
/// <inheritdoc cref="Image"/>
public static readonly DependencyProperty ImageProperty = DependencyProperty.Register(
nameof(Image), typeof(MagickImage), typeof(ImageControl), new PropertyMetadata(default(MagickImage)));
/// <summary>
/// The <see cref="MagickImage"/> displayed in this <see cref="ImageControl"/>
/// </summary>
public MagickImage Image
{
get => (MagickImage)GetValue(ImageProperty);
set => SetValue(ImageProperty, value);
}
/// <inheritdoc cref="ImageName"/>
public static readonly DependencyProperty ImageNameProperty = DependencyProperty.Register(
nameof(ImageName), typeof(object), typeof(ImageControl),
new PropertyMetadata(default(object)));
/// <summary>
/// The name of the loaded image
/// </summary>
public object ImageName
{
get => (object)GetValue(ImageNameProperty);
set => SetValue(ImageNameProperty, value);
}
/// <inheritdoc />
public ImageControl()
{
InitializeComponent();
}
get => (MagickImage)GetValue(ImageProperty);
set => SetValue(ImageProperty, value);
}
}
/// <inheritdoc cref="ImageName"/>
public static readonly DependencyProperty ImageNameProperty = DependencyProperty.Register(
nameof(ImageName), typeof(object), typeof(ImageControl),
new PropertyMetadata(default(object)));
/// <summary>
/// The name of the loaded image
/// </summary>
public object ImageName
{
get => (object)GetValue(ImageNameProperty);
set => SetValue(ImageNameProperty, value);
}
/// <inheritdoc />
public ImageControl()
{
InitializeComponent();
}
}
+37 -38
View File
@@ -6,49 +6,48 @@ using System.IO;
using System.Windows.Data;
using System.Windows.Media.Imaging;
namespace GUI.Converters
namespace GUI.Converters;
internal class ImageConverter : IValueConverter
{
internal class ImageConverter : IValueConverter
#region Implementation of IValueConverter
/// <inheritdoc />
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
#region Implementation of IValueConverter
/// <inheritdoc />
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
if (value is not MagickImage image)
{
if (value is not MagickImage image)
{
return Binding.DoNothing;
}
try
{
using var stream = new MemoryStream();
// Save image to stream
image.Write(stream, MagickFormat.Png);
// Build Bitmap from stream
var imageSource = new BitmapImage();
imageSource.BeginInit();
imageSource.StreamSource = stream;
imageSource.CacheOption = BitmapCacheOption.OnLoad;
imageSource.EndInit();
return imageSource;
}
catch (Exception e)
{
Log.Error($"{e.Message}");
return Binding.DoNothing;
}
return Binding.DoNothing;
}
/// <inheritdoc />
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
try
{
throw new NotImplementedException();
}
using var stream = new MemoryStream();
#endregion
// Save image to stream
image.Write(stream, MagickFormat.Png);
// Build Bitmap from stream
var imageSource = new BitmapImage();
imageSource.BeginInit();
imageSource.StreamSource = stream;
imageSource.CacheOption = BitmapCacheOption.OnLoad;
imageSource.EndInit();
return imageSource;
}
catch (Exception e)
{
Log.Error($"{e.Message}");
return Binding.DoNothing;
}
}
}
/// <inheritdoc />
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
-27
View File
@@ -15,31 +15,4 @@
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="img\command-processing_screentypes_controlgroup_005.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\editor_startpage_project-exist_001.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\editor_windows_position_006.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\historian_assistent_001.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\zrs_MetadataEditor_variables_001.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\zrs_REPORTS_EfficencyClass_009.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\zrs_ZAMS_3rd-connector_014.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="img\zrs_ZAMS_filter-alarmgroup_001.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
+86 -20
View File
@@ -2,9 +2,13 @@
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;
@@ -14,33 +18,91 @@ 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 : ScreenshotScanner, INotifyPropertyChanged
internal class ImageViewModel : INotifyPropertyChanged
{
private static ITesseractConfiguration CreateTesseractConfiguration() =>
new TesseractScreenshotConfiguration()
/// <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 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);
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 override void OnProcessing(IProcessor sender, ICollection<MagickImage> inputs)
protected void OnProcessing(IProcessor sender, ICollection<MagickImage> inputs)
{
Application.Current.Dispatcher.Invoke(() =>
{
@@ -52,30 +114,28 @@ internal class ImageViewModel : ScreenshotScanner, INotifyPropertyChanged
}
/// <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);
}
#endregion
public ImageViewModel(MagickImage image) : this()
{
Image = image;
}
private void Clear()
/// <inheritdoc />
public void Clear()
{
Scanner.Clear();
Application.Current.Dispatcher.Invoke(() =>
{
ScannedText = string.Empty;
Words.Clear();
Lookup.Clear();
Edited.Clear();
}
);
}
#endregion
#region File Handling
private void OpenFile()
{
var dialog = new OpenFileDialog()
@@ -105,10 +165,14 @@ internal class ImageViewModel : ScreenshotScanner, INotifyPropertyChanged
);
}
#endregion
#region Updating data
private void UpdateConfidence()
{
Confidence = Lookup.Keys.Any()
? Lookup.Keys.Sum(key => key.Confidence) / Lookup.Keys.Count
Confidence = Scanner.Lookup.Keys.Any()
? Scanner.Lookup.Keys.Sum(key => key.Confidence) / Scanner.Lookup.Keys.Count
: 0;
}
@@ -121,7 +185,7 @@ internal class ImageViewModel : ScreenshotScanner, INotifyPropertyChanged
Clear();
if (Image != null)
{
Process(new[] { Image });
Scanner.Process(new[] { Image });
}
UpdateWords();
@@ -135,13 +199,15 @@ internal class ImageViewModel : ScreenshotScanner, INotifyPropertyChanged
{
Application.Current.Dispatcher.Invoke(() =>
{
foreach (var word in Lookup.Keys)
foreach (var word in Scanner.Lookup.Keys)
{
Words.Add(word);
}
});
}
#endregion
#region Properties
private float _confidence;
+9 -10
View File
@@ -1,12 +1,11 @@
namespace GUI.ViewModels
{
public class LogViewModel
{
public LoggingCollection LoggingCollection { get; }
namespace GUI.ViewModels;
public LogViewModel(LoggingCollection loggingCollection)
{
LoggingCollection = loggingCollection;
}
public class LogViewModel
{
public LoggingCollection LoggingCollection { get; }
public LogViewModel(LoggingCollection loggingCollection)
{
LoggingCollection = loggingCollection;
}
}
}
+3 -3
View File
@@ -160,7 +160,7 @@
<CheckBox
x:Name="EnableThreshold"
Content="Apply Threshold"
IsChecked="{Binding ImageProcessorConfiguration.EnableThresholding}" />
IsChecked="{Binding ProcessorConfig.EnableThresholding}" />
<Grid
Margin="4"
@@ -209,7 +209,7 @@
<CheckBox
Content="Resize"
IsChecked="{Binding ImageProcessorConfiguration.EnableResizing}" />
IsChecked="{Binding ProcessorConfig.EnableResizing}" />
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@@ -235,7 +235,7 @@
<CheckBox
Content="Filter connected components"
IsChecked="{Binding ImageProcessorConfiguration.FilterConnectedComponents}" />
IsChecked="{Binding ProcessorConfig.FilterConnectedComponents}" />
</UniformGrid>
</Grid>
</Window>
+37 -38
View File
@@ -5,44 +5,43 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
namespace GUI.Views
namespace GUI.Views;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class ImageView : Window
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class ImageView : Window
private ImageViewModel ViewModel => (ImageViewModel)DataContext;
public ImageView()
{
private ImageViewModel ViewModel => (ImageViewModel)DataContext;
public ImageView()
{
DataContext = new ImageViewModel();
InitializeComponent();
}
public ImageView(MagickImage image)
{
DataContext = new ImageViewModel(image);
InitializeComponent();
}
private void SldThreshold1_OnDragCompleted(object sender, DragCompletedEventArgs args)
{
var vm = ViewModel;
vm.ImageProcessorConfiguration.ThresholdWidth = (int)Math.Round(((Slider)sender).Value);
}
private void SldThreshold2_OnDragCompleted(object sender, DragCompletedEventArgs args)
{
var vm = ViewModel;
vm.ImageProcessorConfiguration.ThresholdHeight = (int)Math.Round(((Slider)sender).Value);
}
private void SldBorder_OnDragCompleted(object sender, DragCompletedEventArgs e)
{
var vm = ViewModel;
vm.ImageProcessorConfiguration.Border = (int)Math.Round(((Slider)sender).Value);
}
DataContext = new ImageViewModel();
InitializeComponent();
}
}
public ImageView(MagickImage image)
{
DataContext = new ImageViewModel(image);
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)
{
var vm = ViewModel;
vm.ProcessorConfig.Border = (int)Math.Round(((Slider)sender).Value);
}
}
+10 -11
View File
@@ -1,17 +1,16 @@
using GUI.ViewModels;
using System.Windows;
namespace GUI.Views
namespace GUI.Views;
/// <summary>
/// Interaction logic for LogView.xaml
/// </summary>
public partial class LogView : Window
{
/// <summary>
/// Interaction logic for LogView.xaml
/// </summary>
public partial class LogView : Window
public LogView(LoggingCollection loggingCollection)
{
public LogView(LoggingCollection loggingCollection)
{
InitializeComponent();
DataContext = new LogViewModel(loggingCollection);
}
InitializeComponent();
DataContext = new LogViewModel(loggingCollection);
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

@@ -0,0 +1,57 @@
using ReportGenerator.Generator.Interface;
using System.Text;
namespace ReportGenerator.Generator.Abstract;
public abstract class DocumentGeneratorBase : StreamWriterBase, IDocumentGenerator
{
/// <inheritdoc />
protected DocumentGeneratorBase() { }
/// <inheritdoc />
protected DocumentGeneratorBase(Stream stream) : base(stream) { }
/// <inheritdoc />
protected DocumentGeneratorBase(string filePath) : base(File.Open(filePath, FileMode.Create,
FileAccess.Write))
{ }
/// <inheritdoc />
protected DocumentGeneratorBase(Stream stream, Encoding encoding) : base(stream, encoding) { }
#region Writing
/// <inheritdoc />
public virtual IDocumentGenerator Append(string? text = default)
{
Write(text);
return this;
}
/// <inheritdoc />
public virtual IDocumentGenerator AppendLine(string? text = default)
{
WriteLine(text);
return this;
}
/// <inheritdoc />
public abstract IDocumentGenerator AppendHeading(int level, string text);
/// <inheritdoc />
public abstract IDocumentGenerator AppendParagraph(string? text = default);
/// <inheritdoc />
public IDocumentGenerator AppendTable(int columns, Action<ITableGenerator> table)
{
Write(() => MakeTable(columns, new MemoryStream()), table);
return this;
}
protected abstract ITableGenerator MakeTable(int columns, Stream stream);
#endregion
/// <inheritdoc />
public abstract string FormatImage(string path, IBounds? bounds = default);
}
@@ -0,0 +1,198 @@
using ReportGenerator.Generator.Interface;
using System.Text;
namespace ReportGenerator.Generator.Abstract;
public abstract class StreamWriterBase : IStreamWriter
{
private bool _isOpen;
private bool _isClosed;
/// <summary>
/// Underlying <see cref="Stream"/>
/// </summary>
private Stream Stream { get; }
/// <summary>
/// Internal <see cref="StreamWriter"/> for generating the output <see cref="string"/>
/// </summary>
protected TextWriter Writer { get; }
/// <summary>
/// Constructor; Configures the
/// <see cref="StreamWriterBase"/> to write to the memory
/// </summary>
protected StreamWriterBase() : this(new MemoryStream()) { }
/// <inheritdoc cref="StreamWriterBase(System.IO.Stream, Encoding)"/>
protected StreamWriterBase(Stream stream) : this(stream, Encoding.UTF8) { }
/// <summary>
/// Constructor; Configures the <see cref="StreamWriterBase"/>
/// to write to the specified <paramref name="stream"/>
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to write to</param>
/// <param name="encoding">Text <see cref="Encoding"/> of the written data</param>
protected StreamWriterBase(Stream stream, Encoding encoding)
{
Stream = stream;
Writer = new StreamWriter(stream, encoding);
}
#region Control
public void Open()
{
if (_isOpen)
{
throw new InvalidOperationException($"{GetType()} has already been opened");
}
if (_isClosed)
{
throw new InvalidOperationException($"Cannot call open on a closed {GetType()}");
}
_isOpen = true;
OnOpen();
}
public void Close()
{
if (_isClosed)
{
throw new InvalidOperationException($"{GetType()} has already been closed");
}
if (!_isOpen)
{
throw new InvalidOperationException($"{GetType()} has never been opened");
}
_isClosed = true;
OnClose();
Writer.Flush();
}
/// <summary>
/// Called once the internal writer has been initialized
/// and the <see cref="Stream"/> is ready for writing
/// </summary>
protected virtual void OnOpen() { }
/// <summary>
/// Called once the document is about to be closed
/// </summary>
protected virtual void OnClose() { }
#endregion
#region Reading
/// <inheritdoc />
public StreamReader Read()
{
Writer.Flush();
Stream.Seek(0, SeekOrigin.Begin);
return new StreamReader(Stream, Writer.Encoding);
}
#endregion
#region Writing
public IStreamWriter Write(IStreamWriter writer)
{
using var reader = writer.Read();
return Write(reader);
}
public IStreamWriter Write(StreamReader reader)
{
int bytesRead;
char[] buffer = new char[4096];
while ((bytesRead = reader.Read(buffer, 0, buffer.Length)) > 0)
{
Writer.Write(buffer, 0, bytesRead);
}
return this;
}
public IStreamWriter Write(string? text = default)
{
Writer.Write(text);
return this;
}
public IStreamWriter WriteLine(string? text = default)
{
Writer.WriteLine(text);
return this;
}
/// <summary>
/// Creates and configures the <typeparamref name="T"/> using the given functions
/// </summary>
/// <typeparam name="T">Type of the <see cref="IStreamWriter"/></typeparam>
/// <param name="makeFunc">Function used to generate the <typeparamref name="T"/></param>
/// <param name="configFunc">Function used to configure the <typeparamref name="T"/></param>
/// <returns></returns>
public IStreamWriter Write<T>(Func<T> makeFunc, Action<T> configFunc)
where T : IStreamWriter
{
using var writer = makeFunc();
writer.Open();
configFunc(writer);
writer.Close();
Write(writer);
return this;
}
#endregion
#region IDisposable
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
try
{
// Close document
Close();
}
catch (InvalidOperationException)
{
// ignore
}
finally
{
// Dispose stream
Stream.Dispose();
}
}
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
@@ -0,0 +1,67 @@
using ReportGenerator.Generator.Interface;
using System.Text;
namespace ReportGenerator.Generator.Abstract;
public abstract class TableGeneratorBase : StreamWriterBase, ITableGenerator
{
/// <inheritdoc />
public int Columns { get; }
/// <inheritdoc />
protected TableGeneratorBase(int columns) =>
Columns = columns;
/// <inheritdoc />
protected TableGeneratorBase(int columns, Stream stream)
: base(stream) => Columns = columns;
/// <inheritdoc />
protected TableGeneratorBase(int columns, Stream stream, Encoding encoding)
: base(stream, encoding) => Columns = columns;
#region Header
/// <inheritdoc />
public virtual ITableGenerator AppendHeader(string content) =>
AppendHeader(Enumerable.Range(0, Columns).Select(_ => content));
/// <inheritdoc />
public abstract ITableGenerator AppendHeader(IEnumerable<string> row);
/// <inheritdoc />
public virtual ITableGenerator AppendHeader(IEnumerable<IEnumerable<string>> rows)
{
foreach (var row in rows)
{
AppendHeader(row);
}
return this;
}
#endregion
#region Row
/// <inheritdoc />
public virtual ITableGenerator AppendRow(string content) =>
AppendRow(Enumerable.Range(0, Columns).Select(_ => content));
/// <inheritdoc />
public abstract ITableGenerator AppendRow(IEnumerable<string> row);
/// <inheritdoc />
public virtual ITableGenerator AppendRows(IEnumerable<IEnumerable<string>> rows)
{
foreach (var row in rows)
{
AppendRow(row);
}
return this;
}
#endregion
}
@@ -0,0 +1,115 @@
using ReportGenerator.Generator.Abstract;
using ReportGenerator.Generator.Interface;
using System.Reflection;
using System.Text;
namespace ReportGenerator.Generator.Generator;
public class HtmlDocumentGenerator : DocumentGeneratorBase
{
private int _sectionLevel = 0;
public string Title { get; init; } = string.Empty;
/// <inheritdoc />
public HtmlDocumentGenerator() { }
/// <inheritdoc />
public HtmlDocumentGenerator(string filePath) : base(filePath)
{
Title = Path.GetFileNameWithoutExtension(filePath);
}
/// <inheritdoc />
public HtmlDocumentGenerator(Stream stream) : base(stream) { }
/// <inheritdoc />
public HtmlDocumentGenerator(Stream stream, Encoding encoding) : base(stream, encoding) { }
#region State
/// <inheritdoc />
protected override void OnOpen()
{
base.OnOpen();
// Init html document
Write("<!DOCTYPE html>");
Write("<html>");
// Header
Write("<head>");
Write(HtmlTools.Wrap("title", Title));
Write("<meta charset=\"utf-8\">");
Write("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
Write("<style>");
using (var stream = Resources.Get("Style.css"))
{
using var streamReader = new StreamReader(stream);
Write(streamReader);
}
Write("</style>");
Write("</head>");
// Init body
Write("<body>");
}
/// <inheritdoc />
protected override void OnClose()
{
base.OnClose();
// End document
Write("</body>");
Write("</html>");
}
#endregion
#region Writing
/// <inheritdoc cref="AppendParagraph(string)" />
public IDocumentGenerator AppendParagraph(string? text, string? @class) =>
Append(HtmlTools.Wrap("p", text, @class));
/// <inheritdoc />
public override IDocumentGenerator AppendParagraph(string? text = default) =>
AppendParagraph(text, default);
/// <inheritdoc />
public override IDocumentGenerator AppendHeading(int level, string text) =>
Append($"<h{level} id=\"{text}\">{text}</h{level}>");
/// <inheritdoc />
protected override ITableGenerator MakeTable(int columns, Stream stream) =>
new CollapsibleHtmlTableGenerator(columns, stream);
/// <inheritdoc />
public override string FormatImage(string path, IBounds? bounds = default) =>
FormatImage(path, default, bounds);
/// <inheritdoc cref="FormatImage(string,IBounds)" />
public string FormatImage(string path, string? @class, IBounds? bounds = default) =>
HtmlTools.FormatImage(path, @class, bounds);
#endregion
#region Resource Management
private static class Resources
{
private static readonly Assembly assembly = Assembly.GetExecutingAssembly();
private static readonly string basePath =
typeof(HtmlDocumentGenerator).Namespace + ".Resources.";
public static Stream Get(string fileName) =>
assembly.GetManifestResourceStream(basePath + fileName) ??
throw new FileNotFoundException("Could not get resource", fileName);
}
#endregion
}
@@ -0,0 +1,111 @@
using ReportGenerator.Generator.Abstract;
using ReportGenerator.Generator.Interface;
using System.Text;
namespace ReportGenerator.Generator.Generator;
internal class CollapsibleHtmlTableGenerator : HtmlTableGenerator
{
public string DetailsClass { get; init; }
public string Summary { get; init; } = "Show table";
/// <inheritdoc />
public CollapsibleHtmlTableGenerator(int columns)
: base(columns) { }
/// <inheritdoc />
public CollapsibleHtmlTableGenerator(int columns, Stream stream)
: base(columns, stream) { }
/// <inheritdoc />
public CollapsibleHtmlTableGenerator(int columns, Stream stream, Encoding encoding)
: base(columns, stream, encoding) { }
#region Overrides of HtmlTableGenerator
/// <inheritdoc />
protected override void OnOpen()
{
Writer.Write($"<details class=\"{DetailsClass}\" open>");
Writer.Write(HtmlTools.Wrap("summary", Summary));
base.OnOpen();
}
/// <inheritdoc />
protected override void OnClose()
{
base.OnClose();
Writer.Write($"</details>");
}
#endregion
}
internal class HtmlTableGenerator : TableGeneratorBase
{
public string Class { get; init; }
private static readonly (string start, string end) rowFormat = ("<tr>", "</tr>");
private static readonly (string start, string end) headerFormat = ("<th>", "</th>");
private static readonly (string start, string end) columnFormat = ("<td>", "</td>");
/// <inheritdoc />
public HtmlTableGenerator(int columns) : base(columns) { }
/// <inheritdoc />
public HtmlTableGenerator(int columns, Stream stream) : base(columns, stream) { }
/// <inheritdoc />
public HtmlTableGenerator(int columns, Stream stream, Encoding encoding) : base(columns, stream,
encoding)
{ }
#region State
/// <inheritdoc />
protected override void OnOpen()
{
base.OnOpen();
Writer.Write($"<table class={Class}>");
}
/// <inheritdoc />
protected override void OnClose()
{
base.OnClose();
Writer.Write("</table>");
}
#endregion
#region Writing
/// <inheritdoc />
public override ITableGenerator AppendHeader(IEnumerable<string> row) =>
AppendRow(row, rowFormat, headerFormat);
/// <inheritdoc />
public override ITableGenerator AppendRow(IEnumerable<string> row) =>
AppendRow(row, rowFormat, columnFormat);
private ITableGenerator AppendRow(
IEnumerable<string> row,
(string, string) rowFormat,
(string, string) columnFormat
)
{
var (rowStart, rowEnd) = rowFormat;
var (colStart, colEnd) = columnFormat;
this
.Write(rowStart)
.Write(colStart)
.Write(string.Join(colEnd + colStart, row))
.Write(colEnd)
.Write(rowEnd);
return this;
}
#endregion
}
@@ -0,0 +1,44 @@
using ReportGenerator.Generator.Abstract;
using ReportGenerator.Generator.Interface;
using System.Text;
namespace ReportGenerator.Generator.Generator;
public class MarkdownDocumentGenerator : DocumentGeneratorBase
{
/// <inheritdoc />
public MarkdownDocumentGenerator() { }
/// <inheritdoc />
public MarkdownDocumentGenerator(string filePath) : base(filePath) { }
/// <inheritdoc />
public MarkdownDocumentGenerator(Stream stream) : base(stream) { }
/// <inheritdoc />
public MarkdownDocumentGenerator(Stream stream, Encoding encoding) : base(stream, encoding) { }
#region Writing
/// <inheritdoc />
public override IDocumentGenerator AppendHeading(int level, string text) =>
AppendParagraph(new string('#', level) + ' ' + text);
/// <inheritdoc />
protected override ITableGenerator MakeTable(int columns, Stream stream) =>
new MarkdownTableGenerator(columns, stream);
/// <inheritdoc />
public override IDocumentGenerator AppendParagraph(string? text = default)
{
AppendLine(text);
AppendLine();
return this;
}
#endregion
/// <inheritdoc />
public override string FormatImage(string path, IBounds? bounds = default) =>
HtmlTools.FormatImage(path, default, bounds);
}
@@ -0,0 +1,35 @@
using ReportGenerator.Generator.Abstract;
using ReportGenerator.Generator.Interface;
using System.Text;
namespace ReportGenerator.Generator.Generator;
internal class MarkdownTableGenerator : TableGeneratorBase
{
private const string ColumnSeparator = " | ";
/// <inheritdoc />
public MarkdownTableGenerator(int columns)
: base(columns) { }
/// <inheritdoc />
public MarkdownTableGenerator(int columns, Stream stream)
: base(columns, stream) { }
/// <inheritdoc />
public MarkdownTableGenerator(int columns, Stream stream, Encoding encoding)
: base(columns, stream, encoding) { }
/// <inheritdoc />
public override ITableGenerator AppendHeader(IEnumerable<string> row) =>
this
.AppendRow(row)
.AppendRow("---");
/// <inheritdoc />
public override ITableGenerator AppendRow(IEnumerable<string> row)
{
Writer.WriteLine(ColumnSeparator + string.Join(" | ", row) + ColumnSeparator);
return this;
}
}
@@ -0,0 +1,17 @@
td, th {
border: 1px solid #777;
padding: .5rem;
text-align: center
}
table {
border-collapse: collapse
}
tbody tr:nth-child(odd) {
background: #eee
}
caption {
font-size: .8rem
}
@@ -0,0 +1,59 @@
using ReportGenerator.Generator.Interface;
namespace ReportGenerator.Generator;
internal static class HtmlTools
{
public static string Wrap(string tag, string? content, string? @class = default) =>
$"<{tag} class={@class}>{content}</{tag}>";
public static string FormatImage(string path, string? @class = default, IBounds? bounds = default)
{
var style = bounds is null
? string.Empty
: GetCssStyle(bounds);
path += path.EndsWith(".png") ? string.Empty : ".png";
return $"<img src=\"{path}\" style=\"{style}\" class={@class} />";
}
private static string GetCssStyle(IBounds bounds)
{
var style = string.Empty;
// Width
if (bounds.Width.HasValue)
{
style += $"width:{bounds.Width}{bounds.Unit};";
}
if (bounds.MinWidth.HasValue)
{
style += $"min-width:{bounds.MinWidth}{bounds.Unit};";
}
if (bounds.MaxWidth.HasValue)
{
style += $"max-width:{bounds.MaxWidth}{bounds.Unit};";
}
// Height
if (bounds.Height.HasValue)
{
style += $"height:{bounds.Height}{bounds.Unit};";
}
if (bounds.MinHeight.HasValue)
{
style += $"min-height:{bounds.MinHeight}{bounds.Unit};";
}
if (bounds.MaxHeight.HasValue)
{
style += $"max-height:{bounds.MaxHeight}{bounds.Unit};";
}
return style;
}
}
@@ -0,0 +1,12 @@
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; }
}
@@ -0,0 +1,16 @@
namespace ReportGenerator.Generator.Interface;
public interface IDocumentGenerator : IStreamWriter
{
IDocumentGenerator Append(string? text = default);
IDocumentGenerator AppendLine(string? text = default);
IDocumentGenerator AppendParagraph(string? text = default);
IDocumentGenerator AppendHeading(int level, string text);
IDocumentGenerator AppendTable(int columns, Action<ITableGenerator> table);
string FormatImage(string path, IBounds? bounds = default);
}
@@ -0,0 +1,37 @@
namespace ReportGenerator.Generator.Interface;
public interface IStreamWriter : IDisposable
{
/// <inheritdoc cref="TextWriter.WriteLine(string)"/>
IStreamWriter Write(string? text = default);
/// <inheritdoc cref="TextWriter.WriteLine(string)"/>
IStreamWriter WriteLine(string? text = default);
/// <summary>
/// <para>Writes the contents of the given <paramref name="writer"/>
/// to the internal <see cref="Stream"/></para>
/// </summary>
IStreamWriter Write(IStreamWriter writer);
/// <summary>
/// <para>Writes the contents of the given <paramref name="reader"/>
/// to the internal <see cref="Stream"/></para>
/// </summary>
IStreamWriter Write(StreamReader reader);
/// <summary>
/// Finalizes the content writer
/// </summary>
void Close();
/// <summary>
/// Initializes the content writer
/// </summary>
void Open();
/// <summary>
/// Reads the content of the underlying stream
/// </summary>
StreamReader Read();
}
@@ -0,0 +1,14 @@
namespace ReportGenerator.Generator.Interface;
public interface ITableGenerator : IStreamWriter
{
int Columns { get; }
ITableGenerator AppendHeader(string content);
ITableGenerator AppendHeader(IEnumerable<string> row);
ITableGenerator AppendHeader(IEnumerable<IEnumerable<string>> rows);
ITableGenerator AppendRow(string content);
ITableGenerator AppendRow(IEnumerable<string> row);
ITableGenerator AppendRows(IEnumerable<IEnumerable<string>> rows);
}
@@ -0,0 +1,44 @@
using ReportGenerator.Generator.Interface;
namespace ReportGenerator.Generator.Model;
public struct Bounds : IBounds
{
/// <inheritdoc />
public string Unit => "px";
/// <inheritdoc />
public int? MinWidth { get; set; } = null;
/// <inheritdoc />
public int? MinHeight { get; set; } = null;
/// <inheritdoc />
public int? MaxWidth { get; set; } = null;
/// <inheritdoc />
public int? MaxHeight { get; set; } = null;
/// <inheritdoc />
public int? Width { get; set; } = null;
/// <inheritdoc />
public int? Height { get; set; } = null;
public Bounds() { }
public Bounds(int? size)
{
Width = size;
Height = size;
}
public Bounds(int? min, int? max, int? size = null) : this(size)
{
MinWidth = min;
MinHeight = min;
MaxWidth = max;
MaxHeight = max;
}
}
@@ -0,0 +1,27 @@
namespace ReportGenerator.Models;
public readonly struct ImageStats
{
public string ImageName { get; }
public ICollection<string> Reference { get; }
public ICollection<ProcessorStat> Processors { get; }
public ImageStats(
string imageName,
ICollection<string> taggedWords,
IEnumerable<ScanFileInfo> scanResult
)
{
Reference = taggedWords;
ImageName = imageName;
Processors = scanResult
.Select(t =>
{
var (elapsed, words) = t.GetData();
return new ProcessorStat(t.ProcessorName, elapsed, taggedWords, words);
})
.ToArray();
}
}
@@ -0,0 +1,85 @@
using Common.Distance;
namespace ReportGenerator.Models;
public readonly struct ProcessorStat : IDistanceComparer<IEnumerable<string>>
{
/// <summary>
/// The name of the processor
/// </summary>
public string Name { get; }
/// <summary>
/// The total milliseconds it took the processor to process the data
/// </summary>
public double ProcessingTime { get; }
/// <inheritdoc />
public IEnumerable<string> Reference { get; }
/// <inheritdoc />
public IEnumerable<string>? Hypothesis { get; }
/// <inheritdoc />
public double Distance { get; }
/// <summary>
/// Information about the processed values
/// </summary>
public ICollection<IDistanceComparer<string>> Words { get; }
public ProcessorStat(
string name,
double processingTime,
ICollection<string> reference,
ICollection<string> hypothesis
)
{
Name = name;
ProcessingTime = processingTime;
Reference = reference;
Hypothesis = hypothesis;
Distance = Calculator.GetDistance(
reference.OrderBy(s => s).ToArray(),
hypothesis.OrderBy(s => s).ToArray()
) / reference.Count;
Words = GetDistanceInfos(reference, hypothesis).ToArray();
// Words = reference.Select(r => GetDistanceInfo(r, hypothesis)).ToArray();
}
/// <summary>
/// Compares the <paramref name="referenceCollection"/> with all given values in the
/// <paramref name="hypothesisCollection"/>, determining the <see cref="IDistanceComparer{T}"/>
/// with the lowest error
/// </summary>
private static IEnumerable<IDistanceComparer<string>> GetDistanceInfos(
ICollection<string> referenceCollection,
ICollection<string> hypothesisCollection
)
{
var results = new List<IDistanceComparer<string>>();
foreach (var reference in referenceCollection)
{
var tResults = hypothesisCollection
.Select(hypothesis => new DistanceComparer<string>(reference, hypothesis))
.Cast<IDistanceComparer<string>>()
.ToList();
results.AddRange(tResults);
}
var lookup = results
.OrderBy(result => result.Distance)
.ToLookup(result => result.Reference);
foreach (var reference in referenceCollection)
{
yield return lookup[reference].FirstOrDefault() ?? new DistanceComparer<string>(reference);
}
}
}
@@ -0,0 +1,48 @@
using System.Text.Json;
using System.Text.RegularExpressions;
namespace ReportGenerator.Models;
public struct ScanFileInfo
{
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 (double Elapsed, ICollection<string> Words) GetData()
{
using var file = File.OpenRead(Path);
var root = JsonDocument.Parse(file).RootElement;
var words = root
.GetProperty("Words")
.EnumerateArray()
.Select(e => e.GetProperty("Text").GetString() ?? string.Empty)
.ToArray();
var elapsed = root.GetProperty("Elapsed").GetDouble();
return (elapsed, words);
}
public static ScanFileInfo FromPath(string path)
{
var match = parseRegex.Match(System.IO.Path.GetFileName(path));
return new ScanFileInfo
{
Path = path,
ProcessorName = match.Groups["processor"].Value,
ImageName = match.Groups["image"].Value
};
}
/// <inheritdoc />
public override string ToString() => ImageName;
}
@@ -0,0 +1,31 @@
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;
}
+78
View File
@@ -0,0 +1,78 @@
using Common.Extensions;
using ReportGenerator.Generator.Generator;
using ReportGenerator.Models;
namespace ReportGenerator;
internal static class Program
{
internal static void Main(string[] args)
{
// Retrieve data
Console.WriteLine("Getting data");
var tagFileInfos = GetTagFileInfos(args[0]);
var scanFileInfos = GetScanFileInfos(args[1]);
// Parse
Console.WriteLine("Generating report");
var scans = Scan(tagFileInfos, scanFileInfos);
var path = Path.GetFullPath("report.html");
using var document = new HtmlDocumentGenerator(path);
using var report = new ReportGenerator("OCR Report", document, scans)
.AddComparison("Processing summary (Average)", v =>
{
var result = v.Average(out var deviation);
return (result, deviation);
})
// .AddComparison("Processing summary (Cumulative)", v => v.Sum())
.AddComparison("Processing summary (Median)", v =>
{
var result = v.Median(out var deviation);
return (result, deviation);
})
// .AddProcessorStats("Processor Stats")
.AddImageStatsFull("Scan Results");
Console.WriteLine($"Saved report to '{path}'");
}
private static IEnumerable<ImageStats> Scan(
IEnumerable<TagFileInfo> tagFileInfos,
IEnumerable<ScanFileInfo> scanFileInfos
)
{
var scanFileLookup = scanFileInfos.ToLookup(i => i.ImageName);
foreach (var i in tagFileInfos)
{
yield return new ImageStats(
i.ImageName,
i.GetWords().Distinct().OrderBy(w => w).ToArray(),
scanFileLookup[i.ImageName]
);
}
}
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<ScanFileInfo> GetScanFileInfos(string dir)
{
if (!Directory.Exists(dir))
{
throw new ArgumentException($"Invalid scan results directory '{dir}'");
}
return Directory.EnumerateFiles(dir, "*.json").Select(ScanFileInfo.FromPath);
}
}
@@ -0,0 +1,9 @@
{
"profiles": {
"ReportGenerator": {
"commandName": "Project",
"commandLineArgs": "\"img\" \"results\"",
"workingDirectory": "D:\\git\\BA\\Examples\\testdata"
}
}
}
+263
View File
@@ -0,0 +1,263 @@
using ReportGenerator.Generator.Interface;
using ReportGenerator.Generator.Model;
using ReportGenerator.Models;
namespace ReportGenerator;
public class ReportGenerator : IDisposable
{
private IDocumentGenerator Document { get; }
private ICollection<ImageStats> Images { get; }
public ReportGenerator(
IDocumentGenerator document,
IEnumerable<ImageStats> data
)
{
Images = data.ToArray();
Document = document;
document.Open();
}
public ReportGenerator(
string title,
IDocumentGenerator document,
IEnumerable<ImageStats> data
) : this(document, data) => AddTitle(title);
#region Writing
public ReportGenerator AddTitle(string text)
{
Document.AppendHeading(1, text);
return this;
}
public ReportGenerator AddProcessorStats(string title)
{
Document.AppendHeading(2, title);
var processors = new Dictionary<string, ICollection<ImageStats>>();
foreach (var image in Images)
{
foreach (var processor in image.Processors)
{
if (processors.TryGetValue(processor.Name, out var images))
{
images.Add(image);
}
else
{
processors.Add(processor.Name, new List<ImageStats> { image });
}
}
}
foreach (var (processor, images) in processors)
{
var ordered = images
.Select(i => (Stats: i, Distance: i
.Processors
.Where(p => p.Name.Equals(processor))
.Select(p => p.Distance)
.Average()
))
.OrderBy(i => i.Distance)
.ToArray();
Document
.AppendHeading(3, processor)
.AppendTable(2, table =>
{
table.AppendHeader(new[] { "Image", "Preview", "Distance" });
foreach (var (stats, distance) in ordered)
{
var imgPath = Path.Combine("results", $"{processor}.00.{stats.ImageName}.png");
table.AppendRow(new[]
{
$"<a href=\"#{stats.ImageName}\">{stats.ImageName}</a>",
Document.FormatImage(imgPath, new Bounds(0, 150)),
distance.ToString("F2")
});
}
}
);
}
return this;
}
public ReportGenerator AddImageStatsFull(string title)
{
Document.AppendHeading(2, title);
foreach (var stat in Images)
{
Document
.AppendHeading(3, stat.ImageName)
.AppendParagraph(
Document.FormatImage(Path.Combine("img", stat.ImageName), new Bounds(0, 350))
)
.AppendTable(
stat.Reference.Count + 5,
table =>
{
table.AppendHeader(stat
.Reference
.Prepend("Image")
.Prepend("CER (avg)")
.Prepend("WER")
.Prepend("Elapsed")
.Prepend("Processor")
);
var processors = stat.Processors
.OrderBy(s => s.Distance)
.ThenBy(s => s.ProcessingTime);
foreach (var processor in processors)
{
var imgPath = Path.Combine("results", $"{processor.Name}.00.{stat.ImageName}.png");
table.AppendRow(processor.Words
.Select(s => s.ToString() ?? string.Empty)
.Prepend(Document.FormatImage(imgPath, new Bounds(0, 150)))
.Prepend(processor.Words.Average(s => s.Distance).ToString("F2"))
.Prepend($"{processor.Distance * 100:F1}%")
.Prepend($"{processor.ProcessingTime * 1000:F1}ms")
.Prepend(processor.Name)
);
}
})
.AppendParagraph(
$"Comparison data generated based on {stat.Reference.Count} tagged words."
);
}
return this;
}
public ReportGenerator AddComparison(
string title,
Func<IEnumerable<double>, (double, double)> evaluationFunc
)
{
var lookup = Images
.SelectMany(s => s.Processors)
.ToLookup(p => p.Name);
Document.AppendHeading(2, title);
var byWer = lookup
.Select(g =>
{
var (value, deviation) = evaluationFunc(g.Select(p => p.Distance * 100));
return (
Name: g.Key,
Value: value,
Deviation: deviation
);
})
.OrderBy(g => g.Value);
Document.AppendHeading(3, "WER");
AppendComparison(("Error", "%"), byWer);
var byCer = lookup
.Select(g =>
{
var (value, deviation) =
evaluationFunc(g.SelectMany(p => p.Words, (_, word) => word.Distance));
return (
Name: g.Key,
Value: value,
Deviation: deviation
);
})
.OrderBy(g => g.Value);
Document.AppendHeading(3, "CER");
AppendComparison(("Changes", string.Empty), byCer);
var byTime = lookup
.Select(g =>
{
var (value, deviation) = evaluationFunc(g.Select(p => p.ProcessingTime * 1000));
return (
Name: g.Key,
Value: value,
Deviation: deviation
);
})
.OrderBy(g => g.Value);
Document.AppendHeading(3, "Time");
AppendComparison(("Time", "ms"), byTime);
return this;
}
private void AppendComparison(
(string name, string unit)? valueInfo,
IEnumerable<(string, double, double)> values
)
{
const int context = 5;
var tValues = values.ToArray();
var tContext = Math.Min(tValues.Length / 2, context);
Document.AppendTable(3, table =>
{
table
.AppendHeader(new[]
{
"Processor",
valueInfo?.name ?? "Value",
"Deviation"
})
.AppendRows(tValues
.Take(tContext)
.Select(MakeRow))
.AppendRow("...")
.AppendRows(
tValues
.TakeLast(tContext)
.Select(MakeRow)
);
return;
string[] MakeRow((string, double, double) v) =>
new[]
{
v.Item1,
v.Item2.ToString("F2") + valueInfo?.unit,
v.Item3.ToString("F2") + valueInfo?.unit
};
});
}
#endregion
#region IDisposable
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
Document.Dispose();
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="Properties\htmldocument-style.css" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Generator\Generator\Resources\Style.css" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,13 @@
{
"words": [
"interlockings",
"active",
"interlocking",
"text",
"typ",
"static",
"id",
"10034",
"no"
]
}

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

@@ -0,0 +1,35 @@
{
"words": [
"S7-300",
"datenbausteinkonfiguration",
"einstellungen",
"ok",
"abbrechen",
"name",
"seriell",
"ohne",
"id",
"datenbaustein",
"0",
"offset",
"frei",
"archiv kennung",
"SO",
"produkt ID",
"spaltenorientiert",
"seriell",
"schreibstatus",
"ok",
"abbrechen",
"datensatz",
"wert",
"datentyp",
"stelle",
"1",
"doppelwort",
"neuer",
"wert",
"löschen",
"neue"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+13
View File
@@ -0,0 +1,13 @@
{
"words": [
"SEL",
"enter",
"an",
"integer",
"between",
"100",
"and",
"180000",
"ok"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@@ -0,0 +1,23 @@
{
"words": [
"variablendefinition",
"selektieren",
"sie",
"das",
"gewünschte",
"objekt",
"verlassen",
"hilfe",
"neu",
"konfiguration",
"archive",
"float",
"treibervariable",
"bit",
"byte",
"wort",
"doppelwort",
"float",
"string"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

@@ -0,0 +1,19 @@
{
"words": [
"new",
"opc",
"tag",
"declaration",
"a",
"new",
"tag",
"file",
"filename",
"newOpcTag.opct",
"description",
"back",
"finish",
"cancel",
"help"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

@@ -0,0 +1,23 @@
{
"words": [
"select",
"a",
"connection",
"connections",
"(1)",
"D445",
"filter",
"all",
"variables",
"only",
"new",
"existing",
"variable",
"import",
"set",
"resources",
"label",
"ok",
"cancel"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

@@ -0,0 +1,33 @@
{
"words": [
"define",
"physical",
"monitor",
"name",
"MM",
"001",
"position",
"alternative",
"for",
"undedactable",
"M",
"000",
"top",
"left",
"right",
"bottom",
"0",
"1920",
"3840",
"1080",
"options",
"display",
"in",
"the",
"online",
"menu",
"ok",
"cancel",
"help"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

@@ -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"
]
}

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

+76
View File
@@ -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"
]
}

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

+38
View File
@@ -0,0 +1,38 @@
{
"words": [
"filter",
"profiles",
"savve",
"import",
"trend",
"WIZ10",
"curve",
"name",
"WIZ_VAR_10",
"WIZ_VAR_11",
"WIZ_VAR_12",
"filter",
"text",
"export",
"delete",
"play",
"stop",
"continue",
"refresh",
"zoom",
"rezoom",
"cursor",
"on",
"off",
"copy",
"to",
"clipboard",
"diagram",
"settings",
"print",
"axis",
"active",
"color",
"title"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+57
View File
@@ -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"
]
}

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

+23
View File
@@ -0,0 +1,23 @@
{
"words": [
"function",
"wizard",
"settings",
"syntax",
"alarm",
"archive",
"archiveex",
"archivexr",
"archivem",
"archivemr",
"archivemsp",
"archivemspr",
"archiver",
"archivesp",
"parameter",
"options",
"ok",
"cancel",
"help"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

@@ -0,0 +1,52 @@
{
"words": [
"reportassistant",
"data",
"report",
"rdl",
"tools",
"data",
"projects",
"ZAD_CIP",
"ZAD_GBL",
"OEEFactor",
"OEEFactor",
"batchhistory",
"DPA",
"single",
"alarms",
"most",
"frequent",
"equipment",
"related",
"group",
"consumption",
"relative",
"per",
"medium",
"group",
"abs",
"development",
"costs",
"absolute",
"ZAD_MAIN",
"filter",
"equipmentfilter",
"datetimefilter",
"string",
"variables",
"integer",
"report1",
"filter()",
"equipmentfilter",
"element",
"properties",
"model",
"mnestinglevel",
"misc",
"name",
"parameterlist",
"collection",
"type"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

+32
View File
@@ -0,0 +1,32 @@
{
"words": [
"service",
"engine",
"profiles",
"action",
"open",
"administration",
"dialog",
"in",
"the",
"service",
"engine",
"load",
"profile",
"default",
"last",
"freely",
"defined",
"name",
"name",
"from",
"variable",
"linked",
"no",
"save",
"profile",
"ok",
"cancel",
"help"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,60 @@
{
"words": [
"general",
"redundancy",
"advanced",
"settings",
"communication",
"standard",
"port",
"event",
"1200",
"9000",
"startup",
"no",
"start",
"open",
"this",
"box",
"cold",
"start",
"load",
"retain",
"variables",
"hot",
"restart",
"stepping",
"mode",
"delay",
"s",
"0",
"retain",
"data",
"storing",
"path",
"for",
"retain",
"data",
"users",
"public",
"documents",
"zenon_projects",
"700",
"test",
"rt",
"files",
"store",
"variables",
"by",
"name",
"cyclically",
"data",
"real",
"time",
"priority",
"ok",
"cancel",
"apply",
"help"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+20
View File
@@ -0,0 +1,20 @@
{
"words": [
"zoom",
"steps",
"existing",
"zoom",
"steps",
"step",
"to",
"%",
"0",
"100",
"new",
"edit",
"delete",
"ok",
"cancel",
"help"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

@@ -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"
]
}

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

@@ -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"
]
}

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

@@ -0,0 +1,131 @@
{
"words": [
"new",
"report",
"theme",
"extended",
"historian",
"analysis",
"the",
"report",
"templates",
"of",
"this",
"theme",
"perform",
"aggregated",
"and",
"historian",
"analysis",
"and",
"associate",
"the",
"historian",
"data",
"with",
"price",
"values",
"production",
"counters",
"standard",
"values",
"and",
"the",
"equipment",
"modeling",
"there",
"are",
"two",
"types",
"of",
"report",
"templates",
"of",
"this",
"theme",
"those",
"that",
"create",
"aggregated",
"trend",
"data",
"and",
"thos",
"that",
"create",
"distribution",
"data",
"report",
"template",
"relative",
"historian",
"aggregation",
"with",
"equipment",
"group",
"and",
"variable",
"selection",
"reports",
"based",
"on",
"this",
"template",
"calcculate",
"for",
"1",
"period",
"variable",
"filtering",
"is",
"based",
"on",
"equipment",
"groups",
"the",
"data",
"is",
"shown",
"in",
"charts",
"and",
"tables",
"preview",
"new",
"report",
"relative",
"historian",
"aggregation",
"with",
"equipment",
"group",
"and",
"variable",
"selection",
"from",
"filler",
"labeller",
"packer",
"ZAD_GBL",
"ENERGY",
"MACHINE",
"CYCLIC",
"DATA",
"PM",
"kwh",
"variable",
"name",
"absolute",
"consumption",
"relative",
"consumption",
"zenon",
"analyzer",
"www",
"copadata",
"com",
"page",
"ok",
"cancel"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

+38
View File
@@ -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"
]
}

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

+79
View File
@@ -0,0 +1,79 @@
{
"words": [
"provider",
"options",
"microsoft",
"ole",
"db",
"provider",
"for",
"adbc",
"drivers",
"select",
"a",
"page",
"general",
"script",
"help",
"provider",
"options",
"enable",
"name",
"dynamic",
"parameter",
"nested",
"queries",
"level",
"zero",
"only",
"allow",
"inprocess",
"non",
"transacted",
"updates",
"index",
"ad",
"access",
"path",
"disallow",
"adhoc",
"access",
"supports",
"like",
"operator",
"linked",
"server",
"using",
"this",
"provider",
"my",
"sql",
"server",
"these",
"options",
"are",
"applied",
"to",
"all",
"linked",
"servers",
"that",
"use",
"this",
"provider",
"ready",
"ok",
"cancel",
"view",
"connection",
"properties",
"connection",
"server",
"CDSBG036",
"ZA2",
"haraldr",
"cd",
"ok",
"cancel"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

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