From 52371ace3aba6c255242b9dbc79de4d9e1831857 Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Tue, 10 Sep 2024 17:10:54 +0300 Subject: [PATCH] add editor, fix some bugs WIP --- Azaion.Annotator/Azaion.Annotator.csproj | 2 +- .../Controls/AnnotationClasses.xaml | 50 +++++ .../Controls/AnnotationClasses.xaml.cs | 11 + Azaion.Annotator/Controls/CanvasEditor.cs | 61 ++++-- Azaion.Annotator/DTO/Config.cs | 29 ++- Azaion.Annotator/DTO/FormState.cs | 2 - Azaion.Annotator/DTO/Label.cs | 62 +++--- Azaion.Annotator/DatasetExplorer.xaml | 81 ++++++- Azaion.Annotator/DatasetExplorer.xaml.cs | 202 +++++++++++++++++- .../Extensions/ColorExtensions.cs | 4 +- .../Extensions/WindowExtensions.cs | 15 ++ Azaion.Annotator/GalleryManager.cs | 17 +- Azaion.Annotator/MainWindow.xaml | 50 +---- Azaion.Annotator/MainWindow.xaml.cs | 33 ++- Azaion.Annotator/PlayerControlHandler.cs | 13 +- Azaion.Annotator/config.json | 14 +- 16 files changed, 498 insertions(+), 148 deletions(-) create mode 100644 Azaion.Annotator/Controls/AnnotationClasses.xaml create mode 100644 Azaion.Annotator/Controls/AnnotationClasses.xaml.cs create mode 100644 Azaion.Annotator/Extensions/WindowExtensions.cs diff --git a/Azaion.Annotator/Azaion.Annotator.csproj b/Azaion.Annotator/Azaion.Annotator.csproj index e657879..5850804 100644 --- a/Azaion.Annotator/Azaion.Annotator.csproj +++ b/Azaion.Annotator/Azaion.Annotator.csproj @@ -37,7 +37,7 @@ PreserveNewest - PreserveNewest + Always diff --git a/Azaion.Annotator/Controls/AnnotationClasses.xaml b/Azaion.Annotator/Controls/AnnotationClasses.xaml new file mode 100644 index 0000000..c070efa --- /dev/null +++ b/Azaion.Annotator/Controls/AnnotationClasses.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Azaion.Annotator/Controls/AnnotationClasses.xaml.cs b/Azaion.Annotator/Controls/AnnotationClasses.xaml.cs new file mode 100644 index 0000000..70b9dd4 --- /dev/null +++ b/Azaion.Annotator/Controls/AnnotationClasses.xaml.cs @@ -0,0 +1,11 @@ +using System.Windows.Controls; + +namespace Azaion.Annotator.Controls; + +public partial class AnnotationClasses : DataGrid +{ + public AnnotationClasses() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Azaion.Annotator/Controls/CanvasEditor.cs b/Azaion.Annotator/Controls/CanvasEditor.cs index c8e1ddc..8b5438a 100644 --- a/Azaion.Annotator/Controls/CanvasEditor.cs +++ b/Azaion.Annotator/Controls/CanvasEditor.cs @@ -13,7 +13,8 @@ namespace Azaion.Annotator.Controls; public class CanvasEditor : Canvas { private Point _lastPos; - + public SelectionState SelectionState { get; set; } = SelectionState.None; + private readonly Rectangle _newAnnotationRect; private Point _newAnnotationStartPos; @@ -30,6 +31,19 @@ public class CanvasEditor : Canvas public FormState FormState { get; set; } = null!; public IMediator Mediator { get; set; } = null!; + public static readonly DependencyProperty GetTimeFuncProp = + DependencyProperty.Register( + nameof(GetTimeFunc), + typeof(Func), + typeof(CanvasEditor), + new PropertyMetadata(null)); + + public Func GetTimeFunc + { + get => (Func)GetValue(GetTimeFuncProp); + set => SetValue(GetTimeFuncProp, value); + } + private AnnotationClass _currentAnnClass = null!; public AnnotationClass CurrentAnnClass { @@ -84,11 +98,7 @@ public class CanvasEditor : Canvas Stroke = new SolidColorBrush(Colors.Gray), Fill = new SolidColorBrush(Color.FromArgb(128, 128, 128, 128)), }; - Loaded += Init; - } - private void Init(object sender, RoutedEventArgs e) - { KeyDown += (_, args) => { Console.WriteLine($"pressed {args.Key}"); @@ -98,15 +108,21 @@ public class CanvasEditor : Canvas MouseUp += CanvasMouseUp; SizeChanged += CanvasResized; Cursor = Cursors.Cross; - _horizontalLine.X1 = 0; - _horizontalLine.X2 = ActualWidth; - _verticalLine.Y1 = 0; - _verticalLine.Y2 = ActualHeight; - + Children.Add(_newAnnotationRect); Children.Add(_horizontalLine); Children.Add(_verticalLine); Children.Add(_classNameHint); + + Loaded += Init; + } + + private void Init(object sender, RoutedEventArgs e) + { + _horizontalLine.X1 = 0; + _horizontalLine.X2 = ActualWidth; + _verticalLine.Y1 = 0; + _verticalLine.Y2 = ActualHeight; } private void CanvasMouseDown(object sender, MouseButtonEventArgs e) @@ -125,22 +141,22 @@ public class CanvasEditor : Canvas if (e.LeftButton != MouseButtonState.Pressed) return; - if (FormState.SelectionState == SelectionState.NewAnnCreating) + if (SelectionState == SelectionState.NewAnnCreating) NewAnnotationCreatingMove(sender, e); - if (FormState.SelectionState == SelectionState.AnnResizing) + if (SelectionState == SelectionState.AnnResizing) AnnotationResizeMove(sender, e); - if (FormState.SelectionState == SelectionState.AnnMoving) + if (SelectionState == SelectionState.AnnMoving) AnnotationPositionMove(sender, e); } private void CanvasMouseUp(object sender, MouseButtonEventArgs e) { - if (FormState.SelectionState == SelectionState.NewAnnCreating) + if (SelectionState == SelectionState.NewAnnCreating) CreateAnnotation(e.GetPosition(this)); - FormState.SelectionState = SelectionState.None; + SelectionState = SelectionState.None; e.Handled = true; } @@ -154,7 +170,7 @@ public class CanvasEditor : Canvas private void AnnotationResizeStart(object sender, MouseEventArgs e) { - FormState.SelectionState = SelectionState.AnnResizing; + SelectionState = SelectionState.AnnResizing; _lastPos = e.GetPosition(this); _curRec = (Rectangle)sender; _curAnn = (AnnotationControl)((Grid)_curRec.Parent).Parent; @@ -163,7 +179,7 @@ public class CanvasEditor : Canvas private void AnnotationResizeMove(object sender, MouseEventArgs e) { - if (FormState.SelectionState != SelectionState.AnnResizing) + if (SelectionState != SelectionState.AnnResizing) return; var currentPos = e.GetPosition(this); @@ -224,13 +240,13 @@ public class CanvasEditor : Canvas _curAnn.IsSelected = true; - FormState.SelectionState = SelectionState.AnnMoving; + SelectionState = SelectionState.AnnMoving; e.Handled = true; } private void AnnotationPositionMove(object sender, MouseEventArgs e) { - if (FormState.SelectionState != SelectionState.AnnMoving) + if (SelectionState != SelectionState.AnnMoving) return; var currentPos = e.GetPosition(this); @@ -255,12 +271,12 @@ public class CanvasEditor : Canvas SetTop(_newAnnotationRect, _newAnnotationStartPos.Y); _newAnnotationRect.MouseMove += NewAnnotationCreatingMove; - FormState.SelectionState = SelectionState.NewAnnCreating; + SelectionState = SelectionState.NewAnnCreating; } private void NewAnnotationCreatingMove(object sender, MouseEventArgs e) { - if (FormState.SelectionState != SelectionState.NewAnnCreating) + if (SelectionState != SelectionState.NewAnnCreating) return; var currentPos = e.GetPosition(this); @@ -284,8 +300,7 @@ public class CanvasEditor : Canvas if (width < MIN_SIZE || height < MIN_SIZE) return; - var mainWindow = (MainWindow)((Window)((Grid)Parent).Parent).Owner; - var time = TimeSpan.FromMilliseconds(mainWindow.VideoView.MediaPlayer.Time); + var time = GetTimeFunc(); CreateAnnotation(CurrentAnnClass, time, new CanvasLabel { Width = width, diff --git a/Azaion.Annotator/DTO/Config.cs b/Azaion.Annotator/DTO/Config.cs index 4836f63..52e4d8c 100644 --- a/Azaion.Annotator/DTO/Config.cs +++ b/Azaion.Annotator/DTO/Config.cs @@ -1,6 +1,5 @@ using System.IO; using System.Text; -using System.Windows.Media; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Size = System.Windows.Size; @@ -11,21 +10,22 @@ namespace Azaion.Annotator.DTO; public class Config { public const string ThumbnailPrefix = "_thumb"; + public const string ThumbnailsCacheFile = "thumbnails.cache"; public string VideosDirectory { get; set; } public string LabelsDirectory { get; set; } public string ImagesDirectory { get; set; } public string ResultsDirectory { get; set; } public string ThumbnailsDirectory { get; set; } + public string UnknownImages { get; set; } public List AnnotationClasses { get; set; } = []; private Dictionary? _annotationClassesDict; public Dictionary AnnotationClassesDict => _annotationClassesDict ??= AnnotationClasses.ToDictionary(x => x.Id); - public Size WindowSize { get; set; } - public Point WindowLocation { get; set; } - public bool FullScreen { get; set; } + public WindowConfig MainWindowConfig { get; set; } + public WindowConfig DatasetExplorerConfig { get; set; } public double LeftPanelWidth { get; set; } public double RightPanelWidth { get; set; } @@ -38,6 +38,13 @@ public class Config public ThumbnailConfig ThumbnailConfig { get; set; } } +public class WindowConfig +{ + public Size WindowSize { get; set; } + public Point WindowLocation { get; set; } + public bool FullScreen { get; set; } +} + public class ThumbnailConfig { public Size Size { get; set; } @@ -60,6 +67,7 @@ public class FileConfigRepository(ILogger logger) : IConfi private const string DEFAULT_IMAGES_DIR = "images"; private const string DEFAULT_RESULTS_DIR = "results"; private const string DEFAULT_THUMBNAILS_DIR = "thumbnails"; + private const string DEFAULT_UNKNOWN_IMG_DIR = "unknown"; private static readonly Size DefaultWindowSize = new(1280, 720); private static readonly Point DefaultWindowLocation = new(100, 100); @@ -82,9 +90,18 @@ public class FileConfigRepository(ILogger logger) : IConfi ImagesDirectory = Path.Combine(exePath, DEFAULT_IMAGES_DIR), ResultsDirectory = Path.Combine(exePath, DEFAULT_RESULTS_DIR), ThumbnailsDirectory = Path.Combine(exePath, DEFAULT_THUMBNAILS_DIR), + UnknownImages = Path.Combine(exePath, DEFAULT_UNKNOWN_IMG_DIR), - WindowLocation = DefaultWindowLocation, - WindowSize = DefaultWindowSize, + MainWindowConfig = new WindowConfig + { + WindowSize = DefaultWindowSize, + WindowLocation = DefaultWindowLocation + }, + DatasetExplorerConfig = new WindowConfig + { + WindowSize = DefaultWindowSize, + WindowLocation = DefaultWindowLocation + }, ShowHelpOnStart = true, VideoFormats = DefaultVideoFormats, diff --git a/Azaion.Annotator/DTO/FormState.cs b/Azaion.Annotator/DTO/FormState.cs index c92d0e5..a52d5be 100644 --- a/Azaion.Annotator/DTO/FormState.cs +++ b/Azaion.Annotator/DTO/FormState.cs @@ -6,8 +6,6 @@ namespace Azaion.Annotator.DTO; public class FormState { - public SelectionState SelectionState { get; set; } = SelectionState.None; - public MediaFileInfo? CurrentMedia { get; set; } public string VideoName => string.IsNullOrEmpty(CurrentMedia?.Name) ? "" diff --git a/Azaion.Annotator/DTO/Label.cs b/Azaion.Annotator/DTO/Label.cs index b282fa0..fb49a13 100644 --- a/Azaion.Annotator/DTO/Label.cs +++ b/Azaion.Annotator/DTO/Label.cs @@ -7,11 +7,16 @@ namespace Azaion.Annotator.DTO; public abstract class Label { - [JsonProperty(PropertyName = "cl")] - public int ClassNumber { get; set; } - - public Label(){} - public Label(int classNumber) { ClassNumber = classNumber; } + [JsonProperty(PropertyName = "cl")] public int ClassNumber { get; set; } + + public Label() + { + } + + public Label(int classNumber) + { + ClassNumber = classNumber; + } } public class CanvasLabel : Label @@ -20,8 +25,11 @@ public class CanvasLabel : Label public double Y { get; set; } public double Width { get; set; } public double Height { get; set; } - - public CanvasLabel() { } + + public CanvasLabel() + { + } + public CanvasLabel(int classNumber, double x, double y, double width, double height) : base(classNumber) { X = x; @@ -36,17 +44,17 @@ public class CanvasLabel : Label var ch = canvasSize.Height; var canvasAr = cw / ch; var videoAr = videoSize.Width / videoSize.Height; - + ClassNumber = label.ClassNumber; - + var left = label.CenterX - label.Width / 2; var top = label.CenterY - label.Height / 2; - + if (videoAr > canvasAr) //100% width { var realHeight = cw / videoAr; //real video height in pixels on canvas var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom - + X = left * cw; Y = top * realHeight + blackStripHeight; Width = label.Width * cw; @@ -67,19 +75,18 @@ public class CanvasLabel : Label public class YoloLabel : Label { - [JsonProperty(PropertyName = "x")] - public double CenterX { get; set; } + [JsonProperty(PropertyName = "x")] public double CenterX { get; set; } - [JsonProperty(PropertyName = "y")] - public double CenterY { get; set; } + [JsonProperty(PropertyName = "y")] public double CenterY { get; set; } - [JsonProperty(PropertyName = "w")] - public double Width { get; set; } + [JsonProperty(PropertyName = "w")] public double Width { get; set; } + + [JsonProperty(PropertyName = "h")] public double Height { get; set; } + + public YoloLabel() + { + } - [JsonProperty(PropertyName = "h")] - public double Height { get; set; } - - public YoloLabel() { } public YoloLabel(int classNumber, double centerX, double centerY, double width, double height) : base(classNumber) { CenterX = centerX; @@ -96,7 +103,7 @@ public class YoloLabel : Label var videoAr = videoSize.Width / videoSize.Height; ClassNumber = canvasLabel.ClassNumber; - + double left, top; if (videoAr > canvasAr) //100% width { @@ -125,8 +132,8 @@ public class YoloLabel : Label { if (string.IsNullOrEmpty(s)) return null; - - var strings = s.Replace(',','.').Split(' '); + + var strings = s.Replace(',', '.').Split(' '); if (strings.Length != 5) return null; @@ -158,6 +165,11 @@ public class YoloLabel : Label .ToList()!; } + public static async Task WriteToFile(IEnumerable labels, string filename) + { + var labelsStr = string.Join(Environment.NewLine, labels.Select(x => x.ToString())); + await File.WriteAllTextAsync(filename, labelsStr); + } + public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.'); - } \ No newline at end of file diff --git a/Azaion.Annotator/DatasetExplorer.xaml b/Azaion.Annotator/DatasetExplorer.xaml index b28a222..8843055 100644 --- a/Azaion.Annotator/DatasetExplorer.xaml +++ b/Azaion.Annotator/DatasetExplorer.xaml @@ -6,6 +6,7 @@ xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel" xmlns:local="clr-namespace:Azaion.Annotator" xmlns:dto="clr-namespace:Azaion.Annotator.DTO" + xmlns:controls="clr-namespace:Azaion.Annotator.Controls" mc:Ignorable="d" Title="Браузер анотацій" Height="900" Width="1200"> @@ -28,16 +29,82 @@ - - - + + + + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + Background="Black"> + + + + + + + + + + + + + + + + + + + + - + + + + + + + + Loading: + + + + + + + + + + diff --git a/Azaion.Annotator/DatasetExplorer.xaml.cs b/Azaion.Annotator/DatasetExplorer.xaml.cs index 0b00114..bcc7671 100644 --- a/Azaion.Annotator/DatasetExplorer.xaml.cs +++ b/Azaion.Annotator/DatasetExplorer.xaml.cs @@ -1,55 +1,237 @@ using System.Collections.ObjectModel; using System.IO; using System.Windows; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; using Azaion.Annotator.DTO; +using Azaion.Annotator.Extensions; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using MessageBox = System.Windows.MessageBox; namespace Azaion.Annotator; public partial class DatasetExplorer { private readonly Config _config; + private readonly ILogger _logger; public ObservableCollection ThumbnailsDtos { get; set; } = new(); + private ObservableCollection AllAnnotationClasses { get; set; } = new(); - public DatasetExplorer(Config config) + private int _tempSelectedClassIdx = 0; + private readonly string _thumbnailsCacheFile; + private IConfigRepository _configRepository; + private readonly FormState _formState; + private static Dictionary> LabelsCache { get; set; } = new(); + + public string CurrentImage { get; set; } + + public DatasetExplorer(Config config, ILogger logger, IConfigRepository configRepository, FormState formState) { _config = config; + _logger = logger; + _configRepository = configRepository; + _formState = formState; + _thumbnailsCacheFile = Path.Combine(config.ThumbnailsDirectory, Config.ThumbnailsCacheFile); + if (File.Exists(_thumbnailsCacheFile)) + { + var cache = JsonConvert.DeserializeObject>>(File.ReadAllText(_thumbnailsCacheFile)); + LabelsCache = cache ?? new Dictionary>(); + } InitializeComponent(); DataContext = this; - Loaded += async (sender, args) => await LoadThumbnails(); + Loaded += (_, _) => + { + AllAnnotationClasses = new ObservableCollection( + new List { new(-1, "All") } + .Concat(_config.AnnotationClasses)); + LvClasses.ItemsSource = AllAnnotationClasses; + + LvClasses.SelectionChanged += async (_, _) => + { + var selectedClass = (AnnotationClass)LvClasses.SelectedItem; + await SelectClass(selectedClass); + }; + LvClasses.SelectedIndex = 0; + + SizeChanged += async (_, _) => await SaveUserSettings(); + LocationChanged += async (_, _) => await SaveUserSettings(); + StateChanged += async (_, _) => await SaveUserSettings(); + }; Closing += (sender, args) => { args.Cancel = true; Visibility = Visibility.Hidden; }; + + ThumbnailsView.KeyDown += (sender, args) => + { + switch (args.Key) + { + case Key.Delete: + DeleteAnnotations(); + break; + case Key.Enter: + EditAnnotation(); + break; + } + }; + + ThumbnailsView.MouseDoubleClick += async (_, _) => await EditAnnotation(); + + ExplorerEditor.KeyDown += (_, args) => + { + var key = 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) + return; + + LvClasses.SelectedIndex = keyNumber.Value; + }; + + ExplorerEditor.GetTimeFunc = () => _formState.GetTime(CurrentImage!)!.Value; } - private async Task LoadThumbnails() + private async Task EditAnnotation() + { + if (ThumbnailsView.SelectedItem == null) + return; + + var dto = (ThumbnailsView.SelectedItem as ThumbnailDto)!; + ExplorerEditor.Background = new ImageBrush + { + ImageSource = new BitmapImage(new Uri(dto.ImagePath)) + }; + CurrentImage = dto.ImagePath; + Switcher.SelectedIndex = 1; + LvClasses.SelectedIndex = 1; + + var time = _formState.GetTime(CurrentImage)!.Value; + foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath)) + { + var annClass = _config.AnnotationClasses[ann.ClassNumber]; + var annInfo = new CanvasLabel(ann, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize); + Dispatcher.Invoke(() => ExplorerEditor.CreateAnnotation(annClass, time, annInfo)); + } + + Switcher.SelectionChanged += (_, args) => + { + //From Explorer to Editor + if (Switcher.SelectedIndex == 1) + { + _tempSelectedClassIdx = LvClasses.SelectedIndex; + LvClasses.ItemsSource = _config.AnnotationClasses; + } + else + { + 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(); + await ThrottleExt.Throttle(() => + { + _configRepository.Save(_config); + return Task.CompletedTask; + }, TimeSpan.FromSeconds(5)); + } + + private void DeleteAnnotations() + { + var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.YesNo, MessageBoxImage.Question); + if (result != MessageBoxResult.Yes) + return; + + var selected = ThumbnailsView.SelectedItems.Count; + for (var i = 0; i < selected; i++) + { + var dto = (ThumbnailsView.SelectedItems[0] as ThumbnailDto)!; + File.Delete(dto.ImagePath); + File.Delete(dto.LabelPath); + File.Delete(dto.ThumbnailPath); + ThumbnailsDtos.Remove(dto); + } + } + + private async Task ReloadThumbnails() { if (!Directory.Exists(_config.ThumbnailsDirectory)) return; + ThumbnailsDtos.Clear(); var thumbnails = Directory.GetFiles(_config.ThumbnailsDirectory, "*.jpg"); + var thumbNum = 0; foreach (var thumbnail in thumbnails) { - var name = Path.GetFileNameWithoutExtension(thumbnail)[..^Config.ThumbnailPrefix.Length]; - var imageName = Path.Combine(_config.ImagesDirectory, name); - foreach (var imageFormat in _config.ImageFormats) + await AddThumbnail(thumbnail); + + if (thumbNum % 1000 == 0) + await File.WriteAllTextAsync(_thumbnailsCacheFile, JsonConvert.SerializeObject(LabelsCache)); + thumbNum++; + } + await File.WriteAllTextAsync(_thumbnailsCacheFile, JsonConvert.SerializeObject(LabelsCache)); + } + + private async Task AddThumbnail(string thumbnail) + { + var name = Path.GetFileNameWithoutExtension(thumbnail)[..^Config.ThumbnailPrefix.Length]; + var imageName = Path.Combine(_config.ImagesDirectory, name); + foreach (var imageFormat in _config.ImageFormats) + { + imageName = $"{imageName}.{imageFormat}"; + if (File.Exists(imageName)) + break; + } + + var labelPath = Path.Combine(_config.LabelsDirectory, $"{name}.txt"); + + if (!LabelsCache.TryGetValue(name, out var classes)) + { + if (!File.Exists(labelPath)) { - imageName = $"{imageName}.{imageFormat}"; - if (File.Exists(imageName)) - break; + _logger.LogError($"No label {labelPath} found ! Image {(!File.Exists(imageName) ? "not exists!" : "exists.")}"); + return; } + var labels = await YoloLabel.ReadFromFile(labelPath); + classes = labels.Select(x => x.ClassNumber).Distinct().ToList(); + LabelsCache.Add(name, classes); + } + + if (classes.Contains(ExplorerEditor.CurrentAnnClass.Id) || ExplorerEditor.CurrentAnnClass.Id == -1) + { ThumbnailsDtos.Add(new ThumbnailDto { ThumbnailPath = thumbnail, ImagePath = imageName, - LabelPath = Path.Combine(_config.LabelsDirectory, $"{name}.txt"), + LabelPath = labelPath }); } + } } \ No newline at end of file diff --git a/Azaion.Annotator/Extensions/ColorExtensions.cs b/Azaion.Annotator/Extensions/ColorExtensions.cs index 44057d4..e96aa92 100644 --- a/Azaion.Annotator/Extensions/ColorExtensions.cs +++ b/Azaion.Annotator/Extensions/ColorExtensions.cs @@ -7,7 +7,9 @@ public static class ColorExtensions public static Color ToColor(this int id) { var index = id % ColorValues.Length; - var hex = $"#40{ColorValues[index]}"; + var hex = index == -1 + ? "#40DDDDDD" + : $"#40{ColorValues[index]}"; var color =(Color)ColorConverter.ConvertFromString(hex); return color; } diff --git a/Azaion.Annotator/Extensions/WindowExtensions.cs b/Azaion.Annotator/Extensions/WindowExtensions.cs new file mode 100644 index 0000000..48ec952 --- /dev/null +++ b/Azaion.Annotator/Extensions/WindowExtensions.cs @@ -0,0 +1,15 @@ +using System.Windows; +using Azaion.Annotator.DTO; + +namespace Azaion.Annotator.Extensions; + +public static class WindowExtensions +{ + public static WindowConfig GetConfig(this Window window) => + new() + { + WindowSize = new Size(window.Width, window.Height), + WindowLocation = new Point(window.Left, window.Top), + FullScreen = window.WindowState == WindowState.Maximized + }; +} \ No newline at end of file diff --git a/Azaion.Annotator/GalleryManager.cs b/Azaion.Annotator/GalleryManager.cs index d42be1b..3470a94 100644 --- a/Azaion.Annotator/GalleryManager.cs +++ b/Azaion.Annotator/GalleryManager.cs @@ -42,8 +42,11 @@ public class GalleryManager(Config config, ILogger logger) : IGa try { var bitmap = await GenerateThumbnail(img); - var thumbnailName = Path.Combine(thumbnailsDir.FullName, $"{imgName}{Config.ThumbnailPrefix}.jpg"); - bitmap.Save(thumbnailName, ImageFormat.Jpeg); + if (bitmap != null) + { + var thumbnailName = Path.Combine(thumbnailsDir.FullName, $"{imgName}{Config.ThumbnailPrefix}.jpg"); + bitmap.Save(thumbnailName, ImageFormat.Jpeg); + } } catch (Exception e) { @@ -54,7 +57,7 @@ public class GalleryManager(Config config, ILogger logger) : IGa } } - private async Task GenerateThumbnail(FileInfo img) + private async Task GenerateThumbnail(FileInfo img) { var width = (int)config.ThumbnailConfig.Size.Width; var height = (int)config.ThumbnailConfig.Size.Height; @@ -62,7 +65,7 @@ public class GalleryManager(Config config, ILogger logger) : IGa var imgName = Path.GetFileNameWithoutExtension(img.Name); var labelName = Path.Combine(config.LabelsDirectory, $"{imgName}.txt"); - var originalImage = Image.FromFile(img.FullName); + var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(img.FullName))); var bitmap = new Bitmap(width, height); @@ -72,6 +75,12 @@ public class GalleryManager(Config config, ILogger logger) : IGa g.InterpolationMode = InterpolationMode.Default; 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."); + return null; + } var labels = (await YoloLabel.ReadFromFile(labelName)) .Select(x => new CanvasLabel(x, size, size)) .ToList(); diff --git a/Azaion.Annotator/MainWindow.xaml b/Azaion.Annotator/MainWindow.xaml index 4fd13df..10b1eca 100644 --- a/Azaion.Annotator/MainWindow.xaml +++ b/Azaion.Annotator/MainWindow.xaml @@ -160,51 +160,11 @@ - - - - - - - - - - - - - - - - - - - - - + + AnnotationClasses { get; set; } = new(); + private ObservableCollection AnnotationClasses { get; set; } = new(); private bool _suspendLayout; private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(100); @@ -66,6 +66,15 @@ public partial class MainWindow VideoView.Loaded += VideoView_Loaded; Closed += OnFormClosed; + + if (!Directory.Exists(_config.LabelsDirectory)) + Directory.CreateDirectory(_config.LabelsDirectory); + if (!Directory.Exists(_config.ImagesDirectory)) + Directory.CreateDirectory(_config.ImagesDirectory); + if (!Directory.Exists(_config.ResultsDirectory)) + Directory.CreateDirectory(_config.ResultsDirectory); + + Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time); } private void VideoView_Loaded(object sender, RoutedEventArgs e) @@ -84,16 +93,22 @@ public partial class MainWindow _suspendLayout = true; - Left = _config.WindowLocation.X; - Top = _config.WindowLocation.Y; + Left = _config.MainWindowConfig.WindowLocation.X; + Top = _config.MainWindowConfig.WindowLocation.Y; + Width = _config.MainWindowConfig.WindowSize.Width; + Height = _config.MainWindowConfig.WindowSize.Height; - Width = _config.WindowSize.Width; - Height = _config.WindowSize.Height; + _datasetExplorer.Left = _config.MainWindowConfig.WindowLocation.X; + _datasetExplorer.Top = _config.DatasetExplorerConfig.WindowLocation.Y; + _datasetExplorer.Width = _config.DatasetExplorerConfig.WindowSize.Width; + _datasetExplorer.Height = _config.DatasetExplorerConfig.WindowSize.Height; + if (_config.DatasetExplorerConfig.FullScreen) + _datasetExplorer.WindowState = WindowState.Maximized; MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_config.LeftPanelWidth); MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_config.RightPanelWidth); - if (_config.FullScreen) + if (_config.MainWindowConfig.FullScreen) WindowState = WindowState.Maximized; _suspendLayout = false; @@ -190,9 +205,8 @@ public partial class MainWindow _config.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value; _config.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value; - _config.WindowSize = new Size(Width, Height); - _config.WindowLocation = new Point(Left, Top); - _config.FullScreen = WindowState == WindowState.Maximized; + + _config.MainWindowConfig = this.GetConfig(); await ThrottleExt.Throttle(() => { _configRepository.Save(_config); @@ -303,7 +317,6 @@ public partial class MainWindow _mediaPlayer.Stop(); _mediaPlayer.Dispose(); _libVLC.Dispose(); - _config.AnnotationClasses = AnnotationClasses.ToList(); _configRepository.Save(_config); Application.Current.Shutdown(); } diff --git a/Azaion.Annotator/PlayerControlHandler.cs b/Azaion.Annotator/PlayerControlHandler.cs index fbfc77c..251578d 100644 --- a/Azaion.Annotator/PlayerControlHandler.cs +++ b/Azaion.Annotator/PlayerControlHandler.cs @@ -69,8 +69,6 @@ public class PlayerControlHandler : public async Task Handle(KeyEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation($"Catch {notification.Args.Key} by {notification.Sender.GetType().Name}"); - var key = notification.Args.Key; var keyNumber = (int?)null; @@ -79,7 +77,7 @@ public class PlayerControlHandler : if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9) keyNumber = key - Key.NumPad1; if (keyNumber.HasValue) - SelectClass(_mainWindow.AnnotationClasses[keyNumber.Value]); + SelectClass((AnnotationClass)_mainWindow.LvClasses.Items[keyNumber.Value]); if (_keysControlEnumDict.TryGetValue(key, out var value)) await ControlPlayback(value); @@ -234,16 +232,9 @@ public class PlayerControlHandler : var currentAnns = _mainWindow.Editor.CurrentAnns .Select(x => new YoloLabel(x.Info, _mainWindow.Editor.RenderSize, _formState.CurrentVideoSize)) .ToList(); - var labels = string.Join(Environment.NewLine, currentAnns.Select(x => x.ToString())); - if (!Directory.Exists(_config.LabelsDirectory)) - Directory.CreateDirectory(_config.LabelsDirectory); - if (!Directory.Exists(_config.ImagesDirectory)) - Directory.CreateDirectory(_config.ImagesDirectory); - if (!Directory.Exists(_config.ResultsDirectory)) - Directory.CreateDirectory(_config.ResultsDirectory); + await YoloLabel.WriteToFile(currentAnns, Path.Combine(_config.LabelsDirectory, $"{fName}.txt")); - await File.WriteAllTextAsync(Path.Combine(_config.LabelsDirectory, $"{fName}.txt"), labels); var resultHeight = (uint)Math.Round(RESULT_WIDTH / _formState.CurrentVideoSize.Width * _formState.CurrentVideoSize.Height); await _mainWindow.AddAnnotation(time, currentAnns); diff --git a/Azaion.Annotator/config.json b/Azaion.Annotator/config.json index 8858c13..a4109d6 100644 --- a/Azaion.Annotator/config.json +++ b/Azaion.Annotator/config.json @@ -3,6 +3,7 @@ "LabelsDirectory": "E:\\labels", "ImagesDirectory": "E:\\images", "ResultsDirectory": "E:\\results", + "UnknownImages": "E:\\unknown", "ThumbnailsDirectory": "E:\\thumbnails", "AnnotationClasses": [ { "Id": 0, "Name": "Броньована техніка", "Color": "#40FF0000" }, @@ -16,13 +17,20 @@ { "Id": 8, "Name": "Танк з захистом", "Color": "#40008000" }, { "Id": 9, "Name": "Дим", "Color": "#40000080" } ], - "WindowSize": "1920,1080", - "WindowLocation": "200,121", + "MainWindowConfig": { + "WindowSize": "1920,1080", + "WindowLocation": "50,50", + "FullScreen": true + }, + "DatasetExplorerConfig": { + "WindowSize": "1920,1080", + "WindowLocation": "50,50", + "FullScreen": true + }, "ThumbnailConfig": { "Size": "480,270", "Border": 10 }, - "FullScreen": true, "LeftPanelWidth": 300, "RightPanelWidth": 300, "ShowHelpOnStart": false,