From d842466594667b4d32cf285e3c0530a0c90deed3 Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Thu, 29 May 2025 00:35:35 +0300 Subject: [PATCH] gps matcher async 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 --- Azaion.Annotator/AnnotatorEventHandler.cs | 51 ++++++-- Azaion.Common/Database/DbFactory.cs | 63 +++++++-- Azaion.Common/Extensions/ResilienceExt.cs | 15 +++ Azaion.Common/Services/AnnotationService.cs | 95 +++++++------- Azaion.Common/Services/FailsafeProducer.cs | 120 +++++++++--------- Azaion.Common/Services/GPSMatcherService.cs | 3 +- Azaion.Common/Services/GalleryService.cs | 29 +++-- Azaion.Common/Services/GpsMatcherClient.cs | 27 ++-- ...ssDistrtibutionProportionWidthConverter.cs | 6 +- Azaion.Dataset/DatasetExplorer.xaml.cs | 24 ++-- Azaion.Inference/requirements.txt | 2 +- Azaion.Suite/MainSuite.xaml.cs | 1 - 12 files changed, 245 insertions(+), 191 deletions(-) create mode 100644 Azaion.Common/Extensions/ResilienceExt.cs diff --git a/Azaion.Annotator/AnnotatorEventHandler.cs b/Azaion.Annotator/AnnotatorEventHandler.cs index c2258dc..e9e18cd 100644 --- a/Azaion.Annotator/AnnotatorEventHandler.cs +++ b/Azaion.Annotator/AnnotatorEventHandler.cs @@ -1,15 +1,16 @@ using System.IO; using System.Windows; using System.Windows.Input; -using System.Windows.Threading; using Azaion.Annotator.DTO; using Azaion.Common; +using Azaion.Common.Database; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.Common.Events; using Azaion.Common.Extensions; using Azaion.Common.Services; using Azaion.CommonSecurity.DTO; +using Azaion.CommonSecurity.Services; using LibVLCSharp.Shared; using MediatR; using Microsoft.Extensions.Logging; @@ -27,7 +28,10 @@ public class AnnotatorEventHandler( ILogger logger, IOptions dirConfig, IOptions annotationConfig, - IInferenceService inferenceService) + IInferenceService inferenceService, + IDbFactory dbFactory, + IAzaionApi api, + FailsafeAnnotationsProducer producer) : INotificationHandler, INotificationHandler, @@ -52,7 +56,7 @@ public class AnnotatorEventHandler( { Key.PageDown, PlaybackControlEnum.Next }, }; - public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken) + public async Task Handle(AnnClassSelectedEvent notification, CancellationToken ct) { SelectClass(notification.DetectionClass); await Task.CompletedTask; @@ -66,7 +70,7 @@ public class AnnotatorEventHandler( mainWindow.LvClasses.SelectNum(detClass.Id); } - public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken = default) + public async Task Handle(KeyEvent keyEvent, CancellationToken ct = default) { if (keyEvent.WindowEnum != WindowEnum.Annotator) return; @@ -82,7 +86,7 @@ public class AnnotatorEventHandler( SelectClass((DetectionClass)mainWindow.LvClasses.DetectionDataGrid.Items[keyNumber.Value]!); if (_keysControlEnumDict.TryGetValue(key, out var value)) - await ControlPlayback(value, cancellationToken); + await ControlPlayback(value, ct); if (key == Key.R) await mainWindow.AutoDetect(); @@ -91,10 +95,10 @@ public class AnnotatorEventHandler( switch (key) { case Key.VolumeMute when mediaPlayer.Volume == 0: - await ControlPlayback(PlaybackControlEnum.TurnOnVolume, cancellationToken); + await ControlPlayback(PlaybackControlEnum.TurnOnVolume, ct); break; case Key.VolumeMute: - await ControlPlayback(PlaybackControlEnum.TurnOffVolume, cancellationToken); + await ControlPlayback(PlaybackControlEnum.TurnOffVolume, ct); break; case Key.Up: case Key.VolumeUp: @@ -112,9 +116,9 @@ public class AnnotatorEventHandler( #endregion } - public async Task Handle(AnnotatorControlEvent notification, CancellationToken cancellationToken = default) + public async Task Handle(AnnotatorControlEvent notification, CancellationToken ct = default) { - await ControlPlayback(notification.PlaybackControl, cancellationToken); + await ControlPlayback(notification.PlaybackControl, ct); mainWindow.VideoView.Focus(); } @@ -201,7 +205,7 @@ public class AnnotatorEventHandler( await Play(ct); } - public async Task Handle(VolumeChangedEvent notification, CancellationToken cancellationToken) + public async Task Handle(VolumeChangedEvent notification, CancellationToken ct) { ChangeVolume(notification.Volume); await Task.CompletedTask; @@ -277,7 +281,7 @@ public class AnnotatorEventHandler( mainWindow.AddAnnotation(annotation); } - public Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) + public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken ct) { try { @@ -306,16 +310,37 @@ public class AnnotatorEventHandler( } } }); + + await dbFactory.DeleteAnnotations(notification.AnnotationNames, ct); + + try + { + foreach (var name in notification.AnnotationNames) + { + File.Delete(Path.Combine(dirConfig.Value.ImagesDirectory, $"{name}{Constants.JPG_EXT}")); + File.Delete(Path.Combine(dirConfig.Value.LabelsDirectory, $"{name}{Constants.TXT_EXT}")); + File.Delete(Path.Combine(dirConfig.Value.ThumbnailsDirectory, $"{name}{Constants.THUMBNAIL_PREFIX}{Constants.JPG_EXT}")); + File.Delete(Path.Combine(dirConfig.Value.ResultsDirectory, $"{name}{Constants.RESULT_PREFIX}{Constants.JPG_EXT}")); + } + } + catch (Exception e) + { + logger.LogError(e, e.Message); + throw; + } + + //Only validators can send Delete to the queue + if (!notification.FromQueue && api.CurrentUser.Role.IsValidator()) + await producer.SendToInnerQueue(notification.AnnotationNames, AnnotationStatus.Deleted, ct); } catch (Exception e) { logger.LogError(e, e.Message); throw; } - return Task.CompletedTask; } - public Task Handle(AnnotationAddedEvent e, CancellationToken cancellationToken) + public Task Handle(AnnotationAddedEvent e, CancellationToken ct) { mainWindow.Dispatcher.Invoke(() => { diff --git a/Azaion.Common/Database/DbFactory.cs b/Azaion.Common/Database/DbFactory.cs index 6b33fbe..a46f7ff 100644 --- a/Azaion.Common/Database/DbFactory.cs +++ b/Azaion.Common/Database/DbFactory.cs @@ -1,10 +1,9 @@ using System.Data.SQLite; -using System.Diagnostics; using System.IO; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; +using Azaion.Common.Extensions; using LinqToDB; -using LinqToDB.Data; using LinqToDB.DataProvider.SQLite; using LinqToDB.Mapping; using Microsoft.Extensions.Logging; @@ -16,13 +15,14 @@ namespace Azaion.Common.Database; public interface IDbFactory { Task Run(Func> func); - Task Run(Func func); - void SaveToDisk(); + Task RunWrite(Func func); + Task RunWrite(Func> func); Task DeleteAnnotations(List annotationNames, CancellationToken cancellationToken = default); } public class DbFactory : IDbFactory { + private readonly ILogger _logger; private readonly AnnotationConfig _annConfig; private string MemoryConnStr => "Data Source=:memory:"; @@ -33,8 +33,12 @@ public class DbFactory : IDbFactory private readonly SQLiteConnection _fileConnection; private readonly DataOptions _fileDataOptions; + private static readonly SemaphoreSlim WriteSemaphore = new(1, 1); + private static readonly Guid SaveTaskId = Guid.NewGuid(); + public DbFactory(IOptions annConfig, ILogger logger) { + _logger = logger; _annConfig = annConfig.Value; _memoryConnection = new SQLiteConnection(MemoryConnStr); @@ -79,26 +83,63 @@ public class DbFactory : IDbFactory return await func(db); } - public async Task Run(Func func) + public async Task RunWrite(Func func) { - await using var db = new AnnotationsDb(_memoryDataOptions); - await func(db); + await WriteSemaphore.WaitAsync(); + try + { + await using var db = new AnnotationsDb(_memoryDataOptions); + await func(db); + ThrottleExt.Throttle(async () => + { + _memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1); + await Task.CompletedTask; + }, SaveTaskId, TimeSpan.FromSeconds(5), true); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + throw; + } + finally + { + WriteSemaphore.Release(); + } } - public void SaveToDisk() + public async Task RunWrite(Func> func) { - _memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1); + await WriteSemaphore.WaitAsync(); + try + { + await using var db = new AnnotationsDb(_memoryDataOptions); + var result = await func(db); + ThrottleExt.Throttle(async () => + { + _memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1); + await Task.CompletedTask; + }, SaveTaskId, TimeSpan.FromSeconds(5), true); + return result; + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + throw; + } + finally + { + WriteSemaphore.Release(); + } } public async Task DeleteAnnotations(List annotationNames, CancellationToken cancellationToken = default) { - await Run(async db => + await RunWrite(async db => { var detDeleted = await db.Detections.DeleteAsync(x => annotationNames.Contains(x.AnnotationName), token: cancellationToken); var annDeleted = await db.Annotations.DeleteAsync(x => annotationNames.Contains(x.Name), token: cancellationToken); Console.WriteLine($"Deleted {detDeleted} detections, {annDeleted} annotations"); }); - SaveToDisk(); } } diff --git a/Azaion.Common/Extensions/ResilienceExt.cs b/Azaion.Common/Extensions/ResilienceExt.cs new file mode 100644 index 0000000..fb8f382 --- /dev/null +++ b/Azaion.Common/Extensions/ResilienceExt.cs @@ -0,0 +1,15 @@ +using Polly; + +public static class ResilienceExt +{ + public static void WithRetry(this Action operation, int retryCount = 3, int delayMs = 150) => + Policy.Handle() + .WaitAndRetry(retryCount, num => TimeSpan.FromMilliseconds(num * delayMs), + (exception, timeSpan) => Console.WriteLine($"Exception: {exception}, TimeSpan: {timeSpan}")) + .Execute(operation); + + public static TResult WithRetry(this Func operation, int retryCount = 3, int delayMs = 150) => + Policy.Handle() + .WaitAndRetry(retryCount, num => TimeSpan.FromMilliseconds(num * delayMs)) + .Execute(operation); +} \ No newline at end of file diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs index ef60ffb..25ba4f2 100644 --- a/Azaion.Common/Services/AnnotationService.cs +++ b/Azaion.Common/Services/AnnotationService.cs @@ -1,4 +1,5 @@ -using System.Drawing.Imaging; +using System.Drawing; +using System.Drawing.Imaging; using System.IO; using System.Net; using Azaion.Common.Database; @@ -21,7 +22,8 @@ using RabbitMQ.Stream.Client.Reliable; namespace Azaion.Common.Services; -public class AnnotationService : IAnnotationService, INotificationHandler +// SHOULD BE ONLY ONE INSTANCE OF AnnotationService. Do not add ANY NotificationHandler to it! +public class AnnotationService : IAnnotationService { private readonly IDbFactory _dbFactory; private readonly FailsafeAnnotationsProducer _producer; @@ -32,16 +34,16 @@ public class AnnotationService : IAnnotationService, INotificationHandler queueConfig, IOptions uiConfig, - IOptions directoriesConfig, IGalleryService galleryService, IMediator mediator, IAzaionApi api, @@ -55,7 +57,6 @@ public class AnnotationService : IAnnotationService, INotificationHandler await InitQueueConsumer()).Wait(); } @@ -80,6 +81,7 @@ public class AnnotationService : IAnnotationService, INotificationHandler { + await _messageProcessingSemaphore.WaitAsync(cancellationToken); try { var email = (string)message.ApplicationProperties[nameof(User.Email)]!; @@ -100,7 +102,7 @@ public class AnnotationService : IAnnotationService, INotificationHandler detections, SourceEnum source, Stream? stream, RoleEnum userRole, string createdEmail, - bool fromQueue = false, + ulong? offset = null, CancellationToken token = default) { var status = AnnotationStatus.Created; var fName = originalMediaName.ToTimeName(time); - var annotation = await _dbFactory.Run(async db => + var annotation = await _dbFactory.RunWrite(async db => { var ann = await db.Annotations .LoadWith(x => x.Detections) @@ -200,27 +206,40 @@ public class AnnotationService : IAnnotationService, INotificationHandler File.Delete(annotation.ImagePath)); + image.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue + } - await _galleryService.CreateThumbnail(annotation, token); - if (_uiConfig.GenerateAnnotatedImage) - await _galleryService.CreateAnnotatedImage(annotation, token); + await YoloLabel.WriteToFile(detections, annotation.LabelPath, token); + + await _galleryService.CreateThumbnail(annotation, image, token); + if (_uiConfig.GenerateAnnotatedImage) + await _galleryService.CreateAnnotatedImage(annotation, image, token); + } + catch (Exception e) + { + _logger.LogError(e, $"Try to save {annotation.ImagePath}, Error: {e.Message}"); + throw; + } + finally + { + _imageAccessSemaphore.Release(); + } await _mediator.Publish(new AnnotationCreatedEvent(annotation), token); - if (!fromQueue) //Send to queue only if we're not getting from queue already + if (!offset.HasValue) //Send to queue only if we're not getting from queue already await _producer.SendToInnerQueue([annotation.Name], status, token); - ThrottleExt.Throttle(async () => - { - _dbFactory.SaveToDisk(); - await Task.CompletedTask; - }, SaveTaskId, TimeSpan.FromSeconds(5), true); return annotation; } @@ -230,7 +249,7 @@ public class AnnotationService : IAnnotationService, INotificationHandler + await _dbFactory.RunWrite(async db => { await db.Annotations .Where(x => annNames.Contains(x.Name)) @@ -241,34 +260,6 @@ public class AnnotationService : IAnnotationService, INotificationHandler - { - _dbFactory.SaveToDisk(); - await Task.CompletedTask; - }, SaveTaskId, TimeSpan.FromSeconds(5), true); - } - - public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken ct) - { - await _dbFactory.DeleteAnnotations(notification.AnnotationNames, ct); - foreach (var name in notification.AnnotationNames) - { - File.Delete(Path.Combine(_dirConfig.ImagesDirectory, $"{name}{Constants.JPG_EXT}")); - File.Delete(Path.Combine(_dirConfig.LabelsDirectory, $"{name}{Constants.TXT_EXT}")); - File.Delete(Path.Combine(_dirConfig.ThumbnailsDirectory, $"{name}{Constants.THUMBNAIL_PREFIX}{Constants.JPG_EXT}")); - File.Delete(Path.Combine(_dirConfig.ResultsDirectory, $"{name}{Constants.RESULT_PREFIX}{Constants.JPG_EXT}")); - } - - //Only validators can send Delete to the queue - if (!notification.FromQueue && _api.CurrentUser.Role.IsValidator()) - await _producer.SendToInnerQueue(notification.AnnotationNames, AnnotationStatus.Deleted, ct); - - ThrottleExt.Throttle(async () => - { - _dbFactory.SaveToDisk(); - await Task.CompletedTask; - }, SaveTaskId, TimeSpan.FromSeconds(5), true); } } diff --git a/Azaion.Common/Services/FailsafeProducer.cs b/Azaion.Common/Services/FailsafeProducer.cs index 98b87bc..a570a65 100644 --- a/Azaion.Common/Services/FailsafeProducer.cs +++ b/Azaion.Common/Services/FailsafeProducer.cs @@ -62,7 +62,7 @@ public class FailsafeAnnotationsProducer { try { - var result = await _dbFactory.Run(async db => + var (records, annotationsDict) = await _dbFactory.Run(async db => { var records = await db.AnnotationsQueueRecords.OrderBy(x => x.DateTime).ToListAsync(token: ct); var editedCreatedNames = records @@ -73,69 +73,67 @@ public class FailsafeAnnotationsProducer var annotationsDict = await db.Annotations.LoadWith(x => x.Detections) .Where(x => editedCreatedNames.Contains(x.Name)) .ToDictionaryAsync(a => a.Name, token: ct); - - var messages = new List(); - foreach (var record in records) - { - var appProperties = new ApplicationProperties - { - { nameof(AnnotationStatus), record.Operation.ToString() }, - { nameof(User.Email), _azaionApi.CurrentUser.Email } - }; - - if (record.Operation.In(AnnotationStatus.Validated, AnnotationStatus.Deleted)) - { - var message = new Message(MessagePackSerializer.Serialize(new AnnotationBulkMessage - { - AnnotationNames = record.AnnotationNames.ToArray(), - AnnotationStatus = record.Operation, - Email = _azaionApi.CurrentUser.Email, - CreatedDate = record.DateTime - })) { ApplicationProperties = appProperties }; - - messages.Add(message); - } - else - { - var annotation = annotationsDict!.GetValueOrDefault(record.AnnotationNames.FirstOrDefault()); - if (annotation == null) - continue; - - var image = record.Operation == AnnotationStatus.Created - ? await File.ReadAllBytesAsync(annotation.ImagePath, ct) - : null; - - var annMessage = new AnnotationMessage - { - Name = annotation.Name, - OriginalMediaName = annotation.OriginalMediaName, - Time = annotation.Time, - Role = annotation.CreatedRole, - Email = annotation.CreatedEmail, - CreatedDate = annotation.CreatedDate, - Status = annotation.AnnotationStatus, - - ImageExtension = annotation.ImageExtension, - Image = image, - Detections = JsonConvert.SerializeObject(annotation.Detections), - Source = annotation.Source, - }; - var message = new Message(MessagePackSerializer.Serialize(annMessage)) { ApplicationProperties = appProperties }; - - messages.Add(message); - } - } - - return (messages, records); + return (records, annotationsDict); }); - if (result.messages.Any()) + var messages = new List(); + foreach (var record in records) { - await _annotationProducer.Send(result.messages, CompressionType.Gzip); - var ids = result.records.Select(x => x.Id).ToList(); - var removed = await _dbFactory.Run(async db => await db.AnnotationsQueueRecords.DeleteAsync(x => ids.Contains(x.Id), token: ct)); + var appProperties = new ApplicationProperties + { + { nameof(AnnotationStatus), record.Operation.ToString() }, + { nameof(User.Email), _azaionApi.CurrentUser.Email } + }; + + if (record.Operation.In(AnnotationStatus.Validated, AnnotationStatus.Deleted)) + { + var message = new Message(MessagePackSerializer.Serialize(new AnnotationBulkMessage + { + AnnotationNames = record.AnnotationNames.ToArray(), + AnnotationStatus = record.Operation, + Email = _azaionApi.CurrentUser.Email, + CreatedDate = record.DateTime + })) { ApplicationProperties = appProperties }; + + messages.Add(message); + } + else + { + var annotation = annotationsDict!.GetValueOrDefault(record.AnnotationNames.FirstOrDefault()); + if (annotation == null) + continue; + + var image = record.Operation == AnnotationStatus.Created + ? await File.ReadAllBytesAsync(annotation.ImagePath, ct) + : null; + + var annMessage = new AnnotationMessage + { + Name = annotation.Name, + OriginalMediaName = annotation.OriginalMediaName, + Time = annotation.Time, + Role = annotation.CreatedRole, + Email = annotation.CreatedEmail, + CreatedDate = annotation.CreatedDate, + Status = annotation.AnnotationStatus, + + ImageExtension = annotation.ImageExtension, + Image = image, + Detections = JsonConvert.SerializeObject(annotation.Detections), + Source = annotation.Source, + }; + var message = new Message(MessagePackSerializer.Serialize(annMessage)) { ApplicationProperties = appProperties }; + + messages.Add(message); + } + } + + if (messages.Any()) + { + await _annotationProducer.Send(messages, CompressionType.Gzip); + var ids = records.Select(x => x.Id).ToList(); + var removed = await _dbFactory.RunWrite(async db => await db.AnnotationsQueueRecords.DeleteAsync(x => ids.Contains(x.Id), token: ct)); sent = true; - _dbFactory.SaveToDisk(); } } catch (Exception e) @@ -153,7 +151,7 @@ public class FailsafeAnnotationsProducer { if (_uiConfig.SilentDetection) return; - await _dbFactory.Run(async db => + await _dbFactory.RunWrite(async db => await db.InsertAsync(new AnnotationQueueRecord { Id = Guid.NewGuid(), diff --git a/Azaion.Common/Services/GPSMatcherService.cs b/Azaion.Common/Services/GPSMatcherService.cs index 9962de9..4847b20 100644 --- a/Azaion.Common/Services/GPSMatcherService.cs +++ b/Azaion.Common/Services/GPSMatcherService.cs @@ -18,7 +18,6 @@ public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDow INotificationHandler, INotificationHandler { - private readonly IGpsMatcherClient _gpsMatcherClient = gpsMatcherClient; private readonly DirectoriesConfig _dirConfig = dirConfig.Value; private const int ZOOM_LEVEL = 18; private const int POINTS_COUNT = 10; @@ -69,7 +68,7 @@ public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDow .ToDictionary(x => x.Filename, x => x.Index); await satelliteTileDownloader.GetTiles(_currentLat, _currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, _detectToken); - _gpsMatcherClient.StartMatching(new StartMatchingEvent + gpsMatcherClient.StartMatching(new StartMatchingEvent { ImagesCount = POINTS_COUNT, Latitude = _currentLat, diff --git a/Azaion.Common/Services/GalleryService.cs b/Azaion.Common/Services/GalleryService.cs index 82616de..548e3a1 100644 --- a/Azaion.Common/Services/GalleryService.cs +++ b/Azaion.Common/Services/GalleryService.cs @@ -61,7 +61,7 @@ public class GalleryService( { foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles()) file.Delete(); - await dbFactory.Run(async db => + await dbFactory.RunWrite(async db => { await db.Detections.DeleteAsync(x => true, token: cancellationToken); await db.Annotations.DeleteAsync(x => true, token: cancellationToken); @@ -157,7 +157,7 @@ public class GalleryService( if (!thumbnails.Contains(fName)) - await CreateThumbnail(annotation, cancellationToken); + await CreateThumbnail(annotation, cancellationToken: cancellationToken); } catch (Exception e) @@ -198,24 +198,23 @@ public class GalleryService( .Select(x => x.Value) .ToList(); - await dbFactory.Run(async db => + await dbFactory.RunWrite(async db => { await db.BulkCopyAsync(copyOptions, annotationsToInsert); await db.BulkCopyAsync(copyOptions, annotationsToInsert.SelectMany(x => x.Detections)); }); - dbFactory.SaveToDisk(); _updateLock.Release(); } } - public async Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default) + 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; - var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken))); + originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken))); var bitmap = new Bitmap(width, height); @@ -282,10 +281,9 @@ public class GalleryService( logger.LogError(e, e.Message); } } - - public async Task CreateAnnotatedImage(Annotation annotation, CancellationToken token) + public async Task CreateAnnotatedImage(Annotation annotation, Image? originalImage = null, CancellationToken token = default) { - var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, token))); + originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, token))); using var g = Graphics.FromImage(originalImage); foreach (var detection in annotation.Detections) @@ -299,17 +297,20 @@ public class GalleryService( 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); } - originalImage.Save(Path.Combine(_dirConfig.ResultsDirectory, $"{annotation.Name}{Constants.RESULT_PREFIX}.jpg"), ImageFormat.Jpeg); + + 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; - double ProcessedThumbnailsPercentage { get; set; } - Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default); + Task CreateThumbnail(Annotation annotation, Image? originalImage = null, CancellationToken cancellationToken = default); Task RefreshThumbnails(); Task ClearThumbnails(CancellationToken cancellationToken = default); - - Task CreateAnnotatedImage(Annotation annotation, CancellationToken token); + Task CreateAnnotatedImage(Annotation annotation, Image? originalImage = null, CancellationToken token = default); } \ No newline at end of file diff --git a/Azaion.Common/Services/GpsMatcherClient.cs b/Azaion.Common/Services/GpsMatcherClient.cs index a527220..bfcb86a 100644 --- a/Azaion.Common/Services/GpsMatcherClient.cs +++ b/Azaion.Common/Services/GpsMatcherClient.cs @@ -32,22 +32,15 @@ public class StartMatchingEvent public class GpsMatcherClient : IGpsMatcherClient { private readonly IMediator _mediator; - private readonly GpsDeniedClientConfig _gpsDeniedClientConfig; - private string _requestAddress; + private readonly string _requestAddress; private readonly RequestSocket _requestSocket = new(); - private string _subscriberAddress; + private readonly string _subscriberAddress; private readonly SubscriberSocket _subscriberSocket = new(); - public GpsMatcherClient(IMediator mediator, IOptions gpsDeniedClientConfig) + public GpsMatcherClient(IMediator mediator, IOptions gpsConfig) { _mediator = mediator; - _gpsDeniedClientConfig = gpsDeniedClientConfig.Value; - Start(); - } - - private void Start(CancellationToken ct = default) - { try { using var process = new Process(); @@ -71,16 +64,16 @@ public class GpsMatcherClient : IGpsMatcherClient //throw; } - _requestAddress = $"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqPort}"; + _requestAddress = $"tcp://{gpsConfig.Value.ZeroMqHost}:{gpsConfig.Value.ZeroMqPort}"; _requestSocket.Connect(_requestAddress); - _subscriberAddress = $"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqSubscriberPort}"; + _subscriberAddress = $"tcp://{gpsConfig.Value.ZeroMqHost}:{gpsConfig.Value.ZeroMqSubscriberPort}"; _subscriberSocket.Connect(_subscriberAddress); _subscriberSocket.Subscribe(""); - _subscriberSocket.ReceiveReady += async (_, e) => await ProcessClientCommand(e.Socket, ct); + _subscriberSocket.ReceiveReady += async (_, e) => await ProcessClientCommand(e.Socket); } - private async Task ProcessClientCommand(NetMQSocket socket, CancellationToken ct) + private async Task ProcessClientCommand(NetMQSocket socket) { while (socket.TryReceiveFrameString(TimeSpan.Zero, out var str)) { @@ -90,10 +83,10 @@ public class GpsMatcherClient : IGpsMatcherClient switch (str) { case "FINISHED": - await _mediator.Publish(new GPSMatcherFinishedEvent(), ct); + await _mediator.Publish(new GPSMatcherFinishedEvent()); break; case "OK": - await _mediator.Publish(new GPSMatcherJobAcceptedEvent(), ct); + await _mediator.Publish(new GPSMatcherJobAcceptedEvent()); break; default: var parts = str.Split(','); @@ -107,7 +100,7 @@ public class GpsMatcherClient : IGpsMatcherClient Latitude = double.Parse(parts[2]), Longitude = double.Parse(parts[3]), MatchType = parts[4] - }, ct); + }); break; } } diff --git a/Azaion.Dataset/Controls/ClassDistrtibutionProportionWidthConverter.cs b/Azaion.Dataset/Controls/ClassDistrtibutionProportionWidthConverter.cs index 9d8731a..7a68222 100644 --- a/Azaion.Dataset/Controls/ClassDistrtibutionProportionWidthConverter.cs +++ b/Azaion.Dataset/Controls/ClassDistrtibutionProportionWidthConverter.cs @@ -25,9 +25,7 @@ namespace Azaion.Dataset.Controls return Math.Max(0, calculatedWidth); // Ensure width is not negative } - public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => + [value]; } } \ No newline at end of file diff --git a/Azaion.Dataset/DatasetExplorer.xaml.cs b/Azaion.Dataset/DatasetExplorer.xaml.cs index 3fb8245..7266a2c 100644 --- a/Azaion.Dataset/DatasetExplorer.xaml.cs +++ b/Azaion.Dataset/DatasetExplorer.xaml.cs @@ -106,21 +106,6 @@ public partial class DatasetExplorer new List { new() {Id = -1, Name = "All", ShortName = "All"}} .Concat(_annotationConfig.DetectionClasses)); LvClasses.Init(AllDetectionClasses); - - _dbFactory.Run(db => - { - var allAnnotations = db.Annotations - .LoadWith(x => x.Detections) - .OrderBy(x => x.AnnotationStatus) - .ThenByDescending(x => x.CreatedDate) - .ToList(); - - foreach (var annotation in allAnnotations) - AddAnnotationToDict(annotation); - return null!; - }); - - DataContext = this; } private async void OnLoaded(object sender, RoutedEventArgs e) @@ -138,6 +123,15 @@ public partial class DatasetExplorer ExplorerEditor.CurrentAnnClass = LvClasses.CurrentDetectionClass ?? _annotationConfig.DetectionClasses.First(); + var allAnnotations = await _dbFactory.Run(async db => + await db.Annotations.LoadWith(x => x.Detections) + .OrderBy(x => x.AnnotationStatus) + .ThenByDescending(x => x.CreatedDate) + .ToListAsync()); + + foreach (var annotation in allAnnotations) + AddAnnotationToDict(annotation); + await ReloadThumbnails(); LoadClassDistribution(); diff --git a/Azaion.Inference/requirements.txt b/Azaion.Inference/requirements.txt index b353bfa..8bd5aac 100644 --- a/Azaion.Inference/requirements.txt +++ b/Azaion.Inference/requirements.txt @@ -3,7 +3,7 @@ Cython opencv-python==4.10.0.84 numpy onnxruntime-gpu -cryptography +cryptography==44.0.2 psutil msgpack pyjwt diff --git a/Azaion.Suite/MainSuite.xaml.cs b/Azaion.Suite/MainSuite.xaml.cs index fbce226..6936c9d 100644 --- a/Azaion.Suite/MainSuite.xaml.cs +++ b/Azaion.Suite/MainSuite.xaml.cs @@ -139,7 +139,6 @@ public partial class MainSuite private void OnFormClosed(object? sender, EventArgs e) { _configUpdater.Save(_appConfig); - _dbFactory.SaveToDisk(); foreach (var window in _openedWindows) window.Value.Close();