using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal; using Ocr.Report.Models; using ReportGeneration.Abstract.Model; using ReportGeneration.Interface; using System; namespace Ocr.Report; public class ReportGenerator : IDisposable { public int? MaxDisplayRows { get; init; } private IDocumentGenerator Document { get; } private ICollection Images { get; } public ReportGenerator( IDocumentGenerator document, IEnumerable data ) { Images = data.ToArray(); Document = document; document.Open(); } public ReportGenerator( string title, IDocumentGenerator document, IEnumerable 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>(); 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 { image }); } } } foreach (var (processor, images) in processors) { IEnumerable<(ImageStats Stats, double Distance)> 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(); if (MaxDisplayRows.HasValue) { ordered = ordered .Take(MaxDisplayRows.Value / 2) .Concat(ordered.TakeLast(MaxDisplayRows.Value / 2)); } Document .AppendHeading(3, processor) .AppendTable(3, 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[] { Ellipsis(stats.ImageName.Replace("\\", "\\\\") + ".png", 25), 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 + 6, table => { table.AppendHeader(stat .Reference .Prepend("Image") .Prepend("Perfect matches") .Prepend("CER (avg)") .Prepend("WER") .Prepend("Elapsed") .Prepend("Processor") ); IEnumerable processors = stat.Processors .OrderBy(s => s.Distance) .ThenBy(s => s.ProcessingTime) .ToArray(); if (MaxDisplayRows.HasValue) { processors = processors .Take(MaxDisplayRows.Value / 2) .Concat(processors.TakeLast(MaxDisplayRows.Value / 2)); } foreach (var processor in processors) { 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.Count(w => w.Distance == 0)} / {processor.Words.Count}") .Prepend(processor.Words.Average(s => s.Distance).ToString("F2")) .Prepend($"{processor.Distance * 100:F1}%") .Prepend($"{processor.ProcessingTime * 1000:F1}ms") .Prepend(Ellipsis(processor.Name, 25)) ); } }) .AppendParagraph( $"Comparison data generated based on {stat.Reference.Count} tagged words." ); } return this; } public ReportGenerator AddComparison( string title, Func, (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 private static string Ellipsis(string str, int maxLength, string ellipsis = "...") { if (str.Length <= maxLength) { return str; } int subStrLen = (maxLength - ellipsis.Length) / 2; return string.Concat( str.Substring(0, subStrLen), ellipsis, str.Substring(str.Length - subStrLen, subStrLen)); } #region IDisposable protected virtual void Dispose(bool disposing) { if (disposing) { Document.Dispose(); } } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion }