diff --git a/Azaion.Annotator/App.xaml.cs b/Azaion.Annotator/App.xaml.cs index 78103e1..afc9b8e 100644 --- a/Azaion.Annotator/App.xaml.cs +++ b/Azaion.Annotator/App.xaml.cs @@ -48,7 +48,7 @@ public partial class App : Application }); services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService().Get()); - services.AddSingleton(); + services.AddSingleton(); }) .UseSerilog() .Build(); diff --git a/Azaion.Annotator/Azaion.Annotator.csproj b/Azaion.Annotator/Azaion.Annotator.csproj index 5850804..e657879 100644 --- a/Azaion.Annotator/Azaion.Annotator.csproj +++ b/Azaion.Annotator/Azaion.Annotator.csproj @@ -37,7 +37,7 @@ PreserveNewest - Always + PreserveNewest diff --git a/Azaion.Annotator/DTO/Config.cs b/Azaion.Annotator/DTO/Config.cs index 52e4d8c..6cb0969 100644 --- a/Azaion.Annotator/DTO/Config.cs +++ b/Azaion.Annotator/DTO/Config.cs @@ -36,6 +36,7 @@ public class Config public List ImageFormats { get; set; } public ThumbnailConfig ThumbnailConfig { get; set; } + public int? LastSelectedExplorerClass { get; set; } } public class WindowConfig diff --git a/Azaion.Annotator/DTO/FormState.cs b/Azaion.Annotator/DTO/FormState.cs index a52d5be..baa35d0 100644 --- a/Azaion.Annotator/DTO/FormState.cs +++ b/Azaion.Annotator/DTO/FormState.cs @@ -17,6 +17,7 @@ public class FormState public int CurrentVolume { get; set; } = 100; public ObservableCollection AnnotationResults { get; set; } = []; + public WindowsEnum ActiveWindow { get; set; } public string GetTimeName(TimeSpan ts) => $"{VideoName}_{ts:hmmssf}"; @@ -38,4 +39,4 @@ public class FormState } catch (Exception e) { return null; } } -} +} \ No newline at end of file diff --git a/Azaion.Annotator/DTO/WindowsEnum.cs b/Azaion.Annotator/DTO/WindowsEnum.cs new file mode 100644 index 0000000..e9e9574 --- /dev/null +++ b/Azaion.Annotator/DTO/WindowsEnum.cs @@ -0,0 +1,8 @@ +namespace Azaion.Annotator.DTO; + +public enum WindowsEnum +{ + None = 0, + Main = 10, + DatasetExplorer = 20 +} \ No newline at end of file diff --git a/Azaion.Annotator/DatasetExplorer.xaml.cs b/Azaion.Annotator/DatasetExplorer.xaml.cs index bcc7671..2ac2db1 100644 --- a/Azaion.Annotator/DatasetExplorer.xaml.cs +++ b/Azaion.Annotator/DatasetExplorer.xaml.cs @@ -1,6 +1,7 @@ using System.Collections.ObjectModel; using System.IO; using System.Windows; +using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -24,16 +25,23 @@ public partial class DatasetExplorer private readonly string _thumbnailsCacheFile; private IConfigRepository _configRepository; private readonly FormState _formState; + private readonly IGalleryManager _galleryManager; private static Dictionary> LabelsCache { get; set; } = new(); - public string CurrentImage { get; set; } + public ThumbnailDto? CurrentThumbnail { get; set; } - public DatasetExplorer(Config config, ILogger logger, IConfigRepository configRepository, FormState formState) + public DatasetExplorer( + Config config, + ILogger logger, + IConfigRepository configRepository, + FormState formState, + IGalleryManager galleryManager) { _config = config; _logger = logger; _configRepository = configRepository; _formState = formState; + _galleryManager = galleryManager; _thumbnailsCacheFile = Path.Combine(config.ThumbnailsDirectory, Config.ThumbnailsCacheFile); if (File.Exists(_thumbnailsCacheFile)) { @@ -43,19 +51,45 @@ public partial class DatasetExplorer InitializeComponent(); DataContext = this; - Loaded += (_, _) => + Loaded += async (_, _) => { AllAnnotationClasses = new ObservableCollection( new List { new(-1, "All") } .Concat(_config.AnnotationClasses)); LvClasses.ItemsSource = AllAnnotationClasses; - LvClasses.SelectionChanged += async (_, _) => + LvClasses.MouseUp += async (_, _) => { var selectedClass = (AnnotationClass)LvClasses.SelectedItem; - await SelectClass(selectedClass); + ExplorerEditor.CurrentAnnClass = selectedClass; + config.LastSelectedExplorerClass = selectedClass.Id; + + if (Switcher.SelectedIndex == 0) + await ReloadThumbnails(); + else + foreach (var ann in ExplorerEditor.CurrentAnns.Where(x => x.IsSelected)) + ann.AnnotationClass = selectedClass; }; - LvClasses.SelectedIndex = 0; + + LvClasses.SelectionChanged += (_, _) => + { + if (Switcher.SelectedIndex != 1) + return; + + var selectedClass = (AnnotationClass)LvClasses.SelectedItem; + if (selectedClass == null) + return; + + ExplorerEditor.CurrentAnnClass = selectedClass; + + foreach (var ann in ExplorerEditor.CurrentAnns.Where(x => x.IsSelected)) + ann.AnnotationClass = selectedClass; + }; + + + LvClasses.SelectedIndex = config.LastSelectedExplorerClass ?? 0; + ExplorerEditor.CurrentAnnClass = (AnnotationClass)LvClasses.SelectedItem; + await ReloadThumbnails(); SizeChanged += async (_, _) => await SaveUserSettings(); LocationChanged += async (_, _) => await SaveUserSettings(); @@ -68,7 +102,7 @@ public partial class DatasetExplorer Visibility = Visibility.Hidden; }; - ThumbnailsView.KeyDown += (sender, args) => + ThumbnailsView.KeyDown += async (sender, args) => { switch (args.Key) { @@ -76,29 +110,15 @@ public partial class DatasetExplorer DeleteAnnotations(); break; case Key.Enter: - EditAnnotation(); + await EditAnnotation(); break; } }; - ThumbnailsView.MouseDoubleClick += async (_, _) => await EditAnnotation(); - ExplorerEditor.KeyDown += (_, args) => - { - var key = args.Key; - var keyNumber = (int?)null; + Activated += (_, _) => { _formState.ActiveWindow = WindowsEnum.DatasetExplorer; }; - if ((int)key >= (int)Key.D1 && (int)key <= (int)Key.D9) - keyNumber = key - Key.D1; - if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9) - keyNumber = key - Key.NumPad1; - if (!keyNumber.HasValue) - return; - - LvClasses.SelectedIndex = keyNumber.Value; - }; - - ExplorerEditor.GetTimeFunc = () => _formState.GetTime(CurrentImage!)!.Value; + ExplorerEditor.GetTimeFunc = () => _formState.GetTime(CurrentThumbnail!.ImagePath)!.Value; } private async Task EditAnnotation() @@ -111,11 +131,12 @@ public partial class DatasetExplorer { ImageSource = new BitmapImage(new Uri(dto.ImagePath)) }; - CurrentImage = dto.ImagePath; + CurrentThumbnail = dto; + Switcher.SelectedIndex = 1; LvClasses.SelectedIndex = 1; - var time = _formState.GetTime(CurrentImage)!.Value; + var time = _formState.GetTime(dto.ImagePath)!.Value; foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath)) { var annClass = _config.AnnotationClasses[ann.ClassNumber]; @@ -123,33 +144,23 @@ public partial class DatasetExplorer Dispatcher.Invoke(() => ExplorerEditor.CreateAnnotation(annClass, time, annInfo)); } - Switcher.SelectionChanged += (_, args) => + Switcher.SelectionChanged += (sender, args) => { - //From Explorer to Editor if (Switcher.SelectedIndex == 1) { + //Editor _tempSelectedClassIdx = LvClasses.SelectedIndex; LvClasses.ItemsSource = _config.AnnotationClasses; } else { + //Explorer LvClasses.ItemsSource = AllAnnotationClasses; LvClasses.SelectedIndex = _tempSelectedClassIdx; } }; } - private async Task SelectClass(AnnotationClass annClass) - { - ExplorerEditor.CurrentAnnClass = annClass; - - if (Switcher.SelectedIndex == 0) - await ReloadThumbnails(); - else - foreach (var ann in ExplorerEditor.CurrentAnns.Where(x => x.IsSelected)) - ann.AnnotationClass = annClass; - } - private async Task SaveUserSettings() { _config.DatasetExplorerConfig = this.GetConfig(); diff --git a/Azaion.Annotator/DatasetExplorerEventHandler.cs b/Azaion.Annotator/DatasetExplorerEventHandler.cs new file mode 100644 index 0000000..7225e4d --- /dev/null +++ b/Azaion.Annotator/DatasetExplorerEventHandler.cs @@ -0,0 +1,62 @@ +using System.IO; +using System.Windows.Input; +using Azaion.Annotator.DTO; +using MediatR; + +namespace Azaion.Annotator; + +public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, + Config config, + IGalleryManager galleryManager, + FormState formState) : INotificationHandler +{ + private readonly Dictionary _keysControlEnumDict = new() + { + { Key.Enter, PlaybackControlEnum.SaveAnnotations }, + { Key.Delete, PlaybackControlEnum.RemoveSelectedAnns }, + { Key.X, PlaybackControlEnum.RemoveAllAnns } + }; + + public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken) + { + if (formState.ActiveWindow != WindowsEnum.DatasetExplorer) + return; + + var key = keyEvent.Args.Key; + var keyNumber = (int?)null; + + if ((int)key >= (int)Key.D1 && (int)key <= (int)Key.D9) keyNumber = key - Key.D1; + if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9) keyNumber = key - Key.NumPad1; + + if (keyNumber.HasValue) + datasetExplorer.LvClasses.SelectedIndex = keyNumber.Value; + else + { + if (datasetExplorer.Switcher.SelectedIndex == 1 && _keysControlEnumDict.TryGetValue(key, out var value)) + await HandleControl(value); + } + } + + private async Task HandleControl(PlaybackControlEnum controlEnum) + { + switch (controlEnum) + { + case PlaybackControlEnum.SaveAnnotations: + var currentAnns = datasetExplorer.ExplorerEditor.CurrentAnns + .Select(x => new YoloLabel(x.Info, datasetExplorer.ExplorerEditor.RenderSize, datasetExplorer.ExplorerEditor.RenderSize)) + .ToList(); + + await YoloLabel.WriteToFile(currentAnns, Path.Combine(config.LabelsDirectory, datasetExplorer.CurrentThumbnail!.LabelPath)); + await galleryManager.CreateThumbnail(datasetExplorer.CurrentThumbnail.ImagePath); + + datasetExplorer.Switcher.SelectedIndex = 0; + break; + case PlaybackControlEnum.RemoveSelectedAnns: + datasetExplorer.ExplorerEditor.RemoveSelectedAnns(); + break; + case PlaybackControlEnum.RemoveAllAnns: + datasetExplorer.ExplorerEditor.RemoveAllAnns(); + break; + } + } +} diff --git a/Azaion.Annotator/Extensions/ThrottleExtensions.cs b/Azaion.Annotator/Extensions/ThrottleExtensions.cs index c9467c5..9cd9520 100644 --- a/Azaion.Annotator/Extensions/ThrottleExtensions.cs +++ b/Azaion.Annotator/Extensions/ThrottleExtensions.cs @@ -10,9 +10,9 @@ public static class ThrottleExt _throttleOn = true; await func(); - _ = Task.Run(() => + _ = Task.Run(async () => { - Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500)); + await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500)); _throttleOn = false; }); } diff --git a/Azaion.Annotator/GalleryManager.cs b/Azaion.Annotator/GalleryManager.cs index 3470a94..7da9033 100644 --- a/Azaion.Annotator/GalleryManager.cs +++ b/Azaion.Annotator/GalleryManager.cs @@ -14,16 +14,27 @@ public class GalleryManager(Config config, ILogger logger) : IGa public int ThumbnailsCount { get; set; } public int ImagesCount { get; set; } + private DirectoryInfo? _thumbnailsDirectory; + private DirectoryInfo ThumbnailsDirectory + { + get + { + if (_thumbnailsDirectory != null) + return _thumbnailsDirectory; + + var dir = new DirectoryInfo(config.ThumbnailsDirectory); + if (!dir.Exists) + Directory.CreateDirectory(config.ThumbnailsDirectory); + _thumbnailsDirectory = new DirectoryInfo(config.ThumbnailsDirectory); + return _thumbnailsDirectory; + } + } + public async Task RefreshThumbnails() { - var dir = new DirectoryInfo(config.ThumbnailsDirectory); - if (!dir.Exists) - Directory.CreateDirectory(config.ThumbnailsDirectory); - var prefixLen = Config.ThumbnailPrefix.Length; - var thumbnailsDir = new DirectoryInfo(config.ThumbnailsDirectory); - var thumbnails = thumbnailsDir.GetFiles() + var thumbnails = ThumbnailsDirectory.GetFiles() .Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen]) .GroupBy(x => x) .Select(gr => gr.Key) @@ -38,15 +49,9 @@ public class GalleryManager(Config config, ILogger logger) : IGa var imgName = Path.GetFileNameWithoutExtension(img.Name); if (thumbnails.Contains(imgName)) continue; - try { - var bitmap = await GenerateThumbnail(img); - if (bitmap != null) - { - var thumbnailName = Path.Combine(thumbnailsDir.FullName, $"{imgName}{Config.ThumbnailPrefix}.jpg"); - bitmap.Save(thumbnailName, ImageFormat.Jpeg); - } + await CreateThumbnail(img.FullName); } catch (Exception e) { @@ -57,15 +62,25 @@ public class GalleryManager(Config config, ILogger logger) : IGa } } - private async Task GenerateThumbnail(FileInfo img) + public async Task CreateThumbnail(string imgPath) + { + var bitmap = await GenerateThumbnail(imgPath); + if (bitmap != null) + { + var thumbnailName = Path.Combine(ThumbnailsDirectory.FullName, $"{Path.GetFileNameWithoutExtension(imgPath)}{Config.ThumbnailPrefix}.jpg"); + bitmap.Save(thumbnailName, ImageFormat.Jpeg); + } + } + + private async Task GenerateThumbnail(string imgPath) { var width = (int)config.ThumbnailConfig.Size.Width; var height = (int)config.ThumbnailConfig.Size.Height; - var imgName = Path.GetFileNameWithoutExtension(img.Name); - var labelName = Path.Combine(config.LabelsDirectory, $"{imgName}.txt"); + var imgName = Path.GetFileName(imgPath); + var labelName = Path.Combine(config.LabelsDirectory, $"{Path.GetFileNameWithoutExtension(imgPath)}.txt"); - var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(img.FullName))); + var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(imgPath))); var bitmap = new Bitmap(width, height); @@ -77,8 +92,8 @@ public class GalleryManager(Config config, ILogger logger) : IGa var size = new Size(originalImage.Width, originalImage.Height); if (!File.Exists(labelName)) { - File.Move(img.FullName, Path.Combine(config.UnknownImages, Path.GetFileName(img.Name))); - logger.LogInformation($"No labels found for image {img.Name}! Moved image to the {config.UnknownImages} folder."); + File.Move(imgPath, Path.Combine(config.UnknownImages, imgName)); + logger.LogInformation($"No labels found for image {imgName}! Moved image to the {config.UnknownImages} folder."); return null; } var labels = (await YoloLabel.ReadFromFile(labelName)) @@ -139,5 +154,6 @@ public interface IGalleryManager { int ThumbnailsCount { get; set; } int ImagesCount { get; set; } + Task CreateThumbnail(string imgPath); Task RefreshThumbnails(); } \ No newline at end of file diff --git a/Azaion.Annotator/MainWindow.xaml.cs b/Azaion.Annotator/MainWindow.xaml.cs index e4c00bc..e24dec5 100644 --- a/Azaion.Annotator/MainWindow.xaml.cs +++ b/Azaion.Annotator/MainWindow.xaml.cs @@ -75,6 +75,8 @@ public partial class MainWindow Directory.CreateDirectory(_config.ResultsDirectory); Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time); + + Activated += (_, _) => { _formState.ActiveWindow = WindowsEnum.Main; }; } private void VideoView_Loaded(object sender, RoutedEventArgs e) diff --git a/Azaion.Annotator/PlayerControlHandler.cs b/Azaion.Annotator/MainWindowEventHandler.cs similarity index 97% rename from Azaion.Annotator/PlayerControlHandler.cs rename to Azaion.Annotator/MainWindowEventHandler.cs index 251578d..9902348 100644 --- a/Azaion.Annotator/PlayerControlHandler.cs +++ b/Azaion.Annotator/MainWindowEventHandler.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging; namespace Azaion.Annotator; -public class PlayerControlHandler : +public class MainWindowEventHandler : INotificationHandler, INotificationHandler, INotificationHandler, @@ -19,7 +19,7 @@ public class PlayerControlHandler : private readonly MainWindow _mainWindow; private readonly FormState _formState; private readonly Config _config; - private readonly ILogger _logger; + private readonly ILogger _logger; private const int STEP = 20; private const int LARGE_STEP = 5000; @@ -37,12 +37,12 @@ public class PlayerControlHandler : { Key.PageDown, PlaybackControlEnum.Next }, }; - public PlayerControlHandler(LibVLC libVLC, + public MainWindowEventHandler(LibVLC libVLC, MediaPlayer mediaPlayer, MainWindow mainWindow, FormState formState, Config config, - ILogger logger) + ILogger logger) { _libVLC = libVLC; _mediaPlayer = mediaPlayer; @@ -69,6 +69,9 @@ public class PlayerControlHandler : public async Task Handle(KeyEvent notification, CancellationToken cancellationToken) { + if (_formState.ActiveWindow != WindowsEnum.Main) + return; + var key = notification.Args.Key; var keyNumber = (int?)null;