feat: Add face encoder settings including GPU support, parallelism, and thumbnail options
This commit is contained in:
parent
d6b778a648
commit
25fdb82d2f
9 changed files with 595 additions and 136 deletions
|
|
@ -5,16 +5,16 @@
|
|||
<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 un file .pkl come --out."
|
||||
<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="Eseguibile" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<TextBlock Text="Cartella Face Encoder" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="face_encoder:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceExecutablePathTextBox" Text="{Binding FaceExecutablePath, Mode=TwoWay}" Watermark="C:\\tools\\face_encoder_cpu.exe" />
|
||||
<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="FileOutline" Width="14" Height="14" />
|
||||
<iconPacks:PackIconMaterial Kind="FolderOutline" Width="14" Height="14" />
|
||||
<TextBlock Text="Scegli..." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
|
@ -28,14 +28,24 @@
|
|||
|
||||
<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="Se l'eseguibile termina con _cpu o _gpu, il checkbox GPU cambia automaticamente il file usato. Se non c'e il suffisso, viene trattato come CPU."
|
||||
<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" />
|
||||
|
|
@ -49,11 +59,11 @@
|
|||
|
||||
<TextBlock Text="Output encodings" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="File out (.pkl):" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceOutputFolderTextBox" Text="{Binding FaceOutputFolderPath, Mode=TwoWay}" Watermark="C:\\output\\encodings.pkl" />
|
||||
<Button Grid.Column="2" Name="FaceSelectOutputButton" Click="SelectFaceOutputFile_Click" Width="104" Margin="6,0,0,0">
|
||||
<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="FileOutline" Width="14" Height="14" />
|
||||
<iconPacks:PackIconMaterial Kind="FolderOutline" Width="14" Height="14" />
|
||||
<TextBlock Text="Scegli..." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
|
@ -64,9 +74,22 @@
|
|||
</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" Content="Esegui Face Encoder" Command="{Binding StartFaceEncoderCommand}" />
|
||||
<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>
|
||||
|
|
@ -77,7 +100,10 @@
|
|||
IsReadOnly="True"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap"
|
||||
MinHeight="180" />
|
||||
FontFamily="Cascadia Mono, Consolas, monospace"
|
||||
Height="220"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Auto" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
|
|
@ -9,9 +11,46 @@ namespace ImageCatalog_2.AvaloniaViews;
|
|||
|
||||
public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
||||
{
|
||||
private INotifyPropertyChanged? _faceAiPropertySource;
|
||||
|
||||
public FaceAiTabView()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (_faceAiPropertySource is not null)
|
||||
{
|
||||
_faceAiPropertySource.PropertyChanged -= OnFaceAiPropertyChanged;
|
||||
}
|
||||
|
||||
_faceAiPropertySource = DataContext as INotifyPropertyChanged;
|
||||
if (_faceAiPropertySource is not null)
|
||||
{
|
||||
_faceAiPropertySource.PropertyChanged += OnFaceAiPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFaceAiPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (!string.Equals(e.PropertyName, nameof(DataModel.FaceCommandOutput), StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputTextBox");
|
||||
if (outputBox is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var textLength = outputBox.Text?.Length ?? 0;
|
||||
outputBox.CaretIndex = textLength;
|
||||
});
|
||||
}
|
||||
|
||||
private async void SelectFaceExecutable_Click(object? sender, RoutedEventArgs e)
|
||||
|
|
@ -29,19 +68,14 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
|||
return;
|
||||
}
|
||||
|
||||
var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
var folders = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Seleziona face_encoder_cpu.exe o face_encoder_gpu.exe",
|
||||
FileTypeFilter =
|
||||
[
|
||||
new FilePickerFileType("Eseguibile") { Patterns = ["*.exe"] },
|
||||
new FilePickerFileType("Tutti i file") { Patterns = ["*.*"] }
|
||||
]
|
||||
Title = "Seleziona la cartella Face Recognition Windows"
|
||||
});
|
||||
|
||||
if (files.Count > 0)
|
||||
if (folders.Count > 0)
|
||||
{
|
||||
executableBox.Text = files[0].Path.LocalPath;
|
||||
executableBox.Text = folders[0].Path.LocalPath;
|
||||
if (DataContext is DataModel model)
|
||||
{
|
||||
model.FaceExecutablePath = executableBox.Text;
|
||||
|
|
@ -49,7 +83,7 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
|||
}
|
||||
}
|
||||
|
||||
private async void SelectFaceOutputFile_Click(object? sender, RoutedEventArgs e)
|
||||
private async void SelectFaceOutputFolder_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputFolderTextBox");
|
||||
if (outputBox is null)
|
||||
|
|
@ -64,21 +98,14 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
|||
return;
|
||||
}
|
||||
|
||||
var files = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
var folders = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Seleziona file output encodings (.pkl)",
|
||||
SuggestedFileName = "encodings.pkl",
|
||||
DefaultExtension = "pkl",
|
||||
FileTypeChoices =
|
||||
[
|
||||
new FilePickerFileType("Pickle file") { Patterns = ["*.pkl"] }
|
||||
],
|
||||
ShowOverwritePrompt = true
|
||||
Title = "Seleziona la cartella output per encodings e log"
|
||||
});
|
||||
|
||||
if (files is not null)
|
||||
if (folders.Count > 0)
|
||||
{
|
||||
outputBox.Text = files.Path.LocalPath;
|
||||
outputBox.Text = folders[0].Path.LocalPath;
|
||||
if (DataContext is DataModel model)
|
||||
{
|
||||
model.FaceOutputFolderPath = outputBox.Text;
|
||||
|
|
@ -100,6 +127,12 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
|||
return;
|
||||
}
|
||||
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
OpenInExplorer(path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
OpenInExplorer(path);
|
||||
|
|
@ -124,6 +157,12 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
|||
return;
|
||||
}
|
||||
|
||||
if (Directory.Exists(outputPath))
|
||||
{
|
||||
OpenInExplorer(outputPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (File.Exists(outputPath))
|
||||
{
|
||||
OpenInExplorer(outputPath);
|
||||
|
|
@ -148,6 +187,12 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
|||
return;
|
||||
}
|
||||
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
OpenInExplorer(path);
|
||||
return;
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(path);
|
||||
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? path : directory);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,8 +60,9 @@ namespace ImageCatalog_2
|
|||
private Process? _faceEncoderProcess;
|
||||
private CancellationTokenSource? _faceEncoderWatcherTokenSource;
|
||||
private Task? _faceEncoderWatcherTask;
|
||||
private CancellationTokenSource? _faceEncoderLogWatcherTokenSource;
|
||||
private Task? _faceEncoderLogWatcherTask;
|
||||
private bool _hasStartedFaceEncoderInSession;
|
||||
private bool _isUpdatingFaceExecutableSelection;
|
||||
|
||||
// ComboBox collections
|
||||
public List<string> AvailableFonts { get; }
|
||||
|
|
@ -230,6 +231,32 @@ namespace ImageCatalog_2
|
|||
set => _ai.FaceRecursive = value;
|
||||
}
|
||||
|
||||
public bool FaceIncludeThumbnails
|
||||
{
|
||||
get => _ai.FaceIncludeThumbnails;
|
||||
set => _ai.FaceIncludeThumbnails = value;
|
||||
}
|
||||
|
||||
public IReadOnlyList<int> FaceParallelismOptions { get; } = [1, 2, 3, 4, 5];
|
||||
|
||||
public int FaceParallelism
|
||||
{
|
||||
get => _ai.FaceParallelism;
|
||||
set => _ai.FaceParallelism = value;
|
||||
}
|
||||
|
||||
public int FaceMinSize
|
||||
{
|
||||
get => _ai.FaceMinSize;
|
||||
set => _ai.FaceMinSize = value;
|
||||
}
|
||||
|
||||
public bool FaceUpsample
|
||||
{
|
||||
get => _ai.FaceUpsample;
|
||||
set => _ai.FaceUpsample = value;
|
||||
}
|
||||
|
||||
public bool FaceGpuOptionEnabled => _ai.FaceGpuOptionEnabled;
|
||||
|
||||
public bool UseFaceGpu
|
||||
|
|
@ -1415,13 +1442,22 @@ namespace ImageCatalog_2
|
|||
return;
|
||||
}
|
||||
|
||||
var executablePath = NormalizeFilePathArgument(FaceExecutablePath);
|
||||
var outputFilePath = NormalizeFilePathArgument(FaceOutputFolderPath);
|
||||
var executableRootPath = NormalizeFilePathArgument(FaceExecutablePath);
|
||||
var outputFolderPath = NormalizeDirectoryPathArgument(FaceOutputFolderPath);
|
||||
var imagesFolder = NormalizeDirectoryPathArgument(DestinationPath);
|
||||
var executablePath = ResolveConfiguredFaceEncoderExecutablePath(executableRootPath, UseFaceGpu);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(executableRootPath))
|
||||
{
|
||||
FaceStatusMessage = "Percorso cartella face encoder non valido.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(executablePath) || !File.Exists(executablePath))
|
||||
{
|
||||
FaceStatusMessage = "Percorso eseguibile non valido.";
|
||||
FaceStatusMessage = UseFaceGpu
|
||||
? "Impossibile trovare face_encoder_gpu.exe nella cartella selezionata."
|
||||
: "Impossibile trovare face_encoder_cpu.exe nella cartella selezionata.";
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1431,38 +1467,33 @@ namespace ImageCatalog_2
|
|||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(outputFilePath))
|
||||
if (string.IsNullOrWhiteSpace(outputFolderPath))
|
||||
{
|
||||
FaceStatusMessage = "Inserisci il file di output .pkl.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.Equals(Path.GetExtension(outputFilePath), ".pkl", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
FaceStatusMessage = "Il file di output deve avere estensione .pkl.";
|
||||
FaceStatusMessage = "Inserisci la cartella di output per encodings e log.";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var outputDirectory = Path.GetDirectoryName(outputFilePath);
|
||||
if (!string.IsNullOrWhiteSpace(outputDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
}
|
||||
Directory.CreateDirectory(outputFolderPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unable to create face output directory for file: {OutputFilePath}", outputFilePath);
|
||||
FaceStatusMessage = "Impossibile creare la cartella del file di output.";
|
||||
_logger.LogError(ex, "Unable to create face output directory: {OutputFolderPath}", outputFolderPath);
|
||||
FaceStatusMessage = "Impossibile creare la cartella di output.";
|
||||
return;
|
||||
}
|
||||
|
||||
FaceExecutablePath = executablePath;
|
||||
FaceOutputFolderPath = outputFilePath;
|
||||
var parallelism = NormalizeFaceParallelism(FaceParallelism);
|
||||
var minSize = NormalizeFaceMinSize(FaceMinSize);
|
||||
var outputFiles = BuildFaceEncoderOutputPaths(outputFolderPath, imagesFolder, DateTime.Now);
|
||||
|
||||
FaceExecutablePath = executableRootPath;
|
||||
FaceOutputFolderPath = outputFolderPath;
|
||||
FaceCommandOutput = string.Empty;
|
||||
FaceStatusMessage = "Esecuzione face encoder in corso...";
|
||||
|
||||
var transcriptLines = new StringBuilder();
|
||||
var outputLines = new StringBuilder();
|
||||
var errorLines = new StringBuilder();
|
||||
|
||||
|
|
@ -1475,23 +1506,50 @@ namespace ImageCatalog_2
|
|||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardInput = false,
|
||||
CreateNoWindow = false,
|
||||
};
|
||||
|
||||
processStartInfo.ArgumentList.Add("--images");
|
||||
processStartInfo.ArgumentList.Add(imagesFolder);
|
||||
processStartInfo.ArgumentList.Add("--out");
|
||||
processStartInfo.ArgumentList.Add(outputFilePath);
|
||||
processStartInfo.ArgumentList.Add(outputFiles.OutputFilePath);
|
||||
processStartInfo.ArgumentList.Add("--log");
|
||||
processStartInfo.ArgumentList.Add(outputFiles.LogFilePath);
|
||||
|
||||
if (FaceRecursive)
|
||||
{
|
||||
processStartInfo.ArgumentList.Add("--recursive");
|
||||
}
|
||||
|
||||
if (FaceIncludeThumbnails)
|
||||
{
|
||||
processStartInfo.ArgumentList.Add("--include-tn");
|
||||
}
|
||||
|
||||
processStartInfo.ArgumentList.Add(UseFaceGpu ? "--multiprocess" : "--multicore");
|
||||
processStartInfo.ArgumentList.Add(parallelism.ToString());
|
||||
processStartInfo.ArgumentList.Add("--min-size");
|
||||
processStartInfo.ArgumentList.Add(minSize.ToString());
|
||||
|
||||
if (FaceUpsample)
|
||||
{
|
||||
processStartInfo.ArgumentList.Add("--upsample");
|
||||
}
|
||||
|
||||
using var process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true };
|
||||
process.OutputDataReceived += (_, args) => AppendFaceProcessOutput(outputLines, args.Data);
|
||||
process.ErrorDataReceived += (_, args) => AppendFaceProcessOutput(errorLines, args.Data);
|
||||
process.OutputDataReceived += (_, args) => AppendFaceProcessOutput(outputLines, transcriptLines, args.Data, isError: false);
|
||||
process.ErrorDataReceived += (_, args) => AppendFaceProcessOutput(errorLines, transcriptLines, args.Data, isError: true);
|
||||
process.Exited += (_, _) =>
|
||||
{
|
||||
_ = InvokeOnUiThreadAsync(() =>
|
||||
{
|
||||
if (!ComputeIsFaceEncoderRunning())
|
||||
{
|
||||
IsFaceEncoderRunning = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (!process.Start())
|
||||
{
|
||||
|
|
@ -1503,14 +1561,21 @@ namespace ImageCatalog_2
|
|||
TrackFaceEncoderProcess(process);
|
||||
await InvokeOnUiThreadAsync(() => IsFaceEncoderRunning = true).ConfigureAwait(false);
|
||||
|
||||
if (UseFaceGpu)
|
||||
{
|
||||
StartFaceEncoderLogWatcher(outputFiles.LogFilePath, outputLines, transcriptLines);
|
||||
}
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
await process.WaitForExitAsync().ConfigureAwait(false);
|
||||
|
||||
var summary = BuildFaceEncoderSummary(process.ExitCode, outputLines, errorLines);
|
||||
var summary = BuildFaceEncoderSummary(process.ExitCode, processStartInfo, outputFiles.OutputFilePath, outputFiles.LogFilePath, outputLines, errorLines);
|
||||
await InvokeOnUiThreadAsync(() =>
|
||||
{
|
||||
FaceCommandOutput = summary;
|
||||
FaceCommandOutput = string.IsNullOrWhiteSpace(FaceCommandOutput)
|
||||
? summary
|
||||
: $"{FaceCommandOutput.TrimEnd()}\n\n{summary}";
|
||||
FaceStatusMessage = process.ExitCode == 0
|
||||
? "Face encoder completato."
|
||||
: $"Face encoder terminato con errore (code {process.ExitCode}).";
|
||||
|
|
@ -1518,6 +1583,7 @@ namespace ImageCatalog_2
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine(ex);
|
||||
_logger.LogError(ex, "Face encoder execution failed.");
|
||||
await InvokeOnUiThreadAsync(() =>
|
||||
{
|
||||
|
|
@ -1527,6 +1593,7 @@ namespace ImageCatalog_2
|
|||
}
|
||||
finally
|
||||
{
|
||||
await StopFaceEncoderLogWatcherAsync().ConfigureAwait(false);
|
||||
ClearTrackedFaceEncoderProcess();
|
||||
await InvokeOnUiThreadAsync(() => IsFaceEncoderRunning = ComputeIsFaceEncoderRunning()).ConfigureAwait(false);
|
||||
}
|
||||
|
|
@ -1569,81 +1636,127 @@ namespace ImageCatalog_2
|
|||
_faceEncoderWatcherTask = WatchFaceEncoderProcessAsync(_faceEncoderWatcherTokenSource.Token);
|
||||
}
|
||||
|
||||
private void RefreshFaceExecutableCapabilities()
|
||||
private void StartFaceEncoderLogWatcher(string logFilePath, StringBuilder outputLines, StringBuilder transcriptLines)
|
||||
{
|
||||
if (_isUpdatingFaceExecutableSelection)
|
||||
_faceEncoderLogWatcherTokenSource?.Cancel();
|
||||
_faceEncoderLogWatcherTokenSource?.Dispose();
|
||||
|
||||
_faceEncoderLogWatcherTokenSource = new CancellationTokenSource();
|
||||
_faceEncoderLogWatcherTask = WatchFaceEncoderLogFileAsync(logFilePath, outputLines, transcriptLines, _faceEncoderLogWatcherTokenSource.Token);
|
||||
}
|
||||
|
||||
private async Task StopFaceEncoderLogWatcherAsync()
|
||||
{
|
||||
var tokenSource = _faceEncoderLogWatcherTokenSource;
|
||||
var task = _faceEncoderLogWatcherTask;
|
||||
|
||||
_faceEncoderLogWatcherTokenSource = null;
|
||||
_faceEncoderLogWatcherTask = null;
|
||||
|
||||
if (tokenSource is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var executableName = Path.GetFileNameWithoutExtension(NormalizeFilePathArgument(_ai.FaceExecutablePath));
|
||||
var supportsGpu = executableName.EndsWith("_cpu", StringComparison.OrdinalIgnoreCase)
|
||||
|| executableName.EndsWith("_gpu", StringComparison.OrdinalIgnoreCase);
|
||||
var useGpu = supportsGpu && executableName.EndsWith("_gpu", StringComparison.OrdinalIgnoreCase);
|
||||
try
|
||||
{
|
||||
await tokenSource.CancelAsync().ConfigureAwait(false);
|
||||
if (task is not null)
|
||||
{
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected when shutting down the watcher.
|
||||
}
|
||||
finally
|
||||
{
|
||||
tokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_ai.FaceGpuOptionEnabled = supportsGpu;
|
||||
_ai.UseFaceGpu = supportsGpu && useGpu;
|
||||
private async Task WatchFaceEncoderLogFileAsync(string logFilePath, StringBuilder outputLines, StringBuilder transcriptLines, CancellationToken token)
|
||||
{
|
||||
long filePosition = 0;
|
||||
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(logFilePath))
|
||||
{
|
||||
using var stream = new FileStream(logFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
||||
if (filePosition > stream.Length)
|
||||
{
|
||||
filePosition = 0;
|
||||
}
|
||||
|
||||
stream.Seek(filePosition, SeekOrigin.Begin);
|
||||
using var reader = new StreamReader(stream);
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var line = await reader.ReadLineAsync(token).ConfigureAwait(false);
|
||||
AppendFaceProcessOutput(outputLines, transcriptLines, line, isError: false);
|
||||
}
|
||||
|
||||
filePosition = stream.Position;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Retry on the next polling interval while the encoder is still writing.
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
// Retry on the next polling interval if the log file is transiently locked.
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(250), token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshFaceExecutableCapabilities()
|
||||
{
|
||||
var executableRoot = NormalizeFilePathArgument(_ai.FaceExecutablePath);
|
||||
var hasCpu = !string.IsNullOrWhiteSpace(ResolveConfiguredFaceEncoderExecutablePath(executableRoot, useGpu: false));
|
||||
var hasGpu = !string.IsNullOrWhiteSpace(ResolveConfiguredFaceEncoderExecutablePath(executableRoot, useGpu: true));
|
||||
|
||||
_ai.FaceGpuOptionEnabled = hasCpu && hasGpu;
|
||||
|
||||
if (hasGpu && !hasCpu)
|
||||
{
|
||||
_ai.UseFaceGpu = true;
|
||||
}
|
||||
else if (!hasGpu)
|
||||
{
|
||||
_ai.UseFaceGpu = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetUseFaceGpu(bool value)
|
||||
{
|
||||
var currentValue = _ai.UseFaceGpu;
|
||||
if (!FaceGpuOptionEnabled)
|
||||
{
|
||||
if (_ai.UseFaceGpu)
|
||||
{
|
||||
_ai.UseFaceGpu = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_ai.UseFaceGpu == value)
|
||||
if (currentValue == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ai.UseFaceGpu = value;
|
||||
|
||||
var currentPath = NormalizeFilePathArgument(_ai.FaceExecutablePath);
|
||||
if (string.IsNullOrWhiteSpace(currentPath))
|
||||
var previousRecommendedUpsample = GetRecommendedFaceUpsample(currentValue);
|
||||
if (_ai.FaceUpsample == previousRecommendedUpsample)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(currentPath);
|
||||
var fileName = Path.GetFileNameWithoutExtension(currentPath);
|
||||
var baseName = fileName.EndsWith("_cpu", StringComparison.OrdinalIgnoreCase)
|
||||
? fileName[..^4]
|
||||
: fileName.EndsWith("_gpu", StringComparison.OrdinalIgnoreCase)
|
||||
? fileName[..^4]
|
||||
: fileName;
|
||||
|
||||
if (string.Equals(baseName, fileName, StringComparison.Ordinal))
|
||||
{
|
||||
_ai.UseFaceGpu = false;
|
||||
_ai.FaceGpuOptionEnabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var updatedFileName = $"{baseName}{(value ? "_gpu" : "_cpu")}{extension}";
|
||||
var directory = Path.GetDirectoryName(currentPath);
|
||||
var updatedPath = string.IsNullOrWhiteSpace(directory)
|
||||
? updatedFileName
|
||||
: Path.Combine(directory, updatedFileName);
|
||||
|
||||
if (string.Equals(updatedPath, currentPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isUpdatingFaceExecutableSelection = true;
|
||||
try
|
||||
{
|
||||
_ai.FaceExecutablePath = updatedPath;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isUpdatingFaceExecutableSelection = false;
|
||||
_ai.FaceUpsample = GetRecommendedFaceUpsample(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1690,7 +1803,7 @@ namespace ImageCatalog_2
|
|||
|
||||
private Process? FindConfiguredFaceEncoderProcess()
|
||||
{
|
||||
var configuredExecutablePath = NormalizeFilePathArgument(FaceExecutablePath);
|
||||
var configuredExecutablePath = ResolveConfiguredFaceEncoderExecutablePath(FaceExecutablePath, UseFaceGpu);
|
||||
if (string.IsNullOrWhiteSpace(configuredExecutablePath))
|
||||
{
|
||||
return null;
|
||||
|
|
@ -1803,7 +1916,7 @@ namespace ImageCatalog_2
|
|||
}
|
||||
}
|
||||
|
||||
private static void AppendFaceProcessOutput(StringBuilder builder, string? line)
|
||||
private void AppendFaceProcessOutput(StringBuilder builder, StringBuilder transcriptBuilder, string? line, bool isError)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
|
|
@ -1814,12 +1927,162 @@ namespace ImageCatalog_2
|
|||
{
|
||||
builder.AppendLine(line);
|
||||
}
|
||||
|
||||
if (isError)
|
||||
{
|
||||
Console.Error.WriteLine(line);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(line);
|
||||
}
|
||||
|
||||
string transcript;
|
||||
lock (transcriptBuilder)
|
||||
{
|
||||
if (isError)
|
||||
{
|
||||
transcriptBuilder.Append("[stderr] ");
|
||||
}
|
||||
|
||||
transcriptBuilder.AppendLine(line);
|
||||
transcript = transcriptBuilder.ToString();
|
||||
}
|
||||
|
||||
_ = InvokeOnUiThreadAsync(() => FaceCommandOutput = transcript);
|
||||
}
|
||||
|
||||
private static string BuildFaceEncoderSummary(int exitCode, StringBuilder outputLines, StringBuilder errorLines)
|
||||
internal static string? ResolveConfiguredFaceEncoderExecutablePath(string configuredPath, bool useGpu)
|
||||
{
|
||||
var variant = useGpu ? "gpu" : "cpu";
|
||||
foreach (var candidate in EnumerateFaceEncoderExecutableCandidates(configuredPath, variant))
|
||||
{
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static (string OutputFilePath, string LogFilePath) BuildFaceEncoderOutputPaths(string outputFolderPath, string imagesFolderPath, DateTime timestamp)
|
||||
{
|
||||
var safeRaceName = BuildSafeFaceEncoderRaceName(imagesFolderPath);
|
||||
var timestampToken = timestamp.ToString("yyyyMMdd_HHmmss");
|
||||
|
||||
return (
|
||||
Path.Combine(outputFolderPath, $"face_encodings_{timestampToken}_{safeRaceName}.pkl"),
|
||||
Path.Combine(outputFolderPath, $"encoder_log_{timestampToken}_{safeRaceName}.txt"));
|
||||
}
|
||||
|
||||
internal static string BuildSafeFaceEncoderRaceName(string imagesFolderPath)
|
||||
{
|
||||
var raceName = new DirectoryInfo(imagesFolderPath).Name;
|
||||
if (string.IsNullOrWhiteSpace(raceName))
|
||||
{
|
||||
return "race";
|
||||
}
|
||||
|
||||
var invalidChars = Path.GetInvalidFileNameChars();
|
||||
var builder = new StringBuilder(raceName.Length);
|
||||
var previousWasSeparator = false;
|
||||
|
||||
foreach (var currentChar in raceName)
|
||||
{
|
||||
if (char.IsWhiteSpace(currentChar) || invalidChars.Contains(currentChar))
|
||||
{
|
||||
if (!previousWasSeparator)
|
||||
{
|
||||
builder.Append('_');
|
||||
previousWasSeparator = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Append(currentChar);
|
||||
previousWasSeparator = false;
|
||||
}
|
||||
|
||||
return builder.ToString().Trim('_') switch
|
||||
{
|
||||
"" => "race",
|
||||
var sanitized => sanitized
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateFaceEncoderExecutableCandidates(string configuredPath, string variant)
|
||||
{
|
||||
var normalizedPath = NormalizeFilePathArgument(configuredPath);
|
||||
if (string.IsNullOrWhiteSpace(normalizedPath))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var executableName = $"face_encoder_{variant}.exe";
|
||||
|
||||
if (File.Exists(normalizedPath))
|
||||
{
|
||||
var fileDirectory = Path.GetDirectoryName(normalizedPath);
|
||||
if (!string.IsNullOrWhiteSpace(fileDirectory))
|
||||
{
|
||||
yield return Path.Combine(fileDirectory, executableName);
|
||||
|
||||
var parentDirectory = Directory.GetParent(fileDirectory)?.FullName;
|
||||
if (!string.IsNullOrWhiteSpace(parentDirectory))
|
||||
{
|
||||
yield return Path.Combine(parentDirectory, $"face_encoder_{variant}", executableName);
|
||||
}
|
||||
}
|
||||
|
||||
yield return normalizedPath;
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return Path.Combine(normalizedPath, executableName);
|
||||
yield return Path.Combine(normalizedPath, $"face_encoder_{variant}", executableName);
|
||||
|
||||
var leafName = Path.GetFileName(normalizedPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
||||
if (leafName.Equals("face_encoder_cpu", StringComparison.OrdinalIgnoreCase)
|
||||
|| leafName.Equals("face_encoder_gpu", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var parentDirectory = Directory.GetParent(normalizedPath)?.FullName;
|
||||
if (!string.IsNullOrWhiteSpace(parentDirectory))
|
||||
{
|
||||
yield return Path.Combine(parentDirectory, $"face_encoder_{variant}", executableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int NormalizeFaceParallelism(int value)
|
||||
{
|
||||
return value is >= 1 and <= 5 ? value : 3;
|
||||
}
|
||||
|
||||
private static int NormalizeFaceMinSize(int value)
|
||||
{
|
||||
return value > 0 ? value : 35;
|
||||
}
|
||||
|
||||
private static bool GetRecommendedFaceUpsample(bool useGpu)
|
||||
{
|
||||
return !useGpu;
|
||||
}
|
||||
|
||||
private static string BuildFaceEncoderSummary(
|
||||
int exitCode,
|
||||
ProcessStartInfo processStartInfo,
|
||||
string outputFilePath,
|
||||
string logFilePath,
|
||||
StringBuilder outputLines,
|
||||
StringBuilder errorLines)
|
||||
{
|
||||
var summary = new StringBuilder();
|
||||
summary.AppendLine($"Exit code: {exitCode}");
|
||||
summary.AppendLine($"Command: {processStartInfo.FileName} {string.Join(" ", processStartInfo.ArgumentList)}");
|
||||
summary.AppendLine($"Output file: {outputFilePath}");
|
||||
summary.AppendLine($"Log file: {logFilePath}");
|
||||
|
||||
lock (outputLines)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -286,6 +286,22 @@ namespace ImageCatalog_2.Models
|
|||
[XmlElement("AI_FaceRecursive")]
|
||||
public bool FaceRecursive { get; set; }
|
||||
|
||||
[JsonPropertyName("FaceIncludeThumbnails")]
|
||||
[XmlElement("AI_FaceIncludeThumbnails")]
|
||||
public bool FaceIncludeThumbnails { get; set; }
|
||||
|
||||
[JsonPropertyName("FaceParallelism")]
|
||||
[XmlElement("AI_FaceParallelism")]
|
||||
public int FaceParallelism { get; set; } = 3;
|
||||
|
||||
[JsonPropertyName("FaceMinSize")]
|
||||
[XmlElement("AI_FaceMinSize")]
|
||||
public int FaceMinSize { get; set; } = 35;
|
||||
|
||||
[JsonPropertyName("FaceUpsample")]
|
||||
[XmlElement("AI_FaceUpsample")]
|
||||
public bool FaceUpsample { get; set; } = true;
|
||||
|
||||
// Race upload settings
|
||||
[JsonPropertyName("ApiLogin")]
|
||||
[XmlElement("RaceUpload_Login")]
|
||||
|
|
|
|||
|
|
@ -72,34 +72,8 @@ static class Program
|
|||
|
||||
internal static bool TrySendConsoleInterrupt(int processId)
|
||||
{
|
||||
var hadConsole = GetConsoleWindow() != IntPtr.Zero;
|
||||
|
||||
try
|
||||
{
|
||||
if (hadConsole)
|
||||
{
|
||||
FreeConsole();
|
||||
}
|
||||
|
||||
if (!AttachConsole(processId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SetConsoleCtrlHandler(null, true);
|
||||
return GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
FreeConsole();
|
||||
SetConsoleCtrlHandler(null, false);
|
||||
|
||||
if (hadConsole)
|
||||
{
|
||||
AllocConsole();
|
||||
RedirectConsoleOutput();
|
||||
}
|
||||
}
|
||||
_ = processId;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
|
|||
3
imagecatalog/Properties/InternalsVisibleTo.cs
Normal file
3
imagecatalog/Properties/InternalsVisibleTo.cs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("MaddoShared.Tests")]
|
||||
|
|
@ -71,6 +71,50 @@ public class AiSettingsViewModel : ViewModelBase
|
|||
}
|
||||
}
|
||||
|
||||
private bool _faceIncludeThumbnails;
|
||||
public bool FaceIncludeThumbnails
|
||||
{
|
||||
get => _faceIncludeThumbnails;
|
||||
set
|
||||
{
|
||||
_faceIncludeThumbnails = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private int _faceParallelism = 3;
|
||||
public int FaceParallelism
|
||||
{
|
||||
get => _faceParallelism;
|
||||
set
|
||||
{
|
||||
_faceParallelism = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private int _faceMinSize = 35;
|
||||
public int FaceMinSize
|
||||
{
|
||||
get => _faceMinSize;
|
||||
set
|
||||
{
|
||||
_faceMinSize = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _faceUpsample = true;
|
||||
public bool FaceUpsample
|
||||
{
|
||||
get => _faceUpsample;
|
||||
set
|
||||
{
|
||||
_faceUpsample = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _faceGpuOptionEnabled;
|
||||
public bool FaceGpuOptionEnabled
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue