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; namespace Azaion.Annotator; public delegate void ThumbnailsUpdatedEventHandler(double thumbnailsPercentage); 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 { if (_thumbnailsDirectory != null) return _thumbnailsDirectory; var dir = new DirectoryInfo(_config.ThumbnailsDirectory); if (!dir.Exists) 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.THUMBNAILS_CACHE_FILE); } public void ClearThumbnails() { foreach(var file in new DirectoryInfo(_config.ThumbnailsDirectory).GetFiles()) file.Delete(); } public async Task RefreshThumbnails() { await _updateLock.WaitAsync(); try { var prefixLen = Config.THUMBNAIL_PREFIX.Length; var thumbnails = ThumbnailsDirectory.GetFiles() .Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen]) .GroupBy(x => x) .Select(gr => gr.Key) .ToHashSet(); 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) => { var imgName = Path.GetFileNameWithoutExtension(file.Name); if (thumbnails.Contains(imgName)) return; try { await CreateThumbnail(file.FullName, cancellationToken); } catch (Exception e) { _logger.LogError(e, $"Failed to generate thumbnail for {file.Name}! Error: {e.Message}"); } }, new ParallelOptions { ProgressFn = async num => { Console.WriteLine($"Processed {num} item by Thread {Environment.CurrentManagedThreadId}"); ThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, num * 100 / (double)imagesCount); ThumbnailsUpdate?.Invoke(ThumbnailsPercentage); await Task.CompletedTask; }, CpuUtilPercent = 100, ProgressUpdateInterval = 200 }); } finally { await SaveLabelsCache(); _updateLock.Release(); } } public async Task SaveLabelsCache() { var labelsCacheStr = JsonConvert.SerializeObject(LabelsCache, new DenseDateTimeConverter()); await File.WriteAllTextAsync(_thumbnailsCacheFile, labelsCacheStr); } public async Task CreateThumbnail(string imgPath, CancellationToken cancellationToken = default) { try { 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 originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(imgPath, cancellationToken))); 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); 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; } 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 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; var frameHeight = size.Height; var frameWidth = size.Width; if (labels.Any()) { 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; 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); } var thumbnailName = Path.Combine(ThumbnailsDirectory.FullName, $"{Path.GetFileNameWithoutExtension(imgPath)}{Config.THUMBNAIL_PREFIX}.jpg"); bitmap.Save(thumbnailName, ImageFormat.Jpeg); return new ThumbnailDto { ThumbnailPath = thumbnailName, ImagePath = imgPath, LabelPath = labelName, ImageDate = File.GetCreationTimeUtc(imgPath) }; } catch (Exception e) { _logger.LogError(e, e.Message); return null; } } } 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(); }