diff --git a/Azaion.Annotator/App.xaml.cs b/Azaion.Annotator/App.xaml.cs index 0127f66..78103e1 100644 --- a/Azaion.Annotator/App.xaml.cs +++ b/Azaion.Annotator/App.xaml.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Data; +using System.Reflection; using System.Windows; using System.Windows.Input; using System.Windows.Threading; @@ -35,6 +36,8 @@ public partial class App : Application { services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddMediatR(c => c.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); services.AddSingleton(_ => new LibVLC()); services.AddSingleton(); diff --git a/Azaion.Annotator/DTO/Config.cs b/Azaion.Annotator/DTO/Config.cs index b66f0d5..4836f63 100644 --- a/Azaion.Annotator/DTO/Config.cs +++ b/Azaion.Annotator/DTO/Config.cs @@ -10,10 +10,13 @@ namespace Azaion.Annotator.DTO; public class Config { + public const string ThumbnailPrefix = "_thumb"; + 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 List AnnotationClasses { get; set; } = []; @@ -31,6 +34,14 @@ public class Config public List VideoFormats { get; set; } public List ImageFormats { get; set; } + + public ThumbnailConfig ThumbnailConfig { get; set; } +} + +public class ThumbnailConfig +{ + public Size Size { get; set; } + public int Border { get; set; } } public interface IConfigRepository @@ -48,9 +59,11 @@ public class FileConfigRepository(ILogger logger) : IConfi private const string DEFAULT_LABELS_DIR = "labels"; private const string DEFAULT_IMAGES_DIR = "images"; private const string DEFAULT_RESULTS_DIR = "results"; + private const string DEFAULT_THUMBNAILS_DIR = "thumbnails"; private static readonly Size DefaultWindowSize = new(1280, 720); private static readonly Point DefaultWindowLocation = new(100, 100); + private static readonly Size DefaultThumbnailSize = new(240, 135); private static readonly List DefaultVideoFormats = ["mp4", "mov", "avi"]; private static readonly List DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"]; @@ -68,13 +81,19 @@ public class FileConfigRepository(ILogger logger) : IConfi LabelsDirectory = Path.Combine(exePath, DEFAULT_LABELS_DIR), ImagesDirectory = Path.Combine(exePath, DEFAULT_IMAGES_DIR), ResultsDirectory = Path.Combine(exePath, DEFAULT_RESULTS_DIR), + ThumbnailsDirectory = Path.Combine(exePath, DEFAULT_THUMBNAILS_DIR), WindowLocation = DefaultWindowLocation, WindowSize = DefaultWindowSize, ShowHelpOnStart = true, VideoFormats = DefaultVideoFormats, - ImageFormats = DefaultImageFormats + ImageFormats = DefaultImageFormats, + ThumbnailConfig = new ThumbnailConfig + { + Size = DefaultThumbnailSize, + Border = 10 + } }; } var str = File.ReadAllText(CONFIG_PATH); diff --git a/Azaion.Annotator/DTO/FormState.cs b/Azaion.Annotator/DTO/FormState.cs index 8374f2c..c92d0e5 100644 --- a/Azaion.Annotator/DTO/FormState.cs +++ b/Azaion.Annotator/DTO/FormState.cs @@ -9,11 +9,14 @@ public class FormState public SelectionState SelectionState { get; set; } = SelectionState.None; public MediaFileInfo? CurrentMedia { get; set; } - public Size CurrentVideoSize { get; set; } public string VideoName => string.IsNullOrEmpty(CurrentMedia?.Name) ? "" : Path.GetFileNameWithoutExtension(CurrentMedia.Name).Replace(" ", ""); + + public string CurrentMrl { get; set; } + public Size CurrentVideoSize { get; set; } public TimeSpan CurrentVideoLength { get; set; } + public int CurrentVolume { get; set; } = 100; public ObservableCollection AnnotationResults { get; set; } = []; diff --git a/Azaion.Annotator/DTO/Label.cs b/Azaion.Annotator/DTO/Label.cs index 4cb019c..a78d054 100644 --- a/Azaion.Annotator/DTO/Label.cs +++ b/Azaion.Annotator/DTO/Label.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.IO; using System.Windows; using Newtonsoft.Json; @@ -146,7 +147,17 @@ public class YoloLabel : Label return null; } } - + + public static async Task> ReadFromFile(string filename, CancellationToken cancellationToken) + { + var str = await File.ReadAllTextAsync(filename, cancellationToken); + + return str.Split(Environment.NewLine) + .Select(Parse) + .Where(ann => ann != null) + .ToList()!; + } + 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 new file mode 100644 index 0000000..341fc0e --- /dev/null +++ b/Azaion.Annotator/DatasetExplorer.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + diff --git a/Azaion.Annotator/DatasetExplorer.xaml.cs b/Azaion.Annotator/DatasetExplorer.xaml.cs new file mode 100644 index 0000000..615836e --- /dev/null +++ b/Azaion.Annotator/DatasetExplorer.xaml.cs @@ -0,0 +1,30 @@ +using System.Windows; + +namespace Azaion.Annotator; + +public partial class DatasetExplorer : Window +{ + private CancellationTokenSource _cancellationTokenSource; + + public DatasetExplorer(IGalleryManager galleryManager) + { + _cancellationTokenSource = new CancellationTokenSource(); + + InitializeComponent(); + Loaded += (sender, args) => + { + _ = Task.Run(async () => + { + while (!_cancellationTokenSource.Token.IsCancellationRequested) + { + await galleryManager.RefreshThumbnails(_cancellationTokenSource.Token); + await Task.Delay(30000, _cancellationTokenSource.Token); + } + }); + }; + + Closing += (sender, args) => _cancellationTokenSource.Cancel(); + } + + +} \ No newline at end of file diff --git a/Azaion.Annotator/Extensions/ThrottleExtensions.cs b/Azaion.Annotator/Extensions/ThrottleExtensions.cs index 17d3d10..c9467c5 100644 --- a/Azaion.Annotator/Extensions/ThrottleExtensions.cs +++ b/Azaion.Annotator/Extensions/ThrottleExtensions.cs @@ -10,7 +10,10 @@ public static class ThrottleExt _throttleOn = true; await func(); - await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500)); - _throttleOn = false; + _ = Task.Run(() => + { + Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500)); + _throttleOn = false; + }); } } \ No newline at end of file diff --git a/Azaion.Annotator/GalleryManager.cs b/Azaion.Annotator/GalleryManager.cs index 775d721..2cc5847 100644 --- a/Azaion.Annotator/GalleryManager.cs +++ b/Azaion.Annotator/GalleryManager.cs @@ -1,10 +1,129 @@ -namespace Azaion.Annotator; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; +using Azaion.Annotator.DTO; +using Color = System.Drawing.Color; +using Size = System.Windows.Size; -public class GalleryManager +namespace Azaion.Annotator; + +public class GalleryManager : IGalleryManager { + private readonly Config _config; - public void CreateThumbnails() + public int ThumbnailsCount { get; set; } + public int ImagesCount { get; set; } + + public GalleryManager(Config config) { - + _config = config; } + + public async Task RefreshThumbnails(CancellationToken cancellationToken) + { + 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() + .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) + { + var imgName = Path.GetFileNameWithoutExtension(img.Name); + 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); + + ThumbnailsCount++; + } + } + + private async Task GenerateThumbnail(FileInfo img, CancellationToken cancellationToken) + { + 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 originalImage = Image.FromFile(img.FullName); + + var bitmap = new Bitmap(width, height); + + using var g = Graphics.FromImage(bitmap); + g.CompositingQuality = CompositingQuality.HighSpeed; + g.SmoothingMode = SmoothingMode.HighSpeed; + g.InterpolationMode = InterpolationMode.Default; + + var size = new Size(originalImage.Width, originalImage.Height); + var labels = (await YoloLabel.ReadFromFile(labelName, cancellationToken)) + .Select(x => new CanvasLabel(x, size, size)) + .ToList(); + + var thumbWhRatio = width / (float)height; + 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) + { + 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; + g.DrawImage(originalImage, new Rectangle(0, 0, width, height), new RectangleF((float)frameX, (float)frameY, (float)frameWidth, (float)frameHeight), GraphicsUnit.Pixel); + + foreach (var label in labels) + { + 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; + } +} + +public interface IGalleryManager +{ + int ThumbnailsCount { get; set; } + int ImagesCount { get; set; } + Task RefreshThumbnails(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/Azaion.Annotator/MainWindow.xaml b/Azaion.Annotator/MainWindow.xaml index eb58fa2..4fd13df 100644 --- a/Azaion.Annotator/MainWindow.xaml +++ b/Azaion.Annotator/MainWindow.xaml @@ -53,6 +53,7 @@ Background="Black" HorizontalAlignment="Stretch"> + @@ -75,6 +76,9 @@ + - - + + + + +