mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 21:56:31 +00:00
d842466594
put cryptography lib to fixed version fix race condition bug in queue handler add lock to db writing and backup to file db on each write
316 lines
13 KiB
C#
316 lines
13 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.RunWrite(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 ConcurrentDictionary<string, 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 = file.Name.ToFName();
|
|
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 = fName,
|
|
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
|
|
};
|
|
|
|
//Remove duplicates
|
|
if (!existingAnnotations.ContainsKey(fName))
|
|
{
|
|
if (missedAnnotations.ContainsKey(fName))
|
|
Console.WriteLine($"{fName} is already exists! Duplicate!");
|
|
else
|
|
missedAnnotations.TryAdd(fName, annotation);
|
|
}
|
|
|
|
|
|
if (!thumbnails.Contains(fName))
|
|
await CreateThumbnail(annotation, cancellationToken: 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
|
|
};
|
|
|
|
//Db could be updated during the long files scraping
|
|
existingAnnotations = new ConcurrentDictionary<string, Annotation>(await dbFactory.Run(async db =>
|
|
await db.Annotations.ToDictionaryAsync(x => x.Name)));
|
|
var insertedDuplicates = missedAnnotations.Where(x => existingAnnotations.ContainsKey(x.Key)).ToList();
|
|
var annotationsToInsert = missedAnnotations
|
|
.Where(a => !existingAnnotations.ContainsKey(a.Key))
|
|
.Select(x => x.Value)
|
|
.ToList();
|
|
|
|
await dbFactory.RunWrite(async db =>
|
|
{
|
|
await db.BulkCopyAsync(copyOptions, annotationsToInsert);
|
|
await db.BulkCopyAsync(copyOptions, annotationsToInsert.SelectMany(x => x.Detections));
|
|
});
|
|
_updateLock.Release();
|
|
}
|
|
}
|
|
|
|
public async Task CreateThumbnail(Annotation annotation, Image? originalImage = null, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var width = (int)_thumbnailConfig.Size.Width;
|
|
var height = (int)_thumbnailConfig.Size.Height;
|
|
|
|
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));
|
|
|
|
g.DrawRectangle(new Pen(brush, width: 3), (float)((label.X - frameX) / scale), (float)((label.Y - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale));
|
|
}
|
|
|
|
bitmap.Save(annotation.ThumbPath, ImageFormat.Jpeg);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.LogError(e, e.Message);
|
|
}
|
|
}
|
|
public async Task CreateAnnotatedImage(Annotation annotation, Image? originalImage = null, CancellationToken token = default)
|
|
{
|
|
originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, token)));
|
|
|
|
using var g = Graphics.FromImage(originalImage);
|
|
foreach (var detection in annotation.Detections)
|
|
{
|
|
var detClass = _annotationConfig.DetectionClassesDict[detection.ClassNumber];
|
|
var color = detClass.Color;
|
|
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
|
|
var det = new CanvasLabel(detection, new Size(originalImage.Width, originalImage.Height));
|
|
g.DrawRectangle(new Pen(brush, width: 3), (float)det.X, (float)det.Y, (float)det.Width, (float)det.Height);
|
|
|
|
var label = detection.Confidence >= 0.995 ? detClass.UIName : $"{detClass.UIName}: {detection.Confidence * 100:F0}%";
|
|
g.DrawTextBox(label, new PointF((float)(det.X + det.Width / 2.0), (float)(det.Y - 24)), brush, Brushes.Black);
|
|
}
|
|
|
|
var imagePath = Path.Combine(_dirConfig.ResultsDirectory, $"{annotation.Name}{Constants.RESULT_PREFIX}.jpg");
|
|
if (File.Exists(imagePath))
|
|
ResilienceExt.WithRetry(() => File.Delete(imagePath));
|
|
|
|
originalImage.Save(imagePath, ImageFormat.Jpeg);
|
|
}
|
|
}
|
|
|
|
public interface IGalleryService
|
|
{
|
|
event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate;
|
|
Task CreateThumbnail(Annotation annotation, Image? originalImage = null, CancellationToken cancellationToken = default);
|
|
Task RefreshThumbnails();
|
|
Task ClearThumbnails(CancellationToken cancellationToken = default);
|
|
Task CreateAnnotatedImage(Annotation annotation, Image? originalImage = null, CancellationToken token = default);
|
|
} |