From 6a5173a20d358f46282cc8d50c4f01e460efa3df Mon Sep 17 00:00:00 2001 From: MaddoScientisto Date: Mon, 16 Feb 2026 18:32:04 +0100 Subject: [PATCH] Add AI/OCR extraction feature with UI and CSV export Integrates optional AI/OCR (AIFotoONLUS.Core) support to extract numbers from images after processing. Adds new "AI" tab in the UI for enabling extraction, selecting models folder, specifying CSV output, and previewing results. Results can be exported to CSV. Uses reflection for AI library invocation, with fallback simulation if unavailable. Persists new AI settings. Updates related NuGet packages and adds theme resources. --- .../MaddoShared.Benchmarks.csproj | 4 +- MaddoShared.Tests/MaddoShared.Tests.csproj | 8 +- imagecatalog/DataModel.cs | 203 ++++++++++++++++++ imagecatalog/ImageCatalog 2.csproj | 1 + imagecatalog/MainWindow.xaml | 73 ++++++- imagecatalog/MainWindow.xaml.cs | 86 ++++++++ imagecatalog/Models/SettingsDto.cs | 13 ++ imagecatalog/Properties/Settings.settings | 11 + 8 files changed, 392 insertions(+), 7 deletions(-) diff --git a/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj b/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj index 7ce2437..b712ff3 100644 --- a/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj +++ b/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,7 @@ - + diff --git a/MaddoShared.Tests/MaddoShared.Tests.csproj b/MaddoShared.Tests/MaddoShared.Tests.csproj index c4a7299..b2f98b7 100644 --- a/MaddoShared.Tests/MaddoShared.Tests.csproj +++ b/MaddoShared.Tests/MaddoShared.Tests.csproj @@ -9,13 +9,13 @@ - - - + + + - + diff --git a/imagecatalog/DataModel.cs b/imagecatalog/DataModel.cs index 8091907..683d296 100644 --- a/imagecatalog/DataModel.cs +++ b/imagecatalog/DataModel.cs @@ -31,6 +31,8 @@ namespace ImageCatalog_2 public ICommand LoadSettingsCommand { get; } public ICommand SelectColorCommand { get; } public ICommand SelectTransparentColorCommand { get; } + public ICommand SelectModelsFolderCommand { get; } + public ICommand SelectCsvOutputCommand { get; } private readonly ITestService _service; private readonly ILogger _logger; @@ -61,6 +63,8 @@ namespace ImageCatalog_2 AsyncTestCommand = new AsyncCommand(TestAsync); AsyncCancelOperationCommand = new AsyncCommand(CancelOperation); ProcessImagesCommand = new AsyncCommand(ProcessImages); + SelectModelsFolderCommand = new RelayCommand(SelectModelsFolder); + SelectCsvOutputCommand = new RelayCommand(SelectCsvOutput); SelectSourceFolderCommand = new RelayCommand(SelectSourceFolder); SelectDestinationFolderCommand = new RelayCommand(SelectDestinationFolder); @@ -74,6 +78,176 @@ namespace ImageCatalog_2 AvailableFonts = LoadAvailableFonts(); } + private async Task RunAiExtractionAsync(CancellationToken token) + { + // Simple stub: scan source folder for supported images and either call AIFotoONLUS.Core + // or simulate results. Write CSV output and populate PreviewResults. + if (string.IsNullOrWhiteSpace(SourcePath) || !System.IO.Directory.Exists(SourcePath)) + { + _logger.LogWarning("Source path invalid for AI extraction: {SourcePath}", SourcePath); + return; + } + + var imageFiles = System.IO.Directory.EnumerateFiles(SourcePath, "*.*", System.IO.SearchOption.TopDirectoryOnly) + .Where(f => f.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) + || f.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) + || f.EndsWith(".png", StringComparison.OrdinalIgnoreCase) + || f.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase) + || f.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (imageFiles.Count == 0) + { + _logger.LogInformation("No image files found for AI extraction in {SourcePath}", SourcePath); + return; + } + + // Clear preview + await InvokeOnUiThreadAsync(() => { PreviewResults.Clear(); }); + + // Try to locate AIFotoONLUS.Core types via reflection to avoid hard reference at compile time + Type? aiProcessorType = null; + object? aiProcessor = null; + + try + { + var assembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name?.Equals("AIFotoONLUS.Core", StringComparison.OrdinalIgnoreCase) == true); + if (assembly != null) + { + aiProcessorType = assembly.GetType("AIFotoONLUS.Core.AiProcessor"); + if (aiProcessorType != null) + { + // Create instance assuming parameterless ctor + aiProcessor = Activator.CreateInstance(aiProcessorType); + } + } + } + catch (Exception ex) + { + _logger.LogDebug(ex, "AIFotoONLUS.Core not available or failed to load via reflection"); + } + + var results = new List(); + + foreach (var file in imageFiles) + { + token.ThrowIfCancellationRequested(); + + string extracted = string.Empty; + + if (aiProcessorType is not null && aiProcessor is not null) + { + try + { + // Preferred method name: ExtractNumbersFromImage(string imagePath) + var method = aiProcessorType.GetMethod("ExtractNumbersFromImage") ?? aiProcessorType.GetMethod("ExtractTextFromImage"); + if (method is not null) + { + var value = method.Invoke(aiProcessor, new object[] { file }); + if (value != null) + extracted = value.ToString() ?? string.Empty; + } + else + { + // No expected method found, fallback to simulated result + extracted = SimulateExtraction(file); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error invoking AI processor for {File}", file); + extracted = SimulateExtraction(file); + } + } + else + { + // Simulate extraction when library not available + extracted = SimulateExtraction(file); + } + + var res = new AiResult { Path = file, Text = extracted }; + results.Add(res); + + await InvokeOnUiThreadAsync(() => PreviewResults.Add(res)); + } + + // Write CSV if requested + if (!string.IsNullOrWhiteSpace(CsvOutputPath)) + { + try + { + var dir = System.IO.Path.GetDirectoryName(CsvOutputPath) ?? string.Empty; + if (!string.IsNullOrWhiteSpace(dir) && !System.IO.Directory.Exists(dir)) + { + System.IO.Directory.CreateDirectory(dir); + } + + using var sw = new System.IO.StreamWriter(CsvOutputPath, false, System.Text.Encoding.UTF8); + sw.WriteLine("Path,Text"); + foreach (var r in results) + { + var safeText = (r.Text ?? string.Empty).Replace("\"", "\"\""); + sw.WriteLine($"\"{r.Path}\",\"{safeText}\""); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to write CSV to {CsvOutputPath}", CsvOutputPath); + } + } + } + + private string SimulateExtraction(string file) + { + // Cheap heuristic: return filename digits + var name = System.IO.Path.GetFileNameWithoutExtension(file); + var digits = new string(name.Where(char.IsDigit).ToArray()); + if (string.IsNullOrEmpty(digits)) return ""; + return digits; + } + + private Task InvokeOnUiThreadAsync(Action action) + { + // Use SynchronizationContext via Task to ensure UI thread update + return Task.Run(() => + { + System.Windows.Application.Current?.Dispatcher.Invoke(action); + }); + } + + // AI properties + private bool _extractNumbers; + public bool ExtractNumbers + { + get => _extractNumbers; + set { _extractNumbers = value; NotifyPropertyChanged(); } + } + + private string _modelsFolderPath = string.Empty; + public string ModelsFolderPath + { + get => _modelsFolderPath; + set { _modelsFolderPath = value; NotifyPropertyChanged(); } + } + + private string _csvOutputPath = string.Empty; + public string CsvOutputPath + { + get => _csvOutputPath; + set { _csvOutputPath = value; NotifyPropertyChanged(); } + } + + // Preview results for DataGrid + private System.Collections.ObjectModel.ObservableCollection _previewResults = new(); + public System.Collections.ObjectModel.ObservableCollection PreviewResults => _previewResults; + + public class AiResult + { + public string Path { get; set; } = string.Empty; + public string Text { get; set; } = string.Empty; + } + private List LoadAvailableFonts() { var fonts = new List(); @@ -991,6 +1165,23 @@ namespace ImageCatalog_2 _results, OnImageProcessed, token); + + // AI integration stub: if ExtractNumbers is enabled, simulate or invoke OCR processing + if (ExtractNumbers) + { + try + { + await RunAiExtractionAsync(token); + } + catch (OperationCanceledException) + { + _logger.LogInformation("AI extraction canceled"); + } + catch (Exception ex) + { + _logger.LogError(ex, "AI extraction failed"); + } + } // Compute final averages and show only averages (do not show raw seconds) var finalProcessed = System.Threading.Volatile.Read(ref _processedAtomic); @@ -1161,6 +1352,8 @@ namespace ImageCatalog_2 public event EventHandler SelectSourceFolderRequested; public event EventHandler SelectDestinationFolderRequested; public event EventHandler SelectLogoFileRequested; + public event EventHandler SelectModelsFolderRequested; + public event EventHandler SelectCsvOutputRequested; public event EventHandler SaveSettingsRequested; public event EventHandler LoadSettingsRequested; public event EventHandler SelectColorRequested; @@ -1183,6 +1376,16 @@ namespace ImageCatalog_2 SelectLogoFileRequested?.Invoke(this, EventArgs.Empty); } + private void SelectModelsFolder(object parameter) + { + SelectModelsFolderRequested?.Invoke(this, EventArgs.Empty); + } + + private void SelectCsvOutput(object parameter) + { + SelectCsvOutputRequested?.Invoke(this, EventArgs.Empty); + } + private void SaveSettings(object parameter) { SaveSettingsRequested?.Invoke(this, null); diff --git a/imagecatalog/ImageCatalog 2.csproj b/imagecatalog/ImageCatalog 2.csproj index 4563b31..af7d536 100644 --- a/imagecatalog/ImageCatalog 2.csproj +++ b/imagecatalog/ImageCatalog 2.csproj @@ -41,6 +41,7 @@ + diff --git a/imagecatalog/MainWindow.xaml b/imagecatalog/MainWindow.xaml index b2bd652..7634adf 100644 --- a/imagecatalog/MainWindow.xaml +++ b/imagecatalog/MainWindow.xaml @@ -4,7 +4,33 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" - Title="Image Catalog - WPF" Height="490" Width="800"> + Title="Image Catalog - WPF" Height="490" Width="800" + Background="{DynamicResource WindowBackgroundBrush}" Foreground="{DynamicResource ControlForegroundBrush}"> + + + + + + + + + + + + + + + + + + + + + + + + + @@ -210,6 +236,51 @@ + + + + + + + + + + + + + + + + + +