Files
annotations/Azaion.Common/Services/GalleryService.cs
T
Alex Bezdieniezhnykh a493606f64 correct app close
fix publishing
2025-03-03 19:37:07 +02:00

279 lines
11 KiB
C#

using System.Collections.Concurrent;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using Azaion.Annotator.Extensions;
using Azaion.Common.Database;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue;
using Azaion.Common.Extensions;
using Azaion.CommonSecurity.DTO;
using LinqToDB;
using LinqToDB.Data;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Color = System.Drawing.Color;
using ParallelOptions = Azaion.Annotator.Extensions.ParallelOptions;
using Size = System.Windows.Size;
namespace Azaion.Common.Services;
public delegate void ThumbnailsUpdatedEventHandler(double thumbnailsPercentage);
public class GalleryService(
IOptions<DirectoriesConfig> directoriesConfig,
IOptions<ThumbnailConfig> thumbnailConfig,
IOptions<AnnotationConfig> annotationConfig,
ILogger<GalleryService> logger,
IDbFactory dbFactory) : IGalleryService
{
private readonly DirectoriesConfig _dirConfig = directoriesConfig.Value;
private readonly ThumbnailConfig _thumbnailConfig = thumbnailConfig.Value;
private readonly AnnotationConfig _annotationConfig = annotationConfig.Value;
public event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate;
private readonly SemaphoreSlim _updateLock = new(1);
public double ProcessedThumbnailsPercentage { get; set; }
private DirectoryInfo? _thumbnailsDirectory;
private DirectoryInfo ThumbnailsDirectory
{
get
{
if (_thumbnailsDirectory != null)
return _thumbnailsDirectory;
var dir = new DirectoryInfo(_dirConfig.ThumbnailsDirectory);
if (!dir.Exists)
Directory.CreateDirectory(_dirConfig.ThumbnailsDirectory);
_thumbnailsDirectory = new DirectoryInfo(_dirConfig.ThumbnailsDirectory);
return _thumbnailsDirectory;
}
}
public async Task ClearThumbnails(CancellationToken cancellationToken = default)
{
foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles())
file.Delete();
await dbFactory.Run(async db =>
{
await db.Detections.DeleteAsync(x => true, token: cancellationToken);
await db.Annotations.DeleteAsync(x => true, token: cancellationToken);
});
}
public async Task RefreshThumbnails()
{
await _updateLock.WaitAsync();
var existingAnnotations = new ConcurrentDictionary<string, Annotation>(await dbFactory.Run(async db =>
await db.Annotations.ToDictionaryAsync(x => x.Name)));
var missedAnnotations = new ConcurrentBag<Annotation>();
try
{
var prefixLen = Constants.THUMBNAIL_PREFIX.Length;
var thumbnails = ThumbnailsDirectory.GetFiles()
.Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen])
.GroupBy(x => x)
.Select(gr => gr.Key)
.ToHashSet();
var files = new DirectoryInfo(_dirConfig.ImagesDirectory).GetFiles();
var imagesCount = files.Length;
await ParallelExt.ForEachAsync(files, async (file, cancellationToken) =>
{
var fName = Path.GetFileNameWithoutExtension(file.Name);
try
{
var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{fName}.txt");
if (!File.Exists(labelName))
{
File.Delete(file.FullName);
logger.LogInformation($"No labels found for image {file.FullName}! Image deleted!");
await dbFactory.DeleteAnnotations([fName], cancellationToken);
return;
}
//Read labels file only when it needed
if (existingAnnotations.ContainsKey(fName) && thumbnails.Contains(fName))
return;
var detections = (await YoloLabel.ReadFromFile(labelName, cancellationToken)).Select(x => new Detection(fName, x)).ToList();
//get names and time
var fileName = Path.GetFileNameWithoutExtension(file.Name);
var strings = fileName.Split("_");
var timeStr = strings.LastOrDefault();
string originalMediaName;
TimeSpan time;
//For some reason, TimeSpan.ParseExact doesn't work on every platform.
if (!string.IsNullOrEmpty(timeStr) &&
timeStr.Length == 6 &&
int.TryParse(timeStr[..1], out var hours) &&
int.TryParse(timeStr[1..3], out var minutes) &&
int.TryParse(timeStr[3..5], out var seconds) &&
int.TryParse(timeStr[5..], out var milliseconds))
{
time = new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
originalMediaName = fileName[..^7];
}
else
{
originalMediaName = fileName;
time = TimeSpan.FromSeconds(0);
}
var annotation = new Annotation
{
Time = time,
OriginalMediaName = originalMediaName,
Name = file.Name.ToFName(),
ImageExtension = Path.GetExtension(file.Name),
Detections = detections,
CreatedDate = File.GetCreationTimeUtc(file.FullName),
Source = SourceEnum.Manual,
CreatedRole = RoleEnum.Validator,
CreatedEmail = Constants.ADMIN_EMAIL,
AnnotationStatus = AnnotationStatus.Validated
};
if (!existingAnnotations.ContainsKey(fName))
missedAnnotations.Add(annotation);
if (!thumbnails.Contains(fName))
await CreateThumbnail(annotation, 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}");
ProcessedThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, num * 100 / (double)imagesCount);
ThumbnailsUpdate?.Invoke(ProcessedThumbnailsPercentage);
await Task.CompletedTask;
},
CpuUtilPercent = 100,
ProgressUpdateInterval = 200
});
}
catch (Exception e)
{
logger.LogError(e, $"Failed to refresh thumbnails! Error: {e.Message}");
}
finally
{
var copyOptions = new BulkCopyOptions
{
MaxBatchSize = 50
};
await dbFactory.Run(async db =>
{
await db.BulkCopyAsync(copyOptions, missedAnnotations);
await db.BulkCopyAsync(copyOptions, missedAnnotations.SelectMany(x => x.Detections));
});
dbFactory.SaveToDisk();
_updateLock.Release();
}
}
public async Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default)
{
try
{
var width = (int)_thumbnailConfig.Size.Width;
var height = (int)_thumbnailConfig.Size.Height;
var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, 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);
var thumbWhRatio = width / (float)height;
var border = _thumbnailConfig.Border;
var frameX = 0.0;
var frameY = 0.0;
var frameHeight = size.Height;
var frameWidth = size.Width;
var labels = annotation.Detections
.Select(x => new CanvasLabel(x, size))
.ToList();
if (annotation.Detections.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 = _annotationConfig.DetectionClassesDict[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);
}
bitmap.Save(annotation.ThumbPath, ImageFormat.Jpeg);
}
catch (Exception e)
{
logger.LogError(e, e.Message);
}
}
}
public interface IGalleryService
{
event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate;
double ProcessedThumbnailsPercentage { get; set; }
Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default);
Task RefreshThumbnails();
Task ClearThumbnails(CancellationToken cancellationToken = default);
}