Initial .NET scaffold: Core, Console, WPF projects
Introduced solution structure for AIFotoONLUS migration to .NET. Added Core library with YOLO-based detection/recognition engine using OpenCvSharp, Console batch runner, and WPF demo frontend with MVVM. Implemented model loading, directory processing, progress reporting, and preferences. Added README with build/run instructions.
This commit is contained in:
parent
314761bf9e
commit
769afc08fb
18 changed files with 976 additions and 0 deletions
16
src/AIFotoONLUS.WPF/AIFotoONLUS.WPF.csproj
Normal file
16
src/AIFotoONLUS.WPF/AIFotoONLUS.WPF.csproj
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NLog" Version="6.1.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AIFotoONLUS.Core\AIFotoONLUS.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
11
src/AIFotoONLUS.WPF/App.xaml
Normal file
11
src/AIFotoONLUS.WPF/App.xaml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<Application x:Class="AIFotoONLUS.WPF.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="clr-namespace:AIFotoONLUS.WPF.Converters"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<converters:InverseBoolConverter x:Key="InverseBoolConverter" />
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
8
src/AIFotoONLUS.WPF/App.xaml.cs
Normal file
8
src/AIFotoONLUS.WPF/App.xaml.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
using System.Windows;
|
||||
|
||||
namespace AIFotoONLUS.WPF
|
||||
{
|
||||
public partial class App : System.Windows.Application
|
||||
{
|
||||
}
|
||||
}
|
||||
21
src/AIFotoONLUS.WPF/Converters/InverseBoolConverter.cs
Normal file
21
src/AIFotoONLUS.WPF/Converters/InverseBoolConverter.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace AIFotoONLUS.WPF.Converters
|
||||
{
|
||||
public class InverseBoolConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool b) return !b;
|
||||
return true;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool b) return !b;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/AIFotoONLUS.WPF/MainWindow.xaml
Normal file
63
src/AIFotoONLUS.WPF/MainWindow.xaml
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<Window x:Class="AIFotoONLUS.WPF.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="AI Foto ONLUS - Demo" Height="460" Width="700">
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,8">
|
||||
<TextBlock VerticalAlignment="Center" Text="Images directory:" Margin="0,0,8,0"/>
|
||||
<TextBox Text="{Binding ImagesDirectory, UpdateSourceTrigger=PropertyChanged}" Width="420" />
|
||||
<Button Content="Browse" Margin="8,0,0,0" Width="75" Command="{Binding BrowseImagesCommand}"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Grid.Row="1" Margin="0,0,0,8">
|
||||
<TextBlock VerticalAlignment="Center" Text="Models directory:" Margin="0,0,8,0"/>
|
||||
<TextBox Text="{Binding ModelsDirectory, UpdateSourceTrigger=PropertyChanged}" Width="340" />
|
||||
<Button Content="Browse" Margin="8,0,0,0" Width="75" Command="{Binding BrowseModelsCommand}"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Grid.Row="2" Margin="0,0,0,8">
|
||||
<Button Content="Load Models" Width="100" Command="{Binding LoadModelsCommand}"/>
|
||||
<Button Content="Process" Width="100" Margin="8,0,0,0" Command="{Binding ProcessCommand}" IsEnabled="{Binding IsProcessing, Converter={StaticResource InverseBoolConverter}}" />
|
||||
<Button Content="Stop" Width="100" Margin="8,0,0,0" Command="{Binding CancelCommand}" IsEnabled="{Binding IsProcessing}" />
|
||||
<TextBlock Text="{Binding Status}" Margin="12,4,0,0" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="3">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*" />
|
||||
<ColumnDefinition Width="3*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0">
|
||||
<DataGrid AutoGenerateColumns="False" IsReadOnly="True" ItemsSource="{Binding Results}" Height="300">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Filename" Binding="{Binding FileName}" Width="*"/>
|
||||
<DataGridTextColumn Header="Text" Binding="{Binding Text}" Width="2*"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<ProgressBar Minimum="0" Maximum="100" Value="{Binding ProgressValue, Mode=OneWay}" Height="20" Margin="0,8,0,0" />
|
||||
<TextBlock Text="{Binding Status}" Margin="0,6,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBlock Text="Processed:" FontWeight="Bold"/>
|
||||
<TextBlock Text=" " />
|
||||
<TextBlock Text="{Binding ProcessedFiles}"/>
|
||||
<TextBlock Text=" / " />
|
||||
<TextBlock Text="{Binding TotalFiles}"/>
|
||||
<TextBlock Text=" " />
|
||||
<TextBlock Text="Imgs/sec:" FontWeight="Bold"/>
|
||||
<TextBlock Text=" " />
|
||||
<TextBlock Text="{Binding ImagesPerSecond, StringFormat=N2}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Preview removed as requested -->
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
23
src/AIFotoONLUS.WPF/MainWindow.xaml.cs
Normal file
23
src/AIFotoONLUS.WPF/MainWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using AIFotoONLUS.WPF.ViewModels;
|
||||
using System;
|
||||
using System.Windows;
|
||||
|
||||
namespace AIFotoONLUS.WPF
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private MainViewModel _vm = new();
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = _vm;
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
base.OnClosed(e);
|
||||
_vm.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/AIFotoONLUS.WPF/Preferences.cs
Normal file
34
src/AIFotoONLUS.WPF/Preferences.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace AIFotoONLUS.WPF
|
||||
{
|
||||
internal static class Preferences
|
||||
{
|
||||
private static readonly string PrefFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AIFotoONLUS", "prefs.txt");
|
||||
|
||||
public static void Save(string imagesDir, string modelsDir)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dir = Path.GetDirectoryName(PrefFile);
|
||||
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir!);
|
||||
File.WriteAllLines(PrefFile, new[] { imagesDir ?? string.Empty, modelsDir ?? string.Empty });
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public static (string imagesDir, string modelsDir) Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(PrefFile)) return (string.Empty, string.Empty);
|
||||
var lines = File.ReadAllLines(PrefFile);
|
||||
var img = lines.Length > 0 ? lines[0] : string.Empty;
|
||||
var mdl = lines.Length > 1 ? lines[1] : string.Empty;
|
||||
return (img, mdl);
|
||||
}
|
||||
catch { return (string.Empty, string.Empty); }
|
||||
}
|
||||
}
|
||||
}
|
||||
198
src/AIFotoONLUS.WPF/ViewModels/MainViewModel.cs
Normal file
198
src/AIFotoONLUS.WPF/ViewModels/MainViewModel.cs
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
using AIFotoONLUS.Core;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace AIFotoONLUS.WPF.ViewModels
|
||||
{
|
||||
public class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
private ModelConfiguration _cfg = new();
|
||||
private NumberRecognitionEngine? _engine;
|
||||
private string _imagesDirectory = string.Empty;
|
||||
private string _modelsDirectory = "models";
|
||||
private string _status = string.Empty;
|
||||
private double _progressValue;
|
||||
private double _imagesPerSecond;
|
||||
private int _totalFiles;
|
||||
private int _processedFiles;
|
||||
private bool _isProcessing;
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
public ObservableCollection<ImageResult> Results { get; } = new();
|
||||
|
||||
public string ImagesDirectory
|
||||
{
|
||||
get => _imagesDirectory;
|
||||
set { _imagesDirectory = value; OnPropertyChanged(nameof(ImagesDirectory)); }
|
||||
}
|
||||
|
||||
public string ModelsDirectory
|
||||
{
|
||||
get => _modelsDirectory;
|
||||
set { _modelsDirectory = value; OnPropertyChanged(nameof(ModelsDirectory)); }
|
||||
}
|
||||
|
||||
public string Status
|
||||
{
|
||||
get => _status;
|
||||
private set { _status = value; OnPropertyChanged(nameof(Status)); }
|
||||
}
|
||||
|
||||
public double ProgressValue
|
||||
{
|
||||
get => _progressValue;
|
||||
private set { _progressValue = value; OnPropertyChanged(nameof(ProgressValue)); }
|
||||
}
|
||||
|
||||
public double ImagesPerSecond
|
||||
{
|
||||
get => _imagesPerSecond;
|
||||
private set { _imagesPerSecond = value; OnPropertyChanged(nameof(ImagesPerSecond)); }
|
||||
}
|
||||
|
||||
public int TotalFiles
|
||||
{
|
||||
get => _totalFiles;
|
||||
private set { _totalFiles = value; OnPropertyChanged(nameof(TotalFiles)); }
|
||||
}
|
||||
|
||||
public int ProcessedFiles
|
||||
{
|
||||
get => _processedFiles;
|
||||
private set { _processedFiles = value; OnPropertyChanged(nameof(ProcessedFiles)); }
|
||||
}
|
||||
|
||||
public bool IsProcessing
|
||||
{
|
||||
get => _isProcessing;
|
||||
private set { _isProcessing = value; OnPropertyChanged(nameof(IsProcessing)); ProcessCommand.RaiseCanExecuteChanged(); CancelCommand.RaiseCanExecuteChanged(); }
|
||||
}
|
||||
|
||||
// No image preview required by user — selection/preview removed
|
||||
|
||||
public RelayCommand BrowseImagesCommand { get; }
|
||||
public RelayCommand BrowseModelsCommand { get; }
|
||||
public RelayCommand LoadModelsCommand { get; }
|
||||
public RelayCommand ProcessCommand { get; }
|
||||
public RelayCommand CancelCommand { get; }
|
||||
|
||||
public MainViewModel()
|
||||
{
|
||||
BrowseImagesCommand = new RelayCommand(_ => BrowseFolder(isModel: false));
|
||||
BrowseModelsCommand = new RelayCommand(_ => BrowseFolder(isModel: true));
|
||||
LoadModelsCommand = new RelayCommand(_ => LoadModels());
|
||||
ProcessCommand = new RelayCommand(async _ => await ProcessAsync(), _ => !IsProcessing);
|
||||
CancelCommand = new RelayCommand(_ => Cancel(), _ => IsProcessing);
|
||||
|
||||
// load prefs
|
||||
var prefs = Preferences.Load();
|
||||
if (!string.IsNullOrWhiteSpace(prefs.imagesDir)) ImagesDirectory = prefs.imagesDir;
|
||||
if (!string.IsNullOrWhiteSpace(prefs.modelsDir)) ModelsDirectory = prefs.modelsDir;
|
||||
}
|
||||
|
||||
private void BrowseFolder(bool isModel)
|
||||
{
|
||||
using var dlg = new System.Windows.Forms.FolderBrowserDialog();
|
||||
var result = dlg.ShowDialog();
|
||||
if (result == System.Windows.Forms.DialogResult.OK)
|
||||
{
|
||||
if (isModel) ModelsDirectory = dlg.SelectedPath; else ImagesDirectory = dlg.SelectedPath;
|
||||
Preferences.Save(ImagesDirectory, ModelsDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadModels()
|
||||
{
|
||||
try
|
||||
{
|
||||
_engine?.Dispose();
|
||||
_cfg = new ModelConfiguration
|
||||
{
|
||||
DetectionCfg = Path.Combine(ModelsDirectory, "detection.cfg"),
|
||||
DetectionWeights = Path.Combine(ModelsDirectory, "detection.weights"),
|
||||
RecognitionCfg = Path.Combine(ModelsDirectory, "recognition.cfg"),
|
||||
RecognitionWeights = Path.Combine(ModelsDirectory, "recognition.weights"),
|
||||
ConfidenceThreshold = 0.5,
|
||||
NmsThreshold = 0.4
|
||||
};
|
||||
|
||||
_engine = new NumberRecognitionEngine(_cfg);
|
||||
Status = "Models loaded";
|
||||
Preferences.Save(ImagesDirectory, ModelsDirectory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Windows.MessageBox.Show(ex.Message, "Error loading models", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
Status = "Error loading models";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessAsync()
|
||||
{
|
||||
if (_engine == null)
|
||||
{
|
||||
System.Windows.MessageBox.Show("Load models first.", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(ImagesDirectory) || !Directory.Exists(ImagesDirectory))
|
||||
{
|
||||
System.Windows.MessageBox.Show("Select a valid directory.", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
Status = "Processing...";
|
||||
Results.Clear();
|
||||
ProgressValue = 0;
|
||||
ImagesPerSecond = 0;
|
||||
TotalFiles = 0;
|
||||
ProcessedFiles = 0;
|
||||
IsProcessing = true;
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
var progress = new Progress<ProcessingStats>(s =>
|
||||
{
|
||||
TotalFiles = s.TotalFiles;
|
||||
ProcessedFiles = s.ProcessedFiles;
|
||||
ImagesPerSecond = s.ImagesPerSecond;
|
||||
ProgressValue = s.TotalFiles > 0 ? (double)s.ProcessedFiles / s.TotalFiles * 100.0 : 0;
|
||||
});
|
||||
try
|
||||
{
|
||||
var resultProgress = new Progress<ImageResult>(r => Results.Add(r));
|
||||
await _engine.ProcessDirectoryAsync(ImagesDirectory, recursive: true, progress: progress, resultProgress: resultProgress, cancellationToken: _cts.Token);
|
||||
Status = $"Done ({Results.Count})";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Windows.MessageBox.Show(ex.Message, "Processing error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
Status = "Error";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsProcessing = false;
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
if (!_isProcessing) return;
|
||||
_cts?.Cancel();
|
||||
Status = "Cancelling...";
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_engine?.Dispose();
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
private void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
}
|
||||
25
src/AIFotoONLUS.WPF/ViewModels/RelayCommand.cs
Normal file
25
src/AIFotoONLUS.WPF/ViewModels/RelayCommand.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using System;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace AIFotoONLUS.WPF.ViewModels
|
||||
{
|
||||
public class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Action<object?> _execute;
|
||||
private readonly Predicate<object?>? _canExecute;
|
||||
|
||||
public RelayCommand(Action<object?> execute, Predicate<object?>? canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
|
||||
|
||||
public void Execute(object? parameter) => _execute(parameter);
|
||||
|
||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue