mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 11:06:30 +00:00
queue + local sqlite WIP
This commit is contained in:
@@ -5,3 +5,4 @@ obj
|
|||||||
*.DotSettings*
|
*.DotSettings*
|
||||||
*.user
|
*.user
|
||||||
log*.txt
|
log*.txt
|
||||||
|
secured-config
|
||||||
@@ -91,8 +91,8 @@ public partial class Annotator
|
|||||||
|
|
||||||
_suspendLayout = true;
|
_suspendLayout = true;
|
||||||
|
|
||||||
MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_appConfig.AnnotatorWindowConfig.LeftPanelWidth);
|
MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_appConfig.AnnotationConfig.LeftPanelWidth);
|
||||||
MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig.AnnotatorWindowConfig.RightPanelWidth);
|
MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig.AnnotationConfig.RightPanelWidth);
|
||||||
|
|
||||||
_suspendLayout = false;
|
_suspendLayout = false;
|
||||||
|
|
||||||
@@ -192,27 +192,13 @@ public partial class Annotator
|
|||||||
if (result != MessageBoxResult.OK)
|
if (result != MessageBoxResult.OK)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// var allWindows = Application.Current.Windows.Cast<Window>();
|
|
||||||
// try
|
|
||||||
// {
|
|
||||||
// foreach (var window in allWindows)
|
|
||||||
// window.IsEnabled = false;
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
// finally
|
|
||||||
// {
|
|
||||||
// foreach (var window in allWindows)
|
|
||||||
// {
|
|
||||||
// window.IsEnabled = true;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
var res = DgAnnotations.SelectedItems.Cast<AnnotationResult>().ToList();
|
var res = DgAnnotations.SelectedItems.Cast<AnnotationResult>().ToList();
|
||||||
foreach (var annotationResult in res)
|
foreach (var annotationResult in res)
|
||||||
{
|
{
|
||||||
var imgName = Path.GetFileNameWithoutExtension(annotationResult.Image);
|
var imgName = Path.GetFileNameWithoutExtension(annotationResult.Image);
|
||||||
var thumbnailPath = Path.Combine(_appConfig.DirectoriesConfig.ThumbnailsDirectory, $"{imgName}{Constants.THUMBNAIL_PREFIX}.jpg");
|
var thumbnailPath = Path.Combine(_appConfig.DirectoriesConfig.ThumbnailsDirectory, $"{imgName}{Constants.THUMBNAIL_PREFIX}.jpg");
|
||||||
File.Delete(annotationResult.Image);
|
File.Delete(annotationResult.Image);
|
||||||
|
|
||||||
File.Delete(Path.Combine(_appConfig.DirectoriesConfig.LabelsDirectory, $"{imgName}.txt"));
|
File.Delete(Path.Combine(_appConfig.DirectoriesConfig.LabelsDirectory, $"{imgName}.txt"));
|
||||||
File.Delete(thumbnailPath);
|
File.Delete(thumbnailPath);
|
||||||
_formState.AnnotationResults.Remove(annotationResult);
|
_formState.AnnotationResults.Remove(annotationResult);
|
||||||
@@ -246,8 +232,8 @@ public partial class Annotator
|
|||||||
if (_suspendLayout)
|
if (_suspendLayout)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_appConfig.AnnotatorWindowConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value;
|
_appConfig.AnnotationConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value;
|
||||||
_appConfig.AnnotatorWindowConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value;
|
_appConfig.AnnotationConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value;
|
||||||
|
|
||||||
await ThrottleExt.Throttle(() =>
|
await ThrottleExt.Throttle(() =>
|
||||||
{
|
{
|
||||||
@@ -710,9 +696,6 @@ public partial class Annotator
|
|||||||
|
|
||||||
var fName = _formState.GetTimeName(timeframe.Time);
|
var fName = _formState.GetTimeName(timeframe.Time);
|
||||||
var imgPath = Path.Combine(_appConfig.DirectoriesConfig.ImagesDirectory, $"{fName}.jpg");
|
var imgPath = Path.Combine(_appConfig.DirectoriesConfig.ImagesDirectory, $"{fName}.jpg");
|
||||||
var img = System.Drawing.Image.FromStream(timeframe.Stream);
|
|
||||||
img.Save(imgPath, ImageFormat.Jpeg);
|
|
||||||
await YoloLabel.WriteToFile(detections, Path.Combine(_appConfig.DirectoriesConfig.LabelsDirectory, $"{fName}.txt"), token);
|
|
||||||
|
|
||||||
Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() };
|
Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() };
|
||||||
Editor.RemoveAllAnns();
|
Editor.RemoveAllAnns();
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media;
|
|
||||||
using Azaion.Annotator.DTO;
|
using Azaion.Annotator.DTO;
|
||||||
|
using Azaion.Common;
|
||||||
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.Services;
|
||||||
using LibVLCSharp.Shared;
|
using LibVLCSharp.Shared;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -19,16 +20,16 @@ public class AnnotatorEventHandler(
|
|||||||
MediaPlayer mediaPlayer,
|
MediaPlayer mediaPlayer,
|
||||||
Annotator mainWindow,
|
Annotator mainWindow,
|
||||||
FormState formState,
|
FormState formState,
|
||||||
IOptions<DirectoriesConfig> directoriesConfig,
|
AnnotationService annotationService,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
ILogger<AnnotatorEventHandler> logger)
|
ILogger<AnnotatorEventHandler> logger,
|
||||||
|
IOptions<DirectoriesConfig> dirConfig)
|
||||||
:
|
:
|
||||||
INotificationHandler<KeyEvent>,
|
INotificationHandler<KeyEvent>,
|
||||||
INotificationHandler<AnnClassSelectedEvent>,
|
INotificationHandler<AnnClassSelectedEvent>,
|
||||||
INotificationHandler<PlaybackControlEvent>,
|
INotificationHandler<PlaybackControlEvent>,
|
||||||
INotificationHandler<VolumeChangedEvent>
|
INotificationHandler<VolumeChangedEvent>
|
||||||
{
|
{
|
||||||
private readonly DirectoriesConfig _directoriesConfig = directoriesConfig.Value;
|
|
||||||
private const int STEP = 20;
|
private const int STEP = 20;
|
||||||
private const int LARGE_STEP = 5000;
|
private const int LARGE_STEP = 5000;
|
||||||
private const int RESULT_WIDTH = 1280;
|
private const int RESULT_WIDTH = 1280;
|
||||||
@@ -59,7 +60,7 @@ public class AnnotatorEventHandler(
|
|||||||
mainWindow.LvClasses.SelectedIndex = annClass.Id;
|
mainWindow.LvClasses.SelectedIndex = annClass.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken)
|
public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (keyEvent.WindowEnum != WindowEnum.Annotator)
|
if (keyEvent.WindowEnum != WindowEnum.Annotator)
|
||||||
return;
|
return;
|
||||||
@@ -75,23 +76,19 @@ public class AnnotatorEventHandler(
|
|||||||
SelectClass((AnnotationClass)mainWindow.LvClasses.Items[keyNumber.Value]!);
|
SelectClass((AnnotationClass)mainWindow.LvClasses.Items[keyNumber.Value]!);
|
||||||
|
|
||||||
if (_keysControlEnumDict.TryGetValue(key, out var value))
|
if (_keysControlEnumDict.TryGetValue(key, out var value))
|
||||||
await ControlPlayback(value);
|
await ControlPlayback(value, cancellationToken);
|
||||||
|
|
||||||
if (key == Key.A)
|
if (key == Key.A)
|
||||||
mainWindow.AutoDetect(null!, null!);
|
mainWindow.AutoDetect(null!, null!);
|
||||||
|
|
||||||
await VolumeControl(key);
|
#region Volume
|
||||||
}
|
|
||||||
|
|
||||||
private async Task VolumeControl(Key key)
|
|
||||||
{
|
|
||||||
switch (key)
|
switch (key)
|
||||||
{
|
{
|
||||||
case Key.VolumeMute when mediaPlayer.Volume == 0:
|
case Key.VolumeMute when mediaPlayer.Volume == 0:
|
||||||
await ControlPlayback(PlaybackControlEnum.TurnOnVolume);
|
await ControlPlayback(PlaybackControlEnum.TurnOnVolume, cancellationToken);
|
||||||
break;
|
break;
|
||||||
case Key.VolumeMute:
|
case Key.VolumeMute:
|
||||||
await ControlPlayback(PlaybackControlEnum.TurnOffVolume);
|
await ControlPlayback(PlaybackControlEnum.TurnOffVolume, cancellationToken);
|
||||||
break;
|
break;
|
||||||
case Key.Up:
|
case Key.Up:
|
||||||
case Key.VolumeUp:
|
case Key.VolumeUp:
|
||||||
@@ -106,15 +103,16 @@ public class AnnotatorEventHandler(
|
|||||||
mainWindow.Volume.Value = vDown;
|
mainWindow.Volume.Value = vDown;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(PlaybackControlEvent notification, CancellationToken cancellationToken)
|
public async Task Handle(PlaybackControlEvent notification, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await ControlPlayback(notification.PlaybackControl);
|
await ControlPlayback(notification.PlaybackControl, cancellationToken);
|
||||||
mainWindow.VideoView.Focus();
|
mainWindow.VideoView.Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ControlPlayback(PlaybackControlEnum controlEnum)
|
private async Task ControlPlayback(PlaybackControlEnum controlEnum, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -222,10 +220,9 @@ public class AnnotatorEventHandler(
|
|||||||
mediaPlayer.Play(new Media(libVLC, mediaInfo.Path));
|
mediaPlayer.Play(new Media(libVLC, mediaInfo.Path));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveAnnotations()
|
//SAVE: MANUAL
|
||||||
|
private async Task SaveAnnotations(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var annGridSelectedIndex = mainWindow.DgAnnotations.SelectedIndex;
|
|
||||||
|
|
||||||
if (formState.CurrentMedia == null)
|
if (formState.CurrentMedia == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -236,16 +233,15 @@ public class AnnotatorEventHandler(
|
|||||||
.Select(x => new YoloLabel(x.Info, mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize))
|
.Select(x => new YoloLabel(x.Info, mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
await YoloLabel.WriteToFile(currentAnns, Path.Combine(_directoriesConfig.LabelsDirectory, $"{fName}.txt"));
|
await mainWindow.AddAnnotations(time, currentAnns, cancellationToken);
|
||||||
await mainWindow.AddAnnotations(time, currentAnns);
|
|
||||||
|
|
||||||
formState.CurrentMedia.HasAnnotations = mainWindow.Annotations.Count != 0;
|
formState.CurrentMedia.HasAnnotations = mainWindow.Annotations.Count != 0;
|
||||||
mainWindow.LvFiles.Items.Refresh();
|
mainWindow.LvFiles.Items.Refresh();
|
||||||
|
mainWindow.Editor.RemoveAllAnns();
|
||||||
|
|
||||||
var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video;
|
var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video;
|
||||||
var destinationPath = Path.Combine(_directoriesConfig.ImagesDirectory, $"{fName}{(isVideo ? ".jpg" : Path.GetExtension(formState.CurrentMedia.Path))}");
|
var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{fName}{(isVideo ? ".jpg" : Path.GetExtension(formState.CurrentMedia.Path))}");
|
||||||
|
|
||||||
mainWindow.Editor.RemoveAllAnns();
|
|
||||||
if (isVideo)
|
if (isVideo)
|
||||||
{
|
{
|
||||||
if (formState.BackgroundTime.HasValue)
|
if (formState.BackgroundTime.HasValue)
|
||||||
@@ -256,22 +252,23 @@ public class AnnotatorEventHandler(
|
|||||||
|
|
||||||
//next item
|
//next item
|
||||||
var annGrid = mainWindow.DgAnnotations;
|
var annGrid = mainWindow.DgAnnotations;
|
||||||
annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGridSelectedIndex + 1);
|
annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGrid.SelectedIndex + 1);
|
||||||
mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
|
mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var resultHeight = (uint)Math.Round(RESULT_WIDTH / formState.CurrentVideoSize.Width * formState.CurrentVideoSize.Height);
|
var resultHeight = (uint)Math.Round(RESULT_WIDTH / formState.CurrentVideoSize.Width * formState.CurrentVideoSize.Height);
|
||||||
mediaPlayer.TakeSnapshot(0, destinationPath, RESULT_WIDTH, resultHeight);
|
mediaPlayer.TakeSnapshot(0, imgPath, RESULT_WIDTH, resultHeight);
|
||||||
mediaPlayer.Play();
|
mediaPlayer.Play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
File.Copy(formState.CurrentMedia.Path, destinationPath, overwrite: true);
|
File.Copy(formState.CurrentMedia.Path, imgPath, overwrite: true);
|
||||||
NextMedia();
|
NextMedia();
|
||||||
}
|
}
|
||||||
|
await annotationService.SaveAnnotation(fName, currentAnns, SourceEnum.Manual, token: cancellationToken);
|
||||||
|
|
||||||
await mediator.Publish(new ImageCreatedEvent(destinationPath));
|
await mediator.Publish(new ImageCreatedEvent(imgPath), cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,8 +51,5 @@
|
|||||||
Тоді будь яка самохідна артилерія на гусеницях, хоч вона являє собою артилерію, мусить бути анотована як "Броньована техніка", оскільки візуально
|
Тоді будь яка самохідна артилерія на гусеницях, хоч вона являє собою артилерію, мусить бути анотована як "Броньована техніка", оскільки візуально
|
||||||
вона значно більш схожа на танк ніж на міномет.
|
вона значно більш схожа на танк ніж на міномет.
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
<CheckBox Grid.Row="7" x:Name="CbShowHelp" Margin="10" Checked="CbShowHelp_OnChecked" Unchecked="CbShowHelp_OnUnchecked">
|
|
||||||
Показувати при запуску
|
|
||||||
</CheckBox>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -6,12 +6,8 @@ namespace Azaion.Annotator;
|
|||||||
|
|
||||||
public partial class HelpWindow : Window
|
public partial class HelpWindow : Window
|
||||||
{
|
{
|
||||||
private readonly AnnotatorWindowConfig _annotatorWindowConfig;
|
public HelpWindow()
|
||||||
|
|
||||||
public HelpWindow(IOptions<AnnotatorWindowConfig> windowConfig)
|
|
||||||
{
|
{
|
||||||
_annotatorWindowConfig = windowConfig.Value;
|
|
||||||
Loaded += (_, _) => CbShowHelp.IsChecked = _annotatorWindowConfig.ShowHelpOnStart;
|
|
||||||
Closing += (sender, args) =>
|
Closing += (sender, args) =>
|
||||||
{
|
{
|
||||||
args.Cancel = true;
|
args.Cancel = true;
|
||||||
@@ -20,7 +16,4 @@ public partial class HelpWindow : Window
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CbShowHelp_OnChecked(object sender, RoutedEventArgs e) => _annotatorWindowConfig.ShowHelpOnStart = true;
|
|
||||||
private void CbShowHelp_OnUnchecked(object sender, RoutedEventArgs e) => _annotatorWindowConfig.ShowHelpOnStart = false;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ using Azaion.Annotator.Extensions;
|
|||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.DTO.Config;
|
using Azaion.Common.DTO.Config;
|
||||||
using Azaion.Common.Services;
|
using Azaion.Common.Services;
|
||||||
|
using Azaion.CommonSecurity.Services;
|
||||||
using Compunet.YoloV8;
|
using Compunet.YoloV8;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
|
|||||||
@@ -7,12 +7,19 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="linq2db.SQLite" Version="5.4.1" />
|
||||||
<PackageReference Include="MediatR" Version="12.4.1" />
|
<PackageReference Include="MediatR" Version="12.4.1" />
|
||||||
|
<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.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
|
<PackageReference Include="RabbitMQ.Stream.Client" Version="1.8.9" />
|
||||||
|
<PackageReference Include="System.Drawing.Common" Version="4.7.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Azaion.CommonSecurity\Azaion.CommonSecurity.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+19
-22
@@ -5,20 +5,7 @@ namespace Azaion.Common;
|
|||||||
|
|
||||||
public class Constants
|
public class Constants
|
||||||
{
|
{
|
||||||
public const string CONFIG_PATH = "config.json";
|
public const string SECURE_RESOURCE_CACHE = "SecureResourceCache";
|
||||||
public const string DEFAULT_DLL_CACHE_DIR = "DllCache";
|
|
||||||
|
|
||||||
#region ApiConfig
|
|
||||||
|
|
||||||
public const string DEFAULT_API_URL = "https://api.azaion.com/";
|
|
||||||
public const int DEFAULT_API_RETRY_COUNT = 3;
|
|
||||||
public const int DEFAULT_API_TIMEOUT_SECONDS = 40;
|
|
||||||
|
|
||||||
public const string CLAIM_NAME_ID = "nameid";
|
|
||||||
public const string CLAIM_EMAIL = "unique_name";
|
|
||||||
public const string CLAIM_ROLE = "role";
|
|
||||||
|
|
||||||
#endregion ApiConfig
|
|
||||||
|
|
||||||
#region DirectoriesConfig
|
#region DirectoriesConfig
|
||||||
|
|
||||||
@@ -44,12 +31,18 @@ public class Constants
|
|||||||
new() { Id = 7, Name = "Накати", ShortName = "Накати" },
|
new() { Id = 7, Name = "Накати", ShortName = "Накати" },
|
||||||
new() { Id = 8, Name = "Танк з захистом", ShortName = "Танк захист" },
|
new() { Id = 8, Name = "Танк з захистом", ShortName = "Танк захист" },
|
||||||
new() { Id = 9, Name = "Дим", ShortName = "Дим" },
|
new() { Id = 9, Name = "Дим", ShortName = "Дим" },
|
||||||
new() { Id = 10, Name = "Літак", ShortName = "Літак" }
|
new() { Id = 10, Name = "Літак", ShortName = "Літак" },
|
||||||
|
new() { Id = 11, Name = "Мотоцикл", ShortName = "Мото" }
|
||||||
];
|
];
|
||||||
|
|
||||||
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
|
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
|
||||||
public static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
|
public static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
|
||||||
|
|
||||||
|
public static int DEFAULT_LEFT_PANEL_WIDTH = 250;
|
||||||
|
public static int DEFAULT_RIGHT_PANEL_WIDTH = 250;
|
||||||
|
|
||||||
|
public const string DEFAULT_ANNOTATIONS_DB_FILE = "annotations.db";
|
||||||
|
|
||||||
# endregion AnnotatorConfig
|
# endregion AnnotatorConfig
|
||||||
|
|
||||||
# region AIRecognitionConfig
|
# region AIRecognitionConfig
|
||||||
@@ -62,13 +55,6 @@ public class Constants
|
|||||||
|
|
||||||
# endregion AIRecognitionConfig
|
# endregion AIRecognitionConfig
|
||||||
|
|
||||||
# region AnnotatorWindowConfig
|
|
||||||
|
|
||||||
public static int DEFAULT_LEFT_PANEL_WIDTH = 250;
|
|
||||||
public static int DEFAULT_RIGHT_PANEL_WIDTH = 250;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Thumbnails
|
#region Thumbnails
|
||||||
|
|
||||||
public static readonly Size DefaultThumbnailSize = new(240, 135);
|
public static readonly Size DefaultThumbnailSize = new(240, 135);
|
||||||
@@ -98,4 +84,15 @@ public class Constants
|
|||||||
return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
|
return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Queue
|
||||||
|
|
||||||
|
public const string MQ_DIRECT_TYPE = "direct";
|
||||||
|
public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations";
|
||||||
|
public const string MQ_ANNOTATIONS_CONFIRM_QUEUE = "azaion-annotations-confirm";
|
||||||
|
public const string ANNOTATION_PRODUCER = "AnnotationsProducer";
|
||||||
|
public const string ANNOTATION_CONFIRM_PRODUCER = "AnnotationsConfirmProducer";
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System.IO;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Azaion.Common.DTO.Queue;
|
||||||
|
using Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
public class Annotation
|
||||||
|
{
|
||||||
|
private static string _labelsDir = null!;
|
||||||
|
private static string _imagesDir = null!;
|
||||||
|
|
||||||
|
public static void InitializeDirs(DirectoriesConfig config)
|
||||||
|
{
|
||||||
|
_labelsDir = config.LabelsDirectory;
|
||||||
|
_imagesDir = config.ImagesDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get; set; } = null!;
|
||||||
|
public DateTime CreatedDate { get; set; }
|
||||||
|
public List<int> Classes { get; set; } = null!;
|
||||||
|
public string CreatedEmail { get; set; } = null!;
|
||||||
|
public RoleEnum CreatedRole { get; set; }
|
||||||
|
public SourceEnum Source { get; set; }
|
||||||
|
public AnnotationStatus AnnotationStatus { get; set; }
|
||||||
|
|
||||||
|
public string ImagePath => Path.Combine(_imagesDir, $"{Name}.jpg");
|
||||||
|
public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AnnotationStatus
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Created = 10,
|
||||||
|
Validated = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnnotationName
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = null!;
|
||||||
|
}
|
||||||
@@ -15,4 +15,9 @@ public class AnnotationConfig
|
|||||||
|
|
||||||
public List<string> VideoFormats { get; set; } = null!;
|
public List<string> VideoFormats { get; set; } = null!;
|
||||||
public List<string> ImageFormats { get; set; } = null!;
|
public List<string> ImageFormats { get; set; } = null!;
|
||||||
|
|
||||||
|
public string AnnotationsDbFile { get; set; } = null!;
|
||||||
|
|
||||||
|
public double LeftPanelWidth { get; set; }
|
||||||
|
public double RightPanelWidth { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Azaion.Common.DTO.Config;
|
|
||||||
|
|
||||||
public class AnnotatorWindowConfig
|
|
||||||
{
|
|
||||||
public double LeftPanelWidth { get; set; }
|
|
||||||
public double RightPanelWidth { get; set; }
|
|
||||||
public bool ShowHelpOnStart { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Azaion.CommonSecurity;
|
||||||
|
using Azaion.CommonSecurity.DTO;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Azaion.Common.DTO.Config;
|
namespace Azaion.Common.DTO.Config;
|
||||||
@@ -8,12 +10,12 @@ public class AppConfig
|
|||||||
{
|
{
|
||||||
public ApiConfig ApiConfig { get; set; } = null!;
|
public ApiConfig ApiConfig { get; set; } = null!;
|
||||||
|
|
||||||
|
public QueueConfig QueueConfig { get; set; } = null!;
|
||||||
|
|
||||||
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
|
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
|
||||||
|
|
||||||
public AnnotationConfig AnnotationConfig { get; set; } = null!;
|
public AnnotationConfig AnnotationConfig { get; set; } = null!;
|
||||||
|
|
||||||
public AnnotatorWindowConfig AnnotatorWindowConfig { get; set; } = null!;
|
|
||||||
|
|
||||||
public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!;
|
public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!;
|
||||||
|
|
||||||
public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
|
public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
|
||||||
@@ -30,7 +32,7 @@ public class ConfigUpdater : IConfigUpdater
|
|||||||
public void CheckConfig()
|
public void CheckConfig()
|
||||||
{
|
{
|
||||||
var exePath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)!;
|
var exePath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)!;
|
||||||
var configFilePath = Path.Combine(exePath, Constants.CONFIG_PATH);
|
var configFilePath = Path.Combine(exePath, SecurityConstants.CONFIG_PATH);
|
||||||
|
|
||||||
if (File.Exists(configFilePath))
|
if (File.Exists(configFilePath))
|
||||||
return;
|
return;
|
||||||
@@ -39,9 +41,9 @@ public class ConfigUpdater : IConfigUpdater
|
|||||||
{
|
{
|
||||||
ApiConfig = new ApiConfig
|
ApiConfig = new ApiConfig
|
||||||
{
|
{
|
||||||
Url = Constants.DEFAULT_API_URL,
|
Url = SecurityConstants.DEFAULT_API_URL,
|
||||||
RetryCount = Constants.DEFAULT_API_RETRY_COUNT,
|
RetryCount = SecurityConstants.DEFAULT_API_RETRY_COUNT,
|
||||||
TimeoutSeconds = Constants.DEFAULT_API_TIMEOUT_SECONDS
|
TimeoutSeconds = SecurityConstants.DEFAULT_API_TIMEOUT_SECONDS
|
||||||
},
|
},
|
||||||
|
|
||||||
AnnotationConfig = new AnnotationConfig
|
AnnotationConfig = new AnnotationConfig
|
||||||
@@ -49,12 +51,11 @@ public class ConfigUpdater : IConfigUpdater
|
|||||||
AnnotationClasses = Constants.DefaultAnnotationClasses,
|
AnnotationClasses = Constants.DefaultAnnotationClasses,
|
||||||
VideoFormats = Constants.DefaultVideoFormats,
|
VideoFormats = Constants.DefaultVideoFormats,
|
||||||
ImageFormats = Constants.DefaultImageFormats,
|
ImageFormats = Constants.DefaultImageFormats,
|
||||||
},
|
|
||||||
|
|
||||||
AnnotatorWindowConfig = new AnnotatorWindowConfig
|
|
||||||
{
|
|
||||||
LeftPanelWidth = Constants.DEFAULT_LEFT_PANEL_WIDTH,
|
LeftPanelWidth = Constants.DEFAULT_LEFT_PANEL_WIDTH,
|
||||||
RightPanelWidth = Constants.DEFAULT_RIGHT_PANEL_WIDTH
|
RightPanelWidth = Constants.DEFAULT_RIGHT_PANEL_WIDTH,
|
||||||
|
|
||||||
|
AnnotationsDbFile = Constants.DEFAULT_ANNOTATIONS_DB_FILE
|
||||||
},
|
},
|
||||||
|
|
||||||
DirectoriesConfig = new DirectoriesConfig
|
DirectoriesConfig = new DirectoriesConfig
|
||||||
@@ -86,6 +87,6 @@ public class ConfigUpdater : IConfigUpdater
|
|||||||
|
|
||||||
public void Save(AppConfig config)
|
public void Save(AppConfig config)
|
||||||
{
|
{
|
||||||
File.WriteAllText(Constants.CONFIG_PATH, JsonConvert.SerializeObject(config, Formatting.Indented), Encoding.UTF8);
|
File.WriteAllText(SecurityConstants.CONFIG_PATH, JsonConvert.SerializeObject(config, Formatting.Indented), Encoding.UTF8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Azaion.Common.DTO.Config;
|
||||||
|
|
||||||
|
public class QueueConfig
|
||||||
|
{
|
||||||
|
public string Host { get; set; } = null!;
|
||||||
|
public int Port { get; set; }
|
||||||
|
|
||||||
|
public string ProducerUsername { get; set; } = null!;
|
||||||
|
public string ProducerPassword { get; set; } = null!;
|
||||||
|
|
||||||
|
public string ConsumerUsername { get; set; } = null!;
|
||||||
|
public string ConsumerPassword { get; set; } = null!;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -158,19 +158,25 @@ public class YoloLabel : Label
|
|||||||
public static async Task<List<YoloLabel>> ReadFromFile(string filename, CancellationToken cancellationToken = default)
|
public static async Task<List<YoloLabel>> ReadFromFile(string filename, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var str = await File.ReadAllTextAsync(filename, cancellationToken);
|
var str = await File.ReadAllTextAsync(filename, cancellationToken);
|
||||||
|
return Deserialize(str);
|
||||||
return str.Split('\n')
|
|
||||||
.Select(Parse)
|
|
||||||
.Where(ann => ann != null)
|
|
||||||
.ToList()!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task WriteToFile(IEnumerable<YoloLabel> labels, string filename, CancellationToken cancellationToken = default)
|
public static async Task WriteToFile(IEnumerable<YoloLabel> labels, string filename, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var labelsStr = string.Join(Environment.NewLine, labels.Select(x => x.ToString()));
|
var labelsStr = Serialize(labels);
|
||||||
await File.WriteAllTextAsync(filename, labelsStr, cancellationToken);
|
await File.WriteAllTextAsync(filename, labelsStr, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string Serialize(IEnumerable<YoloLabel> labels) =>
|
||||||
|
string.Join(Environment.NewLine, labels.Select(x => x.ToString()));
|
||||||
|
|
||||||
|
public static List<YoloLabel> Deserialize(string str) =>
|
||||||
|
str.Split('\n')
|
||||||
|
.Select(Parse)
|
||||||
|
.Where(ann => ann != null)
|
||||||
|
.ToList()!;
|
||||||
|
|
||||||
|
|
||||||
public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.');
|
public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
|
namespace Azaion.Common.DTO.Queue;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public class AnnotationCreatedMessage
|
||||||
|
{
|
||||||
|
[Key(0)] public DateTime CreatedDate { get; set; }
|
||||||
|
[Key(1)] public string Name { get; set; } = null!;
|
||||||
|
[Key(2)] public string Label { get; set; } = null!;
|
||||||
|
[Key(3)] public byte[] Image { get; set; } = null!;
|
||||||
|
[Key(4)] public RoleEnum CreatedRole { get; set; }
|
||||||
|
[Key(5)] public string CreatedEmail { get; set; } = null!;
|
||||||
|
[Key(6)] public SourceEnum Source { get; set; }
|
||||||
|
[Key(7)] public AnnotationStatus Status { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public class AnnotationValidatedMessage
|
||||||
|
{
|
||||||
|
[Key(0)] public string Name { get; set; } = null!;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Azaion.Common.DTO.Queue;
|
||||||
|
|
||||||
|
public enum SourceEnum
|
||||||
|
{
|
||||||
|
AI,
|
||||||
|
Manual
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Azaion.Common.DTO;
|
||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.Data;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Database;
|
||||||
|
|
||||||
|
public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions)
|
||||||
|
{
|
||||||
|
public ITable<Annotation> Annotations => this.GetTable<Annotation>();
|
||||||
|
public ITable<AnnotationName> AnnotationsQueue => this.GetTable<AnnotationName>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.Mapping;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Database;
|
||||||
|
|
||||||
|
public interface IDbFactory
|
||||||
|
{
|
||||||
|
Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func);
|
||||||
|
Task Run(Func<AnnotationsDb, Task> func);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DbFactory : IDbFactory
|
||||||
|
{
|
||||||
|
private readonly DataOptions _dataOptions;
|
||||||
|
|
||||||
|
public DbFactory(IOptions<AnnotationConfig> annConfig)
|
||||||
|
{
|
||||||
|
_dataOptions = LoadOptions(annConfig.Value.AnnotationsDbFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataOptions LoadOptions(string dbFile)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(dbFile))
|
||||||
|
throw new ArgumentException($"Empty AnnotationsDbFile in config!");
|
||||||
|
|
||||||
|
var dataOptions = new DataOptions()
|
||||||
|
.UseSQLiteOfficial($"Data Source={dbFile}")
|
||||||
|
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema);
|
||||||
|
|
||||||
|
_ = dataOptions.UseTracing(TraceLevel.Info, t => Console.WriteLine(t.SqlText));
|
||||||
|
return dataOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func)
|
||||||
|
{
|
||||||
|
await using var db = new AnnotationsDb(_dataOptions);
|
||||||
|
return await func(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Run(Func<AnnotationsDb, Task> func)
|
||||||
|
{
|
||||||
|
await using var db = new AnnotationsDb(_dataOptions);
|
||||||
|
await func(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AnnotationsDbSchemaHolder
|
||||||
|
{
|
||||||
|
public static readonly MappingSchema MappingSchema;
|
||||||
|
|
||||||
|
static AnnotationsDbSchemaHolder()
|
||||||
|
{
|
||||||
|
MappingSchema = new MappingSchema();
|
||||||
|
var builder = new FluentMappingBuilder(MappingSchema);
|
||||||
|
|
||||||
|
builder.Entity<AnnotationName>().HasTableName("annotations_queue");
|
||||||
|
|
||||||
|
builder.Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Azaion.Common.Extensions;
|
||||||
|
|
||||||
|
public static class EnumerableExtensions
|
||||||
|
{
|
||||||
|
public static bool In<T>(this T obj, params T[] objects) =>
|
||||||
|
objects.Contains(obj);
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
namespace Azaion.Annotator.Extensions;
|
namespace Azaion.Common.Extensions;
|
||||||
|
|
||||||
public static class ThrottleExt
|
public static class ThrottleExt
|
||||||
{
|
{
|
||||||
private static bool _throttleOn;
|
private static bool _throttleOn;
|
||||||
public static async Task Throttle(Func<Task> func, TimeSpan? throttleTime = null)
|
public static async Task Throttle(this Func<Task> func, TimeSpan? throttleTime = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (_throttleOn)
|
if (_throttleOn)
|
||||||
return;
|
return;
|
||||||
@@ -12,8 +12,8 @@ public static class ThrottleExt
|
|||||||
await func();
|
await func();
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500));
|
await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500), cancellationToken);
|
||||||
_throttleOn = false;
|
_throttleOn = false;
|
||||||
});
|
}, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
using System.Drawing.Imaging;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using Azaion.Common.Database;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Azaion.Common.DTO.Queue;
|
||||||
|
using Azaion.CommonSecurity.DTO;
|
||||||
|
using Azaion.CommonSecurity.Services;
|
||||||
|
using LinqToDB;
|
||||||
|
using MessagePack;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using RabbitMQ.Stream.Client;
|
||||||
|
using RabbitMQ.Stream.Client.Reliable;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public class AnnotationService
|
||||||
|
{
|
||||||
|
private readonly AzaionApiClient _apiClient;
|
||||||
|
private readonly IDbFactory _dbFactory;
|
||||||
|
private readonly FailsafeAnnotationsProducer _producer;
|
||||||
|
private readonly QueueConfig _queueConfig;
|
||||||
|
private Consumer _consumer = null!;
|
||||||
|
|
||||||
|
public AnnotationService(AzaionApiClient apiClient,
|
||||||
|
IDbFactory dbFactory,
|
||||||
|
FailsafeAnnotationsProducer producer,
|
||||||
|
IOptions<QueueConfig> queueConfig)
|
||||||
|
{
|
||||||
|
_apiClient = apiClient;
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
_producer = producer;
|
||||||
|
_queueConfig = queueConfig.Value;
|
||||||
|
|
||||||
|
Task.Run(async () => await Init()).Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Init()
|
||||||
|
{
|
||||||
|
var consumerSystem = await StreamSystem.Create(new StreamSystemConfig
|
||||||
|
{
|
||||||
|
Endpoints = new List<EndPoint>{new DnsEndPoint(_queueConfig.Host, _queueConfig.Port)},
|
||||||
|
UserName = _queueConfig.ConsumerUsername,
|
||||||
|
Password = _queueConfig.ConsumerPassword
|
||||||
|
});
|
||||||
|
_consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE)
|
||||||
|
{
|
||||||
|
OffsetSpec = new OffsetTypeFirst(),
|
||||||
|
MessageHandler = async (stream, _, _, message) =>
|
||||||
|
await Consume(MessagePackSerializer.Deserialize<AnnotationCreatedMessage>(message.Data.Contents)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//AI / Manual
|
||||||
|
public async Task SaveAnnotation(string fName, List<YoloLabel>? labels, SourceEnum source, MemoryStream? stream = null, CancellationToken token = default) =>
|
||||||
|
await SaveAnnotationInner(DateTime.UtcNow, fName, labels, source, stream, _apiClient.User.Role, _apiClient.User.Email, token);
|
||||||
|
|
||||||
|
//Queue (only from operators)
|
||||||
|
public async Task Consume(AnnotationCreatedMessage message, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (message.CreatedRole == RoleEnum.Validator) //Don't proceed our own messages (or from another Validator)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await SaveAnnotationInner(
|
||||||
|
message.CreatedDate,
|
||||||
|
message.Name,
|
||||||
|
YoloLabel.Deserialize(message.Label),
|
||||||
|
message.Source,
|
||||||
|
new MemoryStream(message.Image),
|
||||||
|
message.CreatedRole,
|
||||||
|
message.CreatedEmail,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveAnnotationInner(DateTime createdDate, string fName, List<YoloLabel>? labels, SourceEnum source, MemoryStream? stream,
|
||||||
|
RoleEnum createdRole,
|
||||||
|
string createdEmail,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
//Flow for roles:
|
||||||
|
// Operator:
|
||||||
|
// sourceEnum: (manual, ai) <AnnotationCreatedMessage>
|
||||||
|
// Validator:
|
||||||
|
// sourceEnum: (manual) if was in received.json then <AnnotationValidatedMessage> else <AnnotationCreatedMessage>
|
||||||
|
// sourceEnum: (queue, AI) if queue CreatedMessage with the same user - do nothing Add to received.json
|
||||||
|
|
||||||
|
var classes = labels?.Select(x => x.ClassNumber).Distinct().ToList() ?? [];
|
||||||
|
AnnotationStatus status;
|
||||||
|
|
||||||
|
var annotation = await _dbFactory.Run(async db =>
|
||||||
|
{
|
||||||
|
var ann = await db.Annotations.FirstOrDefaultAsync(x => x.Name == fName, token: token);
|
||||||
|
status = ann?.AnnotationStatus == AnnotationStatus.Created && createdRole == RoleEnum.Validator
|
||||||
|
? AnnotationStatus.Validated
|
||||||
|
: AnnotationStatus.Created;
|
||||||
|
|
||||||
|
if (ann != null)
|
||||||
|
await db.Annotations
|
||||||
|
.Where(x => x.Name == fName)
|
||||||
|
.Set(x => x.Classes, classes)
|
||||||
|
.Set(x => x.Source, source)
|
||||||
|
.Set(x => x.AnnotationStatus, status)
|
||||||
|
.UpdateAsync(token: token);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ann = new Annotation
|
||||||
|
{
|
||||||
|
CreatedDate = createdDate,
|
||||||
|
Name = fName,
|
||||||
|
Classes = classes,
|
||||||
|
CreatedEmail = createdEmail,
|
||||||
|
CreatedRole = createdRole,
|
||||||
|
AnnotationStatus = status,
|
||||||
|
Source = source
|
||||||
|
};
|
||||||
|
await db.InsertAsync(ann, token: token);
|
||||||
|
}
|
||||||
|
return ann;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stream != null)
|
||||||
|
{
|
||||||
|
var img = System.Drawing.Image.FromStream(stream);
|
||||||
|
img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue
|
||||||
|
}
|
||||||
|
if (labels != null)
|
||||||
|
await YoloLabel.WriteToFile(labels, annotation.LabelPath, token);
|
||||||
|
|
||||||
|
await _producer.SendToQueue(annotation, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using Azaion.Common.Database;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Azaion.Common.DTO.Queue;
|
||||||
|
using LinqToDB;
|
||||||
|
using MessagePack;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using RabbitMQ.Stream.Client;
|
||||||
|
using RabbitMQ.Stream.Client.Reliable;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public class FailsafeAnnotationsProducer
|
||||||
|
{
|
||||||
|
private readonly ILogger<FailsafeAnnotationsProducer> _logger;
|
||||||
|
private readonly IDbFactory _dbFactory;
|
||||||
|
private readonly QueueConfig _queueConfig;
|
||||||
|
|
||||||
|
private Producer _annotationProducer = null!;
|
||||||
|
private Producer _annotationConfirmProducer = null!;
|
||||||
|
|
||||||
|
|
||||||
|
public FailsafeAnnotationsProducer(ILogger<FailsafeAnnotationsProducer> logger, IDbFactory dbFactory, IOptions<QueueConfig> queueConfig)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
_queueConfig = queueConfig.Value;
|
||||||
|
Task.Run(async () => await ProcessQueue()).Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<StreamSystem> GetProducerQueueConfig()
|
||||||
|
{
|
||||||
|
return await StreamSystem.Create(new StreamSystemConfig
|
||||||
|
{
|
||||||
|
|
||||||
|
Endpoints = new List<EndPoint> { new IPEndPoint(IPAddress.Parse(_queueConfig.Host), _queueConfig.Port) },
|
||||||
|
UserName = _queueConfig.ProducerUsername,
|
||||||
|
Password = _queueConfig.ProducerPassword
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Init(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_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));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessQueue(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await Init(cancellationToken);
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var messages = await GetFromQueue(cancellationToken);
|
||||||
|
foreach (var messagesChunk in messages.Chunk(10)) //Sending by 10
|
||||||
|
{
|
||||||
|
var sent = false;
|
||||||
|
while (!sent || cancellationToken.IsCancellationRequested) //Waiting for send
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var createdMessages = messagesChunk
|
||||||
|
.Where(x => x.Status == AnnotationStatus.Created)
|
||||||
|
.Select(x => new Message(MessagePackSerializer.Serialize(x)))
|
||||||
|
.ToList();
|
||||||
|
await _annotationProducer.Send(createdMessages, CompressionType.Gzip);
|
||||||
|
|
||||||
|
var validatedMessages = messagesChunk
|
||||||
|
.Where(x => x.Status == AnnotationStatus.Validated)
|
||||||
|
.Select(x => new Message(MessagePackSerializer.Serialize(x)))
|
||||||
|
.ToList();
|
||||||
|
await _annotationConfirmProducer.Send(validatedMessages, CompressionType.Gzip);
|
||||||
|
|
||||||
|
await _dbFactory.Run(async db =>
|
||||||
|
await db.AnnotationsQueue.DeleteAsync(aq => messagesChunk.Any(x => aq.Name == x.Name), token: cancellationToken));
|
||||||
|
sent = true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, e.Message);
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<AnnotationCreatedMessage>> GetFromQueue(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _dbFactory.Run(async db =>
|
||||||
|
{
|
||||||
|
var annotations = await db.AnnotationsQueue.Join(db.Annotations, aq => aq.Name, a => a.Name, (aq, a) => a)
|
||||||
|
.ToListAsync(token: cancellationToken);
|
||||||
|
|
||||||
|
var messages = new List<AnnotationCreatedMessage>();
|
||||||
|
foreach (var annotation in annotations)
|
||||||
|
{
|
||||||
|
var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken);
|
||||||
|
var label = await File.ReadAllTextAsync(annotation.LabelPath, cancellationToken);
|
||||||
|
var annCreateMessage = new AnnotationCreatedMessage
|
||||||
|
{
|
||||||
|
Name = annotation.Name,
|
||||||
|
|
||||||
|
CreatedRole = annotation.CreatedRole,
|
||||||
|
CreatedEmail = annotation.CreatedEmail,
|
||||||
|
CreatedDate = annotation.CreatedDate,
|
||||||
|
|
||||||
|
Image = image,
|
||||||
|
Label = label,
|
||||||
|
Source = annotation.Source
|
||||||
|
};
|
||||||
|
messages.Add(annCreateMessage);
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendToQueue(Annotation annotation, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _dbFactory.Run(async db =>
|
||||||
|
await db.InsertAsync(new AnnotationName { Name = annotation.Name }, token: cancellationToken));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.Common.DTO.Config;
|
namespace Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
public class ApiConfig
|
public class ApiConfig
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.Common.DTO;
|
namespace Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
public class ApiCredentials(string email, string password) : EventArgs
|
public class ApiCredentials(string email, string password) : EventArgs
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.Common.DTO;
|
namespace Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
public class HardwareInfo
|
public class HardwareInfo
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.Common.DTO;
|
namespace Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
public class LoginResponse
|
public class LoginResponse
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.Common.DTO;
|
namespace Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
public enum RoleEnum
|
public enum RoleEnum
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
|
public class SecureAppConfig
|
||||||
|
{
|
||||||
|
public ApiConfig ApiConfig { get; set; } = null!;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace Azaion.Common.DTO;
|
namespace Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
public class User
|
public class User
|
||||||
{
|
{
|
||||||
@@ -12,9 +12,9 @@ public class User
|
|||||||
{
|
{
|
||||||
var claimDict = claims.ToDictionary(x => x.Type, x => x.Value);
|
var claimDict = claims.ToDictionary(x => x.Type, x => x.Value);
|
||||||
|
|
||||||
Id = Guid.Parse(claimDict[Constants.CLAIM_NAME_ID]);
|
Id = Guid.Parse(claimDict[SecurityConstants.CLAIM_NAME_ID]);
|
||||||
Email = claimDict[Constants.CLAIM_EMAIL];
|
Email = claimDict[SecurityConstants.CLAIM_EMAIL];
|
||||||
if (!Enum.TryParse(claimDict[Constants.CLAIM_ROLE], out RoleEnum role))
|
if (!Enum.TryParse(claimDict[SecurityConstants.CLAIM_ROLE], out RoleEnum role))
|
||||||
role = RoleEnum.None;
|
role = RoleEnum.None;
|
||||||
Role = role;
|
Role = role;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Azaion.CommonSecurity;
|
||||||
|
|
||||||
|
public class SecurityConstants
|
||||||
|
{
|
||||||
|
public const string CONFIG_PATH = "config.json";
|
||||||
|
|
||||||
|
#region ApiConfig
|
||||||
|
|
||||||
|
public const string DEFAULT_API_URL = "https://api.azaion.com/";
|
||||||
|
public const int DEFAULT_API_RETRY_COUNT = 3;
|
||||||
|
public const int DEFAULT_API_TIMEOUT_SECONDS = 40;
|
||||||
|
|
||||||
|
public const string CLAIM_NAME_ID = "nameid";
|
||||||
|
public const string CLAIM_EMAIL = "unique_name";
|
||||||
|
public const string CLAIM_ROLE = "role";
|
||||||
|
|
||||||
|
#endregion ApiConfig
|
||||||
|
}
|
||||||
+34
-5
@@ -1,14 +1,12 @@
|
|||||||
using System.IO;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Security;
|
using System.Security;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.CommonSecurity.DTO;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
|
||||||
|
|
||||||
namespace Azaion.Common.Services;
|
namespace Azaion.CommonSecurity.Services;
|
||||||
|
|
||||||
public class AzaionApiClient(HttpClient httpClient) : IDisposable
|
public class AzaionApiClient(HttpClient httpClient) : IDisposable
|
||||||
{
|
{
|
||||||
@@ -20,6 +18,37 @@ public class AzaionApiClient(HttpClient httpClient) : IDisposable
|
|||||||
private string JwtToken { get; set; } = null!;
|
private string JwtToken { get; set; } = null!;
|
||||||
public User User { get; set; } = null!;
|
public User User { get; set; } = null!;
|
||||||
|
|
||||||
|
public static AzaionApiClient Create(ApiCredentials credentials)
|
||||||
|
{
|
||||||
|
ApiConfig apiConfig;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(SecurityConstants.CONFIG_PATH))
|
||||||
|
throw new FileNotFoundException(SecurityConstants.CONFIG_PATH);
|
||||||
|
var configStr = File.ReadAllText(SecurityConstants.CONFIG_PATH);
|
||||||
|
apiConfig = JsonConvert.DeserializeObject<SecureAppConfig>(configStr)!.ApiConfig;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
apiConfig = new ApiConfig
|
||||||
|
{
|
||||||
|
Url = SecurityConstants.DEFAULT_API_URL,
|
||||||
|
RetryCount = SecurityConstants.DEFAULT_API_RETRY_COUNT ,
|
||||||
|
TimeoutSeconds = SecurityConstants.DEFAULT_API_TIMEOUT_SECONDS
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var api = new AzaionApiClient(new HttpClient
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri(apiConfig.Url),
|
||||||
|
Timeout = TimeSpan.FromSeconds(apiConfig.TimeoutSeconds)
|
||||||
|
});
|
||||||
|
|
||||||
|
api.EnterCredentials(credentials);
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
public void EnterCredentials(ApiCredentials credentials)
|
public void EnterCredentials(ApiCredentials credentials)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(credentials.Email) || string.IsNullOrWhiteSpace(credentials.Password))
|
if (string.IsNullOrWhiteSpace(credentials.Email) || string.IsNullOrWhiteSpace(credentials.Password))
|
||||||
+2
-2
@@ -2,9 +2,9 @@
|
|||||||
using System.Net.NetworkInformation;
|
using System.Net.NetworkInformation;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
namespace Azaion.Common.Services;
|
namespace Azaion.CommonSecurity.Services;
|
||||||
|
|
||||||
public interface IHardwareService
|
public interface IHardwareService
|
||||||
{
|
{
|
||||||
+4
-4
@@ -1,8 +1,7 @@
|
|||||||
using System.IO;
|
using System.Reflection;
|
||||||
using System.Reflection;
|
using Azaion.CommonSecurity.DTO;
|
||||||
using Azaion.Common.DTO;
|
|
||||||
|
|
||||||
namespace Azaion.Common.Services;
|
namespace Azaion.CommonSecurity.Services;
|
||||||
|
|
||||||
public interface IResourceLoader
|
public interface IResourceLoader
|
||||||
{
|
{
|
||||||
@@ -53,6 +52,7 @@ public class ResourceLoader(AzaionApiClient api, ApiCredentials credentials) : I
|
|||||||
var key = Security.MakeEncryptionKey(credentials.Email, credentials.Password, hardwareInfo.Hash);
|
var key = Security.MakeEncryptionKey(credentials.Email, credentials.Password, hardwareInfo.Hash);
|
||||||
var stream = new MemoryStream();
|
var stream = new MemoryStream();
|
||||||
await encryptedStream.DecryptTo(stream, key, cancellationToken);
|
await encryptedStream.DecryptTo(stream, key, cancellationToken);
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
using System.IO;
|
using System.Runtime.InteropServices;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Security;
|
using System.Security;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Azaion.Common.Services;
|
namespace Azaion.CommonSecurity.Services;
|
||||||
|
|
||||||
public static class Security
|
public static class Security
|
||||||
{
|
{
|
||||||
@@ -75,5 +75,6 @@ public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, IGalle
|
|||||||
var (thumbnailDto, detections) = await galleryManager.CreateThumbnail(imageCreatedEvent.ImagePath, cancellationToken);
|
var (thumbnailDto, detections) = await galleryManager.CreateThumbnail(imageCreatedEvent.ImagePath, cancellationToken);
|
||||||
if (thumbnailDto != null && detections != null)
|
if (thumbnailDto != null && detections != null)
|
||||||
datasetExplorer.AddThumbnail(thumbnailDto, detections);
|
datasetExplorer.AddThumbnail(thumbnailDto, detections);
|
||||||
|
await galleryManager.SaveLabelsCache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Annotator", "Dummy\A
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Dataset", "Dummy\Azaion.Dataset\Azaion.Dataset.csproj", "{A2E3D3AE-5DB7-4342-BE20-88A9D1B0C05E}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Dataset", "Dummy\Azaion.Dataset\Azaion.Dataset.csproj", "{A2E3D3AE-5DB7-4342-BE20-88A9D1B0C05E}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.CommonSecurity", "Azaion.CommonSecurity\Azaion.CommonSecurity.csproj", "{E0C7176D-2E91-4928-B3C1-55CC91C8F77D}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -54,6 +56,10 @@ Global
|
|||||||
{A2E3D3AE-5DB7-4342-BE20-88A9D1B0C05E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A2E3D3AE-5DB7-4342-BE20-88A9D1B0C05E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{A2E3D3AE-5DB7-4342-BE20-88A9D1B0C05E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{A2E3D3AE-5DB7-4342-BE20-88A9D1B0C05E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A2E3D3AE-5DB7-4342-BE20-88A9D1B0C05E}.Release|Any CPU.Build.0 = Release|Any CPU
|
{A2E3D3AE-5DB7-4342-BE20-88A9D1B0C05E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E0C7176D-2E91-4928-B3C1-55CC91C8F77D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{E0C7176D-2E91-4928-B3C1-55CC91C8F77D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E0C7176D-2E91-4928-B3C1-55CC91C8F77D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E0C7176D-2E91-4928-B3C1-55CC91C8F77D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{32C4747F-F700-44FD-B4ED-21B4A66B5FAB} = {C307BE2E-FFCC-4BD7-AD89-C82D40B65D03}
|
{32C4747F-F700-44FD-B4ED-21B4A66B5FAB} = {C307BE2E-FFCC-4BD7-AD89-C82D40B65D03}
|
||||||
|
|||||||
+18
-38
@@ -1,14 +1,16 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
using Azaion.Annotator;
|
using Azaion.Annotator;
|
||||||
using Azaion.Annotator.Extensions;
|
using Azaion.Annotator.Extensions;
|
||||||
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.Extensions;
|
using Azaion.Common.Extensions;
|
||||||
using Azaion.Common.Services;
|
using Azaion.Common.Services;
|
||||||
|
using Azaion.CommonSecurity;
|
||||||
|
using Azaion.CommonSecurity.DTO;
|
||||||
|
using Azaion.CommonSecurity.Services;
|
||||||
using Azaion.Dataset;
|
using Azaion.Dataset;
|
||||||
using LibVLCSharp.Shared;
|
using LibVLCSharp.Shared;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -17,7 +19,6 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using KeyEventArgs = System.Windows.Input.KeyEventArgs;
|
using KeyEventArgs = System.Windows.Input.KeyEventArgs;
|
||||||
|
|
||||||
@@ -32,38 +33,7 @@ public partial class App
|
|||||||
|
|
||||||
private AzaionApiClient _apiClient = null!;
|
private AzaionApiClient _apiClient = null!;
|
||||||
private IResourceLoader _resourceLoader = null!;
|
private IResourceLoader _resourceLoader = null!;
|
||||||
|
private Stream _securedConfig = null!;
|
||||||
private static AzaionApiClient CreateApiClient(ApiCredentials credentials)
|
|
||||||
{
|
|
||||||
ApiConfig apiConfig;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!File.Exists(Constants.CONFIG_PATH))
|
|
||||||
throw new FileNotFoundException(Constants.CONFIG_PATH);
|
|
||||||
var configStr = File.ReadAllText(Constants.CONFIG_PATH);
|
|
||||||
apiConfig = JsonConvert.DeserializeObject<AppConfig>(configStr)!.ApiConfig;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine(e);
|
|
||||||
apiConfig = new ApiConfig
|
|
||||||
{
|
|
||||||
Url = "https://api.azaion.com",
|
|
||||||
RetryCount = 3,
|
|
||||||
TimeoutSeconds = 40
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var api = new AzaionApiClient(new HttpClient
|
|
||||||
{
|
|
||||||
BaseAddress = new Uri(apiConfig.Url),
|
|
||||||
Timeout = TimeSpan.FromSeconds(apiConfig.TimeoutSeconds)
|
|
||||||
});
|
|
||||||
|
|
||||||
api.EnterCredentials(credentials);
|
|
||||||
return api;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -83,8 +53,9 @@ public partial class App
|
|||||||
var login = new Login();
|
var login = new Login();
|
||||||
login.CredentialsEntered += async (s, args) =>
|
login.CredentialsEntered += async (s, args) =>
|
||||||
{
|
{
|
||||||
_apiClient = CreateApiClient(args);
|
_apiClient = AzaionApiClient.Create(args);
|
||||||
_resourceLoader = new ResourceLoader(_apiClient, args);
|
_resourceLoader = new ResourceLoader(_apiClient, args);
|
||||||
|
_securedConfig = await _resourceLoader.Load("secured-config.json");
|
||||||
AppDomain.CurrentDomain.AssemblyResolve += (_, a) => _resourceLoader.LoadAssembly(a.Name);
|
AppDomain.CurrentDomain.AssemblyResolve += (_, a) => _resourceLoader.LoadAssembly(a.Name);
|
||||||
|
|
||||||
StartMain();
|
StartMain();
|
||||||
@@ -110,7 +81,8 @@ public partial class App
|
|||||||
_host = Host.CreateDefaultBuilder()
|
_host = Host.CreateDefaultBuilder()
|
||||||
.ConfigureAppConfiguration((context, config) => config
|
.ConfigureAppConfiguration((context, config) => config
|
||||||
.AddCommandLine(Environment.GetCommandLineArgs())
|
.AddCommandLine(Environment.GetCommandLineArgs())
|
||||||
.AddJsonFile(Constants.CONFIG_PATH, optional: true, reloadOnChange: true))
|
.AddJsonFile(SecurityConstants.CONFIG_PATH, optional: true, reloadOnChange: true)
|
||||||
|
.AddJsonStream(_securedConfig))
|
||||||
.ConfigureServices((context, services) =>
|
.ConfigureServices((context, services) =>
|
||||||
{
|
{
|
||||||
services.AddSingleton<MainSuite>();
|
services.AddSingleton<MainSuite>();
|
||||||
@@ -121,9 +93,9 @@ public partial class App
|
|||||||
|
|
||||||
services.Configure<AppConfig>(context.Configuration);
|
services.Configure<AppConfig>(context.Configuration);
|
||||||
services.ConfigureSection<ApiConfig>(context.Configuration);
|
services.ConfigureSection<ApiConfig>(context.Configuration);
|
||||||
|
services.ConfigureSection<QueueConfig>(context.Configuration);
|
||||||
services.ConfigureSection<DirectoriesConfig>(context.Configuration);
|
services.ConfigureSection<DirectoriesConfig>(context.Configuration);
|
||||||
services.ConfigureSection<AnnotationConfig>(context.Configuration);
|
services.ConfigureSection<AnnotationConfig>(context.Configuration);
|
||||||
services.ConfigureSection<AnnotatorWindowConfig>(context.Configuration);
|
|
||||||
services.ConfigureSection<AIRecognitionConfig>(context.Configuration);
|
services.ConfigureSection<AIRecognitionConfig>(context.Configuration);
|
||||||
services.ConfigureSection<ThumbnailConfig>(context.Configuration);
|
services.ConfigureSection<ThumbnailConfig>(context.Configuration);
|
||||||
|
|
||||||
@@ -144,6 +116,7 @@ public partial class App
|
|||||||
});
|
});
|
||||||
services.AddSingleton<AnnotatorEventHandler>();
|
services.AddSingleton<AnnotatorEventHandler>();
|
||||||
services.AddSingleton<VLCFrameExtractor>();
|
services.AddSingleton<VLCFrameExtractor>();
|
||||||
|
services.AddSingleton<IDbFactory, DbFactory>();
|
||||||
|
|
||||||
services.AddHttpClient<AzaionApiClient>((sp, client) =>
|
services.AddHttpClient<AzaionApiClient>((sp, client) =>
|
||||||
{
|
{
|
||||||
@@ -152,6 +125,10 @@ public partial class App
|
|||||||
client.Timeout = TimeSpan.FromSeconds(apiConfig.TimeoutSeconds);
|
client.Timeout = TimeSpan.FromSeconds(apiConfig.TimeoutSeconds);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddSingleton<FailsafeAnnotationsProducer>();
|
||||||
|
|
||||||
|
services.AddSingleton<AnnotationService>();
|
||||||
|
|
||||||
services.AddSingleton<DatasetExplorer>();
|
services.AddSingleton<DatasetExplorer>();
|
||||||
services.AddSingleton<IGalleryManager, GalleryManager>();
|
services.AddSingleton<IGalleryManager, GalleryManager>();
|
||||||
|
|
||||||
@@ -159,6 +136,9 @@ public partial class App
|
|||||||
services.AddSingleton<IAzaionModule, DatasetExplorerModule>();
|
services.AddSingleton<IAzaionModule, DatasetExplorerModule>();
|
||||||
})
|
})
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
Annotation.InitializeDirs(_host.Services.GetRequiredService<IOptions<DirectoriesConfig>>().Value);
|
||||||
|
|
||||||
_mediator = _host.Services.GetRequiredService<IMediator>();
|
_mediator = _host.Services.GetRequiredService<IMediator>();
|
||||||
_logger = _host.Services.GetRequiredService<ILogger<App>>();
|
_logger = _host.Services.GetRequiredService<ILogger<App>>();
|
||||||
_formState = _host.Services.GetRequiredService<FormState>();
|
_formState = _host.Services.GetRequiredService<FormState>();
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="RabbitMQ.Stream.Client" Version="1.8.9" />
|
||||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||||
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
|
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
@@ -37,6 +38,9 @@
|
|||||||
<Content Include="logo.png">
|
<Content Include="logo.png">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
<None Update="config.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
BorderBrush="DimGray"
|
BorderBrush="DimGray"
|
||||||
BorderThickness="0,0,0,1"
|
BorderThickness="0,0,0,1"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
|
Text="admin@azaion.com"
|
||||||
/>
|
/>
|
||||||
<TextBlock Text="Пароль"
|
<TextBlock Text="Пароль"
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
@@ -87,7 +88,8 @@
|
|||||||
Padding="0,5"
|
Padding="0,5"
|
||||||
Width="300"
|
Width="300"
|
||||||
BorderThickness="0,0,0,1"
|
BorderThickness="0,0,0,1"
|
||||||
HorizontalAlignment="Left"/>
|
HorizontalAlignment="Left"
|
||||||
|
Password="Az@1on1000Odm$n"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Button x:Name="LoginBtn"
|
<Button x:Name="LoginBtn"
|
||||||
Content="Вхід"
|
Content="Вхід"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
namespace Azaion.Suite;
|
namespace Azaion.Suite;
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
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"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="Azaion Оператор" Height="60" Width="214"
|
Title="Azaion Оператор" Height="50" Width="214"
|
||||||
WindowStyle="None"
|
WindowStyle="None"
|
||||||
ResizeMode="NoResize"
|
ResizeMode="NoResize"
|
||||||
Top="0"
|
Top="0"
|
||||||
Topmost="True">
|
Topmost="True"
|
||||||
|
MouseMove="OnMouseMove"
|
||||||
|
MouseDown="MainSuite_OnMouseDown">
|
||||||
<Grid Background="Black">
|
<Grid Background="Black">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="48"></ColumnDefinition>
|
<ColumnDefinition Width="48"></ColumnDefinition>
|
||||||
@@ -31,8 +33,8 @@
|
|||||||
<Border Grid.Column="2">
|
<Border Grid.Column="2">
|
||||||
<Button
|
<Button
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Width="24"
|
Width="18"
|
||||||
Height="24"
|
Height="18"
|
||||||
ToolTip="Закрити" Background="Black" BorderBrush="Black" Cursor="Hand"
|
ToolTip="Закрити" Background="Black" BorderBrush="Black" Cursor="Hand"
|
||||||
Name="CloseBtn"
|
Name="CloseBtn"
|
||||||
Click="CloseBtn_OnClick">
|
Click="CloseBtn_OnClick">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Windows.Media;
|
|||||||
using Azaion.Annotator.Extensions;
|
using Azaion.Annotator.Extensions;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.DTO.Config;
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Azaion.Common.Extensions;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using SharpVectors.Converters;
|
using SharpVectors.Converters;
|
||||||
@@ -33,7 +34,7 @@ public partial class MainSuite
|
|||||||
SizeChanged += async (_, _) => await SaveUserSettings();
|
SizeChanged += async (_, _) => await SaveUserSettings();
|
||||||
LocationChanged += async (_, _) => await SaveUserSettings();
|
LocationChanged += async (_, _) => await SaveUserSettings();
|
||||||
StateChanged += async (_, _) => await SaveUserSettings();
|
StateChanged += async (_, _) => await SaveUserSettings();
|
||||||
Left = SystemParameters.WorkArea.Width - Width - 250;
|
Left = (SystemParameters.WorkArea.Width - Width) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
@@ -54,8 +55,8 @@ public partial class MainSuite
|
|||||||
new SvgViewbox
|
new SvgViewbox
|
||||||
{
|
{
|
||||||
SvgSource = azaionModule.SvgIcon,
|
SvgSource = azaionModule.SvgIcon,
|
||||||
Width = 32,
|
Width = 24,
|
||||||
Height = 32,
|
Height = 24,
|
||||||
Margin = new Thickness(0, 0, 10, 0)
|
Margin = new Thickness(0, 0, 10, 0)
|
||||||
},
|
},
|
||||||
new TextBlock
|
new TextBlock
|
||||||
@@ -120,4 +121,20 @@ public partial class MainSuite
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void CloseBtn_OnClick(object sender, RoutedEventArgs e) => Close();
|
private void CloseBtn_OnClick(object sender, RoutedEventArgs e) => Close();
|
||||||
|
|
||||||
|
private double _startMouseMovePosition;
|
||||||
|
|
||||||
|
private void OnMouseMove(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.OriginalSource is Button || e.OriginalSource is StackPanel)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (e.LeftButton == MouseButtonState.Pressed)
|
||||||
|
Left = System.Windows.Forms.Cursor.Position.X - _startMouseMovePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MainSuite_OnMouseDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
_startMouseMovePosition = e.GetPosition(this).X;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,18 @@
|
|||||||
{
|
{
|
||||||
"ApiConfig": {
|
"ApiConfig": {
|
||||||
"Url": "https://api.azaion.com",
|
"Url": "https://api.azaion.com/",
|
||||||
"TimeoutSeconds": 20,
|
"RetryCount": 3,
|
||||||
"RetryCount": 3
|
"TimeoutSeconds": 40.0,
|
||||||
|
"TokenFile": "token.txt"
|
||||||
},
|
},
|
||||||
|
|
||||||
"DirectoriesConfig": {
|
"DirectoriesConfig": {
|
||||||
"VideosDirectory" : "E:\\Azaion1\\Videos",
|
"VideosDirectory": "E:\\Azaion6",
|
||||||
"LabelsDirectory" : "E:\\labels",
|
"LabelsDirectory": "E:\\labels",
|
||||||
"ImagesDirectory" : "E:\\images",
|
"ImagesDirectory": "E:\\images",
|
||||||
"ResultsDirectory" : "E:\\results",
|
"ResultsDirectory": "E:\\results",
|
||||||
"ThumbnailsDirectory" : "E:\\thumbnails",
|
"ThumbnailsDirectory": "E:\\thumbnails"
|
||||||
|
|
||||||
"DllCacheDirectory" : "Cache"
|
|
||||||
},
|
},
|
||||||
|
"AnnotationConfig": {
|
||||||
"AnnotationConfig" : {
|
|
||||||
"AnnotationClasses": [
|
"AnnotationClasses": [
|
||||||
{ "Id": 0, "Name": "Броньована техніка", "ShortName": "Бронь" },
|
{ "Id": 0, "Name": "Броньована техніка", "ShortName": "Бронь" },
|
||||||
{ "Id": 1, "Name": "Вантажівка", "ShortName": "Вантаж" },
|
{ "Id": 1, "Name": "Вантажівка", "ShortName": "Вантаж" },
|
||||||
@@ -27,32 +24,22 @@
|
|||||||
{ "Id": 7, "Name": "Накати", "ShortName": "Накати" },
|
{ "Id": 7, "Name": "Накати", "ShortName": "Накати" },
|
||||||
{ "Id": 8, "Name": "Танк з захистом", "ShortName": "Танк захист" },
|
{ "Id": 8, "Name": "Танк з захистом", "ShortName": "Танк захист" },
|
||||||
{ "Id": 9, "Name": "Дим", "ShortName": "Дим" },
|
{ "Id": 9, "Name": "Дим", "ShortName": "Дим" },
|
||||||
{ "Id": 10, "Name": "Літак", "ShortName": "Літак" }
|
{ "Id": 10, "Name": "Літак", "ShortName": "Літак" },
|
||||||
|
{ "Id": 11, "Name": "Мотоцикл", "ShortName": "Мото" }
|
||||||
],
|
],
|
||||||
"LastSelectedExplorerClass": 1,
|
"LastSelectedExplorerClass": null,
|
||||||
"VideoFormats": ["mov", "mp4"],
|
"VideoFormats": [ "mp4", "mov", "avi" ],
|
||||||
"ImageFormats": ["jpg", "jpeg", "png", "bmp", "gif"]
|
"ImageFormats": [ "jpg", "jpeg", "png", "bmp" ],
|
||||||
|
"AnnotationsDbFile": "annotations.db",
|
||||||
|
"LeftPanelWidth": 220.0,
|
||||||
|
"RightPanelWidth": 230.0
|
||||||
},
|
},
|
||||||
|
|
||||||
"WindowConfig": {
|
|
||||||
"WindowSize": "1920,1080",
|
|
||||||
"WindowLocation": "50,50",
|
|
||||||
"FullScreen": true,
|
|
||||||
"LeftPanelWidth": 220,
|
|
||||||
"RightPanelWidth": 220,
|
|
||||||
"ShowHelpOnStart": false
|
|
||||||
},
|
|
||||||
|
|
||||||
"AIRecognitionConfig": {
|
"AIRecognitionConfig": {
|
||||||
"FrameRecognitionSeconds" : 2,
|
"FrameRecognitionSeconds": 2.0,
|
||||||
"TrackingDistanceConfidence" : 0.15,
|
"TrackingDistanceConfidence": 0.15,
|
||||||
"TrackingProbabilityIncrease" : 15,
|
"TrackingProbabilityIncrease": 15.0,
|
||||||
"TrackingIntersectionThreshold" : 0.8,
|
"TrackingIntersectionThreshold": 0.8,
|
||||||
"FramePeriodRecognition": 4
|
"FramePeriodRecognition": 4
|
||||||
},
|
},
|
||||||
|
"ThumbnailConfig": { "Size": "240,135", "Border": 10 }
|
||||||
"ThumbnailConfig": {
|
|
||||||
"Size" : "240,135",
|
|
||||||
"Border" : 10
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using Azaion.Annotator.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.DTO;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Azaion.Annotator.Test;
|
namespace Azaion.Annotator.Test;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Azaion.Common.Services;
|
using Azaion.Common.Services;
|
||||||
|
using Azaion.CommonSecurity.Services;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Azaion.Annotator.Test;
|
namespace Azaion.Annotator.Test;
|
||||||
|
|||||||
Reference in New Issue
Block a user