From d2186eb326f561fd1b0d5fcf9a42fc60782753fc Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Sun, 29 Sep 2024 16:24:31 +0300 Subject: [PATCH] sort thumbnails by date in DatasetExplorer --- Azaion.Annotator/DTO/LabelInfo.cs | 10 ++ Azaion.Annotator/DTO/ThumbnailDto.cs | 1 + Azaion.Annotator/DatasetExplorer.xaml | 8 +- Azaion.Annotator/DatasetExplorer.xaml.cs | 154 +++++++++--------- .../DatasetExplorerEventHandler.cs | 5 +- .../Extensions/DenseDateTimeConverter.cs | 12 ++ Azaion.Annotator/GalleryManager.cs | 89 +++++++--- Azaion.Annotator/MainWindow.xaml.cs | 11 +- 8 files changed, 182 insertions(+), 108 deletions(-) create mode 100644 Azaion.Annotator/DTO/LabelInfo.cs create mode 100644 Azaion.Annotator/Extensions/DenseDateTimeConverter.cs diff --git a/Azaion.Annotator/DTO/LabelInfo.cs b/Azaion.Annotator/DTO/LabelInfo.cs new file mode 100644 index 0000000..e90e39a --- /dev/null +++ b/Azaion.Annotator/DTO/LabelInfo.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Azaion.Annotator.DTO; + +public class LabelInfo +{ + [JsonProperty("c")] public List Classes { get; set; } = null!; + + [JsonProperty("d")] public DateTime ImageDateTime { get; set; } +} \ No newline at end of file diff --git a/Azaion.Annotator/DTO/ThumbnailDto.cs b/Azaion.Annotator/DTO/ThumbnailDto.cs index 3a908bd..39e04b7 100644 --- a/Azaion.Annotator/DTO/ThumbnailDto.cs +++ b/Azaion.Annotator/DTO/ThumbnailDto.cs @@ -11,6 +11,7 @@ public class ThumbnailDto : INotifyPropertyChanged public string ThumbnailPath { get; set; } public string ImagePath { get; set; } public string LabelPath { get; set; } + public DateTime ImageDate { get; set; } private BitmapImage? _image; public BitmapImage? Image diff --git a/Azaion.Annotator/DatasetExplorer.xaml b/Azaion.Annotator/DatasetExplorer.xaml index c1f9556..4b9acb1 100644 --- a/Azaion.Annotator/DatasetExplorer.xaml +++ b/Azaion.Annotator/DatasetExplorer.xaml @@ -58,7 +58,7 @@ HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Black"> - + - + - + diff --git a/Azaion.Annotator/DatasetExplorer.xaml.cs b/Azaion.Annotator/DatasetExplorer.xaml.cs index 4698af6..64ea924 100644 --- a/Azaion.Annotator/DatasetExplorer.xaml.cs +++ b/Azaion.Annotator/DatasetExplorer.xaml.cs @@ -6,10 +6,10 @@ using System.Windows.Media; using Azaion.Annotator.DTO; using Azaion.Annotator.Extensions; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; using ScottPlot; using Color = ScottPlot.Color; using MessageBox = System.Windows.MessageBox; +using Orientation = ScottPlot.Orientation; namespace Azaion.Annotator; @@ -22,10 +22,9 @@ public partial class DatasetExplorer private ObservableCollection AllAnnotationClasses { get; set; } = new(); private int _tempSelectedClassIdx = 0; - private readonly string _thumbnailsCacheFile; private readonly IConfigRepository _configRepository; private readonly FormState _formState; - private static Dictionary> LabelsCache { get; set; } = new(); + private readonly IGalleryManager _galleryManager; public bool ThumbnailLoading { get; set; } @@ -42,12 +41,7 @@ public partial class DatasetExplorer _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>(); - } + _galleryManager = galleryManager; InitializeComponent(); Loaded += async (_, _) => @@ -126,22 +120,6 @@ public partial class DatasetExplorer Activated += (_, _) => { _formState.ActiveWindow = WindowsEnum.DatasetExplorer; }; - Switcher.SelectionChanged += (sender, args) => - { - switch (Switcher.SelectedIndex) - { - case 0: //ListView - LvClasses.ItemsSource = AllAnnotationClasses; - LvClasses.SelectedIndex = _tempSelectedClassIdx; - ExplorerEditor.Background = null; - break; - case 1: //Editor - _tempSelectedClassIdx = LvClasses.SelectedIndex; - LvClasses.ItemsSource = _config.AnnotationClasses; - break; - } - }; - ExplorerEditor.GetTimeFunc = () => _formState.GetTime(CurrentThumbnail!.ImagePath); galleryManager.ThumbnailsUpdate += thumbnailsPercentage => { @@ -152,7 +130,8 @@ public partial class DatasetExplorer private void LoadClassDistribution() { - var data = LabelsCache.SelectMany(x => x.Value) + var data = _galleryManager.LabelsCache + .SelectMany(x => x.Value.Classes) .GroupBy(x => x) .Select(x => new { @@ -163,7 +142,9 @@ public partial class DatasetExplorer }) .ToList(); + var foregroundColor = Color.FromColor(System.Drawing.Color.Black); var plot = ClassDistribution.Plot; + plot.Add.Bars(data.Select(x => new Bar { Orientation = Orientation.Horizontal, @@ -171,17 +152,21 @@ public partial class DatasetExplorer Label = x.ClassCount > 200 ? x.ClassCount.ToString() : "", FillColor = new Color(x.Color.R, x.Color.G, x.Color.B, x.Color.A), Value = x.ClassCount, - CenterLabel = true + CenterLabel = true, + LabelOffset = 10 })); + foreach (var x in data) { var label = plot.Add.Text(x.Name, 50, -1.5 * x.Key + 1.1); - label.LabelFontSize = 16; + label.LabelFontColor = foregroundColor; + label.LabelFontSize = 18; } plot.Axes.AutoScale(); - //plot.Axes.SetLimits(-200, data.Max(x => x.ClassCount + 3000), -2 * data.Count + 5, 5); - ClassDistribution.Background = new SolidColorBrush(System.Windows.Media.Colors.Black); + plot.HideAxesAndGrid(); + plot.FigureBackground.Color = new("#888888"); + ClassDistribution.Refresh(); } @@ -200,9 +185,7 @@ public partial class DatasetExplorer { ImageSource = await dto.ImagePath.OpenImage() }; - - Switcher.SelectedIndex = 1; - LvClasses.SelectedIndex = 1; + SwitchTab(toEditor: true); var time = _formState.GetTime(dto.ImagePath); ExplorerEditor.RemoveAllAnns(); @@ -227,6 +210,28 @@ public partial class DatasetExplorer } + public void SwitchTab(bool toEditor) + { + if (toEditor) + { + AnnotationsTab.Visibility = Visibility.Collapsed; + EditorTab.Visibility = Visibility.Visible; + _tempSelectedClassIdx = LvClasses.SelectedIndex; + LvClasses.ItemsSource = _config.AnnotationClasses; + + Switcher.SelectedIndex = 1; + LvClasses.SelectedIndex = Math.Max(0, _tempSelectedClassIdx - 1); + } + else + { + AnnotationsTab.Visibility = Visibility.Visible; + EditorTab.Visibility = Visibility.Collapsed; + LvClasses.ItemsSource = AllAnnotationClasses; + LvClasses.SelectedIndex = _tempSelectedClassIdx; + Switcher.SelectedIndex = 0; + } + } + private async Task SaveUserSettings() { _config.DatasetExplorerConfig = this.GetConfig(); @@ -264,81 +269,78 @@ public partial class DatasetExplorer if (!Directory.Exists(_config.ThumbnailsDirectory)) return; - ThumbnailsDtos.Clear(); var thumbnails = Directory.GetFiles(_config.ThumbnailsDirectory, "*.jpg"); - - var thumbNum = 0; - foreach (var thumbnail in thumbnails) + var thumbnailDtos = new List(); + for (int i = 0; i < thumbnails.Length; i++) { - await AddThumbnail(thumbnail); + var thumbnailDto = GetThumbnail(thumbnails[i]); + if (thumbnailDto != null) + thumbnailDtos.Add(thumbnailDto); - if (thumbNum % 1000 == 0) - { - await File.WriteAllTextAsync(_thumbnailsCacheFile, JsonConvert.SerializeObject(LabelsCache)); - LoadingAnnsBar.Value = thumbNum * 100.0 / thumbnails.Length; - } - thumbNum++; + if (i % 1000 == 0) + LoadingAnnsBar.Value = i * 100.0 / thumbnails.Length; } + ThumbnailsDtos.Clear(); + foreach (var th in thumbnailDtos.OrderByDescending(x => x.ImageDate)) + ThumbnailsDtos.Add(th); + LoadingAnnsCaption.Visibility = Visibility.Collapsed; LoadingAnnsBar.Visibility = Visibility.Collapsed; - await File.WriteAllTextAsync(_thumbnailsCacheFile, JsonConvert.SerializeObject(LabelsCache)); } - private async Task AddThumbnail(string thumbnail, CancellationToken cancellationToken = default) + private ThumbnailDto? GetThumbnail(string thumbnail) { try { var name = Path.GetFileNameWithoutExtension(thumbnail)[..^Config.ThumbnailPrefix.Length]; - var imageName = Path.Combine(_config.ImagesDirectory, name); + var imagePath = Path.Combine(_config.ImagesDirectory, name); foreach (var f in _config.ImageFormats) { - var curName = $"{imageName}.{f}"; + var curName = $"{imagePath}.{f}"; if (File.Exists(curName)) { - imageName = curName; + imagePath = curName; break; } } var labelPath = Path.Combine(_config.LabelsDirectory, $"{name}.txt"); - if (!LabelsCache.TryGetValue(name, out var classes)) + if (!_galleryManager.LabelsCache.TryGetValue(Path.GetFileName(imagePath), out var info)) { - if (!File.Exists(labelPath)) + if (File.Exists(labelPath)) + return null; + + var imageExists = File.Exists(imagePath); + if (!imageExists) { - 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; + File.Delete(thumbnail); + _logger.LogError($"No label {labelPath} found ! Image {imagePath} not found, thumbnail {thumbnail} was removed"); } - - var labels = await YoloLabel.ReadFromFile(labelPath, cancellationToken); - 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 + else { - ThumbnailPath = thumbnail, - ImagePath = imageName, - LabelPath = labelPath - }); + File.Move(imagePath, Path.Combine(_config.UnknownImages, imagePath)); + _logger.LogError($"No label {labelPath} found! But Image {imagePath} exists! Image moved to {_config.UnknownImages} directory!"); + } + return null; } + + if (!info.Classes.Contains(ExplorerEditor.CurrentAnnClass.Id) && ExplorerEditor.CurrentAnnClass.Id != -1) + return null; + + return new ThumbnailDto + { + ThumbnailPath = thumbnail, + ImagePath = imagePath, + LabelPath = labelPath, + ImageDate = info.ImageDateTime + }; } catch (Exception e) { _logger.LogError(e, e.Message); + return null; } } } \ No newline at end of file diff --git a/Azaion.Annotator/DatasetExplorerEventHandler.cs b/Azaion.Annotator/DatasetExplorerEventHandler.cs index 8025293..f68b0f2 100644 --- a/Azaion.Annotator/DatasetExplorerEventHandler.cs +++ b/Azaion.Annotator/DatasetExplorerEventHandler.cs @@ -52,8 +52,9 @@ public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, await YoloLabel.WriteToFile(currentAnns, Path.Combine(config.LabelsDirectory, datasetExplorer.CurrentThumbnail!.LabelPath)); await galleryManager.CreateThumbnail(datasetExplorer.CurrentThumbnail.ImagePath); + await galleryManager.SaveLabelsCache(); datasetExplorer.CurrentThumbnail.UpdateImage(); - datasetExplorer.Switcher.SelectedIndex = 0; + datasetExplorer.SwitchTab(toEditor: false); break; case PlaybackControlEnum.RemoveSelectedAnns: datasetExplorer.ExplorerEditor.RemoveSelectedAnns(); @@ -62,7 +63,7 @@ public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, datasetExplorer.ExplorerEditor.RemoveAllAnns(); break; case PlaybackControlEnum.Close: - datasetExplorer.Switcher.SelectedIndex = 0; + datasetExplorer.SwitchTab(toEditor: false); break; } } diff --git a/Azaion.Annotator/Extensions/DenseDateTimeConverter.cs b/Azaion.Annotator/Extensions/DenseDateTimeConverter.cs new file mode 100644 index 0000000..38378bc --- /dev/null +++ b/Azaion.Annotator/Extensions/DenseDateTimeConverter.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json.Converters; + +namespace Azaion.Annotator.Extensions; + +public class DenseDateTimeConverter : IsoDateTimeConverter +{ + public DenseDateTimeConverter() + { + DateTimeFormat = "yy-MM-dd HH:mm:ss"; + } + +} \ No newline at end of file diff --git a/Azaion.Annotator/GalleryManager.cs b/Azaion.Annotator/GalleryManager.cs index 76e3425..95c9beb 100644 --- a/Azaion.Annotator/GalleryManager.cs +++ b/Azaion.Annotator/GalleryManager.cs @@ -1,10 +1,12 @@ -using System.Drawing; +using System.Collections.Concurrent; +using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; using Azaion.Annotator.DTO; using Azaion.Annotator.Extensions; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using Color = System.Drawing.Color; using ParallelOptions = Azaion.Annotator.Extensions.ParallelOptions; using Size = System.Windows.Size; @@ -13,15 +15,21 @@ namespace Azaion.Annotator; public delegate void ThumbnailsUpdatedEventHandler(double thumbnailsPercentage); -public class GalleryManager(Config config, ILogger logger) : IGalleryManager +public class GalleryManager : IGalleryManager { + private readonly ILogger _logger; + public event ThumbnailsUpdatedEventHandler ThumbnailsUpdate; + private readonly string _thumbnailsCacheFile; private readonly SemaphoreSlim _updateLock = new(1); public double ThumbnailsPercentage { get; set; } + public ConcurrentDictionary LabelsCache { get; set; } = new(); private DirectoryInfo? _thumbnailsDirectory; + private readonly Config _config; + private DirectoryInfo ThumbnailsDirectory { get @@ -29,17 +37,24 @@ public class GalleryManager(Config config, ILogger logger) : IGa if (_thumbnailsDirectory != null) return _thumbnailsDirectory; - var dir = new DirectoryInfo(config.ThumbnailsDirectory); + var dir = new DirectoryInfo(_config.ThumbnailsDirectory); if (!dir.Exists) - Directory.CreateDirectory(config.ThumbnailsDirectory); - _thumbnailsDirectory = new DirectoryInfo(config.ThumbnailsDirectory); + Directory.CreateDirectory(_config.ThumbnailsDirectory); + _thumbnailsDirectory = new DirectoryInfo(_config.ThumbnailsDirectory); return _thumbnailsDirectory; } } + public GalleryManager(Config config, ILogger logger) + { + _config = config; + _logger = logger; + _thumbnailsCacheFile = Path.Combine(config.ThumbnailsDirectory, Config.ThumbnailsCacheFile); + } + public void ClearThumbnails() { - foreach(var file in new DirectoryInfo(config.ThumbnailsDirectory).GetFiles()) + foreach(var file in new DirectoryInfo(_config.ThumbnailsDirectory).GetFiles()) file.Delete(); } @@ -56,7 +71,16 @@ public class GalleryManager(Config config, ILogger logger) : IGa .Select(gr => gr.Key) .ToHashSet(); - var files = new DirectoryInfo(config.ImagesDirectory).GetFiles(); + if (File.Exists(_thumbnailsCacheFile)) + { + var cache = JsonConvert.DeserializeObject>( + await File.ReadAllTextAsync(_thumbnailsCacheFile), new DenseDateTimeConverter()); + LabelsCache = cache ?? new ConcurrentDictionary(); + } + else + LabelsCache = new ConcurrentDictionary(); + + var files = new DirectoryInfo(_config.ImagesDirectory).GetFiles(); var imagesCount = files.Length; await ParallelExt.ForEachAsync(files, async (file, cancellationToken) => @@ -70,7 +94,7 @@ public class GalleryManager(Config config, ILogger logger) : IGa } catch (Exception e) { - logger.LogError(e, $"Failed to generate thumbnail for {file.Name}! Error: {e.Message}"); + _logger.LogError(e, $"Failed to generate thumbnail for {file.Name}! Error: {e.Message}"); } }, new ParallelOptions { @@ -87,29 +111,26 @@ public class GalleryManager(Config config, ILogger logger) : IGa } finally { + await SaveLabelsCache(); _updateLock.Release(); } } - public async Task CreateThumbnail(string imgPath, CancellationToken cancellationToken = default) + public async Task SaveLabelsCache() { - 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); - } + var labelsCacheStr = JsonConvert.SerializeObject(LabelsCache, new DenseDateTimeConverter()); + await File.WriteAllTextAsync(_thumbnailsCacheFile, labelsCacheStr); } - private async Task GenerateThumbnail(string imgPath) + public async Task CreateThumbnail(string imgPath, CancellationToken cancellationToken = default) { - var width = (int)config.ThumbnailConfig.Size.Width; - var height = (int)config.ThumbnailConfig.Size.Height; + var width = (int)_config.ThumbnailConfig.Size.Width; + var height = (int)_config.ThumbnailConfig.Size.Height; var imgName = Path.GetFileName(imgPath); - var labelName = Path.Combine(config.LabelsDirectory, $"{Path.GetFileNameWithoutExtension(imgPath)}.txt"); + var labelName = Path.Combine(_config.LabelsDirectory, $"{Path.GetFileNameWithoutExtension(imgPath)}.txt"); - var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(imgPath))); + var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(imgPath, cancellationToken))); var bitmap = new Bitmap(width, height); @@ -121,16 +142,23 @@ public class GalleryManager(Config config, ILogger logger) : IGa var size = new Size(originalImage.Width, originalImage.Height); if (!File.Exists(labelName)) { - 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; + File.Move(imgPath, Path.Combine(_config.UnknownImages, imgName)); + _logger.LogInformation($"No labels found for image {imgName}! Moved image to the {_config.UnknownImages} folder."); + return; } var labels = (await YoloLabel.ReadFromFile(labelName)) .Select(x => new CanvasLabel(x, size, size)) .ToList(); var thumbWhRatio = width / (float)height; - var border = config.ThumbnailConfig.Border; + var border = _config.ThumbnailConfig.Border; + + var classes = labels.Select(x => x.ClassNumber).Distinct().ToList(); + LabelsCache.TryAdd(imgName, new LabelInfo + { + Classes = classes, + ImageDateTime = File.GetCreationTimeUtc(imgPath) + }); var frameX = 0.0; var frameY = 0.0; @@ -169,13 +197,20 @@ public class GalleryManager(Config config, ILogger logger) : IGa foreach (var label in labels) { - var color = config.AnnotationClassesDict[label.ClassNumber].Color; + var color = _config.AnnotationClassesDict[label.ClassNumber].Color; var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B)); var rectangle = new RectangleF((float)((label.X - frameX) / scale), (float)((label.Y - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale)); g.FillRectangle(brush, rectangle); } - return bitmap; + + + + if (bitmap != null) + { + var thumbnailName = Path.Combine(ThumbnailsDirectory.FullName, $"{Path.GetFileNameWithoutExtension(imgPath)}{Config.ThumbnailPrefix}.jpg"); + bitmap.Save(thumbnailName, ImageFormat.Jpeg); + } } } @@ -183,6 +218,8 @@ public interface IGalleryManager { event ThumbnailsUpdatedEventHandler ThumbnailsUpdate; double ThumbnailsPercentage { get; set; } + Task SaveLabelsCache(); + ConcurrentDictionary LabelsCache { get; set; } Task CreateThumbnail(string imgPath, CancellationToken cancellationToken = default); Task RefreshThumbnails(); void ClearThumbnails(); diff --git a/Azaion.Annotator/MainWindow.xaml.cs b/Azaion.Annotator/MainWindow.xaml.cs index 4897ff4..77a4d12 100644 --- a/Azaion.Annotator/MainWindow.xaml.cs +++ b/Azaion.Annotator/MainWindow.xaml.cs @@ -266,7 +266,16 @@ public partial class MainWindow if (existingResult != null) _formState.AnnotationResults.Remove(existingResult); - _formState.AnnotationResults.Add(new AnnotationResult(timeValue, fName, annotations, _config)); + var dict = _formState.AnnotationResults + .Select((x,i) => new { x.Time, Index = i }) + .ToDictionary(x => x.Time, x => x.Index); + + var index = dict.Where(x => x.Key < timeValue) + .OrderBy(x => x.Key - timeValue) + .Select(x => x.Value + 1) + .FirstOrDefault(); + + _formState.AnnotationResults.Insert(index, new AnnotationResult(timeValue, fName, annotations, _config)); await File.WriteAllTextAsync($"{_config.ResultsDirectory}/{fName}.json", JsonConvert.SerializeObject(_formState.AnnotationResults)); }