diff --git a/Ganimede/Ganimede/App.config b/Ganimede/Ganimede/App.config
index f29607d..eeb022f 100644
--- a/Ganimede/Ganimede/App.config
+++ b/Ganimede/Ganimede/App.config
@@ -13,9 +13,6 @@
-
- C:\Users\balbo\source\repos\Ganimede\Ganimede\Ganimede\FFMpeg
-
\ No newline at end of file
diff --git a/Ganimede/Ganimede/Ganimede.csproj b/Ganimede/Ganimede/Ganimede.csproj
index 50153ef..ccf43e6 100644
--- a/Ganimede/Ganimede/Ganimede.csproj
+++ b/Ganimede/Ganimede/Ganimede.csproj
@@ -7,10 +7,12 @@
enable
true
true
+ true
-
+
+
diff --git a/Ganimede/Ganimede/MainWindow.xaml b/Ganimede/Ganimede/MainWindow.xaml
index ef57ac9..a23f49e 100644
--- a/Ganimede/Ganimede/MainWindow.xaml
+++ b/Ganimede/Ganimede/MainWindow.xaml
@@ -5,48 +5,47 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Ganimede"
mc:Ignorable="d"
- Title="Estrattore Frame Video" Height="800" Width="1250"
- Background="#1E2228" WindowStartupLocation="CenterScreen">
+ Title="Ganimede - Video Frame Extractor" Height="750" Width="1200"
+ Background="#F5F7FA" WindowStartupLocation="CenterScreen">
-
- #268BFF
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
@@ -117,132 +190,374 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
diff --git a/Ganimede/Ganimede/MainWindow.xaml.cs b/Ganimede/Ganimede/MainWindow.xaml.cs
index 8798120..5e542e1 100644
--- a/Ganimede/Ganimede/MainWindow.xaml.cs
+++ b/Ganimede/Ganimede/MainWindow.xaml.cs
@@ -8,7 +8,6 @@ using System.Windows.Media.Imaging;
using System.Diagnostics;
using System.Linq;
using System.Collections.Generic;
-using FFMpegCore;
using Ganimede.Properties;
using Ganimede.Services;
using Ganimede.Models;
@@ -30,7 +29,6 @@ namespace Ganimede
{
InitializeComponent();
InitializeUI();
- ConfigureFFMpeg();
}
private void InitializeUI()
@@ -51,9 +49,67 @@ namespace Ganimede
_processingService.ProcessingStopped += OnProcessingStopped;
_processingService.JobQueue.CollectionChanged += (s, e) => UpdateQueueCount();
+ // Initialize settings controls
+ LoadSettingsControls();
+
UpdateQueueCount();
}
+ private void LoadSettingsControls()
+ {
+ // Controls are loaded after InitializeComponent, so we can't access them in constructor
+ // We'll load settings when the Settings tab is first accessed instead
+ Dispatcher.InvokeAsync(() =>
+ {
+ try
+ {
+ // Load frame size
+ var frameSize = Settings.Default.FrameSize;
+ foreach (ComboBoxItem item in FrameSizeComboBox.Items)
+ {
+ if (item.Tag?.ToString() == frameSize)
+ {
+ FrameSizeComboBox.SelectedItem = item;
+ break;
+ }
+ }
+
+ // Load overwrite mode
+ var overwriteMode = Settings.Default.DefaultOverwriteMode;
+ foreach (ComboBoxItem item in OverwriteModeComboBox.Items)
+ {
+ if (item.Tag?.ToString() == overwriteMode)
+ {
+ OverwriteModeComboBox.SelectedItem = item;
+ break;
+ }
+ }
+
+ // Load extraction mode
+ switch (Settings.Default.DefaultExtractionMode)
+ {
+ case "SingleFrame":
+ DefaultModeSingleRadio.IsChecked = true;
+ break;
+ case "Auto":
+ DefaultModeAutoRadio.IsChecked = true;
+ break;
+ default:
+ DefaultModeFullRadio.IsChecked = true;
+ break;
+ }
+
+ // Load folder settings
+ CreateSubfolderCheckBox.IsChecked = Settings.Default.CreateSubfolder;
+ SingleFrameUseSubfolderCheckBox.IsChecked = Settings.Default.SingleFrameUseSubfolder;
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"[ERROR] Failed to load settings controls: {ex.Message}");
+ }
+ }, System.Windows.Threading.DispatcherPriority.Loaded);
+ }
+
private void UpdateQueueCount()
{
Dispatcher.Invoke(() =>
@@ -71,7 +127,7 @@ namespace Ganimede
var failed = _processingService.JobQueue.Count(j => j.Status == JobStatus.Failed);
Dispatcher.Invoke(() =>
{
- JobsSummaryText.Text = $"In attesa: {pending} | In corso: {processing} | Completati: {completed} | Falliti: {failed}";
+ JobsSummaryText.Text = $"Pending: {pending} | Processing: {processing} | Completed: {completed} | Failed: {failed}";
});
}
@@ -81,7 +137,7 @@ namespace Ganimede
{
StartQueueButton.IsEnabled = false;
StopQueueButton.IsEnabled = true;
- StatusText.Text = "Elaborazione coda...";
+ StatusText.Text = "Processing queue...";
UpdateJobsSummary();
});
}
@@ -92,7 +148,7 @@ namespace Ganimede
{
StartQueueButton.IsEnabled = true;
StopQueueButton.IsEnabled = false;
- StatusText.Text = "Coda fermata";
+ StatusText.Text = "Queue stopped";
UpdateJobsSummary();
});
}
@@ -101,7 +157,7 @@ namespace Ganimede
{
Dispatcher.Invoke(() =>
{
- StatusText.Text = $"โ Completato: {job.VideoName}";
+ StatusText.Text = $"โ Completed: {job.VideoName}";
LoadThumbnailsFromFolder(job.OutputFolder);
UpdateJobsSummary();
});
@@ -111,7 +167,7 @@ namespace Ganimede
{
Dispatcher.Invoke(() =>
{
- StatusText.Text = $"โ Fallito: {job.VideoName}";
+ StatusText.Text = $"โ Failed: {job.VideoName}";
UpdateJobsSummary();
});
}
@@ -139,122 +195,16 @@ namespace Ganimede
}
}
- private void ConfigureFFMpeg()
- {
- var ffmpegBin = Settings.Default.FFmpegBinFolder;
- if (!string.IsNullOrEmpty(ffmpegBin) && ValidateFFMpegBinaries(ffmpegBin))
- FFMpegCore.GlobalFFOptions.Configure(o => o.BinaryFolder = ffmpegBin);
- else if (TryUseSystemFFMpeg()) { }
- else if (TryFixMissingFFMpeg(ffmpegBin))
- FFMpegCore.GlobalFFOptions.Configure(o => o.BinaryFolder = ffmpegBin);
- }
-
- private bool ValidateFFMpegBinaries(string binFolder) =>
- Directory.Exists(binFolder) &&
- File.Exists(Path.Combine(binFolder, "ffmpeg.exe")) &&
- File.Exists(Path.Combine(binFolder, "ffprobe.exe"));
-
- private bool TryUseSystemFFMpeg()
- {
- try
- {
- var psi = new ProcessStartInfo { FileName = "ffmpeg", Arguments = "-version", UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true };
- using var p = Process.Start(psi);
- return p != null && p.WaitForExit(4000) && p.ExitCode == 0;
- }
- catch { return false; }
- }
-
- private bool TryFixMissingFFMpeg(string binFolder)
- {
- if (string.IsNullOrEmpty(binFolder) || !Directory.Exists(binFolder)) return false;
- var ffmpegPath = Path.Combine(binFolder, "ffmpeg.exe");
- var ffprobePath = Path.Combine(binFolder, "ffprobe.exe");
- if (!File.Exists(ffmpegPath) && File.Exists(ffprobePath))
- {
- try { File.Copy(ffprobePath, ffmpegPath, true); return File.Exists(ffmpegPath); } catch { return false; }
- }
- return false;
- }
-
- private void BrowseVideoButton_Click(object sender, RoutedEventArgs e)
- {
- var dialog = new WpfOpenFileDialog { Filter = "Video (*.mp4;*.avi;*.mov;*.mkv;*.wmv)|*.mp4;*.avi;*.mov;*.mkv;*.wmv|Tutti i file (*.*)|*.*", Multiselect = true };
- if (dialog.ShowDialog() == true) AddVideosToQueue(dialog.FileNames);
- }
-
- private void ImportFolderButton_Click(object sender, RoutedEventArgs e)
- {
- using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona la cartella con i video", ShowNewFolderButton = false };
- if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
- {
- try
- {
- var files = Directory.EnumerateFiles(dialog.SelectedPath, "*.*", SearchOption.TopDirectoryOnly).Where(IsVideoFile).ToArray();
- if (files.Length == 0)
- {
- WpfMessageBox.Show("Nessun file video valido trovato.", "Importa Cartella", MessageBoxButton.OK, MessageBoxImage.Information);
- return;
- }
- AddVideosToQueue(files);
- StatusText.Text = $"Importati {files.Length} video.";
- }
- catch (Exception ex)
- {
- WpfMessageBox.Show($"Errore: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
- }
-
- private void AddVideosToQueue(string[] paths)
- {
- if (string.IsNullOrEmpty(outputFolder))
- {
- WpfMessageBox.Show("Seleziona prima una cartella di output.", "Cartella Output Richiesta", MessageBoxButton.OK, MessageBoxImage.Warning);
- return;
- }
- var createSub = Settings.Default.CreateSubfolder;
- foreach (var p in paths) _processingService.AddJob(p, outputFolder, createSub);
- StatusText.Text = $"Aggiunti {paths.Length} video (In attesa)";
- Settings.Default.LastVideoPath = paths.FirstOrDefault();
- Settings.Default.Save();
- UpdateQueueCount();
- }
-
- private void SelectOutputFolderButton_Click(object sender, RoutedEventArgs e)
- {
- using var dialog = new System.Windows.Forms.FolderBrowserDialog();
- if (!string.IsNullOrEmpty(outputFolder)) dialog.SelectedPath = outputFolder;
- if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
- {
- outputFolder = dialog.SelectedPath;
- GlobalOutputFolderTextBox.Text = outputFolder;
- StatusText.Text = "Cartella output aggiornata";
- Settings.Default.LastOutputFolder = outputFolder;
- Settings.Default.Save();
- }
- }
-
- private void SettingsButton_Click(object sender, RoutedEventArgs e)
- {
- var win = new SettingsWindow { Owner = this };
- if (win.ShowDialog() == true)
- {
- ConfigureFFMpeg();
- StatusText.Text = "Impostazioni aggiornate";
- }
- }
-
private async void StartQueueButton_Click(object sender, RoutedEventArgs e)
{
if (_processingService.JobQueue.Count == 0)
{
- WpfMessageBox.Show("Nessun video in coda.", "Coda Vuota", MessageBoxButton.OK, MessageBoxImage.Information);
+ WpfMessageBox.Show("No videos in queue.", "Empty Queue", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
if (_processingService.JobQueue.All(j => j.Status != JobStatus.Pending))
{
- WpfMessageBox.Show("Nessun job in stato In attesa.", "Nessun Job", MessageBoxButton.OK, MessageBoxImage.Information);
+ WpfMessageBox.Show("No pending jobs in queue.", "No Jobs", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
await _processingService.StartProcessingAsync();
@@ -264,7 +214,7 @@ namespace Ganimede
private void StopQueueButton_Click(object sender, RoutedEventArgs e)
{
_processingService.StopProcessing();
- StatusText.Text = "Arresto in corso...";
+ StatusText.Text = "Stopping...";
UpdateJobsSummary();
}
@@ -281,15 +231,24 @@ namespace Ganimede
private void ConfigureSelectedButton_Click(object sender, RoutedEventArgs e)
{
if (_selectedJobs.Count == 0) return;
+
var cfg = new JobConfigWindow(_selectedJobs.ToList()) { Owner = this };
if (cfg.ShowDialog() == true)
{
- StatusText.Text = $"Configurazione applicata a {_selectedJobs.Count} job";
- foreach (var job in _selectedJobs.Where(j => string.IsNullOrEmpty(j.CustomOutputFolder)))
+ StatusText.Text = $"Configuration applied to {_selectedJobs.Count} job(s)";
+
+ // Update output folders only if outputFolder is set
+ if (!string.IsNullOrEmpty(outputFolder))
{
- var createSub = Settings.Default.CreateSubfolder;
- job.OutputFolder = job.ExtractionMode == ExtractionMode.SingleFrame ? outputFolder : (createSub ? Path.Combine(outputFolder, job.VideoName) : outputFolder);
+ foreach (var job in _selectedJobs.Where(j => string.IsNullOrEmpty(j.CustomOutputFolder)))
+ {
+ var createSub = Settings.Default.CreateSubfolder;
+ job.OutputFolder = job.ExtractionMode == ExtractionMode.SingleFrame
+ ? outputFolder
+ : (createSub ? Path.Combine(outputFolder, job.VideoName) : outputFolder);
+ }
}
+
UpdateJobsSummary();
}
}
@@ -308,7 +267,7 @@ namespace Ganimede
private void ClearCompletedButton_Click(object sender, RoutedEventArgs e)
{
_processingService.RemoveCompletedJobs();
- StatusText.Text = "Job completati rimossi";
+ StatusText.Text = "Completed jobs removed";
UpdateQueueCount();
}
@@ -317,7 +276,7 @@ namespace Ganimede
var processing = _processingService.JobQueue.Any(j => j.Status == JobStatus.Processing);
if (processing)
{
- var res = WpfMessageBox.Show("Ci sono job in elaborazione.\n\nSi: Ferma e svuota la coda\nNo: Rimuovi solo job non in elaborazione\nAnnulla: Annulla", "Conferma", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
+ var res = WpfMessageBox.Show("There are jobs being processed.\n\nYes: Stop and clear queue\nNo: Remove only non-processing jobs\nCancel: Cancel operation", "Confirm", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
if (res == MessageBoxResult.Cancel) return;
if (res == MessageBoxResult.Yes)
{
@@ -334,16 +293,107 @@ namespace Ganimede
}
else
{
- if (WpfMessageBox.Show("Rimuovere tutti i job?", "Conferma", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
+ if (WpfMessageBox.Show("Remove all jobs from queue?", "Confirm", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
{
_processingService.JobQueue.Clear();
thumbnails.Clear();
}
}
- StatusText.Text = "Coda aggiornata";
+ StatusText.Text = "Queue updated";
UpdateQueueCount();
}
+ private void BrowseVideoButton_Click(object sender, RoutedEventArgs e)
+ {
+ var dialog = new WpfOpenFileDialog { Filter = "Video Files|*.mp4;*.avi;*.mov;*.mkv;*.wmv;*.flv;*.webm|All Files|*.*", Multiselect = true };
+ if (dialog.ShowDialog() == true) AddVideosToQueue(dialog.FileNames);
+ }
+
+ private void ImportFolderButton_Click(object sender, RoutedEventArgs e)
+ {
+ using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Select folder containing videos", ShowNewFolderButton = false };
+ if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
+ {
+ try
+ {
+ var files = Directory.EnumerateFiles(dialog.SelectedPath, "*.*", SearchOption.TopDirectoryOnly).Where(IsVideoFile).ToArray();
+ if (files.Length == 0)
+ {
+ WpfMessageBox.Show("No valid video files found.", "Import Folder", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+ AddVideosToQueue(files);
+ StatusText.Text = $"Imported {files.Length} video(s)";
+ }
+ catch (Exception ex)
+ {
+ WpfMessageBox.Show($"Error: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+ }
+
+ private void AddVideosToQueue(string[] paths)
+ {
+ if (string.IsNullOrEmpty(outputFolder))
+ {
+ WpfMessageBox.Show("Please select an output folder first.", "Output Folder Required", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+ var createSub = Settings.Default.CreateSubfolder;
+ foreach (var p in paths) _processingService.AddJob(p, outputFolder, createSub);
+ StatusText.Text = $"Added {paths.Length} video(s)";
+ Settings.Default.LastVideoPath = paths.FirstOrDefault();
+ Settings.Default.Save();
+ UpdateQueueCount();
+ }
+
+ private void SelectOutputFolderButton_Click(object sender, RoutedEventArgs e)
+ {
+ using var dialog = new System.Windows.Forms.FolderBrowserDialog();
+ if (!string.IsNullOrEmpty(outputFolder)) dialog.SelectedPath = outputFolder;
+ if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
+ {
+ outputFolder = dialog.SelectedPath;
+ GlobalOutputFolderTextBox.Text = outputFolder;
+ StatusText.Text = "Output folder updated";
+ Settings.Default.LastOutputFolder = outputFolder;
+ Settings.Default.Save();
+ }
+ }
+
+ private void SaveSettings_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var selectedFrameSize = FrameSizeComboBox.SelectedItem as ComboBoxItem;
+ Settings.Default.FrameSize = selectedFrameSize?.Tag?.ToString() ?? "original";
+
+ var selectedOverwrite = OverwriteModeComboBox.SelectedItem as ComboBoxItem;
+ Settings.Default.DefaultOverwriteMode = selectedOverwrite?.Tag?.ToString() ?? "Ask";
+
+ if (DefaultModeSingleRadio.IsChecked == true)
+ Settings.Default.DefaultExtractionMode = "SingleFrame";
+ else if (DefaultModeAutoRadio.IsChecked == true)
+ Settings.Default.DefaultExtractionMode = "Auto";
+ else
+ Settings.Default.DefaultExtractionMode = "Full";
+
+ Settings.Default.CreateSubfolder = CreateSubfolderCheckBox.IsChecked ?? true;
+ Settings.Default.SingleFrameUseSubfolder = SingleFrameUseSubfolderCheckBox.IsChecked ?? false;
+
+ Settings.Default.Save();
+
+ StatusText.Text = "โ Settings saved successfully";
+ Debug.WriteLine("[SETTINGS] Settings saved from inline tab");
+ }
+ catch (Exception ex)
+ {
+ StatusText.Text = "โ Failed to save settings";
+ Debug.WriteLine($"[ERROR] Failed to save settings: {ex.Message}");
+ WpfMessageBox.Show($"Error saving settings: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
private static bool IsVideoFile(string path)
{
var ext = Path.GetExtension(path).ToLowerInvariant();
diff --git a/Ganimede/Ganimede/Properties/Settings.Designer.cs b/Ganimede/Ganimede/Properties/Settings.Designer.cs
index cf5d499..68ede6f 100644
--- a/Ganimede/Ganimede/Properties/Settings.Designer.cs
+++ b/Ganimede/Ganimede/Properties/Settings.Designer.cs
@@ -47,18 +47,6 @@ namespace Ganimede.Properties {
}
}
- [global::System.Configuration.UserScopedSettingAttribute()]
- [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
- [global::System.Configuration.DefaultSettingValueAttribute("C:\\Users\\balbo\\source\\repos\\Ganimede\\Ganimede\\Ganimede\\FFMpeg")]
- public string FFmpegBinFolder {
- get {
- return ((string)(this["FFmpegBinFolder"]));
- }
- set {
- this["FFmpegBinFolder"] = value;
- }
- }
-
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("True")]
diff --git a/Ganimede/Ganimede/README.md b/Ganimede/Ganimede/README.md
new file mode 100644
index 0000000..8071445
--- /dev/null
+++ b/Ganimede/Ganimede/README.md
@@ -0,0 +1,150 @@
+# Ganimede - Video Frame Extractor
+
+## ?? Overview
+Ganimede is a modern .NET 8 WPF application for extracting frames from video files. The application features a clean, Material Design-inspired interface with tabbed navigation.
+
+## ? Key Features
+
+### Video Processing
+- **Multiple video formats supported**: MP4, AVI, MOV, MKV, WMV, FLV, WebM
+- **Batch processing**: Add multiple videos to queue
+- **Folder import**: Import entire folders of videos
+- **Three extraction modes**:
+ - **Full**: Extract all frames from video
+ - **Single Frame**: Extract one representative frame
+ - **Auto**: Automatically determine best extraction method
+
+### Modern UI
+- **Tab-based navigation**:
+ - ?? **Processing**: Manage video queue and processing
+ - ?? **Library**: Preview extracted frames
+ - ?? **Settings**: Configure application preferences
+- **Clean Material Design** aesthetic
+- **Real-time progress tracking**
+- **Thumbnail preview** of extracted frames
+
+### Settings
+- **Frame size options**: Original, 320x180, 640x360, 1280x720, 1920x1080
+- **Overwrite behavior**: Ask, Skip, Overwrite
+- **Subfolder creation** options
+- **Extraction mode** defaults
+
+## ??? Technology Stack
+
+### Core Technologies
+- **.NET 8** (Windows)
+- **WPF** (Windows Presentation Foundation)
+- **XAML** for UI design
+
+### Video Processing
+- **FFMediaToolkit 4.8.1** - Native .NET video processing
+- **FFmpeg.AutoGen 7.1.1** - FFmpeg bindings
+- **System.Drawing.Common 10.0.0** - Image manipulation
+
+### Architecture
+- **MVVM-inspired** pattern
+- **Async/await** for responsive UI
+- **ObservableCollection** for data binding
+- **Custom wrapper classes** for video operations:
+ - `VideoAnalyzer` - Video metadata extraction
+ - `FrameExtractor` - Frame extraction operations
+ - `VideoProcessingService` - Queue management
+
+## ?? Project Structure
+
+```
+Ganimede/
+??? VideoProcessing/
+? ??? VideoAnalyzer.cs # Video analysis wrapper
+? ??? FrameExtractor.cs # Frame extraction wrapper
+??? Services/
+? ??? VideoProcessingService.cs # Processing queue management
+??? Models/
+? ??? VideoJob.cs # Job data model
+??? Windows/
+? ??? JobConfigWindow.xaml # Job configuration dialog
+? ??? SettingsWindow.xaml # Legacy settings window (unused)
+??? Helpers/
+? ??? NamingHelper.cs # File naming utilities
+??? Converters/
+? ??? StatusColorConverter.cs # Status-to-color converter
+??? MainWindow.xaml # Main application window
+```
+
+## ?? Recent Updates
+
+### UI Redesign (v2.0)
+- Complete UI overhaul with modern Material Design
+- Tab-based navigation replacing sidebar layout
+- Integrated settings (no separate window needed)
+- Light theme with clean aesthetics
+- Improved color palette (Indigo primary)
+- Enhanced card-based layouts
+- Refined typography and spacing
+
+### Video Processing Refactor (v2.0)
+- **Replaced FFMpegCore** with FFMediaToolkit
+- **No external FFmpeg binaries required**
+- Custom wrapper architecture for video operations
+- Improved performance with direct memory access
+- Simplified configuration (no FFmpeg path needed)
+
+## ?? Requirements
+
+- **Windows** operating system
+- **.NET 8 Runtime** (or SDK for development)
+- **FFmpeg libraries** (automatically included via FFMediaToolkit)
+
+## ?? Color Palette
+
+- **Primary**: #6366F1 (Indigo)
+- **Success**: #10B981 (Green)
+- **Danger**: #EF4444 (Red)
+- **Warning**: #F59E0B (Amber)
+- **Background**: #F5F7FA (Light Gray)
+- **Surface**: #FFFFFF (White)
+- **Border**: #E5E7EB (Gray)
+
+## ?? Usage
+
+1. **Add Videos**: Click "Add Videos" or "Import Folder"
+2. **Select Output**: Choose output folder for extracted frames
+3. **Configure** (Optional): Configure individual jobs or use default settings
+4. **Start Queue**: Process all pending videos
+5. **View Results**: Switch to Library tab to preview extracted frames
+
+## ?? Development
+
+### Building
+```bash
+dotnet build
+```
+
+### Running
+```bash
+dotnet run --project Ganimede\Ganimede.csproj
+```
+
+### Configuration
+Settings are stored in `Settings.settings` and persisted between sessions:
+- Default output folder
+- Frame size preferences
+- Extraction mode defaults
+- Overwrite behavior
+- Subfolder creation options
+
+## ?? License
+
+[Your License Here]
+
+## ?? Author
+
+[Your Name/Organization]
+
+## ?? Known Issues
+
+None currently reported.
+
+## ?? Support
+
+[Your Support Contact]
diff --git a/Ganimede/Ganimede/Services/VideoProcessingService.cs b/Ganimede/Ganimede/Services/VideoProcessingService.cs
index 5223065..4da3e32 100644
--- a/Ganimede/Ganimede/Services/VideoProcessingService.cs
+++ b/Ganimede/Ganimede/Services/VideoProcessingService.cs
@@ -3,11 +3,11 @@ using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Threading;
using System.Diagnostics;
-using FFMpegCore;
using System.IO;
using Ganimede.Models;
using Ganimede.Properties;
using Ganimede.Helpers;
+using Ganimede.VideoProcessing;
namespace Ganimede.Services
{
@@ -146,11 +146,10 @@ namespace Ganimede.Services
{
Debug.WriteLine($"[PROCESS] Starting job: {job.VideoName}");
- // (Do not decide folder change yet; need analysis for Auto mode)
-
- var mediaInfo = await FFProbe.AnalyseAsync(job.VideoPath);
- int frameRate = (int)(mediaInfo.PrimaryVideoStream?.FrameRate ?? 24);
- int totalFrames = (int)(mediaInfo.Duration.TotalSeconds * frameRate);
+ // Analyze video using VideoAnalyzer
+ var mediaInfo = await Task.Run(() => VideoAnalyzer.Analyze(job.VideoPath), cancellationToken);
+ int frameRate = (int)mediaInfo.FrameRate;
+ int totalFrames = mediaInfo.TotalFrames;
Debug.WriteLine($"[INFO] Video {job.VideoName}: {totalFrames} frames at {frameRate} fps, duration {mediaInfo.Duration}");
// Heuristic suggestion
@@ -161,11 +160,10 @@ namespace Ganimede.Services
suggestSingleFrame = true;
else if (mediaInfo.Duration.TotalSeconds >= 3 && mediaInfo.Duration.TotalSeconds <= 45)
{
- var primary = mediaInfo.PrimaryVideoStream;
- if (primary != null && primary.BitRate > 0 && primary.Width > 0 && primary.Height > 0)
+ if (mediaInfo.BitRate > 0 && mediaInfo.Width > 0 && mediaInfo.Height > 0)
{
- double pixels = primary.Width * primary.Height;
- if (primary.BitRate < pixels * 0.3)
+ double pixels = mediaInfo.Width * mediaInfo.Height;
+ if (mediaInfo.BitRate < pixels * 0.3)
suggestSingleFrame = true;
}
}
@@ -224,7 +222,7 @@ namespace Ganimede.Services
job.StatusMessage = "Frame already exists (skipped)";
else
{
- await ExtractFrameAsync(job, targetIndex, frameRate, frameSize, framePath);
+ await ExtractFrameAsync(job, frameTime, frameSize, framePath, cancellationToken);
job.StatusMessage = "Single frame extracted";
}
job.Progress = 100;
@@ -234,32 +232,47 @@ namespace Ganimede.Services
return;
}
- // Full extraction loop (unchanged)
+ // Full extraction using FrameExtractor
int processedFrames = 0;
int skippedFrames = 0;
- for (int i = 0; i < totalFrames; i++)
+
+ await Task.Run(() =>
{
- if (cancellationToken.IsCancellationRequested)
- {
- job.Status = JobStatus.Cancelled;
- job.StatusMessage = "Cancelled by user";
- Debug.WriteLine($"[CANCELLED] Job cancelled: {job.VideoName}");
- return;
- }
- var frameTime = TimeSpan.FromSeconds((double)i / frameRate);
- var fileName = NamingHelper.GenerateFileName(namingPattern, job, i, frameTime, customPrefix);
- string framePath = Path.Combine(job.OutputFolder, fileName);
- if (File.Exists(framePath) && overwriteMode == OverwriteMode.Skip)
- skippedFrames++;
- else
- {
- await ExtractFrameAsync(job, i, frameRate, frameSize, framePath);
- processedFrames++;
- }
- job.Progress = (double)(i + 1) / totalFrames * 100;
- job.StatusMessage = $"Processed {processedFrames}/{totalFrames} frames ({job.Progress:F1}%)" + (skippedFrames > 0 ? $" - Skipped {skippedFrames}" : "");
- if (i % 10 == 0) await Task.Delay(1, cancellationToken);
+ FrameExtractor.ExtractAllFrames(
+ job.VideoPath,
+ job.OutputFolder,
+ (frameIndex, timePosition) => NamingHelper.GenerateFileName(namingPattern, job, frameIndex, timePosition, customPrefix),
+ frameSize.width,
+ frameSize.height,
+ (current, total) =>
+ {
+ if (cancellationToken.IsCancellationRequested)
+ return;
+
+ job.Progress = (double)current / total * 100;
+ job.StatusMessage = $"Processed {current}/{total} frames ({job.Progress:F1}%)";
+ processedFrames = current;
+ },
+ (framePath) =>
+ {
+ if (File.Exists(framePath) && overwriteMode == OverwriteMode.Skip)
+ {
+ skippedFrames++;
+ return true;
+ }
+ return false;
+ }
+ );
+ }, cancellationToken);
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ job.Status = JobStatus.Cancelled;
+ job.StatusMessage = "Cancelled by user";
+ Debug.WriteLine($"[CANCELLED] Job cancelled: {job.VideoName}");
+ return;
}
+
job.Status = JobStatus.Completed;
job.StatusMessage = $"Completed - {processedFrames} frames processed" + (skippedFrames > 0 ? $", {skippedFrames} skipped" : "");
job.Progress = 100;
@@ -327,62 +340,25 @@ namespace Ganimede.Services
return Settings.Default.DefaultCustomPrefix ?? "custom";
}
- private async Task ExtractFrameAsync(VideoJob job, int frameIndex, int frameRate, (int width, int height) frameSize, string framePath)
+ private async Task ExtractFrameAsync(VideoJob job, TimeSpan frameTime, (int width, int height) frameSize, string framePath, CancellationToken cancellationToken)
{
- var frameTime = TimeSpan.FromSeconds((double)frameIndex / frameRate);
-
try
{
- if (frameSize.width == -1 && frameSize.height == -1)
+ await Task.Run(() =>
{
- try
- {
- await FFMpegArguments
- .FromFileInput(job.VideoPath)
- .OutputToFile(framePath, true, options => options
- .Seek(frameTime)
- .WithFrameOutputCount(1)
- .WithVideoCodec("png"))
- .ProcessAsynchronously();
- return;
- }
- catch
- {
- await FFMpegArguments
- .FromFileInput(job.VideoPath)
- .OutputToFile(framePath, true, options => options
- .Seek(frameTime)
- .WithFrameOutputCount(1))
- .ProcessAsynchronously();
- return;
- }
- }
-
- try
- {
- await FFMpegArguments
- .FromFileInput(job.VideoPath)
- .OutputToFile(framePath, true, options => options
- .Seek(frameTime)
- .WithFrameOutputCount(1)
- .WithVideoCodec("png")
- .Resize(frameSize.width, frameSize.height))
- .ProcessAsynchronously();
- }
- catch
- {
- await FFMpegArguments
- .FromFileInput(job.VideoPath)
- .OutputToFile(framePath, true, options => options
- .Seek(frameTime)
- .WithFrameOutputCount(1)
- .Resize(frameSize.width, frameSize.height))
- .ProcessAsynchronously();
- }
+ FrameExtractor.ExtractFrame(
+ job.VideoPath,
+ frameTime,
+ framePath,
+ frameSize.width,
+ frameSize.height
+ );
+ }, cancellationToken);
}
catch (Exception ex)
{
- Debug.WriteLine($"[ERROR] Failed to extract frame {frameIndex} from {job.VideoName}: {ex.Message}");
+ Debug.WriteLine($"[ERROR] Failed to extract frame from {job.VideoName}: {ex.Message}");
+ throw;
}
}
}
diff --git a/Ganimede/Ganimede/VideoProcessing/FrameExtractor.cs b/Ganimede/Ganimede/VideoProcessing/FrameExtractor.cs
new file mode 100644
index 0000000..6901aa1
--- /dev/null
+++ b/Ganimede/Ganimede/VideoProcessing/FrameExtractor.cs
@@ -0,0 +1,194 @@
+using System;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.IO;
+using FFMediaToolkit;
+using FFMediaToolkit.Decoding;
+using FFMediaToolkit.Graphics;
+
+namespace Ganimede.VideoProcessing
+{
+ ///
+ /// Provides frame extraction capabilities from video files using FFMediaToolkit
+ ///
+ public class FrameExtractor
+ {
+ ///
+ /// Extracts a single frame from a video at a specific time position
+ ///
+ /// Path to the video file
+ /// Time position in the video
+ /// Output path for the PNG image
+ /// Target width for resizing (optional, -1 for original)
+ /// Target height for resizing (optional, -1 for original)
+ public static void ExtractFrame(
+ string videoPath,
+ TimeSpan timePosition,
+ string outputPath,
+ int targetWidth = -1,
+ int targetHeight = -1)
+ {
+ if (!File.Exists(videoPath))
+ throw new FileNotFoundException($"Video file not found: {videoPath}");
+
+ try
+ {
+ using var file = MediaFile.Open(videoPath, new MediaOptions { StreamsToLoad = MediaMode.Video, VideoPixelFormat = ImagePixelFormat.Bgr24 });
+
+ if (!file.HasVideo)
+ throw new InvalidOperationException("The file does not contain a video stream");
+
+ // Get the frame at the specified time
+ var imageData = file.Video.GetFrame(timePosition);
+
+ // Create bitmap and copy data
+ using var bitmap = new Bitmap(imageData.ImageSize.Width, imageData.ImageSize.Height, PixelFormat.Format24bppRgb);
+ var rect = new Rectangle(Point.Empty, bitmap.Size);
+ var bitmapData = bitmap.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
+
+ unsafe
+ {
+ var dst = (byte*)bitmapData.Scan0;
+ var rowSize = imageData.ImageSize.Width * 3;
+
+ for (int y = 0; y < imageData.ImageSize.Height; y++)
+ {
+ var srcRow = imageData.Data.Slice(y * imageData.Stride, rowSize);
+ var dstRow = new Span(dst + y * bitmapData.Stride, rowSize);
+ srcRow.CopyTo(dstRow);
+ }
+ }
+
+ bitmap.UnlockBits(bitmapData);
+
+ // Resize if needed
+ if (targetWidth > 0 && targetHeight > 0 && (targetWidth != imageData.ImageSize.Width || targetHeight != imageData.ImageSize.Height))
+ {
+ using var resized = new Bitmap(bitmap, targetWidth, targetHeight);
+ SaveBitmapAsPng(resized, outputPath);
+ }
+ else
+ {
+ SaveBitmapAsPng(bitmap, outputPath);
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException($"Failed to extract frame: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// Extracts all frames from a video
+ ///
+ /// Path to the video file
+ /// Output folder for PNG images
+ /// Function to generate file names for each frame
+ /// Target width for resizing (optional, -1 for original)
+ /// Target height for resizing (optional, -1 for original)
+ /// Progress callback (frame index, total frames)
+ /// Function to determine if a frame should be skipped
+ public static void ExtractAllFrames(
+ string videoPath,
+ string outputFolder,
+ Func fileNameGenerator,
+ int targetWidth = -1,
+ int targetHeight = -1,
+ Action? onProgress = null,
+ Func? shouldSkipFrame = null)
+ {
+ if (!File.Exists(videoPath))
+ throw new FileNotFoundException($"Video file not found: {videoPath}");
+
+ if (!Directory.Exists(outputFolder))
+ Directory.CreateDirectory(outputFolder);
+
+ try
+ {
+ using var file = MediaFile.Open(videoPath, new MediaOptions { StreamsToLoad = MediaMode.Video, VideoPixelFormat = ImagePixelFormat.Bgr24 });
+
+ if (!file.HasVideo)
+ throw new InvalidOperationException("The file does not contain a video stream");
+
+ var video = file.Video;
+ var info = video.Info;
+
+ // Calculate total frames
+ double frameRate = info.AvgFrameRate;
+ int totalFrames = info.NumberOfFrames ?? (int)(info.Duration.TotalSeconds * frameRate);
+
+ int frameIndex = 0;
+
+ // Create a reusable buffer
+ var buffer = new byte[video.Info.FrameSize.Width * video.Info.FrameSize.Height * 3];
+
+ while (video.TryGetNextFrame(buffer))
+ {
+ var timePosition = video.Position;
+ var fileName = fileNameGenerator(frameIndex, timePosition);
+ var fullPath = Path.Combine(outputFolder, fileName);
+
+ // Check if frame should be skipped
+ if (shouldSkipFrame != null && shouldSkipFrame(fullPath))
+ {
+ frameIndex++;
+ onProgress?.Invoke(frameIndex, totalFrames);
+ continue;
+ }
+
+ // Create bitmap from buffer
+ using var bitmap = new Bitmap(info.FrameSize.Width, info.FrameSize.Height, PixelFormat.Format24bppRgb);
+ var rect = new Rectangle(Point.Empty, bitmap.Size);
+ var bitmapData = bitmap.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
+
+ unsafe
+ {
+ var dst = (byte*)bitmapData.Scan0;
+ var rowSize = info.FrameSize.Width * 3;
+
+ for (int y = 0; y < info.FrameSize.Height; y++)
+ {
+ var srcRow = buffer.AsSpan(y * rowSize, rowSize);
+ var dstRow = new Span(dst + y * bitmapData.Stride, rowSize);
+ srcRow.CopyTo(dstRow);
+ }
+ }
+
+ bitmap.UnlockBits(bitmapData);
+
+ // Resize if needed
+ if (targetWidth > 0 && targetHeight > 0)
+ {
+ using var resized = new Bitmap(bitmap, targetWidth, targetHeight);
+ SaveBitmapAsPng(resized, fullPath);
+ }
+ else
+ {
+ SaveBitmapAsPng(bitmap, fullPath);
+ }
+
+ frameIndex++;
+ onProgress?.Invoke(frameIndex, totalFrames);
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException($"Failed to extract frames: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// Saves a bitmap as PNG file
+ ///
+ private static void SaveBitmapAsPng(Bitmap bitmap, string outputPath)
+ {
+ // Ensure directory exists
+ var directory = Path.GetDirectoryName(outputPath);
+ if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+ Directory.CreateDirectory(directory);
+
+ // Save as PNG
+ bitmap.Save(outputPath, ImageFormat.Png);
+ }
+ }
+}
diff --git a/Ganimede/Ganimede/VideoProcessing/VideoAnalyzer.cs b/Ganimede/Ganimede/VideoProcessing/VideoAnalyzer.cs
new file mode 100644
index 0000000..275a348
--- /dev/null
+++ b/Ganimede/Ganimede/VideoProcessing/VideoAnalyzer.cs
@@ -0,0 +1,76 @@
+using System;
+using System.IO;
+using FFMediaToolkit;
+using FFMediaToolkit.Decoding;
+
+namespace Ganimede.VideoProcessing
+{
+ ///
+ /// Provides video analysis capabilities using FFMediaToolkit
+ ///
+ public class VideoAnalyzer
+ {
+ ///
+ /// Analyzes a video file and returns its metadata
+ ///
+ public static VideoMetadata Analyze(string videoPath)
+ {
+ if (!File.Exists(videoPath))
+ throw new FileNotFoundException($"Video file not found: {videoPath}");
+
+ try
+ {
+ using var file = MediaFile.Open(videoPath);
+
+ if (!file.HasVideo)
+ throw new InvalidOperationException("The file does not contain a video stream");
+
+ var video = file.Video;
+ var info = video.Info;
+
+ // Get frame rate
+ double frameRate = info.AvgFrameRate;
+
+ // Calculate total frames from duration and frame rate
+ var duration = info.Duration;
+ int totalFrames = info.NumberOfFrames ?? (int)(duration.TotalSeconds * frameRate);
+
+ // Get video dimensions
+ int width = info.FrameSize.Width;
+ int height = info.FrameSize.Height;
+
+ // Estimate bitrate from file info
+ long bitrate = file.Info.Bitrate;
+
+ return new VideoMetadata
+ {
+ Duration = duration,
+ FrameRate = frameRate,
+ TotalFrames = totalFrames,
+ Width = width,
+ Height = height,
+ BitRate = bitrate,
+ CodecName = info.CodecName ?? "unknown"
+ };
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException($"Failed to analyze video: {ex.Message}", ex);
+ }
+ }
+ }
+
+ ///
+ /// Contains metadata information about a video file
+ ///
+ public class VideoMetadata
+ {
+ public TimeSpan Duration { get; set; }
+ public double FrameRate { get; set; }
+ public int TotalFrames { get; set; }
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public long BitRate { get; set; }
+ public string CodecName { get; set; } = string.Empty;
+ }
+}
diff --git a/Ganimede/Ganimede/Windows/SettingsWindow.xaml b/Ganimede/Ganimede/Windows/SettingsWindow.xaml
index dc8ad8a..57e1594 100644
--- a/Ganimede/Ganimede/Windows/SettingsWindow.xaml
+++ b/Ganimede/Ganimede/Windows/SettingsWindow.xaml
@@ -4,7 +4,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
- Title="Impostazioni" Height="600" Width="640"
+ Title="Impostazioni" Height="550" Width="640"
Background="#1E2228" WindowStartupLocation="CenterOwner">
@@ -19,24 +19,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Ganimede/Ganimede/Windows/SettingsWindow.xaml.cs b/Ganimede/Ganimede/Windows/SettingsWindow.xaml.cs
index 6a98641..5193c70 100644
--- a/Ganimede/Ganimede/Windows/SettingsWindow.xaml.cs
+++ b/Ganimede/Ganimede/Windows/SettingsWindow.xaml.cs
@@ -20,7 +20,6 @@ namespace Ganimede.Windows
private void LoadSettings()
{
- FFmpegPathTextBox.Text = Settings.Default.FFmpegBinFolder;
DefaultOutputTextBox.Text = Settings.Default.LastOutputFolder;
CreateSubfolderCheckBox.IsChecked = Settings.Default.CreateSubfolder;
var singleFrameChk = GetCheckBox("SingleFrameUseSubfolderCheckBox");
@@ -55,8 +54,6 @@ namespace Ganimede.Windows
default:
GetDefaultModeRadio("DefaultModeFullRadio")!.IsChecked = true; break;
}
-
- UpdateFFmpegStatus();
}
private string GetSelectedDefaultExtractionMode()
@@ -68,43 +65,6 @@ namespace Ganimede.Windows
private bool GetSingleFrameUseSubfolder() => GetCheckBox("SingleFrameUseSubfolderCheckBox")?.IsChecked == true;
- private void UpdateFFmpegStatus()
- {
- var path = FFmpegPathTextBox.Text;
- if (string.IsNullOrEmpty(path))
- {
- FFmpegStatusText.Text = "Nessun percorso specificato";
- FFmpegStatusText.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Orange);
- }
- else if (ValidateFFMpegPath(path))
- {
- FFmpegStatusText.Text = "? FFmpeg valido";
- FFmpegStatusText.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.LightGreen);
- }
- else
- {
- FFmpegStatusText.Text = "? Binari FFmpeg non trovati";
- FFmpegStatusText.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Red);
- }
- }
-
- private bool ValidateFFMpegPath(string path)
- {
- if (!Directory.Exists(path)) return false;
- return File.Exists(Path.Combine(path, "ffmpeg.exe")) && File.Exists(Path.Combine(path, "ffprobe.exe"));
- }
-
- private void BrowseFFmpegButton_Click(object sender, RoutedEventArgs e)
- {
- using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona cartella binari FFmpeg", ShowNewFolderButton = false };
- if (!string.IsNullOrEmpty(FFmpegPathTextBox.Text)) dialog.SelectedPath = FFmpegPathTextBox.Text;
- if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
- {
- FFmpegPathTextBox.Text = dialog.SelectedPath;
- UpdateFFmpegStatus();
- }
- }
-
private void BrowseOutputButton_Click(object sender, RoutedEventArgs e)
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog { Description = "Seleziona cartella output predefinita", ShowNewFolderButton = true };
@@ -119,7 +79,6 @@ namespace Ganimede.Windows
{
try
{
- Settings.Default.FFmpegBinFolder = FFmpegPathTextBox.Text;
Settings.Default.LastOutputFolder = DefaultOutputTextBox.Text;
Settings.Default.CreateSubfolder = CreateSubfolderCheckBox.IsChecked ?? true;
var selectedFrameSizeItem = FrameSizeComboBox.SelectedItem as System.Windows.Controls.ComboBoxItem;