mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 22:06:30 +00:00
315 lines
13 KiB
C#
315 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 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))
|
|
logger.LogInformation($"{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 =>
|
|
{
|
|
logger.LogInformation($"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.Left);
|
|
var labelsMaxX = labels.Max(x => x.Left + x.Width);
|
|
|
|
var labelsMinY = labels.Min(x => x.Top);
|
|
var labelsMaxY = labels.Max(x => x.Top + 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.Left - frameX) / scale), (float)((label.Top - 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.Left, (float)det.Top, (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.Left + det.Width / 2.0), (float)(det.Top - 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);
|
|
} |