queue + local sqlite WIP

This commit is contained in:
Alex Bezdieniezhnykh
2024-12-17 18:46:33 +02:00
parent 626767469a
commit 5fa18aa514
47 changed files with 694 additions and 222 deletions
+1
View File
@@ -5,3 +5,4 @@ obj
*.DotSettings* *.DotSettings*
*.user *.user
log*.txt log*.txt
secured-config
+5 -22
View File
@@ -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();
+25 -28
View File
@@ -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);
} }
} }
-3
View File
@@ -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>
+1 -8
View File
@@ -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;
} }
+1
View File
@@ -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;
+8 -1
View File
@@ -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
View File
@@ -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
} }
+42
View File
@@ -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; }
}
+12 -11
View File
@@ -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);
} }
} }
+14
View File
@@ -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!;
}
+12 -6
View File
@@ -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!;
}
+7
View File
@@ -0,0 +1,7 @@
namespace Azaion.Common.DTO.Queue;
public enum SourceEnum
{
AI,
Manual
}
+11
View File
@@ -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>();
}
+65
View File
@@ -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);
} }
} }
+132
View File
@@ -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);
}
}
+125
View File
@@ -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
}
@@ -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,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
{ {
@@ -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();
} }
} }
+6
View File
@@ -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
View File
@@ -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>();
+4
View File
@@ -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>
+3 -1
View File
@@ -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="Вхід"
+1
View File
@@ -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;
+6 -4
View File
@@ -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">
+20 -3
View File
@@ -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 -2
View File
@@ -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
View File
@@ -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;