mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 22:56:29 +00:00
Merge branch 'refs/heads/dev'
# Conflicts: # Azaion.Inference/requirements.txt
This commit is contained in:
+10
-2
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -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!;
|
||||||
+8
-5
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.Common.Extensions;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public static class EnumerableExtensions
|
public static class EnumerableExtensions
|
||||||
{
|
{
|
||||||
@@ -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!;
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
+20
-5
@@ -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
|
||||||
{
|
{
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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!;
|
||||||
|
}
|
||||||
@@ -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>();
|
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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!;
|
|
||||||
}
|
|
||||||
@@ -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!;
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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=*)
|
|
||||||
@@ -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
|
|
||||||
@@ -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]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
zmq_port: 5131
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
zmq_port: 5127
|
|
||||||
@@ -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=*)
|
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,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)}")
|
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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})
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user