feat: Add Face Matcher functionality and related settings
- Implemented FilePathToBitmapConverter for image loading. - Enhanced DataModel with commands and properties for Face Matcher. - Created FaceMatcherResultItem model to store results. - Updated SettingsDto to include Face Matcher paths and tolerance. - Introduced PickerPreferenceService for managing folder paths. - Expanded AiSettingsViewModel to manage Face Matcher settings and results.
This commit is contained in:
parent
f57dc1edba
commit
c261557a29
10 changed files with 2041 additions and 192 deletions
|
|
@ -5,6 +5,8 @@ using Avalonia.Layout;
|
|||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using ImageCatalog_2.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
|
||||
|
|
@ -13,6 +15,7 @@ namespace ImageCatalog_2;
|
|||
public partial class AvaloniaMainWindow : Window
|
||||
{
|
||||
private readonly DataModel _model;
|
||||
private readonly PickerPreferenceService _pickerPreferenceService;
|
||||
private bool _isDarkTheme;
|
||||
|
||||
public AvaloniaMainWindow(DataModel model)
|
||||
|
|
@ -20,6 +23,7 @@ public partial class AvaloniaMainWindow : Window
|
|||
InitializeComponent();
|
||||
|
||||
_model = model;
|
||||
_pickerPreferenceService = Program.ServiceProvider.GetRequiredService<PickerPreferenceService>();
|
||||
DataContext = _model;
|
||||
|
||||
Opened += (_, _) => SyncThemeStateFromCurrentTheme();
|
||||
|
|
@ -30,35 +34,43 @@ public partial class AvaloniaMainWindow : Window
|
|||
|
||||
_model.SelectSourceFolderRequested += async (_, _) =>
|
||||
{
|
||||
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.SourceFolder, _model.SourcePath);
|
||||
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Seleziona cartella sorgente"
|
||||
Title = "Seleziona cartella sorgente",
|
||||
SuggestedStartLocation = suggestedStartLocation
|
||||
});
|
||||
|
||||
if (folders.Count > 0)
|
||||
{
|
||||
_model.SourcePath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
|
||||
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.SourceFolder, _model.SourcePath);
|
||||
}
|
||||
};
|
||||
|
||||
_model.SelectDestinationFolderRequested += async (_, _) =>
|
||||
{
|
||||
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.DestinationFolder, _model.DestinationPath);
|
||||
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Seleziona cartella destinazione"
|
||||
Title = "Seleziona cartella destinazione",
|
||||
SuggestedStartLocation = suggestedStartLocation
|
||||
});
|
||||
|
||||
if (folders.Count > 0)
|
||||
{
|
||||
_model.DestinationPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
|
||||
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.DestinationFolder, _model.DestinationPath);
|
||||
}
|
||||
};
|
||||
|
||||
_model.SelectLogoFileRequested += async (_, _) =>
|
||||
{
|
||||
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.LogoFile, _model.LogoFile);
|
||||
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Seleziona logo",
|
||||
SuggestedStartLocation = suggestedStartLocation,
|
||||
FileTypeFilter =
|
||||
[
|
||||
new FilePickerFileType("Immagini") { Patterns = ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif"] }
|
||||
|
|
@ -68,34 +80,41 @@ public partial class AvaloniaMainWindow : Window
|
|||
if (files.Count > 0)
|
||||
{
|
||||
_model.LogoFile = files[0].Path.LocalPath;
|
||||
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.LogoFile, _model.LogoFile);
|
||||
}
|
||||
};
|
||||
|
||||
_model.SelectModelsFolderRequested += async (_, _) =>
|
||||
{
|
||||
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.ModelsFolder, _model.ModelsFolderPath);
|
||||
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Seleziona cartella modelli"
|
||||
Title = "Seleziona cartella modelli",
|
||||
SuggestedStartLocation = suggestedStartLocation
|
||||
});
|
||||
|
||||
if (folders.Count > 0)
|
||||
{
|
||||
_model.ModelsFolderPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
|
||||
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.ModelsFolder, _model.ModelsFolderPath);
|
||||
}
|
||||
};
|
||||
|
||||
_model.SelectCsvOutputRequested += async (_, _) =>
|
||||
{
|
||||
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.CsvOutput, _model.CsvOutputPath);
|
||||
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Salva CSV",
|
||||
DefaultExtension = "csv",
|
||||
FileTypeChoices = [new FilePickerFileType("CSV") { Patterns = ["*.csv"] }]
|
||||
FileTypeChoices = [new FilePickerFileType("CSV") { Patterns = ["*.csv"] }],
|
||||
SuggestedStartLocation = suggestedStartLocation
|
||||
});
|
||||
|
||||
if (file is not null)
|
||||
{
|
||||
_model.CsvOutputPath = file.Path.LocalPath;
|
||||
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.CsvOutput, _model.CsvOutputPath);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -148,7 +167,7 @@ public partial class AvaloniaMainWindow : Window
|
|||
|
||||
private async void AvaloniaMainWindow_Closing(object? sender, CancelEventArgs e)
|
||||
{
|
||||
if (_isStoppingFaceEncoderForClose || !_model.IsFaceEncoderRunning)
|
||||
if (_isStoppingFaceEncoderForClose || (!_model.IsFaceEncoderRunning && !_model.IsFaceMatcherRunning))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -158,7 +177,15 @@ public partial class AvaloniaMainWindow : Window
|
|||
|
||||
try
|
||||
{
|
||||
await _model.StopFaceEncoderAsync("Arresto face encoder in chiusura...", waitForExit: true);
|
||||
if (_model.IsFaceMatcherRunning)
|
||||
{
|
||||
await _model.StopFaceMatcherAsync("Arresto face matcher in chiusura...", waitForExit: true);
|
||||
}
|
||||
|
||||
if (_model.IsFaceEncoderRunning)
|
||||
{
|
||||
await _model.StopFaceEncoderAsync("Arresto face encoder in chiusura...", waitForExit: true);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,109 +1,326 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:avaloniaDataGrid="clr-namespace:Avalonia.Controls;assembly=Avalonia.Controls.DataGrid"
|
||||
xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia"
|
||||
xmlns:converters="clr-namespace:ImageCatalog_2.Converters"
|
||||
x:Class="ImageCatalog_2.AvaloniaViews.FaceAiTabView">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="4" Spacing="6">
|
||||
<TextBlock Text="Face Recognition Encoder" FontWeight="Bold" />
|
||||
<TextBlock Text="Esegue il face encoder usando la cartella Destinazione corrente come --images e genera automaticamente file .pkl e log nella cartella di output scelta."
|
||||
TextWrapping="Wrap" Opacity="0.8" />
|
||||
<UserControl.Resources>
|
||||
<converters:FilePathToBitmapConverter x:Key="FilePathToBitmapConverter" />
|
||||
</UserControl.Resources>
|
||||
|
||||
<TextBlock Text="Cartella Face Encoder" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="Percorso:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceExecutablePathTextBox" Text="{Binding FaceExecutablePath, Mode=TwoWay}" Watermark="C:\tools\Face_Recognition_Windows" />
|
||||
<Button Grid.Column="2" Name="FaceSelectExecutableButton" Click="SelectFaceExecutable_Click" Width="104" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="FolderOutline" Width="14" Height="14" />
|
||||
<TextBlock Text="Scegli..." />
|
||||
<TabControl Margin="4">
|
||||
<TabItem Header="Encoder">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="4" Spacing="6">
|
||||
<TextBlock Text="Face Recognition Encoder" FontWeight="Bold" />
|
||||
<TextBlock Text="Esegue il face encoder usando la cartella Destinazione corrente come --images e genera automaticamente file .pkl e log nella cartella di output scelta."
|
||||
TextWrapping="Wrap" Opacity="0.8" />
|
||||
|
||||
<TextBlock Text="Cartella Face Encoder" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="Percorso:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceExecutablePathTextBox" Text="{Binding FaceExecutablePath, Mode=TwoWay}" Watermark="C:\tools\Face_Recognition_Windows" />
|
||||
<Button Grid.Column="2" Name="FaceSelectExecutableButton" Click="SelectFaceExecutable_Click" Width="104" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="FolderOutline" Width="14" Height="14" />
|
||||
<TextBlock Text="Scegli..." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Grid.Column="3" Name="FaceOpenExecutableButton" Click="OpenFaceExecutableFolder_Click" Width="72" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
|
||||
<TextBlock Text="Apri" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<CheckBox Content="Ricorsivo (--recursive)" IsChecked="{Binding FaceRecursive, Mode=TwoWay}" />
|
||||
<CheckBox Content="Includi thumbnail (--include-tn)" IsChecked="{Binding FaceIncludeThumbnails, Mode=TwoWay}" />
|
||||
<CheckBox Content="Upsample (--upsample)" IsChecked="{Binding FaceUpsample, Mode=TwoWay}" />
|
||||
<CheckBox Content="Usa GPU"
|
||||
IsChecked="{Binding UseFaceGpu, Mode=TwoWay}"
|
||||
IsEnabled="{Binding FaceGpuOptionEnabled}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Grid.Column="3" Name="FaceOpenExecutableButton" Click="OpenFaceExecutableFolder_Click" Width="72" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
|
||||
<TextBlock Text="Apri" />
|
||||
<TextBlock Text="Seleziona la cartella base di Face Recognition Windows: l'app sceglie automaticamente face_encoder_cpu.exe o face_encoder_gpu.exe in base al checkbox GPU."
|
||||
TextWrapping="Wrap"
|
||||
Opacity="0.75" />
|
||||
|
||||
<Grid ColumnDefinitions="Auto,120,Auto,120,*" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="Parallelismo:" VerticalAlignment="Center" />
|
||||
<ComboBox Grid.Column="1" ItemsSource="{Binding FaceParallelismOptions}" SelectedItem="{Binding FaceParallelism, Mode=TwoWay}" />
|
||||
<TextBlock Grid.Column="2" Text="Min size:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="3" Text="{Binding FaceMinSize, Mode=TwoWay}" Watermark="35" />
|
||||
<TextBlock Grid.Column="4" Text="Usa --multicore in CPU e --multiprocess in GPU." VerticalAlignment="Center" Opacity="0.75" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="Sorgente:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceDestinationPathTextBox" Text="{Binding DestinationPath, Mode=OneWay}" IsReadOnly="True" />
|
||||
<Button Grid.Column="3" Name="FaceOpenDestinationButton" Click="OpenFaceDestinationFolder_Click" Width="72" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
|
||||
<TextBlock Text="Apri" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Output encodings" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="Cartella out:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceOutputFolderTextBox" Text="{Binding FaceOutputFolderPath, Mode=TwoWay}" Watermark="C:\output\face_encoder" />
|
||||
<Button Grid.Column="2" Name="FaceSelectOutputButton" Click="SelectFaceOutputFolder_Click" Width="104" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="FolderOutline" Width="14" Height="14" />
|
||||
<TextBlock Text="Scegli..." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Grid.Column="3" Name="FaceOpenOutputButton" Click="OpenFaceOutputFolder_Click" Width="72" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
|
||||
<TextBlock Text="Apri" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
<TextBlock Text="I file vengono creati come face_encodings_yyyyMMdd_HHmmss_nomecartella.pkl e encoder_log_yyyyMMdd_HHmmss_nomecartella.txt."
|
||||
TextWrapping="Wrap"
|
||||
Opacity="0.75" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,6,0,0">
|
||||
<Button Name="FaceRunButton" Command="{Binding StartFaceEncoderCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<ProgressBar Width="18"
|
||||
Height="18"
|
||||
IsIndeterminate="True"
|
||||
IsVisible="{Binding IsFaceEncoderRunning}"
|
||||
ShowProgressText="False"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="Esegui Face Encoder" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Content="Stop" Command="{Binding StopFaceEncoderCommand}" />
|
||||
<TextBlock VerticalAlignment="Center" Text="{Binding FaceStatusMessage}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<CheckBox Content="Ricorsivo (--recursive)" IsChecked="{Binding FaceRecursive, Mode=TwoWay}" />
|
||||
<CheckBox Content="Includi thumbnail (--include-tn)" IsChecked="{Binding FaceIncludeThumbnails, Mode=TwoWay}" />
|
||||
<CheckBox Content="Upsample (--upsample)" IsChecked="{Binding FaceUpsample, Mode=TwoWay}" />
|
||||
<CheckBox Content="Usa GPU"
|
||||
IsChecked="{Binding UseFaceGpu, Mode=TwoWay}"
|
||||
IsEnabled="{Binding FaceGpuOptionEnabled}" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="Seleziona la cartella base di Face Recognition Windows: l'app sceglie automaticamente face_encoder_cpu.exe o face_encoder_gpu.exe in base al checkbox GPU."
|
||||
TextWrapping="Wrap"
|
||||
Opacity="0.75" />
|
||||
<TextBlock Text="Output comando" FontWeight="Bold" Margin="0,6,0,0" />
|
||||
<TextBox Name="FaceOutputTextBox"
|
||||
Text="{Binding FaceCommandOutput}"
|
||||
IsReadOnly="True"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap"
|
||||
FontFamily="Cascadia Mono, Consolas, monospace"
|
||||
Height="220"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Auto" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
<Grid ColumnDefinitions="Auto,120,Auto,120,*" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="Parallelismo:" VerticalAlignment="Center" />
|
||||
<ComboBox Grid.Column="1" ItemsSource="{Binding FaceParallelismOptions}" SelectedItem="{Binding FaceParallelism, Mode=TwoWay}" />
|
||||
<TextBlock Grid.Column="2" Text="Min size:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="3" Text="{Binding FaceMinSize, Mode=TwoWay}" Watermark="35" />
|
||||
<TextBlock Grid.Column="4" Text="Usa --multicore in CPU e --multiprocess in GPU." VerticalAlignment="Center" Opacity="0.75" />
|
||||
</Grid>
|
||||
<TabItem Header="Matcher Test">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="4" Spacing="6">
|
||||
<TextBlock Text="Face Matcher Test" FontWeight="Bold" />
|
||||
<TextBlock Text="Esegue face_matcher.exe su un'immagine scelta a runtime e confronta i risultati con gli encodings .pkl. I match vengono poi risolti per nome file dentro la cartella Destinazione corrente per facilitare il debug visivo."
|
||||
TextWrapping="Wrap" Opacity="0.8" />
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="Sorgente:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceDestinationPathTextBox" Text="{Binding DestinationPath, Mode=OneWay}" IsReadOnly="True" />
|
||||
<Button Grid.Column="3" Name="FaceOpenDestinationButton" Click="OpenFaceDestinationFolder_Click" Width="72" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
|
||||
<TextBlock Text="Apri" />
|
||||
<TextBlock Text="Eseguibile matcher" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="Percorso:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceMatcherExecutablePathTextBox" Text="{Binding FaceMatcherExecutablePath, Mode=TwoWay}" Watermark="C:\tools\Face_Recognition_Windows\face_matcher.exe" />
|
||||
<Button Grid.Column="2" Click="SelectFaceMatcherExecutable_Click" Width="104" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="FileOutline" Width="14" Height="14" />
|
||||
<TextBlock Text="Scegli..." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Grid.Column="3" Click="OpenFaceMatcherExecutable_Click" Width="72" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
|
||||
<TextBlock Text="Apri" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Immagine da testare" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="Immagine:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceMatcherImagePathTextBox" Text="{Binding FaceMatcherSelectedImagePath, Mode=TwoWay}" Watermark="Percorso immagine da usare per il match" />
|
||||
<Button Grid.Column="2" Click="SelectFaceMatcherImage_Click" Width="104" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="ImageOutline" Width="14" Height="14" />
|
||||
<TextBlock Text="Scegli..." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Grid.Column="3" Click="OpenFaceMatcherImage_Click" Width="72" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
|
||||
<TextBlock Text="Apri" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
<TextBlock Text="Questa immagine resta solo runtime e non viene salvata nel file impostazioni."
|
||||
TextWrapping="Wrap"
|
||||
Opacity="0.75" />
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Preview immagine selezionata" FontWeight="SemiBold" />
|
||||
<Border BorderThickness="1" BorderBrush="#808080" Padding="4" HorizontalAlignment="Left">
|
||||
<Image Width="184"
|
||||
Height="120"
|
||||
Stretch="UniformToFill"
|
||||
Source="{Binding FaceMatcherSelectedImagePath, Converter={StaticResource FilePathToBitmapConverter}}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Output encodings" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="Cartella out:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceOutputFolderTextBox" Text="{Binding FaceOutputFolderPath, Mode=TwoWay}" Watermark="C:\output\face_encoder" />
|
||||
<Button Grid.Column="2" Name="FaceSelectOutputButton" Click="SelectFaceOutputFolder_Click" Width="104" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="FolderOutline" Width="14" Height="14" />
|
||||
<TextBlock Text="Scegli..." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Grid.Column="3" Name="FaceOpenOutputButton" Click="OpenFaceOutputFolder_Click" Width="72" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
|
||||
<TextBlock Text="Apri" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
<TextBlock Text="I file vengono creati come face_encodings_yyyyMMdd_HHmmss_nomecartella.pkl e encoder_log_yyyyMMdd_HHmmss_nomecartella.txt."
|
||||
TextWrapping="Wrap"
|
||||
Opacity="0.75" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="Destinazione attuale:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceMatcherDestinationPathTextBox" Text="{Binding DestinationPath, Mode=OneWay}" IsReadOnly="True" />
|
||||
<Button Grid.Column="2" Click="OpenFaceDestinationFolder_Click" Width="72" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
|
||||
<TextBlock Text="Apri" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,6,0,0">
|
||||
<Button Name="FaceRunButton" Command="{Binding StartFaceEncoderCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<ProgressBar Width="18"
|
||||
Height="18"
|
||||
IsIndeterminate="True"
|
||||
IsVisible="{Binding IsFaceEncoderRunning}"
|
||||
ShowProgressText="False"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="Esegui Face Encoder" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Content="Stop" Command="{Binding StopFaceEncoderCommand}" />
|
||||
<TextBlock VerticalAlignment="Center" Text="{Binding FaceStatusMessage}" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="Encodings e file di appoggio" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="Encodings .pkl:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceMatcherEncodingsPathTextBox" Text="{Binding FaceMatcherEncodingsPath, Mode=TwoWay}" Watermark="Se vuoto usa l'ultimo .pkl in output encodings" />
|
||||
<Button Grid.Column="2" Click="SelectFaceMatcherEncodings_Click" Width="104" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="FileCodeOutline" Width="14" Height="14" />
|
||||
<TextBlock Text="Scegli..." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Grid.Column="3" Click="OpenFaceMatcherEncodings_Click" Width="72" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
|
||||
<TextBlock Text="Apri" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Output comando" FontWeight="Bold" Margin="0,6,0,0" />
|
||||
<TextBox Name="FaceOutputTextBox"
|
||||
Text="{Binding FaceCommandOutput}"
|
||||
IsReadOnly="True"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap"
|
||||
FontFamily="Cascadia Mono, Consolas, monospace"
|
||||
Height="220"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Auto" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="Risultato CSV:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceMatcherOutputPathTextBox" Text="{Binding FaceMatcherOutputPath, Mode=TwoWay}" Watermark="Opzionale: file/cartella output CSV" />
|
||||
<Button Grid.Column="2" Click="SelectFaceMatcherOutput_Click" Width="104" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="FileDelimitedOutline" Width="14" Height="14" />
|
||||
<TextBlock Text="Scegli..." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Grid.Column="3" Click="OpenFaceMatcherOutput_Click" Width="72" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
|
||||
<TextBlock Text="Apri" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="Log matcher:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceMatcherLogPathTextBox" Text="{Binding FaceMatcherLogPath, Mode=TwoWay}" Watermark="Opzionale: file/cartella log txt" />
|
||||
<Button Grid.Column="2" Click="SelectFaceMatcherLog_Click" Width="104" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="FileDocumentOutline" Width="14" Height="14" />
|
||||
<TextBlock Text="Scegli..." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Grid.Column="3" Click="OpenFaceMatcherLog_Click" Width="72" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
|
||||
<TextBlock Text="Apri" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="Auto,100,*" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="Tolleranza:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Text="{Binding FaceMatcherTolerance, Mode=TwoWay}" Watermark="0.50" />
|
||||
<TextBlock Grid.Column="2" Text="Range utile: 0.35 - 0.75. Più bassa = match più restrittivo, più alta = più permissivo." VerticalAlignment="Center" TextWrapping="Wrap" Opacity="0.75" />
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,6,0,0">
|
||||
<Button Command="{Binding StartFaceMatcherCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<ProgressBar Width="18"
|
||||
Height="18"
|
||||
IsIndeterminate="True"
|
||||
IsVisible="{Binding IsFaceMatcherRunning}"
|
||||
ShowProgressText="False"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="Avvia Face Matcher" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Content="Stop" Command="{Binding StopFaceMatcherCommand}" />
|
||||
<TextBlock VerticalAlignment="Center" Text="{Binding FaceMatcherStatusMessage}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Output comando" FontWeight="Bold" Margin="0,6,0,0" />
|
||||
<TextBox Name="FaceMatcherOutputTextBox"
|
||||
Text="{Binding FaceMatcherCommandOutput}"
|
||||
IsReadOnly="True"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap"
|
||||
FontFamily="Cascadia Mono, Consolas, monospace"
|
||||
Height="220"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Auto" />
|
||||
|
||||
<TextBlock Text="Risultati" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<avaloniaDataGrid:DataGrid ItemsSource="{Binding FaceMatcherResults}"
|
||||
IsReadOnly="True"
|
||||
AutoGenerateColumns="False"
|
||||
CanUserResizeColumns="True"
|
||||
DoubleTapped="FaceMatcherResults_DoubleTapped"
|
||||
MinHeight="220">
|
||||
<avaloniaDataGrid:DataGrid.Columns>
|
||||
<avaloniaDataGrid:DataGridTextColumn Header="File" Binding="{Binding PhotoId}" Width="2*" />
|
||||
<avaloniaDataGrid:DataGridTextColumn Header="Score" Binding="{Binding ScoreDisplay}" Width="Auto" />
|
||||
<avaloniaDataGrid:DataGridTextColumn Header="Trovato in destinazione" Binding="{Binding ResolvedImagePath}" Width="3*" />
|
||||
<avaloniaDataGrid:DataGridTemplateColumn Header="Candidati" Width="Auto">
|
||||
<avaloniaDataGrid:DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding CandidateCount}" Margin="10,0,0,0" />
|
||||
</DataTemplate>
|
||||
</avaloniaDataGrid:DataGridTemplateColumn.CellTemplate>
|
||||
</avaloniaDataGrid:DataGridTemplateColumn>
|
||||
<avaloniaDataGrid:DataGridTextColumn Header="Debug" Binding="{Binding DebugSummary}" Width="4*" />
|
||||
</avaloniaDataGrid:DataGrid.Columns>
|
||||
</avaloniaDataGrid:DataGrid>
|
||||
|
||||
<TextBlock Text="Gallery match risolti" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<ItemsControl ItemsSource="{Binding FaceMatcherResults}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Button Click="OpenFaceMatcherPreview_Click"
|
||||
Tag="{Binding}"
|
||||
Width="184"
|
||||
Margin="0,0,8,8"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch">
|
||||
<StackPanel Spacing="4">
|
||||
<Border BorderThickness="1" BorderBrush="#808080" Padding="4">
|
||||
<Image Width="168"
|
||||
Height="112"
|
||||
Stretch="UniformToFill"
|
||||
Source="{Binding ResolvedImagePath, Converter={StaticResource FilePathToBitmapConverter}}" />
|
||||
</Border>
|
||||
<TextBlock Text="{Binding PhotoId}" FontWeight="SemiBold" TextWrapping="Wrap" />
|
||||
<TextBlock Text="{Binding ScoreDisplay}" Opacity="0.8" />
|
||||
<TextBlock Text="{Binding ResolvedImagePath}" TextWrapping="Wrap" MaxHeight="36" Opacity="0.8" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</UserControl>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,32 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using ImageCatalog_2.Models;
|
||||
using ImageCatalog_2.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ImageCatalog_2.AvaloniaViews;
|
||||
|
||||
public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
||||
{
|
||||
private INotifyPropertyChanged? _faceAiPropertySource;
|
||||
private readonly PickerPreferenceService _pickerPreferenceService;
|
||||
|
||||
public FaceAiTabView()
|
||||
{
|
||||
_pickerPreferenceService = Program.ServiceProvider.GetRequiredService<PickerPreferenceService>();
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
}
|
||||
|
|
@ -35,12 +47,21 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
|||
|
||||
private void OnFaceAiPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (!string.Equals(e.PropertyName, nameof(DataModel.FaceCommandOutput), StringComparison.Ordinal))
|
||||
if (string.Equals(e.PropertyName, nameof(DataModel.FaceCommandOutput), StringComparison.Ordinal))
|
||||
{
|
||||
ScrollOutputTextBoxToEnd("FaceOutputTextBox");
|
||||
return;
|
||||
}
|
||||
|
||||
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputTextBox");
|
||||
if (string.Equals(e.PropertyName, nameof(DataModel.FaceMatcherCommandOutput), StringComparison.Ordinal))
|
||||
{
|
||||
ScrollOutputTextBoxToEnd("FaceMatcherOutputTextBox");
|
||||
}
|
||||
}
|
||||
|
||||
private void ScrollOutputTextBoxToEnd(string controlName)
|
||||
{
|
||||
var outputBox = this.FindControl<Avalonia.Controls.TextBox>(controlName);
|
||||
if (outputBox is null)
|
||||
{
|
||||
return;
|
||||
|
|
@ -55,129 +76,121 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
|||
|
||||
private async void SelectFaceExecutable_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var executableBox = this.FindControl<Avalonia.Controls.TextBox>("FaceExecutablePathTextBox");
|
||||
if (executableBox is null)
|
||||
var currentPath = DataContext is DataModel currentModel ? currentModel.FaceExecutablePath : null;
|
||||
var folders = await OpenFolderPickerAsync("Seleziona la cartella Face Recognition Windows", PickerPreferenceKeys.FaceExecutableFolder, currentPath);
|
||||
if (folders.Count > 0 && DataContext is DataModel model)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
var storageProvider = topLevel?.StorageProvider;
|
||||
if (storageProvider is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var folders = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Seleziona la cartella Face Recognition Windows"
|
||||
});
|
||||
|
||||
if (folders.Count > 0)
|
||||
{
|
||||
executableBox.Text = folders[0].Path.LocalPath;
|
||||
if (DataContext is DataModel model)
|
||||
{
|
||||
model.FaceExecutablePath = executableBox.Text;
|
||||
}
|
||||
model.FaceExecutablePath = folders[0].Path.LocalPath;
|
||||
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.FaceExecutableFolder, model.FaceExecutablePath);
|
||||
}
|
||||
}
|
||||
|
||||
private async void SelectFaceOutputFolder_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputFolderTextBox");
|
||||
if (outputBox is null)
|
||||
var currentPath = DataContext is DataModel currentModel ? currentModel.FaceOutputFolderPath : null;
|
||||
var folders = await OpenFolderPickerAsync("Seleziona la cartella output per encodings e log", PickerPreferenceKeys.FaceOutputFolder, currentPath);
|
||||
if (folders.Count > 0 && DataContext is DataModel model)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
var storageProvider = topLevel?.StorageProvider;
|
||||
if (storageProvider is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var folders = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Seleziona la cartella output per encodings e log"
|
||||
});
|
||||
|
||||
if (folders.Count > 0)
|
||||
{
|
||||
outputBox.Text = folders[0].Path.LocalPath;
|
||||
if (DataContext is DataModel model)
|
||||
{
|
||||
model.FaceOutputFolderPath = outputBox.Text;
|
||||
}
|
||||
model.FaceOutputFolderPath = folders[0].Path.LocalPath;
|
||||
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.FaceOutputFolder, model.FaceOutputFolderPath);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenFaceExecutableFolder_Click(object? sender, RoutedEventArgs e)
|
||||
private async void SelectFaceMatcherExecutable_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var executableBox = this.FindControl<Avalonia.Controls.TextBox>("FaceExecutablePathTextBox");
|
||||
if (executableBox is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var files = await OpenFilePickerAsync(
|
||||
"Seleziona face_matcher.exe",
|
||||
[new FilePickerFileType("Eseguibile") { Patterns = ["*.exe"] }],
|
||||
PickerPreferenceKeys.FaceMatcherExecutable,
|
||||
DataContext is DataModel currentModel ? currentModel.FaceMatcherExecutablePath : null);
|
||||
|
||||
var path = executableBox.Text?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
if (files.Count > 0 && DataContext is DataModel model)
|
||||
{
|
||||
return;
|
||||
model.FaceMatcherExecutablePath = files[0].Path.LocalPath;
|
||||
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.FaceMatcherExecutable, model.FaceMatcherExecutablePath);
|
||||
}
|
||||
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
OpenInExplorer(path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
OpenInExplorer(path);
|
||||
return;
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(path);
|
||||
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? path : directory);
|
||||
}
|
||||
|
||||
private void OpenFaceOutputFolder_Click(object? sender, RoutedEventArgs e)
|
||||
private async void SelectFaceMatcherImage_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputFolderTextBox");
|
||||
if (outputBox is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var files = await OpenFilePickerAsync(
|
||||
"Seleziona immagine per il match",
|
||||
[new FilePickerFileType("Immagini") { Patterns = ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] }],
|
||||
PickerPreferenceKeys.FaceMatcherImage,
|
||||
DataContext is DataModel currentModel ? currentModel.FaceMatcherSelectedImagePath : null);
|
||||
|
||||
var outputPath = outputBox.Text?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(outputPath))
|
||||
if (files.Count > 0 && DataContext is DataModel model)
|
||||
{
|
||||
return;
|
||||
model.FaceMatcherSelectedImagePath = files[0].Path.LocalPath;
|
||||
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.FaceMatcherImage, model.FaceMatcherSelectedImagePath);
|
||||
}
|
||||
|
||||
if (Directory.Exists(outputPath))
|
||||
{
|
||||
OpenInExplorer(outputPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (File.Exists(outputPath))
|
||||
{
|
||||
OpenInExplorer(outputPath);
|
||||
return;
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(outputPath);
|
||||
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? outputPath : directory);
|
||||
}
|
||||
|
||||
private async void SelectFaceMatcherEncodings_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var files = await OpenFilePickerAsync(
|
||||
"Seleziona file encodings .pkl",
|
||||
[new FilePickerFileType("Encodings") { Patterns = ["*.pkl"] }],
|
||||
PickerPreferenceKeys.FaceMatcherEncodings,
|
||||
DataContext is DataModel currentModel ? currentModel.FaceMatcherEncodingsPath : null);
|
||||
|
||||
if (files.Count > 0 && DataContext is DataModel model)
|
||||
{
|
||||
model.FaceMatcherEncodingsPath = files[0].Path.LocalPath;
|
||||
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.FaceMatcherEncodings, model.FaceMatcherEncodingsPath);
|
||||
}
|
||||
}
|
||||
|
||||
private async void SelectFaceMatcherOutput_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var file = await SaveFilePickerAsync(
|
||||
"Seleziona output CSV del matcher",
|
||||
"csv",
|
||||
[new FilePickerFileType("CSV") { Patterns = ["*.csv"] }],
|
||||
PickerPreferenceKeys.FaceMatcherOutput,
|
||||
DataContext is DataModel currentModel ? currentModel.FaceMatcherOutputPath : null);
|
||||
|
||||
if (file is not null && DataContext is DataModel model)
|
||||
{
|
||||
model.FaceMatcherOutputPath = file.Path.LocalPath;
|
||||
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.FaceMatcherOutput, model.FaceMatcherOutputPath);
|
||||
}
|
||||
}
|
||||
|
||||
private async void SelectFaceMatcherLog_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var file = await SaveFilePickerAsync(
|
||||
"Seleziona log TXT del matcher",
|
||||
"txt",
|
||||
[new FilePickerFileType("Log") { Patterns = ["*.txt", "*.log"] }],
|
||||
PickerPreferenceKeys.FaceMatcherLog,
|
||||
DataContext is DataModel currentModel ? currentModel.FaceMatcherLogPath : null);
|
||||
|
||||
if (file is not null && DataContext is DataModel model)
|
||||
{
|
||||
model.FaceMatcherLogPath = file.Path.LocalPath;
|
||||
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.FaceMatcherLog, model.FaceMatcherLogPath);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenFaceExecutableFolder_Click(object? sender, RoutedEventArgs e) => OpenFromTextBox("FaceExecutablePathTextBox");
|
||||
|
||||
private void OpenFaceOutputFolder_Click(object? sender, RoutedEventArgs e) => OpenFromTextBox("FaceOutputFolderTextBox");
|
||||
|
||||
private void OpenFaceMatcherExecutable_Click(object? sender, RoutedEventArgs e) => OpenFromTextBox("FaceMatcherExecutablePathTextBox");
|
||||
|
||||
private void OpenFaceMatcherImage_Click(object? sender, RoutedEventArgs e) => OpenFromTextBox("FaceMatcherImagePathTextBox");
|
||||
|
||||
private void OpenFaceMatcherEncodings_Click(object? sender, RoutedEventArgs e) => OpenFromTextBox("FaceMatcherEncodingsPathTextBox");
|
||||
|
||||
private void OpenFaceMatcherOutput_Click(object? sender, RoutedEventArgs e) => OpenFromTextBox("FaceMatcherOutputPathTextBox");
|
||||
|
||||
private void OpenFaceMatcherLog_Click(object? sender, RoutedEventArgs e) => OpenFromTextBox("FaceMatcherLogPathTextBox");
|
||||
|
||||
private void OpenFaceDestinationFolder_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var destBox = this.FindControl<Avalonia.Controls.TextBox>("FaceDestinationPathTextBox");
|
||||
string? path = destBox?.Text?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(path) && DataContext is DataModel model)
|
||||
string? path = null;
|
||||
if (DataContext is DataModel model)
|
||||
{
|
||||
path = (model.DestinationPath ?? string.Empty).Trim();
|
||||
}
|
||||
|
|
@ -197,6 +210,335 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
|||
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? path : directory);
|
||||
}
|
||||
|
||||
private async void OpenFaceMatcherPreview_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button { Tag: FaceMatcherResultItem item })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await OpenFaceMatcherPreviewAsync(item);
|
||||
}
|
||||
|
||||
private async Task OpenFaceMatcherPreviewAsync(FaceMatcherResultItem item)
|
||||
{
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
var dialog = BuildFaceMatcherPreviewDialog(item);
|
||||
if (owner is not null)
|
||||
{
|
||||
await dialog.ShowDialog(owner);
|
||||
return;
|
||||
}
|
||||
|
||||
dialog.Show();
|
||||
}
|
||||
|
||||
private async void FaceMatcherResults_DoubleTapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
if (sender is not Avalonia.Controls.DataGrid { SelectedItem: FaceMatcherResultItem item })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await OpenFaceMatcherPreviewAsync(item);
|
||||
}
|
||||
|
||||
private Window BuildFaceMatcherPreviewDialog(FaceMatcherResultItem item)
|
||||
{
|
||||
var dialog = new Window
|
||||
{
|
||||
Title = $"Preview match: {item.PhotoId}",
|
||||
Width = 1180,
|
||||
Height = 900,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner
|
||||
};
|
||||
|
||||
Bitmap? bitmap = null;
|
||||
var dimensionText = "n/d";
|
||||
if (!string.IsNullOrWhiteSpace(item.ResolvedImagePath) && File.Exists(item.ResolvedImagePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
bitmap = new Bitmap(item.ResolvedImagePath);
|
||||
dimensionText = $"{bitmap.PixelSize.Width} x {bitmap.PixelSize.Height}px";
|
||||
}
|
||||
catch
|
||||
{
|
||||
bitmap = null;
|
||||
}
|
||||
}
|
||||
|
||||
var fileInfo = !string.IsNullOrWhiteSpace(item.ResolvedImagePath) && File.Exists(item.ResolvedImagePath)
|
||||
? new FileInfo(item.ResolvedImagePath)
|
||||
: null;
|
||||
|
||||
var debugBuilder = new StringBuilder();
|
||||
debugBuilder.AppendLine($"File matcher: {item.PhotoId}");
|
||||
debugBuilder.AppendLine($"Score: {item.ScoreDisplay}");
|
||||
debugBuilder.AppendLine($"Path risolto: {item.ResolvedImagePath}");
|
||||
debugBuilder.AppendLine($"Candidati trovati in destinazione: {item.CandidateCount}");
|
||||
debugBuilder.AppendLine($"Dimensioni immagine: {dimensionText}");
|
||||
if (fileInfo is not null)
|
||||
{
|
||||
debugBuilder.AppendLine($"Dimensione file: {fileInfo.Length / 1024.0:F1} KB");
|
||||
debugBuilder.AppendLine($"Ultima modifica: {fileInfo.LastWriteTime:yyyy-MM-dd HH:mm:ss}");
|
||||
}
|
||||
|
||||
debugBuilder.AppendLine($"Immagine ricerca: {item.SearchImagePath}");
|
||||
debugBuilder.AppendLine($"CSV risultati: {item.CsvPath}");
|
||||
debugBuilder.AppendLine($"Log matcher: {item.LogPath}");
|
||||
if (!string.IsNullOrWhiteSpace(item.DebugSummary))
|
||||
{
|
||||
debugBuilder.AppendLine($"Dettagli riga: {item.DebugSummary}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(item.RawRow))
|
||||
{
|
||||
debugBuilder.AppendLine($"Raw CSV: {item.RawRow}");
|
||||
}
|
||||
|
||||
var layout = new Grid
|
||||
{
|
||||
Margin = new Avalonia.Thickness(16),
|
||||
RowDefinitions = new RowDefinitions("Auto,*,Auto")
|
||||
};
|
||||
|
||||
var header = new StackPanel { Spacing = 6 };
|
||||
header.Children.Add(new TextBlock
|
||||
{
|
||||
Text = item.PhotoId,
|
||||
FontWeight = FontWeight.Bold,
|
||||
FontSize = 18
|
||||
});
|
||||
header.Children.Add(new TextBlock
|
||||
{
|
||||
Text = string.IsNullOrWhiteSpace(item.ScoreDisplay)
|
||||
? "Score: n/d"
|
||||
: $"Score: {item.ScoreDisplay}%",
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Opacity = 0.9
|
||||
});
|
||||
header.Children.Add(new TextBlock
|
||||
{
|
||||
Text = string.IsNullOrWhiteSpace(item.ResolvedImagePath)
|
||||
? "Nessun file immagine risolto nella cartella Destinazione."
|
||||
: item.ResolvedImagePath,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Opacity = 0.8
|
||||
});
|
||||
layout.Children.Add(header);
|
||||
|
||||
var contentGrid = new Grid
|
||||
{
|
||||
Margin = new Avalonia.Thickness(0, 12, 0, 12),
|
||||
RowDefinitions = new RowDefinitions("Auto,*,Auto"),
|
||||
RowSpacing = 12
|
||||
};
|
||||
Grid.SetRow(contentGrid, 1);
|
||||
|
||||
var zoomLevel = 1.0;
|
||||
var zoomText = new TextBlock
|
||||
{
|
||||
Text = "100%",
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
MinWidth = 52,
|
||||
TextAlignment = TextAlignment.Center
|
||||
};
|
||||
|
||||
var imageControl = bitmap is null
|
||||
? null
|
||||
: new Image
|
||||
{
|
||||
Source = bitmap,
|
||||
Stretch = Stretch.None,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
RenderTransform = new ScaleTransform(1, 1)
|
||||
};
|
||||
|
||||
void UpdateZoom(double delta)
|
||||
{
|
||||
if (imageControl is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
zoomLevel = Math.Clamp(zoomLevel + delta, 0.1, 8.0);
|
||||
imageControl.RenderTransform = new ScaleTransform(zoomLevel, zoomLevel);
|
||||
zoomText.Text = $"{zoomLevel * 100:0}%";
|
||||
}
|
||||
|
||||
var toolbar = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Spacing = 8
|
||||
};
|
||||
|
||||
var zoomOutButton = new Button { Content = "Zoom -", MinWidth = 80, IsEnabled = imageControl is not null };
|
||||
zoomOutButton.Click += (_, _) => UpdateZoom(-0.1);
|
||||
toolbar.Children.Add(zoomOutButton);
|
||||
|
||||
var zoomInButton = new Button { Content = "Zoom +", MinWidth = 80, IsEnabled = imageControl is not null };
|
||||
zoomInButton.Click += (_, _) => UpdateZoom(0.1);
|
||||
toolbar.Children.Add(zoomInButton);
|
||||
|
||||
var resetZoomButton = new Button { Content = "100%", MinWidth = 72, IsEnabled = imageControl is not null };
|
||||
resetZoomButton.Click += (_, _) =>
|
||||
{
|
||||
if (imageControl is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
zoomLevel = 1.0;
|
||||
imageControl.RenderTransform = new ScaleTransform(1, 1);
|
||||
zoomText.Text = "100%";
|
||||
};
|
||||
toolbar.Children.Add(resetZoomButton);
|
||||
toolbar.Children.Add(zoomText);
|
||||
contentGrid.Children.Add(toolbar);
|
||||
|
||||
var previewBorder = new Border
|
||||
{
|
||||
BorderBrush = Brushes.Gray,
|
||||
BorderThickness = new Avalonia.Thickness(1),
|
||||
Padding = new Avalonia.Thickness(8),
|
||||
Child = new ScrollViewer
|
||||
{
|
||||
HorizontalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto,
|
||||
VerticalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto,
|
||||
Content = bitmap is null
|
||||
? new TextBlock
|
||||
{
|
||||
Text = "Anteprima non disponibile",
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
}
|
||||
: imageControl
|
||||
}
|
||||
};
|
||||
Grid.SetRow(previewBorder, 1);
|
||||
contentGrid.Children.Add(previewBorder);
|
||||
|
||||
var debugBox = new TextBox
|
||||
{
|
||||
Text = debugBuilder.ToString(),
|
||||
IsReadOnly = true,
|
||||
AcceptsReturn = true,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
FontFamily = new FontFamily("Cascadia Mono, Consolas, monospace"),
|
||||
MinHeight = 180
|
||||
};
|
||||
Grid.SetRow(debugBox, 2);
|
||||
contentGrid.Children.Add(debugBox);
|
||||
|
||||
layout.Children.Add(contentGrid);
|
||||
|
||||
var footer = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Spacing = 8
|
||||
};
|
||||
Grid.SetRow(footer, 2);
|
||||
|
||||
var openFileButton = new Button { Content = "Apri file" };
|
||||
openFileButton.Click += (_, _) => OpenInExplorer(item.ResolvedImagePath);
|
||||
footer.Children.Add(openFileButton);
|
||||
|
||||
var openFolderButton = new Button { Content = "Apri cartella" };
|
||||
openFolderButton.Click += (_, _) =>
|
||||
{
|
||||
var directory = Path.GetDirectoryName(item.ResolvedImagePath);
|
||||
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? item.ResolvedImagePath : directory);
|
||||
};
|
||||
footer.Children.Add(openFolderButton);
|
||||
|
||||
var closeButton = new Button { Content = "Chiudi", MinWidth = 88 };
|
||||
closeButton.Click += (_, _) => dialog.Close();
|
||||
footer.Children.Add(closeButton);
|
||||
|
||||
layout.Children.Add(footer);
|
||||
dialog.Content = layout;
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private void OpenFromTextBox(string textBoxName)
|
||||
{
|
||||
var textBox = this.FindControl<Avalonia.Controls.TextBox>(textBoxName);
|
||||
var path = textBox?.Text?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Directory.Exists(path) || File.Exists(path))
|
||||
{
|
||||
OpenInExplorer(path);
|
||||
return;
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(path);
|
||||
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? path : directory);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(string title, string preferenceKey, string? currentPath)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
var storageProvider = topLevel?.StorageProvider;
|
||||
if (storageProvider is null)
|
||||
{
|
||||
return Array.Empty<IStorageFolder>();
|
||||
}
|
||||
|
||||
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(storageProvider, preferenceKey, currentPath);
|
||||
|
||||
return await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = title,
|
||||
SuggestedStartLocation = suggestedStartLocation
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(string title, IReadOnlyList<FilePickerFileType> fileTypes, string preferenceKey, string? currentPath)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
var storageProvider = topLevel?.StorageProvider;
|
||||
if (storageProvider is null)
|
||||
{
|
||||
return Array.Empty<IStorageFile>();
|
||||
}
|
||||
|
||||
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(storageProvider, preferenceKey, currentPath);
|
||||
|
||||
return await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = title,
|
||||
FileTypeFilter = fileTypes,
|
||||
SuggestedStartLocation = suggestedStartLocation
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<IStorageFile?> SaveFilePickerAsync(string title, string defaultExtension, IReadOnlyList<FilePickerFileType> fileTypes, string preferenceKey, string? currentPath)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
var storageProvider = topLevel?.StorageProvider;
|
||||
if (storageProvider is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(storageProvider, preferenceKey, currentPath);
|
||||
|
||||
return await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = title,
|
||||
DefaultExtension = defaultExtension,
|
||||
FileTypeChoices = fileTypes,
|
||||
SuggestedStartLocation = suggestedStartLocation
|
||||
});
|
||||
}
|
||||
|
||||
private static void OpenInExplorer(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
|
|
|
|||
31
imagecatalog/Converters/FilePathToBitmapConverter.cs
Normal file
31
imagecatalog/Converters/FilePathToBitmapConverter.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media.Imaging;
|
||||
|
||||
namespace ImageCatalog_2.Converters;
|
||||
|
||||
public sealed class FilePathToBitmapConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not string path || string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return new Bitmap(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
24
imagecatalog/Models/FaceMatcherResultItem.cs
Normal file
24
imagecatalog/Models/FaceMatcherResultItem.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
namespace ImageCatalog_2.Models;
|
||||
|
||||
public sealed class FaceMatcherResultItem
|
||||
{
|
||||
public string PhotoId { get; init; } = string.Empty;
|
||||
|
||||
public double? Score { get; init; }
|
||||
|
||||
public string ScoreDisplay => Score.HasValue ? Score.Value.ToString("0.###") : string.Empty;
|
||||
|
||||
public string ResolvedImagePath { get; init; } = string.Empty;
|
||||
|
||||
public int CandidateCount { get; init; }
|
||||
|
||||
public string RawRow { get; init; } = string.Empty;
|
||||
|
||||
public string DebugSummary { get; init; } = string.Empty;
|
||||
|
||||
public string SearchImagePath { get; init; } = string.Empty;
|
||||
|
||||
public string CsvPath { get; init; } = string.Empty;
|
||||
|
||||
public string LogPath { get; init; } = string.Empty;
|
||||
}
|
||||
|
|
@ -314,6 +314,14 @@ namespace ImageCatalog_2.Models
|
|||
[XmlElement("AI_FaceUpsample")]
|
||||
public bool FaceUpsample { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("FaceMatcherExecutablePath")]
|
||||
[XmlElement("AI_FaceMatcherExecutablePath")]
|
||||
public string FaceMatcherExecutablePath { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("FaceMatcherTolerance")]
|
||||
[XmlElement("AI_FaceMatcherTolerance")]
|
||||
public double FaceMatcherTolerance { get; set; } = 0.5;
|
||||
|
||||
// Race upload settings
|
||||
[JsonPropertyName("ApiLogin")]
|
||||
[XmlElement("RaceUpload_Login")]
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ static class Program
|
|||
var userPrefsPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"ImageCatalog", "userprefs.xml");
|
||||
services.AddSingleton(new ParametriSetup(userPrefsPath));
|
||||
services.AddSingleton<PickerPreferenceService>();
|
||||
services.AddSingleton<PicSettings>();
|
||||
|
||||
services.AddCatalogCommunication(options =>
|
||||
|
|
|
|||
93
imagecatalog/Services/PickerPreferenceService.cs
Normal file
93
imagecatalog/Services/PickerPreferenceService.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
using Avalonia.Platform.Storage;
|
||||
using ImageCatalog;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ImageCatalog_2.Services;
|
||||
|
||||
public static class PickerPreferenceKeys
|
||||
{
|
||||
public const string SourceFolder = "Picker.SourceFolder.LastPath";
|
||||
public const string DestinationFolder = "Picker.DestinationFolder.LastPath";
|
||||
public const string LogoFile = "Picker.LogoFile.LastPath";
|
||||
public const string ModelsFolder = "Picker.ModelsFolder.LastPath";
|
||||
public const string CsvOutput = "Picker.CsvOutput.LastPath";
|
||||
public const string FaceExecutableFolder = "Picker.FaceExecutableFolder.LastPath";
|
||||
public const string FaceOutputFolder = "Picker.FaceOutputFolder.LastPath";
|
||||
public const string FaceMatcherExecutable = "Picker.FaceMatcherExecutable.LastPath";
|
||||
public const string FaceMatcherImage = "Picker.FaceMatcherImage.LastPath";
|
||||
public const string FaceMatcherEncodings = "Picker.FaceMatcherEncodings.LastPath";
|
||||
public const string FaceMatcherOutput = "Picker.FaceMatcherOutput.LastPath";
|
||||
public const string FaceMatcherLog = "Picker.FaceMatcherLog.LastPath";
|
||||
}
|
||||
|
||||
public sealed class PickerPreferenceService
|
||||
{
|
||||
private readonly ParametriSetup _userPreferences;
|
||||
|
||||
public PickerPreferenceService(ParametriSetup userPreferences)
|
||||
{
|
||||
_userPreferences = userPreferences;
|
||||
}
|
||||
|
||||
public async Task<IStorageFolder?> TryGetStartFolderAsync(IStorageProvider storageProvider, string preferenceKey, string? currentPath = null)
|
||||
{
|
||||
var startPath = GetPreferredStartDirectory(preferenceKey, currentPath);
|
||||
if (string.IsNullOrWhiteSpace(startPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await storageProvider.TryGetFolderFromPathAsync(new Uri(startPath)).ConfigureAwait(true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void RememberPath(string preferenceKey, string? selectedPath)
|
||||
{
|
||||
var directory = TryGetExistingDirectory(selectedPath);
|
||||
if (string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_userPreferences.AggiornaParametro(preferenceKey, directory);
|
||||
_userPreferences.SalvaParametriSetup();
|
||||
}
|
||||
|
||||
private string? GetPreferredStartDirectory(string preferenceKey, string? currentPath)
|
||||
{
|
||||
var storedPath = _userPreferences.LeggiParametroString(preferenceKey);
|
||||
return TryGetExistingDirectory(storedPath) ?? TryGetExistingDirectory(currentPath);
|
||||
}
|
||||
|
||||
private static string? TryGetExistingDirectory(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalizedPath = path.Trim().Trim('"');
|
||||
if (Directory.Exists(normalizedPath))
|
||||
{
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
if (File.Exists(normalizedPath))
|
||||
{
|
||||
return Path.GetDirectoryName(normalizedPath);
|
||||
}
|
||||
|
||||
var containingDirectory = Path.GetDirectoryName(normalizedPath);
|
||||
return !string.IsNullOrWhiteSpace(containingDirectory) && Directory.Exists(containingDirectory)
|
||||
? containingDirectory
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
|
@ -225,6 +225,105 @@ public class AiSettingsViewModel : ViewModelBase
|
|||
}
|
||||
}
|
||||
|
||||
private string _faceMatcherExecutablePath = string.Empty;
|
||||
public string FaceMatcherExecutablePath
|
||||
{
|
||||
get => _faceMatcherExecutablePath;
|
||||
set
|
||||
{
|
||||
_faceMatcherExecutablePath = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _faceMatcherEncodingsPath = string.Empty;
|
||||
public string FaceMatcherEncodingsPath
|
||||
{
|
||||
get => _faceMatcherEncodingsPath;
|
||||
set
|
||||
{
|
||||
_faceMatcherEncodingsPath = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _faceMatcherOutputPath = string.Empty;
|
||||
public string FaceMatcherOutputPath
|
||||
{
|
||||
get => _faceMatcherOutputPath;
|
||||
set
|
||||
{
|
||||
_faceMatcherOutputPath = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _faceMatcherLogPath = string.Empty;
|
||||
public string FaceMatcherLogPath
|
||||
{
|
||||
get => _faceMatcherLogPath;
|
||||
set
|
||||
{
|
||||
_faceMatcherLogPath = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private double _faceMatcherTolerance = 0.5;
|
||||
public double FaceMatcherTolerance
|
||||
{
|
||||
get => _faceMatcherTolerance;
|
||||
set
|
||||
{
|
||||
_faceMatcherTolerance = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _faceMatcherSelectedImagePath = string.Empty;
|
||||
public string FaceMatcherSelectedImagePath
|
||||
{
|
||||
get => _faceMatcherSelectedImagePath;
|
||||
set
|
||||
{
|
||||
_faceMatcherSelectedImagePath = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isFaceMatcherRunning;
|
||||
public bool IsFaceMatcherRunning
|
||||
{
|
||||
get => _isFaceMatcherRunning;
|
||||
set
|
||||
{
|
||||
_isFaceMatcherRunning = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _faceMatcherStatusMessage = string.Empty;
|
||||
public string FaceMatcherStatusMessage
|
||||
{
|
||||
get => _faceMatcherStatusMessage;
|
||||
set
|
||||
{
|
||||
_faceMatcherStatusMessage = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _faceMatcherCommandOutput = string.Empty;
|
||||
public string FaceMatcherCommandOutput
|
||||
{
|
||||
get => _faceMatcherCommandOutput;
|
||||
set
|
||||
{
|
||||
_faceMatcherCommandOutput = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private double _aiProgress;
|
||||
public double AiProgress
|
||||
{
|
||||
|
|
@ -237,4 +336,6 @@ public class AiSettingsViewModel : ViewModelBase
|
|||
}
|
||||
|
||||
public ObservableCollection<AiResultItem> PreviewResults { get; } = new();
|
||||
|
||||
public ObservableCollection<FaceMatcherResultItem> FaceMatcherResults { get; } = new();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue