diff --git a/Azaion.Annotator/Azaion.Annotator.csproj b/Azaion.Annotator/Azaion.Annotator.csproj index e590b0d..e657879 100644 --- a/Azaion.Annotator/Azaion.Annotator.csproj +++ b/Azaion.Annotator/Azaion.Annotator.csproj @@ -27,6 +27,7 @@ + diff --git a/Azaion.Annotator/DTO/Label.cs b/Azaion.Annotator/DTO/Label.cs index a78d054..b282fa0 100644 --- a/Azaion.Annotator/DTO/Label.cs +++ b/Azaion.Annotator/DTO/Label.cs @@ -148,9 +148,9 @@ public class YoloLabel : Label } } - public static async Task> ReadFromFile(string filename, CancellationToken cancellationToken) + public static async Task> ReadFromFile(string filename) { - var str = await File.ReadAllTextAsync(filename, cancellationToken); + var str = await File.ReadAllTextAsync(filename); return str.Split(Environment.NewLine) .Select(Parse) diff --git a/Azaion.Annotator/DTO/ThumbnailDto.cs b/Azaion.Annotator/DTO/ThumbnailDto.cs new file mode 100644 index 0000000..80b2330 --- /dev/null +++ b/Azaion.Annotator/DTO/ThumbnailDto.cs @@ -0,0 +1,57 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Windows.Media.Imaging; + +namespace Azaion.Annotator.DTO; + +public class ThumbnailDto : INotifyPropertyChanged +{ + public string ThumbnailPath { get; set; } + public string ImagePath { get; set; } + public string LabelPath { get; set; } + + private BitmapImage? _image; + public BitmapImage? Image + { + get + { + if (_image == null) + LoadImageAsync(); + return _image; + } + set + { + _image = value; + OnPropertyChanged(); + } + } + + private async void LoadImageAsync() + { + await Task.Run(() => + { + try + { + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.UriSource = new Uri(ThumbnailPath); + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.DecodePixelWidth = 480; + bitmap.DecodePixelHeight = 270; + bitmap.EndInit(); + bitmap.Freeze(); // Freeze to make it cross-thread accessible + Image = bitmap; + } + catch (Exception e) + { + Console.WriteLine(e); + } + }); + } + + public event PropertyChangedEventHandler? PropertyChanged; + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/Azaion.Annotator/DatasetExplorer.xaml b/Azaion.Annotator/DatasetExplorer.xaml index 341fc0e..b28a222 100644 --- a/Azaion.Annotator/DatasetExplorer.xaml +++ b/Azaion.Annotator/DatasetExplorer.xaml @@ -3,9 +3,18 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel" xmlns:local="clr-namespace:Azaion.Annotator" + xmlns:dto="clr-namespace:Azaion.Annotator.DTO" mc:Ignorable="d" - Title="Браузер анотацій" Height="450" Width="800"> + Title="Браузер анотацій" Height="900" Width="1200"> + + + + + + + + + + diff --git a/Azaion.Annotator/DatasetExplorer.xaml.cs b/Azaion.Annotator/DatasetExplorer.xaml.cs index 615836e..0b00114 100644 --- a/Azaion.Annotator/DatasetExplorer.xaml.cs +++ b/Azaion.Annotator/DatasetExplorer.xaml.cs @@ -1,30 +1,55 @@ -using System.Windows; +using System.Collections.ObjectModel; +using System.IO; +using System.Windows; +using Azaion.Annotator.DTO; namespace Azaion.Annotator; -public partial class DatasetExplorer : Window +public partial class DatasetExplorer { - private CancellationTokenSource _cancellationTokenSource; + private readonly Config _config; - public DatasetExplorer(IGalleryManager galleryManager) + public ObservableCollection ThumbnailsDtos { get; set; } = new(); + + public DatasetExplorer(Config config) { - _cancellationTokenSource = new CancellationTokenSource(); + _config = config; InitializeComponent(); - Loaded += (sender, args) => - { - _ = Task.Run(async () => - { - while (!_cancellationTokenSource.Token.IsCancellationRequested) - { - await galleryManager.RefreshThumbnails(_cancellationTokenSource.Token); - await Task.Delay(30000, _cancellationTokenSource.Token); - } - }); - }; + DataContext = this; + Loaded += async (sender, args) => await LoadThumbnails(); - Closing += (sender, args) => _cancellationTokenSource.Cancel(); + Closing += (sender, args) => + { + args.Cancel = true; + Visibility = Visibility.Hidden; + }; } + private async Task LoadThumbnails() + { + if (!Directory.Exists(_config.ThumbnailsDirectory)) + return; + var thumbnails = Directory.GetFiles(_config.ThumbnailsDirectory, "*.jpg"); + + 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) + { + imageName = $"{imageName}.{imageFormat}"; + if (File.Exists(imageName)) + break; + } + + ThumbnailsDtos.Add(new ThumbnailDto + { + ThumbnailPath = thumbnail, + ImagePath = imageName, + LabelPath = Path.Combine(_config.LabelsDirectory, $"{name}.txt"), + }); + } + } } \ No newline at end of file diff --git a/Azaion.Annotator/GalleryManager.cs b/Azaion.Annotator/GalleryManager.cs index 2cc5847..d42be1b 100644 --- a/Azaion.Annotator/GalleryManager.cs +++ b/Azaion.Annotator/GalleryManager.cs @@ -3,31 +3,25 @@ using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; using Azaion.Annotator.DTO; +using Microsoft.Extensions.Logging; using Color = System.Drawing.Color; using Size = System.Windows.Size; namespace Azaion.Annotator; -public class GalleryManager : IGalleryManager +public class GalleryManager(Config config, ILogger logger) : IGalleryManager { - private readonly Config _config; - public int ThumbnailsCount { get; set; } public int ImagesCount { get; set; } - public GalleryManager(Config config) + public async Task RefreshThumbnails() { - _config = config; - } - - public async Task RefreshThumbnails(CancellationToken cancellationToken) - { - var dir = new DirectoryInfo(_config.ThumbnailsDirectory); + var dir = new DirectoryInfo(config.ThumbnailsDirectory); if (!dir.Exists) - Directory.CreateDirectory(_config.ThumbnailsDirectory); + Directory.CreateDirectory(config.ThumbnailsDirectory); var prefixLen = Config.ThumbnailPrefix.Length; - var thumbnailsDir = new DirectoryInfo(_config.ThumbnailsDirectory); + var thumbnailsDir = new DirectoryInfo(config.ThumbnailsDirectory); var thumbnails = thumbnailsDir.GetFiles() .Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen]) @@ -36,7 +30,7 @@ public class GalleryManager : IGalleryManager .ToHashSet(); ThumbnailsCount = thumbnails.Count; - var files = new DirectoryInfo(_config.ImagesDirectory).GetFiles(); + var files = new DirectoryInfo(config.ImagesDirectory).GetFiles(); ImagesCount = files.Length; foreach (var img in files) @@ -45,21 +39,28 @@ public class GalleryManager : IGalleryManager if (thumbnails.Contains(imgName)) continue; - var bitmap = await GenerateThumbnail(img, cancellationToken); - var thumbnailName = Path.Combine(thumbnailsDir.FullName, $"{imgName}{Config.ThumbnailPrefix}.jpg"); - bitmap.Save(thumbnailName, ImageFormat.Jpeg); + try + { + var bitmap = await GenerateThumbnail(img); + var thumbnailName = Path.Combine(thumbnailsDir.FullName, $"{imgName}{Config.ThumbnailPrefix}.jpg"); + bitmap.Save(thumbnailName, ImageFormat.Jpeg); + } + catch (Exception e) + { + logger.LogError(e, $"Failed to generate thumbnail for {img.Name}"); + } ThumbnailsCount++; } } - private async Task GenerateThumbnail(FileInfo img, CancellationToken cancellationToken) + private async Task GenerateThumbnail(FileInfo img) { - 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.GetFileNameWithoutExtension(img.Name); - var labelName = Path.Combine(_config.LabelsDirectory, $"{imgName}.txt"); + var labelName = Path.Combine(config.LabelsDirectory, $"{imgName}.txt"); var originalImage = Image.FromFile(img.FullName); @@ -71,39 +72,43 @@ public class GalleryManager : IGalleryManager g.InterpolationMode = InterpolationMode.Default; var size = new Size(originalImage.Width, originalImage.Height); - var labels = (await YoloLabel.ReadFromFile(labelName, cancellationToken)) + 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 labelsMinX = labels.Any() ? labels.Min(x => x.X); - var labelsMaxX = labels.Max(x => x.X + x.Width); - - var labelsMinY = labels.Min(x => x.Y); - var labelsMaxY = labels.Max(x => x.Y + x.Height); - - var labelsHeight = labelsMaxY - labelsMinY + 2 * border; - var labelsWidth = labelsMaxX - labelsMinX + 2 * border; - - var frameHeight = 0.0; - var frameWidth = 0.0; var frameX = 0.0; var frameY = 0.0; - if (labelsWidth / labelsHeight > thumbWhRatio) + var frameHeight = size.Height; + var frameWidth = size.Width; + + if (labels.Any()) { - frameWidth = labelsWidth; - frameHeight = Math.Min(labelsWidth / thumbWhRatio, size.Height); - frameX = Math.Max(0, labelsMinX - border); - frameY = Math.Max(0, 0.5 * (labelsMinY + labelsMaxY - frameHeight) - border); - } - else - { - frameHeight = labelsHeight; - frameWidth = Math.Min(labelsHeight * thumbWhRatio, size.Width); - frameY = Math.Max(0, labelsMinY - border); - frameX = Math.Max(0, 0.5 * (labelsMinX + labelsMaxX - frameWidth) - border); + var labelsMinX = labels.Min(x => x.X); + var labelsMaxX = labels.Max(x => x.X + x.Width); + + var labelsMinY = labels.Min(x => x.Y); + var labelsMaxY = labels.Max(x => x.Y + x.Height); + + var labelsHeight = labelsMaxY - labelsMinY + 2 * border; + var labelsWidth = labelsMaxX - labelsMinX + 2 * border; + + if (labelsWidth / labelsHeight > thumbWhRatio) + { + frameWidth = labelsWidth; + frameHeight = Math.Min(labelsWidth / thumbWhRatio, size.Height); + frameX = Math.Max(0, labelsMinX - border); + frameY = Math.Max(0, 0.5 * (labelsMinY + labelsMaxY - frameHeight) - border); + } + else + { + frameHeight = labelsHeight; + frameWidth = Math.Min(labelsHeight * thumbWhRatio, size.Width); + frameY = Math.Max(0, labelsMinY - border); + frameX = Math.Max(0, 0.5 * (labelsMinX + labelsMaxX - frameWidth) - border); + } } var scale = frameHeight / height; @@ -111,7 +116,7 @@ public class GalleryManager : IGalleryManager 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)); @@ -125,5 +130,5 @@ public interface IGalleryManager { int ThumbnailsCount { get; set; } int ImagesCount { get; set; } - Task RefreshThumbnails(CancellationToken cancellationToken); + Task RefreshThumbnails(); } \ No newline at end of file diff --git a/Azaion.Annotator/MainWindow.xaml.cs b/Azaion.Annotator/MainWindow.xaml.cs index 4136851..b1116b7 100644 --- a/Azaion.Annotator/MainWindow.xaml.cs +++ b/Azaion.Annotator/MainWindow.xaml.cs @@ -27,6 +27,7 @@ public partial class MainWindow private readonly IConfigRepository _configRepository; private readonly HelpWindow _helpWindow; private readonly ILogger _logger; + private readonly IGalleryManager _galleryManager; private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); public ObservableCollection AnnotationClasses { get; set; } = new(); @@ -48,7 +49,8 @@ public partial class MainWindow IConfigRepository configRepository, HelpWindow helpWindow, DatasetExplorer datasetExplorer, - ILogger logger) + ILogger logger, + IGalleryManager galleryManager) { InitializeComponent(); _libVLC = libVLC; @@ -60,6 +62,7 @@ public partial class MainWindow _helpWindow = helpWindow; _datasetExplorer = datasetExplorer; _logger = logger; + _galleryManager = galleryManager; VideoView.Loaded += VideoView_Loaded; Closed += OnFormClosed; @@ -70,6 +73,15 @@ public partial class MainWindow Core.Initialize(); InitControls(); + _ = Task.Run(async () => + { + while (true) + { + await _galleryManager.RefreshThumbnails(); + await Task.Delay(30000); + } + }); + _suspendLayout = true; Left = _config.WindowLocation.X; @@ -222,7 +234,7 @@ public partial class MainWindow var name = Path.GetFileNameWithoutExtension(file.Name); var time = _formState.GetTime(name)!.Value; - await AddAnnotation(time, await YoloLabel.ReadFromFile(file.FullName, cancellationToken)); + await AddAnnotation(time, await YoloLabel.ReadFromFile(file.FullName)); } }