From 742f1ffee985d6838b91efbce58b54942f968a88 Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Fri, 20 Sep 2024 09:23:12 +0300 Subject: [PATCH] fix editing non-timed annotations --- .../Controls/AnnotationControl.cs | 4 +- Azaion.Annotator/Controls/CanvasEditor.cs | 13 ++- Azaion.Annotator/DTO/FormState.cs | 25 +++-- Azaion.Annotator/DTO/Label.cs | 27 ++--- Azaion.Annotator/DTO/ThumbnailDto.cs | 2 + Azaion.Annotator/DatasetExplorer.xaml | 61 ++++++++--- Azaion.Annotator/DatasetExplorer.xaml.cs | 101 ++++++++++++------ Azaion.Annotator/GalleryManager.cs | 79 +++++++++----- Azaion.Annotator/MainWindow.xaml | 3 + Azaion.Annotator/MainWindow.xaml.cs | 22 ++-- 10 files changed, 218 insertions(+), 119 deletions(-) diff --git a/Azaion.Annotator/Controls/AnnotationControl.cs b/Azaion.Annotator/Controls/AnnotationControl.cs index 54e73c0..8d13940 100644 --- a/Azaion.Annotator/Controls/AnnotationControl.cs +++ b/Azaion.Annotator/Controls/AnnotationControl.cs @@ -14,7 +14,7 @@ public class AnnotationControl : Border private readonly Grid _grid; private readonly TextBlock _classNameLabel; - public TimeSpan Time { get; set; } + public TimeSpan? Time { get; set; } private AnnotationClass _annotationClass = null!; public AnnotationClass AnnotationClass @@ -41,7 +41,7 @@ public class AnnotationControl : Border } } - public AnnotationControl(AnnotationClass annotationClass, TimeSpan time, Action resizeStart) + public AnnotationControl(AnnotationClass annotationClass, TimeSpan? time, Action resizeStart) { Time = time; _resizeStart = resizeStart; diff --git a/Azaion.Annotator/Controls/CanvasEditor.cs b/Azaion.Annotator/Controls/CanvasEditor.cs index 8b5438a..143ec9d 100644 --- a/Azaion.Annotator/Controls/CanvasEditor.cs +++ b/Azaion.Annotator/Controls/CanvasEditor.cs @@ -34,13 +34,13 @@ public class CanvasEditor : Canvas public static readonly DependencyProperty GetTimeFuncProp = DependencyProperty.Register( nameof(GetTimeFunc), - typeof(Func), + typeof(Func), typeof(CanvasEditor), new PropertyMetadata(null)); - public Func GetTimeFunc + public Func GetTimeFunc { - get => (Func)GetValue(GetTimeFuncProp); + get => (Func)GetValue(GetTimeFuncProp); set => SetValue(GetTimeFuncProp, value); } @@ -310,7 +310,7 @@ public class CanvasEditor : Canvas }); } - public AnnotationControl CreateAnnotation(AnnotationClass annClass, TimeSpan time, CanvasLabel canvasLabel) + public AnnotationControl CreateAnnotation(AnnotationClass annClass, TimeSpan? time, CanvasLabel canvasLabel) { var annotationControl = new AnnotationControl(annClass, time, AnnotationResizeStart) { @@ -354,7 +354,10 @@ public class CanvasEditor : Canvas public void ClearExpiredAnnotations(TimeSpan time) { - var expiredAnns = CurrentAnns.Where(x => Math.Abs((time - x.Time).TotalMilliseconds) > _viewThreshold.TotalMilliseconds).ToList(); + var expiredAnns = CurrentAnns.Where(x => + x.Time.HasValue && + Math.Abs((time - x.Time.Value).TotalMilliseconds) > _viewThreshold.TotalMilliseconds) + .ToList(); RemoveAnnotations(expiredAnns); } } \ No newline at end of file diff --git a/Azaion.Annotator/DTO/FormState.cs b/Azaion.Annotator/DTO/FormState.cs index baa35d0..d15850f 100644 --- a/Azaion.Annotator/DTO/FormState.cs +++ b/Azaion.Annotator/DTO/FormState.cs @@ -24,19 +24,18 @@ public class FormState public TimeSpan? GetTime(string name) { var timeStr = name.Split("_").LastOrDefault(); - if (string.IsNullOrEmpty(timeStr)) + if (string.IsNullOrEmpty(timeStr) || timeStr.Length < 7) return null; - - try - { - //For some reason, TimeSpan.ParseExact doesn't work on every platform. - return new TimeSpan( - days: 0, - hours: int.Parse(timeStr[0..1]), - minutes: int.Parse(timeStr[1..3]), - seconds: int.Parse(timeStr[3..5]), - milliseconds: int.Parse(timeStr[5..6]) * 100); - } - catch (Exception e) { return null; } + + //For some reason, TimeSpan.ParseExact doesn't work on every platform. + if (!int.TryParse(timeStr[0..1], out var hours)) + return null; + if (!int.TryParse(timeStr[1..3], out var minutes)) + return null; + if (!int.TryParse(timeStr[3..5], out var seconds)) + return null; + if (!int.TryParse(timeStr[5..6], out var milliseconds)) + return null; + return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100); } } \ No newline at end of file diff --git a/Azaion.Annotator/DTO/Label.cs b/Azaion.Annotator/DTO/Label.cs index fb49a13..cfe16f5 100644 --- a/Azaion.Annotator/DTO/Label.cs +++ b/Azaion.Annotator/DTO/Label.cs @@ -135,31 +135,24 @@ public class YoloLabel : Label var strings = s.Replace(',', '.').Split(' '); if (strings.Length != 5) - return null; + throw new Exception("Wrong labels format!"); - try + var res = new YoloLabel { - var res = new YoloLabel - { - ClassNumber = int.Parse(strings[0], CultureInfo.InvariantCulture), - CenterX = double.Parse(strings[1], CultureInfo.InvariantCulture), - CenterY = double.Parse(strings[2], CultureInfo.InvariantCulture), - Width = double.Parse(strings[3], CultureInfo.InvariantCulture), - Height = double.Parse(strings[4], CultureInfo.InvariantCulture) - }; - return res; - } - catch (Exception) - { - return null; - } + ClassNumber = int.Parse(strings[0], CultureInfo.InvariantCulture), + CenterX = double.Parse(strings[1], CultureInfo.InvariantCulture), + CenterY = double.Parse(strings[2], CultureInfo.InvariantCulture), + Width = double.Parse(strings[3], CultureInfo.InvariantCulture), + Height = double.Parse(strings[4], CultureInfo.InvariantCulture) + }; + return res; } public static async Task> ReadFromFile(string filename) { var str = await File.ReadAllTextAsync(filename); - return str.Split(Environment.NewLine) + return str.Split('\n') .Select(Parse) .Where(ann => ann != null) .ToList()!; diff --git a/Azaion.Annotator/DTO/ThumbnailDto.cs b/Azaion.Annotator/DTO/ThumbnailDto.cs index 822b15b..3a908bd 100644 --- a/Azaion.Annotator/DTO/ThumbnailDto.cs +++ b/Azaion.Annotator/DTO/ThumbnailDto.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.IO; using System.Runtime.CompilerServices; using System.Windows.Media.Imaging; using Azaion.Annotator.Extensions; @@ -26,6 +27,7 @@ public class ThumbnailDto : INotifyPropertyChanged OnPropertyChanged(); } } + public string ImageName => Path.GetFileName(ImagePath); public void UpdateImage() => _image = null; diff --git a/Azaion.Annotator/DatasetExplorer.xaml b/Azaion.Annotator/DatasetExplorer.xaml index d0febe5..1b48431 100644 --- a/Azaion.Annotator/DatasetExplorer.xaml +++ b/Azaion.Annotator/DatasetExplorer.xaml @@ -12,7 +12,22 @@ - + + + + + + + + @@ -78,6 +93,7 @@ + @@ -87,23 +103,40 @@ - Loading: + Завантаження: - - + + + + + База іконок: - - + + + + + + diff --git a/Azaion.Annotator/DatasetExplorer.xaml.cs b/Azaion.Annotator/DatasetExplorer.xaml.cs index 1136a86..3694bf3 100644 --- a/Azaion.Annotator/DatasetExplorer.xaml.cs +++ b/Azaion.Annotator/DatasetExplorer.xaml.cs @@ -1,10 +1,8 @@ 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; using Azaion.Annotator.DTO; using Azaion.Annotator.Extensions; using Microsoft.Extensions.Logging; @@ -35,7 +33,8 @@ public partial class DatasetExplorer Config config, ILogger logger, IConfigRepository configRepository, - FormState formState) + FormState formState, + IGalleryManager galleryManager) { _config = config; _logger = logger; @@ -93,6 +92,8 @@ public partial class DatasetExplorer SizeChanged += async (_, _) => await SaveUserSettings(); LocationChanged += async (_, _) => await SaveUserSettings(); StateChanged += async (_, _) => await SaveUserSettings(); + + RefreshThumbBar.Value = galleryManager.ThumbnailsPercentage; }; Closing += (sender, args) => @@ -139,7 +140,12 @@ public partial class DatasetExplorer } }; - ExplorerEditor.GetTimeFunc = () => _formState.GetTime(CurrentThumbnail!.ImagePath)!.Value; + ExplorerEditor.GetTimeFunc = () => _formState.GetTime(CurrentThumbnail!.ImagePath); + galleryManager.ThumbnailsUpdate += thumbnailsPercentage => + { + Dispatcher.Invoke(() => RefreshThumbBar.Value = thumbnailsPercentage); + }; + } private async Task EditAnnotation() @@ -161,7 +167,7 @@ public partial class DatasetExplorer Switcher.SelectedIndex = 1; LvClasses.SelectedIndex = 1; - var time = _formState.GetTime(dto.ImagePath)!.Value; + var time = _formState.GetTime(dto.ImagePath); ExplorerEditor.RemoveAllAnns(); foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath)) { @@ -215,6 +221,9 @@ public partial class DatasetExplorer private async Task ReloadThumbnails() { + LoadingAnnsCaption.Visibility = Visibility.Visible; + LoadingAnnsBar.Visibility = Visibility.Visible; + if (!Directory.Exists(_config.ThumbnailsDirectory)) return; @@ -227,50 +236,72 @@ public partial class DatasetExplorer await AddThumbnail(thumbnail); if (thumbNum % 1000 == 0) + { await File.WriteAllTextAsync(_thumbnailsCacheFile, JsonConvert.SerializeObject(LabelsCache)); + LoadingAnnsBar.Value = thumbNum * 100.0 / thumbnails.Length; + } thumbNum++; } + + LoadingAnnsCaption.Visibility = Visibility.Collapsed; + LoadingAnnsBar.Visibility = Visibility.Collapsed; 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 f in _config.ImageFormats) + try { - var curName = $"{imageName}.{f}"; - if (File.Exists(curName)) + var name = Path.GetFileNameWithoutExtension(thumbnail)[..^Config.ThumbnailPrefix.Length]; + var imageName = Path.Combine(_config.ImagesDirectory, name); + foreach (var f in _config.ImageFormats) { - imageName = curName; - break; - } - } - - var labelPath = Path.Combine(_config.LabelsDirectory, $"{name}.txt"); - - if (!LabelsCache.TryGetValue(name, out var classes)) - { - if (!File.Exists(labelPath)) - { - _logger.LogError($"No label {labelPath} found ! Image {(!File.Exists(imageName) ? "not exists!" : "exists.")}"); - return; + var curName = $"{imageName}.{f}"; + if (File.Exists(curName)) + { + imageName = curName; + break; + } } - var labels = await YoloLabel.ReadFromFile(labelPath); - classes = labels.Select(x => x.ClassNumber).Distinct().ToList(); - LabelsCache.Add(name, classes); - } + var labelPath = Path.Combine(_config.LabelsDirectory, $"{name}.txt"); - if (classes.Contains(ExplorerEditor.CurrentAnnClass.Id) || ExplorerEditor.CurrentAnnClass.Id == -1) - { - ThumbnailsDtos.Add(new ThumbnailDto + if (!LabelsCache.TryGetValue(name, out var classes)) { - ThumbnailPath = thumbnail, - ImagePath = imageName, - LabelPath = labelPath - }); - } + if (!File.Exists(labelPath)) + { + var imageExists = File.Exists(imageName); + if (!imageExists) + { + _logger.LogError($"No label {labelPath} found ! Image {imageName} not found, removing thumbnail {thumbnail}"); + File.Delete(thumbnail); + } + else + { + _logger.LogError($"No label {labelPath} found! But Image {imageName} exists! Image moved to {_config.UnknownImages} directory!"); + File.Move(imageName, Path.Combine(_config.UnknownImages, imageName)); + } + 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 = labelPath + }); + } + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + } } } \ No newline at end of file diff --git a/Azaion.Annotator/GalleryManager.cs b/Azaion.Annotator/GalleryManager.cs index 7da9033..6e637f6 100644 --- a/Azaion.Annotator/GalleryManager.cs +++ b/Azaion.Annotator/GalleryManager.cs @@ -9,10 +9,15 @@ using Size = System.Windows.Size; namespace Azaion.Annotator; +public delegate void ThumbnailsUpdatedEventHandler(double thumbnailsPercentage); + public class GalleryManager(Config config, ILogger logger) : IGalleryManager { - public int ThumbnailsCount { get; set; } - public int ImagesCount { get; set; } + public event ThumbnailsUpdatedEventHandler ThumbnailsUpdate; + private const int UPDATE_STEP = 20; + private readonly SemaphoreSlim _updateLock = new(1); + + public double ThumbnailsPercentage { get; set; } private DirectoryInfo? _thumbnailsDirectory; private DirectoryInfo ThumbnailsDirectory @@ -30,35 +35,54 @@ public class GalleryManager(Config config, ILogger logger) : IGa } } + public void ClearThumbnails() + { + foreach(var file in new DirectoryInfo(config.ThumbnailsDirectory).GetFiles()) + file.Delete(); + } + public async Task RefreshThumbnails() { - var prefixLen = Config.ThumbnailPrefix.Length; - - var thumbnails = ThumbnailsDirectory.GetFiles() - .Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen]) - .GroupBy(x => x) - .Select(gr => gr.Key) - .ToHashSet(); - ThumbnailsCount = thumbnails.Count; - - var files = new DirectoryInfo(config.ImagesDirectory).GetFiles(); - ImagesCount = files.Length; - - foreach (var img in files) + await _updateLock.WaitAsync(); + try { - var imgName = Path.GetFileNameWithoutExtension(img.Name); - if (thumbnails.Contains(imgName)) - continue; - try + var prefixLen = Config.ThumbnailPrefix.Length; + + var thumbnails = ThumbnailsDirectory.GetFiles() + .Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen]) + .GroupBy(x => x) + .Select(gr => gr.Key) + .ToHashSet(); + var thumbnailsCount = thumbnails.Count; + + var files = new DirectoryInfo(config.ImagesDirectory).GetFiles(); + var imagesCount = files.Length; + + for (int i = 0; i < files.Length; i++) { - await CreateThumbnail(img.FullName); - } - catch (Exception e) - { - logger.LogError(e, $"Failed to generate thumbnail for {img.Name}"); + var img = files[i]; + ThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, i * 100 / (double)imagesCount); + var imgName = Path.GetFileNameWithoutExtension(img.Name); + if (i % UPDATE_STEP == 0) + ThumbnailsUpdate?.Invoke(ThumbnailsPercentage); + if (thumbnails.Contains(imgName)) + continue; + try + { + await CreateThumbnail(img.FullName); + thumbnailsCount++; + } + catch (Exception e) + { + logger.LogError(e, $"Failed to generate thumbnail for {img.Name}! Error: {e.Message}"); + } } - ThumbnailsCount++; + await Task.Delay(10000); + } + finally + { + _updateLock.Release(); } } @@ -152,8 +176,9 @@ public class GalleryManager(Config config, ILogger logger) : IGa public interface IGalleryManager { - int ThumbnailsCount { get; set; } - int ImagesCount { get; set; } + event ThumbnailsUpdatedEventHandler ThumbnailsUpdate; + double ThumbnailsPercentage { get; set; } Task CreateThumbnail(string imgPath); Task RefreshThumbnails(); + void ClearThumbnails(); } \ No newline at end of file diff --git a/Azaion.Annotator/MainWindow.xaml b/Azaion.Annotator/MainWindow.xaml index 10b1eca..2745e3b 100644 --- a/Azaion.Annotator/MainWindow.xaml +++ b/Azaion.Annotator/MainWindow.xaml @@ -79,6 +79,9 @@ + - { ShowTimeAnnotations(TimeSpan.FromMilliseconds(_mediaPlayer.Time)); - }; - - VideoSlider.ValueChanged += (value, newValue) => + + VideoSlider.ValueChanged += (value, newValue) => _mediaPlayer.Position = (float)(newValue / VideoSlider.Maximum); - VideoSlider.KeyDown += (sender, args) => _mediator.Publish(new KeyEvent(sender, args)); + VideoSlider.KeyDown += (sender, args) => + _mediator.Publish(new KeyEvent(sender, args)); - Volume.ValueChanged += (_, newValue) => _mediator.Publish(new VolumeChangedEvent((int)newValue)); + Volume.ValueChanged += (_, newValue) => + _mediator.Publish(new VolumeChangedEvent((int)newValue)); SizeChanged += async (_, _) => await SaveUserSettings(); LocationChanged += async (_, _) => await SaveUserSettings(); @@ -401,4 +401,14 @@ public partial class MainWindow } private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => _ = SaveUserSettings(); + + private void ReloadThumbnailsItemClick(object sender, RoutedEventArgs e) + { + var result = MessageBox.Show($"Видалити всі іконки і згенерувати нову базу іконок в {_config.ThumbnailsDirectory}?", + "Підтвердження оновлення іконок", MessageBoxButton.YesNo, MessageBoxImage.Question); + if (result != MessageBoxResult.Yes) + return; + _galleryManager.ClearThumbnails(); + _galleryManager.RefreshThumbnails(); + } }