Merge branch 'refs/heads/dev'

# Conflicts:
#	Azaion.Inference/requirements.txt
This commit is contained in:
Alex Bezdieniezhnykh
2025-06-14 16:09:33 +03:00
192 changed files with 4603 additions and 2673 deletions
+10 -2
View File
@@ -1,6 +1,9 @@
.idea .idea
bin bin
obj obj
*.dll
*.exe
*.log
.vs .vs
*.DotSettings* *.DotSettings*
*.user *.user
@@ -11,6 +14,11 @@ venv
*.c *.c
*.pyd *.pyd
cython_debug* cython_debug*
dist dist-dlls
AzaionSuiteInstaller.exe dist-azaion
Azaion*.exe
Azaion*.bin
azaion\.*\.big azaion\.*\.big
_internal
*.spec
+1 -1
View File
@@ -500,7 +500,7 @@
Padding="2" Width="25" Padding="2" Width="25"
Height="25" Height="25"
ToolTip="Розпізнати за допомогою AI. Клавіша: [R]" Background="Black" BorderBrush="Black" ToolTip="Розпізнати за допомогою AI. Клавіша: [R]" Background="Black" BorderBrush="Black"
Click="AutoDetect"> Click="AIDetectBtn_OnClick">
<Path Stretch="Fill" Fill="LightGray" Data="M144.317 85.269h223.368c15.381 0 29.391 6.325 39.567 16.494l.025-.024c10.163 10.164 16.477 24.193 16.477 <Path Stretch="Fill" Fill="LightGray" Data="M144.317 85.269h223.368c15.381 0 29.391 6.325 39.567 16.494l.025-.024c10.163 10.164 16.477 24.193 16.477
39.599v189.728c0 15.401-6.326 29.425-16.485 39.584-10.159 10.159-24.183 16.484-39.584 16.484H144.317c-15.4 39.599v189.728c0 15.401-6.326 29.425-16.485 39.584-10.159 10.159-24.183 16.484-39.584 16.484H144.317c-15.4
0-29.437-6.313-39.601-16.476-10.152-10.152-16.47-24.167-16.47-39.592V141.338c0-15.374 6.306-29.379 16.463-39.558l.078-.078c10.178-10.139 0-29.437-6.313-39.601-16.476-10.152-10.152-16.47-24.167-16.47-39.592V141.338c0-15.374 6.306-29.379 16.463-39.558l.078-.078c10.178-10.139
+76 -119
View File
@@ -37,9 +37,9 @@ public partial class Annotator
private readonly IConfigUpdater _configUpdater; private readonly IConfigUpdater _configUpdater;
private readonly HelpWindow _helpWindow; private readonly HelpWindow _helpWindow;
private readonly ILogger<Annotator> _logger; private readonly ILogger<Annotator> _logger;
private readonly AnnotationService _annotationService;
private readonly IDbFactory _dbFactory; private readonly IDbFactory _dbFactory;
private readonly IInferenceService _inferenceService; private readonly IInferenceService _inferenceService;
private readonly IInferenceClient _inferenceClient;
private ObservableCollection<DetectionClass> AnnotationClasses { get; set; } = new(); private ObservableCollection<DetectionClass> AnnotationClasses { get; set; } = new();
private bool _suspendLayout; private bool _suspendLayout;
@@ -47,16 +47,15 @@ public partial class Annotator
public readonly CancellationTokenSource MainCancellationSource = new(); public readonly CancellationTokenSource MainCancellationSource = new();
public CancellationTokenSource DetectionCancellationSource = new(); public CancellationTokenSource DetectionCancellationSource = new();
public bool FollowAI = false;
public bool IsInferenceNow = false; public bool IsInferenceNow = false;
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(50); private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(50);
private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(150); private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(150);
private readonly IGpsMatcherService _gpsMatcherService;
private static readonly Guid SaveConfigTaskId = Guid.NewGuid(); private static readonly Guid SaveConfigTaskId = Guid.NewGuid();
public ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new(); public ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new();
public ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new(); public ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new();
public Dictionary<string, MediaFileInfo> MediaFilesDict = new();
public IntervalTree<TimeSpan, Annotation> TimedAnnotations { get; set; } = new(); public IntervalTree<TimeSpan, Annotation> TimedAnnotations { get; set; } = new();
@@ -69,9 +68,9 @@ public partial class Annotator
FormState formState, FormState formState,
HelpWindow helpWindow, HelpWindow helpWindow,
ILogger<Annotator> logger, ILogger<Annotator> logger,
AnnotationService annotationService,
IDbFactory dbFactory, IDbFactory dbFactory,
IInferenceService inferenceService, IInferenceService inferenceService,
IInferenceClient inferenceClient,
IGpsMatcherService gpsMatcherService) IGpsMatcherService gpsMatcherService)
{ {
InitializeComponent(); InitializeComponent();
@@ -84,10 +83,9 @@ public partial class Annotator
_formState = formState; _formState = formState;
_helpWindow = helpWindow; _helpWindow = helpWindow;
_logger = logger; _logger = logger;
_annotationService = annotationService;
_dbFactory = dbFactory; _dbFactory = dbFactory;
_inferenceService = inferenceService; _inferenceService = inferenceService;
_gpsMatcherService = gpsMatcherService; _inferenceClient = inferenceClient;
Loaded += OnLoaded; Loaded += OnLoaded;
Closed += OnFormClosed; Closed += OnFormClosed;
@@ -100,16 +98,47 @@ public partial class Annotator
{ {
_appConfig.DirectoriesConfig.VideosDirectory = TbFolder.Text; _appConfig.DirectoriesConfig.VideosDirectory = TbFolder.Text;
await ReloadFiles(); await ReloadFiles();
await SaveUserSettings(); SaveUserSettings();
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, e.Message); _logger.LogError(e, e.Message);
} }
}; };
_inferenceClient.AIAvailabilityReceived += (_, command) =>
{
Dispatcher.Invoke(() =>
{
_logger.LogInformation(command.Message);
var aiEnabled = command.Message == "enabled";
AIDetectBtn.IsEnabled = aiEnabled;
var aiDisabledText = "Будь ласка, зачекайте, наразі розпізнавання AI недоступне";
var messagesDict = new Dictionary<string, string>
{
{ "disabled", aiDisabledText },
{ "downloading", "Будь ласка зачекайте, йде завантаження AI для Вашої відеокарти" },
{ "converting", "Будь ласка зачекайте, йде налаштування AI під Ваше залізо. (5-12 хвилин в залежності від моделі відеокарти, до 50 хв на старих GTX1650)" },
{ "uploading", "Будь ласка зачекайте, йде зберігання" },
{ "enabled", "AI готовий для розпізнавання" }
};
if (command.Message?.StartsWith("Error") ?? false)
{
_logger.LogError(command.Message);
StatusHelp.Text = command.Message;
}
else
StatusHelp.Text = messagesDict!.GetValueOrDefault(command.Message, aiDisabledText);
if (aiEnabled)
StatusHelp.Foreground = aiEnabled ? Brushes.White : Brushes.Red;
});
};
_inferenceClient.Send(RemoteCommand.Create(CommandType.AIAvailabilityCheck));
Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time); Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time);
MapMatcherComponent.Init(_appConfig, _gpsMatcherService); MapMatcherComponent.Init(_appConfig, gpsMatcherService);
} }
private void OnLoaded(object sender, RoutedEventArgs e) private void OnLoaded(object sender, RoutedEventArgs e)
@@ -126,9 +155,6 @@ public partial class Annotator
TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory; TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory;
LvClasses.Init(_appConfig.AnnotationConfig.DetectionClasses); LvClasses.Init(_appConfig.AnnotationConfig.DetectionClasses);
if (LvFiles.Items.IsEmpty)
BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.Initial]);
} }
public void BlinkHelp(string helpText, int times = 2) public void BlinkHelp(string helpText, int times = 2)
@@ -175,8 +201,6 @@ public partial class Annotator
LvFiles.MouseDoubleClick += async (_, _) => LvFiles.MouseDoubleClick += async (_, _) =>
{ {
if (IsInferenceNow)
FollowAI = false;
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play)); await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play));
}; };
@@ -199,9 +223,9 @@ public partial class Annotator
Volume.ValueChanged += (_, newValue) => Volume.ValueChanged += (_, newValue) =>
_mediator.Publish(new VolumeChangedEvent((int)newValue)); _mediator.Publish(new VolumeChangedEvent((int)newValue));
SizeChanged += async (_, _) => await SaveUserSettings(); SizeChanged += (_, _) => SaveUserSettings();
LocationChanged += async (_, _) => await SaveUserSettings(); LocationChanged += (_, _) => SaveUserSettings();
StateChanged += async (_, _) => await SaveUserSettings(); StateChanged += (_, _) => SaveUserSettings();
DgAnnotations.MouseDoubleClick += (sender, args) => DgAnnotations.MouseDoubleClick += (sender, args) =>
{ {
@@ -225,9 +249,9 @@ public partial class Annotator
return; return;
var res = DgAnnotations.SelectedItems.Cast<AnnotationResult>().ToList(); var res = DgAnnotations.SelectedItems.Cast<AnnotationResult>().ToList();
var annotations = res.Select(x => x.Annotation).ToList(); var annotationNames = res.Select(x => x.Annotation.Name).ToList();
await _mediator.Publish(new AnnotationsDeletedEvent(annotations)); await _mediator.Publish(new AnnotationsDeletedEvent(annotationNames));
break; break;
} }
}; };
@@ -238,8 +262,6 @@ public partial class Annotator
public void OpenAnnotationResult(AnnotationResult res) public void OpenAnnotationResult(AnnotationResult res)
{ {
if (IsInferenceNow)
FollowAI = false;
_mediaPlayer.SetPause(true); _mediaPlayer.SetPause(true);
Editor.RemoveAllAnns(); Editor.RemoveAllAnns();
_mediaPlayer.Time = (long)res.Annotation.Time.TotalMilliseconds; _mediaPlayer.Time = (long)res.Annotation.Time.TotalMilliseconds;
@@ -253,7 +275,7 @@ public partial class Annotator
ShowAnnotations(res.Annotation, showImage: true); ShowAnnotations(res.Annotation, showImage: true);
} }
private async Task SaveUserSettings() private void SaveUserSettings()
{ {
if (_suspendLayout) if (_suspendLayout)
return; return;
@@ -261,7 +283,7 @@ public partial class Annotator
_appConfig.UIConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value; _appConfig.UIConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value;
_appConfig.UIConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value; _appConfig.UIConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value;
await ThrottleExt.ThrottleRunFirst(() => ThrottleExt.Throttle(() =>
{ {
_configUpdater.Save(_appConfig); _configUpdater.Save(_appConfig);
return Task.CompletedTask; return Task.CompletedTask;
@@ -324,6 +346,10 @@ public partial class Annotator
//Add manually //Add manually
public void AddAnnotation(Annotation annotation) public void AddAnnotation(Annotation annotation)
{ {
var mediaInfo = (MediaFileInfo)LvFiles.SelectedItem;
if ((mediaInfo?.FName ?? "") != annotation.OriginalMediaName)
return;
var time = annotation.Time; var time = annotation.Time;
var previousAnnotations = TimedAnnotations.Query(time); var previousAnnotations = TimedAnnotations.Query(time);
TimedAnnotations.Remove(previousAnnotations); TimedAnnotations.Remove(previousAnnotations);
@@ -341,10 +367,8 @@ public partial class Annotator
_logger.LogError(e, e.Message); _logger.LogError(e, e.Message);
throw; throw;
} }
} }
var dict = _formState.AnnotationResults var dict = _formState.AnnotationResults
.Select((x, i) => new { x.Annotation.Time, Index = i }) .Select((x, i) => new { x.Annotation.Time, Index = i })
.ToDictionary(x => x.Time, x => x.Index); .ToDictionary(x => x.Time, x => x.Index);
@@ -398,11 +422,9 @@ public partial class Annotator
mediaFile.HasAnnotations = labelsDict.ContainsKey(mediaFile.FName); mediaFile.HasAnnotations = labelsDict.ContainsKey(mediaFile.FName);
AllMediaFiles = new ObservableCollection<MediaFileInfo>(allFiles); AllMediaFiles = new ObservableCollection<MediaFileInfo>(allFiles);
MediaFilesDict = AllMediaFiles.GroupBy(x => x.Name)
.ToDictionary(gr => gr.Key, gr => gr.First());
LvFiles.ItemsSource = AllMediaFiles; LvFiles.ItemsSource = AllMediaFiles;
BlinkHelp(AllMediaFiles.Count == 0
? HelpTexts.HelpTextsDict[HelpTextEnum.Initial]
: HelpTexts.HelpTextsDict[HelpTextEnum.PlayVideo]);
DataContext = this; DataContext = this;
} }
@@ -447,7 +469,7 @@ public partial class Annotator
{ {
Title = "Open Video folder", Title = "Open Video folder",
IsFolderPicker = true, IsFolderPicker = true,
InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) InitialDirectory = Path.GetDirectoryName(_appConfig.DirectoriesConfig.VideosDirectory)
}; };
var dialogResult = dlg.ShowDialog(); var dialogResult = dlg.ShowDialog();
@@ -462,14 +484,13 @@ public partial class Annotator
private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e) private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e)
{ {
FilteredMediaFiles = new ObservableCollection<MediaFileInfo>(AllMediaFiles.Where(x => x.Name.ToLower().Contains(TbFilter.Text.ToLower())).ToList()); FilteredMediaFiles = new ObservableCollection<MediaFileInfo>(AllMediaFiles.Where(x => x.Name.ToLower().Contains(TbFilter.Text.ToLower())).ToList());
MediaFilesDict = FilteredMediaFiles.ToDictionary(x => x.FName);
LvFiles.ItemsSource = FilteredMediaFiles; LvFiles.ItemsSource = FilteredMediaFiles;
LvFiles.ItemsSource = FilteredMediaFiles; LvFiles.ItemsSource = FilteredMediaFiles;
} }
private void PlayClick(object sender, RoutedEventArgs e) private void PlayClick(object sender, RoutedEventArgs e)
{ {
if (IsInferenceNow)
FollowAI = false;
_mediator.Publish(new AnnotatorControlEvent(_mediaPlayer.CanPause ? PlaybackControlEnum.Pause : PlaybackControlEnum.Play)); _mediator.Publish(new AnnotatorControlEvent(_mediaPlayer.CanPause ? PlaybackControlEnum.Pause : PlaybackControlEnum.Play));
} }
@@ -492,7 +513,7 @@ public partial class Annotator
_helpWindow.Activate(); _helpWindow.Activate();
} }
private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => _ = SaveUserSettings(); private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => SaveUserSettings();
private void LvFilesContextOpening(object sender, ContextMenuEventArgs e) private void LvFilesContextOpening(object sender, ContextMenuEventArgs e)
{ {
@@ -500,13 +521,22 @@ public partial class Annotator
LvFilesContextMenu.DataContext = listItem!.DataContext; LvFilesContextMenu.DataContext = listItem!.DataContext;
} }
public void AutoDetect(object sender, RoutedEventArgs e) private async void AIDetectBtn_OnClick(object sender, RoutedEventArgs e)
{
try
{
await AutoDetect();
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
}
}
public async Task AutoDetect()
{ {
if (IsInferenceNow) if (IsInferenceNow)
{
FollowAI = true;
return; return;
}
if (LvFiles.Items.IsEmpty) if (LvFiles.Items.IsEmpty)
return; return;
@@ -516,96 +546,23 @@ public partial class Annotator
Dispatcher.Invoke(() => Editor.ResetBackground()); Dispatcher.Invoke(() => Editor.ResetBackground());
IsInferenceNow = true; IsInferenceNow = true;
FollowAI = true; AIDetectBtn.IsEnabled = false;
DetectionCancellationSource = new CancellationTokenSource(); DetectionCancellationSource = new CancellationTokenSource();
var detectToken = DetectionCancellationSource.Token;
_ = Task.Run(async () => var files = (FilteredMediaFiles.Count == 0 ? AllMediaFiles : FilteredMediaFiles)
{ .Skip(LvFiles.SelectedIndex)
while (!detectToken.IsCancellationRequested)
{
var files = new List<string>();
await Dispatcher.Invoke(async () =>
{
//Take all medias
files = (LvFiles.ItemsSource as IEnumerable<MediaFileInfo>)?.Skip(LvFiles.SelectedIndex)
//.Where(x => !x.HasAnnotations)
.Take(Constants.DETECTION_BATCH_SIZE)
.Select(x => x.Path) .Select(x => x.Path)
.ToList() ?? []; .ToList();
if (files.Count != 0)
{
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play), detectToken);
await ReloadAnnotations();
}
});
if (files.Count == 0) if (files.Count == 0)
break; return;
await _inferenceService.RunInference(files, async annotationImage => await ProcessDetection(annotationImage, detectToken), detectToken); await _inferenceService.RunInference(files, DetectionCancellationSource.Token);
Dispatcher.Invoke(() =>
{
if (LvFiles.SelectedIndex + files.Count >= LvFiles.Items.Count)
DetectionCancellationSource.Cancel();
LvFiles.SelectedIndex += files.Count;
});
}
Dispatcher.Invoke(() =>
{
LvFiles.Items.Refresh(); LvFiles.Items.Refresh();
IsInferenceNow = false; IsInferenceNow = false;
FollowAI = false; StatusHelp.Text = "Розпізнавання зваершено";
}); AIDetectBtn.IsEnabled = true;
});
}
private async Task ProcessDetection(AnnotationImage annotationImage, CancellationToken ct = default)
{
await Dispatcher.Invoke(async () =>
{
try
{
var annotation = await _annotationService.SaveAnnotation(annotationImage, ct);
if (annotation.OriginalMediaName != _formState.CurrentMedia?.FName)
{
var nextFile = (LvFiles.ItemsSource as IEnumerable<MediaFileInfo>)?
.Select((info, i) => new
{
MediaInfo = info,
Index = i
})
.FirstOrDefault(x => x.MediaInfo.FName == annotation.OriginalMediaName);
if (nextFile != null)
{
LvFiles.SelectedIndex = nextFile.Index;
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play), ct);
}
}
AddAnnotation(annotation);
if (FollowAI)
SeekTo(annotationImage.Milliseconds, false);
var log = string.Join(Environment.NewLine, annotation.Detections.Select(det =>
$"{_appConfig.AnnotationConfig.DetectionClassesDict[det.ClassNumber].Name}: " +
$"xy=({det.CenterX:F2},{det.CenterY:F2}), " +
$"size=({det.Width:F2}, {det.Height:F2}), " +
$"conf: {det.Confidence*100:F0}%"));
Dispatcher.Invoke(() =>
{
if (_formState.CurrentMedia != null)
_formState.CurrentMedia.HasAnnotations = true;
LvFiles.Items.Refresh();
StatusHelp.Text = log;
});
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
});
} }
private void SwitchGpsPanel(object sender, RoutedEventArgs e) private void SwitchGpsPanel(object sender, RoutedEventArgs e)
+119 -28
View File
@@ -1,15 +1,18 @@
using System.IO; using System.IO;
using System.Reflection.Metadata;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media;
using Azaion.Annotator.Controls;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Common; using Azaion.Common;
using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue;
using Azaion.Common.Events; using Azaion.Common.Events;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
using Azaion.Common.Services; using Azaion.Common.Services;
using GMap.NET;
using GMap.NET.WindowsPresentation;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using MediatR; using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -23,16 +26,23 @@ public class AnnotatorEventHandler(
MediaPlayer mediaPlayer, MediaPlayer mediaPlayer,
Annotator mainWindow, Annotator mainWindow,
FormState formState, FormState formState,
AnnotationService annotationService, IAnnotationService annotationService,
ILogger<AnnotatorEventHandler> logger, ILogger<AnnotatorEventHandler> logger,
IOptions<DirectoriesConfig> dirConfig, IOptions<DirectoriesConfig> dirConfig,
IInferenceService inferenceService) IOptions<AnnotationConfig> annotationConfig,
IInferenceService inferenceService,
IDbFactory dbFactory,
IAzaionApi api,
FailsafeAnnotationsProducer producer)
: :
INotificationHandler<KeyEvent>, INotificationHandler<KeyEvent>,
INotificationHandler<AnnClassSelectedEvent>, INotificationHandler<AnnClassSelectedEvent>,
INotificationHandler<AnnotatorControlEvent>, INotificationHandler<AnnotatorControlEvent>,
INotificationHandler<VolumeChangedEvent>, INotificationHandler<VolumeChangedEvent>,
INotificationHandler<AnnotationsDeletedEvent> INotificationHandler<AnnotationsDeletedEvent>,
INotificationHandler<AnnotationAddedEvent>,
INotificationHandler<SetStatusTextEvent>,
INotificationHandler<GPSMatcherResultEvent>
{ {
private const int STEP = 20; private const int STEP = 20;
private const int LARGE_STEP = 5000; private const int LARGE_STEP = 5000;
@@ -50,7 +60,7 @@ public class AnnotatorEventHandler(
{ Key.PageDown, PlaybackControlEnum.Next }, { Key.PageDown, PlaybackControlEnum.Next },
}; };
public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken) public async Task Handle(AnnClassSelectedEvent notification, CancellationToken ct)
{ {
SelectClass(notification.DetectionClass); SelectClass(notification.DetectionClass);
await Task.CompletedTask; await Task.CompletedTask;
@@ -64,7 +74,7 @@ public class AnnotatorEventHandler(
mainWindow.LvClasses.SelectNum(detClass.Id); 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) if (keyEvent.WindowEnum != WindowEnum.Annotator)
return; return;
@@ -80,19 +90,19 @@ public class AnnotatorEventHandler(
SelectClass((DetectionClass)mainWindow.LvClasses.DetectionDataGrid.Items[keyNumber.Value]!); SelectClass((DetectionClass)mainWindow.LvClasses.DetectionDataGrid.Items[keyNumber.Value]!);
if (_keysControlEnumDict.TryGetValue(key, out var value)) if (_keysControlEnumDict.TryGetValue(key, out var value))
await ControlPlayback(value, cancellationToken); await ControlPlayback(value, ct);
if (key == Key.R) if (key == Key.R)
mainWindow.AutoDetect(null!, null!); await mainWindow.AutoDetect();
#region Volume #region Volume
switch (key) switch (key)
{ {
case Key.VolumeMute when mediaPlayer.Volume == 0: case Key.VolumeMute when mediaPlayer.Volume == 0:
await ControlPlayback(PlaybackControlEnum.TurnOnVolume, cancellationToken); await ControlPlayback(PlaybackControlEnum.TurnOnVolume, ct);
break; break;
case Key.VolumeMute: case Key.VolumeMute:
await ControlPlayback(PlaybackControlEnum.TurnOffVolume, cancellationToken); await ControlPlayback(PlaybackControlEnum.TurnOffVolume, ct);
break; break;
case Key.Up: case Key.Up:
case Key.VolumeUp: case Key.VolumeUp:
@@ -110,9 +120,9 @@ public class AnnotatorEventHandler(
#endregion #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(); mainWindow.VideoView.Focus();
} }
@@ -130,10 +140,6 @@ public class AnnotatorEventHandler(
break; break;
case PlaybackControlEnum.Pause: case PlaybackControlEnum.Pause:
mediaPlayer.Pause(); mediaPlayer.Pause();
if (mainWindow.IsInferenceNow)
mainWindow.FollowAI = false;
if (!mediaPlayer.IsPlaying)
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]);
if (formState.BackgroundTime.HasValue) if (formState.BackgroundTime.HasValue)
{ {
@@ -203,7 +209,7 @@ public class AnnotatorEventHandler(
await Play(ct); await Play(ct);
} }
public async Task Handle(VolumeChangedEvent notification, CancellationToken cancellationToken) public async Task Handle(VolumeChangedEvent notification, CancellationToken ct)
{ {
ChangeVolume(notification.Volume); ChangeVolume(notification.Volume);
await Task.CompletedTask; await Task.CompletedTask;
@@ -227,7 +233,6 @@ public class AnnotatorEventHandler(
await Task.Delay(100, ct); await Task.Delay(100, ct);
mediaPlayer.Stop(); mediaPlayer.Stop();
mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}"; mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}";
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]);
mediaPlayer.Play(new Media(libVLC, mediaInfo.Path)); mediaPlayer.Play(new Media(libVLC, mediaInfo.Path));
if (formState.CurrentMedia.MediaType == MediaTypes.Image) if (formState.CurrentMedia.MediaType == MediaTypes.Image)
mediaPlayer.SetPause(true); mediaPlayer.SetPause(true);
@@ -280,17 +285,24 @@ public class AnnotatorEventHandler(
mainWindow.AddAnnotation(annotation); mainWindow.AddAnnotation(annotation);
} }
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken ct)
{ {
var annResDict = formState.AnnotationResults.ToDictionary(x => x.Annotation.Name, x => x); try
foreach (var ann in notification.Annotations)
{ {
if (!annResDict.TryGetValue(ann.Name, out var value)) mainWindow.Dispatcher.Invoke(() =>
continue; {
var namesSet = notification.AnnotationNames.ToHashSet();
formState.AnnotationResults.Remove(value); var remainAnnotations = formState.AnnotationResults
mainWindow.TimedAnnotations.Remove(ann); .Where(x => !namesSet.Contains(x.Annotation?.Name ?? "")).ToList();
} formState.AnnotationResults.Clear();
foreach (var ann in remainAnnotations)
formState.AnnotationResults.Add(ann);
var timedAnnsToRemove = mainWindow.TimedAnnotations
.Where(x => namesSet.Contains(x.Value.Name))
.Select(x => x.Value).ToList();
mainWindow.TimedAnnotations.Remove(timedAnnsToRemove);
if (formState.AnnotationResults.Count == 0) if (formState.AnnotationResults.Count == 0)
{ {
@@ -301,6 +313,85 @@ public class AnnotatorEventHandler(
mainWindow.LvFiles.Items.Refresh(); mainWindow.LvFiles.Items.Refresh();
} }
} }
await Task.CompletedTask; });
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;
}
}
public Task Handle(AnnotationAddedEvent e, CancellationToken ct)
{
mainWindow.Dispatcher.Invoke(() =>
{
mainWindow.AddAnnotation(e.Annotation);
var log = string.Join(Environment.NewLine, e.Annotation.Detections.Select(det =>
$"Розпізнавання {e.Annotation.OriginalMediaName}: {annotationConfig.Value.DetectionClassesDict[det.ClassNumber].ShortName}: " +
$"xy=({det.CenterX:F2},{det.CenterY:F2}), " +
$"розмір=({det.Width:F2}, {det.Height:F2}), " +
$"conf: {det.Confidence*100:F0}%"));
mainWindow.LvFiles.Items.Refresh();
var media = mainWindow.MediaFilesDict.GetValueOrDefault(e.Annotation.OriginalMediaName);
if (media != null)
media.HasAnnotations = true;
mainWindow.LvFiles.Items.Refresh();
mainWindow.StatusHelp.Text = log;
});
return Task.CompletedTask;
}
public Task Handle(SetStatusTextEvent e, CancellationToken cancellationToken)
{
mainWindow.Dispatcher.Invoke(() =>
{
mainWindow.StatusHelp.Text = e.Text;
mainWindow.StatusHelp.Foreground = e.IsError ? Brushes.Red : Brushes.White;
});
return Task.CompletedTask;
}
public Task Handle(GPSMatcherResultEvent e, CancellationToken cancellationToken)
{
mainWindow.Dispatcher.Invoke(() =>
{
var mapMatcher = mainWindow.MapMatcherComponent;
var marker = new GMapMarker(new PointLatLng(e.Latitude, e.Longitude));
var ann = mapMatcher.Annotations[e.Index];
marker.Shape = new CircleVisual(marker, size: 14, text: e.Image, background: Brushes.Blue);
mapMatcher.SatelliteMap.Markers.Add(marker);
ann.Lat = e.Latitude;
ann.Lon = e.Longitude;
mapMatcher.SatelliteMap.Position = new PointLatLng(e.Latitude, e.Longitude);
mapMatcher.SatelliteMap.ZoomAndCenterMarkers(null);
});
return Task.CompletedTask;
} }
} }
+14 -11
View File
@@ -7,24 +7,27 @@
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<VersionDate>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd"))</VersionDate>
<VersionSeconds>$([System.Convert]::ToInt32($([System.DateTime]::UtcNow.TimeOfDay.TotalMinutes)))</VersionSeconds>
<AssemblyVersion>$(VersionDate).$(VersionSeconds)</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<InformationalVersion>$(AssemblyVersion)</InformationalVersion>
<Copyright>Copyright @ $([System.DateTime]::UtcNow.ToString("yyyy")) Azaion LLC. All rights reserved.</Copyright>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="GMap.NET.WinPresentation" Version="2.1.7" /> <PackageReference Include="GMap.NET.WinPresentation" Version="2.1.7" />
<PackageReference Include="libc.translation" Version="7.1.1" /> <PackageReference Include="libc.translation" Version="7.1.1" />
<PackageReference Include="LibVLCSharp" Version="3.9.1" /> <PackageReference Include="LibVLCSharp" Version="3.9.1" />
<PackageReference Include="LibVLCSharp.WPF" Version="3.9.1" /> <PackageReference Include="LibVLCSharp.WPF" Version="3.9.1" />
<PackageReference Include="MediatR" Version="12.4.1" /> <PackageReference Include="MediatR" Version="12.5.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.3" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="RangeTree" Version="3.0.1" /> <PackageReference Include="RangeTree" Version="3.0.1" />
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="SkiaSharp" Version="2.88.9" /> <PackageReference Include="SkiaSharp" Version="2.88.9" />
<PackageReference Include="System.Data.SQLite" Version="1.0.119" /> <PackageReference Include="System.Data.SQLite" Version="1.0.119" />
<PackageReference Include="System.Data.SQLite.EF6" Version="1.0.119" /> <PackageReference Include="System.Data.SQLite.EF6" Version="1.0.119" />
+11 -10
View File
@@ -11,7 +11,7 @@ namespace Azaion.Annotator.Controls
{ {
public readonly GMapMarker Marker; public readonly GMapMarker Marker;
public CircleVisual(GMapMarker m, Brush background) public CircleVisual(GMapMarker m, int size, string text, Brush background)
{ {
ShadowEffect = new DropShadowEffect(); ShadowEffect = new DropShadowEffect();
Marker = m; Marker = m;
@@ -22,14 +22,14 @@ namespace Azaion.Annotator.Controls
MouseLeave += CircleVisual_MouseLeave; MouseLeave += CircleVisual_MouseLeave;
Loaded += OnLoaded; Loaded += OnLoaded;
Text = "?"; Text = text;
StrokeArrow.EndLineCap = PenLineCap.Triangle; StrokeArrow.EndLineCap = PenLineCap.Triangle;
StrokeArrow.LineJoin = PenLineJoin.Round; StrokeArrow.LineJoin = PenLineJoin.Round;
RenderTransform = _scale; RenderTransform = _scale;
Width = Height = 22; Width = Height = size;
FontSize = Width / 1.55; FontSize = Width / 1.55;
Background = background; Background = background;
@@ -80,7 +80,7 @@ namespace Azaion.Annotator.Controls
FontWeights.Bold, FontWeights.Bold,
FontStretches.Normal); FontStretches.Normal);
FormattedText _fText; FormattedText _fText = null!;
private Brush _background = Brushes.Blue; private Brush _background = Brushes.Blue;
@@ -178,16 +178,17 @@ namespace Azaion.Annotator.Controls
void ForceUpdateText() void ForceUpdateText()
{ {
_fText = new FormattedText(_text, _fText = new FormattedText(_text,
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
FlowDirection.LeftToRight, FlowDirection.LeftToRight,
Font, Font,
FontSize, FontSize,
Foreground); Foreground, 1.0);
IsChanged = true; IsChanged = true;
} }
string _text; string _text = null!;
public string Text public string Text
{ {
@@ -205,9 +206,9 @@ namespace Azaion.Annotator.Controls
} }
} }
Visual _child; Visual _child = null!;
public virtual Visual Child public virtual Visual? Child
{ {
get => _child; get => _child;
set set
@@ -228,7 +229,7 @@ namespace Azaion.Annotator.Controls
} }
// cache the new child // cache the new child
_child = value; _child = value!;
InvalidateVisual(); InvalidateVisual();
} }
@@ -295,7 +296,7 @@ namespace Azaion.Annotator.Controls
} }
} }
protected override Visual GetVisualChild(int index) protected override Visual? GetVisualChild(int index)
{ {
return Child; return Child;
} }
+9 -44
View File
@@ -1,12 +1,9 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing;
using System.IO; using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using Azaion.Common;
using Azaion.Common.Database; using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
@@ -14,7 +11,6 @@ using Azaion.Common.Extensions;
using Azaion.Common.Services; using Azaion.Common.Services;
using GMap.NET; using GMap.NET;
using GMap.NET.MapProviders; using GMap.NET.MapProviders;
using GMap.NET.WindowsPresentation;
using Microsoft.WindowsAPICodePack.Dialogs; using Microsoft.WindowsAPICodePack.Dialogs;
namespace Azaion.Annotator.Controls; namespace Azaion.Annotator.Controls;
@@ -23,7 +19,7 @@ public partial class MapMatcher : UserControl
{ {
private AppConfig _appConfig = null!; private AppConfig _appConfig = null!;
List<MediaFileInfo> _allMediaFiles = new(); List<MediaFileInfo> _allMediaFiles = new();
private Dictionary<int, Annotation> _annotations = new(); public Dictionary<int, Annotation> Annotations = new();
private string _currentDir = null!; private string _currentDir = null!;
private IGpsMatcherService _gpsMatcherService = null!; private IGpsMatcherService _gpsMatcherService = null!;
@@ -46,8 +42,11 @@ public partial class MapMatcher : UserControl
private async Task OpenGpsLocation(int gpsFilesIndex) private async Task OpenGpsLocation(int gpsFilesIndex)
{ {
var media = GpsFiles.Items[gpsFilesIndex] as MediaFileInfo; //var media = GpsFiles.Items[gpsFilesIndex] as MediaFileInfo;
var ann = _annotations.GetValueOrDefault(gpsFilesIndex); var ann = Annotations.GetValueOrDefault(gpsFilesIndex);
if (ann == null)
return;
GpsImageEditor.Background = new ImageBrush GpsImageEditor.Background = new ImageBrush
{ {
ImageSource = await Path.Combine(_currentDir, ann.Name).OpenImage() ImageSource = await Path.Combine(_currentDir, ann.Name).OpenImage()
@@ -98,7 +97,7 @@ public partial class MapMatcher : UserControl
_allMediaFiles = mediaFiles; _allMediaFiles = mediaFiles;
GpsFiles.ItemsSource = new ObservableCollection<MediaFileInfo>(_allMediaFiles); GpsFiles.ItemsSource = new ObservableCollection<MediaFileInfo>(_allMediaFiles);
_annotations = mediaFiles.Select((x, i) => (i, new Annotation Annotations = mediaFiles.Select((x, i) => (i, new Annotation
{ {
Name = x.Name, Name = x.Name,
OriginalMediaName = x.Name OriginalMediaName = x.Name
@@ -107,41 +106,7 @@ public partial class MapMatcher : UserControl
var initialLat = double.Parse(TbLat.Text); var initialLat = double.Parse(TbLat.Text);
var initialLon = double.Parse(TbLon.Text); var initialLon = double.Parse(TbLon.Text);
await _gpsMatcherService.RunGpsMatching(dir.FullName, initialLat, initialLon, async res => await SetMarker(res)); await _gpsMatcherService.RunGpsMatching(dir.FullName, initialLat, initialLon);
}
private async Task SetMarker(GpsMatchResult result)
{
await Dispatcher.Invoke(async () =>
{
var marker = new GMapMarker(new PointLatLng(result.Latitude, result.Longitude));
var ann = _annotations[result.Index];
marker.Shape = new CircleVisual(marker, System.Windows.Media.Brushes.Blue)
{
Text = ann.Name
};
SatelliteMap.Markers.Add(marker);
ann.Lat = result.Latitude;
ann.Lon = result.Longitude;
SatelliteMap.Position = new PointLatLng(result.Latitude, result.Longitude);
SatelliteMap.ZoomAndCenterMarkers(null);
});
}
private async Task SetFromCsv(List<MediaFileInfo> mediaFiles)
{
var csvResults = GpsMatchResult.ReadFromCsv(Constants.CSV_PATH);
var csvDict = csvResults
.Where(x => x.MatchType == "stitched")
.ToDictionary(x => x.Index);
foreach (var ann in _annotations)
{
var csvRes = csvDict.GetValueOrDefault(ann.Key);
if (csvRes == null)
continue;
await SetMarker(csvRes);
}
} }
private async void TestGps(object sender, RoutedEventArgs e) private async void TestGps(object sender, RoutedEventArgs e)
@@ -151,6 +116,6 @@ public partial class MapMatcher : UserControl
var initialLat = double.Parse(TbLat.Text); var initialLat = double.Parse(TbLat.Text);
var initialLon = double.Parse(TbLon.Text); var initialLon = double.Parse(TbLon.Text);
await _gpsMatcherService.RunGpsMatching(TbGpsMapFolder.Text, initialLat, initialLon, async res => await SetMarker(res)); await _gpsMatcherService.RunGpsMatching(TbGpsMapFolder.Text, initialLat, initialLon);
} }
} }
+9 -9
View File
@@ -7,25 +7,25 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="CsvHelper" Version="33.0.1" /> <PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="LazyCache" Version="2.4.0" />
<PackageReference Include="linq2db.SQLite" Version="5.4.1" /> <PackageReference Include="linq2db.SQLite" Version="5.4.1" />
<PackageReference Include="MediatR" Version="12.4.1" /> <PackageReference Include="MediatR" Version="12.5.0" />
<PackageReference Include="MessagePack" Version="3.1.0" /> <PackageReference Include="MessagePack" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.5" />
<PackageReference Include="NetMQ" Version="4.0.1.16" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Polly" Version="8.5.2" /> <PackageReference Include="Polly" Version="8.5.2" />
<PackageReference Include="RabbitMQ.Stream.Client" Version="1.8.9" /> <PackageReference Include="RabbitMQ.Stream.Client" Version="1.8.9" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageReference Include="Stub.System.Data.SQLite.Core.NetStandard" Version="1.0.119" /> <PackageReference Include="Stub.System.Data.SQLite.Core.NetStandard" Version="1.0.119" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" /> <PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
<PackageReference Include="System.Drawing.Common" Version="5.0.3" /> <PackageReference Include="System.Drawing.Common" Version="5.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Azaion.CommonSecurity\Azaion.CommonSecurity.csproj" />
</ItemGroup>
</Project> </Project>
+44 -20
View File
@@ -1,12 +1,14 @@
using System.Windows; using System.Windows;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
namespace Azaion.Common; namespace Azaion.Common;
public class Constants public class Constants
{ {
public const string JPG_EXT = ".jpg"; public const string JPG_EXT = ".jpg";
public const string TXT_EXT = ".txt";
#region DirectoriesConfig #region DirectoriesConfig
public const string DEFAULT_VIDEO_DIR = "video"; public const string DEFAULT_VIDEO_DIR = "video";
@@ -21,20 +23,33 @@ public class Constants
#region AnnotatorConfig #region AnnotatorConfig
public static readonly List<DetectionClass> DefaultAnnotationClasses = public static readonly AnnotationConfig DefaultAnnotationConfig = new()
{
DetectionClasses = DefaultAnnotationClasses!,
VideoFormats = DefaultVideoFormats!,
ImageFormats = DefaultImageFormats!,
AnnotationsDbFile = DEFAULT_ANNOTATIONS_DB_FILE
};
private static readonly List<DetectionClass> DefaultAnnotationClasses =
[ [
new() { Id = 0, Name = "Броньована техніка", ShortName = "Бронь" }, new() { Id = 0, Name = "ArmorVehicle", ShortName = "Броня", Color = "#FF0000".ToColor() },
new() { Id = 1, Name = "Вантажівка", ShortName = "Вантаж" }, new() { Id = 1, Name = "Truck", ShortName = "Вантаж.", Color = "#00FF00".ToColor() },
new() { Id = 2, Name = "Машина легкова", ShortName = "Машина" }, new() { Id = 2, Name = "Vehicle", ShortName = "Машина", Color = "#0000FF".ToColor() },
new() { Id = 3, Name = "Артилерія", ShortName = "Арта" }, new() { Id = 3, Name = "Atillery", ShortName = "Арта", Color = "#FFFF00".ToColor() },
new() { Id = 4, Name = "Тінь від техніки", ShortName = "Тінь" }, new() { Id = 4, Name = "Shadow", ShortName = "Тінь", Color = "#FF00FF".ToColor() },
new() { Id = 5, Name = "Окопи", ShortName = "Окопи" }, new() { Id = 5, Name = "Trenches", ShortName = "Окопи", Color = "#00FFFF".ToColor() },
new() { Id = 6, Name = "Військовий", ShortName = "Військов" }, new() { Id = 6, Name = "MilitaryMan", ShortName = "Військов", Color = "#188021".ToColor() },
new() { Id = 7, Name = "Накати", ShortName = "Накати" }, new() { Id = 7, Name = "TyreTracks", ShortName = "Накати", Color = "#800000".ToColor() },
new() { Id = 8, Name = "Танк з захистом", ShortName = "Танк захист" }, new() { Id = 8, Name = "AdditArmoredTank", ShortName = "Танк.захист", Color = "#008000".ToColor() },
new() { Id = 9, Name = "Дим", ShortName = "Дим" }, new() { Id = 9, Name = "Smoke", ShortName = "Дим", Color = "#000080".ToColor() },
new() { Id = 10, Name = "Літак", ShortName = "Літак" }, new() { Id = 10, Name = "Plane", ShortName = "Літак", Color = "#000080".ToColor() },
new() { Id = 11, Name = "Мотоцикл", ShortName = "Мото" } new() { Id = 11, Name = "Moto", ShortName = "Мото", Color = "#808000".ToColor() },
new() { Id = 12, Name = "CamouflageNet", ShortName = "Сітка", Color = "#800080".ToColor() },
new() { Id = 13, Name = "CamouflageBranches", ShortName = "Гілки", Color = "#2f4f4f".ToColor() },
new() { Id = 14, Name = "Roof", ShortName = "Дах", Color = "#1e90ff".ToColor() },
new() { Id = 15, Name = "Building", ShortName = "Будівля", Color = "#ffb6c1".ToColor() },
new() { Id = 16, Name = "Caponier", ShortName = "Капонір", Color = "#ffb6c1".ToColor() },
]; ];
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"]; public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
@@ -49,17 +64,31 @@ public class Constants
# region AIRecognitionConfig # region AIRecognitionConfig
public static readonly AIRecognitionConfig DefaultAIRecognitionConfig = new()
{
FrameRecognitionSeconds = DEFAULT_FRAME_RECOGNITION_SECONDS,
TrackingDistanceConfidence = TRACKING_DISTANCE_CONFIDENCE,
TrackingProbabilityIncrease = TRACKING_PROBABILITY_INCREASE,
TrackingIntersectionThreshold = TRACKING_INTERSECTION_THRESHOLD,
FramePeriodRecognition = DEFAULT_FRAME_PERIOD_RECOGNITION
};
public const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2; public const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
public const double TRACKING_DISTANCE_CONFIDENCE = 0.15; public const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
public const double TRACKING_PROBABILITY_INCREASE = 15; public const double TRACKING_PROBABILITY_INCREASE = 15;
public const double TRACKING_INTERSECTION_THRESHOLD = 0.8; public const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
public const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4; public const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4;
public const int DETECTION_BATCH_SIZE = 4;
# endregion AIRecognitionConfig # endregion AIRecognitionConfig
#region Thumbnails #region Thumbnails
public static readonly ThumbnailConfig DefaultThumbnailConfig = new()
{
Size = DefaultThumbnailSize,
Border = DEFAULT_THUMBNAIL_BORDER
};
public static readonly Size DefaultThumbnailSize = new(240, 135); public static readonly Size DefaultThumbnailSize = new(240, 135);
public const int DEFAULT_THUMBNAIL_BORDER = 10; public const int DEFAULT_THUMBNAIL_BORDER = 10;
@@ -69,12 +98,7 @@ public class Constants
#endregion #endregion
#region Queue
public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations"; public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations";
public const string MQ_ANNOTATIONS_CONFIRM_QUEUE = "azaion-annotations-confirm";
#endregion
#region Database #region Database
@@ -52,6 +52,7 @@
CanUserResizeColumns="False" CanUserResizeColumns="False"
SelectionChanged="DetectionDataGrid_SelectionChanged" SelectionChanged="DetectionDataGrid_SelectionChanged"
x:FieldModifier="public" x:FieldModifier="public"
PreviewKeyDown="OnKeyBanActivity"
> >
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTemplateColumn Width="50" Header="Клавіша" CanUserSort="False"> <DataGridTemplateColumn Width="50" Header="Клавіша" CanUserSort="False">
@@ -1,6 +1,7 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
@@ -86,4 +87,11 @@ public partial class DetectionClasses
{ {
DetectionDataGrid.SelectedIndex = keyNumber; DetectionDataGrid.SelectedIndex = keyNumber;
} }
private void OnKeyBanActivity(object sender, KeyEventArgs e)
{
if (e.Key.In(Key.Enter, Key.Down, Key.Up, Key.PageDown, Key.PageUp))
e.Handled = true;
}
} }
+9 -2
View File
@@ -7,9 +7,10 @@ using Azaion.Common.Extensions;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged public class AnnotationThumbnail(Annotation annotation, bool isValidator) : INotifyPropertyChanged
{ {
public Annotation Annotation { get; set; } = annotation; public Annotation Annotation { get; set; } = annotation;
public bool IsValidator { get; set; } = isValidator;
private BitmapImage? _thumbnail; private BitmapImage? _thumbnail;
public BitmapImage? Thumbnail public BitmapImage? Thumbnail
@@ -28,11 +29,17 @@ public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged
} }
public string ImageName => Path.GetFileName(Annotation.ImagePath); public string ImageName => Path.GetFileName(Annotation.ImagePath);
public bool IsSeed => Annotation.AnnotationStatus == AnnotationStatus.Created; public string CreatedDate => $"{Annotation.CreatedDate:dd.MM.yyyy HH:mm:ss}";
public string CreatedEmail => Annotation.CreatedEmail;
public bool IsSeed => IsValidator &&
Annotation.AnnotationStatus.In(AnnotationStatus.Created, AnnotationStatus.Edited) &&
!Annotation.CreatedRole.IsValidator();
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
public void UpdateUI() => OnPropertyChanged(nameof(IsSeed));
} }
+16
View File
@@ -0,0 +1,16 @@
using CommandLine;
using MessagePack;
namespace Azaion.Common.DTO;
[MessagePackObject]
public class ApiCredentials : EventArgs
{
[Key(nameof(Email))]
[Option('e', "email", Required = true, HelpText = "User Email")]
public string Email { get; set; } = null!;
[Key(nameof(Password))]
[Option('p', "pass", Required = true, HelpText = "User Password")]
public string Password { get; set; } = null!;
}
@@ -0,0 +1,7 @@
namespace Azaion.Common.DTO;
public class BusinessExceptionDto
{
public int ErrorCode { get; set; }
public string Message { get; set; } = string.Empty;
}
+11
View File
@@ -0,0 +1,11 @@
using System.Windows.Media;
namespace Azaion.Common.DTO;
public class ClusterDistribution
{
public string Label { get; set; } = "";
public Color Color { get; set; }
public int ClassCount { get; set; }
public double BarWidth { get; set; }
}
+6 -23
View File
@@ -1,13 +1,14 @@
using System.IO; using System.IO;
using System.Text; using System.Text;
using Azaion.CommonSecurity; using Azaion.CommonSecurity;
using Azaion.CommonSecurity.DTO;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Azaion.Common.DTO.Config; namespace Azaion.Common.DTO.Config;
public class AppConfig public class AppConfig
{ {
public LoaderClientConfig LoaderClientConfig { get; set; } = null!;
public InferenceClientConfig InferenceClientConfig { get; set; } = null!; public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!; public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
@@ -45,14 +46,7 @@ public class ConfigUpdater : IConfigUpdater
var appConfig = new AppConfig var appConfig = new AppConfig
{ {
AnnotationConfig = new AnnotationConfig AnnotationConfig = Constants.DefaultAnnotationConfig,
{
DetectionClasses = Constants.DefaultAnnotationClasses,
VideoFormats = Constants.DefaultVideoFormats,
ImageFormats = Constants.DefaultImageFormats,
AnnotationsDbFile = Constants.DEFAULT_ANNOTATIONS_DB_FILE
},
UIConfig = new UIConfig UIConfig = new UIConfig
{ {
@@ -72,20 +66,8 @@ public class ConfigUpdater : IConfigUpdater
GpsRouteDirectory = Constants.DEFAULT_GPS_ROUTE_DIRECTORY GpsRouteDirectory = Constants.DEFAULT_GPS_ROUTE_DIRECTORY
}, },
ThumbnailConfig = new ThumbnailConfig ThumbnailConfig = Constants.DefaultThumbnailConfig,
{ AIRecognitionConfig = Constants.DefaultAIRecognitionConfig
Size = Constants.DefaultThumbnailSize,
Border = Constants.DEFAULT_THUMBNAIL_BORDER
},
AIRecognitionConfig = new AIRecognitionConfig
{
FrameRecognitionSeconds = Constants.DEFAULT_FRAME_RECOGNITION_SECONDS,
TrackingDistanceConfidence = Constants.TRACKING_DISTANCE_CONFIDENCE,
TrackingProbabilityIncrease = Constants.TRACKING_PROBABILITY_INCREASE,
TrackingIntersectionThreshold = Constants.TRACKING_INTERSECTION_THRESHOLD,
FramePeriodRecognition = Constants.DEFAULT_FRAME_PERIOD_RECOGNITION
}
}; };
Save(appConfig); Save(appConfig);
} }
@@ -95,6 +77,7 @@ public class ConfigUpdater : IConfigUpdater
//Save only user's config //Save only user's config
var publicConfig = new var publicConfig = new
{ {
config.LoaderClientConfig,
config.InferenceClientConfig, config.InferenceClientConfig,
config.GpsDeniedClientConfig, config.GpsDeniedClientConfig,
config.DirectoriesConfig, config.DirectoriesConfig,
+1
View File
@@ -5,4 +5,5 @@ public class UIConfig
public double LeftPanelWidth { get; set; } public double LeftPanelWidth { get; set; }
public double RightPanelWidth { get; set; } public double RightPanelWidth { get; set; }
public bool GenerateAnnotatedImage { get; set; } public bool GenerateAnnotatedImage { get; set; }
public bool SilentDetection { get; set; }
} }
@@ -1,7 +1,10 @@
namespace Azaion.Common.DTO.Config; namespace Azaion.Common.DTO;
public class DirectoriesConfig public class DirectoriesConfig
{ {
public string ApiResourcesDirectory { get; set; } = null!;
public string VideosDirectory { get; set; } = null!; public string VideosDirectory { get; set; } = null!;
public string LabelsDirectory { get; set; } = null!; public string LabelsDirectory { get; set; } = null!;
public string ImagesDirectory { get; set; } = null!; public string ImagesDirectory { get; set; } = null!;
@@ -1,19 +1,22 @@
namespace Azaion.CommonSecurity.DTO; namespace Azaion.Common.DTO;
public abstract class ExternalClientConfig public abstract class ExternalClientConfig
{ {
public string ZeroMqHost { get; set; } = ""; public string ZeroMqHost { get; set; } = "";
public int ZeroMqPort { get; set; } public int ZeroMqPort { get; set; }
public double OneTryTimeoutSeconds { get; set; } }
public int RetryCount {get;set;}
public class LoaderClientConfig : ExternalClientConfig
{
public string ApiUrl { get; set; } = null!;
} }
public class InferenceClientConfig : ExternalClientConfig public class InferenceClientConfig : ExternalClientConfig
{ {
public string ResourcesFolder { get; set; } = ""; public string ApiUrl { get; set; } = null!;
} }
public class GpsDeniedClientConfig : ExternalClientConfig public class GpsDeniedClientConfig : ExternalClientConfig
{ {
public int ZeroMqSubscriberPort { get; set; } public int ZeroMqReceiverPort { get; set; }
} }
-51
View File
@@ -1,51 +0,0 @@
namespace Azaion.Common.DTO;
using System.Collections.Generic;
using System.IO;
public class GpsMatchResult
{
public int Index { get; set; }
public string Image { get; set; } = null!;
public double Latitude { get; set; }
public double Longitude { get; set; }
public int KeyPoints { get; set; }
public int Rotation { get; set; }
public string MatchType { get; set; } = null!;
public static List<GpsMatchResult> ReadFromCsv(string csvFilePath)
{
var imageDatas = new List<GpsMatchResult>();
using var reader = new StreamReader(csvFilePath);
//read header
reader.ReadLine();
if (reader.EndOfStream)
return new List<GpsMatchResult>();
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
if (string.IsNullOrWhiteSpace(line))
continue;
var values = line.Split(',');
if (values.Length == 6)
{
imageDatas.Add(new GpsMatchResult
{
Image = GetFilename(values[0]),
Latitude = double.Parse(values[1]),
Longitude = double.Parse(values[2]),
KeyPoints = int.Parse(values[3]),
Rotation = int.Parse(values[4]),
MatchType = values[5]
});
}
}
return imageDatas;
}
private static string GetFilename(string imagePath) =>
Path.GetFileNameWithoutExtension(imagePath)
.Replace("-small", "");
}
@@ -1,4 +1,4 @@
namespace Azaion.Common.Extensions; namespace Azaion.Common.DTO;
public static class EnumerableExtensions public static class EnumerableExtensions
{ {
+9
View File
@@ -0,0 +1,9 @@
namespace Azaion.Common.DTO;
public class InitConfig
{
public LoaderClientConfig LoaderClientConfig { get; set; } = null!;
public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
}
+4 -1
View File
@@ -146,8 +146,11 @@ public class YoloLabel : Label
return null; return null;
var strings = s.Replace(',', '.').Split(' '); var strings = s.Replace(',', '.').Split(' ');
if (strings.Length != 5) if (strings.Length < 5)
throw new Exception("Wrong labels format!"); throw new Exception("Wrong labels format!");
if (strings.Length > 5)
strings = strings[..5];
var res = new YoloLabel var res = new YoloLabel
{ {
@@ -1,4 +1,4 @@
namespace Azaion.CommonSecurity.DTO; namespace Azaion.Common.DTO;
public class LoginResponse public class LoginResponse
{ {
@@ -1,11 +1,10 @@
using Azaion.Common.Database; using Azaion.Common.Database;
using Azaion.CommonSecurity.DTO;
namespace Azaion.Common.DTO.Queue; namespace Azaion.Common.DTO.Queue;
using MessagePack; using MessagePack;
[MessagePackObject] [MessagePackObject]
public class AnnotationCreatedMessage public class AnnotationMessage
{ {
[Key(0)] public DateTime CreatedDate { get; set; } [Key(0)] public DateTime CreatedDate { get; set; }
[Key(1)] public string Name { get; set; } = null!; [Key(1)] public string Name { get; set; } = null!;
@@ -13,15 +12,18 @@ public class AnnotationCreatedMessage
[Key(3)] public TimeSpan Time { get; set; } [Key(3)] public TimeSpan Time { get; set; }
[Key(4)] public string ImageExtension { get; set; } = null!; [Key(4)] public string ImageExtension { get; set; } = null!;
[Key(5)] public string Detections { get; set; } = null!; [Key(5)] public string Detections { get; set; } = null!;
[Key(6)] public byte[] Image { get; set; } = null!; [Key(6)] public byte[]? Image { get; set; } = null!;
[Key(7)] public RoleEnum CreatedRole { get; set; } [Key(7)] public RoleEnum Role { get; set; }
[Key(8)] public string CreatedEmail { get; set; } = null!; [Key(8)] public string Email { get; set; } = null!;
[Key(9)] public SourceEnum Source { get; set; } [Key(9)] public SourceEnum Source { get; set; }
[Key(10)] public AnnotationStatus Status { get; set; } [Key(10)] public AnnotationStatus Status { get; set; }
} }
[MessagePackObject] [MessagePackObject]
public class AnnotationValidatedMessage public class AnnotationBulkMessage
{ {
[Key(0)] public string Name { get; set; } = null!; [Key(0)] public string[] AnnotationNames { get; set; } = null!;
[Key(1)] public AnnotationStatus AnnotationStatus { get; set; }
[Key(2)] public string Email { get; set; } = null!;
[Key(3)] public DateTime CreatedDate { get; set; }
} }
@@ -1,9 +1,9 @@
using MessagePack; using MessagePack;
namespace Azaion.CommonSecurity.DTO.Commands; namespace Azaion.Common.DTO;
[MessagePackObject] [MessagePackObject]
public class RemoteCommand(CommandType commandType, byte[]? data = null) public class RemoteCommand(CommandType commandType, byte[]? data = null, string? message = null)
{ {
[Key("CommandType")] [Key("CommandType")]
public CommandType CommandType { get; set; } = commandType; public CommandType CommandType { get; set; } = commandType;
@@ -11,11 +11,16 @@ public class RemoteCommand(CommandType commandType, byte[]? data = null)
[Key("Data")] [Key("Data")]
public byte[]? Data { get; set; } = data; public byte[]? Data { get; set; } = data;
[Key("Message")]
public string? Message { get; set; } = message;
public static RemoteCommand Create(CommandType commandType) => public static RemoteCommand Create(CommandType commandType) =>
new(commandType); new(commandType);
public static RemoteCommand Create<T>(CommandType commandType, T data) where T : class => public static RemoteCommand Create<T>(CommandType commandType, T data, string? message = null) where T : class =>
new(commandType, MessagePackSerializer.Serialize(data)); new(commandType, MessagePackSerializer.Serialize(data), message);
public override string ToString() => $"({CommandType.ToString().ToUpper()}: Data: {Data?.Length ?? 0} bytes. Message: {Message})";
} }
[MessagePackObject] [MessagePackObject]
@@ -32,9 +37,19 @@ public class LoadFileData(string filename, string? folder = null )
public enum CommandType public enum CommandType
{ {
None = 0, None = 0,
Ok = 3,
Login = 10, Login = 10,
ListRequest = 15,
ListFiles = 18,
Load = 20, Load = 20,
LoadBigSmall = 22,
UploadBigSmall = 24,
DataBytes = 25,
Inference = 30, Inference = 30,
InferenceData = 35,
StopInference = 40, StopInference = 40,
Exit = 100 AIAvailabilityCheck = 80,
AIAvailabilityResult = 85,
Error = 90,
Exit = 100,
} }
@@ -1,6 +1,4 @@
using Azaion.Common.Extensions; namespace Azaion.Common.DTO;
namespace Azaion.CommonSecurity.DTO;
public enum RoleEnum public enum RoleEnum
{ {
+21
View File
@@ -0,0 +1,21 @@
namespace Azaion.Common.DTO;
public class User
{
public string Id { get; set; } = "";
public string Email { get; set; } = "";
public RoleEnum Role { get; set; }
public UserConfig? UserConfig { get; set; } = null!;
}
public class UserConfig
{
public UserQueueOffsets? QueueOffsets { get; set; } = new();
}
public class UserQueueOffsets
{
public ulong AnnotationsOffset { get; set; }
public ulong AnnotationsConfirmOffset { get; set; }
public ulong AnnotationsCommandsOffset { get; set; }
}
+6 -2
View File
@@ -2,7 +2,6 @@
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue; using Azaion.Common.DTO.Queue;
using Azaion.CommonSecurity.DTO;
using MessagePack; using MessagePack;
namespace Azaion.Common.Database; namespace Azaion.Common.Database;
@@ -31,6 +30,9 @@ public class Annotation
[IgnoreMember]public SourceEnum Source { get; set; } [IgnoreMember]public SourceEnum Source { get; set; }
[IgnoreMember]public AnnotationStatus AnnotationStatus { get; set; } [IgnoreMember]public AnnotationStatus AnnotationStatus { get; set; }
[IgnoreMember]public DateTime ValidateDate { get; set; }
[IgnoreMember]public string ValidateEmail { get; set; } = null!;
[Key("d")] public IEnumerable<Detection> Detections { get; set; } = null!; [Key("d")] public IEnumerable<Detection> Detections { get; set; } = null!;
[Key("t")] public long Milliseconds { get; set; } [Key("t")] public long Milliseconds { get; set; }
@@ -56,5 +58,7 @@ public enum AnnotationStatus
{ {
None = 0, None = 0,
Created = 10, Created = 10,
Validated = 20 Edited = 20,
Validated = 30,
Deleted = 40
} }
-6
View File
@@ -1,6 +0,0 @@
namespace Azaion.Common.Database;
public class AnnotationName
{
public string Name { get; set; } = null!;
}
@@ -0,0 +1,9 @@
namespace Azaion.Common.Database;
public class AnnotationQueueRecord
{
public Guid Id { get; set; }
public DateTime DateTime { get; set; }
public AnnotationStatus Operation { get; set; }
public List<string> AnnotationNames { get; set; } = null!;
}
+1 -2
View File
@@ -7,7 +7,6 @@ namespace Azaion.Common.Database;
public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions) public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions)
{ {
public ITable<Annotation> Annotations => this.GetTable<Annotation>(); public ITable<Annotation> Annotations => this.GetTable<Annotation>();
public ITable<AnnotationName> AnnotationsQueue => this.GetTable<AnnotationName>(); public ITable<AnnotationQueueRecord> AnnotationsQueueRecords => this.GetTable<AnnotationQueueRecord>();
public ITable<Detection> Detections => this.GetTable<Detection>(); public ITable<Detection> Detections => this.GetTable<Detection>();
public ITable<QueueOffset> QueueOffsets => this.GetTable<QueueOffset>();
} }
+65 -34
View File
@@ -1,28 +1,28 @@
using System.Data.SQLite; using System.Data.SQLite;
using System.Diagnostics;
using System.IO; using System.IO;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using LinqToDB; using LinqToDB;
using LinqToDB.Data;
using LinqToDB.DataProvider.SQLite; using LinqToDB.DataProvider.SQLite;
using LinqToDB.Mapping; using LinqToDB.Mapping;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json;
namespace Azaion.Common.Database; namespace Azaion.Common.Database;
public interface IDbFactory public interface IDbFactory
{ {
Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func); Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func);
Task Run(Func<AnnotationsDb, Task> func); Task RunWrite(Func<AnnotationsDb, Task> func);
void SaveToDisk(); Task<T> RunWrite<T>(Func<AnnotationsDb, Task<T>> func);
Task DeleteAnnotations(List<Annotation> annotations, CancellationToken cancellationToken = default);
Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default); Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default);
} }
public class DbFactory : IDbFactory public class DbFactory : IDbFactory
{ {
private readonly ILogger<DbFactory> _logger;
private readonly AnnotationConfig _annConfig; private readonly AnnotationConfig _annConfig;
private string MemoryConnStr => "Data Source=:memory:"; private string MemoryConnStr => "Data Source=:memory:";
@@ -33,8 +33,12 @@ public class DbFactory : IDbFactory
private readonly SQLiteConnection _fileConnection; private readonly SQLiteConnection _fileConnection;
private readonly DataOptions _fileDataOptions; 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) public DbFactory(IOptions<AnnotationConfig> annConfig, ILogger<DbFactory> logger)
{ {
_logger = logger;
_annConfig = annConfig.Value; _annConfig = annConfig.Value;
_memoryConnection = new SQLiteConnection(MemoryConnStr); _memoryConnection = new SQLiteConnection(MemoryConnStr);
@@ -53,32 +57,24 @@ public class DbFactory : IDbFactory
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema); .UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema);
if (!File.Exists(_annConfig.AnnotationsDbFile)) if (!File.Exists(_annConfig.AnnotationsDbFile))
CreateDb(); SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile);
RecreateTables();
_fileConnection.Open(); _fileConnection.Open();
_fileConnection.BackupDatabase(_memoryConnection, "main", "main", -1, null, -1); _fileConnection.BackupDatabase(_memoryConnection, "main", "main", -1, null, -1);
} }
private void CreateDb() private void RecreateTables()
{ {
SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile);
using var db = new AnnotationsDb(_fileDataOptions); using var db = new AnnotationsDb(_fileDataOptions);
var schema = db.DataProvider.GetSchemaProvider().GetSchema(db);
var existingTables = schema.Tables.Select(x => x.TableName).ToHashSet();
if (!existingTables.Contains(Constants.ANNOTATIONS_TABLENAME))
db.CreateTable<Annotation>(); db.CreateTable<Annotation>();
db.CreateTable<AnnotationName>(); if (!existingTables.Contains(Constants.DETECTIONS_TABLENAME))
db.CreateTable<Detection>(); db.CreateTable<Detection>();
db.CreateTable<QueueOffset>(); if (!existingTables.Contains(Constants.ANNOTATIONS_QUEUE_TABLENAME))
db.QueueOffsets.BulkCopy(new List<QueueOffset> db.CreateTable<AnnotationQueueRecord>();
{
new()
{
Offset = 0,
QueueName = Constants.MQ_ANNOTATIONS_QUEUE
},
new()
{
Offset = 0,
QueueName = Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE
}
});
} }
public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func) public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func)
@@ -87,32 +83,63 @@ public class DbFactory : IDbFactory
return await func(db); return await func(db);
} }
public async Task Run(Func<AnnotationsDb, Task> func) public async Task RunWrite(Func<AnnotationsDb, Task> func)
{
await WriteSemaphore.WaitAsync();
try
{ {
await using var db = new AnnotationsDb(_memoryDataOptions); await using var db = new AnnotationsDb(_memoryDataOptions);
await func(db); await func(db);
} ThrottleExt.Throttle(async () =>
public void SaveToDisk()
{ {
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1); _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 async Task DeleteAnnotations(List<Annotation> annotations, CancellationToken cancellationToken = default) public async Task<T> RunWrite<T>(Func<AnnotationsDb, Task<T>> func)
{ {
var names = annotations.Select(x => x.Name).ToList(); await WriteSemaphore.WaitAsync();
await DeleteAnnotations(names, cancellationToken); 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) 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 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); var annDeleted = await db.Annotations.DeleteAsync(x => annotationNames.Contains(x.Name), token: cancellationToken);
Console.WriteLine($"Deleted {detDeleted} detections, {annDeleted} annotations"); Console.WriteLine($"Deleted {detDeleted} detections, {annDeleted} annotations");
}); });
SaveToDisk();
} }
} }
@@ -142,8 +169,12 @@ public static class AnnotationsDbSchemaHolder
builder.Entity<Detection>() builder.Entity<Detection>()
.HasTableName(Constants.DETECTIONS_TABLENAME); .HasTableName(Constants.DETECTIONS_TABLENAME);
builder.Entity<AnnotationName>() builder.Entity<AnnotationQueueRecord>()
.HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME); .HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME)
.HasPrimaryKey(x => x.Id)
.Property(x => x.AnnotationNames)
.HasDataType(DataType.NVarChar)
.HasConversion(list => JsonConvert.SerializeObject(list), str => JsonConvert.DeserializeObject<List<string>>(str) ?? new List<string>());
builder.Build(); builder.Build();
} }
-7
View File
@@ -1,7 +0,0 @@
namespace Azaion.Common.Database;
public class QueueOffset
{
public string QueueName { get; set; } = null!;
public ulong Offset { get; set; }
}
@@ -3,7 +3,13 @@ using MediatR;
namespace Azaion.Common.Events; namespace Azaion.Common.Events;
public class AnnotationsDeletedEvent(List<Annotation> annotations) : INotification public class AnnotationsDeletedEvent(List<string> annotationNames, bool fromQueue = false) : INotification
{ {
public List<Annotation> Annotations { get; set; } = annotations; public List<string> AnnotationNames { get; set; } = annotationNames;
public bool FromQueue { get; set; } = fromQueue;
}
public class AnnotationAddedEvent(Annotation annotation) : INotification
{
public Annotation Annotation { get; set; } = annotation;
} }
+8
View File
@@ -0,0 +1,8 @@
using MediatR;
namespace Azaion.Common.Events;
public class LoadErrorEvent(string error) : INotification
{
public string Error { get; set; } = error;
}
@@ -0,0 +1,9 @@
using MediatR;
namespace Azaion.Common.Events;
public class SetStatusTextEvent(string text, bool isError = false) : INotification
{
public string Text { get; set; } = text;
public bool IsError { get; set; } = isError;
}
@@ -0,0 +1,3 @@
namespace Azaion.CommonSecurity.Exceptions;
public class BusinessException(string message) : Exception(message);
@@ -1,4 +1,5 @@
using System.IO; using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
namespace Azaion.Common.Extensions; namespace Azaion.Common.Extensions;
@@ -22,4 +23,7 @@ public static class BitmapExtensions
image.Freeze(); image.Freeze();
return image; return image;
} }
public static Color CreateTransparent(this Color color, byte transparency) =>
Color.FromArgb(transparency, color.R, color.G, color.B);
} }
@@ -0,0 +1,30 @@
namespace Azaion.Common.Extensions;
public static class CancellationTokenExtensions
{
public static void WaitForCancel(this CancellationToken token, TimeSpan timeout)
{
try
{
Task.Delay(timeout, token).Wait(token);
}
catch (OperationCanceledException)
{
//Don't need to catch exception, need only return from the waiting
}
}
public static Task AsTask(this CancellationToken cancellationToken)
{
if (!cancellationToken.CanBeCanceled)
return new TaskCompletionSource<bool>().Task;
if (cancellationToken.IsCancellationRequested)
return Task.FromCanceled(cancellationToken);
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
var registration = cancellationToken.Register(() => tcs.TrySetResult(true));
tcs.Task.ContinueWith(_ => registration.Dispose(), TaskScheduler.Default);
return tcs.Task;
}
}
@@ -12,4 +12,7 @@ public static class ColorExtensions
color.A = (byte)(MIN_ALPHA + (int)Math.Round(confidence * (MAX_ALPHA - MIN_ALPHA))); color.A = (byte)(MIN_ALPHA + (int)Math.Round(confidence * (MAX_ALPHA - MIN_ALPHA)));
return color; return color;
} }
public static Color ToColor(this string hexColor) =>
(Color)ColorConverter.ConvertFromString(hexColor);
} }
+15
View File
@@ -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);
}
+48 -34
View File
@@ -4,54 +4,68 @@ namespace Azaion.Common.Extensions;
public static class ThrottleExt public static class ThrottleExt
{ {
private static ConcurrentDictionary<Guid, bool> _taskStates = new(); private class ThrottleState(Func<Task> action)
public static async Task ThrottleRunFirst(Func<Task> func, Guid actionId, TimeSpan? throttleTime = null, CancellationToken cancellationToken = default)
{ {
if (_taskStates.ContainsKey(actionId) && _taskStates[actionId]) public Func<Task> Action { get; set; } = action ?? throw new ArgumentNullException(nameof(action));
return; public bool IsCoolingDown = false;
public bool CallScheduledDuringCooldown = false;
_taskStates[actionId] = true; public Task CooldownTask = Task.CompletedTask;
try public readonly object StateLock = new();
{
await func();
}
catch (Exception e)
{
Console.WriteLine(e);
} }
_ = Task.Run(async () => private static readonly ConcurrentDictionary<Guid, ThrottleState> ThrottlerStates = new();
public static void Throttle(Func<Task> action, Guid actionId, TimeSpan interval, bool scheduleCallAfterCooldown = false)
{ {
await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500), cancellationToken); ArgumentNullException.ThrowIfNull(action);
_taskStates[actionId] = false; if (actionId == Guid.Empty)
}, cancellationToken); throw new ArgumentException("Throttle identifier cannot be empty.", nameof(actionId));
if (interval <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(interval), "Interval must be positive.");
var state = ThrottlerStates.GetOrAdd(actionId, new ThrottleState(action));
state.Action = action;
lock (state.StateLock)
{
if (!state.IsCoolingDown)
{
state.IsCoolingDown = true;
state.CooldownTask = ExecuteAndManageCooldownStaticAsync(actionId, interval, state);
}
else
{
if (scheduleCallAfterCooldown)
state.CallScheduledDuringCooldown = true;
}
}
} }
public static async Task ThrottleRunAfter(Func<Task> func, Guid actionId, TimeSpan? throttleTime = null, CancellationToken cancellationToken = default) private static async Task ExecuteAndManageCooldownStaticAsync(Guid throttleId, TimeSpan interval, ThrottleState state)
{
if (_taskStates.ContainsKey(actionId) && _taskStates[actionId])
return;
_taskStates[actionId] = true;
_ = Task.Run(async () =>
{ {
try try
{ {
await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500), cancellationToken); await state.Action();
await func();
} }
catch (Exception) catch (Exception ex)
{ {
_taskStates[actionId] = false; Console.WriteLine($"[Throttled Action Error - ID: {throttleId}] {ex.GetType().Name}: {ex.Message}");
} }
finally finally
{ {
_taskStates[actionId] = false; await Task.Delay(interval);
lock (state.StateLock)
{
if (state.CallScheduledDuringCooldown)
{
state.CallScheduledDuringCooldown = false;
state.CooldownTask = ExecuteAndManageCooldownStaticAsync(throttleId, interval, state);
}
else
{
state.IsCoolingDown = false;
}
}
} }
}, cancellationToken);
await Task.CompletedTask;
} }
} }
+79
View File
@@ -0,0 +1,79 @@
using System.IO;
using Azaion.Common.DTO;
using Newtonsoft.Json;
namespace Azaion.Common;
public class SecurityConstants
{
public const string CONFIG_PATH = "config.json";
private const string DEFAULT_API_URL = "https://api.azaion.com";
#region ExternalClientsConfig
private const string DEFAULT_ZMQ_LOADER_HOST = "127.0.0.1";
private const int DEFAULT_ZMQ_LOADER_PORT = 5025;
public const string EXTERNAL_LOADER_PATH = "azaion-loader.exe";
public const string EXTERNAL_INFERENCE_PATH = "azaion-inference.exe";
public const string EXTERNAL_GPS_DENIED_FOLDER = "gps-denied";
public static readonly string ExternalGpsDeniedPath = Path.Combine(EXTERNAL_GPS_DENIED_FOLDER, "image-matcher.exe");
public const string DEFAULT_ZMQ_INFERENCE_HOST = "127.0.0.1";
public const int DEFAULT_ZMQ_INFERENCE_PORT = 5227;
public const string DEFAULT_ZMQ_GPS_DENIED_HOST = "127.0.0.1";
public const int DEFAULT_ZMQ_GPS_DENIED_PORT = 5227;
# region Cache keys
public const string CURRENT_USER_CACHE_KEY = "CurrentUser";
public const string HARDWARE_INFO_KEY = "HardwareInfo";
# endregion
public static readonly InitConfig DefaultInitConfig = new()
{
LoaderClientConfig = new LoaderClientConfig
{
ZeroMqHost = DEFAULT_ZMQ_LOADER_HOST,
ZeroMqPort = DEFAULT_ZMQ_LOADER_PORT,
ApiUrl = DEFAULT_API_URL
},
InferenceClientConfig = new InferenceClientConfig
{
ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST,
ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT,
ApiUrl = DEFAULT_API_URL
},
GpsDeniedClientConfig = new GpsDeniedClientConfig
{
ZeroMqHost = DEFAULT_ZMQ_GPS_DENIED_HOST,
ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT
},
DirectoriesConfig = new DirectoriesConfig
{
ApiResourcesDirectory = ""
}
};
#endregion ExternalClientsConfig
public static InitConfig ReadInitConfig()
{
try
{
if (!File.Exists(CONFIG_PATH))
throw new FileNotFoundException(CONFIG_PATH);
var configStr = File.ReadAllText(CONFIG_PATH);
var config = JsonConvert.DeserializeObject<InitConfig>(configStr);
return config ?? DefaultInitConfig;
}
catch (Exception e)
{
Console.WriteLine(e);
return DefaultInitConfig;
}
}
}
+136 -86
View File
@@ -1,4 +1,5 @@
using System.Drawing.Imaging; using System.Drawing;
using System.Drawing.Imaging;
using System.IO; using System.IO;
using System.Net; using System.Net;
using Azaion.Common.Database; using Azaion.Common.Database;
@@ -7,12 +8,11 @@ using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue; using Azaion.Common.DTO.Queue;
using Azaion.Common.Events; using Azaion.Common.Events;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.Services;
using LinqToDB; using LinqToDB;
using LinqToDB.Data; using LinqToDB.Data;
using MediatR; using MediatR;
using MessagePack; using MessagePack;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using RabbitMQ.Stream.Client; using RabbitMQ.Stream.Client;
@@ -20,45 +20,48 @@ using RabbitMQ.Stream.Client.Reliable;
namespace Azaion.Common.Services; namespace Azaion.Common.Services;
public class AnnotationService : 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 IDbFactory _dbFactory;
private readonly FailsafeAnnotationsProducer _producer; private readonly FailsafeAnnotationsProducer _producer;
private readonly IGalleryService _galleryService; private readonly IGalleryService _galleryService;
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly IHardwareService _hardwareService; private readonly IAzaionApi _api;
private readonly IAuthProvider _authProvider; private readonly ILogger<AnnotationService> _logger;
private readonly QueueConfig _queueConfig; private readonly QueueConfig _queueConfig;
private Consumer _consumer = null!; private Consumer _consumer = null!;
private readonly UIConfig _uiConfig; private readonly UIConfig _uiConfig;
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( public AnnotationService(
IResourceLoader resourceLoader,
IDbFactory dbFactory, IDbFactory dbFactory,
FailsafeAnnotationsProducer producer, FailsafeAnnotationsProducer producer,
IOptions<QueueConfig> queueConfig, IOptions<QueueConfig> queueConfig,
IOptions<UIConfig> uiConfig, IOptions<UIConfig> uiConfig,
IGalleryService galleryService, IGalleryService galleryService,
IMediator mediator, IMediator mediator,
IHardwareService hardwareService, IAzaionApi api,
IAuthProvider authProvider) ILogger<AnnotationService> logger)
{ {
_dbFactory = dbFactory; _dbFactory = dbFactory;
_producer = producer; _producer = producer;
_galleryService = galleryService; _galleryService = galleryService;
_mediator = mediator; _mediator = mediator;
_hardwareService = hardwareService; _api = api;
_authProvider = authProvider; _logger = logger;
_queueConfig = queueConfig.Value; _queueConfig = queueConfig.Value;
_uiConfig = uiConfig.Value; _uiConfig = uiConfig.Value;
Task.Run(async () => await Init()).Wait(); Task.Run(async () => await InitQueueConsumer()).Wait();
} }
private async Task Init(CancellationToken cancellationToken = default) private async Task InitQueueConsumer(CancellationToken cancellationToken = default)
{ {
if (!_authProvider.CurrentUser.Role.IsValidator()) if (!_api.CurrentUser.Role.IsValidator())
return; return;
var consumerSystem = await StreamSystem.Create(new StreamSystemConfig var consumerSystem = await StreamSystem.Create(new StreamSystemConfig
@@ -68,44 +71,64 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
Password = _queueConfig.ConsumerPassword Password = _queueConfig.ConsumerPassword
}); });
var offset = (await _dbFactory.Run(db => db.QueueOffsets.FirstOrDefaultAsync( var offsets = _api.CurrentUser.UserConfig?.QueueOffsets ?? new UserQueueOffsets();
x => x.QueueName == Constants.MQ_ANNOTATIONS_QUEUE, token: cancellationToken))
)?.Offset ?? 0;
_consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE) _consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE)
{ {
Reference = _hardwareService.GetHardware().Hash, Reference = _api.CurrentUser.Email,
OffsetSpec = new OffsetTypeOffset(offset + 1), OffsetSpec = new OffsetTypeOffset(offsets.AnnotationsOffset),
MessageHandler = async (_, _, context, message) => MessageHandler = async (_, _, context, message) =>
{ {
var msg = MessagePackSerializer.Deserialize<AnnotationCreatedMessage>(message.Data.Contents); await _messageProcessingSemaphore.WaitAsync(cancellationToken);
await _dbFactory.Run(async db => await db.QueueOffsets try
.Where(x => x.QueueName == Constants.MQ_ANNOTATIONS_QUEUE)
.Set(x => x.Offset, context.Offset)
.UpdateAsync(token: cancellationToken));
await ThrottleExt.ThrottleRunAfter(() =>
{ {
_dbFactory.SaveToDisk(); var email = (string)message.ApplicationProperties[nameof(User.Email)]!;
return Task.CompletedTask; if (!Enum.TryParse<AnnotationStatus>((string)message.ApplicationProperties[nameof(AnnotationStatus)], out var annotationStatus))
}, SaveTaskId, TimeSpan.FromSeconds(5), cancellationToken);
if (msg.CreatedEmail == _authProvider.CurrentUser.Email) //Don't process messages by yourself
return; return;
if (email != _api.CurrentUser.Email) //Don't process messages by yourself
{
if (annotationStatus.In(AnnotationStatus.Created, AnnotationStatus.Edited))
{
var msg = MessagePackSerializer.Deserialize<AnnotationMessage>(message.Data.Contents);
await SaveAnnotationInner( await SaveAnnotationInner(
msg.CreatedDate, msg.CreatedDate,
msg.OriginalMediaName, msg.OriginalMediaName,
msg.Time, msg.Time,
JsonConvert.DeserializeObject<List<Detection>>(msg.Detections) ?? [], JsonConvert.DeserializeObject<List<Detection>>(msg.Detections) ?? [],
msg.Source, msg.Source,
new MemoryStream(msg.Image), msg.Image == null ? null : new MemoryStream(msg.Image),
msg.CreatedRole, msg.Role,
msg.CreatedEmail, msg.Email,
generateThumbnail: true, context.Offset,
fromQueue: true,
token: cancellationToken); token: cancellationToken);
} }
else
{
var msg = MessagePackSerializer.Deserialize<AnnotationBulkMessage>(message.Data.Contents);
if (annotationStatus == AnnotationStatus.Validated)
await ValidateAnnotations(msg.AnnotationNames.ToList(), true, cancellationToken);
if (annotationStatus == AnnotationStatus.Deleted)
await _mediator.Publish(new AnnotationsDeletedEvent(msg.AnnotationNames.ToList(), fromQueue:true), cancellationToken);
}
}
offsets.AnnotationsOffset = context.Offset + 1; //to consume on the next launch from the next message
ThrottleExt.Throttle(() =>
{
_api.UpdateOffsets(offsets);
return Task.CompletedTask;
}, SaveQueueOffsetTaskId, TimeSpan.FromSeconds(10), scheduleCallAfterCooldown: true);
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
finally
{
_messageProcessingSemaphore.Release();
}
}
}); });
} }
@@ -113,51 +136,51 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
public async Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default) public async Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default)
{ {
a.Time = TimeSpan.FromMilliseconds(a.Milliseconds); a.Time = TimeSpan.FromMilliseconds(a.Milliseconds);
return await SaveAnnotationInner(DateTime.Now, a.OriginalMediaName, a.Time, a.Detections.ToList(), return await SaveAnnotationInner(DateTime.UtcNow, a.OriginalMediaName, a.Time, a.Detections.ToList(),
SourceEnum.AI, new MemoryStream(a.Image), _authProvider.CurrentUser.Role, _authProvider.CurrentUser.Email, generateThumbnail: true, token: ct); SourceEnum.AI, new MemoryStream(a.Image), _api.CurrentUser.Role, _api.CurrentUser.Email, token: ct);
} }
//Manual //Manual
public async Task<Annotation> SaveAnnotation(string originalMediaName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default) => public async Task<Annotation> SaveAnnotation(string originalMediaName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default) =>
await SaveAnnotationInner(DateTime.UtcNow, originalMediaName, time, detections, SourceEnum.Manual, stream, await SaveAnnotationInner(DateTime.UtcNow, originalMediaName, time, detections, SourceEnum.Manual, stream,
_authProvider.CurrentUser.Role, _authProvider.CurrentUser.Email, generateThumbnail: true, token: token); _api.CurrentUser.Role, _api.CurrentUser.Email, token: token);
//Manual Validate existing private async Task<Annotation> SaveAnnotationInner(DateTime createdDate, string originalMediaName, TimeSpan time,
public async Task ValidateAnnotation(Annotation annotation, CancellationToken token = default) => List<Detection> detections, SourceEnum source, Stream? stream,
await SaveAnnotationInner(DateTime.UtcNow, annotation.OriginalMediaName, annotation.Time, annotation.Detections.ToList(), SourceEnum.Manual, null,
_authProvider.CurrentUser.Role, _authProvider.CurrentUser.Email, token: token);
// Manual save from Validators -> Validated -> stream: azaion-annotations-confirm
// AI, Manual save from Operators -> Created -> stream: azaion-annotations
private async Task<Annotation> SaveAnnotationInner(DateTime createdDate, string originalMediaName, TimeSpan time, List<Detection> detections, SourceEnum source, Stream? stream,
RoleEnum userRole, RoleEnum userRole,
string createdEmail, string createdEmail,
bool generateThumbnail = false, ulong? offset = null,
bool fromQueue = false,
CancellationToken token = default) CancellationToken token = default)
{ {
var status = AnnotationStatus.Created;
AnnotationStatus status;
var fName = originalMediaName.ToTimeName(time); var fName = originalMediaName.ToTimeName(time);
var annotation = await _dbFactory.Run(async db => var annotation = await _dbFactory.RunWrite(async db =>
{ {
var ann = await db.Annotations.FirstOrDefaultAsync(x => x.Name == fName, token: token); var ann = await db.Annotations
status = userRole.IsValidator() && source == SourceEnum.Manual .LoadWith(x => x.Detections)
? AnnotationStatus.Validated .FirstOrDefaultAsync(x => x.Name == fName, token: token);
: AnnotationStatus.Created;
await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token); await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token);
await db.BulkCopyAsync(detections, cancellationToken: token);
if (ann != null) if (ann != null) //Annotation is already exists
{ {
await db.Annotations status = AnnotationStatus.Edited;
var annotationUpdatable = db.Annotations
.Where(x => x.Name == fName) .Where(x => x.Name == fName)
.Set(x => x.Source, source) .Set(x => x.Source, source);
if (userRole.IsValidator() && source == SourceEnum.Manual)
{
annotationUpdatable = annotationUpdatable
.Set(x => x.ValidateDate, createdDate)
.Set(x => x.ValidateEmail, createdEmail);
}
await annotationUpdatable
.Set(x => x.AnnotationStatus, status) .Set(x => x.AnnotationStatus, status)
.Set(x => x.CreatedDate, createdDate)
.Set(x => x.CreatedEmail, createdEmail)
.Set(x => x.CreatedRole, userRole)
.UpdateAsync(token: token); .UpdateAsync(token: token);
ann.Detections = detections; ann.Detections = detections;
} }
else else
@@ -177,43 +200,70 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
}; };
await db.InsertAsync(ann, token: token); await db.InsertAsync(ann, token: token);
} }
await db.BulkCopyAsync(detections, cancellationToken: token);
return ann; return ann;
}); });
//Save image should be done in 1 thread only
await _imageAccessSemaphore.WaitAsync(token);
try
{
Image image = null!;
if (stream != null) if (stream != null)
{ {
var img = System.Drawing.Image.FromStream(stream); image = Image.FromStream(stream);
img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue if (File.Exists(annotation.ImagePath))
ResilienceExt.WithRetry(() => File.Delete(annotation.ImagePath));
image.Save(annotation.ImagePath, ImageFormat.Jpeg);
} }
await YoloLabel.WriteToFile(detections, annotation.LabelPath, token); await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
if (generateThumbnail)
{ await _galleryService.CreateThumbnail(annotation, image, token);
await _galleryService.CreateThumbnail(annotation, token);
if (_uiConfig.GenerateAnnotatedImage) if (_uiConfig.GenerateAnnotatedImage)
await _galleryService.CreateAnnotatedImage(annotation, token); await _galleryService.CreateAnnotatedImage(annotation, image, token);
}
catch (Exception e)
{
_logger.LogError(e, $"Try to save {annotation.ImagePath}, Error: {e.Message}");
throw;
}
finally
{
_imageAccessSemaphore.Release();
} }
if (!fromQueue) //Send to queue only if we're not getting from queue already
await _producer.SendToInnerQueue(annotation, token);
await _mediator.Publish(new AnnotationCreatedEvent(annotation), token); await _mediator.Publish(new AnnotationCreatedEvent(annotation), token);
await ThrottleExt.ThrottleRunAfter(() =>
{ if (!offset.HasValue) //Send to queue only if we're not getting from queue already
_dbFactory.SaveToDisk(); await _producer.SendToInnerQueue([annotation.Name], status, token);
return Task.CompletedTask;
}, SaveTaskId, TimeSpan.FromSeconds(5), token);
return annotation; return annotation;
} }
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) public async Task ValidateAnnotations(List<string> annotationNames, bool fromQueue = false, CancellationToken token = default)
{ {
await _dbFactory.DeleteAnnotations(notification.Annotations, cancellationToken); if (!_api.CurrentUser.Role.IsValidator())
foreach (var annotation in notification.Annotations) return;
var annNames = annotationNames.ToHashSet();
await _dbFactory.RunWrite(async db =>
{ {
File.Delete(annotation.ImagePath); await db.Annotations
File.Delete(annotation.LabelPath); .Where(x => annNames.Contains(x.Name))
File.Delete(annotation.ThumbPath); .Set(x => x.AnnotationStatus, AnnotationStatus.Validated)
} .Set(x => x.ValidateDate, DateTime.UtcNow)
.Set(x => x.ValidateEmail, _api.CurrentUser.Email)
.UpdateAsync(token: token);
});
if (!fromQueue)
await _producer.SendToInnerQueue(annotationNames, AnnotationStatus.Validated, token);
} }
} }
public interface IAnnotationService
{
Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default);
Task<Annotation> SaveAnnotation(string originalMediaName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default);
Task ValidateAnnotations(List<string> annotationNames, bool fromQueue = false, CancellationToken token = default);
}
+125
View File
@@ -0,0 +1,125 @@
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using Azaion.Common.DTO;
using Newtonsoft.Json;
namespace Azaion.Common.Services;
public interface IAzaionApi
{
ApiCredentials Credentials { get; }
User CurrentUser { get; }
void UpdateOffsets(UserQueueOffsets offsets);
//Stream GetResource(string filename, string folder);
}
public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentials) : IAzaionApi
{
private string _jwtToken = null!;
const string APP_JSON = "application/json";
public ApiCredentials Credentials => credentials;
public User CurrentUser
{
get
{
var user = cache.GetFromCache(SecurityConstants.CURRENT_USER_CACHE_KEY,
() => Get<User>("currentUser"));
if (user == null)
throw new Exception("Can't get current user");
return user;
}
}
public void UpdateOffsets(UserQueueOffsets offsets)
{
Put($"/users/queue-offsets/set", new
{
Email = CurrentUser.Email,
Offsets = offsets
});
}
private HttpResponseMessage Send(HttpRequestMessage request)
{
if (string.IsNullOrEmpty(_jwtToken))
Authorize();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken);
var response = client.Send(request);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
Authorize();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken);
response = client.Send(request);
}
if (response.IsSuccessStatusCode)
return response;
var stream = response.Content.ReadAsStream();
var content = new StreamReader(stream).ReadToEnd();
if (response.StatusCode == HttpStatusCode.Conflict)
{
var result = JsonConvert.DeserializeObject<BusinessExceptionDto>(content);
throw new Exception($"Failed: {response.StatusCode}! Error Code: {result?.ErrorCode}. Message: {result?.Message}");
}
throw new Exception($"Failed: {response.StatusCode}! Result: {content}");
}
private T? Get<T>(string url)
{
var response = Send(new HttpRequestMessage(HttpMethod.Get, url));
var stream = response.Content.ReadAsStream();
var json = new StreamReader(stream).ReadToEnd();
return JsonConvert.DeserializeObject<T>(json);
}
private void Put<T>(string url, T obj)
{
Send(new HttpRequestMessage(HttpMethod.Put, url)
{
Content = new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, APP_JSON)
});
}
private void Authorize()
{
try
{
if (string.IsNullOrEmpty(credentials.Email) || credentials.Password.Length == 0)
throw new Exception("Email or password is empty! Please do EnterCredentials first!");
var payload = new
{
email = credentials.Email,
password = credentials.Password
};
var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, APP_JSON);
var message = new HttpRequestMessage(HttpMethod.Post, "login") { Content = content };
var response = client.Send(message);
if (!response.IsSuccessStatusCode)
throw new Exception($"EnterCredentials failed: {response.StatusCode}");
var stream = response.Content.ReadAsStream();
var json = new StreamReader(stream).ReadToEnd();
var result = JsonConvert.DeserializeObject<LoginResponse>(json);
if (string.IsNullOrEmpty(result?.Token))
throw new Exception("JWT Token not found in response");
_jwtToken = result.Token;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}
+27
View File
@@ -0,0 +1,27 @@
using LazyCache;
namespace Azaion.Common.Services;
public interface ICache
{
T GetFromCache<T>(string key, Func<T> fetchFunc, TimeSpan? expiration = null);
void Invalidate(string key);
}
public class MemoryCache : ICache
{
private readonly IAppCache _cache = new CachingService();
public T GetFromCache<T>(string key, Func<T> fetchFunc, TimeSpan? expiration = null)
{
expiration ??= TimeSpan.FromHours(4);
return _cache.GetOrAdd(key, entry =>
{
var result = fetchFunc();
entry.AbsoluteExpirationRelativeToNow = expiration;
return result;
});
}
public void Invalidate(string key) => _cache.Remove(key);
}
+82 -67
View File
@@ -1,14 +1,17 @@
using System.IO; using System.IO;
using System.Net; using System.Net;
using Azaion.Common.Database; using Azaion.Common.Database;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue; using Azaion.Common.DTO.Queue;
using Azaion.Common.Extensions;
using LinqToDB; using LinqToDB;
using MessagePack; using MessagePack;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using RabbitMQ.Stream.Client; using RabbitMQ.Stream.Client;
using RabbitMQ.Stream.Client.AMQP;
using RabbitMQ.Stream.Client.Reliable; using RabbitMQ.Stream.Client.Reliable;
namespace Azaion.Common.Services; namespace Azaion.Common.Services;
@@ -17,17 +20,24 @@ public class FailsafeAnnotationsProducer
{ {
private readonly ILogger<FailsafeAnnotationsProducer> _logger; private readonly ILogger<FailsafeAnnotationsProducer> _logger;
private readonly IDbFactory _dbFactory; private readonly IDbFactory _dbFactory;
private readonly IAzaionApi _azaionApi;
private readonly QueueConfig _queueConfig; private readonly QueueConfig _queueConfig;
private readonly UIConfig _uiConfig;
private Producer _annotationProducer = null!; private Producer _annotationProducer = null!;
private Producer _annotationConfirmProducer = null!;
public FailsafeAnnotationsProducer(ILogger<FailsafeAnnotationsProducer> logger, IDbFactory dbFactory, IOptions<QueueConfig> queueConfig) public FailsafeAnnotationsProducer(ILogger<FailsafeAnnotationsProducer> logger,
IDbFactory dbFactory,
IOptions<QueueConfig> queueConfig,
IOptions<UIConfig> uiConfig,
IAzaionApi azaionApi)
{ {
_logger = logger; _logger = logger;
_dbFactory = dbFactory; _dbFactory = dbFactory;
_azaionApi = azaionApi;
_queueConfig = queueConfig.Value; _queueConfig = queueConfig.Value;
_uiConfig = uiConfig.Value;
Task.Run(async () => await ProcessQueue()); Task.Run(async () => await ProcessQueue());
} }
@@ -41,78 +51,68 @@ public class FailsafeAnnotationsProducer
}); });
} }
private async Task Init(CancellationToken cancellationToken = default) private async Task ProcessQueue(CancellationToken ct = default)
{ {
_annotationProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_QUEUE)); _annotationProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_QUEUE));
_annotationConfirmProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE)); while (!ct.IsCancellationRequested)
}
private async Task ProcessQueue(CancellationToken cancellationToken = default)
{
await Init(cancellationToken);
while (!cancellationToken.IsCancellationRequested)
{
var messages = await GetFromInnerQueue(cancellationToken);
foreach (var messagesChunk in messages.Chunk(10)) //Sending by 10
{ {
var sent = false; var sent = false;
while (!sent || cancellationToken.IsCancellationRequested) //Waiting for send while (!sent || !ct.IsCancellationRequested) //Waiting for send
{ {
try try
{ {
var createdMessages = messagesChunk var (records, annotationsDict) = await _dbFactory.Run(async db =>
.Where(x => x.Status == AnnotationStatus.Created) {
.Select(x => new Message(MessagePackSerializer.Serialize(x))) var records = await db.AnnotationsQueueRecords.OrderBy(x => x.DateTime).ToListAsync(token: ct);
var editedCreatedNames = records
.Where(x => x.Operation.In(AnnotationStatus.Created, AnnotationStatus.Edited))
.Select(x => x.AnnotationNames.FirstOrDefault())
.ToList(); .ToList();
if (createdMessages.Any())
await _annotationProducer.Send(createdMessages, CompressionType.Gzip);
var validatedMessages = messagesChunk var annotationsDict = await db.Annotations.LoadWith(x => x.Detections)
.Where(x => x.Status == AnnotationStatus.Validated) .Where(x => editedCreatedNames.Contains(x.Name))
.Select(x => new Message(MessagePackSerializer.Serialize(x))) .ToDictionaryAsync(a => a.Name, token: ct);
.ToList(); return (records, annotationsDict);
if (validatedMessages.Any()) });
await _annotationConfirmProducer.Send(validatedMessages, CompressionType.Gzip);
await _dbFactory.Run(async db => var messages = new List<Message>();
await db.AnnotationsQueue.DeleteAsync(aq => messagesChunk.Any(x => aq.Name == x.Name), token: cancellationToken)); foreach (var record in records)
sent = true;
_dbFactory.SaveToDisk();
}
catch (Exception e)
{ {
_logger.LogError(e, e.Message); var appProperties = new ApplicationProperties
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); {
} { nameof(AnnotationStatus), record.Operation.ToString() },
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); { nameof(User.Email), _azaionApi.CurrentUser.Email }
} };
}
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
}
}
private async Task<List<AnnotationCreatedMessage>> GetFromInnerQueue(CancellationToken cancellationToken = default) if (record.Operation.In(AnnotationStatus.Validated, AnnotationStatus.Deleted))
{ {
return await _dbFactory.Run(async db => var message = new Message(MessagePackSerializer.Serialize(new AnnotationBulkMessage
{ {
var annotations = await db.AnnotationsQueue.Join( AnnotationNames = record.AnnotationNames.ToArray(),
db.Annotations.LoadWith(x => x.Detections), aq => aq.Name, a => a.Name, (aq, a) => a) AnnotationStatus = record.Operation,
.ToListAsync(token: cancellationToken); Email = _azaionApi.CurrentUser.Email,
CreatedDate = record.DateTime
})) { ApplicationProperties = appProperties };
var messages = new List<AnnotationCreatedMessage>(); messages.Add(message);
var badImages = new List<string>(); }
foreach (var annotation in annotations) else
{ {
try var annotation = annotationsDict!.GetValueOrDefault(record.AnnotationNames.FirstOrDefault());
{ if (annotation == null)
var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken); continue;
var annCreateMessage = new AnnotationCreatedMessage
var image = record.Operation == AnnotationStatus.Created
? await File.ReadAllBytesAsync(annotation.ImagePath, ct)
: null;
var annMessage = new AnnotationMessage
{ {
Name = annotation.Name, Name = annotation.Name,
OriginalMediaName = annotation.OriginalMediaName, OriginalMediaName = annotation.OriginalMediaName,
Time = annotation.Time, Time = annotation.Time,
CreatedRole = annotation.CreatedRole, Role = annotation.CreatedRole,
CreatedEmail = annotation.CreatedEmail, Email = annotation.CreatedEmail,
CreatedDate = annotation.CreatedDate, CreatedDate = annotation.CreatedDate,
Status = annotation.AnnotationStatus, Status = annotation.AnnotationStatus,
@@ -121,27 +121,42 @@ public class FailsafeAnnotationsProducer
Detections = JsonConvert.SerializeObject(annotation.Detections), Detections = JsonConvert.SerializeObject(annotation.Detections),
Source = annotation.Source, Source = annotation.Source,
}; };
messages.Add(annCreateMessage); 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;
}
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, e.Message); _logger.LogError(e, e.Message);
badImages.Add(annotation.Name); await Task.Delay(TimeSpan.FromSeconds(10), ct);
} }
await Task.Delay(TimeSpan.FromSeconds(10), ct);
}
}
await Task.Delay(TimeSpan.FromSeconds(5), ct);
} }
if (badImages.Any()) public async Task SendToInnerQueue(List<string> annotationNames, AnnotationStatus status, CancellationToken cancellationToken = default)
{ {
await db.AnnotationsQueue.Where(x => badImages.Contains(x.Name)).DeleteAsync(token: cancellationToken); if (_uiConfig.SilentDetection)
_dbFactory.SaveToDisk(); return;
} await _dbFactory.RunWrite(async db =>
return messages; await db.InsertAsync(new AnnotationQueueRecord
});
}
public async Task SendToInnerQueue(Annotation annotation, CancellationToken cancellationToken = default)
{ {
await _dbFactory.Run(async db => Id = Guid.NewGuid(),
await db.InsertAsync(new AnnotationName { Name = annotation.Name }, token: cancellationToken)); DateTime = DateTime.UtcNow,
Operation = status,
AnnotationNames = annotationNames
}, token: cancellationToken));
} }
} }
@@ -0,0 +1,14 @@
using MediatR;
namespace Azaion.Common.Services;
public class GPSMatcherEventHandler(IGpsMatcherService gpsMatcherService) :
INotificationHandler<GPSMatcherResultEvent>,
INotificationHandler<GPSMatcherFinishedEvent>
{
public async Task Handle(GPSMatcherResultEvent result, CancellationToken cancellationToken) =>
await gpsMatcherService.SetGpsResult(result, cancellationToken);
public async Task Handle(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken) =>
await gpsMatcherService.FinishGPS(notification, cancellationToken);
}
@@ -0,0 +1,18 @@
using MediatR;
namespace Azaion.Common.Services;
public class GPSMatcherResultEvent : INotification
{
public int Index { get; set; }
public string Image { get; set; } = null!;
public double Latitude { get; set; }
public double Longitude { get; set; }
public int KeyPoints { get; set; }
public int Rotation { get; set; }
public string MatchType { get; set; } = null!;
}
public class GPSMatcherJobAcceptedEvent : INotification {}
public class GPSMatcherFinishedEvent : INotification {}
+63 -39
View File
@@ -1,73 +1,97 @@
using System.Diagnostics; using System.IO;
using System.IO;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.CommonSecurity;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace Azaion.Common.Services; namespace Azaion.Common.Services;
public interface IGpsMatcherService public interface IGpsMatcherService
{ {
Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, Func<GpsMatchResult, Task> processResult, CancellationToken detectToken = default); Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, CancellationToken detectToken = default);
void StopGpsMatching(); void StopGpsMatching();
Task SetGpsResult(GPSMatcherResultEvent result, CancellationToken detectToken = default);
Task FinishGPS(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken);
} }
public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDownloader satelliteTileDownloader, IOptions<DirectoriesConfig> dirConfig) : IGpsMatcherService public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDownloader satelliteTileDownloader, IOptions<DirectoriesConfig> dirConfig) : IGpsMatcherService
{ {
private readonly DirectoriesConfig _dirConfig = dirConfig.Value;
private const int ZOOM_LEVEL = 18; private const int ZOOM_LEVEL = 18;
private const int POINTS_COUNT = 10; private const int POINTS_COUNT = 10;
private const int DISTANCE_BETWEEN_POINTS_M = 100; private const int DISTANCE_BETWEEN_POINTS_M = 100;
private const double SATELLITE_RADIUS_M = DISTANCE_BETWEEN_POINTS_M * (POINTS_COUNT + 1); private const double SATELLITE_RADIUS_M = DISTANCE_BETWEEN_POINTS_M * (POINTS_COUNT + 1);
public async Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, Func<GpsMatchResult, Task> processResult, CancellationToken detectToken = default) private string _routeDir = "";
{ private string _userRouteDir = "";
var currentLat = initialLatitude; private List<string> _allRouteImages = new();
var currentLon = initialLongitude; private Dictionary<string, int> _currentRouteImages = new();
private double _currentLat;
private double _currentLon;
private CancellationToken _detectToken;
private int _currentIndex;
var routeDir = Path.Combine(SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER, dirConfig.Value.GpsRouteDirectory);
if (Directory.Exists(routeDir))
Directory.Delete(routeDir, true);
Directory.CreateDirectory(routeDir);
var routeFiles = new List<string>(); public async Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, CancellationToken detectToken = default)
foreach (var file in Directory.GetFiles(userRouteDir))
{ {
routeFiles.Add(file); _routeDir = Path.Combine(SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER, _dirConfig.GpsRouteDirectory);
File.Copy(file, Path.Combine(routeDir, Path.GetFileName(file))); _userRouteDir = userRouteDir;
_allRouteImages = Directory.GetFiles(userRouteDir)
.OrderBy(x => x).ToList();
_currentLat = initialLatitude;
_currentLon = initialLongitude;
_detectToken = detectToken;
await StartMatchingRound(0);
} }
var indexOffset = 0; private async Task StartMatchingRound(int startIndex)
while (routeFiles.Any())
{ {
await satelliteTileDownloader.GetTiles(currentLat, currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, detectToken); //empty route dir
gpsMatcherClient.StartMatching(new StartMatchingEvent if (Directory.Exists(_routeDir))
Directory.Delete(_routeDir, true);
Directory.CreateDirectory(_routeDir);
_currentRouteImages = _allRouteImages
.Skip(startIndex)
.Take(POINTS_COUNT)
.Select((fullName, index) =>
{
var filename = Path.GetFileName(fullName);
File.Copy(Path.Combine(_userRouteDir, filename), Path.Combine(_routeDir, filename));
return new { Filename = Path.GetFileNameWithoutExtension(fullName), Index = startIndex + index };
})
.ToDictionary(x => x.Filename, x => x.Index);
await satelliteTileDownloader.GetTiles(_currentLat, _currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, _detectToken);
await gpsMatcherClient.StartMatching(new StartMatchingEvent
{ {
ImagesCount = POINTS_COUNT, ImagesCount = POINTS_COUNT,
Latitude = initialLatitude, Latitude = _currentLat,
Longitude = initialLongitude, Longitude = _currentLon,
SatelliteImagesDir = dirConfig.Value.GpsSatDirectory, SatelliteImagesDir = _dirConfig.GpsSatDirectory,
RouteDir = dirConfig.Value.GpsRouteDirectory RouteDir = _dirConfig.GpsRouteDirectory
}); });
while (true)
{
var result = gpsMatcherClient.GetResult();
if (result == null)
break;
result.Index += indexOffset;
await processResult(result);
currentLat = result.Latitude;
currentLon = result.Longitude;
routeFiles.RemoveAt(0);
}
indexOffset += POINTS_COUNT;
}
} }
public void StopGpsMatching() public void StopGpsMatching()
{ {
gpsMatcherClient.Stop(); gpsMatcherClient.Stop();
} }
public async Task SetGpsResult(GPSMatcherResultEvent result, CancellationToken detectToken = default)
{
_currentIndex = _currentRouteImages[result.Image];
_currentRouteImages.Remove(result.Image);
_currentLat = result.Latitude;
_currentLon = result.Longitude;
await Task.CompletedTask;
}
public async Task FinishGPS(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken)
{
if (_currentRouteImages.Count == 0 && _currentIndex < _allRouteImages.Count)
await StartMatchingRound(_currentIndex);
}
} }
+38 -21
View File
@@ -9,7 +9,6 @@ using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue; using Azaion.Common.DTO.Queue;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
using Azaion.CommonSecurity.DTO;
using LinqToDB; using LinqToDB;
using LinqToDB.Data; using LinqToDB.Data;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -61,7 +60,7 @@ public class GalleryService(
{ {
foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles()) foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles())
file.Delete(); file.Delete();
await dbFactory.Run(async db => await dbFactory.RunWrite(async db =>
{ {
await db.Detections.DeleteAsync(x => true, token: cancellationToken); await db.Detections.DeleteAsync(x => true, token: cancellationToken);
await db.Annotations.DeleteAsync(x => true, token: cancellationToken); await db.Annotations.DeleteAsync(x => true, token: cancellationToken);
@@ -73,7 +72,7 @@ public class GalleryService(
await _updateLock.WaitAsync(); await _updateLock.WaitAsync();
var existingAnnotations = new ConcurrentDictionary<string, Annotation>(await dbFactory.Run(async db => var existingAnnotations = new ConcurrentDictionary<string, Annotation>(await dbFactory.Run(async db =>
await db.Annotations.ToDictionaryAsync(x => x.Name))); await db.Annotations.ToDictionaryAsync(x => x.Name)));
var missedAnnotations = new ConcurrentBag<Annotation>(); var missedAnnotations = new ConcurrentDictionary<string, Annotation>();
try try
{ {
var prefixLen = Constants.THUMBNAIL_PREFIX.Length; var prefixLen = Constants.THUMBNAIL_PREFIX.Length;
@@ -89,7 +88,7 @@ public class GalleryService(
await ParallelExt.ForEachAsync(files, async (file, cancellationToken) => await ParallelExt.ForEachAsync(files, async (file, cancellationToken) =>
{ {
var fName = Path.GetFileNameWithoutExtension(file.Name); var fName = file.Name.ToFName();
try try
{ {
var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{fName}.txt"); var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{fName}.txt");
@@ -136,7 +135,7 @@ public class GalleryService(
{ {
Time = time, Time = time,
OriginalMediaName = originalMediaName, OriginalMediaName = originalMediaName,
Name = file.Name.ToFName(), Name = fName,
ImageExtension = Path.GetExtension(file.Name), ImageExtension = Path.GetExtension(file.Name),
Detections = detections, Detections = detections,
CreatedDate = File.GetCreationTimeUtc(file.FullName), CreatedDate = File.GetCreationTimeUtc(file.FullName),
@@ -146,11 +145,18 @@ public class GalleryService(
AnnotationStatus = AnnotationStatus.Validated AnnotationStatus = AnnotationStatus.Validated
}; };
//Remove duplicates
if (!existingAnnotations.ContainsKey(fName)) if (!existingAnnotations.ContainsKey(fName))
missedAnnotations.Add(annotation); {
if (missedAnnotations.ContainsKey(fName))
Console.WriteLine($"{fName} is already exists! Duplicate!");
else
missedAnnotations.TryAdd(fName, annotation);
}
if (!thumbnails.Contains(fName)) if (!thumbnails.Contains(fName))
await CreateThumbnail(annotation, cancellationToken); await CreateThumbnail(annotation, cancellationToken: cancellationToken);
} }
catch (Exception e) catch (Exception e)
@@ -181,24 +187,33 @@ public class GalleryService(
{ {
MaxBatchSize = 50 MaxBatchSize = 50
}; };
await dbFactory.Run(async db =>
//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, missedAnnotations); await db.BulkCopyAsync(copyOptions, annotationsToInsert);
await db.BulkCopyAsync(copyOptions, missedAnnotations.SelectMany(x => x.Detections)); await db.BulkCopyAsync(copyOptions, annotationsToInsert.SelectMany(x => x.Detections));
}); });
dbFactory.SaveToDisk();
_updateLock.Release(); _updateLock.Release();
} }
} }
public async Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default) public async Task CreateThumbnail(Annotation annotation, Image? originalImage = null, CancellationToken cancellationToken = default)
{ {
try try
{ {
var width = (int)_thumbnailConfig.Size.Width; var width = (int)_thumbnailConfig.Size.Width;
var height = (int)_thumbnailConfig.Size.Height; 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); var bitmap = new Bitmap(width, height);
@@ -265,10 +280,9 @@ public class GalleryService(
logger.LogError(e, e.Message); logger.LogError(e, e.Message);
} }
} }
public async Task CreateAnnotatedImage(Annotation annotation, Image? originalImage = null, CancellationToken token = default)
public async Task CreateAnnotatedImage(Annotation annotation, CancellationToken token)
{ {
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); using var g = Graphics.FromImage(originalImage);
foreach (var detection in annotation.Detections) foreach (var detection in annotation.Detections)
@@ -282,17 +296,20 @@ public class GalleryService(
var label = detection.Confidence >= 0.995 ? detClass.UIName : $"{detClass.UIName}: {detection.Confidence * 100:F0}%"; 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); 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 public interface IGalleryService
{ {
event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate; event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate;
double ProcessedThumbnailsPercentage { get; set; } Task CreateThumbnail(Annotation annotation, Image? originalImage = null, CancellationToken cancellationToken = default);
Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default);
Task RefreshThumbnails(); Task RefreshThumbnails();
Task ClearThumbnails(CancellationToken cancellationToken = default); Task ClearThumbnails(CancellationToken cancellationToken = default);
Task CreateAnnotatedImage(Annotation annotation, Image? originalImage = null, CancellationToken token = default);
Task CreateAnnotatedImage(Annotation annotation, CancellationToken token);
} }
+75 -44
View File
@@ -1,18 +1,18 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.CommonSecurity; using Azaion.Common.Events;
using Azaion.CommonSecurity.DTO; using MediatR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using NetMQ; using NetMQ;
using NetMQ.Sockets; using NetMQ.Sockets;
namespace Azaion.Common.Services; namespace Azaion.Common.Services;
public interface IGpsMatcherClient public interface IGpsMatcherClient : IDisposable
{ {
Task StartMatching(StartMatchingEvent startEvent);
void StartMatching(StartMatchingEvent startEvent);
GpsMatchResult? GetResult(int retries = 2, int tryTimeoutSeconds = 5, CancellationToken ct = default);
void Stop(); void Stop();
} }
@@ -23,29 +23,28 @@ public class StartMatchingEvent
public int ImagesCount { get; set; } public int ImagesCount { get; set; }
public double Latitude { get; set; } public double Latitude { get; set; }
public double Longitude { get; set; } public double Longitude { get; set; }
public string ProcessingType { get; set; } = "cuda";
public int Altitude { get; set; } = 400; public int Altitude { get; set; } = 400;
public double CameraSensorWidth { get; set; } = 23.5; public double CameraSensorWidth { get; set; } = 23.5;
public double CameraFocalLength { get; set; } = 24; public double CameraFocalLength { get; set; } = 24;
public override string ToString() => public override string ToString() =>
$"{RouteDir},{SatelliteImagesDir},{ImagesCount},{Latitude},{Longitude},{ProcessingType},{Altitude},{CameraSensorWidth},{CameraFocalLength}"; $"{RouteDir},{SatelliteImagesDir},{ImagesCount},{Latitude},{Longitude},{Altitude},{CameraSensorWidth},{CameraFocalLength}";
} }
public class GpsMatcherClient : IGpsMatcherClient public class GpsMatcherClient : IGpsMatcherClient
{ {
private readonly GpsDeniedClientConfig _gpsDeniedClientConfig; private readonly IMediator _mediator;
private readonly ILogger<GpsMatcherClient> _logger;
private readonly string _requestAddress;
private readonly RequestSocket _requestSocket = new(); private readonly RequestSocket _requestSocket = new();
private readonly string _subscriberAddress;
private readonly SubscriberSocket _subscriberSocket = new(); private readonly SubscriberSocket _subscriberSocket = new();
private readonly NetMQPoller _poller = new();
public GpsMatcherClient(IOptions<GpsDeniedClientConfig> gpsDeniedClientConfig) public GpsMatcherClient(IMediator mediator, IOptions<GpsDeniedClientConfig> gpsConfig, ILogger<GpsMatcherClient> logger)
{
_gpsDeniedClientConfig = gpsDeniedClientConfig.Value;
Start();
}
private void Start()
{ {
_mediator = mediator;
_logger = logger;
try try
{ {
using var process = new Process(); using var process = new Process();
@@ -61,58 +60,90 @@ public class GpsMatcherClient : IGpsMatcherClient
process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); }; process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); }; process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
//process.Start(); process.Start();
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e); Console.WriteLine(e);
//throw; //throw;
} }
_requestSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqPort}");
_subscriberSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqSubscriberPort}"); _requestAddress = $"tcp://{gpsConfig.Value.ZeroMqHost}:{gpsConfig.Value.ZeroMqPort}";
_requestSocket.Connect(_requestAddress);
_subscriberAddress = $"tcp://{gpsConfig.Value.ZeroMqHost}:{gpsConfig.Value.ZeroMqReceiverPort}";
_subscriberSocket.Connect(_subscriberAddress);
_subscriberSocket.Subscribe(""); _subscriberSocket.Subscribe("");
_subscriberSocket.ReceiveReady += async (sender, e) => await ProcessClientCommand(sender, e);
_poller.Add(_subscriberSocket);
_poller.RunAsync();
} }
public void StartMatching(StartMatchingEvent e) private async Task ProcessClientCommand(object? sender, NetMQSocketEventArgs e)
{ {
_requestSocket.SendFrame(e.ToString()); while (e.Socket.TryReceiveFrameString(TimeSpan.FromMilliseconds(100), out var str))
var response = _requestSocket.ReceiveFrameString();
if (response != "OK")
throw new Exception("Start Matching Failed");
}
public GpsMatchResult? GetResult(int retries = 15, int tryTimeoutSeconds = 5, CancellationToken ct = default)
{ {
var tryNum = 0; try
while (!ct.IsCancellationRequested && tryNum++ < retries)
{ {
if (!_subscriberSocket.TryReceiveFrameString(TimeSpan.FromSeconds(tryTimeoutSeconds), out var update)) if (string.IsNullOrEmpty(str))
continue; continue;
if (update == "FINISHED")
return null;
var parts = update.Split(','); switch (str)
{
case "FINISHED":
await _mediator.Publish(new GPSMatcherFinishedEvent());
break;
case "OK":
await _mediator.Publish(new GPSMatcherJobAcceptedEvent());
break;
default:
var parts = str.Split(',');
if (parts.Length != 5) if (parts.Length != 5)
throw new Exception("Matching Result Failed"); throw new Exception("Matching Result Failed");
return new GpsMatchResult var filename = Path.GetFileNameWithoutExtension(parts[1]);
await _mediator.Publish(new GPSMatcherResultEvent
{ {
Index = int.Parse(parts[0]), Index = int.Parse(parts[0]),
Image = parts[1], Image = filename,
Latitude = double.Parse(parts[2]), Latitude = double.Parse(parts[2]),
Longitude = double.Parse(parts[3]), Longitude = double.Parse(parts[3]),
MatchType = parts[4] MatchType = parts[4]
}; });
break;
} }
if (!ct.IsCancellationRequested)
throw new Exception($"Unable to get bytes after {tryNum} retries, {tryTimeoutSeconds} seconds each");
return null;
} }
catch (Exception ex)
public void Stop()
{ {
_requestSocket.SendFrame("STOP"); _logger.LogError(ex, ex.Message);
}
}
}
public async Task StartMatching(StartMatchingEvent e)
{
_requestSocket.SendFrame(e.ToString());
_requestSocket.TryReceiveFrameString(TimeSpan.FromMilliseconds(300), out var response);
if (response != "OK")
{
_logger.LogError(response);
await _mediator.Publish(new SetStatusTextEvent(response ?? "", true));
}
}
public void Stop() => _requestSocket.SendFrame("STOP");
public void Dispose()
{
_poller.Stop();
_poller.Dispose();
_requestSocket.SendFrame("EXIT");
_requestSocket.Disconnect(_requestAddress);
_requestSocket.Dispose();
_subscriberSocket.Disconnect(_subscriberAddress);
_subscriberSocket.Dispose();
} }
} }
+109
View File
@@ -0,0 +1,109 @@
using System.Diagnostics;
using System.Text;
using Azaion.Common.DTO;
using MessagePack;
using Microsoft.Extensions.Options;
using NetMQ;
using NetMQ.Sockets;
namespace Azaion.Common.Services;
public interface IInferenceClient : IDisposable
{
event EventHandler<RemoteCommand>? InferenceDataReceived;
event EventHandler<RemoteCommand>? AIAvailabilityReceived;
void Send(RemoteCommand create);
void Stop();
}
public class InferenceClient : IInferenceClient
{
public event EventHandler<RemoteCommand>? BytesReceived;
public event EventHandler<RemoteCommand>? InferenceDataReceived;
public event EventHandler<RemoteCommand>? AIAvailabilityReceived;
private readonly DealerSocket _dealer = new();
private readonly NetMQPoller _poller = new();
private readonly Guid _clientId = Guid.NewGuid();
private readonly InferenceClientConfig _inferenceClientConfig;
private readonly LoaderClientConfig _loaderClientConfig;
public InferenceClient(IOptions<InferenceClientConfig> inferenceConfig, IOptions<LoaderClientConfig> loaderConfig)
{
_inferenceClientConfig = inferenceConfig.Value;
_loaderClientConfig = loaderConfig.Value;
Start();
}
private void Start()
{
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = SecurityConstants.EXTERNAL_INFERENCE_PATH,
Arguments = $"-p {_inferenceClientConfig.ZeroMqPort} -lp {_loaderClientConfig.ZeroMqPort} -a {_inferenceClientConfig.ApiUrl}",
//RedirectStandardOutput = true,
//RedirectStandardError = true,
//CreateNoWindow = true
};
process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
process.Start();
}
catch (Exception e)
{
Console.WriteLine(e);
//throw;
}
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
_dealer.Connect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}");
_dealer.ReceiveReady += (_, e) => ProcessClientCommand(e.Socket);
_poller.Add(_dealer);
_ = Task.Run(() => _poller.RunAsync());
}
private void ProcessClientCommand(NetMQSocket socket, CancellationToken ct = default)
{
while (socket.TryReceiveFrameBytes(TimeSpan.Zero, out var bytes))
{
if (bytes?.Length == 0)
continue;
var remoteCommand = MessagePackSerializer.Deserialize<RemoteCommand>(bytes, cancellationToken: ct);
switch (remoteCommand.CommandType)
{
case CommandType.DataBytes:
BytesReceived?.Invoke(this, remoteCommand);
break;
case CommandType.InferenceData:
InferenceDataReceived?.Invoke(this, remoteCommand);
break;
case CommandType.AIAvailabilityResult:
AIAvailabilityReceived?.Invoke(this, remoteCommand);
break;
}
}
}
public void Stop() =>
Send(RemoteCommand.Create(CommandType.StopInference));
public void Send(RemoteCommand command) =>
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
public void Dispose()
{
_poller.Stop();
_poller.Dispose();
_dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit)));
_dealer.Disconnect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}");
_dealer.Close();
_dealer.Dispose();
}
}
+52 -26
View File
@@ -1,11 +1,10 @@
using System.Text; using Azaion.Common.Database;
using Azaion.Common.Database; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.CommonSecurity; using Azaion.Common.Events;
using Azaion.CommonSecurity.DTO.Commands; using Azaion.Common.Extensions;
using Azaion.CommonSecurity.Services; using MediatR;
using MessagePack; using MessagePack;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@@ -13,44 +12,71 @@ namespace Azaion.Common.Services;
public interface IInferenceService public interface IInferenceService
{ {
Task RunInference(List<string> mediaPaths, Func<AnnotationImage, Task> processAnnotation, CancellationToken detectToken = default); Task RunInference(List<string> mediaPaths, CancellationToken ct = default);
void StopInference(); void StopInference();
} }
public class InferenceService(ILogger<InferenceService> logger, IInferenceClient client, IOptions<AIRecognitionConfig> aiConfigOptions) : IInferenceService public class InferenceService : IInferenceService
{ {
public async Task RunInference(List<string> mediaPaths, Func<AnnotationImage, Task> processAnnotation, CancellationToken detectToken = default) private readonly IInferenceClient _client;
private readonly IAzaionApi _azaionApi;
private readonly IOptions<AIRecognitionConfig> _aiConfigOptions;
private readonly IAnnotationService _annotationService;
private readonly IMediator _mediator;
private CancellationTokenSource _inferenceCancelTokenSource = new();
public InferenceService(
ILogger<InferenceService> logger,
IInferenceClient client,
IAzaionApi azaionApi,
IOptions<AIRecognitionConfig> aiConfigOptions,
IAnnotationService annotationService,
IMediator mediator)
{ {
var aiConfig = aiConfigOptions.Value; _client = client;
_azaionApi = azaionApi;
_aiConfigOptions = aiConfigOptions;
_annotationService = annotationService;
_mediator = mediator;
aiConfig.Paths = mediaPaths; client.InferenceDataReceived += async (sender, command) =>
client.Send(RemoteCommand.Create(CommandType.Inference, aiConfig));
while (!detectToken.IsCancellationRequested)
{ {
try try
{ {
var bytes = client.GetBytes(ct: detectToken); if (command.Message == "DONE")
if (bytes == null) {
throw new Exception("Can't get bytes from inference client"); _inferenceCancelTokenSource?.Cancel();
if (bytes.Length == 4 && Encoding.UTF8.GetString(bytes) == "DONE")
return; return;
}
var annotationImage = MessagePackSerializer.Deserialize<AnnotationImage>(bytes, cancellationToken: detectToken); var annImage = MessagePackSerializer.Deserialize<AnnotationImage>(command.Data);
await ProcessDetection(annImage);
await processAnnotation(annotationImage);
} }
catch (Exception e) catch (Exception e)
{ {
logger.LogError(e, e.Message); logger.LogError(e, e.Message);
break;
}
} }
};
} }
public void StopInference() private async Task ProcessDetection(AnnotationImage annotationImage, CancellationToken ct = default)
{ {
client.Send(RemoteCommand.Create(CommandType.StopInference)); var annotation = await _annotationService.SaveAnnotation(annotationImage, ct);
await _mediator.Publish(new AnnotationAddedEvent(annotation), ct);
} }
public async Task RunInference(List<string> mediaPaths, CancellationToken ct = default)
{
_inferenceCancelTokenSource = new CancellationTokenSource();
_client.Send(RemoteCommand.Create(CommandType.Login, _azaionApi.Credentials));
var aiConfig = _aiConfigOptions.Value;
aiConfig.Paths = mediaPaths;
_client.Send(RemoteCommand.Create(CommandType.Inference, aiConfig));
using var combinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ct, _inferenceCancelTokenSource.Token);
await combinedTokenSource.Token.AsTask();
}
public void StopInference() => _client.Stop();
} }
+103
View File
@@ -0,0 +1,103 @@
using System.Diagnostics;
using System.IO;
using System.Text;
using Azaion.Common.DTO;
using MessagePack;
using NetMQ;
using NetMQ.Sockets;
using Serilog;
using Exception = System.Exception;
namespace Azaion.Common.Services;
public class LoaderClient(LoaderClientConfig config, ILogger logger, CancellationToken ct = default) : IDisposable
{
private readonly DealerSocket _dealer = new();
private readonly Guid _clientId = Guid.NewGuid();
public void StartClient()
{
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = SecurityConstants.EXTERNAL_LOADER_PATH,
Arguments = $"--port {config.ZeroMqPort} --api {config.ApiUrl}",
//CreateNoWindow = true
};
process.OutputDataReceived += (_, e) =>
{
if (e.Data != null) Console.WriteLine(e.Data);
};
process.ErrorDataReceived += (_, e) =>
{
if (e.Data != null) Console.WriteLine(e.Data);
};
process.Start();
}
catch (Exception e)
{
logger.Error(e.Message);
throw;
}
}
public void Connect()
{
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
_dealer.Connect($"tcp://{config.ZeroMqHost}:{config.ZeroMqPort}");
}
public void Login(ApiCredentials credentials)
{
var result = SendCommand(RemoteCommand.Create(CommandType.Login, credentials));
if (result.CommandType != CommandType.Ok)
throw new Exception(result.Message);
}
public MemoryStream LoadFile(string filename, string folder)
{
var result = SendCommand(RemoteCommand.Create(CommandType.Load, new LoadFileData(filename, folder)));
if (result.Data?.Length == 0)
throw new Exception($"Can't load {filename}. Returns 0 bytes");
return new MemoryStream(result.Data!);
}
private RemoteCommand SendCommand(RemoteCommand command, int retryCount = 50, int retryDelayMs = 800)
{
try
{
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
var tryNum = 0;
while (!ct.IsCancellationRequested && tryNum++ < retryCount)
{
if (!_dealer.TryReceiveFrameBytes(TimeSpan.FromMilliseconds(retryDelayMs), out var bytes))
continue;
var res = MessagePackSerializer.Deserialize<RemoteCommand>(bytes, cancellationToken: ct);
if (res.CommandType == CommandType.Error)
throw new Exception(res.Message);
return res;
}
throw new Exception($"Sent {command} {retryCount} times, with wait time {retryDelayMs}ms for each call. No response from client.");
}
catch (Exception e)
{
logger.Error(e, e.Message);
throw;
}
}
public void Stop()
{
_dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit)));
}
public void Dispose()
{
_dealer.Dispose();
}
}
+11 -51
View File
@@ -5,8 +5,10 @@ using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.Events;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
using Azaion.CommonSecurity; using Azaion.CommonSecurity;
using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -25,7 +27,8 @@ public class SatelliteDownloader(
ILogger<SatelliteDownloader> logger, ILogger<SatelliteDownloader> logger,
IOptions<MapConfig> mapConfig, IOptions<MapConfig> mapConfig,
IOptions<DirectoriesConfig> directoriesConfig, IOptions<DirectoriesConfig> directoriesConfig,
IHttpClientFactory httpClientFactory) IHttpClientFactory httpClientFactory,
IMediator mediator)
: ISatelliteDownloader : ISatelliteDownloader
{ {
private const int INPUT_TILE_SIZE = 256; private const int INPUT_TILE_SIZE = 256;
@@ -44,14 +47,18 @@ public class SatelliteDownloader(
public async Task GetTiles(double centerLat, double centerLon, double radiusM, int zoomLevel, CancellationToken token = default) public async Task GetTiles(double centerLat, double centerLon, double radiusM, int zoomLevel, CancellationToken token = default)
{ {
await mediator.Publish(new SetStatusTextEvent($"Завантажується супутникові зображення по координатах: центр: lat: {centerLat:F3} lon: {centerLon:F3} квадрат {radiusM}м * {radiusM}м, zoom: {zoomLevel}..."), token);
//empty Satellite directory //empty Satellite directory
if (Directory.Exists(_satDirectory)) if (Directory.Exists(_satDirectory))
Directory.Delete(_satDirectory, true); Directory.Delete(_satDirectory, true);
Directory.CreateDirectory(_satDirectory); Directory.CreateDirectory(_satDirectory);
var downloadTilesResult = await DownloadTiles(centerLat, centerLon, radiusM, zoomLevel, token); var downloadTilesResult = await DownloadTiles(centerLat, centerLon, radiusM, zoomLevel, token);
var image = await ComposeTiles(downloadTilesResult.Tiles, token); await mediator.Publish(new SetStatusTextEvent("Завершено! Склеюється в 1 зображення..."), token);
if (image != null) var image = ComposeTiles(downloadTilesResult.Tiles, token);
if (image == null)
return;
await mediator.Publish(new SetStatusTextEvent("Розбиття на малі зображення для опрацювання..."), token);
await SplitToTiles(image, downloadTilesResult, token); await SplitToTiles(image, downloadTilesResult, token);
} }
@@ -103,52 +110,7 @@ public class SatelliteDownloader(
await Task.Run(() => Parallel.ForEach(cropTasks, action => action()), token); await Task.Run(() => Parallel.ForEach(cropTasks, action => action()), token);
} }
private async Task SplitToTiles_OLD(Image<Rgba32> image, DownloadTilesResult bounds, CancellationToken token = default) private Image<Rgba32>? ComposeTiles(ConcurrentDictionary<(int x, int y), byte[]> downloadedTiles, CancellationToken token = default)
{
ArgumentNullException.ThrowIfNull(image);
ArgumentNullException.ThrowIfNull(bounds);
if (bounds.LatMax <= bounds.LatMin || bounds.LonMax <= bounds.LonMin || image.Width <= 0 || image.Height <= 0)
throw new ArgumentException("Invalid coordinate bounds (LatMax <= LatMin or LonMax <= LonMin) or image dimensions (Width/Height <= 0).");
var latRange = bounds.LatMax - bounds.LatMin;
var lonRange = bounds.LonMax - bounds.LonMin;
var degreesPerPixelLat = latRange / image.Height;
var degreesPerPixelLon = lonRange / image.Width;
var rowIndex = 0;
for (int top = 0; top <= image.Height - CROP_HEIGHT; top += STEP_Y)
{
token.ThrowIfCancellationRequested();
int colIndex = 0;
for (int left = 0; left <= image.Width - CROP_WIDTH; left += STEP_X)
{
token.ThrowIfCancellationRequested();
var cropBox = new Rectangle(left, top, CROP_WIDTH, CROP_HEIGHT);
using (var croppedImage = image.Clone(ctx => ctx.Crop(cropBox)))
{
var cropTlLat = bounds.LatMax - (top * degreesPerPixelLat);
var cropTlLon = bounds.LonMin + (left * degreesPerPixelLon);
var cropBrLat = cropTlLat - (CROP_HEIGHT * degreesPerPixelLat);
var cropBrLon = cropTlLon + (CROP_WIDTH * degreesPerPixelLon);
var outputFilename = Path.Combine(_satDirectory,
$"map_{rowIndex:D4}_{colIndex:D4}_tl_{cropTlLat:F6}_{cropTlLon:F6}_br_{cropBrLat:F6}_{cropBrLon:F6}.tif"
);
using (var resizedImage = croppedImage.Clone(ctx => ctx.Resize(OUTPUT_TILE_SIZE, OUTPUT_TILE_SIZE, KnownResamplers.Lanczos3)))
await resizedImage.SaveAsTiffAsync(outputFilename, token);
}
colIndex++;
}
rowIndex++;
}
}
private async Task<Image<Rgba32>?> ComposeTiles(ConcurrentDictionary<(int x, int y), byte[]> downloadedTiles, CancellationToken token = default)
{ {
if (downloadedTiles.IsEmpty) if (downloadedTiles.IsEmpty)
return null; return null;
@@ -192,8 +154,6 @@ public class SatelliteDownloader(
} }
}); });
// await largeImage.SaveAsync(Path.Combine(_satDirectory, "full_map.tif"),
// new TiffEncoder { Compression = TiffCompression.Deflate }, token);
return largeImage; return largeImage;
} }
@@ -1,19 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MessagePack" Version="3.1.0" />
<PackageReference Include="MessagePack.Annotations" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
<PackageReference Include="NetMQ" Version="4.0.1.13" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
</ItemGroup>
</Project>
@@ -1,16 +0,0 @@
using MessagePack;
namespace Azaion.CommonSecurity.DTO;
[MessagePackObject]
public class ApiCredentials(string email, string password) : EventArgs
{
[Key(nameof(Email))]
public string Email { get; set; } = email;
[Key(nameof(Password))]
public string Password { get; set; } = password;
[Key(nameof(Folder))]
public string Folder { get; set; } = null!;
}
-11
View File
@@ -1,11 +0,0 @@
namespace Azaion.CommonSecurity.DTO;
public class HardwareInfo
{
public string CPU { get; set; } = null!;
public string GPU { get; set; } = null!;
public string MacAddress { get; set; } = null!;
public string Memory { get; set; } = null!;
public string Hash { get; set; } = null!;
}
@@ -1,7 +0,0 @@
namespace Azaion.CommonSecurity.DTO;
public class SecureAppConfig
{
public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
}
-11
View File
@@ -1,11 +0,0 @@
using MessagePack;
namespace Azaion.CommonSecurity.DTO;
[MessagePackObject]
public class User
{
[Key("i")] public string Id { get; set; } = "";
[Key("e")] public string Email { get; set; } = "";
[Key("r")]public RoleEnum Role { get; set; }
}
@@ -1,44 +0,0 @@
using Azaion.CommonSecurity.DTO;
namespace Azaion.CommonSecurity;
public class SecurityConstants
{
public const string CONFIG_PATH = "config.json";
public const string DUMMY_DIR = "dummy";
#region ExternalClientsConfig
public const string EXTERNAL_INFERENCE_PATH = "azaion-inference.exe";
public const string EXTERNAL_GPS_DENIED_FOLDER = "gps-denied";
public static readonly string ExternalGpsDeniedPath = Path.Combine(EXTERNAL_GPS_DENIED_FOLDER, "image-matcher.exe");
public const string DEFAULT_ZMQ_INFERENCE_HOST = "127.0.0.1";
public const int DEFAULT_ZMQ_INFERENCE_PORT = 5227;
public const string DEFAULT_ZMQ_GPS_DENIED_HOST = "127.0.0.1";
public const int DEFAULT_ZMQ_GPS_DENIED_PORT = 5227;
public const int DEFAULT_RETRY_COUNT = 25;
public const int DEFAULT_TIMEOUT_SECONDS = 5;
public static readonly SecureAppConfig DefaultSecureAppConfig = new()
{
InferenceClientConfig = new InferenceClientConfig
{
ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST,
ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT,
OneTryTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS,
RetryCount = DEFAULT_RETRY_COUNT,
ResourcesFolder = ""
},
GpsDeniedClientConfig = new GpsDeniedClientConfig
{
ZeroMqHost = DEFAULT_ZMQ_GPS_DENIED_HOST,
ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT,
OneTryTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS,
RetryCount = DEFAULT_RETRY_COUNT,
}
};
#endregion ExternalClientsConfig
}
@@ -1,26 +0,0 @@
using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.DTO.Commands;
using Microsoft.Extensions.DependencyInjection;
namespace Azaion.CommonSecurity.Services;
public interface IAuthProvider
{
void Login(ApiCredentials credentials);
User CurrentUser { get; }
}
public class AuthProvider(IInferenceClient inferenceClient) : IAuthProvider
{
public User CurrentUser { get; private set; } = null!;
public void Login(ApiCredentials credentials)
{
inferenceClient.Send(RemoteCommand.Create(CommandType.Login, credentials));
var user = inferenceClient.Get<User>();
if (user == null)
throw new Exception("Can't get user from Auth provider");
CurrentUser = user;
}
}
@@ -8,105 +8,96 @@ namespace Azaion.CommonSecurity.Services;
public interface IHardwareService public interface IHardwareService
{ {
HardwareInfo GetHardware(); //HardwareInfo GetHardware();
} }
public class HardwareService : IHardwareService public class HardwareService : IHardwareService
{ {
private const string WIN32_GET_HARDWARE_COMMAND = // private const string WIN32_GET_HARDWARE_COMMAND =
"powershell -Command \"" + // "powershell -Command \"" +
"Get-CimInstance -ClassName Win32_Processor | Select-Object -ExpandProperty Name | Write-Output; " + // "Get-CimInstance -ClassName Win32_Processor | Select-Object -ExpandProperty Name | Write-Output; " +
"Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty Name | Write-Output; " + // "Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty Name | Write-Output; " +
"Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -ExpandProperty TotalVisibleMemorySize | Write-Output" + // "Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -ExpandProperty TotalVisibleMemorySize | Write-Output" +
"\""; // "\"";
//
// private const string UNIX_GET_HARDWARE_COMMAND =
// "/bin/bash -c \"free -g | grep Mem: | awk '{print $2}' && " +
// "lscpu | grep 'Model name:' | cut -d':' -f2 && " +
// "lspci | grep VGA | cut -d':' -f3\"";
private const string UNIX_GET_HARDWARE_COMMAND = // public HardwareInfo GetHardware()
"/bin/bash -c \"free -g | grep Mem: | awk '{print $2}' && " + // {
"lscpu | grep 'Model name:' | cut -d':' -f2 && " + // try
"lspci | grep VGA | cut -d':' -f3\""; // {
// var output = RunCommand(Environment.OSVersion.Platform == PlatformID.Win32NT
// ? WIN32_GET_HARDWARE_COMMAND
// : UNIX_GET_HARDWARE_COMMAND);
//
// var lines = output
// .Replace("TotalVisibleMemorySize=", "")
// .Replace("Name=", "")
// .Replace(" ", " ")
// .Trim()
// .Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries)
// .Select(x => x.Trim())
// .ToArray();
//
// if (lines.Length < 3)
// throw new Exception("Can't get hardware info");
//
// var hardwareInfo = new HardwareInfo
// {
// CPU = lines[0],
// GPU = lines[1],
// Memory = lines[2],
// MacAddress = GetMacAddress()
// };
// return hardwareInfo;
// }
// catch (Exception ex)
// {
// Console.WriteLine(ex.Message);
// throw;
// }
// }
public HardwareInfo GetHardware() // private string GetMacAddress()
{ // {
try // var macAddress = NetworkInterface
{ // .GetAllNetworkInterfaces()
var output = RunCommand(Environment.OSVersion.Platform == PlatformID.Win32NT // .Where(nic => nic.OperationalStatus == OperationalStatus.Up)
? WIN32_GET_HARDWARE_COMMAND // .Select(nic => nic.GetPhysicalAddress().ToString())
: UNIX_GET_HARDWARE_COMMAND); // .FirstOrDefault();
//
// return macAddress ?? string.Empty;
// }
//
// private string RunCommand(string command)
// {
// try
// {
// using var process = new Process();
// process.StartInfo.FileName = Environment.OSVersion.Platform == PlatformID.Unix ? "/bin/bash" : "cmd.exe";
// process.StartInfo.Arguments = Environment.OSVersion.Platform == PlatformID.Unix
// ? $"-c \"{command}\""
// : $"/c {command}";
// process.StartInfo.RedirectStandardOutput = true;
// process.StartInfo.UseShellExecute = false;
// process.StartInfo.CreateNoWindow = true;
//
// process.Start();
// var result = process.StandardOutput.ReadToEnd();
// process.WaitForExit();
//
// return result.Trim();
// }
// catch
// {
// return string.Empty;
// }
// }
var lines = output // private static string ToHash(string str) =>
.Replace("TotalVisibleMemorySize=", "") // Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str)));
.Replace("Name=", "")
.Replace(" ", " ")
.Trim()
.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries);
var memoryStr = "Unknown RAM";
if (lines.Length > 0)
{
memoryStr = lines[0];
if (int.TryParse(memoryStr, out var memKb))
memoryStr = $"{Math.Round(memKb / 1024.0 / 1024.0)} Gb";
}
var macAddress = MacAddress();
var hardwareInfo = new HardwareInfo
{
Memory = memoryStr,
CPU = lines.Length > 1 && string.IsNullOrEmpty(lines[1])
? "Unknown CPU"
: lines[1].Trim(),
GPU = lines.Length > 2 && string.IsNullOrEmpty(lines[2])
? "Unknown GPU"
: lines[2],
MacAddress = macAddress
};
hardwareInfo.Hash = ToHash($"Az|{hardwareInfo.CPU}|{hardwareInfo.GPU}|{macAddress}");
return hardwareInfo;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}
}
private string MacAddress()
{
var macAddress = NetworkInterface
.GetAllNetworkInterfaces()
.Where(nic => nic.OperationalStatus == OperationalStatus.Up)
.Select(nic => nic.GetPhysicalAddress().ToString())
.FirstOrDefault();
return macAddress ?? string.Empty;
}
private string RunCommand(string command)
{
try
{
using var process = new Process();
process.StartInfo.FileName = Environment.OSVersion.Platform == PlatformID.Unix ? "/bin/bash" : "cmd.exe";
process.StartInfo.Arguments = Environment.OSVersion.Platform == PlatformID.Unix
? $"-c \"{command}\""
: $"/c {command}";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.Start();
var result = process.StandardOutput.ReadToEnd();
process.WaitForExit();
return result.Trim();
}
catch
{
return string.Empty;
}
}
private static string ToHash(string str) =>
Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str)));
} }
@@ -1,99 +0,0 @@
using System.Diagnostics;
using System.Text;
using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.DTO.Commands;
using MessagePack;
using Microsoft.Extensions.Options;
using NetMQ;
using NetMQ.Sockets;
namespace Azaion.CommonSecurity.Services;
public interface IInferenceClient
{
void Send(RemoteCommand create);
T? Get<T>(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) where T : class;
byte[]? GetBytes(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default);
void Stop();
}
public class InferenceClient : IInferenceClient
{
private readonly DealerSocket _dealer = new();
private readonly Guid _clientId = Guid.NewGuid();
private readonly InferenceClientConfig _inferenceClientConfig;
public InferenceClient(IOptions<InferenceClientConfig> config)
{
_inferenceClientConfig = config.Value;
Start();
}
private void Start()
{
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = SecurityConstants.EXTERNAL_INFERENCE_PATH,
//Arguments = $"-e {credentials.Email} -p {credentials.Password} -f {apiConfig.ResourcesFolder}",
//RedirectStandardOutput = true,
//RedirectStandardError = true,
//CreateNoWindow = true
};
process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
process.Start();
}
catch (Exception e)
{
Console.WriteLine(e);
//throw;
}
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
_dealer.Connect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}");
}
public void Stop()
{
if (!_dealer.IsDisposed)
{
_dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit)));
_dealer.Close();
}
}
public void Send(RemoteCommand command)
{
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
}
public void SendString(string text) =>
Send(new RemoteCommand(CommandType.Load, MessagePackSerializer.Serialize(text)));
public T? Get<T>(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) where T : class
{
var bytes = GetBytes(retries, tryTimeoutSeconds, ct);
return bytes != null ? MessagePackSerializer.Deserialize<T>(bytes, cancellationToken: ct) : null;
}
public byte[]? GetBytes(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default)
{
var tryNum = 0;
while (!ct.IsCancellationRequested && tryNum++ < retries)
{
if (!_dealer.TryReceiveFrameBytes(TimeSpan.FromSeconds(tryTimeoutSeconds), out var bytes))
continue;
return bytes;
}
if (!ct.IsCancellationRequested)
throw new Exception($"Unable to get bytes after {tryNum} retries, {tryTimeoutSeconds} seconds each");
return null;
}
}
@@ -1,22 +0,0 @@
using Azaion.CommonSecurity.DTO.Commands;
using Microsoft.Extensions.DependencyInjection;
namespace Azaion.CommonSecurity.Services;
public interface IResourceLoader
{
MemoryStream LoadFile(string fileName, string? folder = null);
}
public class ResourceLoader([FromKeyedServices(SecurityConstants.EXTERNAL_INFERENCE_PATH)] IInferenceClient inferenceClient) : IResourceLoader
{
public MemoryStream LoadFile(string fileName, string? folder = null)
{
inferenceClient.Send(RemoteCommand.Create(CommandType.Load, new LoadFileData(fileName, folder)));
var bytes = inferenceClient.GetBytes();
if (bytes == null)
throw new Exception($"Unable to receive {fileName}");
return new MemoryStream(bytes);
}
}
+12 -2
View File
@@ -7,6 +7,16 @@
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<VersionDate>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd"))</VersionDate>
<VersionSeconds>$([System.Convert]::ToInt32($([System.DateTime]::UtcNow.TimeOfDay.TotalMinutes)))</VersionSeconds>
<AssemblyVersion>$(VersionDate).$(VersionSeconds)</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<InformationalVersion>$(AssemblyVersion)</InformationalVersion>
<Copyright>Copyright @ $([System.DateTime]::UtcNow.ToString("yyyy")) Azaion LLC. All rights reserved.</Copyright>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<Page Update="DatasetExplorer.xaml"> <Page Update="DatasetExplorer.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
@@ -16,8 +26,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" />
<PackageReference Include="ScottPlot.WPF" Version="5.0.46" /> <PackageReference Include="ScottPlot.WPF" Version="5.0.46" />
<PackageReference Include="VirtualizingWrapPanel" Version="2.1.0" /> <PackageReference Include="VirtualizingWrapPanel" Version="2.1.0" />
</ItemGroup> </ItemGroup>
@@ -0,0 +1,43 @@
<UserControl x:Class="Azaion.Dataset.Controls.ClassDistribution"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:controls="clr-namespace:Azaion.Dataset.Controls"
xmlns:dto="clr-namespace:Azaion.Common.DTO;assembly=Azaion.Common"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400"
FontFamily="Segoe UI">
<UserControl.Resources>
<controls:ProportionToWidthConverter x:Key="ProportionToWidthConverter"/>
</UserControl.Resources>
<ListView ItemsSource="{Binding Items, RelativeSource={RelativeSource AncestorType=controls:ClassDistribution}}"
BorderThickness="0" Background="#FF333333" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="Margin" Value="0,1"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type dto:ClusterDistribution}">
<Grid Height="18" x:Name="ItemGrid"> <!-- Give the Grid a name -->
<Border HorizontalAlignment="Left" VerticalAlignment="Stretch">
<Border.Width>
<MultiBinding Converter="{StaticResource ProportionToWidthConverter}">
<Binding Path="BarWidth"/>
<Binding Path="ActualWidth" ElementName="ItemGrid"/>
</MultiBinding>
</Border.Width>
<Border.Background>
<SolidColorBrush Color="{Binding Color}" Opacity="0.5"/>
</Border.Background>
</Border>
<TextBlock Text="{Binding Label}" VerticalAlignment="Center" Margin="5,0,0,0" Foreground="White" FontSize="12"/>
<TextBlock Text="{Binding ClassCount}" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,5,0" Foreground="White" FontSize="12"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</UserControl>
@@ -0,0 +1,22 @@
using System.Windows;
using System.Windows.Controls;
using Azaion.Common.DTO;
namespace Azaion.Dataset.Controls;
public partial class ClassDistribution : UserControl
{
public static readonly DependencyProperty ItemsProperty =
DependencyProperty.Register(nameof(Items), typeof(IEnumerable<ClusterDistribution>), typeof(ClassDistribution), new PropertyMetadata(null));
public IEnumerable<ClusterDistribution> Items
{
get => (IEnumerable<ClusterDistribution>)GetValue(ItemsProperty);
set => SetValue(ItemsProperty, value);
}
public ClassDistribution()
{
InitializeComponent();
}
}
@@ -0,0 +1,31 @@
using System.Globalization;
using System.Windows.Data;
namespace Azaion.Dataset.Controls
{
public class ProportionToWidthConverter : IMultiValueConverter
{
private const double MinPixelBarWidth = 2.0;
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values == null || values.Length < 2 ||
!(values[0] is double proportion) ||
!(values[1] is double containerActualWidth))
return MinPixelBarWidth; // Default or fallback width
if (containerActualWidth <= 0 || !double.IsFinite(containerActualWidth) || double.IsNaN(containerActualWidth))
return MinPixelBarWidth; // Container not ready or invalid
double calculatedWidth = proportion * containerActualWidth;
if (proportion >= 0 && calculatedWidth < MinPixelBarWidth)
return MinPixelBarWidth;
return Math.Max(0, calculatedWidth); // Ensure width is not negative
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) =>
[value];
}
}
+15 -3
View File
@@ -4,9 +4,9 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel" xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel"
xmlns:scottPlot="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF"
xmlns:controls="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common" xmlns:controls="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common"
xmlns:dto="clr-namespace:Azaion.Common.DTO;assembly=Azaion.Common" xmlns:dto="clr-namespace:Azaion.Common.DTO;assembly=Azaion.Common"
xmlns:controls1="clr-namespace:Azaion.Dataset.Controls"
mc:Ignorable="d" mc:Ignorable="d"
Title="Переглядач анотацій" Height="900" Width="1200" Title="Переглядач анотацій" Height="900" Width="1200"
WindowState="Maximized"> WindowState="Maximized">
@@ -30,7 +30,8 @@
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="32"></RowDefinition> <RowDefinition Height="18"></RowDefinition>
<RowDefinition Height="14"></RowDefinition>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Image <Image
Grid.Row="0" Grid.Row="0"
@@ -42,6 +43,17 @@
Grid.Row="1" Grid.Row="1"
Foreground="LightGray" Foreground="LightGray"
Text="{Binding ImageName}" /> Text="{Binding ImageName}" />
<TextBlock
Grid.Row="2"
HorizontalAlignment="Right"
Foreground="Gray">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}: {1}">
<Binding Mode="OneWay" Path="CreatedDate"></Binding>
<Binding Mode="OneWay" Path="CreatedEmail"></Binding>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid> </Grid>
</Border> </Border>
</DataTemplate> </DataTemplate>
@@ -92,7 +104,7 @@
</controls:CanvasEditor> </controls:CanvasEditor>
</TabItem> </TabItem>
<TabItem Name="ClassDistributionTab" Header="Розподіл класів"> <TabItem Name="ClassDistributionTab" Header="Розподіл класів">
<scottPlot:WpfPlot x:Name="ClassDistribution" /> <controls1:ClassDistribution x:Name="ClassDistributionPlot"/>
</TabItem> </TabItem>
</TabControl> </TabControl>
<StatusBar <StatusBar
+40 -55
View File
@@ -6,13 +6,12 @@ using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.Events; using Azaion.Common.Events;
using Azaion.Common.Extensions;
using Azaion.Common.Services; using Azaion.Common.Services;
using LinqToDB; using LinqToDB;
using MediatR; using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using ScottPlot;
using Color = ScottPlot.Color;
namespace Azaion.Dataset; namespace Azaion.Dataset;
@@ -35,6 +34,7 @@ public partial class DatasetExplorer
private readonly IMediator _mediator; private readonly IMediator _mediator;
public readonly List<DetectionClass> AnnotationsClasses; public readonly List<DetectionClass> AnnotationsClasses;
private IAzaionApi _azaionApi;
public bool ThumbnailLoading { get; set; } public bool ThumbnailLoading { get; set; }
@@ -48,7 +48,8 @@ public partial class DatasetExplorer
IGalleryService galleryService, IGalleryService galleryService,
FormState formState, FormState formState,
IDbFactory dbFactory, IDbFactory dbFactory,
IMediator mediator) IMediator mediator,
IAzaionApi azaionApi)
{ {
InitializeComponent(); InitializeComponent();
@@ -58,6 +59,7 @@ public partial class DatasetExplorer
_galleryService = galleryService; _galleryService = galleryService;
_dbFactory = dbFactory; _dbFactory = dbFactory;
_mediator = mediator; _mediator = mediator;
_azaionApi = azaionApi;
var photoModes = Enum.GetValues(typeof(PhotoMode)).Cast<PhotoMode>().ToList(); var photoModes = Enum.GetValues(typeof(PhotoMode)).Cast<PhotoMode>().ToList();
_annotationsDict = _annotationConfig.DetectionClasses.SelectMany(cls => photoModes.Select(mode => (int)mode + cls.Id)) _annotationsDict = _annotationConfig.DetectionClasses.SelectMany(cls => photoModes.Select(mode => (int)mode + cls.Id))
@@ -86,6 +88,7 @@ public partial class DatasetExplorer
ThumbnailsView.SelectionChanged += (_, _) => ThumbnailsView.SelectionChanged += (_, _) =>
{ {
StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {SelectedAnnotations.Count}"; StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {SelectedAnnotations.Count}";
ValidateBtn.Visibility = ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>().Any(x => x.IsSeed) ValidateBtn.Visibility = ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>().Any(x => x.IsSeed)
? Visibility.Visible ? Visibility.Visible
: Visibility.Hidden; : Visibility.Hidden;
@@ -101,20 +104,6 @@ public partial class DatasetExplorer
new List<DetectionClass> { new() {Id = -1, Name = "All", ShortName = "All"}} new List<DetectionClass> { new() {Id = -1, Name = "All", ShortName = "All"}}
.Concat(_annotationConfig.DetectionClasses)); .Concat(_annotationConfig.DetectionClasses));
LvClasses.Init(AllDetectionClasses); LvClasses.Init(AllDetectionClasses);
_dbFactory.Run(async db =>
{
var allAnnotations = await db.Annotations
.LoadWith(x => x.Detections)
.OrderBy(x => x.AnnotationStatus)
.ThenByDescending(x => x.CreatedDate)
.ToListAsync();
foreach (var annotation in allAnnotations)
AddAnnotationToDict(annotation);
}).GetAwaiter().GetResult();
DataContext = this;
} }
private async void OnLoaded(object sender, RoutedEventArgs e) private async void OnLoaded(object sender, RoutedEventArgs e)
@@ -132,8 +121,17 @@ public partial class DatasetExplorer
ExplorerEditor.CurrentAnnClass = LvClasses.CurrentDetectionClass ?? _annotationConfig.DetectionClasses.First(); 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(); await ReloadThumbnails();
await LoadClassDistribution(); LoadClassDistribution();
DataContext = this; DataContext = this;
} }
@@ -145,47 +143,29 @@ public partial class DatasetExplorer
_annotationsDict[-1][annotation.Name] = annotation; _annotationsDict[-1][annotation.Name] = annotation;
} }
private async Task LoadClassDistribution() private void LoadClassDistribution()
{ {
var data = _annotationsDict var data = _annotationsDict
.Where(x => x.Key != -1) .Where(x => x.Key != -1)
.Select(gr => new .OrderBy(x => x.Key)
.Select(gr => new ClusterDistribution
{ {
gr.Key, Label = $"{_annotationConfig.DetectionClassesDict[gr.Key].UIName}: {gr.Value.Count}",
_annotationConfig.DetectionClassesDict[gr.Key].ShortName, Color = _annotationConfig.DetectionClassesDict[gr.Key].Color,
_annotationConfig.DetectionClassesDict[gr.Key].Color,
ClassCount = gr.Value.Count ClassCount = gr.Value.Count
}) })
.Where(x => x.ClassCount > 0)
.ToList(); .ToList();
var foregroundColor = Color.FromColor(System.Drawing.Color.Black); var maxClassCount = Math.Max(1, data.Max(x => x.ClassCount));
var bars = data.Select(x => new Bar foreach (var cl in data)
{ {
Orientation = Orientation.Horizontal, cl.Color = cl.Color.CreateTransparent(150);
Position = -1.5 * x.Key + 1, cl.BarWidth = Math.Clamp(cl.ClassCount / (double)maxClassCount, 0, 1);
Label = x.ClassCount > 200 ? x.ClassCount.ToString() : "",
FillColor = new Color(x.Color.R, x.Color.G, x.Color.B, x.Color.A),
Value = x.ClassCount,
CenterLabel = true,
LabelOffset = 10
}).ToList();
ClassDistribution.Plot.Add.Bars(bars);
foreach (var x in data)
{
var label = ClassDistribution.Plot.Add.Text(x.ShortName, 50, -1.5 * x.Key + 1.1);
label.LabelFontColor = foregroundColor;
label.LabelFontSize = 18;
} }
ClassDistribution.Plot.Axes.AutoScale(); ClassDistributionPlot.Items = data;
ClassDistribution.Plot.HideAxesAndGrid();
ClassDistribution.Plot.FigureBackground.Color = new("#888888");
ClassDistribution.Refresh();
await Task.CompletedTask;
} }
private async void RefreshThumbnailsBtnClick(object sender, RoutedEventArgs e) private async void RefreshThumbnailsBtnClick(object sender, RoutedEventArgs e)
@@ -271,10 +251,9 @@ public partial class DatasetExplorer
if (result != MessageBoxResult.Yes) if (result != MessageBoxResult.Yes)
return; return;
var annotations = ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>().Select(x => x.Annotation) var annotationNames = ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>().Select(x => x.Annotation.Name).ToList();
.ToList();
await _mediator.Publish(new AnnotationsDeletedEvent(annotations)); await _mediator.Publish(new AnnotationsDeletedEvent(annotationNames));
ThumbnailsView.SelectedIndex = Math.Min(SelectedAnnotations.Count, tempSelected); ThumbnailsView.SelectedIndex = Math.Min(SelectedAnnotations.Count, tempSelected);
} }
@@ -282,12 +261,18 @@ public partial class DatasetExplorer
{ {
SelectedAnnotations.Clear(); SelectedAnnotations.Clear();
SelectedAnnotationDict.Clear(); SelectedAnnotationDict.Clear();
var annotations = _annotationsDict[ExplorerEditor.CurrentAnnClass.YoloId]; var annThumbnails = _annotationsDict[ExplorerEditor.CurrentAnnClass.YoloId]
foreach (var ann in annotations.OrderByDescending(x => x.Value.CreatedDate)) .Select(x => new AnnotationThumbnail(x.Value, _azaionApi.CurrentUser.Role.IsValidator()))
.OrderBy(x => !x.IsSeed)
.ThenByDescending(x =>x.Annotation.CreatedDate);
//var dict = annThumbnails.Take(20).ToDictionary(x => x.Annotation.Name, x => x.IsSeed);
foreach (var thumb in annThumbnails)
{ {
var annThumb = new AnnotationThumbnail(ann.Value); SelectedAnnotations.Add(thumb);
SelectedAnnotations.Add(annThumb); SelectedAnnotationDict.Add(thumb.Annotation.Name, thumb);
SelectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb);
} }
await Task.CompletedTask; await Task.CompletedTask;
} }
+39 -14
View File
@@ -1,17 +1,18 @@
using System.IO; using System.Windows.Input;
using System.Windows; using Azaion.Common.Database;
using System.Windows.Input;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Queue;
using Azaion.Common.Events; using Azaion.Common.Events;
using Azaion.Common.Services; using Azaion.Common.Services;
using MediatR; using MediatR;
using Microsoft.Extensions.Logging;
namespace Azaion.Dataset; namespace Azaion.Dataset;
public class DatasetExplorerEventHandler( public class DatasetExplorerEventHandler(
ILogger<DatasetExplorerEventHandler> logger,
DatasetExplorer datasetExplorer, DatasetExplorer datasetExplorer,
AnnotationService annotationService) : IAnnotationService annotationService,
IAzaionApi azaionApi) :
INotificationHandler<KeyEvent>, INotificationHandler<KeyEvent>,
INotificationHandler<DatasetExplorerControlEvent>, INotificationHandler<DatasetExplorerControlEvent>,
INotificationHandler<AnnotationCreatedEvent>, INotificationHandler<AnnotationCreatedEvent>,
@@ -24,7 +25,9 @@ public class DatasetExplorerEventHandler(
{ Key.X, PlaybackControlEnum.RemoveAllAnns }, { Key.X, PlaybackControlEnum.RemoveAllAnns },
{ Key.Escape, PlaybackControlEnum.Close }, { Key.Escape, PlaybackControlEnum.Close },
{ Key.Down, PlaybackControlEnum.Next }, { Key.Down, PlaybackControlEnum.Next },
{ Key.PageDown, PlaybackControlEnum.Next },
{ Key.Up, PlaybackControlEnum.Previous }, { Key.Up, PlaybackControlEnum.Previous },
{ Key.PageUp, PlaybackControlEnum.Previous },
{ Key.V, PlaybackControlEnum.ValidateAnnotations}, { Key.V, PlaybackControlEnum.ValidateAnnotations},
}; };
@@ -95,41 +98,56 @@ public class DatasetExplorerEventHandler(
var annotations = datasetExplorer.ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>() var annotations = datasetExplorer.ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>()
.Select(x => x.Annotation) .Select(x => x.Annotation)
.ToList(); .ToList();
foreach (var annotation in annotations) await annotationService.ValidateAnnotations(annotations.Select(x => x.Name).ToList(), token: cancellationToken);
await annotationService.ValidateAnnotation(annotation, cancellationToken); foreach (var ann in datasetExplorer.SelectedAnnotations.Where(x => annotations.Contains(x.Annotation)))
{
ann.Annotation.AnnotationStatus = AnnotationStatus.Validated;
if (datasetExplorer.SelectedAnnotationDict.TryGetValue(ann.Annotation.Name, out var value))
value.Annotation.AnnotationStatus = AnnotationStatus.Validated;
ann.UpdateUI();
}
break; break;
} }
} }
public async Task Handle(AnnotationCreatedEvent notification, CancellationToken cancellationToken) public Task Handle(AnnotationCreatedEvent notification, CancellationToken cancellationToken)
{
datasetExplorer.Dispatcher.Invoke(() =>
{ {
var annotation = notification.Annotation; var annotation = notification.Annotation;
var selectedClass = datasetExplorer.LvClasses.CurrentClassNumber; var selectedClass = datasetExplorer.LvClasses.CurrentClassNumber;
//TODO: For editing existing need to handle updates
datasetExplorer.AddAnnotationToDict(annotation); datasetExplorer.AddAnnotationToDict(annotation);
if (annotation.Classes.Contains(selectedClass) || selectedClass == -1) if (annotation.Classes.Contains(selectedClass) || selectedClass == -1)
{ {
var annThumb = new AnnotationThumbnail(annotation); var index = 0;
var annThumb = new AnnotationThumbnail(annotation, azaionApi.CurrentUser.Role.IsValidator());
if (datasetExplorer.SelectedAnnotationDict.ContainsKey(annThumb.Annotation.Name)) if (datasetExplorer.SelectedAnnotationDict.ContainsKey(annThumb.Annotation.Name))
{ {
datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name); datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name);
var ann = datasetExplorer.SelectedAnnotations.FirstOrDefault(x => x.Annotation.Name == annThumb.Annotation.Name); var ann = datasetExplorer.SelectedAnnotations.FirstOrDefault(x => x.Annotation.Name == annThumb.Annotation.Name);
if (ann != null) if (ann != null)
{
index = datasetExplorer.SelectedAnnotations.IndexOf(ann);
datasetExplorer.SelectedAnnotations.Remove(ann); datasetExplorer.SelectedAnnotations.Remove(ann);
} }
}
datasetExplorer.SelectedAnnotations.Insert(0, annThumb); datasetExplorer.SelectedAnnotations.Insert(index, annThumb);
datasetExplorer.SelectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb); datasetExplorer.SelectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb);
} }
await Task.CompletedTask; });
return Task.CompletedTask;
} }
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken)
{ {
var names = notification.Annotations.Select(x => x.Name).ToList(); try
{
datasetExplorer.Dispatcher.Invoke(() =>
{
var annThumbs = datasetExplorer.SelectedAnnotationDict var annThumbs = datasetExplorer.SelectedAnnotationDict
.Where(x => names.Contains(x.Key)) .Where(x => notification.AnnotationNames.Contains(x.Key))
.Select(x => x.Value) .Select(x => x.Value)
.ToList(); .ToList();
foreach (var annThumb in annThumbs) foreach (var annThumb in annThumbs)
@@ -137,6 +155,13 @@ public class DatasetExplorerEventHandler(
datasetExplorer.SelectedAnnotations.Remove(annThumb); datasetExplorer.SelectedAnnotations.Remove(annThumb);
datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name); datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name);
} }
});
}
catch (Exception e)
{
logger.LogError(e, e.Message);
throw;
}
await Task.CompletedTask; await Task.CompletedTask;
} }
} }
-2
View File
@@ -14,5 +14,3 @@ cdef class Annotation:
cdef format_time(self, ms) cdef format_time(self, ms)
cdef bytes serialize(self) cdef bytes serialize(self)
cdef to_str(self, class_names)
+2 -2
View File
@@ -32,12 +32,12 @@ cdef class Annotation:
d.annotation_name = self.name d.annotation_name = self.name
self.image = b'' self.image = b''
cdef to_str(self, class_names): def __str__(self):
if not self.detections: if not self.detections:
return f"{self.name}: No detections" return f"{self.name}: No detections"
detections_str = ", ".join( detections_str = ", ".join(
f"{class_names[d.cls]} {d.confidence * 100:.1f}% ({d.x:.2f}, {d.y:.2f})" f"class: {d.cls} {d.confidence * 100:.1f}% ({d.x:.2f}, {d.y:.2f})"
for d in self.detections for d in self.detections
) )
return f"{self.name}: {detections_str}" return f"{self.name}: {detections_str}"
-17
View File
@@ -1,17 +0,0 @@
from user cimport User
from credentials cimport Credentials
cdef class ApiClient:
cdef Credentials credentials
cdef str token, folder, api_url
cdef User user
cdef set_credentials(self, Credentials credentials)
cdef login(self)
cdef set_token(self, str token)
cdef get_user(self)
cdef load_bytes(self, str filename, str folder=*)
cdef upload_file(self, str filename, str folder=*)
cdef load_ai_model(self, bint is_tensor=*)
-132
View File
@@ -1,132 +0,0 @@
import json
from http import HTTPStatus
from uuid import UUID
import jwt
import requests
cimport constants
from hardware_service cimport HardwareService, HardwareInfo
from security cimport Security
from io import BytesIO
from user cimport User, RoleEnum
cdef class ApiClient:
"""Handles API authentication and downloading of the AI model."""
def __init__(self):
self.credentials = None
self.user = None
self.token = None
cdef set_credentials(self, Credentials credentials):
self.credentials = credentials
cdef login(self):
response = requests.post(f"{constants.API_URL}/login",
json={"email": self.credentials.email, "password": self.credentials.password})
response.raise_for_status()
token = response.json()["token"]
self.set_token(token)
cdef set_token(self, str token):
self.token = token
claims = jwt.decode(token, options={"verify_signature": False})
try:
id = str(UUID(claims.get("nameid", "")))
except ValueError:
raise ValueError("Invalid GUID format in claims")
email = claims.get("unique_name", "")
role_str = claims.get("role", "")
if role_str == "ApiAdmin":
role = RoleEnum.ApiAdmin
elif role_str == "Admin":
role = RoleEnum.Admin
elif role_str == "ResourceUploader":
role = RoleEnum.ResourceUploader
elif role_str == "Validator":
role = RoleEnum.Validator
elif role_str == "Operator":
role = RoleEnum.Operator
else:
role = RoleEnum.NONE
self.user = User(id, email, role)
cdef get_user(self):
if self.user is None:
self.login()
return self.user
cdef upload_file(self, str filename, str folder=None):
folder = folder or self.credentials.folder
if self.token is None:
self.login()
url = f"{constants.API_URL}/resources/{folder}"
headers = { "Authorization": f"Bearer {self.token}" }
files = dict(data=open(<str>filename, 'rb'))
try:
r = requests.post(url, headers=headers, files=files, allow_redirects=True)
r.raise_for_status()
print(f"Upload success: {r.status_code}")
except Exception as e:
print(f"Upload fail: {e}")
cdef load_bytes(self, str filename, str folder=None):
folder = folder or self.credentials.folder
hardware_service = HardwareService()
cdef HardwareInfo hardware = hardware_service.get_hardware_info()
if self.token is None:
self.login()
url = f"{constants.API_URL}/resources/get/{folder}"
headers = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
payload = json.dumps(
{
"password": self.credentials.password,
"hardware": hardware.to_json_object(),
"fileName": filename
}, indent=4)
response = requests.post(url, data=payload, headers=headers, stream=True)
if response.status_code == HTTPStatus.UNAUTHORIZED or response.status_code == HTTPStatus.FORBIDDEN:
self.login()
headers = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
response = requests.post(url, data=payload, headers=headers, stream=True)
if response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR:
print('500!')
hw_hash = Security.get_hw_hash(hardware)
key = Security.get_api_encryption_key(self.credentials, hw_hash)
resp_bytes = response.raw.read()
data = Security.decrypt_to(resp_bytes, key)
constants.log(<str>f'Downloaded file: {filename}, {len(data)} bytes')
return data
cdef load_ai_model(self, bint is_tensor=False):
if is_tensor:
big_file = <str> constants.AI_TENSOR_MODEL_FILE_BIG
small_file = <str> constants.AI_TENSOR_MODEL_FILE_SMALL
else:
big_file = <str>constants.AI_ONNX_MODEL_FILE_BIG
small_file = <str> constants.AI_ONNX_MODEL_FILE_SMALL
with open(big_file, 'rb') as binary_file:
encrypted_bytes_big = binary_file.read()
print('read encrypted big file')
print(f'small file: {small_file}')
encrypted_bytes_small = self.load_bytes(small_file)
print('read encrypted small file')
encrypted_model_bytes = encrypted_bytes_small + encrypted_bytes_big
key = Security.get_model_encryption_key()
model_bytes = Security.decrypt_to(encrypted_model_bytes, key)
return model_bytes
+10 -8
View File
@@ -1,13 +1,11 @@
# -*- mode: python ; coding: utf-8 -*- # -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_submodules
from PyInstaller.utils.hooks import collect_all from PyInstaller.utils.hooks import collect_all
datas = [] datas = [('venv\\Lib\\site-packages\\cv2', 'cv2')]
binaries = [] binaries = []
hiddenimports = ['constants', 'annotation', 'credentials', 'file_data', 'user', 'security', 'secure_model', 'api_client', 'hardware_service', 'remote_command', 'ai_config', 'inference_engine', 'inference', 'remote_command_handler'] hiddenimports = ['constants', 'file_data', 'remote_command', 'remote_command_handler', 'annotation', 'loader_client', 'ai_config', 'tensorrt_engine', 'onnx_engine', 'inference_engine', 'inference', 'main-inf']
tmp_ret = collect_all('jwt') hiddenimports += collect_submodules('cv2')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('requests')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('psutil') tmp_ret = collect_all('psutil')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('msgpack') tmp_ret = collect_all('msgpack')
@@ -16,7 +14,7 @@ tmp_ret = collect_all('zmq')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('cryptography') tmp_ret = collect_all('cryptography')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('cv2') tmp_ret = collect_all('numpy')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('onnxruntime') tmp_ret = collect_all('onnxruntime')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
@@ -24,7 +22,11 @@ tmp_ret = collect_all('tensorrt')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('pycuda') tmp_ret = collect_all('pycuda')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('re') tmp_ret = collect_all('pynvml')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('jwt')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('loguru')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
-28
View File
@@ -1,28 +0,0 @@
pyinstaller --name=azaion-inference ^
--collect-all pyyaml ^
--collect-all jwt ^
--collect-all requests ^
--collect-all psutil ^
--collect-all msgpack ^
--collect-all zmq ^
--collect-all cryptography ^
--collect-all cv2 ^
--collect-all onnxruntime ^
--collect-all tensorrt ^
--collect-all pycuda ^
--collect-all re ^
--hidden-import constants ^
--hidden-import annotation ^
--hidden-import credentials ^
--hidden-import file_data ^
--hidden-import user ^
--hidden-import security ^
--hidden-import secure_model ^
--hidden-import api_client ^
--hidden-import hardware_service ^
--hidden-import remote_command ^
--hidden-import ai_config ^
--hidden-import inference_engine ^
--hidden-import inference ^
--hidden-import remote_command_handler ^
start.py
+61
View File
@@ -0,0 +1,61 @@
echo Build Cython app
set CURRENT_DIR=%cd%
REM Change to the parent directory of the current location
cd /d %~dp0
echo remove dist folder:
if exist dist rmdir dist /s /q
if exist build rmdir build /s /q
echo install python and dependencies
if not exist venv (
python -m venv venv
)
venv\Scripts\python -m pip install --upgrade pip
venv\Scripts\pip install -r requirements.txt
venv\Scripts\pip install --upgrade pyinstaller pyinstaller-hooks-contrib
venv\Scripts\python setup.py build_ext --inplace
echo install azaion-inference
venv\Scripts\pyinstaller --name=azaion-inference ^
--collect-submodules cv2 ^
--add-data "venv\Lib\site-packages\cv2;cv2" ^
--collect-all psutil ^
--collect-all msgpack ^
--collect-all zmq ^
--collect-all cryptography ^
--collect-all numpy ^
--collect-all onnxruntime ^
--collect-all tensorrt ^
--collect-all pycuda ^
--collect-all pynvml ^
--collect-all jwt ^
--collect-all loguru ^
--hidden-import constants ^
--hidden-import file_data ^
--hidden-import remote_command ^
--hidden-import remote_command_handler ^
--hidden-import annotation ^
--hidden-import loader_client ^
--hidden-import ai_config ^
--hidden-import tensorrt_engine ^
--hidden-import onnx_engine ^
--hidden-import inference_engine ^
--hidden-import inference ^
--hidden-import main-inf ^
start.py
robocopy "dist\azaion-inference\_internal" "..\dist-azaion\_internal" "ai_config.cp312-win_amd64.pyd" "annotation.cp312-win_amd64.pyd"
robocopy "dist\azaion-inference\_internal" "..\dist-azaion\_internal" "constants.cp312-win_amd64.pyd" "file_data.cp312-win_amd64.pyd"
robocopy "dist\azaion-inference\_internal" "..\dist-azaion\_internal" "remote_command.cp312-win_amd64.pyd" "remote_command_handler.cp312-win_amd64.pyd"
robocopy "dist\azaion-inference\_internal" "..\dist-azaion\_internal" "inference.cp312-win_amd64.pyd" "inference_engine.cp312-win_amd64.pyd"
robocopy "dist\azaion-inference\_internal" "..\dist-azaion\_internal" "loader_client.cp312-win_amd64.pyd" "tensorrt_engine.cp312-win_amd64.pyd"
robocopy "dist\azaion-inference\_internal" "..\dist-azaion\_internal" "onnx_engine.cp312-win_amd64.pyd" "main_inference.cp312-win_amd64.pyd"
robocopy "dist\azaion-inference\_internal" "..\dist-dlls\_internal" /E
robocopy "dist\azaion-inference" "..\dist-azaion" "azaion-inference.exe"
cd /d %CURRENT_DIR%
-1
View File
@@ -1 +0,0 @@
zmq_port: 5131
-1
View File
@@ -1 +0,0 @@
zmq_port: 5127
+6 -9
View File
@@ -4,17 +4,14 @@ cdef int QUEUE_MAXSIZE # Maximum size of the command queue
cdef str COMMANDS_QUEUE # Name of the commands queue in rabbit cdef str COMMANDS_QUEUE # Name of the commands queue in rabbit
cdef str ANNOTATIONS_QUEUE # Name of the annotations queue in rabbit cdef str ANNOTATIONS_QUEUE # Name of the annotations queue in rabbit
cdef str API_URL # Base URL for the external API
cdef str QUEUE_CONFIG_FILENAME # queue config filename to load from api cdef str QUEUE_CONFIG_FILENAME # queue config filename to load from api
cdef str AI_ONNX_MODEL_FILE_BIG cdef str AI_ONNX_MODEL_FILE
cdef str AI_ONNX_MODEL_FILE_SMALL
cdef str AI_TENSOR_MODEL_FILE_BIG cdef str CDN_CONFIG
cdef str AI_TENSOR_MODEL_FILE_SMALL cdef str MODELS_FOLDER
cdef int SMALL_SIZE_KB
cdef bytes DONE_SIGNAL cdef log(str log_message)
cdef logerror(str error)
cdef log(str log_message, bytes client_id=*)
+36 -14
View File
@@ -1,21 +1,43 @@
import time import sys
from loguru import logger
cdef str CONFIG_FILE = "config.yaml" # Port for the zmq cdef str CONFIG_FILE = "config.yaml" # Port for the zmq
cdef int QUEUE_MAXSIZE = 1000 # Maximum size of the command queue
cdef str COMMANDS_QUEUE = "azaion-commands"
cdef str ANNOTATIONS_QUEUE = "azaion-annotations"
cdef str API_URL = "https://api.azaion.com" # Base URL for the external API
cdef str QUEUE_CONFIG_FILENAME = "secured-config.json" cdef str QUEUE_CONFIG_FILENAME = "secured-config.json"
cdef str AI_ONNX_MODEL_FILE = "azaion.onnx"
cdef str AI_ONNX_MODEL_FILE_BIG = "azaion.onnx.big" cdef str CDN_CONFIG = "cdn.yaml"
cdef str AI_ONNX_MODEL_FILE_SMALL = "azaion.onnx.small" cdef str MODELS_FOLDER = "models"
cdef str AI_TENSOR_MODEL_FILE_BIG = "azaion.engine.big" cdef int SMALL_SIZE_KB = 3
cdef str AI_TENSOR_MODEL_FILE_SMALL = "azaion.engine.small"
cdef log(str log_message, bytes client_id=None): logger.remove()
local_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) log_format = "[{time:HH:mm:ss} {level}] {message}"
client_str = '' if client_id is None else f' {client_id}' logger.add(
print(f'[{local_time}{client_str}]: {log_message}') sink="Logs/log_inference_{time:YYYYMMDD}.txt",
level="INFO",
format=log_format,
enqueue=True,
rotation="1 day",
retention="30 days",
)
logger.add(
sys.stdout,
level="DEBUG",
format=log_format,
filter=lambda record: record["level"].name in ("INFO", "DEBUG", "SUCCESS"),
colorize=True
)
logger.add(
sys.stderr,
level="WARNING",
format=log_format,
colorize=True
)
cdef log(str log_message):
logger.info(log_message)
cdef logerror(str error):
logger.error(error)
+18
View File
@@ -4,3 +4,21 @@ cdef class FileData:
@staticmethod @staticmethod
cdef from_msgpack(bytes data) cdef from_msgpack(bytes data)
cdef bytes serialize(self)
cdef class UploadFileData(FileData):
cdef public bytes resource
@staticmethod
cdef from_msgpack(bytes data)
cdef bytes serialize(self)
cdef class FileList:
cdef public list[str] files
@staticmethod
cdef from_msgpack(bytes data)
cdef bytes serialize(self)
+40 -2
View File
@@ -1,7 +1,6 @@
from msgpack import unpackb from msgpack import unpackb, packb
cdef class FileData: cdef class FileData:
def __init__(self, str folder, str filename): def __init__(self, str folder, str filename):
self.folder = folder self.folder = folder
self.filename = filename self.filename = filename
@@ -12,3 +11,42 @@ cdef class FileData:
return FileData( return FileData(
unpacked.get("Folder"), unpacked.get("Folder"),
unpacked.get("Filename")) unpacked.get("Filename"))
cdef bytes serialize(self):
return packb({
"Folder": self.folder,
"Filename": self.filename
})
cdef class UploadFileData(FileData):
def __init__(self, bytes resource, str folder, str filename):
super().__init__(folder, filename)
self.resource = resource
@staticmethod
cdef from_msgpack(bytes data):
unpacked = unpackb(data, strict_map_key=False)
return UploadFileData(
unpacked.get("Resource"),
unpacked.get("Folder"),
unpacked.get("Filename"))
cdef bytes serialize(self):
return packb({
"Resource": self.resource,
"Folder": self.folder,
"Filename": self.filename
})
cdef class FileList:
def __init__(self, list[str] files):
self.files = files
@staticmethod
cdef from_msgpack(bytes data):
unpacked = unpackb(data, strict_map_key=False)
return FileList(unpacked.get("files"))
cdef bytes serialize(self):
return packb({ "files": self.files })
-11
View File
@@ -1,11 +0,0 @@
cdef class HardwareInfo:
cdef str cpu, gpu, memory, mac_address
cdef to_json_object(self)
cdef class HardwareService:
cdef bint is_windows
cdef get_mac_address(self, interface=*)
@staticmethod
cdef has_nvidia_gpu()
cdef HardwareInfo get_hardware_info(self)
-84
View File
@@ -1,84 +0,0 @@
import re
import subprocess
import psutil
cdef class HardwareInfo:
def __init__(self, str cpu, str gpu, str memory, str mac_address):
self.cpu = cpu
self.gpu = gpu
self.memory = memory
self.mac_address = mac_address
cdef to_json_object(self):
return {
"CPU": self.cpu,
"GPU": self.gpu,
"MacAddress": self.mac_address,
"Memory": self.memory
}
def __str__(self):
return f'CPU: {self.cpu}. GPU: {self.gpu}. Memory: {self.memory}. MAC Address: {self.mac_address}'
cdef class HardwareService:
"""Handles hardware information retrieval and hash generation."""
def __init__(self):
try:
res = subprocess.check_output("ver", shell=True).decode('utf-8')
if "Microsoft Windows" in res:
self.is_windows = True
else:
self.is_windows = False
except Exception:
print('Error during os type checking')
self.is_windows = False
cdef get_mac_address(self, interface="Ethernet"):
addresses = psutil.net_if_addrs()
for interface_name, interface_info in addresses.items():
if interface_name == interface:
for addr in interface_info:
if addr.family == psutil.AF_LINK:
return addr.address.replace('-', '')
return None
@staticmethod
cdef has_nvidia_gpu():
try:
output = subprocess.check_output(['nvidia-smi']).decode()
match = re.search(r'CUDA Version:\s*([\d.]+)', output)
if match:
return float(match.group(1)) > 11
return False
except Exception as e:
print(e)
return False
cdef HardwareInfo get_hardware_info(self):
if self.is_windows:
os_command = (
"powershell -Command \""
"Get-CimInstance -ClassName Win32_Processor | Select-Object -ExpandProperty Name | Write-Output; "
"Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty Name | Write-Output; "
"Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -ExpandProperty TotalVisibleMemorySize | Write-Output"
"\""
)
else:
os_command = (
"/bin/bash -c \" lscpu | grep 'Model name:' | cut -d':' -f2 && "
"lspci | grep VGA | cut -d':' -f3 && "
"free -g | grep Mem: | awk '{print $2}' && \""
)
# in case of subprocess error do:
# cdef bytes os_command_bytes = os_command.encode('utf-8')
# and use os_command_bytes
result = subprocess.check_output(os_command, shell=True).decode('utf-8')
lines = [line.strip() for line in result.splitlines() if line.strip()]
cdef str cpu = lines[0].replace("Name=", "").replace(" ", " ")
cdef str gpu = lines[1].replace("Name=", "").replace(" ", " ")
cdef str memory = lines[2].replace("TotalVisibleMemorySize=", "").replace(" ", " ")
cdef str mac_address = self.get_mac_address()
return HardwareInfo(cpu, gpu, memory, mac_address)
+6 -3
View File
@@ -1,23 +1,26 @@
from remote_command cimport RemoteCommand from remote_command cimport RemoteCommand
from annotation cimport Annotation, Detection from annotation cimport Annotation, Detection
from ai_config cimport AIRecognitionConfig from ai_config cimport AIRecognitionConfig
from api_client cimport ApiClient from loader_client cimport LoaderClient
from inference_engine cimport InferenceEngine from inference_engine cimport InferenceEngine
cdef class Inference: cdef class Inference:
cdef ApiClient api_client cdef LoaderClient loader_client
cdef InferenceEngine engine cdef InferenceEngine engine
cdef object on_annotation cdef object on_annotation
cdef Annotation _previous_annotation cdef Annotation _previous_annotation
cdef AIRecognitionConfig ai_config cdef AIRecognitionConfig ai_config
cdef object class_names
cdef bint stop_signal cdef bint stop_signal
cdef str model_input cdef str model_input
cdef int model_width cdef int model_width
cdef int model_height cdef int model_height
cdef build_tensor_engine(self, object updater_callback)
cdef init_ai(self)
cdef bint is_building_engine
cdef bint is_video(self, str filepath) cdef bint is_video(self, str filepath)
cdef run_inference(self, RemoteCommand cmd) cdef run_inference(self, RemoteCommand cmd)
cdef _process_video(self, RemoteCommand cmd, AIRecognitionConfig ai_config, str video_name) cdef _process_video(self, RemoteCommand cmd, AIRecognitionConfig ai_config, str video_name)
cdef _process_images(self, RemoteCommand cmd, AIRecognitionConfig ai_config, list[str] image_paths) cdef _process_images(self, RemoteCommand cmd, AIRecognitionConfig ai_config, list[str] image_paths)
+100 -26
View File
@@ -1,40 +1,119 @@
import json
import mimetypes import mimetypes
import subprocess import time
import cv2 import cv2
import numpy as np import numpy as np
cimport constants
from remote_command cimport RemoteCommand from remote_command cimport RemoteCommand
from annotation cimport Detection, Annotation from annotation cimport Detection, Annotation
from ai_config cimport AIRecognitionConfig from ai_config cimport AIRecognitionConfig
from inference_engine cimport OnnxEngine, TensorRTEngine import pynvml
from hardware_service cimport HardwareService
cdef int tensor_gpu_index
cdef int check_tensor_gpu_index():
try:
pynvml.nvmlInit()
deviceCount = pynvml.nvmlDeviceGetCount()
if deviceCount == 0:
constants.logerror('No NVIDIA GPUs found.')
return -1
for i in range(deviceCount):
handle = pynvml.nvmlDeviceGetHandleByIndex(i)
major, minor = pynvml.nvmlDeviceGetCudaComputeCapability(handle)
if major > 6 or (major == 6 and minor >= 1):
constants.log('found NVIDIA GPU!')
return i
constants.logerror('NVIDIA GPU doesnt support TensorRT!')
return -1
except pynvml.NVMLError:
return -1
finally:
try:
pynvml.nvmlShutdown()
except:
constants.logerror('Failed to shutdown pynvml cause probably no NVIDIA GPU')
pass
tensor_gpu_index = check_tensor_gpu_index()
if tensor_gpu_index > -1:
from tensorrt_engine import TensorRTEngine
else:
from onnx_engine import OnnxEngine
cdef class Inference: cdef class Inference:
def __init__(self, api_client, on_annotation): def __init__(self, loader_client, on_annotation):
self.api_client = api_client self.loader_client = loader_client
self.on_annotation = on_annotation self.on_annotation = on_annotation
self.stop_signal = False self.stop_signal = False
self.model_input = None self.model_input = None
self.model_width = 0 self.model_width = 0
self.model_height = 0 self.model_height = 0
self.engine = None self.engine = None
self.class_names = None self.is_building_engine = False
def init_ai(self): cdef build_tensor_engine(self, object updater_callback):
if tensor_gpu_index == -1:
return
try:
engine_filename = TensorRTEngine.get_engine_filename(0)
models_dir = constants.MODELS_FOLDER
self.is_building_engine = True
updater_callback('downloading')
res = self.loader_client.load_big_small_resource(engine_filename, models_dir)
if res.err is None:
constants.log('tensor rt engine is here, no need to build')
self.is_building_engine = False
updater_callback('enabled')
return
constants.logerror(res.err)
# time.sleep(8) # prevent simultaneously loading dll and models
updater_callback('converting')
constants.log('try to load onnx')
res = self.loader_client.load_big_small_resource(constants.AI_ONNX_MODEL_FILE, models_dir)
if res.err is not None:
updater_callback(f'Error. {res.err}')
model_bytes = TensorRTEngine.convert_from_onnx(res.data)
updater_callback('uploading')
res = self.loader_client.upload_big_small_resource(model_bytes, <str> engine_filename, models_dir)
if res.err is not None:
updater_callback(f'Error. {res.err}')
constants.log(f'uploaded {engine_filename} to CDN and API')
self.is_building_engine = False
updater_callback('enabled')
except Exception as e:
updater_callback(f'Error. {str(e)}')
cdef init_ai(self):
if self.engine is not None: if self.engine is not None:
return return
is_nvidia = HardwareService.has_nvidia_gpu() models_dir = constants.MODELS_FOLDER
if is_nvidia: if tensor_gpu_index > -1:
model_bytes = self.api_client.load_ai_model(is_tensor=True) while self.is_building_engine:
self.engine = TensorRTEngine(model_bytes, batch_size=4) time.sleep(1)
engine_filename = TensorRTEngine.get_engine_filename(0)
res = self.loader_client.load_big_small_resource(engine_filename, models_dir)
if res.err is not None:
raise Exception(res.err)
self.engine = TensorRTEngine(res.data)
else: else:
model_bytes = self.api_client.load_ai_model() res = self.loader_client.load_big_small_resource(constants.AI_ONNX_MODEL_FILE, models_dir)
self.engine = OnnxEngine(model_bytes, batch_size=4) if res.err is not None:
raise Exception(res.err)
self.engine = OnnxEngine(res.data)
self.model_height, self.model_width = self.engine.get_input_shape() self.model_height, self.model_width = self.engine.get_input_shape()
self.class_names = self.engine.get_class_names()
cdef preprocess(self, frames): cdef preprocess(self, frames):
blobs = [cv2.dnn.blobFromImage(frame, blobs = [cv2.dnn.blobFromImage(frame,
@@ -47,13 +126,11 @@ cdef class Inference:
return np.vstack(blobs) return np.vstack(blobs)
cdef postprocess(self, output, ai_config): cdef postprocess(self, output, ai_config):
print('enter postprocess')
cdef list[Detection] detections = [] cdef list[Detection] detections = []
cdef int ann_index cdef int ann_index
cdef float x1, y1, x2, y2, conf, cx, cy, w, h cdef float x1, y1, x2, y2, conf, cx, cy, w, h
cdef int class_id cdef int class_id
cdef list[list[Detection]] results = [] cdef list[list[Detection]] results = []
print('start try: code')
try: try:
for ann_index in range(len(output[0])): for ann_index in range(len(output[0])):
detections.clear() detections.clear()
@@ -127,7 +204,6 @@ cdef class Inference:
self.stop_signal = False self.stop_signal = False
self.init_ai() self.init_ai()
print(ai_config.paths)
for m in ai_config.paths: for m in ai_config.paths:
if self.is_video(m): if self.is_video(m):
videos.append(m) videos.append(m)
@@ -135,12 +211,12 @@ cdef class Inference:
images.append(m) images.append(m)
# images first, it's faster # images first, it's faster
if len(images) > 0: if len(images) > 0:
for chunk in self.split_list_extend(images, ai_config.model_batch_size): for chunk in self.split_list_extend(images, self.engine.get_batch_size()):
print(f'run inference on {" ".join(chunk)}...') constants.log(f'run inference on {" ".join(chunk)}...')
self._process_images(cmd, ai_config, chunk) self._process_images(cmd, ai_config, chunk)
if len(videos) > 0: if len(videos) > 0:
for v in videos: for v in videos:
print(f'run inference on {v}...') constants.log(f'run inference on {v}...')
self._process_video(cmd, ai_config, v) self._process_video(cmd, ai_config, v)
@@ -161,7 +237,7 @@ cdef class Inference:
batch_frames.append(frame) batch_frames.append(frame)
batch_timestamps.append(int(v_input.get(cv2.CAP_PROP_POS_MSEC))) batch_timestamps.append(int(v_input.get(cv2.CAP_PROP_POS_MSEC)))
if len(batch_frames) == ai_config.model_batch_size: if len(batch_frames) == self.engine.get_batch_size():
input_blob = self.preprocess(batch_frames) input_blob = self.preprocess(batch_frames)
outputs = self.engine.run(input_blob) outputs = self.engine.run(input_blob)
@@ -175,10 +251,9 @@ cdef class Inference:
annotation.image = image.tobytes() annotation.image = image.tobytes()
self._previous_annotation = annotation self._previous_annotation = annotation
print(annotation.to_str(self.class_names)) print(annotation)
self.on_annotation(cmd, annotation) self.on_annotation(cmd, annotation)
batch_frames.clear() batch_frames.clear()
batch_timestamps.clear() batch_timestamps.clear()
v_input.release() v_input.release()
@@ -203,7 +278,6 @@ cdef class Inference:
annotation = Annotation(image_paths[i], timestamps[i], detections) annotation = Annotation(image_paths[i], timestamps[i], detections)
_, image = cv2.imencode('.jpg', frames[i]) _, image = cv2.imencode('.jpg', frames[i])
annotation.image = image.tobytes() annotation.image = image.tobytes()
print(annotation.to_str(self.class_names))
self.on_annotation(cmd, annotation) self.on_annotation(cmd, annotation)
+1 -21
View File
@@ -6,24 +6,4 @@ cdef class InferenceEngine:
cdef public int batch_size cdef public int batch_size
cdef tuple get_input_shape(self) cdef tuple get_input_shape(self)
cdef int get_batch_size(self) cdef int get_batch_size(self)
cdef get_class_names(self) cdef run(self, input_data)
cpdef run(self, input_data)
cdef class OnnxEngine(InferenceEngine):
cdef object session
cdef list model_inputs
cdef str input_name
cdef object input_shape
cdef object class_names
cdef class TensorRTEngine(InferenceEngine):
cdef object stream
cdef object context
cdef str input_name
cdef str output_name
cdef object d_input
cdef object d_output
cdef object input_shape
cdef object output_shape
cdef object h_output
cdef object class_names
+1 -129
View File
@@ -1,13 +1,3 @@
import json
import struct
from typing import List, Tuple
import numpy as np
import onnxruntime as onnx
import tensorrt as trt
import pycuda.driver as cuda
import pycuda.autoinit # required for automatically initialize CUDA, do not remove.
cdef class InferenceEngine: cdef class InferenceEngine:
def __init__(self, model_bytes: bytes, batch_size: int = 1, **kwargs): def __init__(self, model_bytes: bytes, batch_size: int = 1, **kwargs):
self.batch_size = batch_size self.batch_size = batch_size
@@ -18,123 +8,5 @@ cdef class InferenceEngine:
cdef int get_batch_size(self): cdef int get_batch_size(self):
return self.batch_size return self.batch_size
cpdef run(self, input_data): cdef run(self, input_data):
raise NotImplementedError("Subclass must implement run") raise NotImplementedError("Subclass must implement run")
cdef get_class_names(self):
raise NotImplementedError("Subclass must implement get_class_names")
cdef class OnnxEngine(InferenceEngine):
def __init__(self, model_bytes: bytes, batch_size: int = 1, **kwargs):
super().__init__(model_bytes, batch_size)
self.batch_size = batch_size
self.session = onnx.InferenceSession(model_bytes, providers=["CUDAExecutionProvider", "CPUExecutionProvider"])
self.model_inputs = self.session.get_inputs()
self.input_name = self.model_inputs[0].name
self.input_shape = self.model_inputs[0].shape
if self.input_shape[0] != -1:
self.batch_size = self.input_shape[0]
print(f'AI detection model input: {self.model_inputs} {self.input_shape}')
model_meta = self.session.get_modelmeta()
print("Metadata:", model_meta.custom_metadata_map)
self.class_names = eval(model_meta.custom_metadata_map["names"])
cdef tuple get_input_shape(self):
shape = self.input_shape
return shape[2], shape[3]
cdef int get_batch_size(self):
return self.batch_size
cdef get_class_names(self):
return self.class_names
cpdef run(self, input_data):
return self.session.run(None, {self.input_name: input_data})
cdef class TensorRTEngine(InferenceEngine):
def __init__(self, model_bytes: bytes, batch_size: int = 4, **kwargs):
super().__init__(model_bytes, batch_size)
self.batch_size = batch_size
print('Enter init TensorRT')
try:
logger = trt.Logger(trt.Logger.WARNING)
metadata_len = struct.unpack("<I", model_bytes[:4])[0]
try:
metadata = json.loads(model_bytes[4:4 + metadata_len])
print(f"Model metadata: {json.dumps(metadata, indent=2)}")
string_dict = metadata['names']
self.class_names = {int(k): v for k, v in string_dict.items()}
except json.JSONDecodeError:
print(f"Failed to parse metadata")
return
engine_data = model_bytes[4 + metadata_len:]
runtime = trt.Runtime(logger)
engine = runtime.deserialize_cuda_engine(engine_data)
if engine is None:
raise RuntimeError(f"Failed to load TensorRT engine from bytes")
self.context = engine.create_execution_context()
# input
self.input_name = engine.get_tensor_name(0)
engine_input_shape = engine.get_tensor_shape(self.input_name)
if engine_input_shape[0] != -1:
self.batch_size = engine_input_shape[0]
self.input_shape = [
self.batch_size,
engine_input_shape[1], # Channels (usually fixed at 3 for RGB)
1280 if engine_input_shape[2] == -1 else engine_input_shape[2], # Height
1280 if engine_input_shape[3] == -1 else engine_input_shape[3] # Width
]
self.context.set_input_shape(self.input_name, self.input_shape)
input_size = trt.volume(self.input_shape) * np.dtype(np.float32).itemsize
self.d_input = cuda.mem_alloc(input_size)
# output
self.output_name = engine.get_tensor_name(1)
engine_output_shape = tuple(engine.get_tensor_shape(self.output_name))
self.output_shape = [
batch_size if self.input_shape[0] == -1 else self.input_shape[0],
300 if engine_output_shape[1] == -1 else engine_output_shape[1], # max detections number
6 if engine_output_shape[2] == -1 else engine_output_shape[2] # x1 y1 x2 y2 conf cls
]
self.h_output = cuda.pagelocked_empty(tuple(self.output_shape), dtype=np.float32)
self.d_output = cuda.mem_alloc(self.h_output.nbytes)
self.stream = cuda.Stream()
except Exception as e:
raise RuntimeError(f"Failed to initialize TensorRT engine: {str(e)}")
cdef tuple get_input_shape(self):
return self.input_shape[2], self.input_shape[3]
cdef int get_batch_size(self):
return self.batch_size
cdef get_class_names(self):
return self.class_names
cpdef run(self, input_data):
try:
cuda.memcpy_htod_async(self.d_input, input_data, self.stream)
self.context.set_tensor_address(self.input_name, int(self.d_input)) # input buffer
self.context.set_tensor_address(self.output_name, int(self.d_output)) # output buffer
self.context.execute_async_v3(stream_handle=self.stream.handle)
self.stream.synchronize()
# Fix: Remove the stream parameter from memcpy_dtoh
cuda.memcpy_dtoh(self.h_output, self.d_output)
output = self.h_output.reshape(self.output_shape)
return [output]
except Exception as e:
raise RuntimeError(f"Failed to run TensorRT inference: {str(e)}")
+18
View File
@@ -0,0 +1,18 @@
from remote_command cimport RemoteCommand
cdef class LoadResult:
cdef public str err
cdef public bytes data
cdef class LoaderClient:
cdef object _loader_context
cdef object _socket
cdef RemoteCommand _send_receive_command(self, RemoteCommand command)
cdef load_big_small_resource(self, str filename, str directory)
cdef upload_big_small_resource(self, bytes content, str filename, str directory)
cdef stop(self)
+44
View File
@@ -0,0 +1,44 @@
import zmq
from remote_command cimport RemoteCommand, CommandType
from file_data cimport FileData, UploadFileData
cdef class LoadResult:
def __init__(self, str err, bytes data=None):
self.err = err
self.data = data
cdef class LoaderClient:
def __init__(self, str zmq_host, int zmq_port):
self._loader_context = zmq.Context()
self._socket = self._loader_context.socket(zmq.DEALER)
self._socket.connect(f'tcp://{zmq_host}:{zmq_port}')
cdef RemoteCommand _send_receive_command(self, RemoteCommand command):
self._socket.send(command.serialize())
return RemoteCommand.from_msgpack(self._socket.recv())
cdef load_big_small_resource(self, str filename, str directory):
cdef FileData file_data = FileData(folder=directory, filename=filename)
cdef RemoteCommand response = self._send_receive_command(RemoteCommand(CommandType.LOAD_BIG_SMALL, data=file_data.serialize()))
if response.command_type == CommandType.DATA_BYTES:
return LoadResult(None, response.data)
elif response.command_type == CommandType.ERROR:
return LoadResult(f"Error from server: {response.message}")
else:
return LoadResult(f"Unexpected response command type: {response.command_type}")
cdef upload_big_small_resource(self, bytes content, str filename, str directory):
cdef UploadFileData upload_file_data = UploadFileData(content, folder=directory, filename=filename)
cdef RemoteCommand upload_resp = self._send_receive_command(RemoteCommand(CommandType.UPLOAD_BIG_SMALL, data=upload_file_data.serialize()))
if upload_resp.command_type == CommandType.OK:
return LoadResult(None, None)
elif upload_resp.command_type == CommandType.ERROR:
return LoadResult(f"Error from server: {upload_resp.message}")
else:
return LoadResult(f"Unexpected response command type: {upload_resp.command_type}")
cdef stop(self):
if self._socket and not self._socket.closed:
self._socket.close()
if self._loader_context and not self._loader_context.closed:
self._loader_context.term()
@@ -4,50 +4,48 @@ from queue import Queue
cimport constants cimport constants
from threading import Thread from threading import Thread
from api_client cimport ApiClient
from annotation cimport Annotation from annotation cimport Annotation
from inference cimport Inference from inference cimport Inference
from loader_client cimport LoaderClient
from remote_command cimport RemoteCommand, CommandType from remote_command cimport RemoteCommand, CommandType
from remote_command_handler cimport RemoteCommandHandler from remote_command_handler cimport RemoteCommandHandler
from credentials cimport Credentials
from file_data cimport FileData
from user cimport User
cdef class CommandProcessor: cdef class CommandProcessor:
cdef ApiClient api_client
cdef RemoteCommandHandler remote_handler cdef RemoteCommandHandler remote_handler
cdef object inference_queue cdef object inference_queue
cdef bint running cdef bint running
cdef Inference inference cdef Inference inference
cdef LoaderClient loader_client
def __init__(self): def __init__(self, int zmq_port, str loader_zmq_host, int loader_zmq_port, str api_url):
self.api_client = ApiClient() self.remote_handler = RemoteCommandHandler(zmq_port, self.on_command)
self.remote_handler = RemoteCommandHandler(self.on_command)
self.inference_queue = Queue(maxsize=constants.QUEUE_MAXSIZE) self.inference_queue = Queue(maxsize=constants.QUEUE_MAXSIZE)
self.remote_handler.start() self.remote_handler.start()
self.running = True self.running = True
self.inference = Inference(self.api_client, self.on_annotation) self.loader_client = LoaderClient(loader_zmq_host, loader_zmq_port)
self.inference = Inference(self.loader_client, self.on_annotation)
def start(self): def start(self):
while self.running: while self.running:
try: try:
command = self.inference_queue.get(timeout=0.5) command = self.inference_queue.get(timeout=0.5)
self.inference.run_inference(command) self.inference.run_inference(command)
self.remote_handler.send(command.client_id, <bytes>'DONE'.encode('utf-8')) end_inference_command = RemoteCommand(CommandType.INFERENCE_DATA, None, 'DONE')
self.remote_handler.send(command.client_id, end_inference_command.serialize())
except queue.Empty: except queue.Empty:
continue continue
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
print('EXIT!') constants.log('EXIT!')
cdef on_command(self, RemoteCommand command): cdef on_command(self, RemoteCommand command):
try: try:
if command.command_type == CommandType.LOGIN: if command.command_type == CommandType.INFERENCE:
self.login(command)
elif command.command_type == CommandType.LOAD:
self.load_file(command)
elif command.command_type == CommandType.INFERENCE:
self.inference_queue.put(command) self.inference_queue.put(command)
elif command.command_type == CommandType.AI_AVAILABILITY_CHECK:
self.inference.build_tensor_engine(lambda status: self.remote_handler.send(command.client_id,
RemoteCommand(CommandType.AI_AVAILABILITY_RESULT, None, status).serialize()))
elif command.command_type == CommandType.STOP_INFERENCE: elif command.command_type == CommandType.STOP_INFERENCE:
self.inference.stop() self.inference.stop()
elif command.command_type == CommandType.EXIT: elif command.command_type == CommandType.EXIT:
@@ -56,24 +54,14 @@ cdef class CommandProcessor:
else: else:
pass pass
except Exception as e: except Exception as e:
print(f"Error handling client: {e}") constants.logerror(f"Error handling client: {e}")
cdef login(self, RemoteCommand command):
cdef User user
self.api_client.set_credentials(Credentials.from_msgpack(command.data))
user = self.api_client.get_user()
self.remote_handler.send(command.client_id, user.serialize())
cdef load_file(self, RemoteCommand command):
cdef FileData file_data = FileData.from_msgpack(command.data)
response = self.api_client.load_bytes(file_data.filename, file_data.folder)
self.remote_handler.send(command.client_id, response)
cdef on_annotation(self, RemoteCommand cmd, Annotation annotation): cdef on_annotation(self, RemoteCommand cmd, Annotation annotation):
data = annotation.serialize() cdef RemoteCommand response = RemoteCommand(CommandType.INFERENCE_DATA, annotation.serialize())
self.remote_handler.send(cmd.client_id, data) self.remote_handler.send(cmd.client_id, response.serialize())
def stop(self): def stop(self):
self.inference.stop() self.inference.stop()
self.remote_handler.stop() self.remote_handler.stop()
self.loader_client.stop()
self.running = False self.running = False
+26
View File
@@ -0,0 +1,26 @@
from inference_engine cimport InferenceEngine
import onnxruntime as onnx
cimport constants
cdef class OnnxEngine(InferenceEngine):
def __init__(self, model_bytes: bytes, batch_size: int = 1, **kwargs):
super().__init__(model_bytes, batch_size)
self.session = onnx.InferenceSession(model_bytes, providers=["CUDAExecutionProvider", "CPUExecutionProvider"])
self.model_inputs = self.session.get_inputs()
self.input_name = self.model_inputs[0].name
self.input_shape = self.model_inputs[0].shape
self.batch_size = self.input_shape[0] if self.input_shape[0] != -1 else batch_size
constants.log(f'AI detection model input: {self.model_inputs} {self.input_shape}')
model_meta = self.session.get_modelmeta()
constants.log(f"Metadata: {model_meta.custom_metadata_map}")
cpdef tuple get_input_shape(self):
shape = self.input_shape
return shape[2], shape[3]
cpdef int get_batch_size(self):
return self.batch_size
cpdef run(self, input_data):
return self.session.run(None, {self.input_name: input_data})
+13
View File
@@ -1,14 +1,27 @@
cdef enum CommandType: cdef enum CommandType:
OK = 3
LOGIN = 10 LOGIN = 10
LIST_REQUEST = 15
LIST_FILES = 18
LOAD = 20 LOAD = 20
LOAD_BIG_SMALL = 22
UPLOAD_BIG_SMALL = 24
DATA_BYTES = 25
INFERENCE = 30 INFERENCE = 30
INFERENCE_DATA = 35
STOP_INFERENCE = 40 STOP_INFERENCE = 40
AI_AVAILABILITY_CHECK = 80
AI_AVAILABILITY_RESULT = 85
ERROR = 90
EXIT = 100 EXIT = 100
cdef class RemoteCommand: cdef class RemoteCommand:
cdef public bytes client_id cdef public bytes client_id
cdef CommandType command_type cdef CommandType command_type
cdef str message
cdef bytes data cdef bytes data
@staticmethod @staticmethod
cdef from_msgpack(bytes data) cdef from_msgpack(bytes data)
cdef bytes serialize(self)
+20 -2
View File
@@ -1,16 +1,27 @@
import msgpack import msgpack
cdef class RemoteCommand: cdef class RemoteCommand:
def __init__(self, CommandType command_type, bytes data): def __init__(self, CommandType command_type, bytes data=None, str message=None):
self.command_type = command_type self.command_type = command_type
self.data = data self.data = data
self.message = message
def __str__(self): def __str__(self):
command_type_names = { command_type_names = {
3: "OK",
10: "LOGIN", 10: "LOGIN",
15: "LIST_REQUEST",
18: "LIST_FILES",
20: "LOAD", 20: "LOAD",
22: "LOAD_BIG_SMALL",
24: "UPLOAD_BIG_SMALL",
25: "DATA_BYTES",
30: "INFERENCE", 30: "INFERENCE",
35: "INFERENCE_DATA",
40: "STOP_INFERENCE", 40: "STOP_INFERENCE",
80: "AI_AVAILABILITY_CHECK",
85: "AI_AVAILABILITY_RESULT",
90: "ERROR",
100: "EXIT" 100: "EXIT"
} }
data_str = f'{len(self.data)} bytes' if self.data else '' data_str = f'{len(self.data)} bytes' if self.data else ''
@@ -19,4 +30,11 @@ cdef class RemoteCommand:
@staticmethod @staticmethod
cdef from_msgpack(bytes data): cdef from_msgpack(bytes data):
unpacked = msgpack.unpackb(data, strict_map_key=False) unpacked = msgpack.unpackb(data, strict_map_key=False)
return RemoteCommand(unpacked.get("CommandType"), unpacked.get("Data")) return RemoteCommand(unpacked.get("CommandType"), unpacked.get("Data"), unpacked.get("Message"))
cdef bytes serialize(self):
return msgpack.packb({
"CommandType": self.command_type,
"Data": self.data,
"Message": self.message
})
+14 -16
View File
@@ -3,19 +3,15 @@ import zmq
from threading import Thread, Event from threading import Thread, Event
from remote_command cimport RemoteCommand from remote_command cimport RemoteCommand
cimport constants cimport constants
import yaml
cdef class RemoteCommandHandler: cdef class RemoteCommandHandler:
def __init__(self, object on_command): def __init__(self, int zmq_port, object on_command):
self._on_command = on_command self._on_command = on_command
self._context = zmq.Context.instance() self._context = zmq.Context()
self._router = self._context.socket(zmq.ROUTER) self._router = self._context.socket(zmq.ROUTER)
self._router.setsockopt(zmq.LINGER, 0) self._router.setsockopt(zmq.LINGER, 0)
with open(<str>constants.CONFIG_FILE, "r") as f: self._router.bind(f'tcp://*:{zmq_port}')
config = yaml.safe_load(f)
port = config["zmq_port"]
self._router.bind(f'tcp://*:{port}')
self._dealer = self._context.socket(zmq.DEALER) self._dealer = self._context.socket(zmq.DEALER)
self._dealer.setsockopt(zmq.LINGER, 0) self._dealer.setsockopt(zmq.LINGER, 0)
@@ -31,7 +27,7 @@ cdef class RemoteCommandHandler:
for _ in range(4): # 4 worker threads for _ in range(4): # 4 worker threads
worker = Thread(target=self._worker_loop, daemon=True) worker = Thread(target=self._worker_loop, daemon=True)
self._workers.append(worker) self._workers.append(worker)
print(f'Listening to commands on port {port}...') constants.log(f'Listening to commands on port {zmq_port}...')
cdef start(self): cdef start(self):
self._proxy_thread.start() self._proxy_thread.start()
@@ -43,7 +39,7 @@ cdef class RemoteCommandHandler:
zmq.proxy_steerable(self._router, self._dealer, control=self._control) zmq.proxy_steerable(self._router, self._dealer, control=self._control)
except zmq.error.ZMQError as e: except zmq.error.ZMQError as e:
if self._shutdown_event.is_set(): if self._shutdown_event.is_set():
print("Shutdown, exit proxy loop.") constants.log("Shutdown, exit proxy loop.")
else: else:
raise raise
@@ -62,21 +58,23 @@ cdef class RemoteCommandHandler:
client_id, message = worker_socket.recv_multipart() client_id, message = worker_socket.recv_multipart()
cmd = RemoteCommand.from_msgpack(<bytes> message) cmd = RemoteCommand.from_msgpack(<bytes> message)
cmd.client_id = client_id cmd.client_id = client_id
constants.log(<str>f'{cmd}', client_id) constants.log(cmd)
self._on_command(cmd) self._on_command(cmd)
except Exception as e: except Exception as e:
if not self._shutdown_event.is_set(): if not self._shutdown_event.is_set():
print(f"Worker error: {e}") constants.log(f"Worker error: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
finally: finally:
worker_socket.close() worker_socket.close()
cdef send(self, bytes client_id, bytes data): cdef send(self, bytes client_id, bytes data):
with self._context.socket(zmq.DEALER) as socket: self._router.send_multipart([client_id, data])
socket.connect("inproc://backend")
socket.send_multipart([client_id, data]) # with self._context.socket(zmq.DEALER) as socket:
constants.log(<str>f'Sent {len(data)} bytes.', client_id) # socket.connect("inproc://backend")
# socket.send_multipart([client_id, data])
# # constants.log(<str>f'Sent {len(data)} bytes.', client_id)
cdef stop(self): cdef stop(self):
self._shutdown_event.set() self._shutdown_event.set()
@@ -84,6 +82,7 @@ cdef class RemoteCommandHandler:
self._control.send(b"TERMINATE", flags=zmq.DONTWAIT) self._control.send(b"TERMINATE", flags=zmq.DONTWAIT)
except zmq.error.ZMQError: except zmq.error.ZMQError:
pass pass
self._router.close(linger=0) self._router.close(linger=0)
self._dealer.close(linger=0) self._dealer.close(linger=0)
self._control.close(linger=0) self._control.close(linger=0)
@@ -91,5 +90,4 @@ cdef class RemoteCommandHandler:
self._proxy_thread.join(timeout=2) self._proxy_thread.join(timeout=2)
while any(w.is_alive() for w in self._workers): while any(w.is_alive() for w in self._workers):
time.sleep(0.1) time.sleep(0.1)
self._context.term() self._context.term()

Some files were not shown because too many files have changed in this diff Show More