mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 06:46:30 +00:00
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
This commit is contained in:
@@ -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<AnnotatorEventHandler> logger,
|
||||
IOptions<DirectoriesConfig> dirConfig,
|
||||
IOptions<AnnotationConfig> annotationConfig,
|
||||
IInferenceService inferenceService)
|
||||
IInferenceService inferenceService,
|
||||
IDbFactory dbFactory,
|
||||
IAzaionApi api,
|
||||
FailsafeAnnotationsProducer producer)
|
||||
:
|
||||
INotificationHandler<KeyEvent>,
|
||||
INotificationHandler<AnnClassSelectedEvent>,
|
||||
@@ -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(() =>
|
||||
{
|
||||
|
||||
@@ -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<T> Run<T>(Func<AnnotationsDb, Task<T>> func);
|
||||
Task Run(Func<AnnotationsDb, Task> func);
|
||||
void SaveToDisk();
|
||||
Task RunWrite(Func<AnnotationsDb, Task> func);
|
||||
Task<T> RunWrite<T>(Func<AnnotationsDb, Task<T>> func);
|
||||
Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class DbFactory : IDbFactory
|
||||
{
|
||||
private readonly ILogger<DbFactory> _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<AnnotationConfig> annConfig, ILogger<DbFactory> 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<AnnotationsDb, Task> func)
|
||||
public async Task RunWrite(Func<AnnotationsDb, Task> 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<T> RunWrite<T>(Func<AnnotationsDb, Task<T>> 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<string> 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Exception>()
|
||||
.WaitAndRetry(retryCount, num => TimeSpan.FromMilliseconds(num * delayMs),
|
||||
(exception, timeSpan) => Console.WriteLine($"Exception: {exception}, TimeSpan: {timeSpan}"))
|
||||
.Execute(operation);
|
||||
|
||||
public static TResult WithRetry<TResult>(this Func<TResult> operation, int retryCount = 3, int delayMs = 150) =>
|
||||
Policy.Handle<Exception>()
|
||||
.WaitAndRetry(retryCount, num => TimeSpan.FromMilliseconds(num * delayMs))
|
||||
.Execute(operation);
|
||||
}
|
||||
@@ -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<AnnotationsDeletedEvent>
|
||||
// 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<Annota
|
||||
private readonly QueueConfig _queueConfig;
|
||||
private Consumer _consumer = null!;
|
||||
private readonly UIConfig _uiConfig;
|
||||
private readonly DirectoriesConfig _dirConfig;
|
||||
private static readonly Guid SaveTaskId = Guid.NewGuid();
|
||||
private readonly SemaphoreSlim _imageAccessSemaphore = new(1, 1);
|
||||
private readonly SemaphoreSlim _messageProcessingSemaphore = new(1, 1);
|
||||
private static readonly Guid SaveQueueOffsetTaskId = Guid.NewGuid();
|
||||
|
||||
|
||||
public AnnotationService(
|
||||
IDbFactory dbFactory,
|
||||
FailsafeAnnotationsProducer producer,
|
||||
IOptions<QueueConfig> queueConfig,
|
||||
IOptions<UIConfig> uiConfig,
|
||||
IOptions<DirectoriesConfig> directoriesConfig,
|
||||
IGalleryService galleryService,
|
||||
IMediator mediator,
|
||||
IAzaionApi api,
|
||||
@@ -55,7 +57,6 @@ public class AnnotationService : IAnnotationService, INotificationHandler<Annota
|
||||
_logger = logger;
|
||||
_queueConfig = queueConfig.Value;
|
||||
_uiConfig = uiConfig.Value;
|
||||
_dirConfig = directoriesConfig.Value;
|
||||
|
||||
Task.Run(async () => await InitQueueConsumer()).Wait();
|
||||
}
|
||||
@@ -80,6 +81,7 @@ public class AnnotationService : IAnnotationService, INotificationHandler<Annota
|
||||
OffsetSpec = new OffsetTypeOffset(offsets.AnnotationsOffset),
|
||||
MessageHandler = async (_, _, context, message) =>
|
||||
{
|
||||
await _messageProcessingSemaphore.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var email = (string)message.ApplicationProperties[nameof(User.Email)]!;
|
||||
@@ -100,7 +102,7 @@ public class AnnotationService : IAnnotationService, INotificationHandler<Annota
|
||||
msg.Image == null ? null : new MemoryStream(msg.Image),
|
||||
msg.Role,
|
||||
msg.Email,
|
||||
fromQueue: true,
|
||||
context.Offset,
|
||||
token: cancellationToken);
|
||||
}
|
||||
else
|
||||
@@ -124,6 +126,10 @@ public class AnnotationService : IAnnotationService, INotificationHandler<Annota
|
||||
{
|
||||
_logger.LogError(e, e.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_messageProcessingSemaphore.Release();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -145,12 +151,12 @@ public class AnnotationService : IAnnotationService, INotificationHandler<Annota
|
||||
List<Detection> 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<Annota
|
||||
return ann;
|
||||
});
|
||||
|
||||
if (stream != null)
|
||||
//Save image should be done in 1 thread only
|
||||
await _imageAccessSemaphore.WaitAsync(token);
|
||||
try
|
||||
{
|
||||
var img = System.Drawing.Image.FromStream(stream);
|
||||
img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue
|
||||
}
|
||||
await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
|
||||
Image image = null!;
|
||||
if (stream != null)
|
||||
{
|
||||
image = Image.FromStream(stream);
|
||||
if (File.Exists(annotation.ImagePath))
|
||||
ResilienceExt.WithRetry(() => 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<Annota
|
||||
return;
|
||||
|
||||
var annNames = annotationNames.ToHashSet();
|
||||
await _dbFactory.Run(async db =>
|
||||
await _dbFactory.RunWrite(async db =>
|
||||
{
|
||||
await db.Annotations
|
||||
.Where(x => annNames.Contains(x.Name))
|
||||
@@ -241,34 +260,6 @@ public class AnnotationService : IAnnotationService, INotificationHandler<Annota
|
||||
});
|
||||
if (!fromQueue)
|
||||
await _producer.SendToInnerQueue(annotationNames, AnnotationStatus.Validated, token);
|
||||
|
||||
ThrottleExt.Throttle(async () =>
|
||||
{
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Message>();
|
||||
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<Message>();
|
||||
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(),
|
||||
|
||||
@@ -18,7 +18,6 @@ public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDow
|
||||
INotificationHandler<GPSMatcherResultEvent>,
|
||||
INotificationHandler<GPSMatcherFinishedEvent>
|
||||
{
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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> gpsDeniedClientConfig)
|
||||
public GpsMatcherClient(IMediator mediator, IOptions<GpsDeniedClientConfig> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -106,21 +106,6 @@ public partial class DatasetExplorer
|
||||
new List<DetectionClass> { 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();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Cython
|
||||
opencv-python==4.10.0.84
|
||||
numpy
|
||||
onnxruntime-gpu
|
||||
cryptography
|
||||
cryptography==44.0.2
|
||||
psutil
|
||||
msgpack
|
||||
pyjwt
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user